Files
scriptc/packages/runtime/src/scr_console.c
T

505 lines
19 KiB
C

#include "scr_runtime.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#ifdef _WIN32
#include <fcntl.h> /* _O_BINARY */
#include <io.h> /* _setmode, _fileno */
#include <windows.h>
#endif
/* POSIX executables submit each JavaScript-visible stdout/stderr chunk with
* write(2)/writev(2) on the descriptor itself. The C stream layer added a
* copy, several stream calls per console line, and — for the unbuffered
* stderr stream — one write(2) per argument, separator, and newline. Windows
* (CRT text/binary modes, console code pages), WASI, and library artifacts
* (whose host owns the process's C streams) keep the C stream path, now fed
* one formatted chunk per call. */
#if !defined(_WIN32) && !defined(__wasi__) && !defined(SCR_LIB)
#define SCR_STDIO_DIRECT 1
#include <sys/uio.h>
#include <unistd.h>
#if defined(__linux__)
#include <stdio_ext.h> /* __fpending: glibc and musl */
#endif
#else
#define SCR_STDIO_DIRECT 0
#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; }
bool scr_exit_frees(void) {
#if defined(SCR_RC_AUDIT) || defined(SCR_LSAN_IMMORTAL)
return true;
#else
if (!scr_context_is_main()) {
#ifdef SCR_WORKERS
scr_context_note_script_end();
#endif
return true;
}
static int forced = -1; /* the main thread reads it, once */
if (forced < 0) {
const char *value = getenv("SCRIPTC_EXIT_FREE");
forced = value != NULL && value[0] == '1';
}
return forced != 0;
#endif
}
#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_obj_lifetime_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 */
static void scr_rc_audit_at_exit(void) {
scr_buffer_pool_reset(); /* the pool's own slab reference */
/* 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;
}
/* ProgramLifetime instances (@scriptc/types) are never freed, and neither
* is anything they hold: a strict audit would only report that deliberate
* retention. */
long lifetime = scr_obj_lifetime_count();
if (lifetime > 0) {
fprintf(stderr, "scriptc RC audit skipped: %ld program-lifetime object(s) never freed\n",
lifetime);
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();
if (strings != 0 || arrays != 0 || maps != 0 || boxes != 0 || closures != 0 ||
objects != 0 || unions != 0 || dyns != 0 || bytes != 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) live at exit\n",
strings, arrays, maps, boxes, closures, objects, unions, dyns,
bytes);
_Exit(99);
}
}
#endif
static void scr_flush_at_exit(void) { fflush(stdout); }
static void scr_collect_cycles_at_exit(void) {
if (scr_exit_frees()) 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_v22(void) {}
void scr_init(void) {
#ifdef SCR_WORKERS
/* Generated inline exception polls read the active cell directly. */
scr_exc_swap_cell(NULL);
if (scr_context_is_main()) {
#endif
#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);
#ifdef SCR_WORKERS
}
#endif
/* 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. */
scr_atexit(scr_flush_at_exit);
#ifdef SCR_RC_AUDIT
scr_atexit(scr_rc_audit_at_exit);
#endif
scr_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. */
/* 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_capture)(int fd, const void *data, size_t len) = NULL;
SCR_TL bool scr_stdin_fed = false;
SCR_TL void (*scr_stdin_fed_attach)(void) = NULL;
SCR_TL bool (*scr_stdin_fed_waiting)(void) = NULL;
static SCR_TL ScrBytes **scr_stdin_fed_q;
static SCR_TL size_t scr_stdin_fed_head, scr_stdin_fed_len, scr_stdin_fed_cap;
static SCR_TL bool scr_stdin_fed_end;
void scr_stdin_fed_push(ScrBytes *chunk) {
if (scr_stdin_fed_end) {
if (chunk) scr_bytes_release(chunk);
return;
}
if (!chunk) {
scr_stdin_fed_end = true;
return;
}
if (scr_stdin_fed_head + scr_stdin_fed_len == scr_stdin_fed_cap) {
if (scr_stdin_fed_head > 0) {
memmove(scr_stdin_fed_q, scr_stdin_fed_q + scr_stdin_fed_head,
scr_stdin_fed_len * sizeof *scr_stdin_fed_q);
scr_stdin_fed_head = 0;
}
if (scr_stdin_fed_len == scr_stdin_fed_cap) {
scr_stdin_fed_cap = scr_stdin_fed_cap ? scr_stdin_fed_cap * 2 : 8;
scr_stdin_fed_q = realloc(scr_stdin_fed_q, scr_stdin_fed_cap * sizeof *scr_stdin_fed_q);
if (!scr_stdin_fed_q) scr_trap("scriptc: out of memory\n");
}
}
scr_stdin_fed_q[scr_stdin_fed_head + scr_stdin_fed_len++] = chunk;
}
ScrBytes *scr_stdin_fed_take(bool *ended) {
if (scr_stdin_fed_len == 0) {
*ended = scr_stdin_fed_end;
return NULL;
}
*ended = false;
ScrBytes *chunk = scr_stdin_fed_q[scr_stdin_fed_head++];
if (--scr_stdin_fed_len == 0) scr_stdin_fed_head = 0;
return chunk;
}
void scr_stdin_fed_clear(void) {
for (size_t i = 0; i < scr_stdin_fed_len; i++)
scr_bytes_release(scr_stdin_fed_q[scr_stdin_fed_head + i]);
free(scr_stdin_fed_q);
scr_stdin_fed_q = NULL;
scr_stdin_fed_head = scr_stdin_fed_len = scr_stdin_fed_cap = 0;
scr_stdin_fed_end = false;
scr_stdin_fed = false;
scr_stdin_fed_attach = NULL;
scr_stdin_fed_waiting = NULL;
}
/* Lines up to this size render on the C stack (fiber stacks are 256 KiB). */
#define SCR_CONSOLE_STACK_LINE 2048
#if SCR_STDIO_DIRECT
/* True when the C stdout buffer holds bytes: runtime-internal output that
* went through stdio without flushing. A direct write must submit them
* first so stdout keeps source order and merged (2>&1) redirections keep
* stdout-before-stderr order. Unknown platforms flush unconditionally. */
static inline bool scr_stdout_pending(void) {
#if defined(__linux__)
return __fpending(stdout) != 0;
#elif defined(__APPLE__)
return (stdout->_flags & __SWR) && stdout->_bf._base && stdout->_p > stdout->_bf._base;
#else
return true;
#endif
}
/* Submit every byte of iov[0..count) to fd. Short writes continue with the
* remainder and EINTR retries. Any other failure (EAGAIN on a non-blocking
* descriptor, EPIPE, EBADF, ...) returns its errno with the rest of the
* chunk dropped — what the C stream flush did before. */
static int scr_fd_writev_all(int fd, struct iovec *iov, int count) {
while (count > 0 && iov->iov_len == 0) iov++, count--;
while (count > 0) {
ssize_t written = count == 1 ? write(fd, iov->iov_base, iov->iov_len) : writev(fd, iov, count);
if (written < 0) {
if (errno == EINTR) continue;
return errno ? errno : EIO;
}
if (written == 0) return EIO;
size_t left = (size_t)written;
while (count > 0 && left >= iov->iov_len) left -= iov->iov_len, iov++, count--;
if (count > 0) {
iov->iov_base = (char *)iov->iov_base + left;
iov->iov_len -= left;
}
}
return 0;
}
/* Worker executables run several runtime threads that all write to the
* process descriptors. Hold the matching C stream's lock (recursive, so the
* pending-stdout flush inside is fine) around each direct submission, so a
* chunk that needs several write(2) calls, and the stdout flush that precedes
* it, cannot interleave with another thread's output. */
static inline FILE *scr_fd_stream_lock(int fd) {
#ifdef SCR_WORKERS
FILE *stream = fd == 2 ? stderr : stdout;
flockfile(stream);
return stream;
#else
(void)fd;
return NULL;
#endif
}
static inline void scr_fd_stream_unlock(FILE *stream) {
#ifdef SCR_WORKERS
funlockfile(stream);
#else
(void)stream;
#endif
}
#endif
/* Render args into line (capacity from scr_console_capacity): space-joined
* with a trailing newline. Returns the byte count. Tags may carry
* SCR_ARG_GLUE, so every kind test masks with SCR_ARG_KIND: a glued string
* part must reserve its full length, exactly as scr_console_render copies
* it. */
static size_t scr_console_capacity(size_t n, const ScrLogArg *args) {
size_t capacity = 1;
for (size_t i = 0; i < n; i++) {
size_t length = (args[i].tag & SCR_ARG_KIND) == SCR_ARG_STR ? args[i].v.s->len : 32;
if (length >= SIZE_MAX - capacity) scr_trap("scriptc: console output too large\n");
capacity += length + 1;
}
return capacity;
}
/* *wide records a string argument that may hold a lone surrogate (only
* once one may exist). Glued parts of one concatenation that complete a
* surrogate pair across their seam render as the paired character. */
static size_t scr_console_render(char *line, size_t n, const ScrLogArg *args, bool *wide) {
size_t used = 0;
bool lone = scr_wtf8_any(), joins = false, maybe = false;
for (size_t i = 0; i < n; i++) {
const ScrLogArg *arg = &args[i];
if (i && !(arg->tag & SCR_ARG_GLUE)) line[used++] = ' ';
int kind = arg->tag & SCR_ARG_KIND;
if (kind == SCR_ARG_STR) {
if (lone && !arg->v.s->ascii) {
maybe = true;
joins |= scr_wtf8_joins(line, used, arg->v.s->data, arg->v.s->len);
}
memcpy(line + used, arg->v.s->data, arg->v.s->len);
used += arg->v.s->len;
} else if (kind == SCR_ARG_BOOL) {
const char *text = arg->v.b ? "true" : "false";
size_t length = arg->v.b ? 4 : 5;
memcpy(line + used, text, length);
used += length;
} else if (kind == SCR_ARG_F64 && arg->v.f == 0 && signbit(arg->v.f)) {
/* console.log renders numbers via inspect, which distinguishes -0
* (String(-0) is "0", but console.log(-0) prints "-0"). A number
* inside a concatenation (SCR_ARG_NUM) keeps String()'s "0". */
memcpy(line + used, "-0", 2);
used += 2;
} else {
used += scr_f64_to_str(arg->v.f, line + used);
}
}
if (joins) used = scr_wtf8_fuse(line, used);
line[used++] = '\n';
*wide = maybe;
return used;
}
/* The global console's group indentation: console.group installs its
* indenting rewrite (scr_console_native.c) while a group is open, so
* programs without groups carry none of it. Console instances never use
* it. */
SCR_TL char *(*scr_console_indenter)(const char *line, size_t *used) = NULL;
/* Console methods submit one formatted chunk to their backing stream before
* returning, as Node's console does: a caller observing a live child sees
* this line before the next JS turn. Errors are ignored (the global Node
* console uses ignoreErrors=true for stream writes). */
static void scr_console_write(int fd, size_t n, const ScrLogArg *args, bool global) {
#if SCR_STDIO_DIRECT
/* A large single string goes out without a copy: string + newline in one
* vectored write. */
if (!scr_stdio_write_hook && !scr_stdio_capture && !(global && scr_console_indenter) && n == 1 &&
(args[0].tag & SCR_ARG_KIND) == SCR_ARG_STR &&
args[0].v.s->len >= SCR_CONSOLE_STACK_LINE) {
char *owned;
const char *bytes = scr_str_utf8(args[0].v.s, &owned);
FILE *locked = scr_fd_stream_lock(fd);
if (scr_stdout_pending()) fflush(stdout);
struct iovec iov[2] = {{(void *)bytes, args[0].v.s->len}, {(void *)"\n", 1}};
(void)scr_fd_writev_all(fd, iov, 2);
scr_fd_stream_unlock(locked);
scr_str_utf8_end(owned);
return;
}
#endif
char stack[SCR_CONSOLE_STACK_LINE];
size_t capacity = scr_console_capacity(n, args);
char *line = capacity <= sizeof stack ? stack : malloc(capacity);
if (!line) scr_trap("scriptc: out of memory\n");
bool wide = false;
size_t used = scr_console_render(line, n, args, &wide);
if (global && scr_console_indenter) {
char *indented = scr_console_indenter(line, &used);
if (line != stack) free(line);
line = indented;
}
if (scr_stdio_write_hook) {
/* A patched write receives the JavaScript string, lone surrogates kept. */
scr_stdio_write_hook(fd, line, used, wide);
if (scr_exc_pending()) scr_exc_clear();
} else {
if (wide) scr_wtf8_to_utf8(line, used);
(void)scr_stdio_write_raw(fd, line, used);
}
if (line != stack) free(line);
}
void scr_console_log(size_t n, const ScrLogArg *args) {
scr_console_write(1, n, args, true);
}
/* console.error and console.warn. Runtime-internal stdout output that is
* still buffered goes out first so it cannot cross this stderr line under a
* merged redirection (2>&1); JavaScript-visible stdout writes have already
* been submitted. */
void scr_console_error(size_t n, const ScrLogArg *args) {
scr_console_write(2, n, args, true);
}
void scr_console_log_parts(size_t n, const ScrLogArg *args) {
scr_console_write(1, n, args, true);
}
void scr_console_error_parts(size_t n, const ScrLogArg *args) {
scr_console_write(2, n, args, true);
}
/* A Console instance's line on process stdout (fd 1) or stderr (fd 2):
* the global console's group indentation does not apply. */
void scr_console_write_fd(int fd, size_t n, const ScrLogArg *args) {
scr_console_write(fd == 2 ? 2 : 1, n, args, false);
}
/* 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.
*
* Buffered runtime-internal stdout bytes go out first (for both streams) to
* preserve stdout order and the merged-fd ordering convention. */
int scr_stdio_write_raw(int fd, const void *data, size_t len) {
if (__builtin_expect(scr_stdio_capture != NULL, 0) && scr_stdio_capture(fd == 2 ? 2 : 1, data, len)) return 0;
#if SCR_STDIO_DIRECT
FILE *locked = scr_fd_stream_lock(fd == 2 ? 2 : 1);
if (scr_stdout_pending()) fflush(stdout);
struct iovec iov = {(void *)data, len};
int result = len == 0 ? 0 : scr_fd_writev_all(fd == 2 ? 2 : 1, &iov, 1);
scr_fd_stream_unlock(locked);
return result;
#else
FILE *out = fd == 2 ? stderr : stdout;
if (fd == 2) fflush(stdout);
if (len > 0 && fwrite(data, 1, len, out) != len) return errno ? errno : EIO;
if (fflush(out) != 0) return errno ? errno : EIO;
return 0;
#endif
}
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);
(void)scr_stdio_write_raw(fd, data, len);
return true;
}
/* 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);
char *owned;
const char *bytes = scr_str_utf8(s, &owned);
(void)scr_stdio_write_raw(fd, bytes, s->len);
scr_str_utf8_end(owned);
return true;
}
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);
}