From 225cbe7092599dcd0c289d69a5d0c94ac5d31e90 Mon Sep 17 00:00:00 2001 From: Neil Date: Fri, 16 Oct 2026 01:39:05 -0700 Subject: [PATCH 1/2] fix(orcad): a locked Electron browser sidecar profile no longer fails orcad's stop on Windows --- .../electron-serve-browser-process.test.ts | 46 ++++++++++++++++++- .../orcad/electron-serve-browser-process.ts | 9 +++- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/main/orcad/electron-serve-browser-process.test.ts b/src/main/orcad/electron-serve-browser-process.test.ts index 4782063b27bd..e9606427dcb1 100644 --- a/src/main/orcad/electron-serve-browser-process.test.ts +++ b/src/main/orcad/electron-serve-browser-process.test.ts @@ -5,12 +5,29 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as RunProcessModule from '@orca/process-host' +import type * as FsPromisesModule from 'node:fs/promises' import type { RuntimeBrowserCommandHost } from '../runtime/browser/browser-command-contracts' -const { spawnProcessMock } = vi.hoisted(() => ({ - spawnProcessMock: vi.fn<(spec: ProcessSpec) => ReturnType>() +const { spawnProcessMock, lockedRemovalPaths } = vi.hoisted(() => ({ + spawnProcessMock: + vi.fn<(spec: ProcessSpec) => ReturnType>(), + lockedRemovalPaths: new Set() })) +// Stands in for Windows, where the killed sidecar's helpers still hold its profile for a moment. +vi.mock('node:fs/promises', async (importOriginal) => { + const actual = await importOriginal() + const rm: typeof actual.rm = async (path, options) => { + if (lockedRemovalPaths.has(String(path))) { + throw Object.assign(new Error(`EBUSY: resource busy or locked, rmdir '${String(path)}'`), { + code: 'EBUSY' + }) + } + return actual.rm(path, options) + } + return { ...actual, rm, default: { ...actual, rm } } +}) + // Why wrap rather than fake: the provider must still go through the real spawn // path (argv/env handling, a real pid to signal); only the program is swapped // for a stub that speaks the sidecar's serve protocol. @@ -279,6 +296,31 @@ describe('ElectronServeBrowserProcess start-up', () => { expect(existsSync(userDataPath)).toBe(false) expect(() => process.kill(metadata.pid, 0)).toThrow() }) + + it('still stops cleanly when the sidecar profile stays locked after the process exits', async () => { + const processHandle = await startProvider() + const userDataPath = (spawnSpec().args ?? []) + .find((arg) => arg.startsWith('--user-data-dir='))! + .slice('--user-data-dir='.length) + const metadata = JSON.parse( + await readFile(join(userDataPath, 'orca-runtime.json'), 'utf8') + ) as { pid: number } + lockedRemovalPaths.add(userDataPath) + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + + try { + await expect(processHandle.stop()).resolves.toBeUndefined() + expect(warn).toHaveBeenCalledWith( + '[orcad] Electron browser sidecar data was not removed:', + expect.objectContaining({ code: 'EBUSY' }) + ) + expect(() => process.kill(metadata.pid, 0)).toThrow() + } finally { + lockedRemovalPaths.delete(userDataPath) + warn.mockRestore() + await rm(userDataPath, { recursive: true, force: true }) + } + }) }) describe('ElectronServeBrowserProcess command routing', () => { diff --git a/src/main/orcad/electron-serve-browser-process.ts b/src/main/orcad/electron-serve-browser-process.ts index 89427210ecb7..1b12ba403c2a 100644 --- a/src/main/orcad/electron-serve-browser-process.ts +++ b/src/main/orcad/electron-serve-browser-process.ts @@ -1,4 +1,4 @@ -import { mkdtemp, rm } from 'node:fs/promises' +import { mkdtemp } from 'node:fs/promises' import { createServer, type AddressInfo } from 'node:net' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -12,6 +12,7 @@ import { BROWSER_UNAVAILABLE_ERROR_CODE } from '../../shared/runtime-types' import { readRuntimeMetadata } from '../runtime/runtime-metadata' import { spawnProcess } from '@orca/process-host' import { isProcessAlive } from '../../shared/serve-bundle-coordination' +import { removeTree } from '../../shared/windows-transient-lock-removal' import { BROWSER_SIDECAR_OWNER_ENV } from '../../shared/browser-sidecar-lifetime' import { electronIsolatedProfileSwitches } from '../../shared/electron-isolated-profile-switches' import { headlessServeOzonePlatformSwitches } from '../../shared/headless-serve-ozone-platform' @@ -197,7 +198,11 @@ export class ElectronServeBrowserProcess { } } if (sidecarDataPath) { - await rm(sidecarDataPath, { recursive: true, force: true }) + // Why best-effort: on Windows the killed sidecar's helpers hold this throwaway profile for a + // moment, and a leftover temp dir must not fail orcad's stop and strand its instance lock. + await removeTree(sidecarDataPath).catch((error: unknown) => + console.warn('[orcad] Electron browser sidecar data was not removed:', error) + ) } } From e6bc0c46c6d53cb434cc09ad432e33e3af050fef Mon Sep 17 00:00:00 2001 From: Neil Date: Fri, 16 Oct 2026 01:55:15 -0700 Subject: [PATCH 2/2] test(orcad): give the stop-request teardown room for slow Windows ARM deletes --- src/main/orcad/orcad-stop-request-shutdown.integration.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/orcad/orcad-stop-request-shutdown.integration.test.ts b/src/main/orcad/orcad-stop-request-shutdown.integration.test.ts index 98857ff3c7f9..ed15c9bae70b 100644 --- a/src/main/orcad/orcad-stop-request-shutdown.integration.test.ts +++ b/src/main/orcad/orcad-stop-request-shutdown.integration.test.ts @@ -60,7 +60,8 @@ afterEach(async () => { await killProfileDaemons(join(root, 'data')) await removeTestRoot(root) } -}) + // Why: windows-11-arm runners took 15-60+ s to delete the freshly written slot and runtime here. +}, 180_000) describe.skipIf(skip)('packaged orcad stop request', () => { it(`stops cleanly within ${ORCAD_SHUTDOWN_DEADLINE_MS} ms of its slot's stop-request file`, async () => {