diff --git a/src/lib/libdev.js b/src/lib/libdev.js new file mode 100644 index 0000000000000..05847b7fcb5c0 --- /dev/null +++ b/src/lib/libdev.js @@ -0,0 +1,144 @@ +/** + * @license + * Copyright 2013 The Emscripten Authors + * SPDX-License-Identifier: MIT + */ + +addToLibrary({ + $DEV__deps: [ + '$FS', + '$ERRNO_CODES', +#if ENVIRONMENT_MAY_BE_NODE + '$nodeTTY', + '$nodeFsync', +#endif + ], + $DEV: { + readWriteHelper: (stream, cb, method) => { + try { + var nbytes = cb(); + } catch (e) { + // Convert Node errors into ErrnoError + if (e && e.code && ERRNO_CODES[e.code]) { + throw new FS.ErrnoError(ERRNO_CODES[e.code]); + } + if (e?.errno) { + // propagate errno + throw e; + } + // Other errors converted to EIO. +#if ASSERTIONS + console.error(`Error thrown in ${method}:`); + console.error(e); +#endif + throw new FS.ErrnoError({{{ cDefs.EIO }}}); + } + if (nbytes === undefined) { + // Prevent an infinite loop caused by incorrect code that doesn't return a + // value + // Maybe we should set nbytes = buffer.length here instead? +#if ASSERTIONS + console.warn( + `${method} returned undefined; a correct implementation must return a number`, + ); +#endif + throw new FS.ErrnoError({{{ cDefs.EIO }}}); + } + if (nbytes !== 0) { + stream.node.timestamp = Date.now(); + } + return nbytes; + }, + devs: [], + register(dev, ops) { + DEV.devs[dev] = ops; + FS.registerDevice(dev, DEV.stream_ops); + }, + TTY_OPS: { + ioctl_tiocgwinsz(tty) { + const { rows = 24, columns = 80 } = tty.devops.getTerminalSize?.() ?? {}; + return [rows, columns]; + }, + }, + stream_ops: { + open(stream) { + var devops = DEV.devs[stream.node.rdev]; + if (!devops) { + throw new FS.ErrnoError({{{ cDefs.ENODEV }}}); + } + stream.devops = devops; + stream.seekable = false; + stream.tty = + stream.devops.tty ?? + (stream.devops.isatty + ? { + ops: DEV.TTY_OPS, + devops, + } + : undefined); + devops.open?.(stream); + }, + close(stream) { + // flush any pending line data + stream.stream_ops.fsync(stream); + }, + fsync(stream) { + stream.devops.fsync?.(stream.devops); + }, + read: function (stream, buffer, offset, length, pos /* ignored */) { + buffer = buffer.subarray(offset, offset + length); + return DEV.readWriteHelper(stream, () => stream.devops.read(stream.devops, buffer), "read"); + }, + write: function (stream, buffer, offset, length, pos /* ignored */) { + buffer = buffer.subarray(offset, offset + length); + return DEV.readWriteHelper(stream, () => stream.devops.write(stream.devops, buffer), "write"); + }, + }, +#if ENVIRONMENT_MAY_BE_NODE + nodeInputDevice: (nodeStream) => ({ + isatty: nodeTTY.isatty(nodeStream.fd), + fsync() { + nodeFsync(nodeStream.fd); + }, + read(ops, buffer) { + return fs.readSync(nodeStream.fd, buffer, 0, buffer.length); + }, + }), + nodeOutputDevice: (nodeStream) => ({ + isatty: nodeTTY.isatty(nodeStream.fd), + fsync() { + nodeFsync(nodeStream.fd); + }, + write(ops, buffer) { + return fs.writeSync(nodeStream.fd, buffer, 0, buffer.length); + }, + getTerminalSize() { + return nodeStream; + } + }), +#endif + }, +#if ENVIRONMENT_MAY_BE_NODE + $nodeTTY: "require('node:tty');", + $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 + const isStdStream = fd === 0 || fd === 1 || fd === 2; + if ( + isStdStream && + (e?.code === "ENOTSUP" || e?.code === "EBADF" || e?.code === "EPERM") + ) { + return; + } + + throw e; + } + } +#endif +}); diff --git a/src/lib/libfs.js b/src/lib/libfs.js index d148c84f5a2d2..59ad63306d11b 100644 --- a/src/lib/libfs.js +++ b/src/lib/libfs.js @@ -1465,17 +1465,40 @@ FS.staticInit();`; // them instead. if (input) { FS.createDevice('/dev', 'stdin', input); - } else { + } else +#if ENVIRONMENT_MAY_BE_NODE + if (ENVIRONMENT_IS_NODE) { + DEV.register(FS.makedev(7, 0), DEV.nodeInputDevice(process.stdin)); + FS.mkdev('/dev/stdin', FS.makedev(7, 0)); + } else +#endif + { FS.symlink('/dev/tty', '/dev/stdin'); } + if (output) { FS.createDevice('/dev', 'stdout', null, output); - } else { + } else +#if ENVIRONMENT_MAY_BE_NODE + if (ENVIRONMENT_IS_NODE) { + DEV.register(FS.makedev(7, 1), DEV.nodeOutputDevice(process.stdout)); + FS.mkdev('/dev/stdout', FS.makedev(7, 1)); + } else +#endif + { FS.symlink('/dev/tty', '/dev/stdout'); } + if (error) { FS.createDevice('/dev', 'stderr', null, error); - } else { + } else +#if ENVIRONMENT_MAY_BE_NODE + if (ENVIRONMENT_IS_NODE) { + DEV.register(FS.makedev(7, 2), DEV.nodeOutputDevice(process.stderr)); + FS.mkdev('/dev/stderr', FS.makedev(7, 2)); + } else +#endif + { FS.symlink('/dev/tty1', '/dev/stderr'); } diff --git a/src/lib/libtty.js b/src/lib/libtty.js index ba392b12c32ff..0568c5084cad0 100644 --- a/src/lib/libtty.js +++ b/src/lib/libtty.js @@ -6,99 +6,48 @@ addToLibrary({ $TTY__deps: [ + '$DEV', '$FS', '$UTF8ArrayToString', - '$FS_stdin_getChar' + '$FS_stdin_getChar', ], -#if !MINIMAL_RUNTIME - $TTY__postset: () => { - addAtInit('TTY.init();'); - addAtExit('TTY.shutdown();'); - }, -#endif $TTY: { - ttys: [], - init() { - // https://github.com/emscripten-core/emscripten/pull/1555 - // if (ENVIRONMENT_IS_NODE) { - // // currently, FS.init does not distinguish if process.stdin is a file or TTY - // // device, it always assumes it's a TTY device. because of this, we're forcing - // // process.stdin to UTF8 encoding to at least make stdin reading compatible - // // with text files until FS.init can be refactored. - // process.stdin.setEncoding('utf8'); - // } - }, - shutdown() { - // https://github.com/emscripten-core/emscripten/pull/1555 - // if (ENVIRONMENT_IS_NODE) { - // // inolen: any idea as to why node -e 'process.stdin.read()' wouldn't exit immediately (with process.stdin being a tty)? - // // isaacs: because now it's reading from the stream, you've expressed interest in it, so that read() kicks off a _read() which creates a ReadReq operation - // // inolen: I thought read() in that case was a synchronous operation that just grabbed some amount of buffered data if it exists? - // // isaacs: it is. but it also triggers a _read() call, which calls readStart() on the handle - // // isaacs: do process.stdin.pause() and i'd think it'd probably close the pending call - // process.stdin.pause(); - // } - }, + ttys: {}, register(dev, ops) { - TTY.ttys[dev] = { input: [], output: [], ops: ops }; - FS.registerDevice(dev, TTY.stream_ops); - }, - stream_ops: { - open(stream) { - var tty = TTY.ttys[stream.node.rdev]; - if (!tty) { - throw new FS.ErrnoError({{{ cDefs.ENODEV }}}); - } - stream.tty = tty; - stream.seekable = false; - }, - close(stream) { - // flush any pending line data - stream.tty.ops.fsync(stream.tty); - }, - fsync(stream) { - stream.tty.ops.fsync(stream.tty); - }, - read(stream, buffer, offset, length, pos /* ignored */) { - if (!stream.tty || !stream.tty.ops.get_char) { - throw new FS.ErrnoError({{{ cDefs.ENXIO }}}); - } - var bytesRead = 0; - for (var i = 0; i < length; i++) { - var result; - try { - result = stream.tty.ops.get_char(stream.tty); - } catch (e) { - throw new FS.ErrnoError({{{ cDefs.EIO }}}); + const tty = { input: [], output: [], ops }; + TTY.ttys[dev] = tty; + const devops = { + tty, + write(devops, buffer) { + if (!ops.put_char) { + throw new FS.ErrnoError({{{ cDefs.ENXIO }}}); } - if (result === undefined && bytesRead === 0) { - throw new FS.ErrnoError({{{ cDefs.EAGAIN }}}); + for (var i = 0; i < buffer.length; i++) { + ops.put_char(tty, buffer[i]); } - if (result === null || result === undefined) break; - bytesRead++; - buffer[offset+i] = result; - } - if (bytesRead) { - stream.node.atime = Date.now(); - } - return bytesRead; - }, - write(stream, buffer, offset, length, pos) { - if (!stream.tty || !stream.tty.ops.put_char) { - throw new FS.ErrnoError({{{ cDefs.ENXIO }}}); - } - try { - for (var i = 0; i < length; i++) { - stream.tty.ops.put_char(stream.tty, buffer[offset+i]); + return i; + }, + read(devops, buffer) { + if (!ops.get_char) { + throw new FS.ErrnoError({{{ cDefs.ENXIO }}}); } - } catch (e) { - throw new FS.ErrnoError({{{ cDefs.EIO }}}); - } - if (length) { - stream.node.mtime = stream.node.ctime = Date.now(); - } - return i; + var bytesRead = 0; + for (var i = 0; i < buffer.length; i++) { + var result = ops.get_char(tty); + if (result === undefined && bytesRead === 0) { + throw new FS.ErrnoError({{{ cDefs.EAGAIN }}}); + } + if (result === null || result === undefined) break; + bytesRead++; + buffer[i] = result; + } + return bytesRead; + }, + }; + if (ops.fsync) { + devops.fsync = (devops) => ops.fsync(tty) } + DEV.register(dev, devops); }, default_tty_ops: { get_char(tty) { diff --git a/src/modules.mjs b/src/modules.mjs index 83fcf1d86d8b3..8b261f0d68a64 100644 --- a/src/modules.mjs +++ b/src/modules.mjs @@ -111,6 +111,7 @@ function calculateLibraries() { 'libfs.js', 'libmemfs.js', 'libtty.js', + 'libdev.js', 'libpipefs.js', // ok to include it by default since it's only used if the syscall is used 'libsockfs.js', // ok to include it by default since it's only used if the syscall is used ); diff --git a/test/codesize/test_codesize_cxx_ctors1.json b/test/codesize/test_codesize_cxx_ctors1.json index dff56a4063fb0..ce2a799f79cba 100644 --- a/test/codesize/test_codesize_cxx_ctors1.json +++ b/test/codesize/test_codesize_cxx_ctors1.json @@ -1,10 +1,10 @@ { - "a.out.js": 19194, - "a.out.js.gz": 7969, + "a.out.js": 21541, + "a.out.js.gz": 9245, "a.out.nodebug.wasm": 132638, "a.out.nodebug.wasm.gz": 49927, - "total": 151832, - "total_gz": 57896, + "total": 154179, + "total_gz": 59172, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_ctors2.json b/test/codesize/test_codesize_cxx_ctors2.json index fb2e77ec4ae92..9d28e6c55f4f7 100644 --- a/test/codesize/test_codesize_cxx_ctors2.json +++ b/test/codesize/test_codesize_cxx_ctors2.json @@ -1,10 +1,10 @@ { - "a.out.js": 19171, - "a.out.js.gz": 7957, + "a.out.js": 21518, + "a.out.js.gz": 9227, "a.out.nodebug.wasm": 132064, "a.out.nodebug.wasm.gz": 49586, - "total": 151235, - "total_gz": 57543, + "total": 153582, + "total_gz": 58813, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_except.json b/test/codesize/test_codesize_cxx_except.json index c29dbd2b26b46..663a6d9bd7f04 100644 --- a/test/codesize/test_codesize_cxx_except.json +++ b/test/codesize/test_codesize_cxx_except.json @@ -1,10 +1,10 @@ { - "a.out.js": 23174, - "a.out.js.gz": 8960, + "a.out.js": 25523, + "a.out.js.gz": 10247, "a.out.nodebug.wasm": 172516, "a.out.nodebug.wasm.gz": 57438, - "total": 195690, - "total_gz": 66398, + "total": 198039, + "total_gz": 67685, "sent": [ "__cxa_begin_catch", "__cxa_end_catch", diff --git a/test/codesize/test_codesize_cxx_except_wasm.json b/test/codesize/test_codesize_cxx_except_wasm.json index cb737cf73678b..c2669158a3a0b 100644 --- a/test/codesize/test_codesize_cxx_except_wasm.json +++ b/test/codesize/test_codesize_cxx_except_wasm.json @@ -1,10 +1,10 @@ { - "a.out.js": 19026, - "a.out.js.gz": 7904, + "a.out.js": 21374, + "a.out.js.gz": 9183, "a.out.nodebug.wasm": 147922, "a.out.nodebug.wasm.gz": 55312, - "total": 166948, - "total_gz": 63216, + "total": 169296, + "total_gz": 64495, "sent": [ "_abort_js", "_tzset_js", diff --git a/test/codesize/test_codesize_cxx_except_wasm_legacy.json b/test/codesize/test_codesize_cxx_except_wasm_legacy.json index 71fbc6b06eb6d..8d6f586f60387 100644 --- a/test/codesize/test_codesize_cxx_except_wasm_legacy.json +++ b/test/codesize/test_codesize_cxx_except_wasm_legacy.json @@ -1,10 +1,10 @@ { - "a.out.js": 19100, - "a.out.js.gz": 7929, + "a.out.js": 21448, + "a.out.js.gz": 9206, "a.out.nodebug.wasm": 145729, "a.out.nodebug.wasm.gz": 54945, - "total": 164829, - "total_gz": 62874, + "total": 167177, + "total_gz": 64151, "sent": [ "_abort_js", "_tzset_js", diff --git a/test/codesize/test_codesize_cxx_lto.json b/test/codesize/test_codesize_cxx_lto.json index 7bea9337c40d2..93b17ae068d85 100644 --- a/test/codesize/test_codesize_cxx_lto.json +++ b/test/codesize/test_codesize_cxx_lto.json @@ -1,10 +1,10 @@ { - "a.out.js": 18563, - "a.out.js.gz": 7666, + "a.out.js": 20913, + "a.out.js.gz": 8928, "a.out.nodebug.wasm": 101956, "a.out.nodebug.wasm.gz": 39460, - "total": 120519, - "total_gz": 47126, + "total": 122869, + "total_gz": 48388, "sent": [ "a (emscripten_resize_heap)", "b (_setitimer_js)", diff --git a/test/codesize/test_codesize_cxx_mangle.json b/test/codesize/test_codesize_cxx_mangle.json index cd1eeff3bb99d..616b0282bb35b 100644 --- a/test/codesize/test_codesize_cxx_mangle.json +++ b/test/codesize/test_codesize_cxx_mangle.json @@ -1,10 +1,10 @@ { - "a.out.js": 23224, - "a.out.js.gz": 8983, + "a.out.js": 25573, + "a.out.js.gz": 10269, "a.out.nodebug.wasm": 238957, - "a.out.nodebug.wasm.gz": 79842, - "total": 262181, - "total_gz": 88825, + "a.out.nodebug.wasm.gz": 79847, + "total": 264530, + "total_gz": 90116, "sent": [ "__cxa_begin_catch", "__cxa_end_catch", diff --git a/test/codesize/test_codesize_cxx_noexcept.json b/test/codesize/test_codesize_cxx_noexcept.json index 4881c4a93cac7..b683c6634442f 100644 --- a/test/codesize/test_codesize_cxx_noexcept.json +++ b/test/codesize/test_codesize_cxx_noexcept.json @@ -1,10 +1,10 @@ { - "a.out.js": 19194, - "a.out.js.gz": 7969, + "a.out.js": 21541, + "a.out.js.gz": 9245, "a.out.nodebug.wasm": 134661, "a.out.nodebug.wasm.gz": 50777, - "total": 153855, - "total_gz": 58746, + "total": 156202, + "total_gz": 60022, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_file_preload.expected.js b/test/codesize/test_codesize_file_preload.expected.js index 785751e68113b..00825b17fefdd 100644 --- a/test/codesize/test_codesize_file_preload.expected.js +++ b/test/codesize/test_codesize_file_preload.expected.js @@ -350,7 +350,6 @@ function initRuntime() { runtimeInitialized = true; // Begin ATINITS hooks if (!Module["noFSInit"] && !FS.initialized) FS.init(); - TTY.init(); // End ATINITS hooks wasmExports["c"](); // Begin ATPOSTCTORS hooks @@ -686,6 +685,240 @@ var PATH_FS = { } }; +var ERRNO_CODES = { + "EPERM": 63, + "ENOENT": 44, + "ESRCH": 71, + "EINTR": 27, + "EIO": 29, + "ENXIO": 60, + "E2BIG": 1, + "ENOEXEC": 45, + "EBADF": 8, + "ECHILD": 12, + "EAGAIN": 6, + "EWOULDBLOCK": 6, + "ENOMEM": 48, + "EACCES": 2, + "EFAULT": 21, + "ENOTBLK": 105, + "EBUSY": 10, + "EEXIST": 20, + "EXDEV": 75, + "ENODEV": 43, + "ENOTDIR": 54, + "EISDIR": 31, + "EINVAL": 28, + "ENFILE": 41, + "EMFILE": 33, + "ENOTTY": 59, + "ETXTBSY": 74, + "EFBIG": 22, + "ENOSPC": 51, + "ESPIPE": 70, + "EROFS": 69, + "EMLINK": 34, + "EPIPE": 64, + "EDOM": 18, + "ERANGE": 68, + "ENOMSG": 49, + "EIDRM": 24, + "ECHRNG": 106, + "EL2NSYNC": 156, + "EL3HLT": 107, + "EL3RST": 108, + "ELNRNG": 109, + "EUNATCH": 110, + "ENOCSI": 111, + "EL2HLT": 112, + "EDEADLK": 16, + "ENOLCK": 46, + "EBADE": 113, + "EBADR": 114, + "EXFULL": 115, + "ENOANO": 104, + "EBADRQC": 103, + "EBADSLT": 102, + "EDEADLOCK": 16, + "EBFONT": 101, + "ENOSTR": 100, + "ENODATA": 116, + "ETIME": 117, + "ENOSR": 118, + "ENONET": 119, + "ENOPKG": 120, + "EREMOTE": 121, + "ENOLINK": 47, + "EADV": 122, + "ESRMNT": 123, + "ECOMM": 124, + "EPROTO": 65, + "EMULTIHOP": 36, + "EDOTDOT": 125, + "EBADMSG": 9, + "ENOTUNIQ": 126, + "EBADFD": 127, + "EREMCHG": 128, + "ELIBACC": 129, + "ELIBBAD": 130, + "ELIBSCN": 131, + "ELIBMAX": 132, + "ELIBEXEC": 133, + "ENOSYS": 52, + "ENOTEMPTY": 55, + "ENAMETOOLONG": 37, + "ELOOP": 32, + "EOPNOTSUPP": 138, + "EPFNOSUPPORT": 139, + "ECONNRESET": 15, + "ENOBUFS": 42, + "EAFNOSUPPORT": 5, + "EPROTOTYPE": 67, + "ENOTSOCK": 57, + "ENOPROTOOPT": 50, + "ESHUTDOWN": 140, + "ECONNREFUSED": 14, + "EADDRINUSE": 3, + "ECONNABORTED": 13, + "ENETUNREACH": 40, + "ENETDOWN": 38, + "ETIMEDOUT": 73, + "EHOSTDOWN": 142, + "EHOSTUNREACH": 23, + "EINPROGRESS": 26, + "EALREADY": 7, + "EDESTADDRREQ": 17, + "EMSGSIZE": 35, + "EPROTONOSUPPORT": 66, + "ESOCKTNOSUPPORT": 137, + "EADDRNOTAVAIL": 4, + "ENETRESET": 39, + "EISCONN": 30, + "ENOTCONN": 53, + "ETOOMANYREFS": 141, + "EUSERS": 136, + "EDQUOT": 19, + "ESTALE": 72, + "ENOTSUP": 138, + "ENOMEDIUM": 148, + "EILSEQ": 25, + "EOVERFLOW": 61, + "ECANCELED": 11, + "ENOTRECOVERABLE": 56, + "EOWNERDEAD": 62, + "ESTRPIPE": 135 +}; + +var nodeTTY = require("node:tty"); + +var 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 + const isStdStream = fd === 0 || fd === 1 || fd === 2; + if (isStdStream && (e?.code === "ENOTSUP" || e?.code === "EBADF" || e?.code === "EPERM")) { + return; + } + throw e; + } +}; + +var DEV = { + readWriteHelper: (stream, cb, method) => { + try { + var nbytes = cb(); + } catch (e) { + // Convert Node errors into ErrnoError + if (e && e.code && ERRNO_CODES[e.code]) { + throw new FS.ErrnoError(ERRNO_CODES[e.code]); + } + if (e?.errno) { + // propagate errno + throw e; + } + // Other errors converted to EIO. + throw new FS.ErrnoError(29); + } + if (nbytes === undefined) { + // Prevent an infinite loop caused by incorrect code that doesn't return a + // value + // Maybe we should set nbytes = buffer.length here instead? + throw new FS.ErrnoError(29); + } + if (nbytes !== 0) { + stream.node.timestamp = Date.now(); + } + return nbytes; + }, + devs: [], + register(dev, ops) { + DEV.devs[dev] = ops; + FS.registerDevice(dev, DEV.stream_ops); + }, + TTY_OPS: { + ioctl_tiocgwinsz(tty) { + const {rows = 24, columns = 80} = tty.devops.getTerminalSize?.() ?? {}; + return [ rows, columns ]; + } + }, + stream_ops: { + open(stream) { + var devops = DEV.devs[stream.node.rdev]; + if (!devops) { + throw new FS.ErrnoError(43); + } + stream.devops = devops; + stream.seekable = false; + stream.tty = stream.devops.tty ?? (stream.devops.isatty ? { + ops: DEV.TTY_OPS, + devops + } : undefined); + devops.open?.(stream); + }, + close(stream) { + // flush any pending line data + stream.stream_ops.fsync(stream); + }, + fsync(stream) { + stream.devops.fsync?.(stream.devops); + }, + read: function(stream, buffer, offset, length, pos) { + buffer = buffer.subarray(offset, offset + length); + return DEV.readWriteHelper(stream, () => stream.devops.read(stream.devops, buffer), "read"); + }, + write: function(stream, buffer, offset, length, pos) { + buffer = buffer.subarray(offset, offset + length); + return DEV.readWriteHelper(stream, () => stream.devops.write(stream.devops, buffer), "write"); + } + }, + nodeInputDevice: nodeStream => ({ + isatty: nodeTTY.isatty(nodeStream.fd), + fsync() { + nodeFsync(nodeStream.fd); + }, + read(ops, buffer) { + return fs.readSync(nodeStream.fd, buffer, 0, buffer.length); + } + }), + nodeOutputDevice: nodeStream => ({ + isatty: nodeTTY.isatty(nodeStream.fd), + fsync() { + nodeFsync(nodeStream.fd); + }, + write(ops, buffer) { + return fs.writeSync(nodeStream.fd, buffer, 0, buffer.length); + }, + getTerminalSize() { + return nodeStream; + } + }) +}; + var UTF8Decoder = globalThis.TextDecoder && new TextDecoder; var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => { @@ -862,73 +1095,46 @@ var FS_stdin_getChar = () => { }; var TTY = { - ttys: [], - init() {}, - shutdown() {}, + ttys: {}, register(dev, ops) { - TTY.ttys[dev] = { + const tty = { input: [], output: [], ops }; - FS.registerDevice(dev, TTY.stream_ops); - }, - stream_ops: { - open(stream) { - var tty = TTY.ttys[stream.node.rdev]; - if (!tty) { - throw new FS.ErrnoError(43); - } - stream.tty = tty; - stream.seekable = false; - }, - close(stream) { - // flush any pending line data - stream.tty.ops.fsync(stream.tty); - }, - fsync(stream) { - stream.tty.ops.fsync(stream.tty); - }, - read(stream, buffer, offset, length, pos) { - if (!stream.tty || !stream.tty.ops.get_char) { - throw new FS.ErrnoError(60); - } - var bytesRead = 0; - for (var i = 0; i < length; i++) { - var result; - try { - result = stream.tty.ops.get_char(stream.tty); - } catch (e) { - throw new FS.ErrnoError(29); + TTY.ttys[dev] = tty; + const devops = { + tty, + write(devops, buffer) { + if (!ops.put_char) { + throw new FS.ErrnoError(60); } - if (result === undefined && bytesRead === 0) { - throw new FS.ErrnoError(6); + for (var i = 0; i < buffer.length; i++) { + ops.put_char(tty, buffer[i]); } - if (result === null || result === undefined) break; - bytesRead++; - buffer[offset + i] = result; - } - if (bytesRead) { - stream.node.atime = Date.now(); - } - return bytesRead; - }, - write(stream, buffer, offset, length, pos) { - if (!stream.tty || !stream.tty.ops.put_char) { - throw new FS.ErrnoError(60); - } - try { - for (var i = 0; i < length; i++) { - stream.tty.ops.put_char(stream.tty, buffer[offset + i]); + return i; + }, + read(devops, buffer) { + if (!ops.get_char) { + throw new FS.ErrnoError(60); } - } catch (e) { - throw new FS.ErrnoError(29); - } - if (length) { - stream.node.mtime = stream.node.ctime = Date.now(); + var bytesRead = 0; + for (var i = 0; i < buffer.length; i++) { + var result = ops.get_char(tty); + if (result === undefined && bytesRead === 0) { + throw new FS.ErrnoError(6); + } + if (result === null || result === undefined) break; + bytesRead++; + buffer[i] = result; + } + return bytesRead; } - return i; + }; + if (ops.fsync) { + devops.fsync = devops => ops.fsync(tty); } + DEV.register(dev, devops); }, default_tty_ops: { get_char(tty) { @@ -2612,16 +2818,25 @@ var FS = { // them instead. if (input) { FS.createDevice("/dev", "stdin", input); + } else if (ENVIRONMENT_IS_NODE) { + DEV.register(FS.makedev(7, 0), DEV.nodeInputDevice(process.stdin)); + FS.mkdev("/dev/stdin", FS.makedev(7, 0)); } else { FS.symlink("/dev/tty", "/dev/stdin"); } if (output) { FS.createDevice("/dev", "stdout", null, output); + } else if (ENVIRONMENT_IS_NODE) { + DEV.register(FS.makedev(7, 1), DEV.nodeOutputDevice(process.stdout)); + FS.mkdev("/dev/stdout", FS.makedev(7, 1)); } else { FS.symlink("/dev/tty", "/dev/stdout"); } if (error) { FS.createDevice("/dev", "stderr", null, error); + } else if (ENVIRONMENT_IS_NODE) { + DEV.register(FS.makedev(7, 2), DEV.nodeOutputDevice(process.stderr)); + FS.mkdev("/dev/stderr", FS.makedev(7, 2)); } else { FS.symlink("/dev/tty1", "/dev/stderr"); } diff --git a/test/codesize/test_codesize_file_preload.json b/test/codesize/test_codesize_file_preload.json index a84ee7c65dad3..753e7ec743b86 100644 --- a/test/codesize/test_codesize_file_preload.json +++ b/test/codesize/test_codesize_file_preload.json @@ -1,10 +1,10 @@ { - "a.out.js": 22141, - "a.out.js.gz": 9184, + "a.out.js": 24491, + "a.out.js.gz": 10462, "a.out.nodebug.wasm": 1648, "a.out.nodebug.wasm.gz": 939, - "total": 23789, - "total_gz": 10123, + "total": 26139, + "total_gz": 11401, "sent": [ "a (fd_write)" ], diff --git a/test/codesize/test_codesize_files_js_fs.json b/test/codesize/test_codesize_files_js_fs.json index 78a3cf7f21ebf..0623026e7bd6f 100644 --- a/test/codesize/test_codesize_files_js_fs.json +++ b/test/codesize/test_codesize_files_js_fs.json @@ -1,10 +1,10 @@ { - "a.out.js": 17834, - "a.out.js.gz": 7308, + "a.out.js": 20180, + "a.out.js.gz": 8565, "a.out.nodebug.wasm": 381, "a.out.nodebug.wasm.gz": 260, - "total": 18215, - "total_gz": 7568, + "total": 20561, + "total_gz": 8825, "sent": [ "a (fd_write)", "b (fd_read)", diff --git a/test/codesize/test_codesize_hello_O0.json b/test/codesize/test_codesize_hello_O0.json index ed7a13ce715c6..77120e9d757c3 100644 --- a/test/codesize/test_codesize_hello_O0.json +++ b/test/codesize/test_codesize_hello_O0.json @@ -1,10 +1,10 @@ { - "a.out.js": 24261, - "a.out.js.gz": 8714, + "a.out.js": 24283, + "a.out.js.gz": 8726, "a.out.nodebug.wasm": 14850, "a.out.nodebug.wasm.gz": 7311, - "total": 39111, - "total_gz": 16025, + "total": 39133, + "total_gz": 16037, "sent": [ "fd_write" ], diff --git a/test/codesize/test_codesize_hello_dylink.json b/test/codesize/test_codesize_hello_dylink.json index efb0f7d1c501f..6cde14e13d265 100644 --- a/test/codesize/test_codesize_hello_dylink.json +++ b/test/codesize/test_codesize_hello_dylink.json @@ -1,10 +1,10 @@ { - "a.out.js": 26185, - "a.out.js.gz": 11171, + "a.out.js": 28536, + "a.out.js.gz": 12476, "a.out.nodebug.wasm": 17668, "a.out.nodebug.wasm.gz": 8921, - "total": 43853, - "total_gz": 20092, + "total": 46204, + "total_gz": 21397, "sent": [ "__syscall_stat64", "emscripten_resize_heap", diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 8eaf79b3ee12a..5779b24e5e2b3 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 244343, - "a.out.nodebug.wasm": 577447, - "total": 821790, + "a.out.js": 246720, + "a.out.nodebug.wasm": 577444, + "total": 824167, "sent": [ "IMG_Init", "IMG_Load", diff --git a/test/codesize/test_codesize_minimal_O0.expected.js b/test/codesize/test_codesize_minimal_O0.expected.js index ad72f50b73f66..9738b0c9ecd50 100644 --- a/test/codesize/test_codesize_minimal_O0.expected.js +++ b/test/codesize/test_codesize_minimal_O0.expected.js @@ -1067,6 +1067,7 @@ Module['FS_createPreloadedFile'] = FS.createPreloadedFile; 'FS_fileDataToTypedArray', 'FS_stdin_getChar', 'FS_mkdirTree', + 'nodeFsync', '_setNetworkCallback', ]; missingLibrarySymbols.forEach(missingLibrarySymbol) @@ -1254,6 +1255,8 @@ missingLibrarySymbols.forEach(missingLibrarySymbol) 'FS_createLazyFile', 'MEMFS', 'TTY', + 'DEV', + 'nodeTTY', 'PIPEFS', 'SOCKFS', ]; diff --git a/test/codesize/test_codesize_minimal_O0.json b/test/codesize/test_codesize_minimal_O0.json index 8d4cc47adfaba..044b8d4c8028e 100644 --- a/test/codesize/test_codesize_minimal_O0.json +++ b/test/codesize/test_codesize_minimal_O0.json @@ -1,10 +1,10 @@ { - "a.out.js": 19452, - "a.out.js.gz": 6998, + "a.out.js": 19474, + "a.out.js.gz": 7010, "a.out.nodebug.wasm": 1015, "a.out.nodebug.wasm.gz": 602, - "total": 20467, - "total_gz": 7600, + "total": 20489, + "total_gz": 7612, "sent": [], "imports": [], "exports": [ diff --git a/test/codesize/test_unoptimized_code_size.json b/test/codesize/test_unoptimized_code_size.json index 3fd577e8caa13..ed3525208d196 100644 --- a/test/codesize/test_unoptimized_code_size.json +++ b/test/codesize/test_unoptimized_code_size.json @@ -1,16 +1,16 @@ { - "hello_world.js": 57078, - "hello_world.js.gz": 17753, + "hello_world.js": 57115, + "hello_world.js.gz": 17768, "hello_world.wasm": 14850, "hello_world.wasm.gz": 7311, "no_asserts.js": 26654, "no_asserts.js.gz": 8901, "no_asserts.wasm": 12010, "no_asserts.wasm.gz": 5880, - "strict.js": 54896, - "strict.js.gz": 17061, + "strict.js": 54933, + "strict.js.gz": 17077, "strict.wasm": 14850, "strict.wasm.gz": 7311, - "total": 180338, - "total_gz": 64217 + "total": 180412, + "total_gz": 64248 } diff --git a/test/other/libtty.c b/test/other/libtty.c new file mode 100644 index 0000000000000..8146dbef65176 --- /dev/null +++ b/test/other/libtty.c @@ -0,0 +1,209 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +EM_JS_DEPS(main, "$TTY"); + +// clang-format off +EM_JS(void, init, (void), { + var major = 100; + var tty_ops = { + get_char: function (tty) { + if (tty.input.length > 0) { + return tty.input.shift(); + } + return undefined; + }, + put_char: function (tty, val) { + if (val !== 0 && val !== 10) { + tty.output.push(val); + } + }, + fsync: function (tty) { + console.log('fsync called'); + tty.output = []; + }, + ioctl_tcgets: function (tty) { + return { + c_iflag: 0, + c_oflag: 0, + c_cflag: 0, + c_lflag: 0, + c_cc: new Array(32).fill(0), + }; + }, + ioctl_tcsets: function (tty, optional_actions, data) { + return 0; + }, + ioctl_tiocgwinsz: function (tty) { + return [25, 80]; + }, + }; + var device = FS.makedev(major, 0); + TTY.register(device, tty_ops); + FS.mkdev('/custom_tty', device); + // Populate the TTY input buffer with test data "ABCD" + TTY.ttys[device].input = [65, 66, 67, 68]; + + // TTY without get_char - should cause ENXIO on read + var tty_no_getchar = {put_char: function (tty, val) {}}; + var device_no_getchar = FS.makedev(major + 1, 0); + TTY.register(device_no_getchar, tty_no_getchar); + FS.mkdev('/tty_no_getchar', device_no_getchar); + + // TTY without put_char - should cause ENXIO on write + var tty_no_putchar = { + get_char: function (tty) { + return 0; + }, + }; + var device_no_putchar = FS.makedev(major + 2, 0); + TTY.register(device_no_putchar, tty_no_putchar); + FS.mkdev('/tty_no_putchar', device_no_putchar); + + // TTY with throwing get_char - should cause EIO on read + var tty_throw_getchar = { + get_char: function (tty) { + throw new Error('get_char error'); + }, + put_char: function (tty, val) {}, + }; + var device_throw_getchar = FS.makedev(major + 3, 0); + TTY.register(device_throw_getchar, tty_throw_getchar); + FS.mkdev('/tty_throw_getchar', device_throw_getchar); + + // TTY with throwing put_char - should cause EIO on write + var tty_throw_putchar = { + get_char: function (tty) { + return 0; + }, + put_char: function (tty, val) { + throw new Error('put_char error'); + }, + }; + var device_throw_putchar = FS.makedev(major + 4, 0); + TTY.register(device_throw_putchar, tty_throw_putchar); + FS.mkdev('/tty_throw_putchar', device_throw_putchar); + + // TTY with empty input (returns undefined immediately) - should cause EAGAIN on + // read + var tty_empty = { + get_char: function (tty) { + return undefined; + }, + put_char: function (tty, val) {}, + }; + var device_empty = FS.makedev(major + 5, 0); + TTY.register(device_empty, tty_empty); + FS.mkdev('/tty_empty', device_empty); +}); +// clang-format on + +int main() { + init(); + char readBuffer[256] = {0}; + char writeBuffer[] = "Test"; + struct winsize ws; + struct termios term; + + printf("\nTest 1: open custom TTY device and check isatty\n"); + int fd = open("/custom_tty", O_RDWR); + assert(fd >= 0); + printf("isatty: %d\n", isatty(fd)); + printf("errno after open: %s\n", strerror(errno)); + errno = 0; + + printf("\nTest 2: read from TTY with data\n"); + ssize_t bytesRead = read(fd, readBuffer, sizeof(readBuffer)); + printf("read bytes: %zd\n", bytesRead); + printf("read data: %s\n", bytesRead > 0 ? readBuffer : "(none)"); + printf("errno after read: %s\n", strerror(errno)); + errno = 0; + + printf("\nTest 3: write to TTY\n"); + ssize_t bytesWritten = write(fd, writeBuffer, strlen(writeBuffer)); + printf("write bytes: %zd\n", bytesWritten); + printf("errno after write: %s\n", strerror(errno)); + errno = 0; + + printf("\nTest 4: ioctl TIOCGWINSZ\n"); + int result = ioctl(fd, TIOCGWINSZ, &ws); + printf("ioctl TIOCGWINSZ: %d\n", result); + printf("ws_row: %d ws_col: %d\n", ws.ws_row, ws.ws_col); + printf("errno after ioctl: %s\n", strerror(errno)); + errno = 0; + + printf("\nTest 5: ioctl TCGETS\n"); + result = ioctl(fd, TCGETS, &term); + printf("ioctl TCGETS: %d\n", result); + printf("errno after TCGETS: %s\n", strerror(errno)); + errno = 0; + + printf("\nTest 6: ioctl TCSETS\n"); + result = ioctl(fd, TCSETS, &term); + printf("ioctl TCSETS: %d\n", result); + printf("errno after TCSETS: %s\n", strerror(errno)); + errno = 0; + + printf("\nTest 7: fsync\n"); + result = fsync(fd); + printf("fsync: %d\n", result); + printf("errno after fsync: %s\n", strerror(errno)); + errno = 0; + + close(fd); + + printf("\nTest 8: no put_char\n"); + fd = open("/tty_no_putchar", O_WRONLY); + assert(fd >= 0); + bytesWritten = write(fd, writeBuffer, strlen(writeBuffer)); + printf("write: %zd\n", bytesWritten); + printf("errno: %s\n", strerror(errno)); + errno = 0; + close(fd); + + printf("\nTest 9: no get_char\n"); + fd = open("/tty_no_getchar", O_RDONLY); + assert(fd >= 0); + bytesRead = read(fd, readBuffer, sizeof(readBuffer)); + printf("read: %zd\n", bytesRead); + printf("errno: %s\n", strerror(errno)); + errno = 0; + close(fd); + + printf("\nTest 10: put_char throws\n"); + fd = open("/tty_throw_putchar", O_WRONLY); + assert(fd >= 0); + bytesWritten = write(fd, writeBuffer, strlen(writeBuffer)); + printf("write: %zd\n", bytesWritten); + printf("errno: %s\n", strerror(errno)); + errno = 0; + close(fd); + + printf("\nTest 11: get_char throws\n"); + fd = open("/tty_throw_getchar", O_RDONLY); + assert(fd >= 0); + bytesRead = read(fd, readBuffer, sizeof(readBuffer)); + printf("read: %zd\n", bytesRead); + printf("errno: %s\n", strerror(errno)); + errno = 0; + close(fd); + + printf("\nTest 12: get_char returns undefined\n"); + fd = open("/tty_empty", O_RDONLY); + assert(fd >= 0); + bytesRead = read(fd, readBuffer, sizeof(readBuffer)); + printf("read: %zd\n", bytesRead); + printf("errno: %s\n", strerror(errno)); + errno = 0; + close(fd); + + printf("\ndone\n"); + return 0; +} diff --git a/test/other/libtty.out b/test/other/libtty.out new file mode 100644 index 0000000000000..0cd5c04e16eb2 --- /dev/null +++ b/test/other/libtty.out @@ -0,0 +1,54 @@ + +Test 1: open custom TTY device and check isatty +isatty: 1 +errno after open: Success + +Test 2: read from TTY with data +read bytes: 4 +read data: ABCD +errno after read: Success + +Test 3: write to TTY +write bytes: 4 +errno after write: Success + +Test 4: ioctl TIOCGWINSZ +ioctl TIOCGWINSZ: 0 +ws_row: 25 ws_col: 80 +errno after ioctl: Success + +Test 5: ioctl TCGETS +ioctl TCGETS: 0 +errno after TCGETS: Success + +Test 6: ioctl TCSETS +ioctl TCSETS: 0 +errno after TCSETS: Success + +Test 7: fsync +fsync called +fsync: 0 +errno after fsync: Success +fsync called + +Test 8: no put_char +write: -1 +errno: No such device or address + +Test 9: no get_char +read: -1 +errno: No such device or address + +Test 10: put_char throws +write: -1 +errno: I/O error + +Test 11: get_char throws +read: -1 +errno: I/O error + +Test 12: get_char returns undefined +read: -1 +errno: Resource temporarily unavailable + +done diff --git a/test/other/test_node_stdio_isatty.c b/test/other/test_node_stdio_isatty.c new file mode 100644 index 0000000000000..2f672fadc385a --- /dev/null +++ b/test/other/test_node_stdio_isatty.c @@ -0,0 +1,17 @@ +#include "emscripten.h" +#include "stdio.h" +#include "unistd.h" + +EM_JS(int, init, (void), { + var dev = FS.makedev(FS.createDevice.major++, 0); + DEV.register(dev, DEV.nodeOutputDevice({fd: Module.outFd})); + FS.mkdev('/dev/origout', dev); + return FS.open('/dev/origout', 1).fd; +}); + +int main(void) { + int outfd = init(); + FILE* outfile = fdopen(outfd, "w"); + + fprintf(outfile, "%d%d%d\n", isatty(0), isatty(1), isatty(2)); +} diff --git a/test/other/test_node_stdio_isatty.mjs b/test/other/test_node_stdio_isatty.mjs new file mode 100644 index 0000000000000..307eaa9857f4f --- /dev/null +++ b/test/other/test_node_stdio_isatty.mjs @@ -0,0 +1,30 @@ +import createModule from '../../out/test/out.mjs'; +import * as fs from 'node:fs'; +const outFd = fs.openSync('/proc/self/fd/1', 'w'); + +const arg = process.argv.at(-1); +if (arg[0] === "1") { + fs.closeSync(0); + fs.openSync("/dev/tty", 'r'); +} else { + fs.closeSync(0); + fs.openSync("/dev/null", 'r'); +} + +if (arg[1] === "1") { + fs.closeSync(1); + fs.openSync("/dev/tty", 'w'); +} else { + fs.closeSync(1); + fs.openSync("/dev/null", 'w'); +} + +if (arg[2] === "1") { + fs.closeSync(2); + fs.openSync("/dev/tty", 'w'); +} else { + fs.closeSync(2); + fs.openSync("/dev/null", 'w'); +} + +await createModule({ outFd }); diff --git a/test/other/test_node_term_size.c b/test/other/test_node_term_size.c new file mode 100644 index 0000000000000..a8f4d63bcf2a4 --- /dev/null +++ b/test/other/test_node_term_size.c @@ -0,0 +1,23 @@ +#include "emscripten.h" +#include "stdio.h" +#include "sys/ioctl.h" +#include "unistd.h" + +EM_JS(int, init, (void), { + var dev = FS.makedev(FS.createDevice.major++, 0); + DEV.register(dev, DEV.nodeOutputDevice({fd: Module.outFd})); + FS.mkdev('/dev/origout', dev); + return FS.open('/dev/origout', 1).fd; +}); + +int main(void) { + int outfd = init(); + FILE* outfile = fdopen(outfd, "w"); + + struct winsize w; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); + + fprintf (outfile, "rows %d\n", w.ws_row); + fprintf (outfile, "columns %d\n", w.ws_col); + return 0; +} diff --git a/test/other/test_node_term_size.mjs b/test/other/test_node_term_size.mjs new file mode 100644 index 0000000000000..d53514f766f29 --- /dev/null +++ b/test/other/test_node_term_size.mjs @@ -0,0 +1,15 @@ +import createModule from '../../out/test/out.mjs'; +import * as fs from 'node:fs'; +import * as tty from 'node:tty'; + +const outFd = fs.openSync('/proc/self/fd/1', 'w'); + +fs.closeSync(1); +fs.openSync("/dev/tty", 1); + +globalThis.process = Object.assign({}, process, {stdout: new tty.WriteStream(1)}); +process.stdout.fd = 1; +process.stdout.columns = 180; +process.stdout.rows = 50; + +await createModule({ outFd }); diff --git a/test/test_other.py b/test/test_other.py index 81d892921162c..b2862361c0915 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -13176,6 +13176,9 @@ def test_unistd_isatty(self): self.skipTest('depends on /dev filesystem') self.do_runf('unistd/isatty.c', 'success') + def test_libtty(self): + self.do_other_test('libtty.c', cflags=["-O2"]) + def test_unistd_login(self): self.do_run_in_out_file_test('unistd/login.c') @@ -15443,3 +15446,16 @@ def test_logReadFiles(self): create_file('pre.js', 'Module.logReadFiles = 1;') output = self.do_runf('checksummer.c', args=['test.txt'], cflags=['--pre-js=pre.js']) self.assertContained('read file: /test.txt', output) + + @crossplatform + @no_windows('opens /proc/self/fd/1') + def test_node_stdio_isatty(self): + self.run_process([EMCC, test_file('other/test_node_stdio_isatty.c'), '-o', 'out.mjs']) + for arg in ['111', '011', '101', '110', '100', '010', '001', '000']: + self.assertEqual(self.run_js(test_file('other/test_node_stdio_isatty.mjs'), args=[arg]), arg + '\n') + + @crossplatform + @no_windows('opens /proc/self/fd/1') + def test_node_term_size(self): + self.run_process([EMCC, test_file('other/test_node_term_size.c'), '-o', 'out.mjs']) + self.assertEqual(self.run_js(test_file('other/test_node_term_size.mjs')), 'rows 50\ncolumns 180\n')