feat: validate direct bundled Bun startup before loading orcad

This commit is contained in:
m4air
2026-09-27 16:29:49 -07:00
parent 3eb1adec20
commit 07000430bb
19 changed files with 419 additions and 702 deletions
-9
View File
@@ -71,15 +71,6 @@ jobs:
# build:orcad reuses the host addon validated by native-runtime preparation.
- run: pnpm build:orcad
- run: pnpm test:bun:profile --artifact
- uses: actions/setup-node@v6
if: runner.arch == 'X64'
with:
node-version: '18'
- name: Verify Node 18 loads and hands off to bundled Bun
if: runner.arch == 'X64'
run: |
node out/orcad/orcad.js --orcad-smoke-load-check
node out/orcad/orcad.js --orcad-profile-state-preflight 00000000-0000-4000-8000-000000000018
linux_glibc_floor:
needs: changes
+18 -20
View File
@@ -4,6 +4,8 @@ import { fork, spawnSync } from 'node:child_process'
import { build } from 'esbuild'
import {
buildOrcadEntry,
buildOrcadLauncher,
ORCAD_BUN_TARGET,
externalNativeAddons,
ORCAD_EXTERNAL_MODULES,
ORCAD_CHILD_ENTRY_POINTS
@@ -88,7 +90,7 @@ async function stageParcelWatcher(target) {
},
bundle: true,
platform: 'node',
target: 'node18',
target: ORCAD_BUN_TARGET,
format: 'cjs',
outfile: join(OUT_DIR, ORCAD_PARCEL_WATCHER_ENTRY),
external: ['./watcher.node'],
@@ -159,7 +161,7 @@ function buildForkedChild(entryPoint, outfile) {
entryPoints: [entryPoint],
bundle: true,
platform: 'node',
target: 'node18',
target: ORCAD_BUN_TARGET,
format: 'cjs',
outfile,
external: ORCAD_EXTERNAL_MODULES,
@@ -186,10 +188,11 @@ const childResults = await Promise.all([
)
])
const result = await buildOrcadEntry(OUT_FILE)
const launcher = await buildOrcadLauncher(OUT_FILE)
const result = await buildOrcadEntry(join(OUT_DIR, 'orcad-app.js'))
const output = Object.values(result.metafile.outputs).find(
(o) => o.entryPoint === 'src/main/orcad/main.ts'
(o) => o.entryPoint === 'src/main/orcad/orcad-app.ts'
)
// Why check `original` and not just `path`: when electron is bundleable, esbuild
// rewrites `path` to the resolved file under node_modules and the naive check passes
@@ -211,7 +214,11 @@ function collectImporters(metafiles, matches) {
return importers
}
const metafiles = [result.metafile, ...childResults.map((child) => child.metafile)]
const metafiles = [
launcher.metafile,
result.metafile,
...childResults.map((child) => child.metafile)
]
const electronImporters = collectImporters(
metafiles,
(specifier) => specifier === 'electron' || specifier.startsWith('electron/')
@@ -241,24 +248,16 @@ if (graphErrors.length > 0) {
// install the PTY controller. Once orcad ships, it should become a ratchet entry
// point so the two numbers cannot drift.
process.exitCode = 1
} else {
// Why smoke-load and not just read the metafile: the import scan proves no module
// *names* electron, but the rollback graph can still fail to resolve under plain Node — a
// dynamic require, a missing native, a top-level throw. The plain-node-entry-guard
// smoke-loads its entries for exactly this reason, and orcad cannot join that guard
// because it is an esbuild artifact rather than a rollup input.
// Why an exit code and not a message match: these bundles are minified onto one line, so
// Node's uncaught-exception report echoes that whole line — which contains every string
// literal in the bundle. A crash therefore "matches" any expected message, and a textual
// assertion passes against a bundle that never loaded.
const smoke = spawnSync(process.execPath, [OUT_FILE, '--orcad-smoke-load-check'], {
} else if (targetIsCurrent) {
// Exercise the validated entry and application with the shipped runtime.
const smoke = spawnSync(bunRuntimeOutput, [OUT_FILE, '--orcad-smoke-load-check'], {
encoding: 'utf8',
timeout: 60_000
})
const smokeOutput = `${smoke.stdout ?? ''}${smoke.stderr ?? ''}`
if (smoke.error || smoke.signal || smoke.status !== 0) {
console.error(
`[build-orcad] the bundle lost Node load compatibility.\n` +
`[build-orcad] the entry failed to load under bundled Bun.\n` +
`Expected a clean load-check exit, got status=${smoke.status ?? 'none'} ` +
`signal=${smoke.signal ?? 'none'} ` +
`error=${smoke.error?.message ?? 'none'}\n${smokeOutput.slice(0, 2000)}`
@@ -271,7 +270,7 @@ if (graphErrors.length > 0) {
// token and a PTY — `smoke:orcad-terminal` does that end to end, through orcad.
// The verdict is carried by the exit code for the same minification reason as above.
const daemonSmoke = spawnSync(
process.execPath,
bunRuntimeOutput,
[
'-e',
`const mod = require(${JSON.stringify(DAEMON_OUT_FILE)})\n` +
@@ -287,7 +286,7 @@ if (graphErrors.length > 0) {
const daemonSmokeOutput = `${daemonSmoke.stdout ?? ''}${daemonSmoke.stderr ?? ''}`
if (daemonSmoke.error || daemonSmoke.signal || daemonSmoke.status !== 0) {
console.error(
`[build-orcad] the daemon child lost Node load compatibility.\n` +
`[build-orcad] the daemon child failed to load under bundled Bun.\n` +
`Expected a clean load check, got status=${daemonSmoke.status ?? 'none'} ` +
`signal=${daemonSmoke.signal ?? 'none'} ` +
`error=${daemonSmoke.error?.message ?? 'none'}\n${daemonSmokeOutput.slice(0, 2000)}`
@@ -304,7 +303,6 @@ if (graphErrors.length > 0) {
}
try {
await smokeProfileStateWorkers(OUT_DIR)
if (targetIsCurrent) {
await smokeProfileStateWorkers(OUT_DIR, { runtimePath: bunRuntimeOutput })
}
+3 -1
View File
@@ -5,7 +5,8 @@ import { pathToFileURL } from 'node:url'
import {
externalNativeAddons,
ORCAD_CHILD_ENTRY_POINTS,
ORCAD_ENTRY_POINT
ORCAD_ENTRY_POINT,
ORCAD_LAUNCHER_ENTRY_POINT
} from './orcad-entry-build.mjs'
import { bunProfileTestPaths } from './bun-profile-test-paths.mjs'
@@ -59,6 +60,7 @@ export function discoverBunProfileTests(root = ROOT) {
export async function collectBunProfileInputs({ root = ROOT, entryPoints } = {}) {
const entries = entryPoints ?? [
ORCAD_ENTRY_POINT,
ORCAD_LAUNCHER_ENTRY_POINT,
...Object.values(ORCAD_CHILD_ENTRY_POINTS),
...BUILD_SCRIPTS,
...discoverBunProfileTests(root)
+3 -1
View File
@@ -17,7 +17,9 @@ export function bunProfileTestPaths({ artifact = false } = {}) {
'src/main/providers/local-pty-bun-artifact.integration.test.ts',
'src/main/providers/agent-foreground-process-git-bash.win32.test.ts',
'src/main/orcad/orcad-bun-launcher.integration.test.ts',
'config/scripts/zip-extractor-command.test.mjs'
'config/scripts/zip-extractor-command.test.mjs',
'config/scripts/orcad-launcher-build.test.mjs',
'src/main/orcad/orcad-bundle-native-load-order.test.ts'
]
: [])
]
+19 -2
View File
@@ -3,7 +3,9 @@ import { join } from 'node:path'
const root = join(import.meta.dirname, '..', '..')
export const ORCAD_ENTRY_POINT = 'src/main/orcad/main.ts'
export const ORCAD_ENTRY_POINT = 'src/main/orcad/orcad-app.ts'
export const ORCAD_LAUNCHER_ENTRY_POINT = 'src/main/orcad/main.ts'
export const ORCAD_BUN_TARGET = 'es2024'
export const ORCAD_CHILD_ENTRY_POINTS = {
watcher: 'src/main/ipc/parcel-watcher-process-entry.ts',
daemon: 'src/main/daemon/daemon-entry.ts',
@@ -44,7 +46,7 @@ export function buildOrcadEntry(outfile) {
entryPoints: [join(root, ORCAD_ENTRY_POINT)],
bundle: true,
platform: 'node',
target: 'node18',
target: ORCAD_BUN_TARGET,
format: 'cjs',
outfile,
external: ORCAD_EXTERNAL_MODULES,
@@ -56,3 +58,18 @@ export function buildOrcadEntry(outfile) {
logLevel: 'error'
})
}
export function buildOrcadLauncher(outfile) {
return build({
entryPoints: [join(root, ORCAD_LAUNCHER_ENTRY_POINT)],
bundle: true,
platform: 'node',
target: ORCAD_BUN_TARGET,
format: 'cjs',
outfile,
external: ['./orcad-app'],
metafile: true,
minify: true,
logLevel: 'error'
})
}
@@ -0,0 +1,111 @@
import { existsSync, realpathSync, statSync } from 'node:fs'
import { copyFile, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { buildOrcadLauncher } from './orcad-entry-build.mjs'
import { orcadBunRuntimeFilename } from '../../src/shared/orcad-artifacts'
import { ORCAD_BUN_VERSION } from '../../src/shared/orcad-bun-runtime'
import { runProcess } from '../../src/shared/child-process/run-process'
const bundledRuntime = resolve('out/orcad', orcadBunRuntimeFilename(process.platform))
const nodeRuntime =
process.env.ORCA_TEST_NODE_EXECUTABLE ?? (process.versions.bun ? 'node' : process.execPath)
let directory
let launcher
let buildResult
beforeAll(async () => {
directory = await mkdtemp(join(tmpdir(), 'orca launcher boundary '))
launcher = join(directory, 'orcad.js')
buildResult = await buildOrcadLauncher(launcher)
})
afterAll(async () => {
if (directory) {
await rm(directory, { recursive: true, force: true })
}
})
it('keeps the application graph outside the validated Bun entry', () => {
const inputs = Object.keys(buildResult.metafile.inputs)
expect(inputs.some((input) => input.endsWith('/orcad/main.ts'))).toBe(true)
expect(inputs.some((input) => input.endsWith('/orcad/orcad-app.ts'))).toBe(false)
expect(inputs.some((input) => input.includes('/persistence/'))).toBe(false)
expect(
Object.values(buildResult.metafile.outputs).flatMap((output) => output.imports)
).toContainEqual(expect.objectContaining({ path: './orcad-app', external: true }))
})
it('rejects Node without loading application code', async () => {
const result = await runProcess({ program: nodeRuntime, args: [launcher], timeoutMs: 10_000 })
expect(result.code, result.stderr).toBe(78)
expect(result.stderr).toContain('not Node')
})
describe.skipIf(!existsSync(bundledRuntime))('bundled launcher application boundary', () => {
beforeAll(async () => {
await copyFile(bundledRuntime, join(directory, orcadBunRuntimeFilename(process.platform)))
await writeFile(join(directory, '.build-target'), `${process.platform}-${process.arch}\n`)
await writeFile(
join(directory, 'orcad-app.js'),
`
const { Database } = require('bun:sqlite')
const db = new Database(':memory:')
const value = db.query('SELECT 42 AS value').get().value
db.close()
console.log(JSON.stringify({ bun: process.versions.bun, value, args: process.argv.slice(2), entry: process.argv[1] }))
`
)
})
it('loads a Bun-only application directly without changing arguments', async () => {
const result = await runProcess({
program: join(directory, orcadBunRuntimeFilename(process.platform)),
args: [launcher, '--data-dir', 'a path with spaces'],
timeoutMs: 10_000
})
expect(result.code, result.stderr).toBe(0)
const { entry, ...response } = JSON.parse(result.stdout)
expect(response).toEqual({
bun: ORCAD_BUN_VERSION,
value: 42,
args: ['--data-dir', 'a path with spaces']
})
const actual = statSync(entry, { bigint: true })
const expected = statSync(launcher, { bigint: true })
expect(actual.ino).toBe(expected.ino)
expect(actual.dev).toBe(expected.dev)
})
it.skipIf(process.platform === 'win32')(
'resolves a directly launched symlink before locating application assets',
async () => {
const links = await mkdtemp(join(tmpdir(), 'orca launcher link '))
try {
const link = join(links, 'orcad.js')
await symlink(launcher, link)
const result = await runProcess({
program: join(directory, orcadBunRuntimeFilename(process.platform)),
args: [link],
timeoutMs: 10_000
})
expect(result.code, result.stderr).toBe(0)
expect(JSON.parse(result.stdout).entry).toBe(realpathSync(launcher))
} finally {
await rm(links, { recursive: true, force: true })
}
}
)
it('reports a missing application as a configuration fault', async () => {
await rm(join(directory, 'orcad-app.js'))
const result = await runProcess({
program: join(directory, orcadBunRuntimeFilename(process.platform)),
args: [launcher],
timeoutMs: 10_000
})
expect(result.code, result.stderr).toBe(78)
expect(result.stderr).toContain('application could not load')
})
})
@@ -28,6 +28,8 @@ import { dirname, join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { randomBytes } from 'node:crypto'
import process from 'node:process'
import { orcadBunRuntimeFilename } from '../../src/shared/orcad-artifacts.ts'
import { createServeStopRequest } from '../../src/shared/serve-supervisor-control.ts'
const projectDir = resolve(import.meta.dirname, '../..')
const serveEntry = join(projectDir, 'out', 'main', 'index.js')
@@ -175,7 +177,8 @@ function resolveLaunch(userDataDir) {
if (target === 'orcad') {
return {
label: `orcad (${ORCAD_ENTRY})`,
command: process.execPath,
controlIpc: true,
command: join(projectDir, 'out', 'orcad', orcadBunRuntimeFilename(process.platform)),
args: [ORCAD_ENTRY, '--port', String(PORT), '--json'],
env: { ORCA_USER_DATA: userDataDir }
}
@@ -231,10 +234,14 @@ async function main() {
let pairing = null
const child = spawn(launch.command, launch.args, {
stdio: ['ignore', 'pipe', 'pipe'],
stdio: launch.controlIpc ? ['ignore', 'pipe', 'pipe', 'ipc'] : ['ignore', 'pipe', 'pipe'],
env: { ...process.env, ...launch.env }
})
const stopControl = createServeStopRequest(child)
child.on('message', stopControl.handleMessage)
child.once('exit', () => child.off('message', stopControl.handleMessage))
try {
const ready = await waitForReady(child)
log(`ready: ${ready.advertisedEndpoint}`)
@@ -385,7 +392,11 @@ async function main() {
// waiting for a second 'exit' that will never fire reported a bogus shutdown failure
// stacked on top of the real error.
if (child.exitCode === null && child.signalCode === null) {
child.kill('SIGTERM')
if (launch.controlIpc) {
stopControl.request()
} else {
child.kill('SIGTERM')
}
const exited = await Promise.race([
new Promise((r) => child.on('exit', () => r(true))),
new Promise((r) => setTimeout(() => r(false), SHUTDOWN_TIMEOUT_MS))
+1
View File
@@ -0,0 +1 @@
export { createServeStopRequest } from '../../shared/serve-supervisor-control'
+3 -3
View File
@@ -14,7 +14,6 @@ const { order, profileProbe } = vi.hoisted(() => {
return { order, profileProbe: vi.fn(async () => {}) }
})
vi.mock('./orcad-bundled-runtime', () => ({ handoffToBundledOrcad: () => false }))
vi.mock('./orcad-profile-preflight', () => ({
preflightBundledOrcadStartup: async () => {
order.push('profile-admission')
@@ -24,6 +23,7 @@ vi.mock('./orcad-profile-preflight', () => ({
beforeEach(() => {
vi.resetModules()
vi.spyOn(process, 'versions', 'get').mockReturnValue({ ...process.versions, bun: 'test' })
order.length = 0
})
afterEach(() => {
@@ -52,14 +52,14 @@ describe('orcad entry', () => {
'runs the selected disposable probe without starting a server: $flag',
async ({ flag, nativeFeatures }) => {
vi.spyOn(process, 'argv', 'get').mockReturnValue(['runtime', 'orcad.js', flag, 'nonce'])
await import('./main')
await import('./orcad-app')
expect(profileProbe).toHaveBeenCalledExactlyOnceWith('nonce', { nativeFeatures })
expect(order).toEqual([])
}
)
it('runs the native preflight before starting the runtime', async () => {
await import('./main')
await import('./orcad-app')
await vi.waitFor(() => expect(order).toContain('main'))
expect(order).toEqual(['profile-admission', 'preflight', 'main'])
+14 -44
View File
@@ -1,54 +1,24 @@
/** Executable entry for `orcad`. See `./orcad-entry.ts`. */
import process from 'node:process'
import { main, resolveOrcadExitCode } from './orcad-entry'
import { runOrcadNativePreflight } from './orcad-native-preflight'
import {
ORCAD_PROFILE_PREFLIGHT_FLAG,
ORCAD_STARTUP_PREFLIGHT_FLAG
} from '../../shared/orcad-profile-preflight'
import { preflightBundledOrcadStartup, runOrcadProfilePreflight } from './orcad-profile-preflight'
import { handoffToBundledOrcad } from './orcad-bundled-runtime'
/** Validate the runtime before loading the application or opening a profile. */
import { realpathSync } from 'node:fs'
import { assertBundledOrcadRuntime, OrcadBundledRuntimeError } from './orcad-bundled-runtime'
// Why exit before the preflight: reaching this line means the whole module graph resolved
// under plain Node, which is all the build guard needs to prove. Probing natives or
// starting a server to prove it would bind a port and take a data-root lock on a build
// machine.
if (process.argv.includes('--orcad-smoke-load-check')) {
process.exit(0)
}
// Why here and not inside startOrcad: this must run before anything requires node-pty,
// and `orcad-entry` reaches it through `await import('../ipc/pty')`. Static imports are
// evaluated before this statement, so the guarantee is that no module in the graph
// requires node-pty at import time — which the bundle's lazy `require("node-pty")` in
// local-pty-provider satisfies. See ./node-pty-precondition.ts for why a child process.
function failStartup(error: unknown): void {
console.error('orcad: failed to start:', error)
// Why a resolved code and not a bare 1: a data-root or bind-address refusal is a
// configuration fault that restarting cannot fix, and a supervisor needs to tell the two
// apart to avoid restart-spinning on it.
process.exit(resolveOrcadExitCode(error))
process.exit(error instanceof OrcadBundledRuntimeError ? 78 : 1)
}
try {
if (!handoffToBundledOrcad()) {
const flag = process.argv[2]
if (
(flag === ORCAD_PROFILE_PREFLIGHT_FLAG || flag === ORCAD_STARTUP_PREFLIGHT_FLAG) &&
process.argv.length === 4
) {
void runOrcadProfilePreflight(process.argv[3], {
nativeFeatures: flag === ORCAD_PROFILE_PREFLIGHT_FLAG
}).catch(failStartup)
} else {
void preflightBundledOrcadStartup()
.then(() => {
runOrcadNativePreflight()
return main()
})
.catch(failStartup)
}
assertBundledOrcadRuntime()
const entry = process.argv[1]
if (!entry) {
throw new OrcadBundledRuntimeError('The Orca entry is missing')
}
process.argv[1] = realpathSync(entry)
void import('./orcad-app').catch((cause: unknown) => {
failStartup(
new OrcadBundledRuntimeError('The bundled Orca application could not load', { cause })
)
})
} catch (error) {
failStartup(error)
}
+45
View File
@@ -0,0 +1,45 @@
/** Bun application entry; the public launcher owns runtime selection. */
import process from 'node:process'
import { main, resolveOrcadExitCode } from './orcad-entry'
import { runOrcadNativePreflight } from './orcad-native-preflight'
import {
ORCAD_PROFILE_PREFLIGHT_FLAG,
ORCAD_STARTUP_PREFLIGHT_FLAG
} from '../../shared/orcad-profile-preflight'
import { preflightBundledOrcadStartup, runOrcadProfilePreflight } from './orcad-profile-preflight'
function failStartup(error: unknown): void {
console.error('orcad: failed to start:', error)
// Why a resolved code and not a bare 1: a data-root or bind-address refusal is a
// configuration fault that restarting cannot fix, and a supervisor needs to tell the two
// apart to avoid restart-spinning on it.
process.exit(resolveOrcadExitCode(error))
}
try {
if (!process.versions.bun) {
throw new Error('orcad requires its bundled Bun runtime')
}
// Load-check evaluates the Bun application graph without opening profile state.
if (process.argv.includes('--orcad-smoke-load-check')) {
process.exit(0)
}
const flag = process.argv[2]
if (
(flag === ORCAD_PROFILE_PREFLIGHT_FLAG || flag === ORCAD_STARTUP_PREFLIGHT_FLAG) &&
process.argv.length === 4
) {
void runOrcadProfilePreflight(process.argv[3], {
nativeFeatures: flag === ORCAD_PROFILE_PREFLIGHT_FLAG
}).catch(failStartup)
} else {
void preflightBundledOrcadStartup()
.then(() => {
runOrcadNativePreflight()
return main()
})
.catch(failStartup)
}
} catch (error) {
failStartup(error)
}
@@ -1,18 +1,16 @@
import { SERVE_STOP_READY, SERVE_STOP_REQUEST } from '../../shared/serve-supervisor-control'
import { build } from 'esbuild'
import { existsSync, readFileSync } from 'node:fs'
import { existsSync } from 'node:fs'
import { copyFile, mkdtemp, readFile, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { spawnProcess } from '../../shared/child-process/run-process'
import { orcadBunRuntimeFilename } from '../../shared/orcad-artifacts'
import { ORCAD_BUN_VERSION } from '../../shared/orcad-bun-runtime'
import { removeTreeSync } from '../../shared/windows-transient-lock-removal'
const runtimePath =
process.env.BUN_EXECUTABLE ?? resolve('out/orcad', orcadBunRuntimeFilename(process.platform))
const nodePath =
process.env.ORCA_TEST_NODE_EXECUTABLE ?? (process.versions.bun ? 'node' : process.execPath)
let directory = ''
const children = new Set<ReturnType<typeof spawnProcess>>()
const runtimes = new Set<number>()
@@ -23,49 +21,16 @@ describe.skipIf(!existsSync(runtimePath))('real Bun launcher lifecycle', () => {
await copyFile(runtimePath, join(directory, orcadBunRuntimeFilename(process.platform)))
await writeFile(join(directory, '.build-target'), `${process.platform}-${process.arch}\n`)
await build({
// Record entry before imports without relying on either process's stdout.
banner: {
js: `
function traceLauncherPhase(phase) {
const runtime = process.versions.bun ? 'bun' : 'node'
try {
require('node:fs').appendFileSync(process.env.ORCA_TEST_PHASES + '.' + runtime,
JSON.stringify({ at: Date.now(), phase, pid: process.pid, runtime, arch: process.arch }) + '\\n')
} catch (error) {
console.error('launcher fixture trace failed:', error)
}
}
traceLauncherPhase('entry')
process.once('exit', code => traceLauncherPhase('exit:' + code))
`
},
stdin: {
contents: `
import {handoffToBundledOrcad, OrcadBundledRuntimeError} from './src/main/orcad/orcad-bundled-runtime'
import {installOrcadShutdownSignals} from './src/main/orcad/orcad-lifecycle'
import {resolveOrcadExitCode} from './src/main/orcad/orcad-exit-code'
import {writeFile} from 'node:fs/promises'
if (!process.versions.bun) {
traceLauncherPhase('before-handoff')
if (!handoffToBundledOrcad()) throw new Error('Missing bundled runtime')
traceLauncherPhase('after-handoff')
process.on('message', signal => process.emit(signal))
} else {
traceLauncherPhase('before-booting')
{
console.log('booting:' + process.pid)
console.log('runtime:' + process.versions.bun)
console.log('channel-env:' + (process.env.ORCA_BUNDLED_LAUNCHER_CHANNEL ?? 'absent'))
traceLauncherPhase('booting-written')
process.on('exit', code => console.log('runtime-exit:' + code))
const keepalive = setInterval(() => {}, 1_000)
const install = async () => {
if (process.env.ORCA_TEST_FAIL_STARTUP === '1') {
const startup = new Promise((_, reject) => setTimeout(() =>
reject(new OrcadBundledRuntimeError('startup configuration failed')), 100))
installOrcadShutdownSignals(async () => (await startup).stop())
await startup
return
}
installOrcadShutdownSignals(async () => {
console.log('flushing')
clearInterval(keepalive)
@@ -75,11 +40,7 @@ describe.skipIf(!existsSync(runtimePath))('real Bun launcher lifecycle', () => {
}, process.env.ORCA_TEST_STALL === '1' ? 100 : undefined)
console.log('ready')
}
// Exercise the shutdown observer before the outer startup-failure reporter.
const start = () => Promise.resolve().then(install)
.catch(error => setImmediate(() => process.exit(resolveOrcadExitCode(error))))
if (process.env.ORCA_TEST_DELAY_INSTALL === '1') setTimeout(start, 300)
else start()
void install()
}
`,
resolveDir: process.cwd(),
@@ -88,7 +49,7 @@ describe.skipIf(!existsSync(runtimePath))('real Bun launcher lifecycle', () => {
outfile: join(directory, 'orcad.js'),
bundle: true,
platform: 'node',
target: 'node18',
target: 'es2024',
format: 'cjs'
})
})
@@ -109,23 +70,11 @@ describe.skipIf(!existsSync(runtimePath))('real Bun launcher lifecycle', () => {
function launch(
options: {
direct?: boolean
nohup?: boolean
delay?: boolean
stall?: boolean
failStartup?: boolean
} = {}
) {
const runtime = options.direct
? join(directory, orcadBunRuntimeFilename(process.platform))
: nodePath
const startedAt = Date.now()
const events: { at: number; event: string }[] = []
const record = (event: string): void => {
if (events.length < 16) {
events.push({ at: Date.now(), event })
}
}
const runtime = join(directory, orcadBunRuntimeFilename(process.platform))
const child = spawnProcess({
program: options.nohup ? 'nohup' : runtime,
args: [...(options.nohup ? [runtime] : []), join(directory, 'orcad.js')],
@@ -133,19 +82,14 @@ describe.skipIf(!existsSync(runtimePath))('real Bun launcher lifecycle', () => {
...process.env,
ORCA_BACKGROUND_LAUNCH: '1',
ORCA_TEST_DONE: join(directory, 'done'),
ORCA_TEST_PHASES: join(directory, 'phases'),
ORCA_TEST_DELAY_INSTALL: options.delay ? '1' : '0',
ORCA_TEST_STALL: options.stall ? '1' : '0',
ORCA_TEST_FAIL_STARTUP: options.failStartup ? '1' : '0'
ORCA_TEST_STALL: options.stall ? '1' : '0'
},
detached: process.platform !== 'win32',
stdio: ['ignore', 'pipe', 'pipe', 'ipc']
})
children.add(child)
child.once('spawn', () => record('spawn'))
let closed = false
child.once('close', () => {
record('close')
closed = true
})
let output = ''
@@ -162,137 +106,62 @@ describe.skipIf(!existsSync(runtimePath))('real Bun launcher lifecycle', () => {
['stdout', child.stdout],
['stderr', child.stderr]
] as const
for (const [name, stream] of streams) {
for (const [, stream] of streams) {
stream.on('data', capture)
stream.once('data', () => record(`${name}:data`))
stream.once('end', () => record(`${name}:end`))
stream.once('close', () => record(`${name}:close`))
}
const exit = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
(resolve, reject) => {
child.once('error', (error) => {
record(`error:${error.message}`)
reject(error)
})
child.once('exit', (code, signal) => {
record(`exit:${code}:${signal}`)
children.delete(child)
resolve({ code, signal })
})
}
)
const diagnostics = () => ({
startedAt,
elapsedMs: Date.now() - startedAt,
events,
pid: child.pid,
exitCode: child.exitCode,
signalCode: child.signalCode,
connected: child.connected,
closed,
streams: streams.map(([name, stream]) => ({
name,
ended: stream.readableEnded,
destroyed: stream.destroyed,
error: stream.errored?.message,
bufferedBytes: stream.readableLength
})),
phases: ['node', 'bun'].map((runtime) => {
try {
return {
runtime,
trace: readFileSync(join(directory, `phases.${runtime}`), 'utf8').slice(0, 4096)
}
} catch (error) {
return { runtime, error: String(error) }
return { child, output: () => output, exit, isClosed: () => closed }
}
it('drains through supervisor IPC without force-killing the runtime', async () => {
const h = launch()
const ready = new Promise<void>((resolve) => {
h.child.on('message', (message) => {
if (message === SERVE_STOP_READY) {
resolve()
}
})
})
return { child, output: () => output, exit, isClosed: () => closed, diagnostics }
}
await ready
h.child.send(SERVE_STOP_REQUEST)
h.child.send(SERVE_STOP_REQUEST)
expect(await h.exit).toEqual({ code: 0, signal: null })
expect(await readFile(join(directory, 'done'), 'utf8')).toBe('flushed')
expect(h.output().match(/flushing/g)).toHaveLength(1)
})
it.each([false, true])(
'drains Bun after its launcher is killed (startup pending: %s)',
async (delay) => {
const h = launch({ delay })
await vi.waitFor(
() => {
expect(h.output()).toContain(delay ? 'booting:' : 'ready')
expect(h.output()).toContain(`runtime:${ORCAD_BUN_VERSION}`)
expect(h.output()).toContain('channel-env:absent')
},
{ timeout: 5_000 }
)
h.child.kill('SIGKILL')
await h.exit
await vi.waitFor(
async () => expect(await readFile(join(directory, 'done'), 'utf8')).toBe('flushed'),
{ timeout: 5_000 }
)
expect(h.output().match(/flushing/g)).toHaveLength(1)
await vi.waitFor(() => expect(h.output()).toContain('runtime-exit:0'))
await vi.waitFor(() => expect(h.isClosed()).toBe(true), { timeout: 5_000 })
it.skipIf(process.platform === 'win32')('survives nohup hangups and drains on TERM', async () => {
const h = launch({ nohup: true })
await vi.waitFor(() => expect(h.output()).toContain('ready'), { timeout: 5_000 })
if (!h.child.pid) {
throw new Error('Missing launcher pid')
}
)
process.kill(-h.child.pid, 'SIGHUP')
await new Promise((resolve) => setTimeout(resolve, 100))
expect(h.child.exitCode).toBeNull()
expect(h.child.signalCode).toBeNull()
expect(h.output()).not.toContain('flushing')
h.child.kill('SIGTERM')
expect(await h.exit).toEqual({ code: 0, signal: null })
expect(await readFile(join(directory, 'done'), 'utf8')).toBe('flushed')
await vi.waitFor(() => expect(h.isClosed()).toBe(true), { timeout: 5_000 })
})
it.skipIf(process.platform === 'win32').each([false, true])(
'survives nohup hangups and drains on TERM (direct Bun: %s)',
async (direct) => {
const h = launch({ direct, nohup: true })
await vi.waitFor(() => expect(h.output()).toContain('ready'), { timeout: 5_000 })
if (!h.child.pid) {
throw new Error('Missing launcher pid')
}
process.kill(-h.child.pid, 'SIGHUP')
await new Promise((resolve) => setTimeout(resolve, 100))
expect(h.child.exitCode).toBeNull()
expect(h.child.signalCode).toBeNull()
expect(h.output()).not.toContain('flushing')
h.child.kill('SIGTERM')
expect(await h.exit).toEqual({ code: 0, signal: null })
expect(await readFile(join(directory, 'done'), 'utf8')).toBe('flushed')
await vi.waitFor(() => expect(h.isClosed()).toBe(true), { timeout: 5_000 })
}
)
it('keeps an unfinished shutdown alive until its failure deadline', async () => {
it.skipIf(process.platform === 'win32')('bounds a stalled durable shutdown', async () => {
const h = launch({ stall: true })
await vi.waitFor(() => expect(h.output()).toContain('ready'), { timeout: 5_000 })
h.child.kill('SIGKILL')
await h.exit
await vi.waitFor(() => expect(h.output()).toContain('runtime-exit:1'))
h.child.kill('SIGTERM')
expect(await h.exit).toEqual({ code: 1, signal: null })
expect(h.output()).toContain('exceeded 100ms')
await vi.waitFor(() => expect(h.isClosed()).toBe(true), { timeout: 5_000 })
})
it.each(process.platform === 'win32' ? [false, true] : [false])(
'forwards launcher stop requests and drains once (startup pending: %s)',
async (delay) => {
const h = launch({ delay })
await vi.waitFor(() => expect(h.output()).toContain(delay ? 'booting:' : 'ready'), {
timeout: 5_000
})
h.child.send('SIGINT')
h.child.send('SIGTERM')
expect(await h.exit).toEqual({ code: 0, signal: null })
expect(await readFile(join(directory, 'done'), 'utf8')).toBe('flushed')
expect(h.output().match(/flushing/g)).toHaveLength(1)
await vi.waitFor(() => expect(h.isClosed()).toBe(true), { timeout: 5_000 })
}
)
it('preserves a startup configuration verdict after early launcher loss', async () => {
const h = launch({ delay: true, failStartup: true })
try {
await vi.waitFor(() => expect(h.output()).toContain('booting:'), { timeout: 5_000 })
} catch (error) {
console.error('Bun launcher startup diagnostics:', JSON.stringify(h.diagnostics()))
throw error
}
h.child.kill('SIGKILL')
await h.exit
await vi.waitFor(() => expect(h.output()).toContain('runtime-exit:78'), { timeout: 5_000 })
expect(h.output()).toContain('shutdown after launcher disconnect failed')
await vi.waitFor(() => expect(h.isClosed()).toBe(true), { timeout: 5_000 })
})
})
@@ -1,12 +1,14 @@
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, expect, it } from 'vitest'
import { orcadBunRuntimeFilename } from '../../shared/orcad-artifacts'
import { runProcessSync } from '../../shared/child-process/run-process'
const REPO_ROOT = join(__dirname, '..', '..', '..')
const directories: string[] = []
const bunRuntime = join(REPO_ROOT, 'out/orcad', orcadBunRuntimeFilename(process.platform))
afterEach(() => {
for (const directory of directories.splice(0)) {
@@ -14,57 +16,53 @@ afterEach(() => {
}
})
it('loads a fresh production import graph before requiring native PTY code', () => {
const directory = mkdtempSync(join(tmpdir(), 'orcad-load-order-'))
directories.push(directory)
const bundle = join(directory, 'orcad.js')
const builder = pathToFileURL(join(REPO_ROOT, 'config/scripts/orcad-entry-build.mjs')).href
const built = runProcessSync({
program: process.execPath,
args: [
'--input-type=module',
'-e',
`import { buildOrcadEntry } from ${JSON.stringify(builder)}; await buildOrcadEntry(${JSON.stringify(bundle)})`
],
cwd: REPO_ROOT,
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
timeoutMs: 60_000
})
expect(built.code, built.stderr.slice(0, 2_000)).toBe(0)
expect(existsSync(bundle)).toBe(true)
const marker = join(directory, 'premature-native-load')
const preload = join(directory, 'preload.cjs')
writeFileSync(
preload,
[
"const Module = require('node:module')",
'const original = Module._load',
'Module._load = function (request, ...rest) {',
" if (request === 'node-pty') {",
` require('node:fs').writeFileSync(${JSON.stringify(marker)}, request)`,
" throw new Error('native PTY required before preflight')",
' }',
' return original.call(this, request, ...rest)',
'}'
].join('\n')
)
const run = (extraArgs: string[] = []) =>
runProcessSync({
it.skipIf(!existsSync(bunRuntime))(
'loads a fresh production import graph before requiring native PTY code',
() => {
const directory = mkdtempSync(join(tmpdir(), 'orcad-load-order-'))
directories.push(directory)
const bundle = join(directory, 'orcad.js')
const builder = pathToFileURL(join(REPO_ROOT, 'config/scripts/orcad-entry-build.mjs')).href
const built = runProcessSync({
program: process.execPath,
// The production load-check exits after module evaluation, before runtime handoff or probes.
args: ['--require', preload, ...extraArgs, bundle, '--orcad-smoke-load-check'],
args: [
'--input-type=module',
'-e',
`import { buildOrcadEntry } from ${JSON.stringify(builder)}; await buildOrcadEntry(${JSON.stringify(bundle)})`
],
cwd: REPO_ROOT,
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
timeoutMs: 30_000
timeoutMs: 60_000
})
expect(built.code, built.stderr.slice(0, 2_000)).toBe(0)
expect(existsSync(bundle)).toBe(true)
const loaded = run()
expect(loaded.code, loaded.stderr.slice(0, 2_000)).toBe(0)
expect(existsSync(marker)).toBe(false)
const marker = join(directory, 'premature-native-load')
const nativeDirectory = join(directory, 'node_modules', 'node-pty')
mkdirSync(nativeDirectory, { recursive: true })
writeFileSync(
join(nativeDirectory, 'index.js'),
`require('node:fs').writeFileSync(${JSON.stringify(marker)}, 'node-pty');
` + `throw new Error('native PTY required before preflight')`
)
const run = (extraArgs: string[] = []) =>
runProcessSync({
program: bunRuntime,
// The application load-check exits after module evaluation, before probes.
args: [...extraArgs, bundle, '--orcad-smoke-load-check'],
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
timeoutMs: 30_000
})
// Prove the interception works without relying on minified source echoed in an error.
const eagerNative = join(directory, 'eager-native.cjs')
writeFileSync(eagerNative, "require('node-pty')")
expect(run(['--require', eagerNative]).code).not.toBe(0)
expect(existsSync(marker)).toBe(true)
}, 90_000)
const loaded = run()
expect(loaded.code, loaded.stderr.slice(0, 2_000)).toBe(0)
expect(existsSync(marker)).toBe(false)
// Prove the interception works without relying on minified source echoed in an error.
const eagerNative = join(directory, 'eager-native.cjs')
writeFileSync(eagerNative, "require('node-pty')")
expect(run(['--require', eagerNative]).code).not.toBe(0)
expect(existsSync(marker)).toBe(true)
},
90_000
)
@@ -1,221 +0,0 @@
import { build } from 'esbuild'
import { chmod, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { runProcess, spawnProcess } from '../../shared/child-process/run-process'
import { shellEscape } from '../ssh/ssh-connection-utils'
let directory = ''
const children = new Set<ReturnType<typeof spawnProcess>>()
beforeEach(async () => {
directory = await mkdtemp(join(tmpdir(), 'orca-handoff-'))
await build({
stdin: {
contents: `
import { handoffToBundledOrcad, OrcadBundledRuntimeError } from './src/main/orcad/orcad-bundled-runtime'
import { installOrcadShutdownSignals, flushOrcadProfileStoreForShutdown } from './src/main/orcad/orcad-lifecycle'
import { writeFile } from 'node:fs/promises'
if (process.env.ORCA_TEST_HANDOFF_CHILD === '1') {
if (process.env.ORCA_TEST_HANDOFF_DURABLE === '1') {
installOrcadShutdownSignals(() => flushOrcadProfileStoreForShutdown({
flushFinalOrThrowAsync: async () => {
console.log('flushing')
await new Promise(resolve => setTimeout(resolve, 250))
await writeFile(process.env.ORCA_TEST_SHUTDOWN_FILE, 'flushed')
},
freezeWritesAsync: async () => console.log('closed')
}))
}
for (const signal of ['SIGINT', 'SIGTERM']) {
process.on(signal, () => {
console.log('received:' + signal)
if (process.env.ORCA_TEST_HANDOFF_DURABLE !== '1') process.exit(29)
})
}
console.log('ready:' + JSON.stringify(process.argv.slice(2)))
console.log('child-pid:' + process.pid)
setTimeout(() => process.exit(99), 4_000)
} else {
try {
if (!handoffToBundledOrcad()) throw new Error('handoff failed')
} catch (error) {
console.error(error.message)
process.exit(error instanceof OrcadBundledRuntimeError ? 78 : 1)
}
}
`,
resolveDir: process.cwd(),
loader: 'ts'
},
outfile: join(directory, 'orcad.js'),
bundle: true,
platform: 'node',
target: 'node18',
format: 'cjs'
})
await writeFile(join(directory, '.build-target'), 'darwin-arm64\n')
const runtime = join(directory, 'bun-runtime')
await writeFile(
runtime,
`#!/bin/sh\nORCA_TEST_HANDOFF_CHILD=1 exec ${shellEscape(process.execPath)} "$@"\n`
)
await chmod(runtime, 0o700)
})
afterEach(async () => {
for (const child of children) {
child.kill('SIGKILL')
}
children.clear()
await rm(directory, { recursive: true, force: true })
})
function launch(
args: string[],
env: NodeJS.ProcessEnv = {},
options: { entry?: string; nohup?: boolean } = {}
) {
const child = spawnProcess({
program: options.nohup ? 'nohup' : process.execPath,
args: [
...(options.nohup ? [process.execPath] : []),
options.entry ?? join(directory, 'orcad.js'),
...args
],
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1', ...env },
stdio: ['ignore', 'pipe', 'pipe'],
detached: true
})
children.add(child)
let output = ''
child.stdout.on('data', (data: Buffer) => {
output += data.toString()
})
child.stderr.on('data', (data: Buffer) => {
output += data.toString()
})
const exit = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
(resolve, reject) => {
child.once('error', reject)
child.once('exit', (code, signal) => {
children.delete(child)
resolve({ code, signal })
})
}
)
return { child, output: () => output, exit }
}
describe.skipIf(process.platform === 'win32')('bundled handoff process lifecycle', () => {
it('refuses a partial installation before launching its adjacent runtime', async () => {
await rm(join(directory, '.build-target'))
const result = await runProcess({
program: process.execPath,
args: [join(directory, 'orcad.js')],
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
timeoutMs: 5_000
})
expect(result.code).toBe(78)
expect(result.stderr).toContain('bundled Orca runtime target is missing')
expect(result.stdout).not.toContain('ready:')
})
it.each(['SIGINT', 'SIGTERM'] as const)(
'forwards %s to the actual child and mirrors its exit',
async (signal) => {
const args = ['--label', 'two words', 'quote"$literal']
const { child, output, exit } = launch(args)
await vi.waitFor(() => expect(output()).toContain(`ready:${JSON.stringify(args)}`), {
timeout: 2_000
})
child.kill(signal)
expect(await exit).toEqual({ code: 29, signal: null })
expect(output()).toContain(`received:${signal}`)
}
)
it.each(
(['SIGINT', 'SIGTERM'] as const).flatMap((signal) =>
(['process group', 'separate service deliveries'] as const).map((delivery) => ({
signal,
delivery
}))
)
)(
'finishes a pending durable flush after duplicate $signal from $delivery',
async ({ signal, delivery }) => {
const shutdownFile = join(directory, 'shutdown-complete')
const { child, output, exit } = launch([], {
ORCA_TEST_HANDOFF_DURABLE: '1',
ORCA_TEST_SHUTDOWN_FILE: shutdownFile
})
await vi.waitFor(() => expect(output()).toContain('child-pid:'), { timeout: 2_000 })
const runtimePid = Number(output().match(/child-pid:(\d+)/)?.[1])
expect(runtimePid).toBeGreaterThan(0)
if (!child.pid) {
throw new Error('Launcher has no process ID')
}
if (delivery === 'process group') {
process.kill(-child.pid, signal)
} else {
process.kill(runtimePid, signal)
await vi.waitFor(() => expect(output()).toContain('flushing'))
child.kill(signal)
}
expect(await exit).toEqual({ code: 0, signal: null })
expect(await readFile(shutdownFile, 'utf8')).toBe('flushed')
expect(output().match(/flushing/g)).toHaveLength(1)
expect(output()).toContain('closed')
if (delivery === 'separate service deliveries') {
expect(output().match(new RegExp(`received:${signal}`, 'g'))).toHaveLength(2)
}
}
)
it('hands off a symlinked entry to its adjacent runtime', async () => {
const aliases = join(directory, 'aliases')
await mkdir(aliases)
const entry = join(aliases, 'orcad.js')
await symlink(join(directory, 'orcad.js'), entry)
const { child, output, exit } = launch([], {}, { entry })
await vi.waitFor(() => expect(output()).toContain('child-pid:'), { timeout: 2_000 })
child.kill('SIGTERM')
expect(await exit).toEqual({ code: 29, signal: null })
})
it('drains the child after its launcher is force-killed', async () => {
const shutdownFile = join(directory, 'shutdown-complete')
const { child, output, exit } = launch([], {
ORCA_TEST_HANDOFF_DURABLE: '1',
ORCA_TEST_SHUTDOWN_FILE: shutdownFile
})
await vi.waitFor(() => expect(output()).toContain('child-pid:'), { timeout: 2_000 })
child.kill('SIGKILL')
expect(await exit).toEqual({ code: null, signal: 'SIGKILL' })
await vi.waitFor(async () => expect(await readFile(shutdownFile, 'utf8')).toBe('flushed'))
expect(output().match(/flushing/g)).toHaveLength(1)
})
it('preserves nohup across a terminal hangup and still stops gracefully on SIGTERM', async () => {
const shutdownFile = join(directory, 'shutdown-complete')
const { child, output, exit } = launch(
[],
{ ORCA_TEST_HANDOFF_DURABLE: '1', ORCA_TEST_SHUTDOWN_FILE: shutdownFile },
{ nohup: true }
)
await vi.waitFor(() => expect(output()).toContain('child-pid:'), { timeout: 2_000 })
if (!child.pid) {
throw new Error('Launcher has no process ID')
}
process.kill(-child.pid, 'SIGHUP')
await new Promise((resolve) => setTimeout(resolve, 100))
expect(child.exitCode).toBeNull()
expect(child.signalCode).toBeNull()
expect(output()).not.toContain('flushing')
child.kill('SIGTERM')
expect(await exit).toEqual({ code: 0, signal: null })
expect(await readFile(shutdownFile, 'utf8')).toBe('flushed')
})
})
+20 -109
View File
@@ -1,6 +1,5 @@
import { EventEmitter } from 'node:events'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { handoffToBundledOrcad } from './orcad-bundled-runtime'
import { assertBundledOrcadRuntime } from './orcad-bundled-runtime'
import { ORCAD_BUN_VERSION } from '../../shared/orcad-bun-runtime'
import { ORCAD_VERSION_FILENAME } from '../../shared/orcad-artifacts'
@@ -12,22 +11,13 @@ const fixture = vi.hoisted(() => ({
vi.mock('node:fs', () => ({ existsSync: fixture.exists, realpathSync: fixture.realpath }))
vi.mock('../../shared/child-process/run-process', () => ({ spawnProcess: fixture.spawn }))
class RuntimeChild extends EventEmitter {
kill = vi.fn()
disconnect = vi.fn()
connected = true
}
let child: RuntimeChild
const signalNames = ['SIGINT', 'SIGTERM', 'SIGHUP'] as const
let oldListeners: Map<NodeJS.Signals, ReturnType<typeof process.rawListeners>>
beforeEach(() => {
oldListeners = new Map(signalNames.map((signal) => [signal, process.rawListeners(signal)]))
child = new RuntimeChild()
vi.spyOn(process, 'versions', 'get').mockReturnValue({
...process.versions,
bun: ORCAD_BUN_VERSION
})
fixture.exists.mockReturnValue(true)
fixture.realpath.mockImplementation((path) => path)
fixture.spawn.mockReturnValue(child)
vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('test process exit')
})
@@ -37,39 +27,32 @@ beforeEach(() => {
})
afterEach(() => {
for (const signal of signalNames) {
for (const listener of process.rawListeners(signal)) {
if (!oldListeners.get(signal)?.includes(listener)) {
process.off(signal, listener)
}
}
}
vi.restoreAllMocks()
vi.clearAllMocks()
})
describe('bundled Orca runtime handoff', () => {
describe('bundled Orca runtime validation', () => {
it('leaves nonpackaged entries on their existing runtime', () => {
fixture.exists.mockReturnValue(false)
expect(handoffToBundledOrcad()).toBe(false)
expect(assertBundledOrcadRuntime()).toBeUndefined()
expect(fixture.spawn).not.toHaveBeenCalled()
})
it('refuses an incomplete slot before starting a process', () => {
fixture.exists.mockImplementation((path) => path.endsWith('.build-target'))
expect(() => handoffToBundledOrcad()).toThrow('bundled Orca runtime is missing')
expect(() => assertBundledOrcadRuntime()).toThrow('bundled Orca runtime is missing')
expect(fixture.spawn).not.toHaveBeenCalled()
})
it('refuses a versioned slot missing both its runtime and target marker', () => {
fixture.exists.mockImplementation((path) => path.endsWith(ORCAD_VERSION_FILENAME))
expect(() => handoffToBundledOrcad()).toThrow('bundled Orca runtime target is missing')
expect(() => assertBundledOrcadRuntime()).toThrow('bundled Orca runtime target is missing')
expect(fixture.spawn).not.toHaveBeenCalled()
})
it('refuses a remaining bundled runtime without its target marker', () => {
fixture.exists.mockImplementation((path) => !path.endsWith('.build-target'))
expect(() => handoffToBundledOrcad()).toThrow('bundled Orca runtime target is missing')
expect(() => assertBundledOrcadRuntime()).toThrow('bundled Orca runtime target is missing')
expect(fixture.realpath).toHaveBeenCalledExactlyOnceWith('/slot/orcad.js')
expect(fixture.spawn).not.toHaveBeenCalled()
})
@@ -80,97 +63,25 @@ describe('bundled Orca runtime handoff', () => {
...process.versions,
bun: ORCAD_BUN_VERSION
})
expect(handoffToBundledOrcad()).toBe(false)
expect(assertBundledOrcadRuntime()).toBeUndefined()
expect(fixture.spawn).not.toHaveBeenCalled()
})
it('refuses an adjacent runtime that reports the wrong Bun version', () => {
fixture.realpath.mockReturnValue('/real/runtime')
vi.spyOn(process, 'versions', 'get').mockReturnValue({ ...process.versions, bun: '0.0.0' })
expect(() => handoffToBundledOrcad()).toThrow(`must be Bun ${ORCAD_BUN_VERSION}`)
expect(() => assertBundledOrcadRuntime()).toThrow(`must be Bun ${ORCAD_BUN_VERSION}`)
})
it.each(['linux', 'darwin', 'win32'] as const)(
'hands off arguments and respects %s signal delivery',
(platform) => {
vi.spyOn(process, 'platform', 'get').mockReturnValue(platform)
expect(handoffToBundledOrcad()).toBe(true)
expect(fixture.spawn).toHaveBeenCalledWith({
program: expect.stringMatching(/bun-runtime(?:\.exe)?$/),
args: ['/slot/orcad.js', '--port', '0'],
env: expect.objectContaining({ ORCA_BUNDLED_LAUNCHER_CHANNEL: '1' }),
detached: true,
stdio: ['inherit', 'inherit', 'inherit', 'ipc']
})
for (const signal of signalNames) {
const listener = process
.rawListeners(signal)
.find((candidate) => !oldListeners.get(signal)?.includes(candidate))
if (signal === 'SIGHUP' && platform === 'win32') {
expect(listener).toBeUndefined()
continue
}
expect(listener).toBeDefined()
if (listener) {
listener.call(process, signal)
}
if (signal === 'SIGHUP') {
expect(child.kill).not.toHaveBeenCalledWith('SIGHUP')
} else if (platform === 'win32') {
expect(child.kill).not.toHaveBeenCalled()
expect(child.disconnect).toHaveBeenCalled()
} else {
expect(child.kill).toHaveBeenLastCalledWith(signal)
}
}
}
)
it('propagates a child exit code and removes every signal listener', () => {
handoffToBundledOrcad()
expect(() => child.emit('exit', 23, null)).toThrow('test process exit')
expect(process.exit).toHaveBeenCalledWith(23)
expect(process.kill).not.toHaveBeenCalled()
for (const signal of signalNames) {
expect(process.rawListeners(signal)).toEqual(oldListeners.get(signal))
}
it('rejects Node before inspecting or opening the application', () => {
vi.spyOn(process, 'versions', 'get').mockReturnValue({ ...process.versions, bun: undefined })
expect(() => assertBundledOrcadRuntime()).toThrow('not Node')
expect(fixture.exists).not.toHaveBeenCalled()
expect(fixture.spawn).not.toHaveBeenCalled()
})
it('locates the runtime beside the resolved entry rather than its symlink', () => {
fixture.realpath.mockImplementation((path) =>
path === '/slot/orcad.js' ? '/real/slot/orcad.js' : path
)
handoffToBundledOrcad()
expect(fixture.spawn).toHaveBeenCalledWith(
expect.objectContaining({
program: expect.stringMatching(/real\/slot\/bun-runtime(?:\.exe)?$/),
args: ['/real/slot/orcad.js', '--port', '0']
})
)
})
it('reports failed spawn as a configuration failure and removes listeners', () => {
handoffToBundledOrcad()
expect(() => child.emit('error', new Error('ENOENT'))).toThrow('test process exit')
expect(process.exit).toHaveBeenCalledWith(78)
for (const signal of signalNames) {
expect(process.rawListeners(signal)).toEqual(oldListeners.get(signal))
}
})
it('mirrors a POSIX signal exit without exiting before the signal is delivered', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('linux')
handoffToBundledOrcad()
child.emit('exit', null, 'SIGTERM')
expect(process.kill).toHaveBeenCalledWith(process.pid, 'SIGTERM')
expect(process.exit).not.toHaveBeenCalled()
})
it('preserves a signal exit without sending unsupported signals on Windows', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
handoffToBundledOrcad()
expect(() => child.emit('exit', null, 'SIGTERM')).toThrow('test process exit')
expect(process.exit).toHaveBeenCalledWith(143)
expect(process.kill).not.toHaveBeenCalled()
it('rejects an external Bun executable instead of handing off', () => {
expect(() => assertBundledOrcadRuntime()).toThrow('Start orcad with its bundled runtime')
expect(fixture.spawn).not.toHaveBeenCalled()
})
})
+9 -55
View File
@@ -1,7 +1,5 @@
import { existsSync, realpathSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { constants } from 'node:os'
import { spawnProcess } from '../../shared/child-process/run-process'
import {
ORCAD_BUILD_TARGET_FILENAME,
ORCAD_VERSION_FILENAME,
@@ -10,13 +8,15 @@ import {
import { ORCAD_BUN_VERSION } from '../../shared/orcad-bun-runtime'
export class OrcadBundledRuntimeError extends Error {}
export const ORCAD_BUNDLED_LAUNCHER_ENV = 'ORCA_BUNDLED_LAUNCHER_CHANNEL'
/** Keep old Node service commands usable without letting Node open the profile. */
export function handoffToBundledOrcad(): boolean {
/** Reject the wrong runtime before application imports can touch a profile. */
export function assertBundledOrcadRuntime(): void {
if (!process.versions.bun) {
throw new OrcadBundledRuntimeError('Start orcad with its bundled bun-runtime, not Node')
}
const script = process.argv[1]
if (!script) {
return false
return
}
const entry = realpathSync(script)
const directory = dirname(entry)
@@ -24,7 +24,7 @@ export function handoffToBundledOrcad(): boolean {
const hasTarget = existsSync(join(directory, ORCAD_BUILD_TARGET_FILENAME))
const hasRuntime = existsSync(runtime)
if (!hasTarget && !hasRuntime && !existsSync(join(directory, ORCAD_VERSION_FILENAME))) {
return false
return
}
if (!hasTarget) {
throw new OrcadBundledRuntimeError('The bundled Orca runtime target is missing')
@@ -38,53 +38,7 @@ export function handoffToBundledOrcad(): boolean {
`The bundled Orca runtime must be Bun ${ORCAD_BUN_VERSION}`
)
}
return false
return
}
const child = spawnProcess({
program: runtime,
args: [entry, ...process.argv.slice(2)],
env: { ...process.env, [ORCAD_BUNDLED_LAUNCHER_ENV]: '1' },
// Windows' default child job kills Bun before it can drain on launcher disconnect.
detached: true,
stdio: ['inherit', 'inherit', 'inherit', 'ipc']
})
// Node resets nohup's disposition; headless runtimes stop through INT/TERM or owner loss.
const ignoreHangup = (): void => {}
if (process.platform !== 'win32') {
process.on('SIGHUP', ignoreHangup)
}
const forwards = (['SIGINT', 'SIGTERM'] as const).map((signal) => {
const forward = (): void => {
if (process.platform === 'win32') {
// Detached Windows children have a separate console; kill() skips durable shutdown.
if (child.connected) {
child.disconnect()
}
} else {
child.kill(signal)
}
}
process.on(signal, forward)
return { signal, forward }
})
const cleanup = (): void => {
process.off('SIGHUP', ignoreHangup)
for (const { signal, forward } of forwards) {
process.off(signal, forward)
}
}
child.once('error', (error) => {
cleanup()
console.error('orcad: could not start the bundled runtime:', error.message)
process.exit(78)
})
child.once('exit', (code, signal) => {
cleanup()
if (signal && process.platform !== 'win32') {
process.kill(process.pid, signal)
return
}
process.exit(code ?? (signal ? 128 + constants.signals[signal] : 1))
})
return true
throw new OrcadBundledRuntimeError(`Start orcad with its bundled runtime: ${runtime}`)
}
+3 -11
View File
@@ -1,16 +1,13 @@
import { registerServeSupervisorControl } from '../../shared/serve-supervisor-control'
import { setRuntimeBrowserCommandsFactory } from '../runtime/runtime-browser-commands-factory'
import { resolveOrcadBrowserProvider } from './orcad-browser-provider'
import { acquireOrcadInstanceLock } from './orcad-instance-lock'
import { ORCAD_BUNDLED_LAUNCHER_ENV } from './orcad-bundled-runtime'
import { resolveOrcadExitCode } from './orcad-exit-code'
import {
acquireProfileStateRuntimeAdmission,
type ProfileStateRuntimeAdmission
} from '../persistence/profile-state/profile-state-access'
const bundledLauncherChannel = process.env[ORCAD_BUNDLED_LAUNCHER_ENV] === '1'
delete process.env[ORCAD_BUNDLED_LAUNCHER_ENV]
function createIdempotentOrcadCleanup(cleanup: () => Promise<void>): () => Promise<void> {
let completion: Promise<void> | null = null
return () => {
@@ -21,7 +18,7 @@ function createIdempotentOrcadCleanup(cleanup: () => Promise<void>): () => Promi
export const ORCAD_SHUTDOWN_DEADLINE_MS = 15_000
/** A launcher and its child can both receive the same process-group or service stop signal. */
/** Repeated service stop signals must not interrupt the durable shutdown. */
export function installOrcadShutdownSignals(
stop: () => Promise<void>,
deadlineMs = ORCAD_SHUTDOWN_DEADLINE_MS
@@ -45,16 +42,11 @@ export function installOrcadShutdownSignals(
}
process.on('SIGINT', () => shutdown('SIGINT'))
process.on('SIGTERM', () => shutdown('SIGTERM'))
registerServeSupervisorControl(process, () => shutdown('supervisor'))
// Headless runtimes survive terminal hangups; INT/TERM are the graceful stop contract.
if (process.platform !== 'win32') {
process.on('SIGHUP', () => {})
}
if (bundledLauncherChannel && typeof process.send === 'function') {
process.once('disconnect', () => shutdown('launcher disconnect'))
if (!process.connected) {
shutdown('launcher disconnect')
}
}
}
export async function startOrcadWithLifecycle<T extends object>(
+1
View File
@@ -66,6 +66,7 @@ export type OrcadArtifact = {
export const ORCAD_ARTIFACTS: readonly OrcadArtifact[] = [
{ filename: 'orcad.js' },
{ filename: 'orcad-app.js' },
// Forked so a native @parcel/watcher fault kills the child, not the server.
{ filename: 'parcel-watcher-process-entry.js' },
// Forked so PTYs outlive the runtime process; its absence makes every restart destructive.
+65
View File
@@ -0,0 +1,65 @@
export const SERVE_STOP_READY = 'orca:serve-stop-ready'
export const SERVE_STOP_REQUEST = 'orca:serve-stop'
export type ServeSupervisorChannel = {
on(event: 'message', listener: (message: unknown) => void): unknown
send?: (message: string, callback: (error: Error | null) => void) => boolean
}
export function registerServeSupervisorControl(
channel: ServeSupervisorChannel,
stop: () => void
): void {
channel.on('message', (message) => {
if (message === SERVE_STOP_REQUEST) {
stop()
}
})
try {
channel.send?.(SERVE_STOP_READY, () => undefined)
} catch {
// Console signals remain available if the supervisor has disconnected.
}
}
type ServeControlChild = {
readonly connected: boolean
send(message: string, callback: (error: Error | null) => void): boolean
}
/** Keep early shutdown requests until the child installs graceful quit handling. */
export function createServeStopRequest(child: ServeControlChild): {
request(): void
handleMessage(value: unknown): void
} {
let ready = false
let pending = false
const reportError = (error: unknown): void => {
if (error) {
console.error('[serve] could not request graceful shutdown:', error)
}
}
const send = (): void => {
if (!ready || !pending || !child.connected) {
return
}
pending = false
try {
child.send(SERVE_STOP_REQUEST, reportError)
} catch (error) {
reportError(error)
}
}
return {
request() {
pending = true
send()
},
handleMessage(value) {
if (value === SERVE_STOP_READY) {
ready = true
send()
}
}
}
}