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.
}
const wasmRunner = WasmRunner(false, runtimeConstructor);
const wasmRunner = WasmRunner({
onStdoutLine(line) {
console.log(line);
},
onStderrLine(line) {
console.error(line);
}
}, runtimeConstructor);
// Instantiate the WebAssembly file
const wasmBytes = new Uint8Array(responseArrayBuffer).buffer;
await wasmRunner.run(wasmBytes);
};
function handleError(e: any) {
console.error(e);
if (e instanceof WebAssembly.RuntimeError) {
console.log(e.stack);
}
async function main(): Promise<void> {
await startWasiTask();
}
try {
startWasiTask().catch(handleError);
} catch (e) {
handleError(e);
}
main();

View File

@ -15,17 +15,42 @@
import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from "@bjorn3/browser_wasi_shim";
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 = {
args?: string[];
onStdout?: (text: string) => void;
onStderr?: (text: string) => void;
onStdout?: (chunk: Uint8Array) => void;
onStdoutLine?: (line: string) => void;
onStderr?: (chunk: Uint8Array) => void;
onStderrLine?: (line: string) => void;
};
export type WasmRunner = {
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);
let swift: SwiftRuntime;
@ -33,17 +58,29 @@ export const WasmRunner = (rawOptions: Options | false, SwiftRuntime: SwiftRunti
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 fds = [
new OpenFile(new File([])), // stdin
ConsoleStdout.lineBuffered((stdout) => {
console.log(stdout);
options.onStdout?.call(undefined, stdout);
}),
ConsoleStdout.lineBuffered((stderr) => {
console.error(stderr);
options.onStderr?.call(undefined, stderr);
}),
stdout,
stderr,
new PreopenDirectory("/", new Map()),
];
@ -129,15 +166,8 @@ export const WasmRunner = (rawOptions: Options | false, SwiftRuntime: SwiftRunti
};
};
const defaultRunnerOptions = (options: Options | false): Options => {
if (!options) return defaultRunnerOptions({});
if (!options.onStdout) {
options.onStdout = () => { };
}
if (!options.onStderr) {
options.onStderr = () => { };
}
if (!options.args) {
const defaultRunnerOptions = (options: Options): Options => {
if (options.args != null) {
options.args = ["main.wasm"];
}
return options;

View File

@ -44,17 +44,12 @@ const startWasiTask = async () => {
const wasmRunner = WasmRunner(
{
onStderr() {
const prevLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 1000;
socket.send(
JSON.stringify({
kind: "stackTrace",
stackTrace: new Error().stack,
})
);
Error.stackTraceLimit = prevLimit;
onStdoutLine(line) {
console.log(line);
},
onStderrLine(line) {
console.error(line);
}
},
runtimeConstructor
);
@ -65,14 +60,32 @@ const startWasiTask = async () => {
};
function handleError(e: any) {
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,
})
);
}
}
}
async function main(): Promise<void> {
try {
startWasiTask().catch(handleError);
window.addEventListener("error", (event) => {
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 = "";
const wasmRunner = WasmRunner(
{
onStdout: (text) => {
testRunOutput += text + "\n";
onStdoutLine: (line) => {
console.log(line);
testRunOutput += line + "\n";
},
onStderr: () => {
socket.send(
JSON.stringify({
kind: "stackTrace",
stackTrace: new Error().stack,
})
);
onStderrLine: (line) => {
console.error(line);
},
},
runtimeConstructor
@ -109,14 +105,33 @@ const startWasiTask = async () => {
function handleError(e: any) {
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()
})
);
}
async function main(): Promise<void> {
try {
startWasiTask().catch(handleError);
await startWasiTask();
} catch (e) {
handleError(e);
}
}
main();

13
package-lock.json generated
View File

@ -11,6 +11,7 @@
"devDependencies": {
"@bjorn3/browser_wasi_shim": "^0.3.0",
"@types/node": "^20.12.7",
"@types/reconnectingwebsocket": "^1.0.10",
"esbuild": "^0.14.38",
"npm-run-all": "^4.1.5",
"reconnecting-websocket": "^4.4.0",
@ -32,6 +33,12 @@
"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": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@ -1318,6 +1325,12 @@
"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": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",

View File

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