fix: isolate headless Bun daemon scope launches

This commit is contained in:
m4air
2026-09-28 04:03:52 -07:00
parent 311aacb8cf
commit a1e6b4e74f
2 changed files with 67 additions and 3 deletions
@@ -1,4 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { bunOwnedRuntimeArgs } from '../../shared/bun-owned-runtime-args'
import { spawnDaemonChildProcess } from './daemon-launched-child-spawn'
const { spawn, fork } = vi.hoisted(() => ({ spawn: vi.fn(), fork: vi.fn() }))
@@ -20,7 +21,11 @@ const options = {
macosLoginSessionWatch: false
}
afterEach(() => vi.clearAllMocks())
afterEach(() => {
vi.clearAllMocks()
vi.unstubAllEnvs()
vi.restoreAllMocks()
})
describe('daemon launch scope ownership', () => {
it('only arms lifetime cleanup through the private scope launcher', () => {
@@ -49,3 +54,47 @@ describe('daemon launch scope ownership', () => {
)
})
})
describe('headless Bun daemon launch', () => {
function useBun(): void {
vi.spyOn(process, 'versions', 'get').mockReturnValue({ ...process.versions, bun: '1.4.2' })
vi.stubEnv('NODE_OPTIONS', '--require=untrusted.js')
vi.stubEnv('NODE_PATH', '/untrusted')
vi.stubEnv('BUN_OPTIONS', '--preload=untrusted.js')
}
it('isolates scoped Bun launches before the daemon entry', () => {
useBun()
spawnDaemonChildProcess(options, true)
const call = spawn.mock.calls[0][0]
const executableIndex = call.args.indexOf(process.execPath)
expect(call.args.slice(executableIndex + 1, executableIndex + 5)).toEqual([
...bunOwnedRuntimeArgs(),
options.forkEntryPath
])
for (const key of ['ELECTRON_RUN_AS_NODE', 'NODE_OPTIONS', 'NODE_PATH', 'BUN_OPTIONS']) {
expect(call.env[key]).toBeUndefined()
}
})
it('scrubs direct Bun forks while preserving the selected ConPTY library', () => {
useBun()
vi.stubEnv('BUN_CONPTY_LIBRARY', '/verified/conpty.dll')
spawnDaemonChildProcess(options, false)
expect(fork.mock.calls[0][0].env).toEqual(
expect.objectContaining({
BUN_CONPTY_LIBRARY: '/verified/conpty.dll',
ORCA_USER_DATA_PATH: options.userDataPath
})
)
expect(fork.mock.calls[0][0].env.NODE_OPTIONS).toBeUndefined()
})
it('preserves an explicitly selected Node executable', () => {
useBun()
spawnDaemonChildProcess({ ...options, relocatedExecPath: '/alternate/node' }, true)
const call = spawn.mock.calls[0][0]
expect(call.args).not.toContain('--no-install')
expect(call.env.ELECTRON_RUN_AS_NODE).toBe('1')
})
})
+17 -2
View File
@@ -1,5 +1,6 @@
import { forkProcess, type ForkSpec } from '../../shared/child-process/fork-process'
import { spawnProcess, type SpawnedProcess } from '../../shared/child-process/run-process'
import { bunOwnedRuntimeArgs } from '../../shared/bun-owned-runtime-args'
import { getAppEnvironment } from '../../shared/app-environment'
import { buildDurableDaemonScopeCommand } from './daemon-cgroup-scope'
import { daemonLogArgs } from './daemon-launch-paths'
@@ -57,13 +58,22 @@ export function spawnDaemonChildProcess(
): SpawnedProcess {
const { forkEntryPath, relocatedExecPath, userDataPath, launchNonce } = options
const scriptArgs = buildDaemonScriptArgs(options)
const usesBun = Boolean(
process.versions.bun && (!relocatedExecPath || relocatedExecPath === process.execPath)
)
// Why: run as plain Node so Electron's GPU/display init can't interfere with node-pty's posix_spawn of the spawn-helper.
const daemonEnv = {
const daemonEnv: NodeJS.ProcessEnv = {
...process.env,
ELECTRON_RUN_AS_NODE: '1',
// Why: the detached plain-Node daemon has no AppEnvironment, but shell rcfiles must live outside swept tmp.
ORCA_USER_DATA_PATH: userDataPath
}
if (usesBun) {
delete daemonEnv.ELECTRON_RUN_AS_NODE
delete daemonEnv.NODE_OPTIONS
delete daemonEnv.NODE_PATH
delete daemonEnv.BUN_OPTIONS
}
// Why cwd: detached daemons outlive dev worktrees; userData keeps process.cwd() valid after a repo/worktree is deleted.
// Why detached/stdio: detached+unref outlives Electron; stdout 'ignore' (else blocks exit), stderr 'pipe' captures startup crashes lost in v1.4.129-rc.1.
const childOptions: Pick<ForkSpec, 'cwd' | 'detached' | 'stdio'> = {
@@ -83,7 +93,12 @@ export function spawnDaemonChildProcess(
}
const scoped = buildDurableDaemonScopeCommand(
relocatedExecPath ?? process.execPath,
[forkEntryPath, ...scriptArgs, '--fresh-daemon-scope'],
[
...(usesBun ? bunOwnedRuntimeArgs() : []),
forkEntryPath,
...scriptArgs,
'--fresh-daemon-scope'
],
launchNonce,
daemonEnv
)