Files
orca/src/packages/process-host
Brennan Benson fa4ef2b961 ci: stop new main-process code from freezing Orca with synchronous launches (#27756)
* ci: ratchet synchronous child-process launches in main and shared code

The Electron main process freezes every window while a synchronous child
process runs, and on Windows each launch is slow because antivirus scans
it. Nothing stopped new synchronous launches from landing in main-process
code, and the spawn observer behind ORCA_MAIN_THREAD_DIAGNOSTICS only saw
async spawns, so the worst blocks were invisible to it.

Ratchet: check:main-sync-spawn-ratchet parses production src/main and
src/shared files (test-only paths excluded) and fails when a file not in
config/main-sync-spawn-baseline.txt reaches execFileSync, spawnSync or
execSync from child_process, runProcessSync from @orca/process-host, or
one of the synchronous secure-file writers (they run icacls on Windows).
Named, aliased, namespace, default, require() and import() forms count.
A baseline entry whose file no longer launches synchronously fails as
stale, so the list can only shrink. It runs in lint and in the PR
static-analysis job next to the process-host import check.

Observer: runProcessSync now reports to the spawn observer, timing the
whole call (that is how long the thread was stuck) and reporting from a
finally so failing, throwing and timed-out children are counted too.

Runtime behavior is unchanged except that the diagnostics observer now
sees synchronous launches; with no observer registered the sync path
pays one extra branch.

* ci: count sync launches per file and gate the repo's sync wrappers

The sync-spawn ratchet only tracked which files launched synchronously, so a
file already on the list (wsl.ts, win32-utils.ts, git-exec-file.ts) could add
more launches unnoticed, and new callers of the repo's own synchronous
wrappers passed CI.

The baseline now stores a per-file count of uses, in the same format as the
focus-read ratchet: a rise or a new file fails, a lower or zero count fails
until `pnpm check:main-sync-spawn-ratchet --prune` lowers it, and --prune
never raises an entry. Every value reference to a gated function counts
(call, hand-off, alias and its uses, re-export); imports alone count nothing.

Gated wrappers: gitExecFileSync (and the git/runner re-export),
restrictWindowsPathSync, hardenSecurePath when called with sync: true,
grantDirAcl, getWslHome, listWslDistros, wslUncDirectoryExists,
isWslAvailable (and the wsl.ts re-export), isPwshAvailable. Their 18
current caller files join the baseline.

Failure messages now give advice per family: the async process-host API for
child-process launches, the async twin for wrappers, and for the secure
writers a note that no async secure writer exists yet, so the sync one stays
and must never become a plain write (that drops the Windows ACL hardening).

Also refresh two stale blockMs descriptions and drop a no-op line from the
spawn-observer "stays silent" test.

* ci: count hardenSecurePath unless visibly async, and tighten ratchet tests

hardenSecurePath was counted only when `{ sync: true }` was written inline,
so moving the options into a variable or a spread dropped the count and the
check then asked for a prune. It now counts every use except a call with no
options or a spread-free object literal whose `sync` is absent or false.

Also: resolve backslash paths in the importing file as well as the
specifier; still visit heritage and instantiation expressions
(`extends mixin(fn)`, `fn<T>`); state the reference-only counting limit;
and point the secure-writer advice at raising this file's baseline count.

Tests cover the hardenSecurePath shapes, the alias chains that need the
fixpoint loop, and a prune that must not raise a grown row.

* test(ci): pin the main sync-spawn ratchet background step

* fix: count secure hardening calls with spread arguments
2026-10-12 11:28:25 -07:00
..

Orca process host

This Node package owns subprocess spawning, forked children, bounded output, cancellation, Windows argument handling, and process-tree termination. Desktop, the headless server, CLI, SSH relay, and daemon use the same implementation. It has no runtime npm dependencies, Electron imports, or native addons.

Import runProcess, runProcessSync, and spawnProcess from @orca/process-host. Import their contracts and default limits from @orca/process-host/process-spec. Other explicit subpaths expose forking, termination, Windows arguments, and bounded output. Application code must use public exports.

spawnProcess guarantees three stream pipes when stdio is omitted, is 'pipe', or starts with ['pipe', 'pipe', 'pipe']. Other specifications return a child with nullable streams. Use PipedProcessSpec and PipedChildProcess when a stream-owning interface requires pipes.

The application supplies policy through process-tree-kill-gate and diagnostics through spawn-observer. Register these before starting managed children. Each process must load one package instance so registration and execution share state. The production exports use one CommonJS output for both import and require callers. The orca-source condition supports tests, dependency analysis, and Node tooling that runs TypeScript through tsx. Source tooling runs in a separate process and must use that condition for every package import; mixing source and compiled output would split the registration state.

From the repository root:

pnpm build:packages
pnpm --filter @orca/process-host typecheck
ORCA_BACKGROUND_LAUNCH=1 pnpm --filter @orca/process-host test

Which copy a caller loads:

  • Root app build, test, typecheck, and launch scripts build dist first. Editor types and root typechecks read its declarations.
  • pnpm dev refreshes dist once at launch. Restart it after package edits, or run pnpm dev --watch to rebuild the package and restart the app after package or main-process source changes.
  • Run pnpm build:packages after package edits before invoking config scripts or Vitest directly; their bootstrap code can read dist.
  • Vitest and tsx --conditions=orca-source load src through the orca-source condition.
  • Packaging ships dist; supported build scripts and CI rebuild it first. The packaging guard checks missing exports/modules and removed-source leftovers, but does not certify source freshness. Build the package before invoking electron-builder directly.

The build needs no prior dist: tsx --conditions=orca-source starts its compiler through source. It serializes builds with a lock, compiles into staging, adds new files before replacing existing files, then removes obsolete output. Failed compilation preserves prior output; unchanged files are not rewritten. Publication is atomic per file, so a concurrent reader can still observe mixed build versions. Detected lock loss stops subsequent writes and preserves the new owner's lock; already published files remain. tsx and proper-lockfile are build-only dependencies.

The package tests cover execution, output bounds, cancellation, termination, and Windows command construction. Windows shim integration stays under src/shared/__tests__/process-host because those tests use the host's filesystem cleanup policy. Root CI runs both sets, and root pnpm typecheck type-checks the package tests through tsconfig.test.json; Windows integration still requires a Windows runner. Package artifact tests verify public exports and shared registration state from staged output. The engines floor is Node 18 because the relay bundles this package for remote hosts. Its compiler uses Node 18 module semantics and Node 18 types to reject newer builtin APIs; CI's Node 18 job runs config/scripts/smoke-process-host-node18.mjs against the staged public output. Repository build and test tools use the root manifest's newer Node requirement.