Skip to content

Commit e270b49

Browse files
runtime初期化/実行の異常系を onError + fatalError で分離伝播 (#218)
* chore: plan runtime fatal error handling changes Agent-Logs-Url: https://github.com/ut-code/my-code/sessions/3499a4d7-2b75-4046-b6d5-24912d4eaf0a Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> * feat(runtime): add fatalError and onError callback propagation Agent-Logs-Url: https://github.com/ut-code/my-code/sessions/3499a4d7-2b75-4046-b6d5-24912d4eaf0a Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> * revert package-lock.json * fix: restore package-lock from sentry to prevent dependency drift Agent-Logs-Url: https://github.com/ut-code/my-code/sessions/f1dea2f9-037f-4cd9-a9e0-cefe2e569bfa Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> * fix(runtime): avoid bundling typescript.js in server handler Agent-Logs-Url: https://github.com/ut-code/my-code/sessions/1f1e6587-2ca0-4873-86cf-5cc4025a539b Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> * fatalErrorをそんなに目立たせる必要ない * handleRuntimeErrorをplainの関数にし、alertを追加 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com>
1 parent f3d76b7 commit e270b49

9 files changed

Lines changed: 184 additions & 54 deletions

File tree

app/terminal/exec.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,16 @@ import { useEmbedContext } from "./embedContext";
1313
import clsx from "clsx";
1414
import { LangConstants } from "@my-code/runtime/languages";
1515
import { useRuntime } from "@my-code/runtime/context";
16+
import { captureException } from "@sentry/nextjs";
1617
import { MinMaxButton, Modal } from "./modal";
1718

19+
function handleRuntimeError(error: unknown) {
20+
captureException(error);
21+
window.alert(
22+
"コード実行環境で予期せぬエラーが発生しました: \n" + String(error)
23+
);
24+
}
25+
1826
interface ExecProps {
1927
/*
2028
* Pythonの場合はメインファイル1つのみを指定する。
@@ -68,8 +76,9 @@ export function ExecFile(props: ExecProps) {
6876
`Language ${props.language.originalLang} does not have a runtime environment.`
6977
);
7078
}
79+
7180
const { ready, runFiles, getCommandlineStr, runtimeInfo, interrupt } =
72-
useRuntime(props.language.runtime);
81+
useRuntime(props.language.runtime, { onError: handleRuntimeError });
7382

7483
// ユーザーがクリックした時(triggered) && ランタイムが準備できた時に、実際にinitCommandを実行する(executing)
7584
const [executionState, setExecutionState] = useState<

app/terminal/repl.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { useEmbedContext } from "./embedContext";
1717
import { LangConstants } from "@my-code/runtime/languages";
1818
import clsx from "clsx";
1919
import { InlineCode } from "@/markdown/codeBlock";
20+
import { captureException } from "@sentry/nextjs";
2021
import {
2122
emptyMutex,
2223
ReplCommand,
@@ -26,6 +27,13 @@ import { useRuntime } from "@my-code/runtime/context";
2627
import { MinMaxButton, Modal } from "./modal";
2728
import { StopButtonContent } from "./exec";
2829

30+
function handleRuntimeError(error: unknown) {
31+
captureException(error);
32+
window.alert(
33+
"コード実行環境で予期せぬエラーが発生しました: \n" + String(error)
34+
);
35+
}
36+
2937
export function writeOutput(
3038
term: Terminal,
3139
output: ReplOutput,
@@ -37,6 +45,7 @@ export function writeOutput(
3745
const message = String(output.message).replace(/\n/g, "\r\n");
3846
switch (output.type) {
3947
case "error":
48+
case "fatalError":
4049
term.writeln(chalk.red(message));
4150
break;
4251
case "trace":
@@ -95,7 +104,7 @@ export function ReplTerminal({
95104
checkSyntax,
96105
splitReplExamples,
97106
runtimeInfo,
98-
} = useRuntime(language.runtime);
107+
} = useRuntime(language.runtime, { onError: handleRuntimeError });
99108
const { tabSize, prompt, promptMore, returnPrefix } = language;
100109
if (!prompt) {
101110
console.warn(`prompt not defined for language: ${language}`);

packages/runtime/src/context.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,20 @@ import { PyodideContext, usePyodide } from "./worker/pyodide";
1010
import { RubyContext, useRuby } from "./worker/ruby";
1111
import { WorkerProvider } from "./worker/runtime";
1212

13-
export function useRuntime(language: RuntimeLang): RuntimeContext {
13+
interface UseRuntimeOptions {
14+
onError?: (error: unknown) => void;
15+
}
16+
export function useRuntime(
17+
language: RuntimeLang,
18+
options?: UseRuntimeOptions
19+
): RuntimeContext {
1420
const runtimes = useRuntimeAll();
1521
const runtime = runtimes[language];
1622
const { init } = runtime;
23+
const { onError } = options ?? {};
1724
useEffect(() => {
18-
init?.();
19-
}, [init]);
25+
init?.(onError);
26+
}, [init, onError]);
2027
return runtime;
2128
}
2229
export function useRuntimeAll(): Record<RuntimeLang, RuntimeContext> {

packages/runtime/src/interface.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface RuntimeContext {
2727
* 初期化とcleanupはuseEffect()で非同期に行うのがよいです。
2828
*
2929
*/
30-
init?: () => void;
30+
init?: (onError?: RuntimeErrorHandler) => void;
3131
/**
3232
* ランタイムの初期化が完了したか、不要である場合true
3333
*/
@@ -148,11 +148,13 @@ export interface RuntimeInfo {
148148
prettyLangName: string;
149149
version?: string;
150150
}
151+
export type RuntimeErrorHandler = (error: unknown) => void;
151152

152153
export const ReplOutputTypeSchema = z.enum([
153154
"stdout",
154155
"stderr",
155156
"error",
157+
"fatalError",
156158
"return",
157159
"trace",
158160
"system",

packages/runtime/src/typescript/runtime.tsx

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@ import {
99
useContext,
1010
useEffect,
1111
useMemo,
12+
useRef,
1213
useState,
1314
} from "react";
14-
import { ReplOutput, RuntimeContext, RuntimeInfo, UpdatedFile } from "../interface";
15+
import {
16+
ReplOutput,
17+
RuntimeContext,
18+
RuntimeErrorHandler,
19+
RuntimeInfo,
20+
UpdatedFile,
21+
} from "../interface";
1522

1623
export const compilerOptions: CompilerOptions = {
1724
lib: ["ESNext", "WebWorker"],
@@ -20,22 +27,26 @@ export const compilerOptions: CompilerOptions = {
2027
};
2128

2229
const TypeScriptContext = createContext<{
23-
init: () => void;
30+
init: (onError?: RuntimeErrorHandler) => void;
2431
tsEnv: VirtualTypeScriptEnvironment | null;
2532
tsVersion?: string;
2633
}>({ init: () => undefined, tsEnv: null });
2734
export function TypeScriptProvider({ children }: { children: ReactNode }) {
2835
const [tsEnv, setTSEnv] = useState<VirtualTypeScriptEnvironment | null>(null);
2936
const [tsVersion, setTSVersion] = useState<string | undefined>(undefined);
3037
const [doInit, setDoInit] = useState(false);
31-
const init = useCallback(() => setDoInit(true), []);
38+
const onErrorRef = useRef<RuntimeErrorHandler | undefined>(undefined);
39+
const init = useCallback((onError?: RuntimeErrorHandler) => {
40+
onErrorRef.current = onError;
41+
setDoInit(true);
42+
}, []);
3243
useEffect(() => {
3344
// useEffectはサーバーサイドでは実行されないが、
3445
// typeof window !== "undefined" でガードしないとなぜかesbuildが"typescript"を
3546
// サーバーサイドでのインポート対象とみなしてしまう。
3647
if (doInit && tsEnv === null && typeof window !== "undefined") {
3748
const abortController = new AbortController();
38-
(async () => {
49+
void (async () => {
3950
const ts = await import("typescript");
4051
const vfs = await import("@typescript/vfs");
4152
const system = vfs.createSystem(new Map());
@@ -70,7 +81,12 @@ export function TypeScriptProvider({ children }: { children: ReactNode }) {
7081
);
7182
setTSEnv(env);
7283
setTSVersion(ts.version);
73-
})();
84+
})().catch((error) => {
85+
if (error instanceof DOMException && error.name === "AbortError") {
86+
return;
87+
}
88+
onErrorRef.current?.(error);
89+
});
7490
return () => {
7591
abortController.abort();
7692
};
@@ -86,9 +102,11 @@ export function TypeScriptProvider({ children }: { children: ReactNode }) {
86102
export function useTypeScript(jsEval: RuntimeContext): RuntimeContext {
87103
const { init: tsInit, tsEnv, tsVersion } = useContext(TypeScriptContext);
88104
const { init: jsInit } = jsEval;
89-
const init = useCallback(() => {
90-
tsInit();
91-
jsInit?.();
105+
const onErrorRef = useRef<RuntimeErrorHandler | undefined>(undefined);
106+
const init = useCallback((onError?: RuntimeErrorHandler) => {
107+
onErrorRef.current = onError;
108+
tsInit(onError);
109+
jsInit?.(onError);
92110
}, [tsInit, jsInit]);
93111

94112
const runFiles = useCallback(
@@ -101,6 +119,7 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext {
101119
onOutput({ type: "error", message: "TypeScript is not ready yet." });
102120
return;
103121
} else {
122+
try {
104123
for (const [filename, content] of Object.entries(files)) {
105124
tsEnv.createFile(filename, content);
106125
}
@@ -151,6 +170,13 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext {
151170
{ ...files, ...emittedFiles },
152171
onOutput
153172
);
173+
} catch (error) {
174+
onErrorRef.current?.(error);
175+
onOutput({
176+
type: "fatalError",
177+
message: error instanceof Error ? error.message : String(error),
178+
});
179+
}
154180
}
155181
},
156182
[tsEnv, jsEval]

packages/runtime/src/wandbox/runtime.tsx

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,27 @@ import {
55
ReactNode,
66
useCallback,
77
useContext,
8+
useEffect,
89
useMemo,
10+
useRef,
911
} from "react";
1012
import useSWR from "swr";
1113
import { compilerInfoFetcher, SelectedCompiler } from "./api";
1214
import { cppRunFiles, selectCppCompiler } from "./cpp";
1315
import { RuntimeLang } from "../languages";
1416
import { rustRunFiles, selectRustCompiler } from "./rust";
15-
import { ReplOutput, RuntimeContext, RuntimeInfo, UpdatedFile } from "../interface";
17+
import {
18+
ReplOutput,
19+
RuntimeContext,
20+
RuntimeErrorHandler,
21+
RuntimeInfo,
22+
UpdatedFile,
23+
} from "../interface";
1624

1725
type WandboxLang = "cpp" | "rust";
1826

1927
interface IWandboxContext {
28+
init: (onError?: RuntimeErrorHandler) => void;
2029
ready: boolean;
2130
getCommandlineStrWithLang: (
2231
lang: WandboxLang
@@ -34,10 +43,17 @@ interface IWandboxContext {
3443
const WandboxContext = createContext<IWandboxContext>(null!);
3544

3645
export function WandboxProvider({ children }: { children: ReactNode }) {
46+
const onErrorRef = useRef<RuntimeErrorHandler | undefined>(undefined);
47+
const init = useCallback((onError?: RuntimeErrorHandler) => {
48+
onErrorRef.current = onError;
49+
}, []);
3750
const { data: compilerList, error } = useSWR("list", compilerInfoFetcher);
38-
if (error) {
39-
console.error("Failed to fetch compiler list from Wandbox:", error);
40-
}
51+
useEffect(() => {
52+
if (error) {
53+
console.error("Failed to fetch compiler list from Wandbox:", error);
54+
onErrorRef.current?.(error);
55+
}
56+
}, [error]);
4157

4258
const ready = !!compilerList;
4359

@@ -76,16 +92,29 @@ export function WandboxProvider({ children }: { children: ReactNode }) {
7692
onOutput({ type: "error", message: "Wandbox is not ready yet." });
7793
return;
7894
}
79-
switch (lang) {
80-
case "cpp":
81-
await cppRunFiles(selectedCompiler.cpp, files, filenames, onOutput);
82-
break;
83-
case "rust":
84-
await rustRunFiles(selectedCompiler.rust, files, filenames, onOutput);
85-
break;
86-
default:
87-
lang satisfies never;
88-
throw new Error(`unsupported language: ${lang}`);
95+
try {
96+
switch (lang) {
97+
case "cpp":
98+
await cppRunFiles(selectedCompiler.cpp, files, filenames, onOutput);
99+
break;
100+
case "rust":
101+
await rustRunFiles(
102+
selectedCompiler.rust,
103+
files,
104+
filenames,
105+
onOutput
106+
);
107+
break;
108+
default:
109+
lang satisfies never;
110+
throw new Error(`unsupported language: ${lang}`);
111+
}
112+
} catch (error) {
113+
onErrorRef.current?.(error);
114+
onOutput({
115+
type: "fatalError",
116+
message: error instanceof Error ? error.message : String(error),
117+
});
89118
}
90119
},
91120
[selectedCompiler]
@@ -94,6 +123,7 @@ export function WandboxProvider({ children }: { children: ReactNode }) {
94123
return (
95124
<WandboxContext.Provider
96125
value={{
126+
init,
97127
ready,
98128
getCommandlineStrWithLang,
99129
runFilesWithLang,
@@ -123,6 +153,7 @@ export function useWandbox(lang: WandboxLang): RuntimeContext {
123153
);
124154

125155
return {
156+
init: context.init,
126157
ready: context.ready,
127158
runFiles,
128159
getCommandlineStr,

packages/runtime/src/worker/pyodide.worker.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,13 @@ async function runCode(
115115
});
116116
} else {
117117
await onOutput({
118-
type: "error",
118+
type: "fatalError",
119119
message: `予期せぬエラー: ${e.message.trim()}`,
120120
});
121121
}
122122
} else {
123123
await onOutput({
124-
type: "error",
124+
type: "fatalError",
125125
message: `予期せぬエラー: ${String(e).trim()}`,
126126
});
127127
}
@@ -175,13 +175,13 @@ async function runFile(
175175
});
176176
} else {
177177
await onOutput({
178-
type: "error",
178+
type: "fatalError",
179179
message: `予期せぬエラー: ${e.message.trim()}`,
180180
});
181181
}
182182
} else {
183183
await onOutput({
184-
type: "error",
184+
type: "fatalError",
185185
message: `予期せぬエラー: ${String(e).trim()}`,
186186
});
187187
}

0 commit comments

Comments
 (0)