From: Jérôme Benoit Date: Thu, 15 Oct 2026 11:24:54 +0000 (+0200) Subject: fix: preserve own worker launch option descriptors (#3342) X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=216e95bfc9bfbe89537496f648d446a12ac556ec;p=poolifier.git fix: preserve own worker launch option descriptors (#3342) --- diff --git a/src/pools/utils.ts b/src/pools/utils.ts index 3f9c4ac29..0e45484b8 100644 --- a/src/pools/utils.ts +++ b/src/pools/utils.ts @@ -71,6 +71,52 @@ const defineEnumerableDataProperty = ( Object.defineProperty(target, key, descriptor) } +const cloneOwnPropertiesWithNullPrototype = ( + source: object, + materializedKeys: readonly PropertyKey[], + materializeEnumerableProperties = false, + omittedKeys: readonly PropertyKey[] = [] +): Record => { + const clone = Object.create(null) as Record + for (const key of Reflect.ownKeys(source)) { + const descriptor = Object.getOwnPropertyDescriptor(source, key) + if (descriptor == null) { + continue + } + if (omittedKeys.includes(key)) { + if (materializeEnumerableProperties && descriptor.enumerable === true) { + Reflect.get(source, key) + } + continue + } + if ( + materializedKeys.includes(key) || + (materializeEnumerableProperties && descriptor.enumerable === true) + ) { + defineEnumerableDataProperty(clone, key, Reflect.get(source, key)) + } else { + Object.setPrototypeOf(descriptor, null) + Object.defineProperty(clone, key, descriptor) + } + } + return clone +} + +const threadWorkerOptionKeys: readonly PropertyKey[] = [ + 'argv', + 'env', + 'eval', + 'execArgv', + 'name', + 'resourceLimits', + 'stderr', + 'stdin', + 'stdout', + 'trackUnmanagedFds', + 'transferList', + 'workerData', +] + const cloneDenseExecArgv = ( execArgv: readonly string[], optionName: string @@ -371,10 +417,17 @@ export const buildClusterSettings = ( export const buildThreadWorkerOptions = ( workerOptions: undefined | WorkerOptions ): WorkerOptions => { - const normalizedWorkerOptions = withNullPrototype( - { env: SHARE_ENV }, - workerOptions - ) + const normalizedWorkerOptions = + workerOptions == null + ? withNullPrototype() + : (cloneOwnPropertiesWithNullPrototype( + workerOptions, + threadWorkerOptionKeys, + true + ) as WorkerOptions) + if (!Object.hasOwn(normalizedWorkerOptions, 'env')) { + defineEnumerableDataProperty(normalizedWorkerOptions, 'env', SHARE_ENV) + } if (Array.isArray(normalizedWorkerOptions.argv)) { normalizedWorkerOptions.argv = cloneArrayOwnSlots( normalizedWorkerOptions.argv @@ -836,12 +889,17 @@ export const createWorker = ( case WorkerTypes.cluster: validateAndMaterializeClusterStdio(cluster.settings.stdio) return cluster.fork( - buildClusterWorkerEnvironment(process.env, opts.env) + buildClusterWorkerEnvironment( + process.env, + Object.hasOwn(opts, 'env') ? opts.env : undefined + ) ) as unknown as Worker case WorkerTypes.thread: return new ThreadWorker( filePath, - buildThreadWorkerOptions(opts.workerOptions) + buildThreadWorkerOptions( + Object.hasOwn(opts, 'workerOptions') ? opts.workerOptions : undefined + ) ) as unknown as Worker default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions diff --git a/src/pools/worker-node.ts b/src/pools/worker-node.ts index 615c3f5df..5632c77a7 100644 --- a/src/pools/worker-node.ts +++ b/src/pools/worker-node.ts @@ -58,8 +58,10 @@ export class WorkerNode super() checkWorkerNodeArguments(type, filePath, opts) this.worker = createWorker(type, filePath, { - env: opts.env, - workerOptions: opts.workerOptions, + env: Object.hasOwn(opts, 'env') ? opts.env : undefined, + workerOptions: Object.hasOwn(opts, 'workerOptions') + ? opts.workerOptions + : undefined, }) this.info = initWorkerInfo(this.worker) this.usage = this.initWorkerUsage() diff --git a/tests/pools/utils.test.mjs b/tests/pools/utils.test.mjs index 561b68feb..0c5a56e01 100644 --- a/tests/pools/utils.test.mjs +++ b/tests/pools/utils.test.mjs @@ -1,4 +1,5 @@ import { Worker as ClusterWorker } from 'node:cluster' +import { once } from 'node:events' import { PassThrough } from 'node:stream' import { SHARE_ENV, Worker as ThreadWorker } from 'node:worker_threads' import { describe, expect, it } from 'vitest' @@ -206,6 +207,73 @@ describe('Pool utils test suite', () => { expect(workerOptions.execArgv).not.toBe(execArgv) }) + it('preserves non-enumerable own thread worker options', () => { + let accessorCalls = 0 + const accessorGetter = () => { + accessorCalls++ + return 'ignored' + } + const environment = { EXPLICIT_ENV: 'worker' } + const execArgv = ['--no-warnings'] + const explicitWorkerOptions = Object.create({ name: 'inherited' }) + Object.defineProperties(explicitWorkerOptions, { + env: { value: environment }, + execArgv: { value: execArgv }, + ignored: { get: accessorGetter }, + }) + + const workerOptions = buildThreadWorkerOptions(explicitWorkerOptions) + + expect(Object.getPrototypeOf(workerOptions)).toBeNull() + expect(Object.hasOwn(workerOptions, 'env')).toBe(true) + expect(workerOptions.env).toBe(environment) + expect(Object.hasOwn(workerOptions, 'execArgv')).toBe(true) + expect(workerOptions.execArgv).toStrictEqual(execArgv) + expect(workerOptions.execArgv).not.toBe(execArgv) + expect(Object.hasOwn(workerOptions, 'name')).toBe(false) + expect(Object.getOwnPropertyDescriptor(workerOptions, 'ignored').get).toBe( + accessorGetter + ) + expect(accessorCalls).toBe(0) + }) + + it('preserves non-enumerable restrictions at the thread worker boundary', async () => { + const environmentKey = 'POOLIFIER_EXPLICIT_WORKER_ENV' + const originalEnvironmentValue = process.env[environmentKey] + process.env[environmentKey] = 'parent' + const explicitWorkerOptions = {} + Object.defineProperties(explicitWorkerOptions, { + env: { value: { [environmentKey]: 'worker' } }, + eval: { value: true }, + execArgv: { value: ['--no-warnings'] }, + }) + let worker + try { + worker = new ThreadWorker( + `const { parentPort } = require('node:worker_threads') +parentPort.postMessage({ + envValue: process.env.${environmentKey}, + execArgv: process.execArgv +})`, + buildThreadWorkerOptions(explicitWorkerOptions) + ) + const [message] = await once(worker, 'message') + expect(message).toStrictEqual({ + envValue: 'worker', + execArgv: ['--no-warnings'], + }) + } finally { + if (worker != null) { + await worker.terminate() + } + if (originalEnvironmentValue == null) { + Reflect.deleteProperty(process.env, environmentKey) + } else { + process.env[environmentKey] = originalEnvironmentValue + } + } + }) + it('normalizes worker option arrays and resource limits by own slots', () => { const argvValue = { value: 'argv' } const argv = new Array(2)