-
-
Notifications
You must be signed in to change notification settings - Fork 34.4k
gh-146416: Emscripten: Improve standard stream handling in node_entry.mjs #146417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
1e4e9c1
gh-146416: Emscripten: Improve standard stream handling in node_entry…
hoodmane 01f9307
More accurate arg name
hoodmane 5473107
Remove debug log
hoodmane 34fc32a
More consistent whitespace
hoodmane 00efa5f
Add nodeFsync definition
hoodmane 1a81834
Fix: rows first then columns
hoodmane 245cb86
Apply suggestions from code review
hoodmane 36a307e
Sleep only 10ms
hoodmane File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,241 @@ | ||
| /** | ||
| * This is a pared down version of | ||
| * https://github.com/pyodide/pyodide/blob/main/src/js/streams.ts | ||
| * | ||
| * It replaces the standard streams devices that Emscripten provides with our | ||
| * own better ones. It fixes the following deficiencies: | ||
| * | ||
| * 1. The emscripten std streams always have isatty set to true. These set | ||
| * isatty to match the value for the stdin/stdout/stderr that node sees. | ||
| * 2. The emscripten std streams don't support the ttygetwinsize ioctl. If | ||
| * isatty() returns true, then these do, and it returns the actual window | ||
| * size as the OS reports it to Node. | ||
| * 3. The emscripten std streams introduce an extra layer of buffering which has | ||
| * to be flushed with fsync(). | ||
| * 4. The emscripten std streams are slow and complex because they go through a | ||
| * character-based handler layer. This is particularly awkward because both | ||
| * sides of this character based layer deal with buffers and so we need | ||
| * complex adaptors, buffering, etc on both sides. Removing this | ||
| * character-based middle layer makes everything better. | ||
| * https://github.com/emscripten-core/emscripten/blob/1aa7fb531f11e11e7ae49b75a24e1a8fe6fa4a7d/src/lib/libtty.js?plain=1#L104-L114 | ||
| * | ||
| * Ideally some version of this should go upstream to Emscripten since it is not | ||
| * in any way specific to Python. But I haven't gotten around to it yet. | ||
| */ | ||
|
|
||
| import * as tty from "node:tty"; | ||
| import * as fs from "node:fs"; | ||
|
|
||
| let FS; | ||
| const DEVOPS = {}; | ||
| const DEVS = {}; | ||
|
|
||
| function isErrnoError(e) { | ||
| return e && typeof e === "object" && "errno" in e; | ||
| } | ||
|
|
||
| const waitBuffer = new Int32Array( | ||
| new WebAssembly.Memory({ shared: true, initial: 1, maximum: 1 }).buffer, | ||
| ); | ||
| function syncSleep(timeout) { | ||
| try { | ||
| Atomics.wait(waitBuffer, 0, 0, timeout); | ||
| return true; | ||
| } catch (_) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Calls the callback and handle node EAGAIN errors. | ||
| */ | ||
| function handleEAGAIN(cb) { | ||
| while (true) { | ||
| try { | ||
| return cb(); | ||
| } catch (e) { | ||
| if (e && e.code === "EAGAIN") { | ||
| // Presumably this means we're in node and tried to read from/write to | ||
| // an O_NONBLOCK file descriptor. Synchronously sleep for 100ms as | ||
| // requested by EAGAIN and try again. In case for some reason we fail to | ||
| // sleep, propagate the error (it will turn into an EOFError). | ||
| if (syncSleep(100)) { | ||
hoodmane marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| continue; | ||
| } | ||
| } | ||
| throw e; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function readWriteHelper(stream, cb, method) { | ||
| let nbytes; | ||
| try { | ||
| nbytes = handleEAGAIN(cb); | ||
| } catch (e) { | ||
| if (e && e.code && Module.ERRNO_CODES[e.code]) { | ||
| throw new FS.ErrnoError(Module.ERRNO_CODES[e.code]); | ||
| } | ||
| if (isErrnoError(e)) { | ||
| // the handler set an errno, propagate it | ||
| throw e; | ||
| } | ||
| console.error("Error thrown in read:"); | ||
| console.error(e); | ||
| throw new FS.ErrnoError(Module.ERRNO_CODES.EIO); | ||
| } | ||
| if (nbytes === undefined) { | ||
| // Prevent an infinite loop caused by incorrect code that doesn't return a | ||
| // value | ||
hoodmane marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // Maybe we should set nbytes = buffer.length here instead? | ||
| console.warn( | ||
| `${method} returned undefined; a correct implementation must return a number`, | ||
| ); | ||
| throw new FS.ErrnoError(Module.ERRNO_CODES.EIO); | ||
| } | ||
| if (nbytes !== 0) { | ||
| stream.node.timestamp = Date.now(); | ||
| } | ||
| return nbytes; | ||
| } | ||
|
|
||
| function asUint8Array(arg) { | ||
| if (ArrayBuffer.isView(arg)) { | ||
| return new Uint8Array(arg.buffer, arg.byteOffset, arg.byteLength); | ||
| } else { | ||
| return new Uint8Array(arg); | ||
| } | ||
| } | ||
|
|
||
| const prepareBuffer = (buffer, offset, length) => | ||
| asUint8Array(buffer).subarray(offset, offset + length); | ||
|
|
||
| const TTY_OPS = { | ||
| ioctl_tiocgwinsz(tty) { | ||
| return tty.devops.ioctl_tiocgwinsz?.(); | ||
| }, | ||
| }; | ||
|
|
||
| const stream_ops = { | ||
| open: function (stream) { | ||
| const devops = DEVOPS[stream.node.rdev]; | ||
| if (!devops) { | ||
| throw new FS.ErrnoError(Module.ERRNO_CODES.ENODEV); | ||
| } | ||
| stream.devops = devops; | ||
| stream.tty = stream.devops.isatty ? { ops: TTY_OPS, devops } : undefined; | ||
| stream.seekable = false; | ||
| }, | ||
| close: function (stream) { | ||
| // flush any pending line data | ||
| stream.stream_ops.fsync(stream); | ||
| }, | ||
| fsync: function (stream) { | ||
| const ops = stream.devops; | ||
| if (ops.fsync) { | ||
| ops.fsync(); | ||
| } | ||
| }, | ||
| read: function (stream, buffer, offset, length, pos /* ignored */) { | ||
| buffer = prepareBuffer(buffer, offset, length); | ||
| return readWriteHelper(stream, () => stream.devops.read(buffer), "read"); | ||
| }, | ||
| write: function (stream, buffer, offset, length, pos /* ignored */) { | ||
| buffer = prepareBuffer(buffer, offset, length); | ||
| return readWriteHelper(stream, () => stream.devops.write(buffer), "write"); | ||
| }, | ||
| }; | ||
|
|
||
| function nodeFsync(fd) { | ||
| try { | ||
| fs.fsyncSync(fd); | ||
| } catch (e) { | ||
| if (e?.code === "EINVAL") { | ||
| return; | ||
| } | ||
| // On Mac, calling fsync when not isatty returns ENOTSUP | ||
| // On Windows, stdin/stdout/stderr may be closed, returning EBADF or EPERM | ||
| if ( | ||
| e?.code === "ENOTSUP" || e?.code === "EBADF" || e?.code === "EPERM" | ||
| ) { | ||
| return; | ||
| } | ||
|
|
||
| throw e; | ||
| } | ||
| } | ||
|
|
||
| class NodeReader { | ||
| constructor(nodeStream) { | ||
| this.nodeStream = nodeStream; | ||
| this.isatty = tty.isatty(nodeStream.fd); | ||
| } | ||
|
|
||
| read(buffer) { | ||
| try { | ||
| return fs.readSync(this.nodeStream.fd, buffer); | ||
| } catch (e) { | ||
| // Platform differences: on Windows, reading EOF throws an exception, | ||
| // but on other OSes, reading EOF returns 0. Uniformize behavior by | ||
| // catching the EOF exception and returning 0. | ||
| if (e.toString().includes("EOF")) { | ||
| return 0; | ||
| } | ||
| throw e; | ||
| } | ||
| } | ||
|
|
||
| fsync() { | ||
| nodeFsync(this.nodeStream.fd); | ||
| } | ||
| } | ||
|
|
||
| class NodeWriter { | ||
| constructor(nodeStream) { | ||
| this.nodeStream = nodeStream; | ||
| this.isatty = tty.isatty(nodeStream.fd); | ||
| } | ||
|
|
||
| write(buffer) { | ||
| return fs.writeSync(this.nodeStream.fd, buffer); | ||
| } | ||
|
|
||
| fsync() { | ||
| nodeFsync(this.nodeStream.fd); | ||
| } | ||
|
|
||
| ioctl_tiocgwinsz() { | ||
| return [this.nodeStream.rows ?? 24, this.nodeStream.columns ?? 80]; | ||
| } | ||
| } | ||
|
|
||
| export function initializeStreams(fsarg) { | ||
| FS = fsarg; | ||
| const major = FS.createDevice.major++; | ||
| DEVS.stdin = FS.makedev(major, 0); | ||
| DEVS.stdout = FS.makedev(major, 1); | ||
| DEVS.stderr = FS.makedev(major, 2); | ||
|
|
||
| FS.registerDevice(DEVS.stdin, stream_ops); | ||
| FS.registerDevice(DEVS.stdout, stream_ops); | ||
| FS.registerDevice(DEVS.stderr, stream_ops); | ||
|
|
||
| FS.unlink("/dev/stdin"); | ||
| FS.unlink("/dev/stdout"); | ||
| FS.unlink("/dev/stderr"); | ||
|
|
||
| FS.mkdev("/dev/stdin", DEVS.stdin); | ||
| FS.mkdev("/dev/stdout", DEVS.stdout); | ||
| FS.mkdev("/dev/stderr", DEVS.stderr); | ||
|
|
||
| DEVOPS[DEVS.stdin] = new NodeReader(process.stdin); | ||
| DEVOPS[DEVS.stdout] = new NodeWriter(process.stdout); | ||
| DEVOPS[DEVS.stderr] = new NodeWriter(process.stderr); | ||
|
|
||
| FS.closeStream(0 /* stdin */); | ||
| FS.closeStream(1 /* stdout */); | ||
| FS.closeStream(2 /* stderr */); | ||
| FS.open("/dev/stdin", 0 /* O_RDONLY */); | ||
| FS.open("/dev/stdout", 1 /* O_WRONLY */); | ||
| FS.open("/dev/stderr", 1 /* O_WRONLY */); | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.