Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/crutchcorn/binarrow. An experimental browser runtime for AArch64 Linux ELF executables
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581import "./styles.css";
import type { ExecutionDiagnostic, ExecutionLimits, ExecutionReport, FeatureResult, FixtureName, RunTarget, WorkerCommand, WorkerMessage,} from "./probe";
function requiredElement<T extends Element>(selector: string): T { const element = document.querySelector<T>(selector); if (!element) { throw new Error(`runtime control markup is missing ${selector}`); } return element;}
const status = requiredElement<HTMLParagraphElement>("#status");const report = requiredElement<HTMLDListElement>("#report");const terminal = requiredElement<HTMLPreElement>("#terminal");const noCompilerDiagnostics = requiredElement<HTMLParagraphElement>( "#no-compiler-diagnostics",);const compilerDiagnostics = requiredElement<HTMLUListElement>( "#compiler-diagnostics",);const syscallTrace = requiredElement<HTMLPreElement>("#syscall-trace");const exitCode = requiredElement<HTMLElement>("#exit-code");const instructionCount = requiredElement<HTMLElement>("#instruction-count");const syscallCount = requiredElement<HTMLElement>("#syscall-count");const outputCount = requiredElement<HTMLElement>("#output-count");const profiledBlockCount = requiredElement<HTMLElement>("#profiled-block-count");const hottestBlock = requiredElement<HTMLElement>("#hottest-block");const hottestBlockExecutions = requiredElement<HTMLElement>( "#hottest-block-executions",);const translatedBlockExecutions = requiredElement<HTMLElement>( "#translated-block-executions",);const translatedInstructions = requiredElement<HTMLElement>( "#translated-instructions",);const translationFallbackBlocks = requiredElement<HTMLElement>( "#translation-fallback-blocks",);const compiledBlocks = requiredElement<HTMLElement>("#compiled-blocks");const translationCacheHits = requiredElement<HTMLElement>( "#translation-cache-hits",);const compiledWasmBytes = requiredElement<HTMLElement>("#compiled-wasm-bytes");const diagnosticCode = requiredElement<HTMLElement>("#diagnostic-code");const diagnosticMessage = requiredElement<HTMLElement>("#diagnostic-message");const fixture = requiredElement<HTMLSelectElement>("#fixture");const programControls = requiredElement<HTMLFieldSetElement>("#program-controls");const programElf = requiredElement<HTMLInputElement>("#program-elf");const programArgv0 = requiredElement<HTMLInputElement>("#program-argv0");const programArguments = requiredElement<HTMLTextAreaElement>("#program-arguments");const programEnvironment = requiredElement<HTMLTextAreaElement>( "#program-environment",);const instructionBudget = requiredElement<HTMLInputElement>("#instruction-budget");const syscallBudget = requiredElement<HTMLInputElement>("#syscall-budget");const outputLimit = requiredElement<HTMLInputElement>("#output-limit");const memoryLimit = requiredElement<HTMLInputElement>("#memory-limit");const filesystemLimit = requiredElement<HTMLInputElement>("#filesystem-limit");const startButton = requiredElement<HTMLButtonElement>("#start");const stopButton = requiredElement<HTMLButtonElement>("#stop");const terminalInput = requiredElement<HTMLInputElement>("#terminal-input");const sendInputButton = requiredElement<HTMLButtonElement>("#send-input");const projectSource = requiredElement<HTMLTextAreaElement>("#project-source");const saveSourceButton = requiredElement<HTMLButtonElement>("#save-source");const projectImage = requiredElement<HTMLInputElement>("#project-image");const replaceImageButton = requiredElement<HTMLButtonElement>("#replace-image");const installImageButton = requiredElement<HTMLButtonElement>("#install-image");const exportImageButton = requiredElement<HTMLButtonElement>("#export-image");const clearBuildCacheButton = requiredElement<HTMLButtonElement>( "#clear-build-cache",);
let worker: Worker;let workerReady = false;let running = false;let waitingInput = false;let runId = 0;let preserveDiagnosticStatus = false;let imageRequestId = 0;let imageOperationPending = false;
interface CompilerDiagnostic { path: string; line: number; column: number; severity: "fatal error" | "error" | "warning" | "note"; message: string;}
function parseCompilerDiagnostics(stderr: string): CompilerDiagnostic[] { const diagnostics: CompilerDiagnostic[] = []; const pattern = /^([^\n:]+(?:\/[^\n:]+)*):(\d+):(\d+):\s+(fatal error|error|warning|note):\s+(.+)$/gm; for (const match of stderr.matchAll(pattern)) { const [, path, line, column, severity, message] = match; if (!path || !line || !column || !severity || !message) { continue; } diagnostics.push({ path, line: Number.parseInt(line, 10), column: Number.parseInt(column, 10), severity: severity as CompilerDiagnostic["severity"], message, }); } return diagnostics;}
function focusProjectSource(line: number, column: number): void { const lines = projectSource.value.split("\n"); const lineIndex = Math.min(Math.max(line - 1, 0), lines.length - 1); const lineText = lines[lineIndex] ?? ""; const columnIndex = Math.min(Math.max(column - 1, 0), lineText.length); let offset = columnIndex; for (let index = 0; index < lineIndex; index += 1) { offset += (lines[index]?.length ?? 0) + 1; } projectSource.focus(); projectSource.setSelectionRange(offset, offset);}
function renderCompilerDiagnostics(stderr: string): void { compilerDiagnostics.replaceChildren(); const diagnostics = parseCompilerDiagnostics(stderr); noCompilerDiagnostics.hidden = diagnostics.length > 0; for (const diagnostic of diagnostics) { const item = document.createElement("li"); const button = document.createElement("button"); button.type = "button"; button.dataset.severity = diagnostic.severity; button.textContent = `${diagnostic.path}:${diagnostic.line}:${diagnostic.column} ${diagnostic.severity}: ${diagnostic.message}`; button.addEventListener("click", () => { if (diagnostic.path === "/project/main.c") { focusProjectSource(diagnostic.line, diagnostic.column); } }); item.append(button); compilerDiagnostics.append(item); }}
function setControls(): void { const busy = running || imageOperationPending; startButton.disabled = !workerReady || busy; stopButton.disabled = !running; fixture.disabled = busy; programElf.disabled = busy; programArgv0.disabled = busy; programArguments.disabled = busy; programEnvironment.disabled = busy; terminalInput.disabled = !waitingInput; sendInputButton.disabled = !waitingInput; projectSource.disabled = !workerReady || busy; saveSourceButton.disabled = !workerReady || busy; projectImage.disabled = !workerReady || busy; replaceImageButton.disabled = !workerReady || busy; installImageButton.disabled = !workerReady || busy; exportImageButton.disabled = !workerReady || busy; clearBuildCacheButton.disabled = !workerReady || busy;}
function updateProgramControls(): void { programControls.hidden = fixture.value !== "uploaded-program";}
function renderFeatures(results: FeatureResult[]): void { report.replaceChildren(); for (const result of results) { const term = document.createElement("dt"); term.textContent = result.name;
const detail = document.createElement("dd"); detail.dataset.supported = String(result.supported); detail.textContent = result.supported ? "Supported" : "Unavailable"; if (result.detail) { detail.title = result.detail; }
report.append(term, detail); }}
function renderDiagnostic(diagnostic?: ExecutionDiagnostic): void { diagnosticCode.textContent = diagnostic?.code ?? "—"; diagnosticMessage.textContent = diagnostic?.message ?? "—";}
function clearExecution(): void { terminal.textContent = ""; renderCompilerDiagnostics(""); syscallTrace.textContent = ""; exitCode.textContent = "—"; instructionCount.textContent = "—"; syscallCount.textContent = "—"; outputCount.textContent = "—"; profiledBlockCount.textContent = "—"; hottestBlock.textContent = "—"; hottestBlockExecutions.textContent = "—"; translatedBlockExecutions.textContent = "—"; translatedInstructions.textContent = "—"; translationFallbackBlocks.textContent = "—"; compiledBlocks.textContent = "—"; translationCacheHits.textContent = "—"; compiledWasmBytes.textContent = "—"; renderDiagnostic();}
function renderExecution(execution: ExecutionReport): void { terminal.textContent = execution.stdout; if (execution.stderr) { terminal.textContent += execution.stderr; } renderCompilerDiagnostics(execution.stderr); syscallTrace.textContent = execution.trace; exitCode.textContent = execution.exitCode === null ? "—" : String(execution.exitCode); instructionCount.textContent = String(execution.executedInstructions); syscallCount.textContent = String(execution.dispatchedSyscalls); outputCount.textContent = String(execution.outputBytes); profiledBlockCount.textContent = String(execution.profiledBlocks); hottestBlock.textContent = execution.hottestBlockStart === null ? "—" : `0x${execution.hottestBlockStart.toString(16)}`; hottestBlockExecutions.textContent = String(execution.hottestBlockExecutions); translatedBlockExecutions.textContent = String( execution.translatedBlockExecutions, ); translatedInstructions.textContent = String(execution.translatedInstructions); translationFallbackBlocks.textContent = String( execution.translationFallbackBlocks, ); compiledBlocks.textContent = String(execution.compiledBlocks); translationCacheHits.textContent = String(execution.translationCacheHits); compiledWasmBytes.textContent = String(execution.compiledWasmBytes); renderDiagnostic(execution.diagnostic);
if (execution.outcome === "waiting-input") { status.textContent = `Waiting for terminal input (up to ${execution.inputMaxBytes} bytes)`; status.dataset.state = "running"; } else if (execution.outcome === "exited") { status.textContent = "Guest exited"; status.dataset.state = "ready"; } else { status.textContent = `Execution stopped: ${execution.diagnostic?.message ?? "unknown diagnostic"}`; status.dataset.state = "error"; }}
function parseLimit(input: HTMLInputElement): bigint { const value = BigInt(input.value); if (value <= 0n) { throw new Error(`${input.labels?.[0]?.textContent ?? input.id} must be positive`); } return value;}
function readLimits(): ExecutionLimits { return { instructionBudget: parseLimit(instructionBudget), syscallBudget: parseLimit(syscallBudget), maxOutputBytes: parseLimit(outputLimit), maxMemoryBytes: parseLimit(memoryLimit), maxFilesystemBytes: parseLimit(filesystemLimit), };}
function createWorker(): void { workerReady = false; imageOperationPending = false; setControls(); worker = new Worker(new URL("./probe.worker.ts", import.meta.url), { type: "module", }); worker.addEventListener("message", (event: MessageEvent<WorkerMessage>) => { if (event.data.kind === "error") { running = false; waitingInput = false; imageOperationPending = false; status.textContent = `Worker failed: ${event.data.message}`; status.dataset.state = "error"; setControls(); return; } if (event.data.kind === "ready") { workerReady = true; renderFeatures(event.data.results); if (!preserveDiagnosticStatus) { status.textContent = "Browser runtime ready"; status.dataset.state = "ready"; } setControls(); return; } if (event.data.kind === "translation-benchmark") { return; } if (event.data.kind === "filesystem-image") { if (event.data.requestId !== imageRequestId) { return; } status.dataset.imageRequestId = String(event.data.requestId); if (event.data.operation === "export") { const url = URL.createObjectURL( new Blob([event.data.snapshot as Uint8Array<ArrayBuffer>], { type: "application/vnd.binarrow.project", }), ); const download = document.createElement("a"); download.href = url; download.download = "binarrow-project.binarrow-project"; download.click(); URL.revokeObjectURL(url); status.textContent = "Project snapshot exported"; } else { status.textContent = event.data.operation === "replace" ? "Project snapshot imported" : event.data.operation === "install" ? "Project image installed" : event.data.operation === "clear-cache" ? "Build cache cleared" : "Project source saved"; } status.dataset.state = "ready"; imageOperationPending = false; setControls(); return; } if (event.data.runId !== runId) { return; } waitingInput = event.data.execution.outcome === "waiting-input"; running = waitingInput; renderExecution(event.data.execution); setControls(); if (waitingInput) { terminalInput.focus(); } }); worker.addEventListener("error", (event) => { running = false; waitingInput = false; imageOperationPending = false; status.textContent = `Worker failed: ${event.message}`; status.dataset.state = "error"; setControls(); });}
async function createRunCommand( nextRunId: number, limits: ExecutionLimits,): Promise<WorkerCommand> { const target = fixture.value as RunTarget; if (target !== "uploaded-program") { return { kind: "run", runId: nextRunId, fixture: target as FixtureName, limits, }; } const file = programElf.files?.[0]; if (!file) { throw new Error("choose an AArch64 ELF executable first"); } if (!programArgv0.value || programArgv0.value.includes("\0")) { throw new Error("argv[0] must be non-empty and must not contain a NUL byte"); } const arguments_ = programArguments.value .split(/\r?\n/) .filter((argument) => argument.length > 0); if (arguments_.some((argument) => argument.includes("\0"))) { throw new Error("arguments must not contain NUL bytes"); } const environment = programEnvironment.value .split(/\r?\n/) .filter((entry) => entry.length > 0); if ( environment.some( (entry) => entry.includes("\0") || !/^[^=]+=/.test(entry), ) ) { throw new Error("environment entries must be name=value without NUL bytes"); } return { kind: "run-program", runId: nextRunId, executable: new Uint8Array(await file.arrayBuffer()), argv0: programArgv0.value, arguments: arguments_, environment, limits, };}
fixture.addEventListener("change", updateProgramControls);
startButton.addEventListener("click", () => { void (async () => { try { const limits = readLimits(); const nextRunId = runId + 1; const command = await createRunCommand(nextRunId, limits); preserveDiagnosticStatus = false; running = true; waitingInput = false; runId = nextRunId; clearExecution(); status.textContent = "Guest running…"; status.dataset.state = "running"; setControls(); if (command.kind === "run-program") { worker.postMessage(command, [command.executable.buffer as ArrayBuffer]); } else { worker.postMessage(command); } } catch (error) { running = false; waitingInput = false; status.textContent = `Cannot start guest: ${error instanceof Error ? error.message : String(error)}`; status.dataset.state = "error"; setControls(); } })();});
stopButton.addEventListener("click", () => { if (!running) { return; } worker.terminate(); running = false; waitingInput = false; preserveDiagnosticStatus = true; status.textContent = "Execution stopped by user"; status.dataset.state = "error"; renderDiagnostic({ code: "stopped.user", message: "the Worker was terminated by the user", }); setControls(); // Give the browser a task boundary to reclaim a compiler Worker's large // Wasm state before constructing its replacement. window.setTimeout(createWorker, 0);});
function sendTerminalInput(): void { if (!waitingInput) { return; } waitingInput = false; status.textContent = "Guest running…"; status.dataset.state = "running"; setControls(); const command: WorkerCommand = { kind: "input", runId, input: `${terminalInput.value}\n`, endOfFile: false, }; terminalInput.value = ""; worker.postMessage(command);}
sendInputButton.addEventListener("click", sendTerminalInput);terminalInput.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); sendTerminalInput(); }});
async function importProjectImage(mode: "replace" | "install"): Promise<void> { const file = projectImage.files?.[0]; if (!file) { status.textContent = "Choose a project image first"; status.dataset.state = "error"; return; } imageOperationPending = true; status.textContent = mode === "replace" ? "Importing project snapshot…" : "Installing project image…"; status.dataset.state = "running"; setControls(); imageRequestId += 1; try { const command: WorkerCommand = { kind: "filesystem-import", requestId: imageRequestId, maxFilesystemBytes: parseLimit(filesystemLimit), mode, snapshot: new Uint8Array(await file.arrayBuffer()), }; worker.postMessage(command, [command.snapshot.buffer as ArrayBuffer]); } catch (error) { imageOperationPending = false; status.textContent = "Cannot read project image: " + (error instanceof Error ? error.message : String(error)); status.dataset.state = "error"; setControls(); }}
replaceImageButton.addEventListener("click", () => { void importProjectImage("replace");});installImageButton.addEventListener("click", () => { void importProjectImage("install");});exportImageButton.addEventListener("click", () => { imageOperationPending = true; status.textContent = "Exporting project snapshot…"; status.dataset.state = "running"; setControls(); imageRequestId += 1; const command: WorkerCommand = { kind: "filesystem-export", requestId: imageRequestId, maxFilesystemBytes: parseLimit(filesystemLimit), }; worker.postMessage(command);});
clearBuildCacheButton.addEventListener("click", () => { imageOperationPending = true; status.textContent = "Clearing build cache…"; status.dataset.state = "running"; setControls(); imageRequestId += 1; const command: WorkerCommand = { kind: "filesystem-clear-cache", requestId: imageRequestId, maxFilesystemBytes: parseLimit(filesystemLimit), }; worker.postMessage(command);});
saveSourceButton.addEventListener("click", () => { imageOperationPending = true; status.textContent = "Saving project source…"; status.dataset.state = "running"; setControls(); imageRequestId += 1; try { const command: WorkerCommand = { kind: "filesystem-write", requestId: imageRequestId, maxFilesystemBytes: parseLimit(filesystemLimit), path: "/project/main.c", contents: new TextEncoder().encode(projectSource.value), }; worker.postMessage(command, [command.contents.buffer as ArrayBuffer]); } catch (error) { imageOperationPending = false; status.textContent = "Cannot save project source: " + (error instanceof Error ? error.message : String(error)); status.dataset.state = "error"; setControls(); }});
updateProgramControls();createWorker();