From c8a3429deaa9fc8a31a4a191403e493a8fe111ad Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 16:52:16 -0500 Subject: [PATCH 1/2] Add the Worker env option and SHARE_ENV new Worker(filename, { env }) builds the worker's process.env as Node's constructor does. undefined or null copies the parent's environment, as before. An object gives the worker its own enumerable string-keyed properties, each converted with String(); names that are array indices are left out, as in Node. SHARE_ENV (the symbol registered as nodejs.worker_threads.SHARE_ENV) has the worker and its parent use the process environment. Any other value throws ERR_INVALID_ARG_TYPE. The option is checked after argv and before name. SHARE_ENV from a worker that has its own copy of the environment is refused. After the first SHARE_ENV worker starts, process.env reads, writes, deletes and enumeration of the process environment hold a lock on POSIX hosts. Programs without such a worker take no lock. Child processes spawned in a worker without an env option now inherit the worker's process.env, as Node passes process.env. Before, they inherited the process environment. The worker unit recognizes SHARE_ENV through scr_dyn_symbol_key_fn, which the symbol unit installs when its registry first holds a symbol. Programs without symbols do not link the symbol unit. Corpus 5998-5999 match Node v24.22.0. --- .../limitations/native-worker-threads.mdx | 2 +- .../src/generated/node-v24-compatibility.json | 10 +- .../generated/node-v24-backlog.json | 23 +++-- .../generated/node-v24-internal.json | 18 +++- internal/compatibility/static-support.json | 3 +- .../ambient/scriptc-node-fallback.d.ts | 5 + .../src/frontend/lowering/builtins/workers.ts | 9 ++ .../frontend/lowering/lower-builtin-values.ts | 2 +- .../corpus/5998-worker-env-option.ts.json | 6 ++ .../corpus/5999-worker-env-nested.cjs.json | 6 ++ packages/runtime/src/scr_child.c | 37 ++++++- packages/runtime/src/scr_context.c | 25 ++++- packages/runtime/src/scr_core.h | 6 ++ packages/runtime/src/scr_dyn.h | 4 + packages/runtime/src/scr_json.c | 1 + packages/runtime/src/scr_lib.c | 42 +++++++- packages/runtime/src/scr_lib.h | 5 + packages/runtime/src/scr_symbol.c | 5 +- packages/runtime/src/scr_worker.c | 4 +- packages/runtime/src/scr_worker.h | 6 +- packages/runtime/src/scr_worker.test.c | 3 +- packages/runtime/src/scr_worker_events.c | 97 ++++++++++++++++++- tests/corpus/5998-worker-env-option.ts | 92 ++++++++++++++++++ tests/corpus/5999-worker-env-nested.cjs | 69 +++++++++++++ .../5998-worker-env-option.ts.json | 1 + .../5999-worker-env-nested.cjs.json | 1 + 26 files changed, 439 insertions(+), 43 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5998-worker-env-option.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5999-worker-env-nested.cjs.json create mode 100644 tests/corpus/5998-worker-env-option.ts create mode 100644 tests/corpus/5999-worker-env-nested.cjs create mode 100644 tests/harness/corpus-costs/5998-worker-env-option.ts.json create mode 100644 tests/harness/corpus-costs/5999-worker-env-nested.cjs.json diff --git a/docs/content/limitations/native-worker-threads.mdx b/docs/content/limitations/native-worker-threads.mdx index 0510936d69..bbb3f4780c 100644 --- a/docs/content/limitations/native-worker-threads.mdx +++ b/docs/content/limitations/native-worker-threads.mdx @@ -1,4 +1,4 @@ -`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). 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. `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. +`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, 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`, 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. Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`. `worker[Symbol.asyncDispose]()` terminates the worker and resolves with `undefined` after its `exit` event, so `await using` disposes a Worker at the end of its block. A worker's uncaught exception reaches its `error` listeners as Node's error serializer sends it: an Error keeps the nearest of `Error` and its six native subclasses (an `AggregateError` arrives as an `Error` named `AggregateError`) along with its name, message, cause, and other data properties and their enumerability, and other thrown values clone as messages do. An unhandled rejection whose reason is not error-like arrives as an `UnhandledPromiseRejection` Error with code `ERR_UNHANDLED_REJECTION` quoting the reason. A thrown value that does not clone, such as a function, arrives as an `ERR_WORKER_UNSERIALIZABLE_ERROR` Error, where Node sends its `util.inspect` string. `new MessageChannel()` from `node:worker_threads` makes two entangled MessagePorts, in one thread or across threads once a port is transferred, with `receiveMessageOnPort`. Port messages arrive in Node's order: each port's queued messages as one batch before `setImmediate` callbacks, a port's own `'close'` after the poll phase, and the other port's `'close'` after its queued messages while the loop keeps running. `receiveMessageOnPort(parentPort)` in a worker takes the parent's next message. A port's `onmessage` property and `addEventListener` are unsupported. `BroadcastChannel` delivers each message to every other open channel with the same name, in any thread, as a `'message'` `MessageEvent` through `onmessage` and `addEventListener`; an open channel keeps the process alive until `close()` or `unref()`. A `messageerror` event is never dispatched. The parent port supports message listeners, `postMessage`, `close`, `start`, `ref`, `unref`, and `hasRef`. Messages clone primitives, plain object graphs, arrays, errors, ArrayBuffers, typed arrays, and DataViews, preserving cycles and aliases. Program class instances arrive as plain objects with their own data properties, without their prototype, methods, accessors, or private fields, as in Node. `markAsUntransferable` marks objects for the current thread, and a marked ArrayBuffer or MessagePort in a transfer list throws `DataCloneError`, as does the ArrayBuffer of a pooled Buffer; marked objects other than ArrayBuffers stay alive until their thread exits. `markAsUncloneable` marks a plain object, an error, or a class instance, and `structuredClone` and `postMessage` then throw `DataCloneError` for it, also where it is nested; as in Node, the mark has no effect on primitives, arrays, typed arrays, and ArrayBuffers. A plain object that the compiler copies into a container with a fixed element type, such as an array literal whose elements all have one shape, is a separate object in the copy, which is not marked. Marked objects stay alive until their thread exits. A MessagePort travels with a message when the message's transfer list (an array or `{ transfer }`) names it, and the sending port is unusable afterwards; a port in a message without that listing throws `DataCloneError`. Other transferable values, functions, proxies, and other native handles are refused. SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread. Graphs made immutable with `@scriptc/threads` are passed by reference instead of cloned; see [Sharing Data Between Workers](/docs/threads). diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 402e161fb4..9c3b8990e9 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 75e789c618..5e88a2887c 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": 1165, - "verify-gap": 2161, + "audit-partial": 1166, + "verify-gap": 2160, "replace-refusal": 41, "classify": 2, "implement": 85 @@ -84427,7 +84427,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" ] } } @@ -85415,12 +85417,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 c930306021..be22e2ecd7 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 @@ -113290,6 +113290,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", @@ -113933,7 +113935,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" @@ -114878,8 +114882,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 71605612ea..89a1aca34c 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -202,7 +202,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 3b522dd3e3..0ef70cc9f2 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -6059,7 +6059,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 aa826cc53a..e3e7cc572b 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 c05430ad3e..0bffb0186c 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts @@ -528,7 +528,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 87ead57559..7d095b5419 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -174,6 +174,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 @@ -669,7 +680,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) { @@ -3074,6 +3089,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, @@ -3136,10 +3154,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]); @@ -3352,8 +3374,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(); @@ -3474,7 +3497,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); @@ -5032,7 +5057,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 17b68712ef..55e68942f2 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -628,6 +628,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 6174913c0b..5406b98966 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4013,6 +4013,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 930d448183..d0b2144801 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 4c973e3e5d..552bf7a9ee 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 383b959861..3d344909b8 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..486a444cb8 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, 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 53041f3d5b..60ef80d143 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 } From 02b218f866b42bda079abe9248a16251e1b3e704 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 17:31:25 -0500 Subject: [PATCH 2/2] Name the worker environment parameter by its struct tag in scr_worker.h scr_worker.h is compiled without SCR_WORKERS (the runtime's common-symbols check), where scr_core.h does not declare the ScrContextEnv typedef. scr_worker_thread_start's environment parameter now uses struct ScrContextEnv, the same type. --- packages/runtime/src/scr_worker.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/src/scr_worker.h b/packages/runtime/src/scr_worker.h index 486a444cb8..b2edb0eb8e 100644 --- a/packages/runtime/src/scr_worker.h +++ b/packages/runtime/src/scr_worker.h @@ -33,7 +33,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, ScrContextEnv *environment); + 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. */