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:
parent
59730f982f
commit
3cb3877d98
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,7 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm
|
||||
.netrc
|
|
@ -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"
|
||||
)
|
||||
]
|
||||
)
|
|
@ -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.
|
|
@ -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)
|
|
@ -0,0 +1,4 @@
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import XCTest
|
||||
|
||||
final class BasicTests: XCTestCase {
|
||||
func testAdd() {
|
||||
XCTAssertEqual(1 + 1, 2)
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
startWasiTask().catch(handleError);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
window.addEventListener("error", (event) => {
|
||||
handleError(event.error);
|
||||
});
|
||||
window.addEventListener("unhandledrejection", (event) => {
|
||||
handleError(event.reason);
|
||||
});
|
||||
await startWasiTask();
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
|
@ -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()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
startWasiTask().catch(handleError);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
await startWasiTask();
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
|
@ -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",
|
||||
|
|
|
@ -23,9 +23,10 @@
|
|||
"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",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue