]> Piment Noir Git Repositories - poolifier.git/commitdiff
fix: preserve own worker launch option descriptors (#3342)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Thu, 15 Oct 2026 11:24:54 +0000 (13:24 +0200)
committerGitHub <noreply@github.com>
Thu, 15 Oct 2026 11:24:54 +0000 (13:24 +0200)
src/pools/utils.ts
src/pools/worker-node.ts
tests/pools/utils.test.mjs

index 3f9c4ac294707d80d179598c44aedcdd1df1ba13..0e45484b8679f1e506b263e5ef6894b0c2ed5ceb 100644 (file)
@@ -71,6 +71,52 @@ const defineEnumerableDataProperty = (
   Object.defineProperty(target, key, descriptor)
 }
 
+const cloneOwnPropertiesWithNullPrototype = (
+  source: object,
+  materializedKeys: readonly PropertyKey[],
+  materializeEnumerableProperties = false,
+  omittedKeys: readonly PropertyKey[] = []
+): Record<PropertyKey, unknown> => {
+  const clone = Object.create(null) as Record<PropertyKey, unknown>
+  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<WorkerOptions>(
-    { env: SHARE_ENV },
-    workerOptions
-  )
+  const normalizedWorkerOptions =
+    workerOptions == null
+      ? withNullPrototype<WorkerOptions>()
+      : (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 = <Worker extends IWorker>(
     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
index 615c3f5df39a60c0efe1537bc2e706bdf84345c0..5632c77a795370077c5b461b8f5bfe230face197 100644 (file)
@@ -58,8 +58,10 @@ export class WorkerNode<Worker extends IWorker, Data = unknown>
     super()
     checkWorkerNodeArguments(type, filePath, opts)
     this.worker = createWorker<Worker>(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()
index 561b68febb00131c8c3e3bc03e8895037dc05984..0c5a56e01eb38807446ec975d2e91aa49fe44e67 100644 (file)
@@ -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)