diff --git a/apps/desktop/electron/backend-claim.test.ts b/apps/desktop/electron/backend-claim.test.ts index 851f88d1c5..ef44c3b4f0 100644 --- a/apps/desktop/electron/backend-claim.test.ts +++ b/apps/desktop/electron/backend-claim.test.ts @@ -66,9 +66,15 @@ test('processStartMarker resolves a real marker for the current process', async assert.match(marker, /^(linux|win|winms|ps):.+/) }) -test('processStartMarker rejects for a PID that does not exist', async () => { +test('a missing PID is classified as ESRCH so reapOrphans can drop the record', async () => { // Largest PIDs are bounded well below this on every supported platform. - await assert.rejects(processStartMarker(2 ** 30 + 12345)) + // Windows Get-Process / macOS `ps -p` used to surface exit code 1, which + // the identity matchers treated as "unknown" and kept forever. The native + // gate throws ESRCH — the errno those catch blocks already map to gone. + await assert.rejects( + processStartMarker(2 ** 30 + 12345), + (error: NodeJS.ErrnoException) => error?.code === 'ESRCH' + ) }) // --- PID-only marker helpers -------------------------------------------------- diff --git a/apps/desktop/electron/backend-claim.ts b/apps/desktop/electron/backend-claim.ts index 761b1bcf60..f00a4d5bc9 100644 --- a/apps/desktop/electron/backend-claim.ts +++ b/apps/desktop/electron/backend-claim.ts @@ -21,6 +21,7 @@ import { execFile } from 'node:child_process' import fs from 'node:fs' import { electronProcessStartMarker } from './parent-process-identity' +import { isPidAlive } from './update-marker' import { hiddenWindowsChildOptions } from './windows-child-options' export function execText(command: string, args: string[], { timeout = 3000 } = {}): Promise { @@ -42,6 +43,15 @@ export function execText(command: string, args: string[], { timeout = 3000 } = { * `claimDecision` / `probeStartMarker`). */ export async function processStartMarker(pid: number): Promise { + // Cheap native dead-PID gate. Windows Get-Process / macOS `ps -p` exit 1 + // on a missing PID (not ESRCH), so the identity matchers used to keep the + // orphan and re-probe it every launch (#92875). ESRCH is the code those + // catch blocks already map to "gone". Alive or uninspectable (EPERM) PIDs + // still fall through to the platform probe. + if (!isPidAlive(pid)) { + throw Object.assign(new Error(`PID ${pid} no longer exists`), { code: 'ESRCH' }) + } + if (process.platform === 'linux') { const stat = await fs.promises.readFile(`/proc/${pid}/stat`, 'utf8')