diff --git a/app/terminal/exec.tsx b/app/terminal/exec.tsx index e016cba..04bf42b 100644 --- a/app/terminal/exec.tsx +++ b/app/terminal/exec.tsx @@ -7,7 +7,7 @@ import { systemMessageColor, useTerminal, } from "./terminal"; -import { writeOutput } from "./repl"; +import { writeOutput, ReplOutput } from "./repl"; import { useEffect, useState } from "react"; import { useEmbedContext } from "./embedContext"; import { RuntimeLang, useRuntime } from "./runtime"; @@ -46,16 +46,25 @@ export function ExecFile(props: ExecProps) { (async () => { clearTerminal(terminalInstanceRef.current!); terminalInstanceRef.current!.write(systemMessageColor("実行中です...")); - const outputs = await runFiles(props.filenames, files); - clearTerminal(terminalInstanceRef.current!); - writeOutput( - terminalInstanceRef.current!, - outputs, - false, - undefined, - null, // ファイル実行で"return"メッセージが返ってくることはないはずなので、Prismを渡す必要はない - props.language - ); + const outputs: ReplOutput[] = []; + let isFirstOutput = true; + await runFiles(props.filenames, files, (output) => { + outputs.push(output); + if (isFirstOutput) { + // Clear "実行中です..." message only on first output + clearTerminal(terminalInstanceRef.current!); + isFirstOutput = false; + } + // Append only the new output + writeOutput( + terminalInstanceRef.current!, + [output], + true, + undefined, + null, // ファイル実行で"return"メッセージが返ってくることはないはずなので、Prismを渡す必要はない + props.language + ); + }); // TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる setExecResult(props.filenames.join(","), outputs); setExecutionState("idle"); diff --git a/app/terminal/repl.tsx b/app/terminal/repl.tsx index f1a9b46..c8cb890 100644 --- a/app/terminal/repl.tsx +++ b/app/terminal/repl.tsx @@ -176,21 +176,19 @@ export function ReplTerminal({ // ランタイムからのoutputを描画し、inputBufferをリセット const handleOutput = useCallback( - (outputs: ReplOutput[]) => { + (output: ReplOutput) => { if (terminalInstanceRef.current) { writeOutput( terminalInstanceRef.current, - outputs, - true, + [output], + false, returnPrefix, Prism, language ); - // 出力が終わったらプロンプトを表示 - updateBuffer(() => [""]); } }, - [Prism, updateBuffer, terminalInstanceRef, returnPrefix, language] + [Prism, terminalInstanceRef, returnPrefix, language] ); const keyHandler = useCallback( @@ -220,11 +218,20 @@ export function ReplTerminal({ terminalInstanceRef.current.writeln(""); const command = inputBuffer.current.join("\n").trim(); inputBuffer.current = []; - const outputs = await runtimeMutex.runExclusive(() => - runCommand(command) - ); - handleOutput(outputs); - addReplOutput?.(terminalId, command, outputs); + const collectedOutputs: ReplOutput[] = []; + let isFirstOutput = true; + await runtimeMutex.runExclusive(async () => { + await runCommand(command, (output) => { + collectedOutputs.push(output); + handleOutput(output); + isFirstOutput = false; + }); + }); + if (!isFirstOutput && terminalInstanceRef.current) { + terminalInstanceRef.current.writeln(""); + } + updateBuffer(() => [""]); + addReplOutput?.(terminalId, command, collectedOutputs); } } else if (code === 127) { // Backspace @@ -301,8 +308,14 @@ export function ReplTerminal({ updateBuffer(() => cmd.command.split("\n")); terminalInstanceRef.current!.writeln(""); inputBuffer.current = []; - handleOutput(cmd.output); + for (const output of cmd.output) { + handleOutput(output); + } + terminalInstanceRef.current!.writeln(""); + updateBuffer(() => [""]); } + } else { + updateBuffer(() => [""]); } terminalInstanceRef.current!.scrollToTop(); setInitCommandState("idle"); @@ -320,7 +333,10 @@ export function ReplTerminal({ const initCommandResult: ReplCommand[] = []; await runtimeMutex.runExclusive(async () => { for (const cmd of initCommand!) { - const outputs = await runCommand(cmd.command); + const outputs: ReplOutput[] = []; + await runCommand(cmd.command, (output) => { + outputs.push(output); + }); initCommandResult.push({ command: cmd.command, output: outputs, @@ -333,7 +349,11 @@ export function ReplTerminal({ updateBuffer(() => cmd.command.split("\n")); terminalInstanceRef.current!.writeln(""); inputBuffer.current = []; - handleOutput(cmd.output); + for (const output of cmd.output) { + handleOutput(output); + } + terminalInstanceRef.current!.writeln(""); + updateBuffer(() => [""]); } } updateBuffer(() => [""]); diff --git a/app/terminal/runtime.tsx b/app/terminal/runtime.tsx index 0423a92..d5e649d 100644 --- a/app/terminal/runtime.tsx +++ b/app/terminal/runtime.tsx @@ -24,14 +24,18 @@ export interface RuntimeContext { mutex?: MutexInterface; interrupt?: () => void; // repl - runCommand?: (command: string) => Promise; + runCommand?: ( + command: string, + onOutput: (output: ReplOutput) => void + ) => Promise; checkSyntax?: (code: string) => Promise; splitReplExamples?: (content: string) => ReplCommand[]; // file runFiles: ( filenames: string[], - files: Readonly> - ) => Promise; + files: Readonly>, + onOutput: (output: ReplOutput) => void + ) => Promise; getCommandlineStr?: (filenames: string[]) => string; } export interface LangConstants { diff --git a/app/terminal/tests.ts b/app/terminal/tests.ts index 6ccfbe0..216c721 100644 --- a/app/terminal/tests.ts +++ b/app/terminal/tests.ts @@ -42,11 +42,16 @@ export function defineTests( if (!printCode) { this.skip(); } - const result = await ( + const outputs: any[] = []; + await ( runtimeRef.current[lang].mutex || emptyMutex - ).runExclusive(() => runtimeRef.current[lang].runCommand!(printCode)); - console.log(`${lang} REPL stdout test: `, result); - expect(result).to.be.deep.include({ type: "stdout", message: msg }); + ).runExclusive(() => + runtimeRef.current[lang].runCommand!(printCode, (output) => { + outputs.push(output); + }) + ); + console.log(`${lang} REPL stdout test: `, outputs); + expect(outputs).to.be.deep.include({ type: "stdout", message: msg }); }); it("should preserve variables across commands", async function () { @@ -68,14 +73,17 @@ export function defineTests( if (!setIntVarCode || !printIntVarCode) { this.skip(); } - const result = await ( + const outputs: any[] = []; + await ( runtimeRef.current[lang].mutex || emptyMutex ).runExclusive(async () => { - await runtimeRef.current[lang].runCommand!(setIntVarCode); - return runtimeRef.current[lang].runCommand!(printIntVarCode); + await runtimeRef.current[lang].runCommand!(setIntVarCode, () => {}); + await runtimeRef.current[lang].runCommand!(printIntVarCode, (output) => { + outputs.push(output); + }); }); - console.log(`${lang} REPL variable preservation test: `, result); - expect(result).to.be.deep.include({ + console.log(`${lang} REPL variable preservation test: `, outputs); + expect(outputs).to.be.deep.include({ type: "stdout", message: value.toString(), }); @@ -96,12 +104,17 @@ export function defineTests( if (!errorCode) { this.skip(); } - const result = await ( + const outputs: any[] = []; + await ( runtimeRef.current[lang].mutex || emptyMutex - ).runExclusive(() => runtimeRef.current[lang].runCommand!(errorCode)); - console.log(`${lang} REPL error capture test: `, result); + ).runExclusive(() => + runtimeRef.current[lang].runCommand!(errorCode, (output) => { + outputs.push(output); + }) + ); + console.log(`${lang} REPL error capture test: `, outputs); // eslint-disable-next-line @typescript-eslint/no-unused-expressions - expect(result.filter((r) => r.message.includes(errorMsg))).to.not.be + expect(outputs.filter((r) => r.message.includes(errorMsg))).to.not.be .empty; }); @@ -126,8 +139,8 @@ export function defineTests( const runPromise = ( runtimeRef.current[lang].mutex || emptyMutex ).runExclusive(async () => { - await runtimeRef.current[lang].runCommand!(setIntVarCode); - return runtimeRef.current[lang].runCommand!(infLoopCode); + await runtimeRef.current[lang].runCommand!(setIntVarCode, () => {}); + await runtimeRef.current[lang].runCommand!(infLoopCode, () => {}); }); // Wait a bit to ensure the infinite loop has started await new Promise((resolve) => setTimeout(resolve, 1000)); @@ -137,13 +150,16 @@ export function defineTests( while (!runtimeRef.current[lang].ready) { await new Promise((resolve) => setTimeout(resolve, 100)); } - const result = await ( + const outputs: any[] = []; + await ( runtimeRef.current[lang].mutex || emptyMutex ).runExclusive(() => - runtimeRef.current[lang].runCommand!(printIntVarCode) + runtimeRef.current[lang].runCommand!(printIntVarCode, (output) => { + outputs.push(output); + }) ); - console.log(`${lang} REPL interrupt recovery test: `, result); - expect(result).to.be.deep.include({ type: "stdout", message: "42" }); + console.log(`${lang} REPL interrupt recovery test: `, outputs); + expect(outputs).to.be.deep.include({ type: "stdout", message: "42" }); }); it("should capture files modified by command", async function () { @@ -162,10 +178,9 @@ export function defineTests( if (!writeCode) { this.skip(); } - const result = await ( + await ( runtimeRef.current[lang].mutex || emptyMutex - ).runExclusive(() => runtimeRef.current[lang].runCommand!(writeCode)); - console.log(`${lang} REPL file modify test: `, result); + ).runExclusive(() => runtimeRef.current[lang].runCommand!(writeCode, () => {})); // wait for files to be updated await new Promise((resolve) => setTimeout(resolve, 100)); expect(filesRef.current[targetFile]).to.equal(msg); @@ -191,11 +206,14 @@ export function defineTests( if (!filename || !code) { this.skip(); } - const result = await runtimeRef.current[lang].runFiles([filename], { + const outputs: any[] = []; + await runtimeRef.current[lang].runFiles([filename], { [filename]: code, + }, (output) => { + outputs.push(output); }); - console.log(`${lang} single file stdout test: `, result); - expect(result).to.be.deep.include({ type: "stdout", message: msg }); + console.log(`${lang} single file stdout test: `, outputs); + expect(outputs).to.be.deep.include({ type: "stdout", message: msg }); }); it("should capture errors", async function () { @@ -220,12 +238,15 @@ export function defineTests( if (!filename || !code) { this.skip(); } - const result = await runtimeRef.current[lang].runFiles([filename], { + const outputs: any[] = []; + await runtimeRef.current[lang].runFiles([filename], { [filename]: code, + }, (output) => { + outputs.push(output); }); - console.log(`${lang} single file error capture test: `, result); + console.log(`${lang} single file error capture test: `, outputs); // eslint-disable-next-line @typescript-eslint/no-unused-expressions - expect(result.filter((r) => r.message.includes(errorMsg))).to.not.be + expect(outputs.filter((r) => r.message.includes(errorMsg))).to.not.be .empty; }); @@ -276,12 +297,16 @@ export function defineTests( if (!codes || !execFiles) { this.skip(); } - const result = await runtimeRef.current[lang].runFiles( + const outputs: any[] = []; + await runtimeRef.current[lang].runFiles( execFiles, - codes + codes, + (output) => { + outputs.push(output); + } ); - console.log(`${lang} multifile stdout test: `, result); - expect(result).to.be.deep.include({ type: "stdout", message: msg }); + console.log(`${lang} multifile stdout test: `, outputs); + expect(outputs).to.be.deep.include({ type: "stdout", message: msg }); }); it("should capture files modified by script", async function () { @@ -306,10 +331,9 @@ export function defineTests( if (!filename || !code) { this.skip(); } - const result = await runtimeRef.current[lang].runFiles([filename], { + await runtimeRef.current[lang].runFiles([filename], { [filename]: code, - }); - console.log(`${lang} file modify test: `, result); + }, () => {}); // wait for files to be updated await new Promise((resolve) => setTimeout(resolve, 100)); expect(filesRef.current[targetFile]).to.equal(msg); diff --git a/app/terminal/typescript/runtime.tsx b/app/terminal/typescript/runtime.tsx index 57dcc8c..b1a747d 100644 --- a/app/terminal/typescript/runtime.tsx +++ b/app/terminal/typescript/runtime.tsx @@ -91,24 +91,25 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext { const { writeFile } = useEmbedContext(); const runFiles = useCallback( - async (filenames: string[], files: Readonly>) => { + async ( + filenames: string[], + files: Readonly>, + onOutput: (output: ReplOutput) => void + ) => { if (tsEnv === null || typeof window === "undefined") { - return [ - { type: "error" as const, message: "TypeScript is not ready yet." }, - ]; + onOutput({ type: "error", message: "TypeScript is not ready yet." }); + return; } else { for (const [filename, content] of Object.entries(files)) { tsEnv.createFile(filename, content); } - const outputs: ReplOutput[] = []; - const ts = await import("typescript"); for (const diagnostic of tsEnv.languageService.getSyntacticDiagnostics( filenames[0] )) { - outputs.push({ + onOutput({ type: "error", message: ts.formatDiagnosticsWithColorAndContext([diagnostic], { getCurrentDirectory: () => "", @@ -121,7 +122,7 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext { for (const diagnostic of tsEnv.languageService.getSemanticDiagnostics( filenames[0] )) { - outputs.push({ + onOutput({ type: "error", message: ts.formatDiagnosticsWithColorAndContext([diagnostic], { getCurrentDirectory: () => "", @@ -143,12 +144,11 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext { } console.log(emitOutput); - const jsOutputs = jsEval.runFiles( + await jsEval.runFiles( [emitOutput.outputFiles[0].name], - files + files, + onOutput ); - - return outputs.concat(await jsOutputs); } }, [tsEnv, writeFile, jsEval] diff --git a/app/terminal/wandbox/api.ts b/app/terminal/wandbox/api.ts index bc4a421..eb88b01 100644 --- a/app/terminal/wandbox/api.ts +++ b/app/terminal/wandbox/api.ts @@ -70,6 +70,14 @@ export interface CompileNdjsonResult { data: string; } +/** + * Output event with original NDJSON type and converted ReplOutput + */ +export interface CompileOutputEvent { + ndjsonType: string; + output: ReplOutput; +} + export interface CompileResult { status: string; signal: string; @@ -102,13 +110,57 @@ interface CompileProps { codes: Record; // codes: Code[]; } -export interface CompileResultWithOutput extends CompileResult { - output: ReplOutput[]; -} export async function compileAndRun( - options: CompileProps -): Promise { + options: CompileProps, + onOutput: (event: CompileOutputEvent) => void +): Promise { + // Helper function to process NDJSON result and call onOutput + const processNdjsonResult = (r: CompileNdjsonResult) => { + switch (r.type) { + case "CompilerMessageS": + if (r.data.trim()) { + for (const line of r.data.trim().split("\n")) { + onOutput({ + ndjsonType: r.type, + output: { type: "stdout", message: line } + }); + } + } + break; + case "CompilerMessageE": + if (r.data.trim()) { + for (const line of r.data.trim().split("\n")) { + onOutput({ + ndjsonType: r.type, + output: { type: "error", message: line } + }); + } + } + break; + case "StdOut": + if (r.data.trim()) { + for (const line of r.data.trim().split("\n")) { + onOutput({ + ndjsonType: r.type, + output: { type: "stdout", message: line } + }); + } + } + break; + case "StdErr": + if (r.data.trim()) { + for (const line of r.data.trim().split("\n")) { + onOutput({ + ndjsonType: r.type, + output: { type: "stderr", message: line } + }); + } + } + break; + } + }; + // Call the ndjson API instead of json API const response = await fetch(new URL("/api/compile.ndjson", WANDBOX), { method: "post", @@ -158,14 +210,20 @@ export async function compileAndRun( for (const line of lines) { if (line.trim().length > 0) { - ndjsonResults.push(JSON.parse(line) as CompileNdjsonResult); + const r = JSON.parse(line) as CompileNdjsonResult; + ndjsonResults.push(r); + // Call onOutput in real-time as we receive data + processNdjsonResult(r); } } } // Process any remaining data in the buffer if (buffer.trim().length > 0) { - ndjsonResults.push(JSON.parse(buffer) as CompileNdjsonResult); + const r = JSON.parse(buffer) as CompileNdjsonResult; + ndjsonResults.push(r); + // Call onOutput for remaining data + processNdjsonResult(r); } } finally { reader.releaseLock(); @@ -185,9 +243,6 @@ export async function compileAndRun( url: "", }; - // Build output array in the order messages are received - const output: ReplOutput[] = []; - for (const r of ndjsonResults) { switch (r.type) { case "Control": @@ -196,42 +251,18 @@ export async function compileAndRun( case "CompilerMessageS": result.compiler_output += r.data; result.compiler_message += r.data; - // Add to output in order - if (r.data.trim()) { - for (const line of r.data.trim().split("\n")) { - output.push({ type: "stdout", message: line }); - } - } break; case "CompilerMessageE": result.compiler_error += r.data; result.compiler_message += r.data; - // Add to output in order - if (r.data.trim()) { - for (const line of r.data.trim().split("\n")) { - output.push({ type: "error", message: line }); - } - } break; case "StdOut": result.program_output += r.data; result.program_message += r.data; - // Add to output in order - if (r.data.trim()) { - for (const line of r.data.trim().split("\n")) { - output.push({ type: "stdout", message: line }); - } - } break; case "StdErr": result.program_error += r.data; result.program_message += r.data; - // Add to output in order - if (r.data.trim()) { - for (const line of r.data.trim().split("\n")) { - output.push({ type: "stderr", message: line }); - } - } break; case "ExitCode": result.status += r.data; @@ -245,8 +276,5 @@ export async function compileAndRun( } } - return { - ...result, - output, - }; + return result; } diff --git a/app/terminal/wandbox/cpp.ts b/app/terminal/wandbox/cpp.ts index 4c508e1..a556ca6 100644 --- a/app/terminal/wandbox/cpp.ts +++ b/app/terminal/wandbox/cpp.ts @@ -84,8 +84,15 @@ export function selectCppCompiler( export async function cppRunFiles( options: SelectedCompiler, files: Record, - filenames: string[] -): Promise { + filenames: string[], + onOutput: (output: ReplOutput) => void +): Promise { + // Constants for stack trace processing + const WANDBOX_PATH = "/home/wandbox"; + + // Track state for processing stack traces + let inStackTrace = false; + const result = await compileAndRun({ ...options, compilerOptionsRaw: [ @@ -94,57 +101,50 @@ export async function cppRunFiles( "_stacktrace.cpp", ], codes: { ...files, "_stacktrace.cpp": _stacktrace_cpp }, - }); - - let outputs = result.output; - - // Find stack trace in the output - const signalIndex = outputs.findIndex( - (line) => - line.type === "stderr" && line.message.startsWith("#!my_code_signal:") - ); - const traceIndex = outputs.findIndex( - (line) => line.type === "stderr" && line.message === "#!my_code_stacktrace:" - ); - - if (signalIndex >= 0) { - outputs[signalIndex] = { - type: "error", - message: outputs[signalIndex].message.slice(17), - } as const; - } - if (traceIndex >= 0) { - // _stacktrace.cpp のコードで出力されるスタックトレースを、js側でパースしていい感じに表示する - const trace = outputs.slice(traceIndex + 1); - const otherOutputs = outputs.slice(0, traceIndex); - const traceOutputs: ReplOutput[] = [{ - type: "trace", - message: "Stack trace (filtered):", - }]; - - for (const line of trace) { - if(line.type === "stderr"){ - // ユーザーのソースコードだけを対象にする - if (line.message.includes("/home/wandbox")) { - traceOutputs.push({ - type: "trace", - message: line.message.replace("/home/wandbox/", ""), - }); - } - }else{ - otherOutputs.push(line); + }, (event) => { + const { ndjsonType, output } = event; + + // Check for signal marker in stderr + if (ndjsonType === "StdErr" && output.message.startsWith("#!my_code_signal:")) { + onOutput({ + type: "error", + message: output.message.slice(17), + }); + return; + } + + // Check for stack trace marker + if (ndjsonType === "StdErr" && output.message === "#!my_code_stacktrace:") { + inStackTrace = true; + onOutput({ + type: "trace", + message: "Stack trace (filtered):", + }); + return; + } + + // Process stack trace lines + if (inStackTrace && ndjsonType === "StdErr") { + // Filter to show only user source code + if (output.message.includes(WANDBOX_PATH)) { + onOutput({ + type: "trace", + message: output.message.replace(`${WANDBOX_PATH}/`, ""), + }); } + return; } - outputs = [...otherOutputs, ...traceOutputs]; - } + + // Output normally + onOutput(output); + }); if (result.status !== "0") { - outputs.push({ + onOutput({ type: "system", message: `ステータス ${result.status} で異常終了しました`, }); } - // TODO: result.signal はいつ使われるのか? - return outputs; + return result.status; } diff --git a/app/terminal/wandbox/runtime.tsx b/app/terminal/wandbox/runtime.tsx index 3199762..9672062 100644 --- a/app/terminal/wandbox/runtime.tsx +++ b/app/terminal/wandbox/runtime.tsx @@ -25,8 +25,9 @@ interface IWandboxContext { lang: WandboxLang ) => ( filenames: string[], - files: Readonly> - ) => Promise; + files: Readonly>, + onOutput: (output: ReplOutput) => void + ) => Promise; } const WandboxContext = createContext(null!); @@ -65,17 +66,22 @@ export function WandboxProvider({ children }: { children: ReactNode }) { // Curried function for language-specific file execution const runFilesWithLang = useCallback( (lang: WandboxLang) => - async (filenames: string[], files: Readonly>) => { + async ( + filenames: string[], + files: Readonly>, + onOutput: (output: ReplOutput) => void + ) => { if (!selectedCompiler) { - return [ - { type: "error" as const, message: "Wandbox is not ready yet." }, - ]; + onOutput({ type: "error", message: "Wandbox is not ready yet." }); + return; } switch (lang) { case "cpp": - return cppRunFiles(selectedCompiler.cpp, files, filenames); + await cppRunFiles(selectedCompiler.cpp, files, filenames, onOutput); + break; case "rust": - return rustRunFiles(selectedCompiler.rust, files, filenames); + await rustRunFiles(selectedCompiler.rust, files, filenames, onOutput); + break; default: lang satisfies never; throw new Error(`unsupported language: ${lang}`); diff --git a/app/terminal/wandbox/rust.ts b/app/terminal/wandbox/rust.ts index 01f24ee..1fe7671 100644 --- a/app/terminal/wandbox/rust.ts +++ b/app/terminal/wandbox/rust.ts @@ -30,8 +30,19 @@ export function selectRustCompiler( export async function rustRunFiles( options: SelectedCompiler, files: Record, - filenames: string[] -): Promise { + filenames: string[], + onOutput: (output: ReplOutput) => void +): Promise { + // Regular expressions for parsing stack traces + const STACK_FRAME_PATTERN = /^\s*\d+:/; + const LOCATION_PATTERN = /^\s*at .\//; + const SYSTEM_CODE_PATTERN = /^\s*at .\/prog.rs/; + + // Track state for processing panic traces + let inPanicHook = false; + let foundBacktraceHeader = false; + const traceLines: string[] = []; + const mainModule = filenames[0].replace(/\.rs$/, ""); const result = await compileAndRun({ ...options, @@ -50,76 +61,68 @@ export async function rustRunFiles( ?.replace(/(?:pub\s+)?(fn\s+main\s*\()/g, "pub $1") .replaceAll(/mod\s+(\w+)\s*;/g, "use super::$1;"), }, - }); - - let outputs = result.output; - - // Find stack trace in the output - const panicIndex = outputs.findIndex( - (line) => line.type === "stderr" && line.message === "#!my_code_panic_hook:" - ); - - if (panicIndex >= 0) { - const traceIndex = - panicIndex + - outputs - .slice(panicIndex) - .findLastIndex( - (line) => - line.type === "stderr" && line.message === "stack backtrace:" - ); - const otherOutputs = outputs.slice(0, panicIndex); - const traceOutputs: ReplOutput[] = [ - { - type: "trace", - message: "Stack trace (filtered):", - }, - ]; - for (const line of outputs.slice(panicIndex + 1, traceIndex)) { - if (line.type === "stderr") { - otherOutputs.push({ - type: "error", - message: line.message, + }, (event) => { + const { ndjsonType, output } = event; + + // Check for panic hook marker + if (ndjsonType === "StdErr" && output.message === "#!my_code_panic_hook:") { + inPanicHook = true; + return; + } + + if (inPanicHook && ndjsonType === "StdErr") { + // Check for stack backtrace header + if (output.message === "stack backtrace:") { + foundBacktraceHeader = true; + onOutput({ + type: "trace", + message: "Stack trace (filtered):", }); - } else { - otherOutputs.push(line); + return; } - } - for (let i = traceIndex + 1; i < outputs.length; i++) { - const line = outputs.at(i)!; - const nextLine = outputs.at(i + 1); - if (line.type === "stderr") { - // ユーザーのソースコードだけを対象にする - if ( - /^\s*\d+:/.test(line.message) && - nextLine && - /^\s*at .\//.test(nextLine.message) && - !/^\s*at .\/prog.rs/.test(nextLine.message) - ) { - traceOutputs.push({ - type: "trace", - message: line.message.replace("prog::", ""), - }); - traceOutputs.push({ - type: "trace", - message: nextLine.message, - }); - i++; // skip next line + + if (foundBacktraceHeader) { + // Process stack trace lines + // Look for pattern: " N: ..." followed by " at ./file.rs:line" + if (STACK_FRAME_PATTERN.test(output.message)) { + traceLines.push(output.message); + } else if (LOCATION_PATTERN.test(output.message)) { + if (traceLines.length > 0) { + // Check if this is user code (not prog.rs) + if (!SYSTEM_CODE_PATTERN.test(output.message)) { + onOutput({ + type: "trace", + message: traceLines[traceLines.length - 1].replace("prog::", ""), + }); + onOutput({ + type: "trace", + message: output.message, + }); + } + traceLines.pop(); // Remove the associated trace line (regardless of match) + } } - } else { - otherOutputs.push(line); + return; } + + // Output panic messages as errors + onOutput({ + type: "error", + message: output.message, + }); + return; } - - outputs = [...otherOutputs, ...traceOutputs]; - } + + // Output normally + onOutput(output); + }); if (result.status !== "0") { - outputs.push({ + onOutput({ type: "system", message: `ステータス ${result.status} で異常終了しました`, }); } - return outputs; + return result.status; } diff --git a/app/terminal/worker/runtime.tsx b/app/terminal/worker/runtime.tsx index abc0291..6ada852 100644 --- a/app/terminal/worker/runtime.tsx +++ b/app/terminal/worker/runtime.tsx @@ -172,17 +172,16 @@ export function WorkerProvider({ }, [initializeWorker, mutex]); const runCommand = useCallback( - async (code: string): Promise => { + async (code: string, onOutput: (output: ReplOutput) => void): Promise => { if (!mutex.isLocked()) { throw new Error(`mutex of context must be locked for runCommand`); } if (!workerApiRef.current || !ready) { - return [ - { - type: "error", - message: `worker runtime is not ready yet.`, - }, - ]; + onOutput({ + type: "error", + message: `worker runtime is not ready yet.`, + }); + return; } if ( @@ -199,6 +198,7 @@ export function WorkerProvider({ code, proxy((item: ReplOutput) => { output.push(item); + onOutput(item); }) ) ); @@ -212,13 +212,12 @@ export function WorkerProvider({ commandHistory.current.push(code); } } - - return output; } catch (error) { if (error instanceof Error) { - return [{ type: "error", message: error.message }]; + onOutput({ type: "error", message: error.message }); + } else { + onOutput({ type: "error", message: String(error) }); } - return [{ type: "error", message: String(error) }]; } }, [ready, writeFile, mutex, trackPromise] @@ -238,23 +237,22 @@ export function WorkerProvider({ const runFiles = useCallback( async ( filenames: string[], - files: Readonly> - ): Promise => { + files: Readonly>, + onOutput: (output: ReplOutput) => void + ): Promise => { if (filenames.length !== 1) { - return [ - { - type: "error", - message: `worker runtime requires exactly one filename.`, - }, - ]; + onOutput({ + type: "error", + message: `worker runtime requires exactly one filename.`, + }); + return; } if (!workerApiRef.current || !ready) { - return [ - { - type: "error", - message: `worker runtime is not ready yet.`, - }, - ]; + onOutput({ + type: "error", + message: `worker runtime is not ready yet.`, + }); + return; } if ( capabilities.current?.interrupt === "buffer" && @@ -263,18 +261,16 @@ export function WorkerProvider({ interruptBuffer.current[0] = 0; } return mutex.runExclusive(async () => { - const output: ReplOutput[] = []; const { updatedFiles } = await trackPromise( workerApiRef.current!.runFile( filenames[0], files, proxy((item: ReplOutput) => { - output.push(item); + onOutput(item); }) ) ); writeFile(updatedFiles); - return output; }); }, [ready, writeFile, mutex, trackPromise] diff --git a/package-lock.json b/package-lock.json index 14f9210..080d38c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7625,7 +7625,6 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.4.5.tgz", "integrity": "sha512-dQ3hZOkUJzeBXfVEPTm2LVbzmWwka1nqd9KyWmB2OMlMfjr7IdUeBX4T7qJctF67d7QDhlX95jMoxu6JG0Eucw==", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" @@ -7655,14 +7654,12 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.0.tgz", "integrity": "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@better-fetch/fetch": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.18.tgz", - "integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==", - "peer": true + "integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==" }, "node_modules/@bjorn3/browser_wasi_shim": { "version": "0.3.0", @@ -7863,7 +7860,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9582,6 +9578,7 @@ "integrity": "sha512-I5sbpSIAHiB+b6UttofhrN/UJXII+4tZPAq1qugzwCwLIL8EZLV7F/JyHUrEIiGgQpEXzpnjlJ+zwcEhheGvCw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@types/node": "^22.15.30", "@types/pg": "^8.8.0" @@ -9596,6 +9593,7 @@ "integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -9749,7 +9747,6 @@ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -12071,7 +12068,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -12154,7 +12150,6 @@ "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", @@ -12697,8 +12692,7 @@ "version": "5.6.0-beta.115", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.115.tgz", "integrity": "sha512-EJXAW6dbxPuwQnLfTmPB5R3M5uu8qp24ltHdjCcfwGpudKxQRoDEbq1IeGrVLIuRc/8TbnT1U07dXUX7kyGYEQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/abort-controller": { "version": "3.0.0", @@ -12736,7 +12730,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -13229,7 +13222,6 @@ "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.1.4.tgz", "integrity": "sha512-NJouLY6IVKv0nDuFoc6FcbKDFzEnmgMNofC9F60Mwx1Ecm7X6/Ecyoe5b+JSVZ42F/0n46/M89gbYP1ZCVv8xQ==", "license": "MIT", - "peer": true, "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", @@ -14611,7 +14603,6 @@ "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -14692,7 +14683,6 @@ "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -14867,7 +14857,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -16898,7 +16887,6 @@ "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -17025,7 +17013,6 @@ "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.8.tgz", "integrity": "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==", "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" } @@ -17788,7 +17775,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", @@ -18379,8 +18365,7 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/micromatch": { "version": "4.0.8", @@ -19200,7 +19185,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": "^20.0.0 || >=22.0.0" } @@ -19242,7 +19226,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.4.10.tgz", "integrity": "sha512-itVlc79QjpKMFMRhP+kbGKaSG/gZM6RCvwhEbwmCNF06CdDiNaoHcbeg0PqkEa2GOcn8KJ0nnc7+yL7EjoYLHQ==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.4.10", "@swc/helpers": "0.5.15", @@ -19759,7 +19742,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -20173,7 +20155,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -20200,7 +20181,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -21436,7 +21416,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -21660,7 +21639,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -21714,7 +21692,6 @@ "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.19.tgz", "integrity": "sha512-t/OMHBNAkknVCI7bVB9OWjUUAwhVv9vsPIAGnNUxnu3FxPQN11rjh0sksLMzc3g7IlTgvHmOTl4JM7JHpcv5wA==", "license": "MIT", - "peer": true, "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.7", @@ -21728,7 +21705,6 @@ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -22084,7 +22060,6 @@ "integrity": "sha512-w6e0WM2YGfYQGmg0dewZeLUYIxAzMYK1R31vaS4HHHjgT32Xqj0eVQH+leegzY51RZPNCvw5pe8DFmW4MGf8Fg==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "bin": { "workerd": "bin/workerd" }, @@ -22110,7 +22085,6 @@ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.27.0.tgz", "integrity": "sha512-YNHZyMNWebFt9jD6dc20tQrCmnSzJj3SoB0FFa90w11Cx4lbP3d+rUZYjb18Zt+OGSMay1wT2PzwT2vCTskkmg==", "license": "MIT OR Apache-2.0", - "peer": true, "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.5.0", @@ -22441,7 +22415,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }