diff --git a/common/changes/@rushstack/node-core-library/IProcessInfo.commandLine_2026-01-04-04-29.json b/common/changes/@rushstack/node-core-library/IProcessInfo.commandLine_2026-01-04-04-29.json new file mode 100644 index 00000000000..eada1fa996b --- /dev/null +++ b/common/changes/@rushstack/node-core-library/IProcessInfo.commandLine_2026-01-04-04-29.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/node-core-library", + "comment": "Introduce a `commandLine` property to `IProcessInfo` that gets populated with the CLI that was passed into the process. In Node 24, the process name is set to \"MainThread\", so this restores some previous functionality from before Node 24.", + "type": "minor" + } + ], + "packageName": "@rushstack/node-core-library" +} \ No newline at end of file diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index cc01c92c873..31872acfdcf 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -620,6 +620,7 @@ export interface IProblemPattern { // @public export interface IProcessInfo { childProcessInfos: IProcessInfo[]; + commandLine?: string; parentProcessInfo: IProcessInfo | undefined; processId: number; processName: string; diff --git a/libraries/node-core-library/src/Executable.ts b/libraries/node-core-library/src/Executable.ts index 97edafde57f..29277c557b0 100644 --- a/libraries/node-core-library/src/Executable.ts +++ b/libraries/node-core-library/src/Executable.ts @@ -229,6 +229,14 @@ export interface IProcessInfo { */ processName: string; + /** + * The full command line of the process, when available. + * + * @remarks On some platforms this may be empty or truncated for kernel processes + * or when the OS does not expose the command line. + */ + commandLine?: string; + /** * The process ID. */ @@ -288,10 +296,11 @@ export function parseProcessListOutput( // PPID PID COMMAND // 51234 56784 process name const NAME_GROUP: 'name' = 'name'; +const COMMAND_LINE_GROUP: 'command' = 'command'; const PROCESS_ID_GROUP: 'pid' = 'pid'; const PARENT_PROCESS_ID_GROUP: 'ppid' = 'ppid'; const PROCESS_LIST_ENTRY_REGEX: RegExp = new RegExp( - `^\\s*(?<${PARENT_PROCESS_ID_GROUP}>\\d+)\\s+(?<${PROCESS_ID_GROUP}>\\d+)\\s+(?<${NAME_GROUP}>.+?)\\s*$` + `^\\s*(?<${PARENT_PROCESS_ID_GROUP}>\\d+)\\s+(?<${PROCESS_ID_GROUP}>\\d+)\\s+(?<${NAME_GROUP}>[^\\s]+)(?:\\s+(?<${COMMAND_LINE_GROUP}>.+))?\\s*$` ); function parseProcessInfoEntry( @@ -299,16 +308,50 @@ function parseProcessInfoEntry( existingProcessInfoById: Map, platform: NodeJS.Platform ): void { - const processListEntryRegex: RegExp = PROCESS_LIST_ENTRY_REGEX; - const match: RegExpMatchArray | null = line.match(processListEntryRegex); - if (!match?.groups) { - throw new InternalError(`Invalid process list entry: ${line}`); + let processName: string; + let commandLine: string | undefined; + let processId: number; + let parentProcessId: number; + + if (platform === 'win32') { + if (line.includes('\t')) { + // Tab-delimited output (PowerShell path with CommandLine) + const win32Match: RegExpMatchArray | null = line.match( + /^\s*(?\d+)\s+(?\d+)\s+(?[^\s]+)(?:\s+(?.+))?\s*$/ + ); + if (!win32Match?.groups) { + throw new InternalError(`Invalid process list entry: ${line}`); + } + processName = win32Match.groups.name; + const cmd: string | undefined = win32Match.groups.cmd; + commandLine = cmd && cmd.length > 0 ? cmd : undefined; + processId = parseInt(win32Match.groups.pid, 10); + parentProcessId = parseInt(win32Match.groups.ppid, 10); + } else { + // Legacy space-delimited listing: treat everything after pid as name, no command line + const tokens: string[] = line.trim().split(/\s+/); + if (tokens.length < 3) { + throw new InternalError(`Invalid process list entry: ${line}`); + } + const [ppidString, pidString, ...nameParts] = tokens; + processName = nameParts.join(' '); + commandLine = undefined; + processId = parseInt(pidString, 10); + parentProcessId = parseInt(ppidString, 10); + } + } else { + const processListEntryRegex: RegExp = PROCESS_LIST_ENTRY_REGEX; + const match: RegExpMatchArray | null = line.match(processListEntryRegex); + if (!match?.groups) { + throw new InternalError(`Invalid process list entry: ${line}`); + } + processName = match.groups[NAME_GROUP]; + const parsedCommandLine: string | undefined = match.groups[COMMAND_LINE_GROUP]; + commandLine = parsedCommandLine && parsedCommandLine.length > 0 ? parsedCommandLine : undefined; + processId = parseInt(match.groups[PROCESS_ID_GROUP], 10); + parentProcessId = parseInt(match.groups[PARENT_PROCESS_ID_GROUP], 10); } - const processName: string = match.groups[NAME_GROUP]; - const processId: number = parseInt(match.groups[PROCESS_ID_GROUP], 10); - const parentProcessId: number = parseInt(match.groups[PARENT_PROCESS_ID_GROUP], 10); - // Only care about the parent process if it is not the same as the current process. let parentProcessInfo: IProcessInfo | undefined; if (parentProcessId !== processId) { @@ -334,10 +377,16 @@ function parseProcessInfoEntry( parentProcessInfo, childProcessInfos: [] }; + if (commandLine !== undefined) { + processInfo.commandLine = commandLine; + } existingProcessInfoById.set(processId, processInfo); } else { // Update placeholder entry processInfo.processName = processName; + if (commandLine !== undefined) { + processInfo.commandLine = commandLine; + } processInfo.parentProcessInfo = parentProcessInfo; } @@ -368,11 +417,11 @@ function getProcessListProcessOptions(): ICommandLineOptions { if (OS_PLATFORM === 'win32') { command = 'powershell.exe'; // Order of declared properties sets the order of the output. - // Put name last to simplify parsing, since it can contain spaces. + // Emit tab-delimited columns to allow the command line to contain spaces. args = [ '-NoProfile', '-Command', - `'PPID PID Name'; Get-CimInstance Win32_Process | % { '{0} {1} {2}' -f $_.ParentProcessId, $_.ProcessId, $_.Name }` + `'PPID\`tPID\`tName\`tCommandLine'; Get-CimInstance Win32_Process | % { '{0}\`t{1}\`t{2}\`t{3}' -f $_.ParentProcessId, $_.ProcessId, $_.Name, ($_.CommandLine -replace "\`t", " ") }` ]; } else { command = 'ps'; @@ -382,7 +431,8 @@ function getProcessListProcessOptions(): ICommandLineOptions { // Order of declared properties sets the order of the output. We will // need to request the "comm" property last in order to ensure that the // process names are not truncated on certain platforms - args = ['-Awo', 'ppid,pid,comm']; + // Include both the comm (thread name) and args (full command line) columns. + args = ['-Awwxo', 'ppid,pid,comm,args']; } return { path: command, args }; } diff --git a/libraries/node-core-library/src/test/Executable.test.ts b/libraries/node-core-library/src/test/Executable.test.ts index 40ec24d6b0e..47f7edba141 100644 --- a/libraries/node-core-library/src/test/Executable.test.ts +++ b/libraries/node-core-library/src/test/Executable.test.ts @@ -386,13 +386,51 @@ describe('Executable process list', () => { ' 1 3 process1\n' ]; + test('captures command line when present (win32)', () => { + const processListMap: Map = parseProcessListOutput( + [ + 'PPID PID NAME\r\n', + '0 100 node.exe\tC:\\Program Files\\nodejs\\node.exe --foo --bar=baz\r\n', + '100 101 helper.exe\thelper.exe --opt arg\r\n' + ], + 'win32' + ); + const results: IProcessInfo[] = [...processListMap.values()].sort((a, b) => a.processId - b.processId); + expect(results.length).toBe(3); + expect(results).toMatchSnapshot(); + }); + + test('captures command line when present (linux)', () => { + const processListMap: Map = parseProcessListOutput( + [ + 'PPID PID COMMAND\n', + '0 10 node /usr/bin/node --foo --bar=baz\n', + '10 11 child /tmp/child.sh arg1\n' + ], + 'linux' + ); + const results: IProcessInfo[] = [...processListMap.values()].sort((a, b) => a.processId - b.processId); + expect(results.length).toBe(3); + expect(results).toMatchSnapshot(); + }); + + test('contains the current command line (sync)', () => { + const results: ReadonlyMap = Executable.getProcessInfoById(); + const currentProcessInfo: IProcessInfo | undefined = results.get(process.pid); + expect(currentProcessInfo).toBeDefined(); + // Ensure we recorded some command line text for the current process + expect(currentProcessInfo?.commandLine?.length ?? 0).toBeGreaterThan(0); + }); + test('contains the current pid (sync)', () => { const results: ReadonlyMap = Executable.getProcessInfoById(); const currentProcessInfo: IProcessInfo | undefined = results.get(process.pid); expect(currentProcessInfo).toBeDefined(); expect(currentProcessInfo?.parentProcessInfo?.processId).toEqual(process.ppid); - // TODO: Fix parsing of process name as "MainThread" for Node 24 - expect(currentProcessInfo?.processName).toMatch(/(node(\.exe)|MainThread)?$/i); + const processIdentity: string = `${currentProcessInfo?.processName ?? ''} ${ + currentProcessInfo?.commandLine ?? '' + }`; + expect(processIdentity).toMatch(/node(\.exe)?/i); }); test('contains the current pid (async)', async () => { @@ -400,8 +438,10 @@ describe('Executable process list', () => { const currentProcessInfo: IProcessInfo | undefined = results.get(process.pid); expect(currentProcessInfo).toBeDefined(); expect(currentProcessInfo?.parentProcessInfo?.processId).toEqual(process.ppid); - // TODO: Fix parsing of process name as "MainThread" for Node 24 - expect(currentProcessInfo?.processName).toMatch(/(node(\.exe)|MainThread)?$/i); + const processIdentity: string = `${currentProcessInfo?.processName ?? ''} ${ + currentProcessInfo?.commandLine ?? '' + }`; + expect(processIdentity).toMatch(/node(\.exe)?/i); }); test('parses win32 output', () => { diff --git a/libraries/node-core-library/src/test/__snapshots__/Executable.test.ts.snap b/libraries/node-core-library/src/test/__snapshots__/Executable.test.ts.snap index ed188d4c581..fe69f530d76 100644 --- a/libraries/node-core-library/src/test/__snapshots__/Executable.test.ts.snap +++ b/libraries/node-core-library/src/test/__snapshots__/Executable.test.ts.snap @@ -1,5 +1,147 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Executable process list captures command line when present (linux) 1`] = ` +Array [ + Object { + "childProcessInfos": Array [ + Object { + "childProcessInfos": Array [ + Object { + "childProcessInfos": Array [], + "commandLine": "/tmp/child.sh arg1", + "parentProcessInfo": [Circular], + "processId": 11, + "processName": "child", + }, + ], + "commandLine": "/usr/bin/node --foo --bar=baz", + "parentProcessInfo": [Circular], + "processId": 10, + "processName": "node", + }, + ], + "parentProcessInfo": undefined, + "processId": 0, + "processName": "", + }, + Object { + "childProcessInfos": Array [ + Object { + "childProcessInfos": Array [], + "commandLine": "/tmp/child.sh arg1", + "parentProcessInfo": [Circular], + "processId": 11, + "processName": "child", + }, + ], + "commandLine": "/usr/bin/node --foo --bar=baz", + "parentProcessInfo": Object { + "childProcessInfos": Array [ + [Circular], + ], + "parentProcessInfo": undefined, + "processId": 0, + "processName": "", + }, + "processId": 10, + "processName": "node", + }, + Object { + "childProcessInfos": Array [], + "commandLine": "/tmp/child.sh arg1", + "parentProcessInfo": Object { + "childProcessInfos": Array [ + [Circular], + ], + "commandLine": "/usr/bin/node --foo --bar=baz", + "parentProcessInfo": Object { + "childProcessInfos": Array [ + [Circular], + ], + "parentProcessInfo": undefined, + "processId": 0, + "processName": "", + }, + "processId": 10, + "processName": "node", + }, + "processId": 11, + "processName": "child", + }, +] +`; + +exports[`Executable process list captures command line when present (win32) 1`] = ` +Array [ + Object { + "childProcessInfos": Array [ + Object { + "childProcessInfos": Array [ + Object { + "childProcessInfos": Array [], + "commandLine": "helper.exe --opt arg", + "parentProcessInfo": [Circular], + "processId": 101, + "processName": "helper.exe", + }, + ], + "commandLine": "C:\\\\Program Files\\\\nodejs\\\\node.exe --foo --bar=baz", + "parentProcessInfo": [Circular], + "processId": 100, + "processName": "node.exe", + }, + ], + "parentProcessInfo": undefined, + "processId": 0, + "processName": "", + }, + Object { + "childProcessInfos": Array [ + Object { + "childProcessInfos": Array [], + "commandLine": "helper.exe --opt arg", + "parentProcessInfo": [Circular], + "processId": 101, + "processName": "helper.exe", + }, + ], + "commandLine": "C:\\\\Program Files\\\\nodejs\\\\node.exe --foo --bar=baz", + "parentProcessInfo": Object { + "childProcessInfos": Array [ + [Circular], + ], + "parentProcessInfo": undefined, + "processId": 0, + "processName": "", + }, + "processId": 100, + "processName": "node.exe", + }, + Object { + "childProcessInfos": Array [], + "commandLine": "helper.exe --opt arg", + "parentProcessInfo": Object { + "childProcessInfos": Array [ + [Circular], + ], + "commandLine": "C:\\\\Program Files\\\\nodejs\\\\node.exe --foo --bar=baz", + "parentProcessInfo": Object { + "childProcessInfos": Array [ + [Circular], + ], + "parentProcessInfo": undefined, + "processId": 0, + "processName": "", + }, + "processId": 100, + "processName": "node.exe", + }, + "processId": 101, + "processName": "helper.exe", + }, +] +`; + exports[`Executable process list parses unix output 1`] = ` Object { "childProcessInfos": Array [ @@ -9,6 +151,7 @@ Object { "childProcessInfos": Array [ Object { "childProcessInfos": Array [], + "commandLine": " ", "parentProcessInfo": [Circular], "processId": 4, "processName": "process2", @@ -43,6 +186,7 @@ Object { "childProcessInfos": Array [ Object { "childProcessInfos": Array [], + "commandLine": " ", "parentProcessInfo": [Circular], "processId": 4, "processName": "process2", @@ -77,6 +221,7 @@ Object { "childProcessInfos": Array [ Object { "childProcessInfos": Array [], + "commandLine": " ", "parentProcessInfo": [Circular], "processId": 4, "processName": "process2", @@ -111,6 +256,7 @@ Object { exports[`Executable process list parses unix output 4`] = ` Object { "childProcessInfos": Array [], + "commandLine": " ", "parentProcessInfo": Object { "childProcessInfos": Array [ [Circular], @@ -153,6 +299,7 @@ Object { "childProcessInfos": Array [ Object { "childProcessInfos": Array [], + "commandLine": " ", "parentProcessInfo": [Circular], "processId": 4, "processName": "process2", @@ -189,6 +336,7 @@ Object { "childProcessInfos": Array [ Object { "childProcessInfos": Array [], + "commandLine": " ", "parentProcessInfo": [Circular], "processId": 4, "processName": "process2", @@ -223,6 +371,7 @@ Object { "childProcessInfos": Array [ Object { "childProcessInfos": Array [], + "commandLine": " ", "parentProcessInfo": [Circular], "processId": 4, "processName": "process2", @@ -257,6 +406,7 @@ Object { "childProcessInfos": Array [ Object { "childProcessInfos": Array [], + "commandLine": " ", "parentProcessInfo": [Circular], "processId": 4, "processName": "process2", @@ -291,6 +441,7 @@ Object { exports[`Executable process list parses unix stream output 4`] = ` Object { "childProcessInfos": Array [], + "commandLine": " ", "parentProcessInfo": Object { "childProcessInfos": Array [ [Circular], @@ -333,6 +484,7 @@ Object { "childProcessInfos": Array [ Object { "childProcessInfos": Array [], + "commandLine": " ", "parentProcessInfo": [Circular], "processId": 4, "processName": "process2",