A throwaway probe that answers one empirical question for Phase 1 of the Windows update-survival work:
What is the minimal set of files that must be copied out of a packaged
win-unpackedbuild so that a copiedOrca.exe— run withELECTRON_RUN_AS_NODE=1from a directory outside the install dir — can: (a) start the terminal daemon and signal ready over IPC, (b) spawn a real ConPTYnode-ptysession, write input, and read output back, (c) do all of that while holding no open file handles into the app/install dir, so an NSIS update could delete the original install.
The daemon is fork()ed from the app's own Electron binary (Orca.exe) with
ELECTRON_RUN_AS_NODE=1, from the install directory. On a Windows update,
electron-builder's NSIS installer (a) runs uninstallOldVersion (deletes the
registered install's files) and (b) CHECK_APP_RUNNING force-closes every
process whose image path is under $INSTDIR. So the daemon dies and its held
file locks can break the update.
The Phase 1 fix copies the daemon's whole file closure out of the install
dir (into %LOCALAPPDATA%/userData) and forks the daemon from the copy, so
its image + all loaded modules live outside $INSTDIR. This spike measures how
small that copy can be while still working.
We keep the Electron binary run as node (not a stock node.exe): a prior
attempt (#7473, reverted) switched to stock node and caused Windows-console
flashing (stock node lacks Electron's kHideConsoleWindows) plus asar breakage.
This spike does not reintroduce stock node.
# Real run (Windows, needs a packaged win-unpacked build):
node tools/daemon-relocation-spike/spike.mjs \
--app-dir <path-to-win-unpacked> \
--work-dir <scratch-dir> \
[--tier full|no-gpu|minimal] \
[--keep-work-dir]
# Offline logic validation (any OS, no build, no launch):
node tools/daemon-relocation-spike/spike.mjs --selftest
Exit code is 0 only when the run PASSES: daemon ready, PTY echo
round-trips the nonce, the daemon's main module is the copied Orca.exe, and
no loaded module resolves under --app-dir.
Every tier includes the irreducible core: Orca.exe, icudtl.dat, both V8
snapshot blobs (snapshot_blob.bin, v8_context_snapshot.bin), the daemon
bundle (out/main/daemon-entry.js + chunks/ + out/package.json), and the
whole node-pty package (native conpty.node + the sibling conpty/ runtime
dir holding conpty.dll + OpenConsole.exe).
Tiers differ only in which top-level *.dll files they carry:
Trimming further is a config change (edit TIER_DEFINITIONS / GPU_DLLS), not
a code change.
The whole node-pty package tree is copied, and the entire win-unpacked
layout is mirrored verbatim (every copy destination is relative to the
win-unpacked root, not the asar-unpacked root). This matters because node-pty is
packaged at resources/node_modules/node-pty — a sibling of
app.asar.unpacked, not under it (see
config/packaged-runtime-node-modules.cjs). Mirroring the full layout preserves
two resolutions from the relocated path:
- The daemon require-closure resolves
require('node-pty')by walking parent dirs up from the mirroredresources/app.asar.unpacked/out/main/daemon-entry.js, which passes throughresources/and findsresources/node_modules/node-pty— exactly as in the packaged app. - node-pty's own native loader resolves
conpty.nodefrombuild/Release(orprebuilds/win32-<arch>) relative to node-pty's own__dirname, and node-pty's Windows addon loadsconpty.dllfrom<dir-of-conpty.node>/conpty/and spawnsOpenConsole.exefrom beside it. Copying the tree verbatim keeps all three side-by-side.
The reverted #7421 added a node-pty patch that reads
ORCA_NODE_PTY_NATIVE_DIR to override the native dir. The current branch's
config/patches/node-pty@1.1.0.patch does NOT contain that override — it was
reverted. The spike therefore relies on layout preservation (copying the
node-pty tree at its default relative path) rather than the env override. The
spike still sets ORCA_NODE_PTY_NATIVE_DIR to the relocated native dir so it
keeps working if pointed at a build that carries the patch, but on this branch
the var is inert.
Implication for the real Phase 1 implementation: if the production copy does
NOT preserve node-pty at the path its loader resolves by default (e.g. if the
daemon-entry is relocated without the sibling node_modules/node-pty), the impl
will need to re-add the ORCA_NODE_PTY_NATIVE_DIR patch from #7421. If it
mirrors the layout as this spike does, the patch is not strictly required —
though re-adding it is the more robust choice.
ndjson-client.mjs is a small standalone NDJSON client (no electron/src
imports) that mirrors src/main/daemon/daemon-server.ts:
- Read the token the server writes to the token file after it begins listening.
- Open a control socket, send
hello {role:'control'}, await{type:'hello', ok:true}. - Open a stream socket with the same
clientId, sendhello {role:'stream'}. createOrAttachon control, thenwriteecho SPIKE-OK-<nonce>\r\n, and readdataevents on the stream socket until the nonce appears alone at line start (executed output, distinct from the echoed input line).
PROTOCOL_VERSION is read at runtime from src/main/daemon/types.ts so the
client never drifts from the daemon.
loaded-modules.ps1 (via loaded-module-probe.mjs) runs
Get-Process -Id <pid> and enumerates .Modules[].FileName. Any module path
under --app-dir is a lock risk (the installer cannot replace a file a live
process maps), so a passing relocation must show zero. It also asserts the
process's main module is the copied Orca.exe, not the install-dir one.
Loaded DLLs are the lock-critical set. Data files (icudtl.dat, asar) are not
memory-mapped as modules, so this probe does not enumerate them — the copy plan
handles those by construction (they are copied, so nothing opens the originals).
This session has no build, so the launch path is unproven. Verified here:
node --check on every .mjs, a green --selftest, and clean
pnpm exec oxlint. Open questions the real CI run must answer:
- Whether
TIER_MINIMAL(no top-level DLLs) bootsOrca.exeas node at all, or whether run-as-node still needsffmpeg.dll/ others — this is the core empirical result. - Whether the daemon bundle require-closure needs any other unpacked
node_modulesbeyondnode-pty(surfaces as a ready-timeout if so). - Whether any loaded module still resolves under
--app-dir(the handle probe will name it).
no-gpu is the safe minimal target to ship: run-as-node Electron does not
initialize the GPU/render stack, so libEGL / libGLESv2 / vk_swiftshader /
vulkan-1 / d3dcompiler_47 are very unlikely to load, while ffmpeg.dll and
the ICU/snapshot data are retained because the Electron bootstrap references
them regardless of run-as-node. Run --tier minimal on CI first: if it PASSES,
ship minimal; if Orca.exe fails to boot without the non-GPU DLLs, fall back to
no-gpu. full is the always-works upper bound for comparison.
