diff --git a/src/api.ts b/src/api.ts index 1d82ede..cbd3b0c 100644 --- a/src/api.ts +++ b/src/api.ts @@ -2,15 +2,6 @@ import axios from "axios" import { languageToId } from "./templates" import { Code, Submission } from "./types" -// function getChromeVersion() { -// var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./) -// return raw ? parseInt(raw[2], 10) : 0 -// } - -// const isLowVersion = getChromeVersion() < 80 - -// const protocol = isLowVersion ? "http" : "https" - function encode(string?: string) { return btoa(String.fromCharCode(...new TextEncoder().encode(string ?? ""))) } @@ -27,7 +18,71 @@ function decode(bytes?: string) { const judge = axios.create({ baseURL: import.meta.env.PUBLIC_JUDGE0API_URL }) const api = axios.create({ baseURL: import.meta.env.PUBLIC_CODEAPI_URL }) +type PythonWorkerRequest = { + id: number + source: string + stdin: string + timeoutMs: number +} + +type PythonWorkerResponse = { + id: number + status: number + output: string +} + +let pythonWorker: Worker | null = null +let pythonWorkerSeq = 0 +const pythonPending = new Map< + number, + { resolve: (v: { status: number; output: string }) => void; timeout: number } +>() + +function getPythonWorker() { + if (pythonWorker) return pythonWorker + pythonWorker = new Worker( + new URL("./workers/pythonSkulpt.worker.ts", import.meta.url), + { type: "module" }, + ) + pythonWorker.onmessage = (event: MessageEvent) => { + const { id, status, output } = event.data ?? ({} as any) + const pending = pythonPending.get(id) + if (!pending) return + clearTimeout(pending.timeout) + pythonPending.delete(id) + pending.resolve({ status, output }) + } + return pythonWorker +} + +function restartPythonWorker() { + if (pythonWorker) pythonWorker.terminate() + pythonWorker = null + pythonPending.clear() +} + +async function runPythonInWorker(source: string, stdin: string) { + const worker = getPythonWorker() + const id = ++pythonWorkerSeq + const timeoutMs = 5000 + + return new Promise<{ status: number; output: string }>((resolve) => { + const timeout = window.setTimeout(() => { + restartPythonWorker() + resolve({ status: 11, output: "运行超时" }) + }, timeoutMs + 250) + + pythonPending.set(id, { resolve, timeout }) + const message: PythonWorkerRequest = { id, source, stdin, timeoutMs } + worker.postMessage(message) + }) +} + export async function submit(code: Code, input: string) { + if (code.language === "python") { + return runPythonInWorker(code.value, input) + } + const encodedCode = encode(code.value) const id = languageToId[code.language] diff --git a/src/desktop/TurtleSection.vue b/src/desktop/TurtleSection.vue index f60ecdf..b1ccc6d 100644 --- a/src/desktop/TurtleSection.vue +++ b/src/desktop/TurtleSection.vue @@ -19,6 +19,10 @@ function runSkulptTurtle() { const canvas = turtleCanvas.value if (!canvas) return canvas.innerHTML = "" + // Prevent UI from being stuck forever on infinite loops (still runs on main thread). + // Skulpt checks this limit periodically and throws a timeout error. + ;(Sk as any).execLimit = 5000 + ;(Sk as any).execStart = new Date() Sk.configure({ output: console.log, read: builtinRead, diff --git a/src/workers/pythonSkulpt.worker.ts b/src/workers/pythonSkulpt.worker.ts new file mode 100644 index 0000000..3cc0b58 --- /dev/null +++ b/src/workers/pythonSkulpt.worker.ts @@ -0,0 +1,122 @@ +/// + +// @ts-ignore +import * as Sk from "skulpt" + +type RunRequest = { + id: number + source: string + stdin: string + timeoutMs: number +} + +type RunResponse = { + id: number + status: number + output: string +} + +const exceptionNameToCn: Record = { + SyntaxError: "格式错误", + IndentationError: "格式错误", + TabError: "格式错误", + NameError: "变量命名错误", + TypeError: "类型错误", + ValueError: "值错误", + IndexError: "索引错误", + KeyError: "键错误", + ZeroDivisionError: "除零错误", + AttributeError: "属性错误", + ImportError: "导入错误", + ModuleNotFoundError: "模块未找到", + RuntimeError: "运行错误", +} + +function translateSkulptError( + name: string, + message: string, + isCompileError: boolean, +) { + if (isCompileError) return "代码格式错误" + if (/exceeded run time limit/i.test(message)) return "运行超时" + + const cnName = exceptionNameToCn[name] ?? "" + const translatedMessage = String(message ?? "") + .replace(/No module named ([^\s]+)/gi, "没有名为 $1 的模块") + .replace(/integer division or modulo by zero/gi, "不能除以零") + .replace(/name '([^']+)' is not defined/gi, "变量 $1 未定义") + .replace(/list index out of range/gi, "列表下标越界") + .replace(/index out of range/gi, "索引越界") + .trim() + + if (cnName) + return translatedMessage ? `${cnName}:${translatedMessage}` : cnName + return translatedMessage || "运行错误" +} + +function skulptRead(path: string) { + const builtinFiles = (Sk as any).builtinFiles + const files = builtinFiles?.files ?? builtinFiles?.["files"] + if (!files) throw new Error("skulpt-stdlib.js has not been loaded") + if (files[path] === undefined) throw new Error(`File not found: '${path}'`) + return files[path] +} + +async function runPythonWithSkulpt( + source: string, + stdin: string, + timeoutMs: number, +) { + const stdout: string[] = [] + const inputLines = (stdin ?? "").split("\n") + let inputIndex = 0 + const normalizedSource = String(source ?? "").replace(/\r\n/g, "\n") + + ;(Sk as any).configure({ + output: (text: string) => stdout.push(String(text)), + read: skulptRead, + inputfun: () => String(inputLines[inputIndex++] ?? ""), + inputfunTakesPrompt: true, + __future__: (Sk as any).python3, + }) + ;(Sk as any).execLimit = Math.max(1, Number(timeoutMs) || 1) + ;(Sk as any).execStart = new Date() + + try { + await (Sk as any).misceval.asyncToPromise(() => + (Sk as any).importMainWithBody("", false, normalizedSource, true), + ) + return { status: 3, output: stdout.join("").trimEnd() } + } catch (err: any) { + const name = String(err?.tp$name ?? err?.name ?? "") + const message = String( + err?.tp$str?.()?.v ?? err?.message ?? err?.toString?.() ?? err ?? "", + ) + + const isCompileError = + name === "SyntaxError" || + name === "IndentationError" || + name === "TabError" || + /SyntaxError|IndentationError|TabError/i.test(message) + + const formattedError = translateSkulptError(name, message, isCompileError) + + return { + status: isCompileError ? 6 : 11, + output: (stdout.join("") + (stdout.length ? "\n" : "") + formattedError) + .trim() + .replace(/\r\n/g, "\n"), + } + } +} + +self.onmessage = async (event: MessageEvent) => { + const { id, source, stdin, timeoutMs } = event.data + try { + const result = await runPythonWithSkulpt(source, stdin, timeoutMs) + ;(self as any).postMessage({ id, ...result } satisfies RunResponse) + } catch (err: any) { + const output = String(err?.message ?? err?.toString?.() ?? err ?? "") + ;(self as any).postMessage({ id, status: 11, output } satisfies RunResponse) + } +}