mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-20 05:40:17 +08:00
- Configure attached Windows consoles for UTF-8 while preserving redirected bytes and restoring host state. - Add cross-platform output coverage and a native pseudo-console regression. Fixes #321 Co-authored-by: euxaristia <25621994+euxaristia@users.noreply.github.com>
212 lines
8.2 KiB
C
212 lines
8.2 KiB
C
#include "scr_runtime.h"
|
|
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#ifdef _WIN32
|
|
#include <fcntl.h> /* _O_BINARY */
|
|
#include <io.h> /* _setmode, _fileno */
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
/* Set by scr_async.c at loop exhaustion; lives here (unconditionally) so
|
|
* binaries that link the console without the async runtime still link, and
|
|
* plain builds satisfy scr_async's reference. */
|
|
static SCR_TL long scr_abandoned_fibers = 0;
|
|
void scr_note_abandoned_fibers(long n) { scr_abandoned_fibers = n; }
|
|
|
|
#ifndef SCR_LIB
|
|
#ifdef SCR_RC_AUDIT
|
|
extern long scr_str_live_count(void); /* scr_string.c */
|
|
extern long scr_arr_live_count(void); /* scr_array.c */
|
|
extern long scr_map_live_count(void); /* scr_map.c */
|
|
extern long scr_box_live_count(void); /* scr_closure.c */
|
|
extern long scr_closure_live_count(void); /* scr_closure.c */
|
|
extern long scr_obj_live_count(void); /* scr_object.c */
|
|
extern long scr_union_live_count(void); /* scr_union.c */
|
|
extern long scr_dyn_live_count(void); /* scr_json.c */
|
|
extern long scr_bytes_live_count(void); /* scr_bytes.c */
|
|
#ifdef SCR_DYNAMIC
|
|
extern long scr_jsval_live_count(void); /* scr_island.c */
|
|
#endif
|
|
|
|
static void scr_rc_audit_at_exit(void) {
|
|
/* Fibers suspended forever at loop exhaustion (awaits nobody resolves —
|
|
* Node exits 0 there too) still hold their stacks and owned values by
|
|
* design; a strict audit would only report that deliberate abandonment. */
|
|
if (scr_abandoned_fibers > 0) {
|
|
fprintf(stderr, "scriptc RC audit skipped: %ld fiber(s) never resumed\n",
|
|
scr_abandoned_fibers);
|
|
return;
|
|
}
|
|
long strings = scr_str_live_count();
|
|
long arrays = scr_arr_live_count();
|
|
long maps = scr_map_live_count();
|
|
long boxes = scr_box_live_count();
|
|
long closures = scr_closure_live_count();
|
|
long objects = scr_obj_live_count();
|
|
long unions = scr_union_live_count();
|
|
long dyns = scr_dyn_live_count();
|
|
long bytes = scr_bytes_live_count();
|
|
#ifdef SCR_DYNAMIC
|
|
long jsvals = scr_jsval_live_count();
|
|
#else
|
|
long jsvals = 0;
|
|
#endif
|
|
if (strings != 0 || arrays != 0 || maps != 0 || boxes != 0 || closures != 0 ||
|
|
objects != 0 || unions != 0 || dyns != 0 || bytes != 0 || jsvals != 0) {
|
|
fflush(stdout);
|
|
fprintf(stderr,
|
|
"scriptc RC AUDIT FAILED: %ld heap string(s), %ld array(s), "
|
|
"%ld map(s), %ld box(es), %ld closure(s), %ld object(s), "
|
|
"%ld union(s), %ld dyn value(s), %ld bytes value(s), "
|
|
"%ld island value(s) live at exit\n",
|
|
strings, arrays, maps, boxes, closures, objects, unions, dyns,
|
|
bytes, jsvals);
|
|
_Exit(99);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static void scr_flush_at_exit(void) { fflush(stdout); }
|
|
|
|
static void scr_collect_cycles_at_exit(void) { scr_collect_cycles(); }
|
|
|
|
#ifdef _WIN32
|
|
static UINT scr_original_console_output_cp = 0;
|
|
|
|
static void scr_restore_console_output_cp(void) {
|
|
if (scr_original_console_output_cp != 0) {
|
|
(void)SetConsoleOutputCP(scr_original_console_output_cp);
|
|
}
|
|
}
|
|
|
|
static void scr_enable_utf8_console_output(void) {
|
|
DWORD mode;
|
|
HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
HANDLE err = GetStdHandle(STD_ERROR_HANDLE);
|
|
bool out_is_console = out != NULL && out != INVALID_HANDLE_VALUE &&
|
|
GetConsoleMode(out, &mode);
|
|
bool err_is_console = err != NULL && err != INVALID_HANDLE_VALUE &&
|
|
GetConsoleMode(err, &mode);
|
|
if (!out_is_console && !err_is_console) return;
|
|
|
|
UINT original = GetConsoleOutputCP();
|
|
if (original == 0 || original == CP_UTF8) return;
|
|
if (!SetConsoleOutputCP(CP_UTF8)) return;
|
|
|
|
/* The code page belongs to the shared console, not this process. Restore
|
|
* it after every scriptc exit hook has finished writing. */
|
|
scr_original_console_output_cp = original;
|
|
if (atexit(scr_restore_console_output_cp) != 0) {
|
|
(void)SetConsoleOutputCP(original);
|
|
scr_original_console_output_cp = 0;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void scr_runtime_abi_v1(void) {}
|
|
|
|
void scr_init(void) {
|
|
#ifdef _WIN32
|
|
/* The CRT opens std streams in TEXT mode, which writes \n as \r\n. Node
|
|
* on Windows does NOT translate — console.log emits \n whether stdout is
|
|
* a pipe or the console (libuv writes the bytes as given) — so the
|
|
* streams switch to binary here or every line would differ from the
|
|
* oracle by a byte. stdin too: readFileSync(0) must see the bytes on the
|
|
* pipe, not a CRLF-collapsed view. */
|
|
_setmode(_fileno(stdout), _O_BINARY);
|
|
_setmode(_fileno(stderr), _O_BINARY);
|
|
_setmode(_fileno(stdin), _O_BINARY);
|
|
/* Byte streams stay raw UTF-8. A real Windows console instead decodes
|
|
* narrow writes through its output code page, which commonly starts as
|
|
* CP437; select UTF-8 only while this process is attached and restore the
|
|
* shared setting at exit. */
|
|
scr_enable_utf8_console_output();
|
|
#endif
|
|
/* A private formatting buffer coalesces the several stdio calls needed to
|
|
* render one console line. Every JavaScript-visible stdout write flushes
|
|
* before returning, so this buffer is never observable as delayed output:
|
|
* Node hands each console.log/process.stdout.write chunk to its backing
|
|
* stream immediately. */
|
|
static char outbuf[1 << 16];
|
|
setvbuf(stdout, outbuf, _IOFBF, sizeof outbuf);
|
|
/* atexit is LIFO; registration order makes exit run: library cleanup
|
|
* (registered later, in scr_lib_init) → cycle collection → RC audit →
|
|
* flush → Windows console-code-page restore. The final collection frees
|
|
* cycles the program dropped, so the audit (and ASan's leak check) see
|
|
* them as freed, not leaked. */
|
|
atexit(scr_flush_at_exit);
|
|
#ifdef SCR_RC_AUDIT
|
|
atexit(scr_rc_audit_at_exit);
|
|
#endif
|
|
atexit(scr_collect_cycles_at_exit);
|
|
}
|
|
#endif /* !SCR_LIB — a library artifact never touches host stdio modes/buffering and
|
|
* registers no atexit handlers: scr_init and its exit hooks (the
|
|
* audit's _Exit(99) included) are executable-lane machinery; the
|
|
* library session reset lives in scr_library.c. */
|
|
|
|
/* ONE formatter for both console streams — console.error/warn print
|
|
* byte-identically to console.log in Node (same inspect rendering), only
|
|
* the stream differs. */
|
|
static void scr_console_write(FILE *out, size_t n, const ScrLogArg *args) {
|
|
char numbuf[32];
|
|
for (size_t i = 0; i < n; i++) {
|
|
if (i > 0) fputc(' ', out);
|
|
const ScrLogArg *a = &args[i];
|
|
switch (a->tag) {
|
|
case SCR_ARG_F64: {
|
|
/* console.log renders numbers via inspect, which distinguishes -0
|
|
* (String(-0) is "0", but console.log(-0) prints "-0"). */
|
|
if (a->v.f == 0 && signbit(a->v.f)) {
|
|
fputs("-0", out);
|
|
break;
|
|
}
|
|
size_t len = scr_f64_to_str(a->v.f, numbuf);
|
|
fwrite(numbuf, 1, len, out);
|
|
break;
|
|
}
|
|
case SCR_ARG_STR:
|
|
fwrite(a->v.s->data, 1, a->v.s->len, out);
|
|
break;
|
|
case SCR_ARG_BOOL:
|
|
fputs(a->v.b ? "true" : "false", out);
|
|
break;
|
|
}
|
|
}
|
|
fputc('\n', out);
|
|
/* Console methods submit one formatted chunk to their backing stream.
|
|
* Keep the C buffer only as a formatter coalescing detail: a caller
|
|
* observing a live child must see this line before the next JS turn. */
|
|
fflush(out);
|
|
}
|
|
|
|
void scr_console_log(size_t n, const ScrLogArg *args) {
|
|
scr_console_write(stdout, n, args);
|
|
}
|
|
|
|
/* console.error and console.warn. Flush stdout first so runtime-internal
|
|
* output that is still being assembled cannot cross this stderr line under
|
|
* a merged redirection (2>&1); JavaScript-visible stdout writes have already
|
|
* flushed themselves. */
|
|
void scr_console_error(size_t n, const ScrLogArg *args) {
|
|
fflush(stdout);
|
|
scr_console_write(stderr, n, args);
|
|
}
|
|
|
|
/* One JavaScript-visible raw write. Node's global console and process stream
|
|
* methods hand each chunk to the backing stream when called; they do not
|
|
* retain stdout until a later stderr write, a 64 KiB threshold, or process
|
|
* exit. The runtime remains synchronous internally, so its backpressure
|
|
* surface is still constantly true, but the bytes are observable promptly.
|
|
*
|
|
* stderr flushes any runtime-internal stdout fragment first to preserve the
|
|
* existing merged-fd ordering convention. */
|
|
void scr_stdio_write(int fd, const void *data, size_t len) {
|
|
FILE *out = fd == 2 ? stderr : stdout;
|
|
if (fd == 2) fflush(stdout);
|
|
if (len > 0) fwrite(data, 1, len, out);
|
|
fflush(out);
|
|
}
|