diff --git a/src/main/daemon/pty-subprocess/native-pty-spawn.ts b/src/main/daemon/pty-subprocess/native-pty-spawn.ts index e0332ba9921..d2f55ebfe95 100644 --- a/src/main/daemon/pty-subprocess/native-pty-spawn.ts +++ b/src/main/daemon/pty-subprocess/native-pty-spawn.ts @@ -1,10 +1,13 @@ +import { appendFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import * as pty from 'node-pty' import { hostReportsChildExitStatus, wrapShellSpawnForMacosTccAttribution } from '../../providers/macos-tcc-login-shell' import type { WindowsShellSpawnAttempt } from '../../providers/windows-shell-fallback-chain' -import { assignHostProcessToKillOnCloseJob } from '../../windows/windows-pty-job' +import { assignHostProcessToKillOnCloseJob, listPtyJobProcessIds, isPtyJobOwnershipAvailable } from '../../windows/windows-pty-job' export type SpawnedDaemonPty = { process: pty.IPty @@ -31,7 +34,7 @@ export function spawnNativeDaemonPty(args: { const wrapped = wrapShellSpawnForMacosTccAttribution(shellPath, shellArgs, args.env) // Why: children inherit job membership, so the host job must exist before the first Windows PTY. if (process.platform === 'win32') { - assignHostProcessToKillOnCloseJob() + appendFileSync(join(tmpdir(), 'orca-pty-job-diagnostic.jsonl'), JSON.stringify({ event: 'host-job', pid: process.pid, available: isPtyJobOwnershipAvailable(), assigned: assignHostProcessToKillOnCloseJob() }) + '\n') } const proc = pty.spawn(wrapped.file, wrapped.args, { name: args.env.TERM ?? 'xterm-256color', @@ -42,6 +45,13 @@ export function spawnNativeDaemonPty(args: { // Why: bundled ConPTY has the wrap-marker behavior xterm expects. ...(process.platform === 'win32' ? { useConptyDll: true } : {}) }) + if (process.platform === 'win32') { + const report = (event: string): void => { + appendFileSync(join(tmpdir(), 'orca-pty-job-diagnostic.jsonl'), JSON.stringify({ event, host: process.pid, shell: proc.pid, shellPath, members: listPtyJobProcessIds(proc) }) + '\n') + } + report('spawn-job') + setTimeout(() => report('job-after-launch'), 3000).unref() + } reportsChildExitStatus = hostReportsChildExitStatus(wrapped.file) args.onMacosTccSpawnStrategy?.(wrapped.file === shellPath ? 'direct' : 'wrapped') return proc diff --git a/tests/e2e/helpers/electron-process-shutdown.ts b/tests/e2e/helpers/electron-process-shutdown.ts index cae5689833e..04193007620 100644 --- a/tests/e2e/helpers/electron-process-shutdown.ts +++ b/tests/e2e/helpers/electron-process-shutdown.ts @@ -1,3 +1,4 @@ +import { tmpdir } from 'node:os' import { readWindowsProcessTableFresh } from '../../../src/main/windows/windows-process-table' import type { ChildProcess } from 'node:child_process' import { execFileSync } from 'node:child_process' @@ -247,6 +248,8 @@ export async function cleanupE2EDaemons(userDataDir: string): Promise { // Why: app quit intentionally leaves daemon PTYs alive for warm reattach. // E2E temp profiles are deleted after each test, so their detached daemons // must be stopped explicitly or CI accumulates orphan Electron/shell trees. + const jobLog = path.join(tmpdir(), 'orca-pty-job-diagnostic.jsonl') + if (existsSync(jobLog)) console.error('NATIVE_JOB_DIAGNOSTIC', readFileSync(jobLog, 'utf8')) const daemonPids = readDaemonPidFiles(userDataDir) console.error('DAEMON_PID_FILES', userDataDir, daemonPids) await reportShutdownTree('before-daemon-cleanup')