Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions config/scripts/build-orcad.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#!/usr/bin/env node
// Package orcad for the pinned Node; keep module loading compatible with legacy Node launchers.
// Package the server separately from its compatibility launcher.
import { fork, spawnSync } from 'node:child_process'
import { build } from 'esbuild'
import {
buildOrcadEntry,
buildOrcadLauncher,
externalNativeAddons,
ORCAD_EXTERNAL_MODULES,
ORCAD_CHILD_ENTRY_POINTS
Expand All @@ -30,6 +31,8 @@ import {
ORCAD_EMOJI_SHORTCODE_DATASET,
ORCAD_FOREIGN_SQLITE_READER_ENTRY,
ORCAD_PORT_SCAN_COMMAND_WORKER_ENTRY,
ORCAD_LAUNCHER_FILENAME,
ORCAD_SERVER_ENTRY_FILENAME,
ORCAD_NODE_PTY_DIR,
ORCAD_NODE_PTY_JS_ARTIFACTS,
ORCAD_NODE_RUNTIME_MARKER_FILENAME,
Expand Down Expand Up @@ -67,7 +70,8 @@ const FOREIGN_SQLITE_READER_OUT_FILE = join(OUT_DIR, ORCAD_FOREIGN_SQLITE_READER
// and `resolveWorkerEntryPath` looks for it next to the running bundle.
const PORT_SCAN_WORKER_ENTRY = join(ROOT, ORCAD_CHILD_ENTRY_POINTS.portScanCommandWorker)
const PORT_SCAN_WORKER_OUT_FILE = join(OUT_DIR, ORCAD_PORT_SCAN_COMMAND_WORKER_ENTRY)
const OUT_FILE = join(OUT_DIR, 'orcad.js')
const OUT_FILE = join(OUT_DIR, ORCAD_LAUNCHER_FILENAME)
const SERVER_OUT_FILE = join(OUT_DIR, ORCAD_SERVER_ENTRY_FILENAME)
const BUILD_TARGET = process.env.ORCAD_BUILD_TARGET
if (!BUILD_TARGET) {
throw new Error('ORCAD_BUILD_TARGET is required; run `pnpm build:orcad`')
Expand Down Expand Up @@ -229,7 +233,8 @@ const childResults = await Promise.all([
)
])

const result = await buildOrcadEntry(OUT_FILE)
const result = await buildOrcadEntry(SERVER_OUT_FILE)
const launcherResult = await buildOrcadLauncher(OUT_FILE)

const output = Object.values(result.metafile.outputs).find(
(o) => o.entryPoint === 'src/main/orcad/main.ts'
Expand All @@ -254,7 +259,11 @@ function collectImporters(metafiles, matches) {
return importers
}

const metafiles = [result.metafile, ...childResults.map((child) => child.metafile)]
const metafiles = [
launcherResult.metafile,
result.metafile,
...childResults.map((child) => child.metafile)
]
const electronImporters = collectImporters(
metafiles,
(specifier) => specifier === 'electron' || specifier.startsWith('electron/')
Expand Down Expand Up @@ -294,7 +303,7 @@ if (graphErrors.length > 0) {
// 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'], {
const smoke = spawnSync(process.execPath, [SERVER_OUT_FILE, '--orcad-smoke-load-check'], {
encoding: 'utf8',
timeout: 60_000
})
Expand Down
2 changes: 2 additions & 0 deletions config/scripts/check-runtime-launcher-protocol-ratchet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { DAEMON_PROTOCOL_SOURCE_PATH, parseDaemonProtocolFacts } from './daemon-
export const RUNTIME_LAUNCHER_PATHS = [
// orcad handoff to its bundled runtime, and the pinned runtimes it can hand off to.
'src/main/orcad/orcad-bundled-runtime.ts',
'src/main/orcad/launcher.ts',
'config/scripts/orcad-entry-build.mjs',
'src/shared/node-runtime-pin.ts',
'src/main/ssh/pinned-runtime-materializer.ts',
'src/main/ssh/runtime-archive-download.ts',
Expand Down
5 changes: 5 additions & 0 deletions config/scripts/headless-detector-compiler-cache.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ it('activates only the actual compiler packages and preserves import graph behav
join(root, 'config', 'scripts', `${name}.mjs`)
)
}
mkdirSync(join(root, 'src', 'shared'), { recursive: true })
cpSync(
new URL('../../src/shared/orcad-artifacts.ts', import.meta.url),
join(root, 'src', 'shared', 'orcad-artifacts.ts')
)
writeFileSync(
join(root, 'entry.ts'),
"import './first'; export * from './exports'; import('./dynamic'); require('./required'); import 'external-package'; import './native.node'"
Expand Down
12 changes: 10 additions & 2 deletions config/scripts/node-server-change-scope.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,18 @@ export function discoverNodeServerTests(root = ROOT) {
}

export async function collectNodeServerInputs({ root = ROOT, entryPoints } = {}) {
const [{ build }, { externalNativeAddons, ORCAD_CHILD_ENTRY_POINTS, ORCAD_ENTRY_POINT }] =
await Promise.all([import('esbuild'), import('./orcad-entry-build.mjs')])
const [
{ build },
{
externalNativeAddons,
ORCAD_CHILD_ENTRY_POINTS,
ORCAD_ENTRY_POINT,
ORCAD_LAUNCHER_ENTRY_POINT
}
] = await Promise.all([import('esbuild'), import('./orcad-entry-build.mjs')])
const entries = entryPoints ?? [
ORCAD_ENTRY_POINT,
ORCAD_LAUNCHER_ENTRY_POINT,
...Object.values(ORCAD_CHILD_ENTRY_POINTS),
...BUILD_SCRIPTS,
...discoverNodeServerTests(root)
Expand Down
1 change: 1 addition & 0 deletions config/scripts/node-server-test-paths.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function nodeServerTestPaths({ artifact = false, crossRuntime = false } =
'src/main/orcad/orcad-packaged-node-pty.integration.test.ts',
'src/main/providers/agent-foreground-process-git-bash.win32.test.ts',
'src/main/orcad/orcad-node-launcher.integration.test.ts',
'src/main/orcad/orcad-launcher-isolation.integration.test.ts',
'src/main/orcad/orcad-stop-request-shutdown.integration.test.ts',
'src/main/orcad/orcad-windows-conpty-breakaway.integration.test.ts',
'src/main/orcad/orcad-serve-parity.integration.test.ts',
Expand Down
25 changes: 24 additions & 1 deletion config/scripts/orcad-entry-build.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { build } from 'esbuild'
import { join } from 'node:path'
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { ORCAD_SERVER_ENTRY_FILENAME } from '../../src/shared/orcad-artifacts.ts'

const root = join(import.meta.dirname, '..', '..')

export const ORCAD_ENTRY_POINT = 'src/main/orcad/main.ts'
export const ORCAD_LAUNCHER_ENTRY_POINT = 'src/main/orcad/launcher.ts'
export const ORCAD_CHILD_ENTRY_POINTS = {
watcher: 'src/main/ipc/parcel-watcher-process-entry.ts',
daemon: 'src/main/daemon/daemon-entry.ts',
Expand Down Expand Up @@ -50,3 +54,22 @@ export function buildOrcadEntry(outfile) {
logLevel: 'error'
})
}

export function buildOrcadLauncher(outfile) {
const serverSha256 = createHash('sha256')
.update(readFileSync(join(dirname(outfile), ORCAD_SERVER_ENTRY_FILENAME)))
.digest('hex')
return build({
entryPoints: [join(root, ORCAD_LAUNCHER_ENTRY_POINT)],
bundle: true,
platform: 'node',
target: 'node18',
format: 'cjs',
outfile,
metafile: true,
minify: true,
sourcemap: false,
define: { ORCAD_SERVER_SHA256: JSON.stringify(serverSha256) },
logLevel: 'error'
})
}
4 changes: 2 additions & 2 deletions config/scripts/orcad-prebuild-slot-contents.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ const require = createRequire(import.meta.url)
export const MANIFEST_SCHEMA_VERSION = 2

/**
* Node 18, the rung C host floor, supports N-API 8 (9 only from 18.17). Pinned here rather
* than left to the headers' default so a header bump cannot silently raise it.
* Keep N-API 8 stable across bundled and host runtimes rather than using the headers'
* default, so a header bump cannot silently change the shipped addon ABI.
*/
export const SLOT_NAPI_VERSION = 8

Expand Down
2 changes: 1 addition & 1 deletion config/scripts/orcad-prebuild-slot-contents.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const next = (slot, overrides = {}) => ({
})

describe('N-API pinning', () => {
it('pins N-API 8 so a host Node 18 (rung C) can load every slot', () => {
it('keeps the N-API 8 addon ABI stable across runtime upgrades', () => {
expect(SLOT_NAPI_VERSION).toBe(8)
// The client's rung C host-Node gate must ask for exactly what the slots are built against.
expect(ORCAD_ADDON_NAPI_VERSION).toBe(SLOT_NAPI_VERSION)
Expand Down
7 changes: 6 additions & 1 deletion config/scripts/orcad-terminal-smoke-change-scope.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { readFileSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
import { classifyNodeServerChanges, collectNodeServerInputs } from './node-server-change-scope.mjs'
import { ORCAD_CHILD_ENTRY_POINTS, ORCAD_ENTRY_POINT } from './orcad-entry-build.mjs'
import {
ORCAD_CHILD_ENTRY_POINTS,
ORCAD_ENTRY_POINT,
ORCAD_LAUNCHER_ENTRY_POINT
} from './orcad-entry-build.mjs'

const ENTRY_POINTS = [
ORCAD_ENTRY_POINT,
ORCAD_LAUNCHER_ENTRY_POINT,
...Object.values(ORCAD_CHILD_ENTRY_POINTS),
'src/cli/index.ts',
'config/scripts/build-orcad-node.mjs',
Expand Down
3 changes: 2 additions & 1 deletion config/scripts/profile-state-worker-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ORCAD_PROFILE_PREFLIGHT_FLAG,
parseOrcadProfilePreflight
} from '../../src/shared/orcad-profile-preflight.ts'
import { ORCAD_SERVER_ENTRY_FILENAME } from '../../src/shared/orcad-artifacts.ts'
import { NODE_RUNTIME_PIN } from '../../src/shared/node-runtime-pin.ts'

async function initializeFixture(directory, databasePath, profileId) {
Expand Down Expand Up @@ -88,7 +89,7 @@ export async function smokeProfileStateWorkers(outDir, { timeoutMs = 30_000, run
const nonce = randomUUID()
const result = runProcessSync({
program: runtimePath,
args: [join(outDir, 'orcad.js'), ORCAD_PROFILE_PREFLIGHT_FLAG, nonce],
args: [join(outDir, ORCAD_SERVER_ENTRY_FILENAME), ORCAD_PROFILE_PREFLIGHT_FLAG, nonce],
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
timeoutMs,
maxOutputBytes: 64 * 1024
Expand Down
7 changes: 5 additions & 2 deletions config/scripts/run-node-server-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
import { join, resolve } from 'node:path'
import { randomUUID } from 'node:crypto'
import { globSync, readFileSync } from 'node:fs'
import { ORCAD_VERSION_FILENAME } from '../../src/shared/orcad-artifacts.ts'
import {
ORCAD_SERVER_ENTRY_FILENAME,
ORCAD_VERSION_FILENAME
} from '../../src/shared/orcad-artifacts.ts'
import { NODE_RUNTIME_PIN } from '../../src/shared/node-runtime-pin.ts'
import {
ORCAD_PROFILE_PREFLIGHT_FLAG,
Expand Down Expand Up @@ -60,7 +63,7 @@ if (artifact) {
const nonce = randomUUID()
const result = runProcessSync({
program: runtimePath,
args: [join(packageDir, 'orcad.js'), ORCAD_PROFILE_PREFLIGHT_FLAG, nonce],
args: [join(packageDir, ORCAD_SERVER_ENTRY_FILENAME), ORCAD_PROFILE_PREFLIGHT_FLAG, nonce],
cwd: root,
env,
timeoutMs: 90_000
Expand Down
8 changes: 6 additions & 2 deletions config/scripts/runtime-serve-terminal-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ import { dirname, join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { randomBytes } from 'node:crypto'
import process from 'node:process'
import { ORCAD_SERVER_ENTRY_FILENAME } from '../../src/shared/orcad-artifacts.ts'
import { packagedNodeRuntimePath } from './build-orcad-node.mjs'
import { currentTarget } from './server-build-target.mjs'

const projectDir = resolve(import.meta.dirname, '../..')
const serveEntry = join(projectDir, 'out', 'main', 'index.js')
const ORCAD_ENTRY = join(projectDir, 'out', 'orcad', 'orcad.js')
const ORCAD_DIR = join(projectDir, 'out', 'orcad')
const ORCAD_ENTRY = join(ORCAD_DIR, ORCAD_SERVER_ENTRY_FILENAME)
const READY_TIMEOUT_MS = 120_000
const OUTPUT_TIMEOUT_MS = 30_000
const SHUTDOWN_TIMEOUT_MS = 15_000
Expand Down Expand Up @@ -175,7 +179,7 @@ function resolveLaunch(userDataDir) {
if (target === 'orcad') {
return {
label: `orcad (${ORCAD_ENTRY})`,
command: process.execPath,
command: packagedNodeRuntimePath(ORCAD_DIR, currentTarget()),
args: [ORCAD_ENTRY, '--port', String(PORT), '--json'],
env: { ORCA_USER_DATA: userDataDir }
}
Expand Down
27 changes: 27 additions & 0 deletions src/main/orcad/launcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createHash } from 'node:crypto'
import { readFileSync, realpathSync } from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join } from 'node:path'
import { ORCAD_SERVER_ENTRY_FILENAME } from '../../shared/orcad-artifacts'
import { assertOrcadServerRuntime, handoffToBundledOrcad } from './orcad-bundled-runtime'

declare const ORCAD_SERVER_SHA256: string

let loadServer: (() => void) | undefined

try {
if (!handoffToBundledOrcad()) {
assertOrcadServerRuntime()
const entry = realpathSync(process.argv[1] ?? __filename)
const server = join(dirname(entry), ORCAD_SERVER_ENTRY_FILENAME)
if (createHash('sha256').update(readFileSync(server)).digest('hex') !== ORCAD_SERVER_SHA256) {
throw new Error('The Orca server does not match its launcher')
}
loadServer = () => createRequire(entry)(server)
}
} catch (error) {
console.error('orcad: failed to launch:', error)
process.exit(78)
}

loadServer?.()
2 changes: 1 addition & 1 deletion src/main/orcad/main-preflight-order.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const { order, profileProbe } = vi.hoisted(() => {
return { order, profileProbe: vi.fn(async () => {}) }
})

vi.mock('./orcad-bundled-runtime', () => ({ handoffToBundledOrcad: () => false }))
vi.mock('./orcad-bundled-runtime', () => ({ assertOrcadServerRuntime: () => {} }))
vi.mock('./orcad-profile-preflight', () => ({
preflightBundledOrcadStartup: async () => {
order.push('profile-admission')
Expand Down
57 changes: 28 additions & 29 deletions src/main/orcad/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
ORCAD_STARTUP_PREFLIGHT_FLAG
} from '../../shared/orcad-profile-preflight'
import { preflightBundledOrcadStartup, runOrcadProfilePreflight } from './orcad-profile-preflight'
import { handoffToBundledOrcad } from './orcad-bundled-runtime'
import { assertOrcadServerRuntime } from './orcad-bundled-runtime'
import {
formatOrcadNativePreflightReport,
ORCAD_NATIVE_PREFLIGHT_FLAG
Expand Down Expand Up @@ -44,7 +44,7 @@ function failStartup(error: unknown): void {
process.exit(resolveOrcadExitCode(error))
}

// Why before the bundled handoff and preflights: launching or completing a stop must not start a runtime.
// One-shot launch and stop commands must skip server startup and preflights.
if (process.argv[2] === WINDOWS_BREAKAWAY_LAUNCH_FLAG) {
runWindowsBreakawayLaunchIfRequested(ORCAD_WINDOWS_BREAKAWAY_CONTRACT, process.argv)
} else if (
Expand All @@ -60,34 +60,33 @@ if (process.argv[2] === WINDOWS_BREAKAWAY_LAUNCH_FLAG) {

function startOrcadProcess(): void {
try {
if (!handoffToBundledOrcad()) {
const flag = process.argv[2]
if (flag === ORCAD_NATIVE_PREFLIGHT_FLAG && process.argv.length === 3) {
void import('./node-pty-precondition').then(({ checkNodePtyPrecondition }) => {
const verdict = checkNodePtyPrecondition()
const report = formatOrcadNativePreflightReport(verdict.status, verdict.reason ?? null)
process.stdout.write(`${report}\n`, () => process.exit(0))
}, failStartup)
} else 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
assertOrcadServerRuntime()
const flag = process.argv[2]
if (flag === ORCAD_NATIVE_PREFLIGHT_FLAG && process.argv.length === 3) {
void import('./node-pty-precondition').then(({ checkNodePtyPrecondition }) => {
const verdict = checkNodePtyPrecondition()
const report = formatOrcadNativePreflightReport(verdict.status, verdict.reason ?? null)
process.stdout.write(`${report}\n`, () => process.exit(0))
}, failStartup)
} else 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
})
// Why exit: the owner reads to EOF, so a lingering native handle must not hold the probe open.
.then(() => process.stdout.write('', () => process.exit(0)))
.catch(failStartup)
} else {
// Why: stdout is the serve readiness API; incidental diagnostics go to stderr.
reserveServeStdoutForReadiness()
void preflightBundledOrcadStartup()
.then(() => {
runOrcadNativePreflight()
return main()
})
// Why exit: the owner reads to EOF, so a lingering native handle must not hold the probe open.
.then(() => process.stdout.write('', () => process.exit(0)))
.catch(failStartup)
} else {
// Why: stdout is the serve readiness API; incidental diagnostics go to stderr.
reserveServeStdoutForReadiness()
void preflightBundledOrcadStartup()
.then(() => {
runOrcadNativePreflight()
return main()
})
.catch(failStartup)
}
.catch(failStartup)
}
} catch (error) {
failStartup(error)
Expand Down
Loading
Loading