Send stdout/stderr from WasmRunner to dev server without decoding as UTF-8 (#471)

* Enable WasmRunner to handle stdout as raw binary

* catch other errors

* move devDeps

* use browser exception handling

* comment out
This commit is contained in:
omochimetaru 2024-05-25 01:33:13 +09:00 committed by GitHub
parent 59730f982f
commit 3cb3877d98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 224 additions and 69 deletions

File diff suppressed because one or more lines are too long

7
Tests/Fixtures/SandboxApp/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm
.netrc

View File

@ -0,0 +1,28 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "DevServerTestApp",
products: [
.executable(name: "app", targets: ["app"])
],
dependencies: [
.package(path: "../../.."),
.package(url: "https://github.com/swiftwasm/JavaScriptKit", from: "0.19.2")
],
targets: [
.executableTarget(
name: "app",
dependencies: [
.product(name: "JavaScriptKit", package: "JavaScriptKit")
],
resources: [
.copy("style.css")
]
),
.testTarget(
name: "SimpleTests"
)
]
)

View File

@ -0,0 +1,4 @@
This application serves as a working environment for experimenting with the behavior of "carton".
Since it is not used by automated tests, it can be easily modified.
If you want to include the behavior created here in automated tests,
please separate the target application for testing.

View File

@ -0,0 +1,33 @@
#if os(WASI)
import WASILibc
typealias FILEPointer = OpaquePointer
#else
import Darwin
typealias FILEPointer = UnsafeMutablePointer<FILE>
#endif
import JavaScriptKit
func fputs(_ string: String, file: FILEPointer) {
_ = string.withCString { (cstr) in
fputs(cstr, file)
}
}
fputs("hello stdout\n", file: stdout)
fputs("hello stderr\n", file: stderr)
//fatalError("hello fatalError")
let document = JSObject.global.document
let button = document.createElement("button")
_ = button.appendChild(
document.createTextNode("click to crash")
)
_ = button.addEventListener("click", JSClosure { (e) in
fatalError("crash")
})
_ = document.body.appendChild(button)

View File

@ -0,0 +1,4 @@
* {
margin: 0;
padding: 0;
}

View File

@ -0,0 +1,7 @@
import XCTest
final class BasicTests: XCTestCase {
func testAdd() {
XCTAssertEqual(1 + 1, 2)
}
}

View File

@ -31,22 +31,22 @@ const startWasiTask = async () => {
// JavaScriptKit module not available, running without JavaScriptKit runtime. // JavaScriptKit module not available, running without JavaScriptKit runtime.
} }
const wasmRunner = WasmRunner(false, runtimeConstructor); const wasmRunner = WasmRunner({
onStdoutLine(line) {
console.log(line);
},
onStderrLine(line) {
console.error(line);
}
}, runtimeConstructor);
// Instantiate the WebAssembly file // Instantiate the WebAssembly file
const wasmBytes = new Uint8Array(responseArrayBuffer).buffer; const wasmBytes = new Uint8Array(responseArrayBuffer).buffer;
await wasmRunner.run(wasmBytes); await wasmRunner.run(wasmBytes);
}; };
function handleError(e: any) { async function main(): Promise<void> {
console.error(e); await startWasiTask();
if (e instanceof WebAssembly.RuntimeError) {
console.log(e.stack);
}
} }
try { main();
startWasiTask().catch(handleError);
} catch (e) {
handleError(e);
}

View File

@ -15,17 +15,42 @@
import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from "@bjorn3/browser_wasi_shim"; import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from "@bjorn3/browser_wasi_shim";
import type { SwiftRuntime, SwiftRuntimeConstructor } from "./JavaScriptKit_JavaScriptKit.resources/Runtime"; import type { SwiftRuntime, SwiftRuntimeConstructor } from "./JavaScriptKit_JavaScriptKit.resources/Runtime";
export class LineDecoder {
constructor(onLine: (line: string) => void) {
this.decoder = new TextDecoder("utf-8", { fatal: false });
this.buffer = "";
this.onLine = onLine;
}
private decoder: TextDecoder;
private buffer: string;
private onLine: (line: string) => void;
send(chunk: Uint8Array) {
this.buffer += this.decoder.decode(chunk, { stream: true });
const lines = this.buffer.split("\n");
for (let i = 0; i < lines.length - 1; i++) {
this.onLine(lines[i]);
}
this.buffer = lines[lines.length - 1];
}
}
export type Options = { export type Options = {
args?: string[]; args?: string[];
onStdout?: (text: string) => void; onStdout?: (chunk: Uint8Array) => void;
onStderr?: (text: string) => void; onStdoutLine?: (line: string) => void;
onStderr?: (chunk: Uint8Array) => void;
onStderrLine?: (line: string) => void;
}; };
export type WasmRunner = { export type WasmRunner = {
run(wasmBytes: ArrayBufferLike, extraWasmImports?: WebAssembly.Imports): Promise<void> run(wasmBytes: ArrayBufferLike, extraWasmImports?: WebAssembly.Imports): Promise<void>
}; };
export const WasmRunner = (rawOptions: Options | false, SwiftRuntime: SwiftRuntimeConstructor | undefined): WasmRunner => { export const WasmRunner = (rawOptions: Options, SwiftRuntime: SwiftRuntimeConstructor | undefined): WasmRunner => {
const options: Options = defaultRunnerOptions(rawOptions); const options: Options = defaultRunnerOptions(rawOptions);
let swift: SwiftRuntime; let swift: SwiftRuntime;
@ -33,17 +58,29 @@ export const WasmRunner = (rawOptions: Options | false, SwiftRuntime: SwiftRunti
swift = new SwiftRuntime(); swift = new SwiftRuntime();
} }
let stdoutLine: LineDecoder | undefined = undefined;
if (options.onStdoutLine != null) {
stdoutLine = new LineDecoder(options.onStdoutLine);
}
const stdout = new ConsoleStdout((chunk) => {
options.onStdout?.call(undefined, chunk);
stdoutLine?.send(chunk);
});
let stderrLine: LineDecoder | undefined = undefined;
if (options.onStderrLine != null) {
stderrLine = new LineDecoder(options.onStderrLine);
}
const stderr = new ConsoleStdout((chunk) => {
options.onStderr?.call(undefined, chunk);
stderrLine?.send(chunk);
});
const args = options.args || []; const args = options.args || [];
const fds = [ const fds = [
new OpenFile(new File([])), // stdin new OpenFile(new File([])), // stdin
ConsoleStdout.lineBuffered((stdout) => { stdout,
console.log(stdout); stderr,
options.onStdout?.call(undefined, stdout);
}),
ConsoleStdout.lineBuffered((stderr) => {
console.error(stderr);
options.onStderr?.call(undefined, stderr);
}),
new PreopenDirectory("/", new Map()), new PreopenDirectory("/", new Map()),
]; ];
@ -129,15 +166,8 @@ export const WasmRunner = (rawOptions: Options | false, SwiftRuntime: SwiftRunti
}; };
}; };
const defaultRunnerOptions = (options: Options | false): Options => { const defaultRunnerOptions = (options: Options): Options => {
if (!options) return defaultRunnerOptions({}); if (options.args != null) {
if (!options.onStdout) {
options.onStdout = () => { };
}
if (!options.onStderr) {
options.onStderr = () => { };
}
if (!options.args) {
options.args = ["main.wasm"]; options.args = ["main.wasm"];
} }
return options; return options;

View File

@ -44,17 +44,12 @@ const startWasiTask = async () => {
const wasmRunner = WasmRunner( const wasmRunner = WasmRunner(
{ {
onStderr() { onStdoutLine(line) {
const prevLimit = Error.stackTraceLimit; console.log(line);
Error.stackTraceLimit = 1000;
socket.send(
JSON.stringify({
kind: "stackTrace",
stackTrace: new Error().stack,
})
);
Error.stackTraceLimit = prevLimit;
}, },
onStderrLine(line) {
console.error(line);
}
}, },
runtimeConstructor runtimeConstructor
); );
@ -65,14 +60,32 @@ const startWasiTask = async () => {
}; };
function handleError(e: any) { function handleError(e: any) {
console.error(e); if (e instanceof Error) {
if (e instanceof WebAssembly.RuntimeError) { const stack = e.stack;
console.log(e.stack); if (stack != null) {
socket.send(
JSON.stringify({
kind: "stackTrace",
stackTrace: stack,
})
);
}
} }
} }
try { async function main(): Promise<void> {
startWasiTask().catch(handleError); try {
} catch (e) { window.addEventListener("error", (event) => {
handleError(e); handleError(event.error);
});
window.addEventListener("unhandledrejection", (event) => {
handleError(event.reason);
});
await startWasiTask();
} catch (e) {
handleError(e);
throw e;
}
} }
main();

View File

@ -45,16 +45,12 @@ const startWasiTask = async () => {
let testRunOutput = ""; let testRunOutput = "";
const wasmRunner = WasmRunner( const wasmRunner = WasmRunner(
{ {
onStdout: (text) => { onStdoutLine: (line) => {
testRunOutput += text + "\n"; console.log(line);
testRunOutput += line + "\n";
}, },
onStderr: () => { onStderrLine: (line) => {
socket.send( console.error(line);
JSON.stringify({
kind: "stackTrace",
stackTrace: new Error().stack,
})
);
}, },
}, },
runtimeConstructor runtimeConstructor
@ -109,14 +105,33 @@ const startWasiTask = async () => {
function handleError(e: any) { function handleError(e: any) {
console.error(e); console.error(e);
if (e instanceof WebAssembly.RuntimeError) {
console.log(e.stack); if (e instanceof Error) {
const stack = e.stack;
if (stack != null) {
socket.send(
JSON.stringify({
kind: "stackTrace",
stackTrace: stack,
})
);
}
} }
socket.send(JSON.stringify({ kind: "errorReport", errorReport: e.toString() }));
socket.send(
JSON.stringify({
kind: "errorReport",
errorReport: e.toString()
})
);
} }
try { async function main(): Promise<void> {
startWasiTask().catch(handleError); try {
} catch (e) { await startWasiTask();
handleError(e); } catch (e) {
handleError(e);
}
} }
main();

13
package-lock.json generated
View File

@ -11,6 +11,7 @@
"devDependencies": { "devDependencies": {
"@bjorn3/browser_wasi_shim": "^0.3.0", "@bjorn3/browser_wasi_shim": "^0.3.0",
"@types/node": "^20.12.7", "@types/node": "^20.12.7",
"@types/reconnectingwebsocket": "^1.0.10",
"esbuild": "^0.14.38", "esbuild": "^0.14.38",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
@ -32,6 +33,12 @@
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"node_modules/@types/reconnectingwebsocket": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@types/reconnectingwebsocket/-/reconnectingwebsocket-1.0.10.tgz",
"integrity": "sha512-30Pq4D3o8BKcdY53dzr0elGFyB/ChYpGrHiRH/GuaZKXXGWq/CsD1QBEu1b8IgdHReOKpo9tjk80UaxSbuXoTQ==",
"dev": true
},
"node_modules/ansi-styles": { "node_modules/ansi-styles": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@ -1318,6 +1325,12 @@
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"@types/reconnectingwebsocket": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@types/reconnectingwebsocket/-/reconnectingwebsocket-1.0.10.tgz",
"integrity": "sha512-30Pq4D3o8BKcdY53dzr0elGFyB/ChYpGrHiRH/GuaZKXXGWq/CsD1QBEu1b8IgdHReOKpo9tjk80UaxSbuXoTQ==",
"dev": true
},
"ansi-styles": { "ansi-styles": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",

View File

@ -23,9 +23,10 @@
"devDependencies": { "devDependencies": {
"@bjorn3/browser_wasi_shim": "^0.3.0", "@bjorn3/browser_wasi_shim": "^0.3.0",
"@types/node": "^20.12.7", "@types/node": "^20.12.7",
"@types/reconnectingwebsocket": "^1.0.10",
"esbuild": "^0.14.38", "esbuild": "^0.14.38",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
"typescript": "^5.4.5" "typescript": "^5.4.5"
} }
} }