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
46 changes: 44 additions & 2 deletions src/main/orcad/electron-serve-browser-process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof RunProcessModule.spawnProcess>>()
const { spawnProcessMock, lockedRemovalPaths } = vi.hoisted(() => ({
spawnProcessMock:
vi.fn<(spec: ProcessSpec) => ReturnType<typeof RunProcessModule.spawnProcess>>(),
lockedRemovalPaths: new Set<string>()
}))

// 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<typeof FsPromisesModule>()
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.
Expand Down Expand Up @@ -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', () => {
Expand Down
9 changes: 7 additions & 2 deletions src/main/orcad/electron-serve-browser-process.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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)
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Loading