Skip to content

worker_threads not respecting --require in process.execArgv or process.env.NODE_OPTIONS #37410

@clemyan

Description

@clemyan
  • Versions:
    • v12.20.2
    • v14.15.5
    • v15.8.0
  • Platforms:
    • CentOS 7 (Linux 3.10.0-1127.8.2.el7.x86_64 #1 SMP Tue May 12 16:57:42 UTC 2020 x86_64 GNU/Linux)
    • Windows 10 (Microsoft Windows NT 10.0.19041.0 x64)
  • Subsystem: worker_threads

What steps will reproduce the bug?

Mutate either process.env.NODE_OPTIONS or process.execArgv to include a --require flag before spawning a worker thread.

Test script
const { Worker, isMainThread, SHARE_ENV } = require('worker_threads')
const { once } = require('events')

if (module.parent && module.parent.id === 'internal/preload') {
  // Preload
  delete require.cache[__filename]
  console.log('preloaded')
} else if (isMainThread && !process.send) {
  // Main
  const tests = [
    async () => {
      console.log('worker process.execArgv')
      const orig = process.execArgv
      process.execArgv = ['--require', __filename, ...process.execArgv]
      const worker = new Worker(__filename)
      await once(worker, 'exit')
      process.execArgv = orig
    },
    async () => {
      console.log('worker process.env.NO')
      const orig = process.env.NODE_OPTIONS
      process.env.NODE_OPTIONS = `--require ${__filename} ${process.env.NODE_OPTIONS}`
      const worker = new Worker(__filename)
      await once(worker, 'exit')
      process.env.NODE_OPTIONS = orig
    },
    async () => {
      console.log('worker SHARE_ENV')
      const orig = process.env.NODE_OPTIONS
      process.env.NODE_OPTIONS = `--require ${__filename} ${process.env.NODE_OPTIONS}`
      const worker = new Worker(__filename, { env: SHARE_ENV })
      await once(worker, 'exit')
      process.env.NODE_OPTIONS = orig
    },
  ]
  ;(async () => {
    for (const f of tests) {
      await f()
      console.log()
    }
  })()
}

What is the expected behavior?

The --require flag is respected in each case:

worker process.execArgv
preloaded

worker process.env.NO
preloaded

worker SHARE_ENV
preloaded

What do you see instead?

The --require flag is not respected:

worker process.execArgv

worker process.env.NO

worker SHARE_ENV

Additional information

tl;dr: Seems like the default value of env and execArgv options of the Worker constructor is the initial value of process.env and process.execArgv instead of the current value. This behavior is in contrast to child_process.fork.

Full test script

This is the full test script with all the tests I have done (detailed below).

Test script
const { Worker, isMainThread, SHARE_ENV, parentPort } = require('worker_threads')
const { fork } = require('child_process')
const { once } = require('events')

if (module.parent && module.parent.id === 'internal/preload') {
  // Preload
  delete require.cache[__filename]
  console.log('preloaded')
} else if (isMainThread && !process.send) {
  // Main
  const tests = [
    async () => {
      console.log('worker control')
      const worker = new Worker(__filename)
      await once(worker, 'exit')
    },
    async () => {
      console.log('worker process.execArgv')
      const orig = process.execArgv
      process.execArgv = ['--require', __filename, ...process.execArgv]
      const worker = new Worker(__filename)
      await once(worker, 'exit')
      process.execArgv = orig
    },
    async () => {
      console.log('worker execArgv option')
      const worker = new Worker(__filename, { execArgv: ['--require', __filename, ...process.execArgv] })
      await once(worker, 'exit')
    },
    async () => {
      console.log('worker process.env.NO')
      const orig = process.env.NODE_OPTIONS
      process.env.NODE_OPTIONS = `--require ${__filename} ${process.env.NODE_OPTIONS}`
      const worker = new Worker(__filename)
      await once(worker, 'exit')
      process.env.NODE_OPTIONS = orig
    },
    async () => {
      console.log('worker SHARE_ENV')
      const orig = process.env.NODE_OPTIONS
      process.env.NODE_OPTIONS = `--require ${__filename} ${process.env.NODE_OPTIONS}`
      const worker = new Worker(__filename, { env: SHARE_ENV })
      await once(worker, 'exit')
      process.env.NODE_OPTIONS = orig
    },
    async () => {
      console.log('worker env option')
      const worker = new Worker(__filename, { env: { NODE_OPTIONS: `--require ${__filename} ${process.env.NODE_OPTIONS}` } })
      await once(worker, 'exit')
    },

    async () => {
      console.log('fork control')
      const cp = fork(__filename)
      await once(cp, 'exit')
    },
    async () => {
      console.log('fork process.execArgv')
      const orig = process.execArgv
      process.execArgv = ['--require', __filename, ...process.execArgv]
      const cp = fork(__filename)
      await once(cp, 'exit')
      process.execArgv = orig
    },
    async () => {
      console.log('fork execArgv option')
      const cp = fork(__filename, { execArgv: ['--require', __filename, ...process.execArgv] })
      await once(cp, 'exit')
    },
    async () => {
      console.log('fork process.env.NO')
      const orig = process.env.NODE_OPTIONS
      process.env.NODE_OPTIONS = `--require ${__filename} ${process.env.NODE_OPTIONS}`
      const cp = fork(__filename)
      await once(cp, 'exit')
      process.env.NODE_OPTIONS = orig
    },
    async () => {
      console.log('fork env option')
      const cp = fork(__filename, { env: { NODE_OPTIONS: `--require ${__filename} ${process.env.NODE_OPTIONS}` } })
      await once(cp, 'exit')
    },
  ]
  ;(async () => {
    for (const f of tests) {
      await f()
      console.log()
    }
  })()
}
Expected output
worker control

worker process.execArgv
preloaded

worker execArgv option
preloaded

worker process.env.NO
preloaded

worker SHARE_ENV
preloaded

worker env option
preloaded

fork control

fork process.execArgv
preloaded

fork execArgv option
preloaded

fork process.env.NO
preloaded

fork env option
preloaded
Actual output
worker control

worker process.execArgv

worker execArgv option
preloaded

worker process.env.NO

worker SHARE_ENV

worker env option
preloaded

fork control

fork process.execArgv
preloaded

fork execArgv option
preloaded

fork process.env.NO
preloaded

fork env option
preloaded

Running node with --require affects worker threads

Modify the test script to use the following test case

async () => {
  console.log('worker control')
  const worker = new Worker(__filename)
  await once(worker, 'exit')
}

Then run the script with node --require ./main.js main.js outputs:

preloaded
worker control
preloaded

which shows the --require flag is respected in both the main thread and the worker thread

--require in Worker options is respected

When explicitly passing execArgv or env.NODE_OPTIONS to the Worker constructor options, the --require flag is respected:

async () => {
  console.log('worker execArgv option')
  const worker = new Worker(__filename, { execArgv: ['--require', __filename, ...process.execArgv] })
  await once(worker, 'exit')
},
async () => {
  console.log('worker env option')
  const worker = new Worker(__filename, { env: { NODE_OPTIONS: `--require ${__filename} ${process.env.NODE_OPTIONS}` } })
  await once(worker, 'exit')
},

The above logs:

worker execArgv option
preloaded

worker env option
preloaded

child_process.fork respects --require in process.execArgv or process.env.NODE_OPTIONS

const { fork } = require('child_process')

async () => {
  console.log('fork process.execArgv')
  const orig = process.execArgv
  process.execArgv = ['--require', __filename, ...process.execArgv]
  const cp = fork(__filename)
  await once(cp, 'exit')
  process.execArgv = orig
},
async () => {
  console.log('fork process.env.NO')
  const orig = process.env.NODE_OPTIONS
  process.env.NODE_OPTIONS = `--require ${__filename} ${process.env.NODE_OPTIONS}`
  const cp = fork(__filename)
  await once(cp, 'exit')
  process.env.NODE_OPTIONS = orig
},

The above logs:

fork process.execArgv
preloaded

fork process.env.NO
preloaded

Metadata

Metadata

Assignees

No one assigned

    Labels

    workerIssues and PRs related to Worker support.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions