diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdout-patch-console-arguments.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdout-patch-console-arguments.js.json new file mode 100644 index 000000000..d8798d615 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdout-patch-console-arguments.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/stdout-patch-console-arguments.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index f1fefa626..040cb2234 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -203,7 +203,7 @@ void scr_init(void) { * the stream differs. */ /* A real definition lets relocatable library links localize this hook; * a tentative common symbol stays externally visible on Mach-O. */ -SCR_TL bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len, bool wide) = NULL; +SCR_TL bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len, int flags) = NULL; SCR_TL bool (*scr_stdio_capture)(int fd, const void *data, size_t len) = NULL; SCR_TL bool scr_stdin_fed = false; @@ -420,7 +420,7 @@ static void scr_console_write(int fd, size_t n, const ScrLogArg *args, bool glob } if (scr_stdio_write_hook) { /* A patched write receives the JavaScript string, lone surrogates kept. */ - scr_stdio_write_hook(fd, line, used, wide); + scr_stdio_write_hook(fd, line, used, (wide ? SCR_STDIO_HOOK_WIDE : 0) | SCR_STDIO_HOOK_CONSOLE); if (scr_exc_pending()) scr_exc_clear(); } else { if (wide) scr_wtf8_to_utf8(line, used); @@ -481,8 +481,16 @@ int scr_stdio_write_raw(int fd, const void *data, size_t len) { #endif } +/* A byte chunk (process.stdout.write(buffer), a stream piped to stdout). */ bool scr_stdio_write(int fd, const void *data, size_t len) { - if (scr_stdio_write_hook) return scr_stdio_write_hook(fd, data, len, false); + if (scr_stdio_write_hook) return scr_stdio_write_hook(fd, data, len, SCR_STDIO_HOOK_BYTES); + (void)scr_stdio_write_raw(fd, data, len); + return true; +} + +/* A string chunk that is plain UTF-8. */ +bool scr_stdio_write_text(int fd, const void *data, size_t len) { + if (scr_stdio_write_hook) return scr_stdio_write_hook(fd, data, len, 0); (void)scr_stdio_write_raw(fd, data, len); return true; } @@ -490,7 +498,7 @@ bool scr_stdio_write(int fd, const void *data, size_t len) { /* A string chunk: a patched write receives the string itself; the stream * receives its UTF-8, each lone surrogate as U+FFFD. */ static __attribute__((noinline)) bool scr_stdio_write_wide(int fd, const ScrStr *s) { - if (scr_stdio_write_hook) return scr_stdio_write_hook(fd, s->data, s->len, true); + if (scr_stdio_write_hook) return scr_stdio_write_hook(fd, s->data, s->len, SCR_STDIO_HOOK_WIDE); char *owned; const char *bytes = scr_str_utf8(s, &owned); (void)scr_stdio_write_raw(fd, bytes, s->len); @@ -500,5 +508,5 @@ static __attribute__((noinline)) bool scr_stdio_write_wide(int fd, const ScrStr bool scr_stdio_write_str(int fd, const ScrStr *s) { if (__builtin_expect(scr_wtf8_any(), 0) && !s->ascii) return scr_stdio_write_wide(fd, s); - return scr_stdio_write(fd, s->data, s->len); + return scr_stdio_write_text(fd, s->data, s->len); } diff --git a/packages/runtime/src/scr_console_native.c b/packages/runtime/src/scr_console_native.c index e11ffd3af..82dc476a1 100644 --- a/packages/runtime/src/scr_console_native.c +++ b/packages/runtime/src/scr_console_native.c @@ -489,8 +489,8 @@ void scr_console_clear(void) { if (!scr_process_is_tty(1)) return; const char *term = getenv("TERM"); if (term && strcmp(term, "dumb") == 0) return; - scr_stdio_write(1, "\x1b[1;1H", 6); - scr_stdio_write(1, "\x1b[0J", 4); + scr_stdio_write_text(1, "\x1b[1;1H", 6); + scr_stdio_write_text(1, "\x1b[0J", 4); } /* console.dir over a dyn value: inspect with custom inspection off. The diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index 78b7bb0f1..de1d12105 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -1507,9 +1507,19 @@ static ScrDyn *scr_stdio_invoke(void *ptr, ScrDyn *self, const char *name, return result; } -static bool scr_stdio_intercept(int fd, const void *data, size_t length, bool wide) { +/* Node's console error handler for a stream write (createWriteErrorHandler): + * console swallows write errors, so it ignores its argument. */ +static ScrDyn *scr_stdio_console_error_handler(ScrClosure *cb, ScrDyn *const *args, size_t argc) { + (void)cb; (void)args; (void)argc; + return scr_dyn_retain(scr_dyn_undefined()); +} + +static SCR_TL ScrDyn *scr_stdio_console_handler = NULL; + +static bool scr_stdio_intercept(int fd, const void *data, size_t length, int flags) { ScrStdio *stream = &scr_stdio_streams[fd == 2 ? 2 : 1]; if (!stream->write || scr_dyn_strict_eq(stream->write, scr_stdio_methods[STDIO_WRITE])) { + bool wide = (flags & SCR_STDIO_HOOK_WIDE) != 0; char *owned = NULL; if (wide && scr_wtf8_find_surrogate(data, length)) { owned = malloc(length ? length : 1); @@ -1522,12 +1532,22 @@ static bool scr_stdio_intercept(int fd, const void *data, size_t length, bool wi free(owned); return true; } - ScrStr *text = scr_str_new(data, length); - ScrDyn *chunk = scr_dyn_new_str(text); - scr_str_release(text); + ScrDyn *chunk; + if (flags & SCR_STDIO_HOOK_BYTES) { + ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)length); + memcpy(b->data, data, length); + chunk = scr_dyn_new_chunk(b); + scr_bytes_release(b); + } else { + ScrStr *text = scr_str_new(data, length); + chunk = scr_dyn_new_str(text); + scr_str_release(text); + } + ScrDyn *argv[2] = {chunk, scr_stdio_console_handler}; + size_t argc = (flags & SCR_STDIO_HOOK_CONSOLE) ? 2 : 1; scr_dyn_this_push(stream, SCR_DYNH_STDIO); ScrDyn *fn = scr_dyn_retain(stream->write); - ScrDyn *result = scr_dyn_call(fn, &chunk, 1, "write"); + ScrDyn *result = scr_dyn_call(fn, argv, argc, "write"); scr_dyn_release(fn); scr_dyn_this_pop(); bool accepted = result && scr_dyn_truthy(result); @@ -1562,6 +1582,8 @@ static void scr_stdio_cleanup(void) { scr_dyn_release(scr_stdio_methods[i]); scr_stdio_methods[i] = NULL; } + scr_dyn_release(scr_stdio_console_handler); + scr_stdio_console_handler = NULL; scr_stdio_initialized = false; } @@ -1582,6 +1604,7 @@ ScrDyn *scr_process_stdio(double fd) { scr_box_set_f64(cb->caps[0], i); scr_stdio_methods[i] = scr_dyn_new_func(cb, scr_stdio_method, i == STDIO_WRITE ? 3 : 1, "", scr_stdio_names[i]); } + scr_stdio_console_handler = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_stdio_console_error_handler, 1, "", ""); scr_stdio_write_hook = scr_stdio_intercept; } return scr_dyn_new_handle(&scr_stdio_streams[fd == 0 ? 0 : fd == 2 ? 2 : 1], SCR_DYNH_STDIO); diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index fa88873af..324a4c041 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -71,9 +71,16 @@ ScrStr *scr_process_cwd(void); /* +1 fresh (getcwd) */ bool scr_stdio_write(int fd, const void *data, size_t len); bool scr_stdio_write_str(int fd, const ScrStr *s); int scr_stdio_write_raw(int fd, const void *data, size_t len); -/* `wide` marks string bytes that may hold lone surrogates, which become - * U+FFFD when they reach the stream itself. */ -extern SCR_TL bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len, bool wide); +/* `flags` describes the chunk a patched write receives: SCR_STDIO_HOOK_WIDE + * marks string bytes that may hold lone surrogates (they become U+FFFD when + * they reach the stream itself), SCR_STDIO_HOOK_CONSOLE a console line + * (Node's console passes its error handler as the second argument), and + * SCR_STDIO_HOOK_BYTES a byte chunk (the replacement receives a Buffer). */ +enum { SCR_STDIO_HOOK_WIDE = 1, SCR_STDIO_HOOK_CONSOLE = 2, SCR_STDIO_HOOK_BYTES = 4 }; +extern SCR_TL bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len, int flags); +/* A string chunk given as bytes (UTF-8, no lone surrogates): what + * scr_stdio_write was for callers that write text. */ +bool scr_stdio_write_text(int fd, const void *data, size_t len); /* A worker created with the stdout or stderr option: answers true when it * took the bytes for fd 1 or 2 instead of the process descriptor. */ extern SCR_TL bool (*scr_stdio_capture)(int fd, const void *data, size_t len); diff --git a/tests/corpus/stdout-patch-console-arguments.js b/tests/corpus/stdout-patch-console-arguments.js new file mode 100644 index 000000000..7f73d785f --- /dev/null +++ b/tests/corpus/stdout-patch-console-arguments.js @@ -0,0 +1,29 @@ +// What a replaced process.stdout.write receives from console: Node's +// console writes each formatted line with stream.write(line, errorHandler), +// so the replacement sees a string and a function as its second argument +// for console.log, info, debug, dir, count, table and grouped lines alike; +// a direct write passes exactly its own arguments. The error handler accepts +// a null error. +const original = process.stdout.write; +const seen = []; +process.stdout.write = function (chunk, encodingOrCallback, callback) { + const kind = typeof chunk === "string" ? "string" : Buffer.isBuffer(chunk) ? "Buffer" : chunk.constructor.name; + seen.push(`${kind}:${JSON.stringify(String(chunk))}:${typeof encodingOrCallback}:${typeof callback}`); + if (typeof encodingOrCallback === "function") encodingOrCallback(null); + return true; +}; +console.log("line", 1); +console.info("info"); +console.debug("debug"); +console.log("%s=%d", "x", 2); +console.dir({ a: 1 }); +console.count(); +console.group("group"); +console.log("inside"); +console.groupEnd(); +console.table([{ a: 1 }]); +process.stdout.write("direct"); +process.stdout.write("hex", "utf8"); +process.stdout.write(Buffer.from("bytes")); +process.stdout.write = original; +console.log(seen.join("\n")); diff --git a/tests/harness/corpus-costs/stdout-patch-console-arguments.js.json b/tests/harness/corpus-costs/stdout-patch-console-arguments.js.json new file mode 100644 index 000000000..b5d3a2f90 --- /dev/null +++ b/tests/harness/corpus-costs/stdout-patch-console-arguments.js.json @@ -0,0 +1 @@ +{ "plain": 5460, "san": 7830 }