diff --git a/src/main/providers/wsl-guest-process-inventory-parser.ts b/src/main/providers/wsl-guest-process-inventory-parser.ts index 9b468057129..b551e8e8709 100644 --- a/src/main/providers/wsl-guest-process-inventory-parser.ts +++ b/src/main/providers/wsl-guest-process-inventory-parser.ts @@ -24,8 +24,10 @@ export function parseWslGuestProcessInventoryPayload( let bootId: string | null = null let expectedCount: number | null = null let seenCount: number | null = null + let skippedCount: number | null = null const rows: WslGuestProcessRow[] = [] const pids = new Set() + const skippedPids = new Set() for (const rawLine of payload.split(/\r?\n/)) { // Remove only the transport CR; trailing spaces belong to the command // remainder and must not be normalized away. @@ -41,13 +43,23 @@ export function parseWslGuestProcessInventoryPayload( bootId = boot[1]! continue } - const count = line.match(/^count (\d+) (\d+)$/) + const count = line.match(/^count (\d+) (\d+)(?: (\d+))?$/) if (count) { if (seenCount !== null) { throw new Error('duplicate_count') } seenCount = Number(count[1]) expectedCount = Number(count[2]) + skippedCount = count[3] === undefined ? 0 : Number(count[3]) + continue + } + const skipped = line.match(/^skip (\d+)$/) + if (skipped) { + const pid = Number(skipped[1]) + if (!Number.isSafeInteger(pid) || pid <= 0 || pids.has(pid) || skippedPids.has(pid)) { + throw new Error('invalid_row') + } + skippedPids.add(pid) continue } const row = line.match(/^row (\d+) (\d+) (\d+) (-?\d+) (-?\d+) (\S+) (\S+) (\d+)(?: (.*))?$/) @@ -92,8 +104,10 @@ export function parseWslGuestProcessInventoryPayload( if ( seenCount === null || expectedCount === null || - seenCount !== expectedCount || - seenCount !== rows.length + skippedCount === null || + seenCount !== rows.length || + skippedCount !== skippedPids.size || + seenCount + skippedCount !== expectedCount ) { throw new Error('row_count_mismatch') } diff --git a/src/main/providers/wsl-guest-process-inventory.test.ts b/src/main/providers/wsl-guest-process-inventory.test.ts index 4aca88ec9b6..ac4a4ae1c3c 100644 --- a/src/main/providers/wsl-guest-process-inventory.test.ts +++ b/src/main/providers/wsl-guest-process-inventory.test.ts @@ -1,4 +1,7 @@ import { execFileSync } from 'node:child_process' +import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { beforeEach, describe, expect, it, vi } from 'vitest' const { runProcessMock } = vi.hoisted(() => ({ runProcessMock: vi.fn() })) @@ -37,6 +40,54 @@ describe('WSL guest process inventory', () => { expect(WSL_GUEST_INVENTORY_SCRIPT).not.toContain('cat "/proc/$_orca_pid/stat"') }) + it('skips a process whose proc stat disappears and keeps the remaining agent row', () => { + const fixtureRoot = mkdtempSync(join(tmpdir(), 'wsl-guest-inventory-')) + try { + mkdirSync(join(fixtureRoot, 'sys/kernel/random'), { recursive: true }) + writeFileSync(join(fixtureRoot, 'sys/kernel/random/boot_id'), `${bootId}\n`) + mkdirSync(join(fixtureRoot, '100')) + mkdirSync(join(fixtureRoot, '101')) + // Linux /proc//stat-shaped output: field 22 is the start-time tick. + writeFileSync( + join(fixtureRoot, '100/stat'), + `100 (bash) S ${Array.from({ length: 18 }, () => '0').join(' ')} 12345 0\n` + ) + writeFileSync( + join(fixtureRoot, '101/stat'), + `101 (codex) S ${Array.from({ length: 18 }, () => '0').join(' ')} 54321 0\n` + ) + const binRoot = join(fixtureRoot, 'bin') + mkdirSync(binRoot) + const fakePs = join(binRoot, 'ps') + writeFileSync( + fakePs, + '#!/bin/sh\nprintf "%s\\n" "999999 0 100 100 100 pts/0 S short-lived" "100 0 100 100 101 pts/0 Ss+ bash" "101 100 100 101 101 pts/0 Sl+ codex"\n' + ) + chmodSync(fakePs, 0o755) + const script = WSL_GUEST_INVENTORY_SCRIPT.replaceAll( + '/proc', + fixtureRoot.replaceAll('\\', '/') + ) + const output = execFileSync('sh', ['-c', script], { + encoding: 'utf8', + env: { ...process.env, PATH: `${binRoot}:${process.env.PATH ?? ''}` } + }) + expect(output).toContain('skip 999999') + const inventory = parseWslGuestProcessInventoryPayload(output, 'Ubuntu') + expect(inventory.rows).toHaveLength(2) + const resolved = resolveWslGuestForegroundProcess(inventory, { + distro: 'Ubuntu', + bootId, + shellPid: 100, + shellStartTime: 12345, + tty: '/dev/pts/0' + }) + expect(resolved).toMatchObject({ status: 'live', processName: 'codex' }) + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }) + } + }) + it('parses fixed fields and preserves whitespace in args', () => { const inventory = parseWslGuestProcessInventoryPayload( payload('row 100 90 90 100 100 pts/0 Sl+ 12345 /usr/bin/node --name "a b" --x'), diff --git a/src/main/providers/wsl-guest-process-inventory.ts b/src/main/providers/wsl-guest-process-inventory.ts index d070874dd0b..06635872ff7 100644 --- a/src/main/providers/wsl-guest-process-inventory.ts +++ b/src/main/providers/wsl-guest-process-inventory.ts @@ -53,12 +53,22 @@ export const WSL_GUEST_INVENTORY_SCRIPT = [ '$_orca_ps', 'EOF', '_orca_seen=0', + '_orca_skipped=0', 'while IFS= read -r _orca_line; do', ' [ -n "$_orca_line" ] || { continue; }', ' IFS=" " read -r _orca_pid _orca_ppid _orca_sid _orca_pgid _orca_tpgid _orca_tty _orca_stat _orca_args </dev/null || { printf "error start_time\\n"; exit 1; }', + ' if { IFS= read -r _orca_procstat < "/proc/$_orca_pid/stat"; } 2>/dev/null; then', + ' :', + ' elif [ ! -e "/proc/$_orca_pid/stat" ]; then', + ' printf "skip %s\\n" "$_orca_pid"', + ' _orca_skipped=$((_orca_skipped + 1))', + ' continue', + ' else', + ' printf "error start_time\\n"', + ' exit 1', + ' fi', ' _orca_after=${_orca_procstat##*) }', ' IFS=" " read -r _orca_dummy1 _orca_dummy2 _orca_dummy3 _orca_dummy4 _orca_dummy5 _orca_dummy6 _orca_dummy7 _orca_dummy8 _orca_dummy9 _orca_dummy10 _orca_dummy11 _orca_dummy12 _orca_dummy13 _orca_dummy14 _orca_dummy15 _orca_dummy16 _orca_dummy17 _orca_dummy18 _orca_dummy19 _orca_start _orca_rest <