diff --git a/src/main/pty/node-pty-runtime-relocation.test.ts b/src/main/pty/node-pty-runtime-relocation.test.ts index 1dfbe033885..ff5e2da053c 100644 --- a/src/main/pty/node-pty-runtime-relocation.test.ts +++ b/src/main/pty/node-pty-runtime-relocation.test.ts @@ -105,7 +105,7 @@ describe('ensureRelocatedNodePtyNativeRuntime', () => { expect(readFileSync(join(destDir!, 'conpty.node'), 'utf8')).toBe('binding') }) - it('removes stale version dirs but keeps the current one', () => { + it('preserves prior version dirs so a surviving daemon keeps its runtime', () => { const sourceDir = join(tempDir, 'source') seedSourceDir(sourceDir) const destRoot = join(tempDir, 'dest') @@ -114,7 +114,11 @@ describe('ensureRelocatedNodePtyNativeRuntime', () => { const destDir = ensureRelocatedNodePtyNativeRuntime({ sourceDir, destRoot, version: '2.0.0' }) expect(destDir).toBe(join(destRoot, '2.0.0')) - expect(existsSync(join(destRoot, '1.0.0'))).toBe(false) + // Why: a daemon adopted from the previous app version still loads + // conpty.dll/OpenConsole.exe from its own version dir; deleting it out + // from under the live daemon breaks its next conpty spawn. The prior + // version dir must survive the new version's relocation. + expect(existsSync(join(destRoot, '1.0.0', 'conpty.node'))).toBe(true) expect(existsSync(join(destRoot, '2.0.0', 'conpty.node'))).toBe(true) }) diff --git a/src/main/pty/node-pty-runtime-relocation.ts b/src/main/pty/node-pty-runtime-relocation.ts index a3821dde5d4..695363e320c 100644 --- a/src/main/pty/node-pty-runtime-relocation.ts +++ b/src/main/pty/node-pty-runtime-relocation.ts @@ -1,12 +1,4 @@ -import { - copyFileSync, - existsSync, - mkdirSync, - readdirSync, - renameSync, - rmSync, - writeFileSync -} from 'node:fs' +import { copyFileSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs' import { createRequire } from 'node:module' import { dirname, join } from 'node:path' import { app } from 'electron' @@ -55,31 +47,6 @@ function copyRuntimeTree(sourceDir: string, destDir: string): void { } } -function removeStaleRuntimeVersions(destRoot: string, keepVersion: string): void { - let entries - try { - entries = readdirSync(destRoot, { withFileTypes: true }) - } catch { - return - } - for (const entry of entries) { - if (!entry.isDirectory() || entry.name === keepVersion) { - continue - } - // Why: an adopted daemon from a previous version may still run (or later - // respawn) binaries out of its own version dir. Renaming a directory - // fails on Windows while anything inside is open, so a successful rename - // proves the dir is unused and safe to delete. - const doomedPath = join(destRoot, `${entry.name}.stale`) - try { - renameSync(join(destRoot, entry.name), doomedPath) - rmSync(doomedPath, { recursive: true, force: true }) - } catch { - // Still in use (or already being cleaned) — retry on a future launch. - } - } -} - export function ensureRelocatedNodePtyNativeRuntime(options: { sourceDir: string destRoot: string @@ -95,7 +62,14 @@ export function ensureRelocatedNodePtyNativeRuntime(options: { copyRuntimeTree(sourceDir, destDir) writeFileSync(join(destDir, RELOCATION_COMPLETE_MARKER), '') } - removeStaleRuntimeVersions(destRoot, version) + // Why: stale per-version dirs are deliberately NOT deleted here. A + // surviving daemon adopted from a previous app version still loads + // conpty.dll/OpenConsole.exe out of its own version dir; on Windows the + // running .exe/.dll images open with FILE_SHARE_DELETE, so a rename+delete + // would succeed and pull the runtime out from under that live daemon, + // breaking its next conpty spawn (ERROR_PATH_NOT_FOUND). Leaving old dirs + // in place is safe — they are small and only cleaned once it's provably + // safe (see follow-up daemon-version plumbing). return destDir } catch { // Fail open: node-pty keeps loading from the install dir, which is the