mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
runtime: link the wave's new entries only into the programs that use them
Every new runtime entry this wave added landed in a unit that baseline programs link (scr_lib.c's write/copyFile flags, scr_bytes.c's string_decoder dyn forms, scr_events.c's prepend registries and stdin.isRaw, scr_async_dyn.c's uncaught prepend, scr_net.c's server.off). A changed unit changes its object's build-id and symbol metadata even when every retained function is byte-identical, so 1517 corpus programs reported a runtime change for code they never call. Each new entry now lives where only a program that uses it can reach it: - writeFile's `flag` option and the fs/promises twins move to scr_fs_flags.c (feature fsFlags); copyFile's mode form reuses scr_fs_cp.c's reflink walk, so it is that unit's SCR_FS_FLAGS build. - string_decoder's checked-dynamic chunk forms move to scr_bytes_strdec_dyn.c (feature strdecDyn). - the prepend registrations, stdin.isRaw and server.off are the SCR_PROCESS_EXTRAS builds of scr_events.c and scr_async_dyn.c and the SCR_NET_SERVER_OFF build of scr_net.c (feature processExtras). The prepend entry points reuse the append registration and move the new entry to the front, so no existing function changes and every default object stays byte-identical. The Windows copyFile arm stays beside the Win32 path helpers in scr_lib.c, inside its _WIN32 block. The source build (the -O1 -fsanitize=address lane) compiles these units from their sources rather than from the prebuilt pack, so RUNTIME_DEFINES names the same three builds the pack matrix selects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
26f15896eb
commit
9172fecf19
@@ -20,6 +20,9 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
|
||||
...(features.wasm ? { wasm: true } : {}),
|
||||
...(features.warningReplace ? { warningReplace: true } : {}),
|
||||
...(features.stdioExtended ? { stdioExtended: true } : {}),
|
||||
...(features.fsFlags ? { fsFlags: true } : {}),
|
||||
...(features.strdecDyn ? { strdecDyn: true } : {}),
|
||||
...(features.processExtras ? { processExtras: true } : {}),
|
||||
...(features.nodeIsland ? { nodeIsland: true } : {}),
|
||||
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
|
||||
...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}),
|
||||
|
||||
@@ -34,6 +34,15 @@ export interface NativeLinkFeatures {
|
||||
/** The program uses process.stdin.setEncoding, string 'data' listeners
|
||||
* or stdio 'resize' events: links the SCR_STDIO_EXTENDED events unit. */
|
||||
stdioExtended?: boolean;
|
||||
/** writeFile's `flag` disposition or copyFile's mode argument:
|
||||
* scr_fs_flags.c and the SCR_FS_FLAGS build of scr_fs_cp.c. */
|
||||
fsFlags?: boolean;
|
||||
/** string_decoder's checked-dynamic chunk forms: scr_bytes_strdec_dyn.c. */
|
||||
strdecDyn?: boolean;
|
||||
/** process.prependListener, stdin.isRaw or server.off: the
|
||||
* SCR_PROCESS_EXTRAS builds of scr_events.c and scr_async_dyn.c and the
|
||||
* SCR_NET_SERVER_OFF build of scr_net.c. */
|
||||
processExtras?: boolean;
|
||||
/** Calls into Node.js (@scriptc/node): scr_node_island.c and the
|
||||
* SCR_NODE_ISLANDS variant of the event loop unit. */
|
||||
nodeIsland?: boolean;
|
||||
|
||||
@@ -161,6 +161,15 @@ export interface CcOptions {
|
||||
warningReplace?: boolean;
|
||||
/** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */
|
||||
stdioExtended?: boolean;
|
||||
/** SCR_FS_FLAGS: writeFile's `flag` option and copyFile's mode argument
|
||||
* (scr_fs_flags.c and that build of scr_fs_cp.c). */
|
||||
fsFlags?: boolean;
|
||||
/** string_decoder's checked-dynamic chunk forms: compiles
|
||||
* scr_bytes_strdec_dyn.c. */
|
||||
strdecDyn?: boolean;
|
||||
/** SCR_PROCESS_EXTRAS / SCR_NET_SERVER_OFF: process.prependListener,
|
||||
* stdin.isRaw and server.off. */
|
||||
processExtras?: boolean;
|
||||
/** Calls into Node.js (@scriptc/node): compiles scr_node_island.c and
|
||||
* the event loop with SCR_NODE_ISLANDS (its helper slot). */
|
||||
nodeIsland?: boolean;
|
||||
|
||||
@@ -153,6 +153,11 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
),
|
||||
unit((u) => u.opts.copying, "scr_copying.c"),
|
||||
unit((u) => u.opts.fileHandle, "scr_file_handle.c"),
|
||||
// writeFile's `flag` option and the fs/promises twins; the copyFile mode
|
||||
// argument is the SCR_FS_FLAGS build of scr_fs_cp.c.
|
||||
unit((u) => u.opts.fsFlags === true, "scr_fs_flags.c"),
|
||||
// string_decoder's checked-dynamic chunk forms, beside their typed twins.
|
||||
unit((u) => u.opts.strdecDyn === true, "scr_bytes_strdec_dyn.c"),
|
||||
// win32 targets compile the libc-shim TU (stpcpy, arc4random_buf,
|
||||
// gmtime_r, strcasestr — the _WIN32 block in scr_runtime.h declares
|
||||
// them) and link advapi32 (the CSPRNG RtlGenRandom/SystemFunction036,
|
||||
@@ -412,6 +417,13 @@ const RUNTIME_DEFINES: readonly {
|
||||
{ define: "SCR_JS_TIER", when: (opts) => opts.dynTier === true },
|
||||
{ define: "SCR_WARNING_REPLACE", when: (opts) => opts.warningReplace },
|
||||
{ define: "SCR_STDIO_EXTENDED", when: (opts) => opts.stdioExtended },
|
||||
// The wave's prepend registrations and stdin.isRaw (scr_events.c,
|
||||
// scr_async_dyn.c), server.off (scr_net.c) and copyFile's mode form
|
||||
// (scr_fs_cp.c). The source build compiles these units from source, so the
|
||||
// defines must name the same builds runtime-pack-matrix.mjs selects.
|
||||
{ define: "SCR_PROCESS_EXTRAS", when: (opts) => opts.processExtras },
|
||||
{ define: "SCR_NET_SERVER_OFF", when: (opts) => opts.processExtras },
|
||||
{ define: "SCR_FS_FLAGS", when: (opts) => opts.fsFlags },
|
||||
// scr_async.c's native promise reactions, which the web streams unit uses,
|
||||
// scr_json.c's and scr_dyn_builtin_class.c's instanceof over its classes,
|
||||
// and scr_fetch.c's bodies of its streams.
|
||||
|
||||
@@ -65,6 +65,9 @@ export interface EarlyExecutableNativeFeatures {
|
||||
wasm?: boolean;
|
||||
warningReplace?: boolean;
|
||||
stdioExtended?: boolean;
|
||||
fsFlags?: boolean;
|
||||
strdecDyn?: boolean;
|
||||
processExtras?: boolean;
|
||||
nodeIsland?: boolean;
|
||||
moduleNamespace?: boolean;
|
||||
moduleNamespaceDyn?: boolean;
|
||||
|
||||
@@ -700,6 +700,9 @@ async function compileExecutableNative(
|
||||
...(features.wasm ? { wasm: true } : {}),
|
||||
...(features.warningReplace ? { warningReplace: true } : {}),
|
||||
...(features.stdioExtended ? { stdioExtended: true } : {}),
|
||||
...(features.fsFlags ? { fsFlags: true } : {}),
|
||||
...(features.strdecDyn ? { strdecDyn: true } : {}),
|
||||
...(features.processExtras ? { processExtras: true } : {}),
|
||||
...(features.nodeIsland ? { nodeIsland: true } : {}),
|
||||
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
|
||||
...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}),
|
||||
|
||||
@@ -1248,6 +1248,33 @@ const RUNTIME_FEATURE_RULES = {
|
||||
"tls.sockGetCertificate",
|
||||
],
|
||||
}),
|
||||
/** writeFile's `flag` disposition ("w" / "wx") and copyFile's mode
|
||||
* argument: scr_fs_flags.c and the SCR_FS_FLAGS build of scr_fs_cp.c.
|
||||
* Programs that never pass either keep both units' default objects. */
|
||||
fsFlags: rule({
|
||||
fns: [
|
||||
"fs.copyFileFlagsSync",
|
||||
"fs.writeFileFlagsSync",
|
||||
"fsp.copyFileFlags",
|
||||
"fsp.writeFileFlags",
|
||||
],
|
||||
}),
|
||||
/** string_decoder's checked-dynamic chunk forms (a stream "data" payload):
|
||||
* scr_bytes_strdec_dyn.c. */
|
||||
strdecDyn: rule({ fns: ["strdec.nextDyn", "strdec.writeDyn"] }),
|
||||
/** The process and stream event registries' extras — prependListener for
|
||||
* signals, 'exit' and uncaught exceptions, stdin.isRaw, and server.off:
|
||||
* the SCR_PROCESS_EXTRAS builds of scr_events.c and scr_async_dyn.c and
|
||||
* the SCR_NET_SERVER_OFF build of scr_net.c. */
|
||||
processExtras: rule({
|
||||
fns: [
|
||||
"net.serverOff",
|
||||
"process.prependExit",
|
||||
"process.prependSignal",
|
||||
"process.prependUncaughtException",
|
||||
"process.stdinIsRaw",
|
||||
],
|
||||
}),
|
||||
zlib: rule({ prefixes: ["zlib."] }),
|
||||
/** node:zlib's Brotli forms: zlib.brotliInstall, which %main calls when
|
||||
* the program uses Brotli (scr_brotli.c). */
|
||||
@@ -1289,6 +1316,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
eventsOnceSignal: false,
|
||||
fetch: false,
|
||||
fileHandle: false,
|
||||
fsFlags: false,
|
||||
fsWatch: false,
|
||||
http2: false,
|
||||
http: false,
|
||||
@@ -1308,6 +1336,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
pki: false,
|
||||
ed25519: false,
|
||||
processEvents: false,
|
||||
processExtras: false,
|
||||
qs: false,
|
||||
regex: false,
|
||||
searchParams: false,
|
||||
@@ -1316,6 +1345,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
moduleNamespace: false,
|
||||
moduleNamespaceDyn: false,
|
||||
stream: false,
|
||||
strdecDyn: false,
|
||||
streamDyn: false,
|
||||
handleProps: false,
|
||||
childLifecycle: false,
|
||||
|
||||
@@ -62,6 +62,12 @@ export const BASE_RUNTIME_SOURCES = [
|
||||
export const OPTIONAL_RUNTIME_SOURCES = [
|
||||
["scr_copying.c", "copying"],
|
||||
["scr_file_handle.c", "fileHandle"],
|
||||
// writeFile's `flag` disposition and the fs/promises twins; the copyFile
|
||||
// mode form is the SCR_FS_FLAGS build of scr_fs_cp.c.
|
||||
["scr_fs_flags.c", "fsFlags"],
|
||||
// string_decoder's checked-dynamic chunk forms, apart from scr_bytes.c's
|
||||
// typed twins so a typed program keeps that object byte for byte.
|
||||
["scr_bytes_strdec_dyn.c", "strdecDyn"],
|
||||
["scr_regex.c", "regex"],
|
||||
["scr_glob.c", "regex"],
|
||||
["scr_idna.c", "regex"],
|
||||
@@ -278,6 +284,37 @@ export function variantsFor(source) {
|
||||
when: { warningReplace: true },
|
||||
defines: ["SCR_WARNING_REPLACE"],
|
||||
});
|
||||
// The process-prepend registries (processExtras) add
|
||||
// scr_process_prepend_uncaught_exception, in a build of its own so the
|
||||
// default object stays put for every program.
|
||||
variants.push({
|
||||
id: "process-extras",
|
||||
when: { processExtras: true },
|
||||
defines: ["SCR_PROCESS_EXTRAS"],
|
||||
});
|
||||
variants.push({
|
||||
id: "warning-replace-process-extras",
|
||||
when: { warningReplace: true, processExtras: true },
|
||||
defines: ["SCR_WARNING_REPLACE", "SCR_PROCESS_EXTRAS"],
|
||||
});
|
||||
}
|
||||
// copyFileSync's mode argument reuses scr_fs_cp.c's reflink walk, so the
|
||||
// entry lives in a build of this unit rather than a unit of its own.
|
||||
if (source === "scr_fs_cp.c") {
|
||||
variants.push({
|
||||
id: "flags",
|
||||
when: { fsFlags: true },
|
||||
defines: ["SCR_FS_FLAGS"],
|
||||
});
|
||||
}
|
||||
// server.off / removeListener: the listener-removal entry, in a build of
|
||||
// its own so a program that never removes a listener keeps the default.
|
||||
if (source === "scr_net.c") {
|
||||
variants.push({
|
||||
id: "server-off",
|
||||
when: { processExtras: true },
|
||||
defines: ["SCR_NET_SERVER_OFF"],
|
||||
});
|
||||
}
|
||||
// Programs with web streams link the promise unit with native promise
|
||||
// reactions, the builtin-class unit whose instanceof knows their classes
|
||||
@@ -331,6 +368,29 @@ export function variantsFor(source) {
|
||||
when: { stdioExtended: true, nodeIsland: true },
|
||||
defines: ["SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS"],
|
||||
});
|
||||
// processExtras adds the prepend registrations and stdin's raw-mode
|
||||
// flag; the combinations above then need their own builds, since the
|
||||
// variant with the most conditions is the one selected.
|
||||
variants.push({
|
||||
id: "process-extras",
|
||||
when: { processExtras: true },
|
||||
defines: ["SCR_PROCESS_EXTRAS"],
|
||||
});
|
||||
variants.push({
|
||||
id: "process-extras-stdio-extended",
|
||||
when: { processExtras: true, stdioExtended: true },
|
||||
defines: ["SCR_PROCESS_EXTRAS", "SCR_STDIO_EXTENDED"],
|
||||
});
|
||||
variants.push({
|
||||
id: "process-extras-node-island",
|
||||
when: { processExtras: true, nodeIsland: true },
|
||||
defines: ["SCR_PROCESS_EXTRAS", "SCR_NODE_ISLANDS"],
|
||||
});
|
||||
variants.push({
|
||||
id: "process-extras-stdio-extended-node-island",
|
||||
when: { processExtras: true, stdioExtended: true, nodeIsland: true },
|
||||
defines: ["SCR_PROCESS_EXTRAS", "SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS"],
|
||||
});
|
||||
}
|
||||
// Programs with Node islands link the event loop with the Node island slot;
|
||||
// every other program keeps the default object, byte for byte.
|
||||
|
||||
@@ -3234,11 +3234,6 @@ ScrPromise *scr_fsp_write_file_mode(ScrStr *path, ScrStr *data, double mode) {
|
||||
return scr_promise_settled_void();
|
||||
}
|
||||
|
||||
ScrPromise *scr_fsp_write_file_flags(ScrStr *path, ScrStr *data, double mode, double exclusive) {
|
||||
scr_fs_write_file_flags(path, data, mode, exclusive);
|
||||
return scr_promise_settled_void();
|
||||
}
|
||||
|
||||
ScrPromise *scr_fsp_mkdir(ScrStr *path) {
|
||||
scr_fs_mkdir(path);
|
||||
return scr_promise_settled_void();
|
||||
@@ -3299,11 +3294,6 @@ ScrPromise *scr_fsp_copyfile(ScrStr *src, ScrStr *dest) {
|
||||
return scr_promise_settled_void();
|
||||
}
|
||||
|
||||
ScrPromise *scr_fsp_copyfile_flags(ScrStr *src, ScrStr *dest, double mode) {
|
||||
scr_fs_copyfile_flags(src, dest, mode);
|
||||
return scr_promise_settled_void();
|
||||
}
|
||||
|
||||
/* readFile(path, encoding | { encoding }): the encoding is checked first,
|
||||
* so an unknown name rejects before the read. */
|
||||
ScrPromise *scr_fsp_read_file_encoded(ScrStr *path, ScrStr *encoding) {
|
||||
|
||||
@@ -341,11 +341,7 @@ static bool scr_rej_check_listener(ScrDyn *fn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* The registration body: `prepend` puts the listener in FRONT of the
|
||||
* registry's existing entries (process.prependListener — Node's
|
||||
* EventEmitter order), the plain form appends. */
|
||||
static void scr_rej_push_at(ScrRejListener **list, size_t *n, size_t *cap, ScrDyn *fn, bool once,
|
||||
bool prepend) {
|
||||
static void scr_rej_push(ScrRejListener **list, size_t *n, size_t *cap, ScrDyn *fn, bool once) {
|
||||
if (*n == *cap) {
|
||||
*cap = *cap ? *cap * 2 : 4;
|
||||
*list = realloc(*list, *cap * sizeof **list);
|
||||
@@ -354,18 +350,7 @@ static void scr_rej_push_at(ScrRejListener **list, size_t *n, size_t *cap, ScrDy
|
||||
ScrRejState *state = calloc(1, sizeof *state);
|
||||
if (!state) scr_ad_oom();
|
||||
state->refs = 1;
|
||||
ScrRejListener entry = (ScrRejListener){scr_dyn_retain(fn), once, ++scr_rej_id, state};
|
||||
if (prepend) {
|
||||
memmove(*list + 1, *list, *n * sizeof **list);
|
||||
(*list)[0] = entry;
|
||||
(*n)++;
|
||||
} else {
|
||||
(*list)[(*n)++] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
static void scr_rej_push(ScrRejListener **list, size_t *n, size_t *cap, ScrDyn *fn, bool once) {
|
||||
scr_rej_push_at(list, n, cap, fn, once, false);
|
||||
(*list)[(*n)++] = (ScrRejListener){scr_dyn_retain(fn), once, ++scr_rej_id, state};
|
||||
}
|
||||
|
||||
static void scr_rej_remove(ScrRejListener *list, size_t *n, ScrDyn *fn) {
|
||||
@@ -482,32 +467,32 @@ static void scr_uncaught_teardown(void) {
|
||||
scr_uncaught_sync_hook();
|
||||
}
|
||||
|
||||
/* Arm the registry's exit-time teardown once, whichever entry point
|
||||
* registers first. */
|
||||
static void scr_uncaught_arm(void) {
|
||||
static SCR_TL bool armed = false;
|
||||
if (armed) return;
|
||||
armed = true;
|
||||
scr_atexit(scr_uncaught_teardown);
|
||||
}
|
||||
|
||||
void scr_process_on_uncaught_exception(ScrDyn *fn, bool once, bool monitor) {
|
||||
if (!scr_rej_check_listener(fn)) return;
|
||||
scr_uncaught_arm();
|
||||
static SCR_TL bool armed = false;
|
||||
if (!armed) { armed = true; scr_atexit(scr_uncaught_teardown); }
|
||||
size_t k = monitor ? 1 : 0;
|
||||
scr_rej_push(&scr_uncaught_ls[k], &scr_uncaught_n[k], &scr_uncaught_cap[k], fn, once);
|
||||
scr_uncaught_sync_hook();
|
||||
}
|
||||
|
||||
#ifdef SCR_PROCESS_EXTRAS
|
||||
/* process.prependListener("uncaughtException"[Monitor], fn): the same
|
||||
* listener in front of the registry's existing entries. */
|
||||
* listener in FRONT of the registry's existing entries (Node's
|
||||
* EventEmitter order). The append path above registers it and the new entry
|
||||
* then moves to the front, so the two share one registration; a refused
|
||||
* listener (a non-function) left the list alone and nothing moves. */
|
||||
void scr_process_prepend_uncaught_exception(ScrDyn *fn, bool monitor) {
|
||||
if (!scr_rej_check_listener(fn)) return;
|
||||
scr_uncaught_arm();
|
||||
size_t k = monitor ? 1 : 0;
|
||||
scr_rej_push_at(&scr_uncaught_ls[k], &scr_uncaught_n[k], &scr_uncaught_cap[k], fn, false, true);
|
||||
scr_uncaught_sync_hook();
|
||||
size_t before = scr_uncaught_n[k];
|
||||
scr_process_on_uncaught_exception(fn, false, monitor);
|
||||
if (scr_uncaught_n[k] != before + 1) return;
|
||||
ScrRejListener entry = scr_uncaught_ls[k][scr_uncaught_n[k] - 1];
|
||||
memmove(scr_uncaught_ls[k] + 1, scr_uncaught_ls[k],
|
||||
(scr_uncaught_n[k] - 1) * sizeof *scr_uncaught_ls[k]);
|
||||
scr_uncaught_ls[k][0] = entry;
|
||||
}
|
||||
#endif
|
||||
|
||||
void scr_process_off_uncaught_exception(ScrDyn *fn, bool monitor) {
|
||||
if (!scr_rej_check_listener(fn)) return;
|
||||
|
||||
@@ -1509,45 +1509,6 @@ double scr_strdec_next(const ScrStr *enc, double pending, const ScrBytes *chunk)
|
||||
return hold;
|
||||
}
|
||||
|
||||
/* The bytes view of a checked-dynamic write chunk, or NULL for a string
|
||||
* chunk (Node's write returns it unchanged, never touching the decoder).
|
||||
* Anything that is not a string or an ArrayBufferView throws Node's
|
||||
* ERR_INVALID_ARG_TYPE for "buf". */
|
||||
static ScrBytes *scr_strdec_dyn_view(const ScrDyn *chunk, bool *is_string) {
|
||||
*is_string = chunk->kind == SCR_DYN_STR;
|
||||
if (*is_string) return NULL;
|
||||
if (
|
||||
chunk->kind == SCR_DYN_TYPED_REF || chunk->kind == SCR_DYN_BYTES ||
|
||||
scr_buffer_storage_is(chunk)) {
|
||||
return scr_bytes_buffer_source(chunk);
|
||||
}
|
||||
scr_dyn_arg_type_fail("buf", "an instance of Buffer, TypedArray, or DataView", chunk);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* decoder.write(chunk) where the chunk crossed the checked-dynamic
|
||||
* boundary (`stream.on("data", (data) => decoder.write(data))`). */
|
||||
ScrStr *scr_strdec_write_dyn(const ScrStr *enc, double pending, const ScrDyn *chunk) {
|
||||
bool is_string = false;
|
||||
ScrBytes *view = scr_strdec_dyn_view(chunk, &is_string);
|
||||
if (is_string) return scr_str_retain(chunk->v.str);
|
||||
if (!view) return NULL;
|
||||
ScrStr *out = scr_strdec_write(enc, pending, view);
|
||||
scr_bytes_release(view);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* The decoder state after that write: a string chunk leaves it alone. */
|
||||
double scr_strdec_next_dyn(const ScrStr *enc, double pending, const ScrDyn *chunk) {
|
||||
bool is_string = false;
|
||||
ScrBytes *view = scr_strdec_dyn_view(chunk, &is_string);
|
||||
if (is_string) return pending;
|
||||
if (!view) return 0;
|
||||
double hold = scr_strdec_next(enc, pending, view);
|
||||
scr_bytes_release(view);
|
||||
return hold;
|
||||
}
|
||||
|
||||
/* decoder.end(): the buffered partial flushes — utf8's truncated sequence
|
||||
* becomes its replacement chars (the maximal-subpart rule), base64's
|
||||
* remainder encodes (padded; unpadded for base64url), utf16le's held
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/* string_decoder's checked-dynamic chunk forms.
|
||||
*
|
||||
* A stream's "data" payload crosses the checked-dynamic boundary, so
|
||||
* decoder.write and decoder.end receive it as an ScrDyn: a string passes
|
||||
* through unchanged without touching the buffered state, an ArrayBufferView
|
||||
* (Buffer, TypedArray, DataView) decodes through scr_bytes.c's per-encoding
|
||||
* helpers with the packed-f64 pending state, and anything else throws Node's
|
||||
* ERR_INVALID_ARG_TYPE for "buf". They live in their own unit because the
|
||||
* static twins in scr_bytes.c belong only to programs whose chunks are
|
||||
* already typed; those programs keep that object byte for byte. */
|
||||
#include "scr_runtime.h"
|
||||
|
||||
/* The bytes view of a checked-dynamic write chunk, or NULL for a string
|
||||
* chunk (Node's write returns it unchanged, never touching the decoder).
|
||||
* Anything that is not a string or an ArrayBufferView throws Node's
|
||||
* ERR_INVALID_ARG_TYPE for "buf". */
|
||||
static ScrBytes *scr_strdec_dyn_view(const ScrDyn *chunk, bool *is_string) {
|
||||
*is_string = chunk->kind == SCR_DYN_STR;
|
||||
if (*is_string) return NULL;
|
||||
if (
|
||||
chunk->kind == SCR_DYN_TYPED_REF || chunk->kind == SCR_DYN_BYTES ||
|
||||
scr_buffer_storage_is(chunk)) {
|
||||
return scr_bytes_buffer_source(chunk);
|
||||
}
|
||||
scr_dyn_arg_type_fail("buf", "an instance of Buffer, TypedArray, or DataView", chunk);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* decoder.write(chunk) where the chunk crossed the checked-dynamic
|
||||
* boundary (`stream.on("data", (data) => decoder.write(data))`). */
|
||||
ScrStr *scr_strdec_write_dyn(const ScrStr *enc, double pending, const ScrDyn *chunk) {
|
||||
bool is_string = false;
|
||||
ScrBytes *view = scr_strdec_dyn_view(chunk, &is_string);
|
||||
if (is_string) return scr_str_retain(chunk->v.str);
|
||||
if (!view) return NULL;
|
||||
ScrStr *out = scr_strdec_write(enc, pending, view);
|
||||
scr_bytes_release(view);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* The decoder state after that write: a string chunk leaves it alone. */
|
||||
double scr_strdec_next_dyn(const ScrStr *enc, double pending, const ScrDyn *chunk) {
|
||||
bool is_string = false;
|
||||
ScrBytes *view = scr_strdec_dyn_view(chunk, &is_string);
|
||||
if (is_string) return pending;
|
||||
if (!view) return 0;
|
||||
double hold = scr_strdec_next(enc, pending, view);
|
||||
scr_bytes_release(view);
|
||||
return hold;
|
||||
}
|
||||
@@ -192,10 +192,7 @@ static bool scr_signal_check(ScrStr *name, ScrDyn *cb) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* The registration body both entry points share: `prepend` puts the
|
||||
* listener in FRONT of the signal's existing ones (process.prependListener
|
||||
* — Node's EventEmitter order), the plain form appends. */
|
||||
static void scr_signal_add(ScrStr *name, ScrDyn *cb, bool once, bool prepend) {
|
||||
void scr_signal_on(ScrStr *name, ScrDyn *cb, bool once) {
|
||||
if (!scr_signal_check(name, cb)) return;
|
||||
if (!scr_context_is_main()) return;
|
||||
#ifdef __wasi__
|
||||
@@ -245,25 +242,37 @@ static void scr_signal_add(ScrStr *name, ScrDyn *cb, bool once, bool prepend) {
|
||||
reg->ls = realloc(reg->ls, reg->cap * sizeof *reg->ls);
|
||||
if (!reg->ls) scr_events_oom();
|
||||
}
|
||||
ScrSigListener entry =
|
||||
(ScrSigListener){scr_dyn_retain(cb), scr_str_retain(name), once, ++scr_sig_id};
|
||||
if (prepend) {
|
||||
memmove(reg->ls + 1, reg->ls, reg->n * sizeof *reg->ls);
|
||||
reg->ls[0] = entry;
|
||||
reg->n++;
|
||||
} else {
|
||||
reg->ls[reg->n++] = entry;
|
||||
}
|
||||
reg->ls[reg->n++] = (ScrSigListener){scr_dyn_retain(cb), scr_str_retain(name), once, ++scr_sig_id};
|
||||
#endif
|
||||
}
|
||||
|
||||
void scr_signal_on(ScrStr *name, ScrDyn *cb, bool once) {
|
||||
scr_signal_add(name, cb, once, false);
|
||||
#ifdef SCR_PROCESS_EXTRAS
|
||||
/* process.prependListener(<signal>, cb): the same listener in FRONT of the
|
||||
* signal's existing registrations (Node's EventEmitter order). The append
|
||||
* path above registers it and the new entry then moves to the front, so the
|
||||
* two share one registration; a refused or inert registration (a throw, a
|
||||
* non-main context) left the list alone and nothing moves. */
|
||||
void scr_signal_prepend(ScrStr *name, ScrDyn *cb) {
|
||||
#ifdef __wasi__
|
||||
scr_signal_on(name, cb, false); /* traps, exactly as the append form does */
|
||||
#else
|
||||
int sig = scr_signal_from_name(name);
|
||||
if (sig < 0) sig = 0;
|
||||
ScrSigReg *reg = scr_sig_regs;
|
||||
while (reg && reg->sig != sig) reg = reg->next;
|
||||
size_t before = reg ? reg->n : 0;
|
||||
scr_signal_on(name, cb, false);
|
||||
if (!reg) {
|
||||
reg = scr_sig_regs;
|
||||
while (reg && reg->sig != sig) reg = reg->next;
|
||||
}
|
||||
if (!reg || reg->n != before + 1) return;
|
||||
ScrSigListener entry = reg->ls[reg->n - 1];
|
||||
memmove(reg->ls + 1, reg->ls, (reg->n - 1) * sizeof *reg->ls);
|
||||
reg->ls[0] = entry;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* process.prependListener(<signal>, cb): the same listener in front of the
|
||||
* signal's existing registrations. */
|
||||
void scr_signal_prepend(ScrStr *name, ScrDyn *cb) { scr_signal_add(name, cb, false, true); }
|
||||
#endif
|
||||
|
||||
static void scr_sig_reg_drop(ScrSigReg *reg) {
|
||||
if (reg->sig > 0) {
|
||||
@@ -1708,6 +1717,31 @@ void scr_process_on_exit(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, doub
|
||||
scr_exit_n++;
|
||||
}
|
||||
|
||||
#ifdef SCR_PROCESS_EXTRAS
|
||||
/* process.stdin.isRaw: the raw-mode flag of a TTY stdin (1/0); a
|
||||
* pipe-backed stdin has no such flag and answers Node's undefined, which
|
||||
* the emitter's union construction spells -1. */
|
||||
double scr_process_stdin_is_raw(void) {
|
||||
if (!scr_context_is_main()) return -1;
|
||||
if (!scr_process_is_tty(0)) return -1;
|
||||
return scr_stdio_raw ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
/* process.prependListener("exit", cb): the same listener in FRONT of the
|
||||
* registry's existing entries (Node's EventEmitter order). The append path
|
||||
* above registers it and the new entry then moves to the front, so the two
|
||||
* share one registration; a registry already swept at exit released the
|
||||
* closure and left the list empty, and nothing moves. */
|
||||
void scr_process_prepend_exit(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, double)) {
|
||||
size_t before = scr_exit_n;
|
||||
scr_process_on_exit(cb, fn, false);
|
||||
if (scr_exit_n != before + 1) return;
|
||||
ScrExitListener entry = scr_exit_ls[scr_exit_n - 1];
|
||||
memmove(scr_exit_ls + 1, scr_exit_ls, (scr_exit_n - 1) * sizeof *scr_exit_ls);
|
||||
scr_exit_ls[0] = entry;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
/* Island code's 'exit' listeners, at their registration position among
|
||||
* the program's own. */
|
||||
@@ -1734,37 +1768,6 @@ void scr_process_off_exit_island(uint32_t island) {
|
||||
}
|
||||
#endif
|
||||
|
||||
/* process.stdin.isRaw: the raw-mode flag of a TTY stdin (1/0); a
|
||||
* pipe-backed stdin has no such flag and answers Node's undefined, which
|
||||
* the emitter's union construction spells -1. */
|
||||
double scr_process_stdin_is_raw(void) {
|
||||
if (!scr_context_is_main()) return -1;
|
||||
if (!scr_process_is_tty(0)) return -1;
|
||||
return scr_stdio_raw ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
/* process.prependListener("exit", cb): the same listener in front of the
|
||||
* registry's existing entries. */
|
||||
void scr_process_prepend_exit(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, double)) {
|
||||
if (scr_exit_ran) {
|
||||
scr_closure_release(cb);
|
||||
return;
|
||||
}
|
||||
if (scr_exit_n == scr_exit_cap) {
|
||||
scr_exit_cap = scr_exit_cap ? scr_exit_cap * 2 : 2;
|
||||
scr_exit_ls = realloc(scr_exit_ls, scr_exit_cap * sizeof *scr_exit_ls);
|
||||
if (!scr_exit_ls) scr_events_oom();
|
||||
}
|
||||
memmove(scr_exit_ls + 1, scr_exit_ls, scr_exit_n * sizeof *scr_exit_ls);
|
||||
scr_exit_ls[0].cb = cb;
|
||||
scr_exit_ls[0].fn = fn;
|
||||
scr_exit_ls[0].once = false;
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
scr_exit_ls[0].island = 0;
|
||||
#endif
|
||||
scr_exit_n++;
|
||||
}
|
||||
|
||||
void scr_process_off_exit(ScrClosure *cb /*borrowed*/) {
|
||||
for (size_t i = 0; i < scr_exit_n; i++) {
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
|
||||
@@ -589,14 +589,19 @@ static void cp_copyfile(const ScrStr *src, const ScrStr *dest, int mode, const c
|
||||
if (e) scr_fs_throw2(e, syscall, src, dest);
|
||||
}
|
||||
|
||||
#if defined(SCR_FS_FLAGS) && !defined(_WIN32)
|
||||
/* copyFileSync(src, dest, mode) and copyFile(src, dest, mode): libuv's
|
||||
* uv_fs_copyfile with Node's flag bits (the C++ binding's range check and
|
||||
* message: any value in [0, 7], truncated to an integer — 1.5 is EXCL).
|
||||
* COPYFILE_EXCL (1) fails EEXIST on an existing destination,
|
||||
* COPYFILE_FICLONE (2) requests a reflink and falls back to a copy, and
|
||||
* COPYFILE_FICLONE_FORCE (4) fails where no reflink is possible. This file
|
||||
* excludes _WIN32, whose arm (CopyFileW and its ENOSYS for FICLONE_FORCE)
|
||||
* lives in scr_lib.c beside the Win32 fs helpers. */
|
||||
* COPYFILE_FICLONE_FORCE (4) fails where no reflink is possible.
|
||||
*
|
||||
* This is the SCR_FS_FLAGS build of the unit: cp_copyfile is the reflink
|
||||
* walk, and keeping the entry behind a define leaves every program that
|
||||
* never passes a mode with the default object, byte for byte. The Windows
|
||||
* arm (CopyFileW and its ENOSYS for FICLONE_FORCE) sits beside the Win32 fs
|
||||
* helpers in scr_lib.c. */
|
||||
void scr_fs_copyfile_flags(ScrStr *src, ScrStr *dest, double mode) {
|
||||
if (!(mode >= 0 && mode <= 7)) {
|
||||
static const char text[] = "mode is out of range: >= 0 && <= 7";
|
||||
@@ -607,6 +612,7 @@ void scr_fs_copyfile_flags(ScrStr *src, ScrStr *dest, double mode) {
|
||||
if (!scr_fs_path_ok(dest, "dest")) return;
|
||||
cp_copyfile(src, dest, (int)mode, "copyfile");
|
||||
}
|
||||
#endif
|
||||
|
||||
/* utimes(dest, atime, mtime) in seconds, through the utimesSync path. */
|
||||
static bool cp_utime(const ScrStr *dest, double atime, double mtime) {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/* fs.writeFile's `flag` option ("w", "wx") and the fs/promises twins of the
|
||||
* flag-aware write and copy forms.
|
||||
*
|
||||
* The write is scr_lib.c's scr_fs_write_file_mode body with the disposition
|
||||
* flag added (the same portable open(2) calls, so it builds for every
|
||||
* target). It lives in this unit rather than beside that function so a
|
||||
* program that never passes `flag` keeps scr_lib.c's object byte for byte;
|
||||
* the fs.copyFile mode form rides scr_fs_cp.c's SCR_FS_FLAGS build, whose
|
||||
* reflink helper it reuses, and that form's Windows arm sits beside the
|
||||
* Win32 path helpers in scr_lib.c. */
|
||||
#include "scr_runtime.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
/* O_BINARY: Windows-only (CRT text mode would translate \n on fd writes);
|
||||
* zero everywhere else. scr_lib.c carries the same guard for its writes. */
|
||||
#ifndef O_BINARY
|
||||
#define O_BINARY 0
|
||||
#endif
|
||||
|
||||
/* Node's mode validation for the options-record write: an integer in
|
||||
* open(2)'s mode_t range. Throws ERR_OUT_OF_RANGE and answers false. */
|
||||
static bool scr_fs_write_mode_ok(double mode) {
|
||||
char recv[48], msg[176];
|
||||
scr_num_received(mode, recv);
|
||||
if (!(isfinite(mode) && trunc(mode) == mode)) {
|
||||
int len = snprintf(msg, sizeof msg,
|
||||
"The value of \"mode\" is out of range. It must be an integer. Received %s",
|
||||
recv);
|
||||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||||
return false;
|
||||
}
|
||||
if (mode < 0 || mode > 4294967295.0) {
|
||||
int len = snprintf(msg, sizeof msg,
|
||||
"The value of \"mode\" is out of range. It must be >= 0 && <= 4294967295. Received %s",
|
||||
recv);
|
||||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* The open(2) write behind the flag form: O_TRUNC plus `extra` (only O_EXCL
|
||||
* today), creation mode applying at CREATION only (umask applying) — an
|
||||
* existing file keeps its permissions, exactly Node (which never chmods
|
||||
* here). Same error shapes as the plain form. */
|
||||
static void scr_fs_write_file_open(ScrStr *path, ScrStr *data, mode_t mode, int extra) {
|
||||
if (!scr_fs_path_ok(path, "path")) return;
|
||||
/* O_BINARY: zero on POSIX; on Windows it keeps the CRT from translating
|
||||
* \n in these byte-exact writes (fopen's "wb" path already does). */
|
||||
int fd = open(path->data, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY | extra, mode);
|
||||
if (fd < 0) {
|
||||
scr_fs_throw(errno, "open", path);
|
||||
return;
|
||||
}
|
||||
char *owned;
|
||||
const char *bytes = scr_str_utf8(data, &owned);
|
||||
size_t at = 0;
|
||||
while (at < data->len) {
|
||||
ssize_t wrote = write(fd, bytes + at, data->len - at);
|
||||
if (wrote < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
int e = errno;
|
||||
close(fd);
|
||||
scr_str_utf8_end(owned);
|
||||
scr_fs_throw(e, "write", path);
|
||||
return;
|
||||
}
|
||||
at += (size_t)wrote;
|
||||
}
|
||||
scr_str_utf8_end(owned);
|
||||
if (close(fd) != 0) scr_fs_throw(errno, "close", path);
|
||||
}
|
||||
|
||||
/* writeFileSync(p, data, { mode, flag }): Node's open(2) with the
|
||||
* disposition flag. `exclusive` is "wx" (O_EXCL: EEXIST when the path
|
||||
* exists); 0 is "w", the default truncate-or-create. */
|
||||
void scr_fs_write_file_flags(ScrStr *path, ScrStr *data, double mode, double exclusive) {
|
||||
if (!scr_fs_write_mode_ok(mode)) return;
|
||||
scr_fs_write_file_open(path, data, (mode_t)mode, exclusive != 0 ? O_EXCL : 0);
|
||||
}
|
||||
|
||||
/* fs.promises.writeFile(path, data, { mode, flag }) and
|
||||
* fs.promises.copyFile(src, dest, mode): the settled-promise twins of
|
||||
* fs.writeFileFlagsSync and fs.copyFileFlagsSync. */
|
||||
ScrPromise *scr_fsp_write_file_flags(ScrStr *path, ScrStr *data, double mode, double exclusive) {
|
||||
scr_fs_write_file_flags(path, data, mode, exclusive);
|
||||
return scr_promise_settled_void();
|
||||
}
|
||||
|
||||
ScrPromise *scr_fsp_copyfile_flags(ScrStr *src, ScrStr *dest, double mode) {
|
||||
scr_fs_copyfile_flags(src, dest, mode);
|
||||
return scr_promise_settled_void();
|
||||
}
|
||||
@@ -3531,9 +3531,12 @@ void scr_fs_write_file(ScrStr *path, ScrStr *data) {
|
||||
scr_fs_write_common(path, data, "wb");
|
||||
}
|
||||
|
||||
/* Node's mode validation for the options-record write: an integer in
|
||||
* open(2)'s mode_t range. Throws ERR_OUT_OF_RANGE and answers false. */
|
||||
static bool scr_fs_write_mode_ok(double mode) {
|
||||
/* writeFileSync(p, data, { mode }): the mode is open(2)'s O_CREAT
|
||||
* argument — it applies at CREATION only (umask applying), and an
|
||||
* existing file keeps its permissions, exactly Node (which never chmods
|
||||
* here). Same error shapes as the plain form. */
|
||||
void scr_fs_write_file_mode(ScrStr *path, ScrStr *data, double mode) {
|
||||
if (!scr_fs_path_ok(path, "path")) return;
|
||||
char recv[48], msg[176];
|
||||
scr_num_received(mode, recv);
|
||||
if (!(isfinite(mode) && trunc(mode) == mode)) {
|
||||
@@ -3541,27 +3544,18 @@ static bool scr_fs_write_mode_ok(double mode) {
|
||||
"The value of \"mode\" is out of range. It must be an integer. Received %s",
|
||||
recv);
|
||||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
if (mode < 0 || mode > 4294967295.0) {
|
||||
int len = snprintf(msg, sizeof msg,
|
||||
"The value of \"mode\" is out of range. It must be >= 0 && <= 4294967295. Received %s",
|
||||
recv);
|
||||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* The open(2) write behind the mode/flag forms: O_TRUNC plus `extra` (only
|
||||
* O_EXCL today), creation mode applying at CREATION only (umask applying) —
|
||||
* an existing file keeps its permissions, exactly Node (which never chmods
|
||||
* here). Same error shapes as the plain form. */
|
||||
static void scr_fs_write_file_open(ScrStr *path, ScrStr *data, mode_t mode, int extra) {
|
||||
if (!scr_fs_path_ok(path, "path")) return;
|
||||
/* O_BINARY: zero on POSIX; on Windows it keeps the CRT from translating
|
||||
* \n in these byte-exact writes (fopen's "wb" path already does). */
|
||||
int fd = open(path->data, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY | extra, mode);
|
||||
int fd = open(path->data, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, (mode_t)mode);
|
||||
if (fd < 0) {
|
||||
scr_fs_throw(errno, "open", path);
|
||||
return;
|
||||
@@ -3585,22 +3579,6 @@ static void scr_fs_write_file_open(ScrStr *path, ScrStr *data, mode_t mode, int
|
||||
if (close(fd) != 0) scr_fs_throw(errno, "close", path);
|
||||
}
|
||||
|
||||
/* writeFileSync(p, data, { mode }). */
|
||||
void scr_fs_write_file_mode(ScrStr *path, ScrStr *data, double mode) {
|
||||
if (!scr_fs_path_ok(path, "path")) return;
|
||||
if (!scr_fs_write_mode_ok(mode)) return;
|
||||
scr_fs_write_file_open(path, data, (mode_t)mode, 0);
|
||||
}
|
||||
|
||||
/* writeFileSync(p, data, { mode, flag }): Node's open(2) with the
|
||||
* disposition flag. `exclusive` is "wx" (O_EXCL: EEXIST when the path
|
||||
* exists); 0 is "w", the default truncate-or-create. */
|
||||
void scr_fs_write_file_flags(ScrStr *path, ScrStr *data, double mode, double exclusive) {
|
||||
if (!scr_fs_path_ok(path, "path")) return;
|
||||
if (!scr_fs_write_mode_ok(mode)) return;
|
||||
scr_fs_write_file_open(path, data, (mode_t)mode, exclusive != 0 ? O_EXCL : 0);
|
||||
}
|
||||
|
||||
void scr_fs_append_file(ScrStr *path, ScrStr *data) {
|
||||
scr_fs_write_common(path, data, "ab");
|
||||
}
|
||||
@@ -4694,6 +4672,12 @@ static bool scr_fs_win_same_file(const WCHAR *a, const WCHAR *b) {
|
||||
first.nFileIndexLow == second.nFileIndexLow;
|
||||
}
|
||||
|
||||
/* The Win32 arm of copyFile's mode form (scr_fs_cp.c's SCR_FS_FLAGS build
|
||||
* has the POSIX one; writeFile's flag form is portable and lives in
|
||||
* scr_fs_flags.c). It sits here because the wide-path and errno helpers
|
||||
* above are this file's; _WIN32 excludes it from the POSIX builds, so every
|
||||
* other platform's scr_lib.c object is unchanged. */
|
||||
|
||||
/* copyFileSync(src, dest, mode) and copyFile(src, dest, mode): libuv's
|
||||
* fs__copyfile in src/win/fs.c. CopyFileW carries only COPYFILE_EXCL (its
|
||||
* bFailIfExists flag); COPYFILE_FICLONE (2) falls back to a plain copy and
|
||||
|
||||
@@ -2438,9 +2438,12 @@ void scr_net_server_on_close(ScrNetServer *s, ScrClosure *cb /*moves*/, bool onc
|
||||
scr_net_ls_add(&s->close_ls, cb, NULL, once);
|
||||
}
|
||||
|
||||
#ifdef SCR_NET_SERVER_OFF
|
||||
/* server.off(event, cb) / removeListener: the LAST listener standing for
|
||||
* the same function leaves the event's list — Node's EventEmitter removes
|
||||
* the most recently added match, so the scan runs back to front. */
|
||||
* the most recently added match, so the scan runs back to front. The
|
||||
* SCR_NET_SERVER_OFF build of the unit carries it, so a program that never
|
||||
* removes a listener keeps the default object byte for byte. */
|
||||
void scr_net_server_off(ScrNetServer *s, ScrClosure *cb /*borrowed*/, double kind) {
|
||||
ScrNetLs *l = kind == SCR_NET_OFF_ERROR ? &s->err_ls
|
||||
: kind == SCR_NET_OFF_CLOSE ? &s->close_ls
|
||||
@@ -2461,6 +2464,7 @@ void scr_net_server_off(ScrNetServer *s, ScrClosure *cb /*borrowed*/, double kin
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* server.on('listening', cb): joins the deferred 'listening' list the
|
||||
* listen(port, cb) callback rides (the sweep fires it once after a
|
||||
|
||||
Reference in New Issue
Block a user