diff --git a/src/features/tmux-installer/main/infrastructure/runtime/TmuxPlatformCommandExecutor.ts b/src/features/tmux-installer/main/infrastructure/runtime/TmuxPlatformCommandExecutor.ts index a3409800..739fea0b 100644 --- a/src/features/tmux-installer/main/infrastructure/runtime/TmuxPlatformCommandExecutor.ts +++ b/src/features/tmux-installer/main/infrastructure/runtime/TmuxPlatformCommandExecutor.ts @@ -28,11 +28,36 @@ export interface RuntimeProcessTableRow { pid: number; ppid: number; command: string; + cpuPercent?: number; + rssBytes?: number; } export function parseRuntimeProcessTable(output: string): RuntimeProcessTableRow[] { const rows: RuntimeProcessTableRow[] = []; for (const line of output.split('\n')) { + const enrichedMatch = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line); + if (enrichedMatch) { + const pid = Number.parseInt(enrichedMatch[1], 10); + const ppid = Number.parseInt(enrichedMatch[2], 10); + const cpuPercent = Number(enrichedMatch[3]); + const rssKb = Number(enrichedMatch[4]); + const command = enrichedMatch[5]?.trim() ?? ''; + if ( + Number.isFinite(pid) && + pid > 0 && + Number.isFinite(ppid) && + ppid >= 0 && + Number.isFinite(cpuPercent) && + cpuPercent >= 0 && + Number.isFinite(rssKb) && + rssKb >= 0 && + command.length > 0 + ) { + rows.push({ pid, ppid, command, cpuPercent, rssBytes: Math.round(rssKb * 1024) }); + continue; + } + } + const match = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line); if (!match) continue; @@ -169,7 +194,12 @@ export class TmuxPlatformCommandExecutor { async listRuntimeProcesses(): Promise { const result = process.platform === 'win32' - ? await this.#wslService.execInPreferredDistro(['ps', '-ax', '-o', 'pid=,ppid=,command=']) + ? await this.#wslService.execInPreferredDistro([ + 'ps', + '-ax', + '-o', + 'pid=,ppid=,pcpu=,rss=,command=', + ]) : await this.#execNativePs(); if (result.exitCode !== 0) { throw new Error(result.stderr || 'Failed to list runtime processes'); @@ -251,7 +281,7 @@ export class TmuxPlatformCommandExecutor { return new Promise((resolve) => { execFile( 'ps', - ['-ax', '-o', 'pid=,ppid=,command='], + ['-ax', '-o', 'pid=,ppid=,pcpu=,rss=,command='], { env: process.env, timeout: 3_000, maxBuffer: 2 * 1024 * 1024 }, (error, stdout, stderr) => { const errorCode = diff --git a/src/features/tmux-installer/main/infrastructure/runtime/__tests__/TmuxPlatformCommandExecutor.test.ts b/src/features/tmux-installer/main/infrastructure/runtime/__tests__/TmuxPlatformCommandExecutor.test.ts index bd9d3166..f8327f1c 100644 --- a/src/features/tmux-installer/main/infrastructure/runtime/__tests__/TmuxPlatformCommandExecutor.test.ts +++ b/src/features/tmux-installer/main/infrastructure/runtime/__tests__/TmuxPlatformCommandExecutor.test.ts @@ -104,7 +104,7 @@ describe('TmuxPlatformCommandExecutor', () => { setPlatform('win32'); const execInPreferredDistro = vi.fn(async () => ({ exitCode: 0, - stdout: ' 42 1 opencode runtime --team-name demo\n', + stdout: ' 42 1 7.5 128 opencode runtime --team-name demo\n', stderr: '', })); const executor = new TmuxPlatformCommandExecutor( @@ -116,9 +116,20 @@ describe('TmuxPlatformCommandExecutor', () => { ); await expect(executor.listRuntimeProcesses()).resolves.toEqual([ - { pid: 42, ppid: 1, command: 'opencode runtime --team-name demo' }, + { + pid: 42, + ppid: 1, + command: 'opencode runtime --team-name demo', + cpuPercent: 7.5, + rssBytes: 131_072, + }, + ]); + expect(execInPreferredDistro).toHaveBeenCalledWith([ + 'ps', + '-ax', + '-o', + 'pid=,ppid=,pcpu=,rss=,command=', ]); - expect(execInPreferredDistro).toHaveBeenCalledWith(['ps', '-ax', '-o', 'pid=,ppid=,command=']); expect(childProcess.execFile).not.toHaveBeenCalled(); }); }); diff --git a/test/main/features/tmux-installer/TmuxPlatformCommandExecutor.test.ts b/test/main/features/tmux-installer/TmuxPlatformCommandExecutor.test.ts index 76ddfc43..c9ffb095 100644 --- a/test/main/features/tmux-installer/TmuxPlatformCommandExecutor.test.ts +++ b/test/main/features/tmux-installer/TmuxPlatformCommandExecutor.test.ts @@ -1,6 +1,5 @@ -import { describe, expect, it } from 'vitest'; - import { parseRuntimeProcessTable } from '@features/tmux-installer/main'; +import { describe, expect, it } from 'vitest'; describe('parseRuntimeProcessTable', () => { it('parses pid, ppid and command rows', () => { @@ -12,6 +11,15 @@ describe('parseRuntimeProcessTable', () => { ]); }); + it('parses optional cpu and rss columns', () => { + expect( + parseRuntimeProcessTable(' 10 1 3.5 120000 /bin/zsh\n 11 10 0.1 42 node demo') + ).toEqual([ + { pid: 10, ppid: 1, command: '/bin/zsh', cpuPercent: 3.5, rssBytes: 122_880_000 }, + { pid: 11, ppid: 10, command: 'node demo', cpuPercent: 0.1, rssBytes: 43_008 }, + ]); + }); + it('skips malformed rows', () => { expect(parseRuntimeProcessTable('bad\n 0 1 nope\n 12 0 /bin/node')).toEqual([ { pid: 12, ppid: 0, command: '/bin/node' }, diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index 254d1dfe..bfcf798f 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -620,6 +620,9 @@ type TeamProvisioningServicePrivateHarness = { applyProcessBootstrapTransportOverlay: ( input: Record ) => Record; + readProcessUsageStatsByPid: ( + pids: readonly number[] + ) => Promise>; }; function privateHarness(svc: TeamProvisioningService): TeamProvisioningServicePrivateHarness { @@ -4747,8 +4750,9 @@ describe('TeamProvisioningService', () => { }; vi.mocked(pidusage).mockResolvedValueOnce(usageByPid); - const first = await (svc as any).readProcessUsageStatsByPid([111]); - const second = await (svc as any).readProcessUsageStatsByPid([111]); + const harness = privateHarness(svc); + const first = await harness.readProcessUsageStatsByPid([111]); + const second = await harness.readProcessUsageStatsByPid([111]); expect(pidusage).toHaveBeenCalledTimes(1); expect(pidusage).toHaveBeenCalledWith([111], EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);