Skip to content

Commit 7b94820

Browse files
committed
Add libdev as a more powerful replacement for libtty
Work in progress.
1 parent f9b340a commit 7b94820

File tree

7 files changed

+255
-80
lines changed

7 files changed

+255
-80
lines changed

src/lib/libdev.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* @license
3+
* Copyright 2013 The Emscripten Authors
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
7+
addToLibrary({
8+
$DEV__deps: [
9+
'$FS',
10+
'$ERRNO_CODES',
11+
#if ENVIRONMENT_MAY_BE_NODE
12+
'$nodeTTY',
13+
'$nodeFsync',
14+
#endif
15+
],
16+
$DEV: {
17+
readWriteHelper: (stream, cb, method) => {
18+
try {
19+
var nbytes = cb();
20+
} catch (e) {
21+
// Convert Node errors into ErrnoError
22+
if (e && e.code && ERRNO_CODES[e.code]) {
23+
throw new FS.ErrnoError(ERRNO_CODES[e.code]);
24+
}
25+
if (e?.errno) {
26+
// propagate errno
27+
throw e;
28+
}
29+
// Other errors converted to EIO.
30+
#if ASSERTIONS
31+
console.error(`Error thrown in ${method}:`);
32+
console.error(e);
33+
#endif
34+
throw new FS.ErrnoError({{{ cDefs.EIO }}});
35+
}
36+
if (nbytes === undefined) {
37+
// Prevent an infinite loop caused by incorrect code that doesn't return a
38+
// value
39+
// Maybe we should set nbytes = buffer.length here instead?
40+
#if ASSERTIONS
41+
console.warn(
42+
`${method} returned undefined; a correct implementation must return a number`,
43+
);
44+
#endif
45+
throw new FS.ErrnoError({{{ cDefs.EIO }}});
46+
}
47+
if (nbytes !== 0) {
48+
stream.node.timestamp = Date.now();
49+
}
50+
return nbytes;
51+
},
52+
devs: [],
53+
register(dev, ops) {
54+
DEV.devs[dev] = ops;
55+
FS.registerDevice(dev, DEV.stream_ops);
56+
},
57+
TTY_OPS: {
58+
ioctl_tiocgwinsz(tty) {
59+
const { rows = 24, columns = 80 } = tty.devops.getTerminalSize?.() ?? {};
60+
return [rows, columns];
61+
},
62+
},
63+
stream_ops: {
64+
open(stream) {
65+
var devops = DEV.devs[stream.node.rdev];
66+
if (!devops) {
67+
throw new FS.ErrnoError({{{ cDefs.ENODEV }}});
68+
}
69+
stream.devops = devops;
70+
stream.seekable = false;
71+
stream.tty =
72+
stream.devops.tty ??
73+
(stream.devops.isatty
74+
? {
75+
ops: DEV.TTY_OPS,
76+
devops,
77+
}
78+
: undefined);
79+
devops.open?.(stream);
80+
},
81+
close(stream) {
82+
// flush any pending line data
83+
stream.stream_ops.fsync(stream);
84+
},
85+
fsync(stream) {
86+
stream.devops.fsync?.(stream.devops);
87+
},
88+
read: function (stream, buffer, offset, length, pos /* ignored */) {
89+
buffer = buffer.subarray(offset, offset + length);
90+
return DEV.readWriteHelper(stream, () => stream.devops.read(stream.devops, buffer), "read");
91+
},
92+
write: function (stream, buffer, offset, length, pos /* ignored */) {
93+
buffer = buffer.subarray(offset, offset + length);
94+
return DEV.readWriteHelper(stream, () => stream.devops.write(stream.devops, buffer), "write");
95+
},
96+
},
97+
#if ENVIRONMENT_MAY_BE_NODE
98+
nodeInputDevice: (nodeStream) => ({
99+
isatty: nodeTTY.isatty(nodeStream.fd),
100+
fsync() {
101+
nodeFsync(nodeStream.fd);
102+
},
103+
read(ops, buffer) {
104+
return fs.readSync(nodeStream.fd, buffer)
105+
},
106+
}),
107+
nodeOutputDevice: (nodeStream) => ({
108+
isatty: nodeTTY.isatty(nodeStream.fd),
109+
fsync() {
110+
nodeFsync(nodeStream.fd);
111+
},
112+
write(ops, buffer) {
113+
return fs.writeSync(nodeStream.fd, buffer)
114+
},
115+
getTerminalSize() {
116+
return nodeStream;
117+
}
118+
}),
119+
#endif
120+
},
121+
#if ENVIRONMENT_MAY_BE_NODE
122+
$nodeTTY: "require('node:tty');",
123+
$nodeFsync: (fd) => {
124+
try {
125+
fs.fsyncSync(fd);
126+
} catch (e) {
127+
if (e?.code === "EINVAL") {
128+
return;
129+
}
130+
// On Mac, calling fsync when not isatty returns ENOTSUP
131+
// On Windows, stdin/stdout/stderr may be closed, returning EBADF or EPERM
132+
const isStdStream = fd === 0 || fd === 1 || fd === 2;
133+
if (
134+
isStdStream &&
135+
(e?.code === "ENOTSUP" || e?.code === "EBADF" || e?.code === "EPERM")
136+
) {
137+
return;
138+
}
139+
140+
throw e;
141+
}
142+
}
143+
#endif
144+
});

src/lib/libfs.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,17 +1465,40 @@ FS.staticInit();`;
14651465
// them instead.
14661466
if (input) {
14671467
FS.createDevice('/dev', 'stdin', input);
1468-
} else {
1468+
} else
1469+
#if ENVIRONMENT_MAY_BE_NODE
1470+
if (ENVIRONMENT_IS_NODE) {
1471+
DEV.register(FS.makedev(7, 0), DEV.nodeInputDevice(process.stdin));
1472+
FS.mkdev('/dev/stdin', FS.makedev(7, 0));
1473+
} else
1474+
#endif
1475+
{
14691476
FS.symlink('/dev/tty', '/dev/stdin');
14701477
}
1478+
14711479
if (output) {
14721480
FS.createDevice('/dev', 'stdout', null, output);
1473-
} else {
1481+
} else
1482+
#if ENVIRONMENT_MAY_BE_NODE
1483+
if (ENVIRONMENT_IS_NODE) {
1484+
DEV.register(FS.makedev(7, 1), DEV.nodeOutputDevice(process.stdout));
1485+
FS.mkdev('/dev/stdout', FS.makedev(7, 1));
1486+
} else
1487+
#endif
1488+
{
14741489
FS.symlink('/dev/tty', '/dev/stdout');
14751490
}
1491+
14761492
if (error) {
14771493
FS.createDevice('/dev', 'stderr', null, error);
1478-
} else {
1494+
} else
1495+
#if ENVIRONMENT_MAY_BE_NODE
1496+
if (ENVIRONMENT_IS_NODE) {
1497+
DEV.register(FS.makedev(7, 2), DEV.nodeOutputDevice(process.stderr));
1498+
FS.mkdev('/dev/stderr', FS.makedev(7, 2));
1499+
} else
1500+
#endif
1501+
{
14791502
FS.symlink('/dev/tty1', '/dev/stderr');
14801503
}
14811504

src/lib/libtty.js

Lines changed: 23 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -6,99 +6,46 @@
66

77
addToLibrary({
88
$TTY__deps: [
9+
'$DEV',
910
'$FS',
1011
'$UTF8ArrayToString',
11-
'$FS_stdin_getChar'
12+
'$FS_stdin_getChar',
1213
],
13-
#if !MINIMAL_RUNTIME
14-
$TTY__postset: () => {
15-
addAtInit('TTY.init();');
16-
addAtExit('TTY.shutdown();');
17-
},
18-
#endif
1914
$TTY: {
20-
ttys: [],
21-
init() {
22-
// https://github.com/emscripten-core/emscripten/pull/1555
23-
// if (ENVIRONMENT_IS_NODE) {
24-
// // currently, FS.init does not distinguish if process.stdin is a file or TTY
25-
// // device, it always assumes it's a TTY device. because of this, we're forcing
26-
// // process.stdin to UTF8 encoding to at least make stdin reading compatible
27-
// // with text files until FS.init can be refactored.
28-
// process.stdin.setEncoding('utf8');
29-
// }
30-
},
31-
shutdown() {
32-
// https://github.com/emscripten-core/emscripten/pull/1555
33-
// if (ENVIRONMENT_IS_NODE) {
34-
// // inolen: any idea as to why node -e 'process.stdin.read()' wouldn't exit immediately (with process.stdin being a tty)?
35-
// // 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
36-
// // inolen: I thought read() in that case was a synchronous operation that just grabbed some amount of buffered data if it exists?
37-
// // isaacs: it is. but it also triggers a _read() call, which calls readStart() on the handle
38-
// // isaacs: do process.stdin.pause() and i'd think it'd probably close the pending call
39-
// process.stdin.pause();
40-
// }
41-
},
15+
ttys: {},
4216
register(dev, ops) {
43-
TTY.ttys[dev] = { input: [], output: [], ops: ops };
44-
FS.registerDevice(dev, TTY.stream_ops);
45-
},
46-
stream_ops: {
47-
open(stream) {
48-
var tty = TTY.ttys[stream.node.rdev];
49-
if (!tty) {
50-
throw new FS.ErrnoError({{{ cDefs.ENODEV }}});
17+
const tty = { input: [], output: [], ops };
18+
TTY.ttys[dev] = tty;
19+
const devops = { tty };
20+
devops.write = (devops, buffer) => {
21+
if (!ops.put_char) {
22+
throw new FS.ErrnoError({{{ cDefs.ENXIO }}});
5123
}
52-
stream.tty = tty;
53-
stream.seekable = false;
54-
},
55-
close(stream) {
56-
// flush any pending line data
57-
stream.tty.ops.fsync?.(stream.tty);
58-
},
59-
fsync(stream) {
60-
stream.tty.ops.fsync?.(stream.tty);
61-
},
62-
read(stream, buffer, offset, length, pos /* ignored */) {
63-
if (!stream.tty || !stream.tty.ops.get_char) {
24+
for (var i = 0; i < buffer.length; i++) {
25+
ops.put_char(tty, buffer[i]);
26+
}
27+
return i;
28+
};
29+
devops.read = (devops, buffer) => {
30+
if (!ops.get_char) {
6431
throw new FS.ErrnoError({{{ cDefs.ENXIO }}});
6532
}
6633
var bytesRead = 0;
67-
for (var i = 0; i < length; i++) {
68-
var result;
69-
try {
70-
result = stream.tty.ops.get_char(stream.tty);
71-
} catch (e) {
72-
throw new FS.ErrnoError({{{ cDefs.EIO }}});
73-
}
34+
for (var i = 0; i < buffer.length; i++) {
35+
var result = ops.get_char(tty);
7436
if (result === undefined && bytesRead === 0) {
7537
throw new FS.ErrnoError({{{ cDefs.EAGAIN }}});
7638
}
7739
if (result === null || result === undefined) break;
7840
bytesRead++;
79-
buffer[offset+i] = result;
80-
}
81-
if (bytesRead) {
82-
stream.node.atime = Date.now();
41+
buffer[i] = result;
8342
}
8443
return bytesRead;
85-
},
86-
write(stream, buffer, offset, length, pos) {
87-
if (!stream.tty || !stream.tty.ops.put_char) {
88-
throw new FS.ErrnoError({{{ cDefs.ENXIO }}});
89-
}
90-
try {
91-
for (var i = 0; i < length; i++) {
92-
stream.tty.ops.put_char(stream.tty, buffer[offset+i]);
93-
}
94-
} catch (e) {
95-
throw new FS.ErrnoError({{{ cDefs.EIO }}});
96-
}
97-
if (length) {
98-
stream.node.mtime = stream.node.ctime = Date.now();
99-
}
100-
return i;
44+
};
45+
if (ops.fsync) {
46+
devops.fsync = (devops) => ops.fsync(tty)
10147
}
48+
DEV.register(dev, devops);
10249
},
10350
default_tty_ops: {
10451
get_char(tty) {

src/modules.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ function calculateLibraries() {
111111
'libfs.js',
112112
'libmemfs.js',
113113
'libtty.js',
114+
'libdev.js',
114115
'libpipefs.js', // ok to include it by default since it's only used if the syscall is used
115116
'libsockfs.js', // ok to include it by default since it's only used if the syscall is used
116117
);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#include "emscripten.h"
2+
#include "stdio.h"
3+
#include "unistd.h"
4+
5+
EM_JS(int, init, (void), {
6+
var dev = FS.makedev(FS.createDevice.major++, 0);
7+
DEV.register(dev, DEV.nodeOutputDevice({fd: Module.outFd}));
8+
FS.mkdev('/dev/origout', dev);
9+
return FS.open('/dev/origout', 1).fd;
10+
});
11+
12+
int main(void) {
13+
int outfd = init();
14+
FILE* outfile = fdopen(outfd, "w");
15+
16+
fprintf(outfile, "%d%d%d\n", isatty(0), isatty(1), isatty(2));
17+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import createModule from '../../out/test/out.mjs';
2+
import * as fs from 'node:fs';
3+
const outFd = fs.openSync('/proc/self/fd/1', 'w');
4+
5+
const arg = process.argv.at(-1);
6+
if (arg[0] === "1") {
7+
fs.closeSync(0);
8+
fs.openSync("/dev/tty", 'r');
9+
} else {
10+
fs.closeSync(0);
11+
fs.openSync("/dev/null", 'r');
12+
}
13+
14+
if (arg[1] === "1") {
15+
fs.closeSync(1);
16+
fs.openSync("/dev/tty", 'w');
17+
} else {
18+
fs.closeSync(1);
19+
fs.openSync("/dev/null", 'w');
20+
}
21+
22+
if (arg[2] === "1") {
23+
fs.closeSync(2);
24+
fs.openSync("/dev/tty", 'w');
25+
} else {
26+
fs.closeSync(2);
27+
fs.openSync("/dev/null", 'w');
28+
}
29+
30+
await createModule({ outFd });

test/test_other.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13176,7 +13176,7 @@ def test_unistd_isatty(self):
1317613176
self.do_runf('unistd/isatty.c', 'success')
1317713177

1317813178
def test_libtty(self):
13179-
self.do_other_test('tty.c')
13179+
self.do_other_test('tty.c', cflags=["-O2"])
1318013180

1318113181
def test_unistd_login(self):
1318213182
self.do_run_in_out_file_test('unistd/login.c')
@@ -15445,3 +15445,16 @@ def test_logReadFiles(self):
1544515445
create_file('pre.js', 'Module.logReadFiles = 1;')
1544615446
output = self.do_runf('checksummer.c', args=['test.txt'], cflags=['--pre-js=pre.js'])
1544715447
self.assertContained('read file: /test.txt', output)
15448+
15449+
@crossplatform
15450+
@no_windows('opens /proc/self/fd/1')
15451+
def test_node_stdio_isatty(self):
15452+
self.run_process([EMCC, test_file('other/test_node_stdio_isatty.c'), '-o', 'out.mjs'])
15453+
for arg in ['111', '011', '101', '110', '100', '010', '001', '000']:
15454+
self.assertEqual(self.run_js(test_file('other/test_node_stdio_isatty.mjs'), args = [arg]), arg + '\n')
15455+
15456+
@crossplatform
15457+
@no_windows('opens /proc/self/fd/1')
15458+
def test_node_term_size(self):
15459+
self.run_process([EMCC, test_file('other/test_node_term_size.c'), '-o', 'out.mjs'])
15460+
self.assertEqual(self.run_js(test_file('other/test_node_term_size.mjs')), 'rows 50\ncolumns 180\n')

0 commit comments

Comments
 (0)