diff --git a/docs/content/limitations/native-worker-threads.mdx b/docs/content/limitations/native-worker-threads.mdx index e45d12ad34..d5225c5eea 100644 --- a/docs/content/limitations/native-worker-threads.mdx +++ b/docs/content/limitations/native-worker-threads.mdx @@ -1,13 +1,15 @@ `node:worker_threads` runs workers as native threads. Worker entry points must resolve at build time: use a source-relative `new URL("./worker.ts", import.meta.url)`, the current module URL, an absolute path, or CommonJS `__filename`. -Each worker initializes its own module state, event loop, environment snapshot, and reference-counted heap. -Nested workers inherit their parent's environment snapshot. -Supported constructor options are `workerData`, `argv`, a `transferList` of MessagePorts, `name` (reported by `threadName` and `worker.threadName`), `stdout`, `stderr` and `stdin`, and `resourceLimits.stackSizeMb`, which sets the worker thread's stack size (at least 192 KiB; without it a worker's stack is 8 MiB, where Node's is 4 MiB). +Each worker initializes its own module state, event loop, and reference-counted heap. +A worker's `process.env` is a copy of its parent's, taken by the constructor. +With the `env` option it holds an object's own enumerable properties converted with `String()`, and with `env: SHARE_ENV` the worker and its parent use one environment, as in Node; `SHARE_ENV` is unsupported in a worker that has its own copy. +Child processes that a worker spawns inherit the worker's `process.env`. +Supported constructor options are `workerData`, `argv`, `env`, a `transferList` of MessagePorts, `name` (reported by `threadName` and `worker.threadName`), `stdout`, `stderr` and `stdin`, and `resourceLimits.stackSizeMb`, which sets the worker thread's stack size (at least 192 KiB; without it a worker's stack is 8 MiB, where Node's is 4 MiB). The `resourceLimits` heap limits (`maxOldGenerationSizeMb`, `maxYoungGenerationSizeMb`, and `codeRangeSizeMb`), `worker.resourceLimits` while the worker runs, and `resourceLimits` read inside a worker are refused; in the main thread `resourceLimits` is `{}`, and after a worker exits `worker.resourceLimits` is `{}`, as in Node. With the `stdout` or `stderr` option, the worker's writes to that stream reach `worker.stdout` or `worker.stderr`, a Readable that ends after the worker's `'exit'` event; its chunks arrive in order with the worker's other events. Without the option a worker writes to the process's own output, and reading `worker.stdout` or `worker.stderr` is refused. With the `stdin` option, `worker.stdin` is a Writable whose chunks the worker reads from `process.stdin`, in order with the worker's other events, and `process.stdin` ends when `worker.stdin` ends; without it `worker.stdin` is `null` and the worker's `process.stdin` ends without data, as in Node. -Runtime paths, `eval`, loaders, and shared environments are unsupported. +Runtime paths, `eval`, and loaders are unsupported. `setEnvironmentData` and `getEnvironmentData` keep a map per thread with SameValueZero keys; each new Worker starts from a clone of its parent's entries taken by the constructor, which throws `DataCloneError` when an entry cannot be cloned. Worker argument strings containing null bytes are also unsupported. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index e5fd082f61..d3a53376cd 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -884,9 +884,9 @@ "apiEntries": 66, "static": { "supported": 0, - "partial": 46, + "partial": 47, "refused": 0, - "not-implemented": 20, + "not-implemented": 19, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -84162,9 +84162,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 7260fff782..93c00ade4e 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -7,8 +7,8 @@ "tierItemCount": 3454, "tiers": { "static": { - "audit-partial": 1197, - "verify-gap": 2130, + "audit-partial": 1198, + "verify-gap": 2129, "replace-refusal": 40, "classify": 2, "implement": 85 @@ -84619,7 +84619,9 @@ "tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-environment.ts", - "tests/corpus/native-worker-roots/main.ts" + "tests/corpus/native-worker-roots/main.ts", + "tests/corpus/5998-worker-env-option.ts", + "tests/corpus/5999-worker-env-nested.cjs" ] } } @@ -85607,12 +85609,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:worker_threads", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:worker_threads.SHARE_ENV", + "tests": [ + "tests/corpus/5998-worker-env-option.ts", + "tests/corpus/5999-worker-env-nested.cjs" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 280c642f2e..e13e21151d 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -941,9 +941,9 @@ "apiEntries": 66, "static": { "supported": 0, - "partial": 46, + "partial": 47, "refused": 0, - "not-implemented": 20, + "not-implemented": 19, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -113569,6 +113569,8 @@ "tests/corpus/5879-worker-stdin.ts", "tests/corpus/5990-worker-uncloneable.mjs", "tests/corpus/5991-worker-async-dispose.mjs", + "tests/corpus/5998-worker-env-option.ts", + "tests/corpus/5999-worker-env-nested.cjs", "tests/corpus/native-worker-clone-errors.ts", "tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-delayed-messages.ts", @@ -114212,7 +114214,9 @@ "tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-environment.ts", - "tests/corpus/native-worker-roots/main.ts" + "tests/corpus/native-worker-roots/main.ts", + "tests/corpus/5998-worker-env-option.ts", + "tests/corpus/5999-worker-env-nested.cjs" ] }, "anchorSource": "exact" @@ -115157,8 +115161,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:worker_threads" + "status": "partial", + "evidence": "compiler-feature:worker_threads.SHARE_ENV", + "tests": [ + "tests/corpus/5998-worker-env-option.ts", + "tests/corpus/5999-worker-env-nested.cjs" + ] }, "anchorSource": "chapter" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index d6dc342ae7..fe4383d451 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -217,7 +217,8 @@ {"chapter": "stream", "symbols": ["readable.map", "readable.filter", "readable.forEach", "readable.toArray", "readable.some", "readable.find", "readable.every", "readable.flatMap", "readable.drop", "readable.take", "readable.reduce"], "status": "partial", "evidence": ["tests/corpus/5335-stream-operators-values.ts", "tests/corpus/5336-stream-operators-promise-timing.ts", "tests/corpus/5337-stream-operators-concurrency.ts", "tests/corpus/5338-stream-operators-checks.cjs"]}, {"chapter": "stream", "symbols": ["stream.Readable.from"], "status": "partial", "evidence": ["tests/corpus/5340-readable-from-options.ts", "tests/corpus/5341-readable-from-dynamic.cjs", "tests/corpus/5420-readable-from-generators.ts", "tests/corpus/5421-readable-from-generators.cjs", "tests/corpus/5610-readable-from-sets-and-views.ts", "tests/corpus/5611-readable-from-class-iterables.ts", "tests/corpus/5612-readable-from-iterables.cjs"]}, {"chapter": "stream", "symbols": ["stream.compose"], "status": "partial", "evidence": ["tests/corpus/5342-stream-compose.ts", "tests/corpus/5343-stream-compose-edges.ts"]}, - {"chapter": "worker_threads", "symbols": ["Worker"], "status": "partial", "evidence": ["tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-environment.ts", "tests/corpus/native-worker-roots/main.ts"]}, + {"chapter": "worker_threads", "symbols": ["Worker"], "status": "partial", "evidence": ["tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-environment.ts", "tests/corpus/native-worker-roots/main.ts", "tests/corpus/5998-worker-env-option.ts", "tests/corpus/5999-worker-env-nested.cjs"]}, + {"chapter": "worker_threads", "symbols": ["SHARE_ENV"], "status": "partial", "evidence": ["tests/corpus/5998-worker-env-option.ts", "tests/corpus/5999-worker-env-nested.cjs"]}, {"chapter": "worker_threads", "symbols": ["MessagePort", "parentPort", "worker_threads.workerData", "isMainThread", "threadId"], "status": "partial", "evidence": ["tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-port-lifetime.ts", "tests/corpus/native-worker-references.ts"]}, {"chapter": "worker_threads", "symbols": ["worker.postMessage", "port.postMessage"], "status": "partial", "evidence": ["tests/corpus/native-worker-message-graphs.ts", "tests/corpus/native-worker-shared-views.ts", "tests/corpus/native-worker-clone-errors.ts", "tests/corpus/native-worker-delayed-messages.ts"]}, {"chapter": "worker_threads", "symbols": ["worker.terminate"], "status": "partial", "evidence": ["tests/corpus/native-worker-terminate-loop.ts", "tests/corpus/native-worker-terminate-wait.ts", "tests/corpus/native-worker-terminate-suspended.ts", "tests/corpus/native-worker-loop-cancellation.ts"]}, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 411291d94c..83991f46fd 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -6340,7 +6340,12 @@ declare module "worker_threads" { export const resourceLimits: ResourceLimits; /* the Worker's name option, trimmed ("WorkerThread" by default); "" in the main thread */ export const threadName: string | null; + /* as options.env, the worker shares this thread's process.env */ + export const SHARE_ENV: unique symbol; export interface WorkerOptions { + /* the worker's process.env: an object's values as strings, or SHARE_ENV; + * a copy of this thread's process.env by default */ + env?: { [name: string]: unknown } | typeof SHARE_ENV | null; workerData?: unknown; transferList?: readonly unknown[]; argv?: readonly unknown[]; diff --git a/packages/compiler/src/frontend/lowering/builtins/workers.ts b/packages/compiler/src/frontend/lowering/builtins/workers.ts index 335fdb9694..ea685e3721 100644 --- a/packages/compiler/src/frontend/lowering/builtins/workers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/workers.ts @@ -8,6 +8,7 @@ import { DYN, F64, STRING, + SYMBOL_T, UNDEFINED_T, VOID, isFfiCallbackParam, @@ -32,6 +33,14 @@ export function lowerWorkerMetadata(member: string, loc: SrcLoc): IrExpr | null return { kind: "libCall", fn: "worker.resourceLimits", args: [], type: DYN, loc }; case "threadName": return { kind: "libCall", fn: "worker.threadName", args: [], type: DYN, loc }; + case "SHARE_ENV": + return { + kind: "libCall", + fn: "sym.for", + args: [{ kind: "strLit", value: "nodejs.worker_threads.SHARE_ENV", type: STRING, loc }], + type: SYMBOL_T, + loc, + }; default: return null; } diff --git a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts index 5e04ee83c1..68d7c7e3c8 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts @@ -526,7 +526,7 @@ function moduleValue(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr { add("win32", moduleValue(lowerer, "path/win32", loc)); } if (module === "worker_threads") { - for (const member of ["isInternalThread", "parentPort", "workerData"]) + for (const member of ["SHARE_ENV", "isInternalThread", "parentPort", "workerData"]) add(member, lowerWorkerMetadata(member, loc)!); } body.push({ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5998-worker-env-option.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5998-worker-env-option.ts.json new file mode 100644 index 0000000000..d50c38ce0d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5998-worker-env-option.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5998-worker-env-option.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5999-worker-env-nested.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5999-worker-env-nested.cjs.json new file mode 100644 index 0000000000..b0d5854bdf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5999-worker-env-nested.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5999-worker-env-nested.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index ec685656dc..6764f6c5a1 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -175,6 +175,17 @@ static void scr_child_error_sync_fields(ScrError *e, bool has_status, double sta scr_error_set_prop(e, "stderr", 6, err); } +/* The [k,v,...] environment of a child whose options give none: the + * calling worker's process.env when the worker has its own, as Node spawns + * with process.env, or NULL to inherit the process environment. +1. */ +static ScrArr *scr_child_default_env(void) { +#ifdef SCR_WORKERS + return scr_context_env_pairs(); +#else + return NULL; +#endif +} + #ifdef _WIN32 /* ── Windows arm: real children via CreateProcessW ──────────────────── * The POSIX arm below owns the corresponding implementation; this arm @@ -670,7 +681,11 @@ static void scr_win_create_process(ScrStr *cmd, ScrArr *args, ScrWinSpawn *sp, cwd_w[0] = L'\0'; if (need > 0) (void)GetCurrentDirectoryW(need, cwd_w); } - WCHAR *env_block = sp->env_pairs != NULL ? scr_child_env_block(sp->env_pairs) : NULL; + ScrArr *inherited = sp->env_pairs != NULL ? NULL : scr_child_default_env(); + WCHAR *env_block = sp->env_pairs != NULL ? scr_child_env_block(sp->env_pairs) + : inherited != NULL ? scr_child_env_block(inherited) + : NULL; + scr_arr_release(inherited); WCHAR *path_w = scr_child_path(env_block); WCHAR *file_w; if (shell) { @@ -3076,6 +3091,9 @@ static bool scr_cap_read(ScrCapBuf *b, int fd) { return true; } +static char **scr_exec_envp(ScrArr *pairs); +static void scr_exec_envp_free(char **envp); + /* ── spawnSync ───────────────────────────────────────────────────────── */ static ScrSpawnRes *scr_spawn_res_new(bool has_status, double status, @@ -3138,10 +3156,14 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou } char **argv = scr_child_argv(cmd, args); + ScrArr *inherited = scr_child_default_env(); + char **envp = inherited != NULL ? scr_exec_envp(inherited) : NULL; + scr_arr_release(inherited); pid_t pid = -1; - int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, environ); + int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, envp != NULL ? envp : environ); posix_spawn_file_actions_destroy(&fa); free(argv); + scr_exec_envp_free(envp); if (cap_out) close(outfd[1]); if (cap_err) close(errfd[1]); @@ -3354,8 +3376,9 @@ static char *scr_exec_display(ScrStr *cmd, ScrArr *args, bool shell) { } /* Builds a NULL-terminated envp from [k,v,...] pairs ("k=v" strings, each - * malloc'd). Free with scr_exec_envp_free. */ + * malloc'd), or NULL for NULL pairs. Free with scr_exec_envp_free. */ static char **scr_exec_envp(ScrArr *pairs) { + if (!pairs) return NULL; size_t n = (size_t)scr_arr_len(pairs) / 2; char **envp = malloc((n + 1) * sizeof(char *)); if (!envp) scr_child_oom(); @@ -3476,7 +3499,9 @@ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) { #endif char **argv = scr_child_argv(cmd, args); - char **envp = o->env_pairs != NULL ? scr_exec_envp(o->env_pairs) : NULL; + ScrArr *inherited = o->env_pairs != NULL ? NULL : scr_child_default_env(); + char **envp = scr_exec_envp(o->env_pairs != NULL ? o->env_pairs : inherited); + scr_arr_release(inherited); pid_t pid = -1; int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, envp != NULL ? envp : environ); @@ -5034,7 +5059,9 @@ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode, #endif char *shell_argv[] = {(char *)"/bin/sh", (char *)"-c", cmd->data, NULL}; char **argv = shell ? shell_argv : scr_child_argv(cmd, args); - char **envp = has_env && env_pairs != NULL ? scr_exec_envp(env_pairs) : NULL; + ScrArr *inherited = has_env && env_pairs != NULL ? NULL : scr_child_default_env(); + char **envp = scr_exec_envp(has_env && env_pairs != NULL ? env_pairs : inherited); + scr_arr_release(inherited); pid_t pid = -1; const char *file = shell ? "/bin/sh" : cmd->data; int spawn_err = posix_spawnp(&pid, file, &fa, &at, argv, diff --git a/packages/runtime/src/scr_context.c b/packages/runtime/src/scr_context.c index d5bb78957d..ce9e6691ae 100644 --- a/packages/runtime/src/scr_context.c +++ b/packages/runtime/src/scr_context.c @@ -79,9 +79,7 @@ const char *scr_context_getenv(const char *name) { return NULL; } -bool scr_context_env_set(const char *name, const char *value) { - ScrContextEnv *environment = scr_context_environment; - if (!environment) return false; +static void scr_context_env_store(ScrContextEnv *environment, const char *name, const char *value) { for (size_t i = 0; i < environment->count; i++) { ScrContextEnvEntry *entry = &environment->entries[i]; if (strcmp(entry->name, name)) continue; @@ -92,7 +90,7 @@ bool scr_context_env_set(const char *name, const char *value) { free(entry->name); memmove(entry, entry + 1, (--environment->count - i) * sizeof(*entry)); } - return true; + return; } if (value) { ScrContextEnvEntry *entries = realloc(environment->entries, (environment->count + 1) * sizeof(*entries)); @@ -100,6 +98,25 @@ bool scr_context_env_set(const char *name, const char *value) { environment->entries = entries; entries[environment->count++] = (ScrContextEnvEntry){scr_context_text_copy(name), scr_context_text_copy(value)}; } +} + +ScrContextEnv *scr_context_env_new(void) { + ScrContextEnv *environment = calloc(1, sizeof(*environment)); + if (!environment) scr_trap("scriptc: out of memory\n"); + environment->entries = calloc(1, sizeof(*environment->entries)); + if (!environment->entries) scr_trap("scriptc: out of memory\n"); + return environment; +} + +void scr_context_env_put(ScrContextEnv *environment, const char *name, const char *value) { + scr_context_env_store(environment, name, value); +} + +bool scr_context_env_private(void) { return scr_context_environment != NULL; } + +bool scr_context_env_set(const char *name, const char *value) { + if (!scr_context_environment) return false; + scr_context_env_store(scr_context_environment, name, value); return true; } diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index 6d251e8ced..1ddc17af47 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -345,7 +345,13 @@ extern SCR_TL void (*scr_context_report_error)(void); extern SCR_TL bool scr_context_report_rejection; typedef struct ScrContextEnv ScrContextEnv; ScrContextEnv *scr_context_env_capture(void); +/* An empty environment, and a variable set in one (copies both strings). */ +ScrContextEnv *scr_context_env_new(void); +void scr_context_env_put(ScrContextEnv *environment, const char *name, const char *value); void scr_context_env_free(ScrContextEnv *environment); +/* True when the calling thread has its own environment; false when it uses + * the process environment (the main thread and SHARE_ENV workers). */ +bool scr_context_env_private(void); void scr_context_env_enter(ScrContextEnv *environment); bool scr_context_env_set(const char *name, const char *value); const char *scr_context_getenv(const char *name); diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 28d642501f..2022acbb8b 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -662,6 +662,10 @@ ScrDyn *scr_dyn_freeze(ScrDyn *value); bool scr_dyn_is_frozen(const ScrDyn *value); ScrDyn *scr_dyn_new_bigint(ScrBigInt *value); /* borrows and retains value */ ScrDyn *scr_dyn_new_symbol(ScrSym *value); /* borrows and retains value */ +/* Symbol.keyFor for units outside the optional symbol unit: the +1 + * registry key of a symbol, or NULL. The symbol unit installs it when its + * registry first holds a symbol; until then no symbol is registered. */ +extern SCR_TL ScrStr *(*scr_dyn_symbol_key_fn)(ScrSym *s); ScrSym *scr_sym_well_known(ScrStr *name); ScrDyn *scr_dyn_symbol_get(const ScrDyn *value, const ScrStr *key); ScrDyn *scr_dyn_symbol_key_get(const ScrDyn *value, const ScrDyn *key, bool optional); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index f2b4696676..c097b91873 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4126,6 +4126,7 @@ ScrDyn *scr_dyn_new_handle(void *h, ScrDynHandleTag tag) { * references the fiber machinery (the unit-test subset links). */ SCR_TL void (*scr_dyn_promise_release_fn)(ScrPromise *p) = NULL; SCR_TL bool (*scr_dyn_promise_identity_fn)(ScrPromise *a, ScrPromise *b) = NULL; +SCR_TL ScrStr *(*scr_dyn_symbol_key_fn)(ScrSym *s) = NULL; ScrDyn *scr_dyn_alloc_promise(void (*release_fn)(ScrPromise *p)) { scr_dyn_promise_release_fn = release_fn; diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 99f0128c92..8776e9c6d5 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -763,9 +763,36 @@ ScrStr *scr_process_exec_path(void) { return scr_str_retain(scr_exec_path_str); } +#if defined(SCR_WORKERS) && !defined(_WIN32) +/* Set when a Worker starts with env: SHARE_ENV. From then on two threads + * can use the process environment, so process.env reads, writes and + * enumeration of it hold scr_env_lock (the Windows environment functions + * synchronize themselves). */ +static atomic_bool scr_env_shared; +static pthread_mutex_t scr_env_lock = PTHREAD_MUTEX_INITIALIZER; + +void scr_env_share(void) { atomic_store_explicit(&scr_env_shared, true, memory_order_relaxed); } + +static bool scr_env_lock_enter(void) { + if (!atomic_load_explicit(&scr_env_shared, memory_order_relaxed)) return false; + pthread_mutex_lock(&scr_env_lock); + return true; +} + +static void scr_env_lock_exit(bool locked) { + if (locked) pthread_mutex_unlock(&scr_env_lock); +} +#else +#ifdef SCR_WORKERS +void scr_env_share(void) {} +#endif +static bool scr_env_lock_enter(void) { return false; } +static void scr_env_lock_exit(bool locked) { (void)locked; } +#endif + ScrStr *scr_env_get(const ScrStr *name) { #ifdef SCR_WORKERS - if (!scr_context_is_main()) { + if (scr_context_env_private()) { const char *value = scr_context_getenv(name->data); return value ? scr_str_new(value, strlen(value)) : NULL; } @@ -789,8 +816,11 @@ ScrStr *scr_env_get(const ScrStr *name) { free(buf); return s; #else - const char *v = scr_getenv(name->data); - return v ? scr_str_new(v, strlen(v)) : NULL; + bool locked = scr_env_lock_enter(); + const char *v = getenv(name->data); + ScrStr *s = v ? scr_str_new(v, strlen(v)) : NULL; + scr_env_lock_exit(locked); + return s; #endif } @@ -855,6 +885,7 @@ void scr_env_set(const ScrStr *name, const ScrStr *value) { #ifdef SCR_WORKERS if (scr_context_env_set(k, v)) goto done; #endif + bool locked = scr_env_lock_enter(); #ifdef _WIN32 /* The WIN32 environment (see scr_env_get): an empty value stays a * present-but-empty variable, and children inherit the write. */ @@ -863,6 +894,7 @@ void scr_env_set(const ScrStr *name, const ScrStr *value) { setenv(k, v, 1); #endif scr_env_tz_changed(name); + scr_env_lock_exit(locked); #ifdef SCR_WORKERS done: #endif @@ -877,12 +909,14 @@ void scr_env_unset(const ScrStr *name) { #ifdef SCR_WORKERS if (scr_context_env_set(name->data, NULL)) return; #endif + bool locked = scr_env_lock_enter(); #ifdef _WIN32 SetEnvironmentVariableA(name->data, NULL); #else unsetenv(name->data); #endif scr_env_tz_changed(name); + scr_env_lock_exit(locked); } static ScrStr *scr_fs_read_file_core(ScrStr *path, bool env_file, size_t *ascii); @@ -1146,12 +1180,14 @@ ScrArr *scr_env_pairs(void) { FreeEnvironmentStringsA(block); } #else + bool locked = scr_env_lock_enter(); for (char **e = environ; *e != NULL; e++) { const char *eq = strchr(*e, '='); if (!eq || eq == *e) continue; scr_arr_push_ref(out, scr_str_new(*e, (size_t)(eq - *e))); scr_arr_push_ref(out, scr_str_new(eq + 1, strlen(eq + 1))); } + scr_env_lock_exit(locked); #endif return out; } diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index bae6782929..59b0b84a27 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -111,6 +111,11 @@ const char *scr_node_debug_initial(void); * environ order — the compiler's process.env snapshot builds its record * from this. +1 fresh array. Never throws. */ ScrArr *scr_env_pairs(void); +#ifdef SCR_WORKERS +/* Called before a Worker that shares the process environment starts: + * process.env operations on that environment then hold a lock. */ +void scr_env_share(void); +#endif /* getpid(2) / getuid(2) — POSIX-only target, always answer. */ double scr_process_pid(void); double scr_process_getuid(void); diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 304f2544f8..f8332d274e 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -120,7 +120,10 @@ ScrSym *scr_sym_for(ScrStr *key) { /* First request for this key: a fresh symbol whose description IS the * key (the spec's Symbol.for behavior), chained into the registry with * the registry's own reference. */ - if (!g_sym_registry) scr_atexit(scr_sym_registry_cleanup); + if (!g_sym_registry) { + scr_atexit(scr_sym_registry_cleanup); + scr_dyn_symbol_key_fn = scr_sym_key_for; + } ScrSym *s = scr_sym_new(key); s->reg_key = scr_str_retain(key); s->reg_next = g_sym_registry; diff --git a/packages/runtime/src/scr_worker.c b/packages/runtime/src/scr_worker.c index 67f4ec0846..8c55c09c3e 100644 --- a/packages/runtime/src/scr_worker.c +++ b/packages/runtime/src/scr_worker.c @@ -374,7 +374,7 @@ ScrWorkerThread *scr_worker_thread_start(ScrWorkerEntry entry, uint32_t root, ScrMessage *data, bool environment_data, ScrMailbox *parent, ScrArr *arguments, size_t stack_size, const ScrStr *name, - unsigned capture) { + unsigned capture, ScrContextEnv *environment) { ScrWorkerThread *worker = calloc(1, sizeof(*worker)); if (!worker) scr_trap("scriptc: out of memory\n"); atomic_init(&worker->references, 2); /* owner and native thread */ @@ -392,7 +392,7 @@ ScrWorkerThread *scr_worker_thread_start(ScrWorkerEntry entry, uint32_t root, worker->data = data; worker->environment_data = environment_data; worker->capture = capture; - worker->environment = scr_context_env_capture(); + worker->environment = environment; worker->name = malloc(sizeof(ScrStr) + name->len + 1); if (!worker->name) scr_trap("scriptc: out of memory\n"); memcpy(worker->name, name, sizeof(ScrStr) + name->len + 1); diff --git a/packages/runtime/src/scr_worker.h b/packages/runtime/src/scr_worker.h index ba27f69af0..b2edb0eb8e 100644 --- a/packages/runtime/src/scr_worker.h +++ b/packages/runtime/src/scr_worker.h @@ -26,12 +26,14 @@ enum { * stack_size of 0 selects the default 8 MiB thread stack. capture (1 * stdout, 2 stderr) posts the worker's writes to those descriptors to the * parent as SCR_WORKER_STDOUT/STDERR events; 4 (stdin) has the worker's - * process.stdin read what scr_worker_thread_post_stdin sends. */ + * process.stdin read what scr_worker_thread_post_stdin sends. Consumes + * environment, the worker's process.env, including on failure; NULL has + * the worker use the process environment. */ ScrWorkerThread *scr_worker_thread_start(ScrWorkerEntry entry, uint32_t root, ScrMessage *data, bool environment_data, ScrMailbox *parent, ScrArr *arguments, size_t stack_size, const ScrStr *name, - unsigned capture); + unsigned capture, struct ScrContextEnv *environment); ScrWorkerThread *scr_worker_thread_retain(ScrWorkerThread *worker); void scr_worker_thread_release(ScrWorkerThread *worker); /* Join is owner-only and idempotent. The caller holds a reference. */ diff --git a/packages/runtime/src/scr_worker.test.c b/packages/runtime/src/scr_worker.test.c index a50fd2fe28..eec2d2492d 100644 --- a/packages/runtime/src/scr_worker.test.c +++ b/packages/runtime/src/scr_worker.test.c @@ -96,7 +96,8 @@ int main(void) { scr_dyn_release(input); assert(wire && !scr_exc_pending()); ScrStr *name = scr_str_new("WorkerThread", 12); - workers[i] = scr_worker_thread_start(entry, i, wire, false, mailbox, NULL, 0, name, 0); + workers[i] = scr_worker_thread_start(entry, i, wire, false, mailbox, NULL, 0, name, 0, + scr_context_env_capture()); scr_str_release(name); assert(workers[i] && !scr_exc_pending()); assert(scr_worker_thread_id(workers[i]) != 0); diff --git a/packages/runtime/src/scr_worker_events.c b/packages/runtime/src/scr_worker_events.c index b4efa3e0d0..86a981d838 100644 --- a/packages/runtime/src/scr_worker_events.c +++ b/packages/runtime/src/scr_worker_events.c @@ -1,4 +1,5 @@ #include "scr_worker.h" +#include "scr_key.h" #ifdef SCR_WORKERS #include @@ -1289,6 +1290,77 @@ static bool scr_worker_stack_size(ScrDyn *limits, size_t *stack_size) { return true; } +/* True for worker_threads.SHARE_ENV: the symbol registered under its key + * (each thread's registry holds one). */ +static bool scr_worker_share_env(const ScrDyn *value) { + if (value->kind != SCR_DYN_SYMBOL || !scr_dyn_symbol_key_fn) return false; + ScrStr *key = scr_dyn_symbol_key_fn(value->v.symbol.value); + if (!key) return false; + static const char share[] = "nodejs.worker_threads.SHARE_ENV"; + bool same = key->len == sizeof share - 1 && !memcmp(key->data, share, sizeof share - 1); + scr_str_release(key); + return same; +} + +/* The worker's process.env from options.env, as Node's constructor makes + * it: undefined or null copies this thread's environment, an object's own + * enumerable string-keyed properties other than array indices become the + * variables (each value converted with String()), and SHARE_ENV has the + * worker use this thread's + * environment. *out is NULL for SHARE_ENV. False with an exception pending. */ +static bool scr_worker_environment(ScrDyn *env, ScrContextEnv **out) { + *out = NULL; + if (env->kind == SCR_DYN_UNDEF || env->kind == SCR_DYN_NULL) { + *out = scr_context_env_capture(); + return true; + } + if (scr_worker_share_env(env)) { + if (scr_context_env_private()) { + scr_worker_refusal("SHARE_ENV in a worker that has its own environment is not supported yet"); + return false; + } + return true; + } + ScrStr *type = scr_dyn_typeof(env); + bool object = type->len == 6 && !memcmp(type->data, "object", 6); + scr_str_release(type); + if (!object) { + scr_dyn_prop_type_fail("options.env", "of type object or one of undefined, null, or worker_threads.SHARE_ENV", env); + return false; + } + ScrDynEntryPairs *pairs = scr_dyn_obj_entry_pairs(env); + if (!pairs) return false; + ScrContextEnv *environment = scr_context_env_new(); + for (size_t i = 0; i < pairs->count; i++) { + ScrStr *text = environment ? scr_dyn_string_coerce_js(pairs->pairs[i].value) : NULL; + uint32_t index; + const ScrStr *key = pairs->pairs[i].key; + if (text && scr_key_array_index(key->data, key->len, &index)) { + /* converted like the others, but Node's worker environment drops + * array-index names */ + scr_str_release(text); + } else if (text) { + /* The environment holds UTF-8: a lone surrogate is stored as U+FFFD. */ + char *name_owned, *value_owned; + const char *name = scr_str_utf8(key, &name_owned); + const char *value = scr_str_utf8(text, &value_owned); + scr_context_env_put(environment, name, value); + scr_str_utf8_end(name_owned); + scr_str_utf8_end(value_owned); + scr_str_release(text); + } else if (environment) { + scr_context_env_free(environment); + environment = NULL; + } + /* the snapshot's pairs are owned: release each, also after a throw */ + scr_str_release(pairs->pairs[i].key); + scr_dyn_release(pairs->pairs[i].value); + } + scr_dyn_entry_pairs_free(pairs); + *out = environment; + return environment != NULL; +} + static ScrDyn *scr_worker_new_options(double root, ScrStr *filename, ScrDyn *options) { if (!scr_worker_events_install()) return NULL; ScrDyn *data = scr_dyn_undefined(); @@ -1296,6 +1368,7 @@ static ScrDyn *scr_worker_new_options(double root, ScrStr *filename, ScrDyn *opt ScrDyn *transfer_list = scr_dyn_undefined(); ScrDyn *limits = scr_dyn_undefined(); ScrDyn *name = scr_dyn_undefined(); + ScrDyn *env = scr_dyn_undefined(); unsigned capture = 0; if (options->kind != SCR_DYN_UNDEF) { if (options->kind != SCR_DYN_OBJ) { @@ -1310,6 +1383,7 @@ static ScrDyn *scr_worker_new_options(double root, ScrStr *filename, ScrDyn *opt else if (entry->key_len == 12 && !memcmp(entry->key, "transferList", 12)) transfer_list = entry->value; else if (entry->key_len == 14 && !memcmp(entry->key, "resourceLimits", 14)) limits = entry->value; else if (entry->key_len == 4 && !memcmp(entry->key, "name", 4)) name = entry->value; + else if (entry->key_len == 3 && !memcmp(entry->key, "env", 3)) env = entry->value; else if (entry->key_len == 6 && !memcmp(entry->key, "stdout", 6)) capture |= scr_dyn_truthy(entry->value) ? 1 : 0; else if (entry->key_len == 6 && !memcmp(entry->key, "stderr", 6)) capture |= scr_dyn_truthy(entry->value) ? 2 : 0; else if (entry->key_len == 5 && !memcmp(entry->key, "stdin", 5)) capture |= scr_dyn_truthy(entry->value) ? 4 : 0; @@ -1320,11 +1394,17 @@ static ScrDyn *scr_worker_new_options(double root, ScrStr *filename, ScrDyn *opt if (!scr_worker_stack_size(limits, &stack_size)) return NULL; ScrArr *arguments = scr_worker_arguments(filename, argv); if (!arguments) return NULL; + ScrContextEnv *environment; + if (!scr_worker_environment(env, &environment)) { + scr_arr_release(arguments); + return NULL; + } /* Node: a truthy name must be a string, trimmed; otherwise WorkerThread */ ScrStr *thread_name; if (scr_dyn_truthy(name)) { if (name->kind != SCR_DYN_STR) { scr_arr_release(arguments); + scr_context_env_free(environment); scr_dyn_prop_type_fail("options.name", "of type string", name); return NULL; } @@ -1333,15 +1413,26 @@ static ScrDyn *scr_worker_new_options(double root, ScrStr *filename, ScrDyn *opt thread_name = scr_str_new("WorkerThread", 12); } ScrDyn *transfer = scr_worker_transfer_list(transfer_list); - if (scr_exc_pending()) { scr_arr_release(arguments); scr_str_release(thread_name); return NULL; } + if (scr_exc_pending()) { + scr_arr_release(arguments); + scr_str_release(thread_name); + scr_context_env_free(environment); + return NULL; + } bool environment_data; ScrMessage *wire = scr_worker_start_message(data, transfer, &environment_data); if (capture & 4) scr_worker_set_stdin_attach(scr_worker_stdin_attach); scr_dyn_release(transfer); - if (!wire) { scr_arr_release(arguments); scr_str_release(thread_name); return NULL; } + if (!wire) { + scr_arr_release(arguments); + scr_str_release(thread_name); + scr_context_env_free(environment); + return NULL; + } + if (!environment) scr_env_share(); ScrWorkerThread *thread = scr_worker_thread_start(sc_worker_entry, (uint32_t)root, wire, environment_data, scr_worker_events, arguments, stack_size, thread_name, - capture); + capture, environment); scr_str_release(thread_name); scr_arr_release(arguments); if (!thread) return NULL; diff --git a/tests/corpus/5998-worker-env-option.ts b/tests/corpus/5998-worker-env-option.ts new file mode 100644 index 0000000000..51ceca48a3 --- /dev/null +++ b/tests/corpus/5998-worker-env-option.ts @@ -0,0 +1,92 @@ +// The Worker env option: by default a worker's process.env is a copy of +// its parent's; an object gives the worker its values as strings; SHARE_ENV +// has the worker and its parent use one environment. Child processes a +// worker spawns inherit the worker's process.env. +import { Worker, SHARE_ENV, isMainThread, parentPort, workerData } from "node:worker_threads"; +import { exec, spawnSync } from "node:child_process"; + +const child = (): string => + spawnSync("sh", ["-c", 'printf %s "${SCRIPTC_ENV_MAIN-unset},${SCRIPTC_ENV_A-unset}"'], { + encoding: "utf8", + }).stdout; + +if (isMainThread) { + process.env.SCRIPTC_ENV_MAIN = "main"; + console.log(typeof SHARE_ENV, SHARE_ENV.toString(), SHARE_ENV.description, SHARE_ENV === Symbol.for("nodejs.worker_threads.SHARE_ENV")); + function named(): number { + return 0; + } + const bad: unknown[] = [1, "x", true, named, Symbol("nodejs.worker_threads.SHARE_ENV")]; + for (const env of bad) { + try { + const options: Record = { env }; + new Worker(new URL(import.meta.url), options); + } catch (e) { + const err = e as Error & { code?: string }; + console.log(err.name, err.code, err.message); + } + } + try { + new Worker(new URL(import.meta.url), { env: { SCRIPTC_ENV_A: Symbol("s") } }); + } catch (e) { + console.log((e as Error).name, (e as Error).message); + } + // env is checked before name. + try { + const options: Record = { env: 1, name: 2 }; + new Worker(new URL(import.meta.url), options); + } catch (e) { + console.log((e as Error & { code?: string }).code, (e as Error).message.slice(0, 26)); + } + const modes: [string, unknown][] = [ + ["default", undefined], + ["null", null], + ["object", { SCRIPTC_ENV_A: 1, SCRIPTC_ENV_B: true, SCRIPTC_ENV_C: null, SCRIPTC_ENV_D: { toString: () => "d" } }], + ["share", SHARE_ENV], + ["registered", Symbol.for("nodejs.worker_threads.SHARE_ENV")], + ]; + let next = 0; + const run = (): void => { + if (next >= modes.length) { + console.log("main done", process.env.SCRIPTC_ENV_MAIN, process.env.SCRIPTC_ENV_WORKER ?? "unset"); + return; + } + const [label, env] = modes[next++]!; + process.env.SCRIPTC_ENV_MAIN = "main"; + delete process.env.SCRIPTC_ENV_WORKER; + const options: Record = { env, workerData: label }; + const worker = new Worker(new URL(import.meta.url), options); + worker.on("message", (m: unknown) => { + console.log(label, JSON.stringify(m)); + // a SHARE_ENV worker sees the parent's later writes + process.env.SCRIPTC_ENV_MAIN = "changed"; + worker.postMessage("check"); + }); + worker.on("exit", () => { + console.log(label, "main sees", process.env.SCRIPTC_ENV_MAIN, process.env.SCRIPTC_ENV_WORKER ?? "unset"); + run(); + }); + }; + run(); +} else { + const port = parentPort!; + const before = [ + process.env.SCRIPTC_ENV_MAIN ?? "unset", + process.env.SCRIPTC_ENV_A ?? "unset", + process.env.SCRIPTC_ENV_B ?? "unset", + process.env.SCRIPTC_ENV_C ?? "unset", + process.env.SCRIPTC_ENV_D ?? "unset", + Object.keys(process.env).filter((k) => k.startsWith("SCRIPTC_ENV_")).sort().join("|"), + child(), + ]; + process.env.SCRIPTC_ENV_WORKER = String(workerData); + delete process.env.SCRIPTC_ENV_A; + port.postMessage(before); + port.once("message", () => { + console.log(" ", workerData, "worker sees", process.env.SCRIPTC_ENV_MAIN ?? "unset", child()); + exec('printf %s "${SCRIPTC_ENV_WORKER-unset}"', (error: Error | null, stdout: string) => { + console.log(" ", workerData, "exec sees", stdout); + port.close(); + }); + }); +} diff --git a/tests/corpus/5999-worker-env-nested.cjs b/tests/corpus/5999-worker-env-nested.cjs new file mode 100644 index 0000000000..d3ac5656e9 --- /dev/null +++ b/tests/corpus/5999-worker-env-nested.cjs @@ -0,0 +1,69 @@ +// The Worker env option from CommonJS: SHARE_ENV through the module object, +// env values computed at run time, a Map (no own properties, so an empty +// environment), array-index names, a getter, and nested workers, +// which copy or share the environment of the worker that starts them. +const threads = require("node:worker_threads"); +const { Worker, isMainThread, parentPort, workerData } = threads; + +const keys = () => + Object.keys(process.env) + .filter((k) => k.startsWith("SCRIPTC_NEST_")) + .sort() + .map((k) => `${k}=${process.env[k]}`) + .join(" "); + +function makeEnv(kind) { + if (kind === "map") return new Map([["SCRIPTC_NEST_MAP", "x"]]); + if (kind === "indices") return { 5: "x", "05": "y", 4294967295: "z", SCRIPTC_NEST_IDX: [1, 2] }; + if (kind === "getter") { + return { + get SCRIPTC_NEST_GET() { + console.log("getter runs"); + return 42; + }, + }; + } + return { SCRIPTC_NEST_OBJ: kind }; +} + +if (isMainThread) { + process.env.SCRIPTC_NEST_MAIN = "main"; + const plans = [ + ["map", makeEnv("map")], + ["indices", makeEnv("indices")], + ["getter", makeEnv("getter")], + ["share-nested", threads.SHARE_ENV], + ["copy-nested", undefined], + ]; + let next = 0; + const run = () => { + if (next >= plans.length) { + console.log("main", keys()); + return; + } + const [label, env] = plans[next++]; + const worker = new Worker(__filename, { env, workerData: { label, depth: 1 } }); + worker.on("message", (m) => console.log(label, m)); + worker.on("exit", () => { + console.log(label, "main", keys()); + run(); + }); + }; + run(); +} else { + const { label, depth } = workerData; + parentPort.postMessage(`depth ${depth}: ${keys() || "(none)"}`); + if (label === "indices") { + // array-index names are left out of a worker's environment + parentPort.postMessage([process.env["5"], process.env["05"], process.env["4294967295"]]); + } + if (depth === 1 && label.endsWith("-nested")) { + process.env.SCRIPTC_NEST_PARENT = String(label); + const env = label === "share-nested" ? threads.SHARE_ENV : undefined; + const inner = new Worker(__filename, { env, workerData: { label, depth: 2 } }); + inner.on("message", (m) => parentPort.postMessage(m)); + inner.on("exit", () => parentPort.postMessage(`after inner: ${keys()}`)); + } else if (depth === 2) { + process.env.SCRIPTC_NEST_INNER = "inner"; + } +} diff --git a/tests/harness/corpus-costs/5998-worker-env-option.ts.json b/tests/harness/corpus-costs/5998-worker-env-option.ts.json new file mode 100644 index 0000000000..5f865d4f30 --- /dev/null +++ b/tests/harness/corpus-costs/5998-worker-env-option.ts.json @@ -0,0 +1 @@ +{ "plain": 4284, "san": 37577 } diff --git a/tests/harness/corpus-costs/5999-worker-env-nested.cjs.json b/tests/harness/corpus-costs/5999-worker-env-nested.cjs.json new file mode 100644 index 0000000000..d2eb44eb96 --- /dev/null +++ b/tests/harness/corpus-costs/5999-worker-env-nested.cjs.json @@ -0,0 +1 @@ +{ "plain": 5907, "san": 5694 }