Skip to content

Commit 936352d

Browse files
committed
Add a IProcessInfo.commandLine property.
1 parent 9a31ab4 commit 936352d

File tree

5 files changed

+269
-16
lines changed

5 files changed

+269
-16
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/node-core-library",
5+
"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.",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@rushstack/node-core-library"
10+
}

common/reviews/api/node-core-library.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,7 @@ export interface IProblemPattern {
620620
// @public
621621
export interface IProcessInfo {
622622
childProcessInfos: IProcessInfo[];
623+
commandLine?: string;
623624
parentProcessInfo: IProcessInfo | undefined;
624625
processId: number;
625626
processName: string;

libraries/node-core-library/src/Executable.ts

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,14 @@ export interface IProcessInfo {
229229
*/
230230
processName: string;
231231

232+
/**
233+
* The full command line of the process, when available.
234+
*
235+
* @remarks On some platforms this may be empty or truncated for kernel processes
236+
* or when the OS does not expose the command line.
237+
*/
238+
commandLine?: string;
239+
232240
/**
233241
* The process ID.
234242
*/
@@ -288,27 +296,62 @@ export function parseProcessListOutput(
288296
// PPID PID COMMAND
289297
// 51234 56784 process name
290298
const NAME_GROUP: 'name' = 'name';
299+
const COMMAND_LINE_GROUP: 'command' = 'command';
291300
const PROCESS_ID_GROUP: 'pid' = 'pid';
292301
const PARENT_PROCESS_ID_GROUP: 'ppid' = 'ppid';
293302
const PROCESS_LIST_ENTRY_REGEX: RegExp = new RegExp(
294-
`^\\s*(?<${PARENT_PROCESS_ID_GROUP}>\\d+)\\s+(?<${PROCESS_ID_GROUP}>\\d+)\\s+(?<${NAME_GROUP}>.+?)\\s*$`
303+
`^\\s*(?<${PARENT_PROCESS_ID_GROUP}>\\d+)\\s+(?<${PROCESS_ID_GROUP}>\\d+)\\s+(?<${NAME_GROUP}>[^\\s]+)(?:\\s+(?<${COMMAND_LINE_GROUP}>.+))?\\s*$`
295304
);
296305

297306
function parseProcessInfoEntry(
298307
line: string,
299308
existingProcessInfoById: Map<number, IProcessInfo>,
300309
platform: NodeJS.Platform
301310
): void {
302-
const processListEntryRegex: RegExp = PROCESS_LIST_ENTRY_REGEX;
303-
const match: RegExpMatchArray | null = line.match(processListEntryRegex);
304-
if (!match?.groups) {
305-
throw new InternalError(`Invalid process list entry: ${line}`);
311+
let processName: string;
312+
let commandLine: string | undefined;
313+
let processId: number;
314+
let parentProcessId: number;
315+
316+
if (platform === 'win32') {
317+
if (line.includes('\t')) {
318+
// Tab-delimited output (PowerShell path with CommandLine)
319+
const win32Match: RegExpMatchArray | null = line.match(
320+
/^\s*(?<ppid>\d+)\s+(?<pid>\d+)\s+(?<name>[^\s]+)(?:\s+(?<cmd>.+))?\s*$/
321+
);
322+
if (!win32Match?.groups) {
323+
throw new InternalError(`Invalid process list entry: ${line}`);
324+
}
325+
processName = win32Match.groups.name;
326+
const cmd: string | undefined = win32Match.groups.cmd;
327+
commandLine = cmd && cmd.length > 0 ? cmd : undefined;
328+
processId = parseInt(win32Match.groups.pid, 10);
329+
parentProcessId = parseInt(win32Match.groups.ppid, 10);
330+
} else {
331+
// Legacy space-delimited listing: treat everything after pid as name, no command line
332+
const tokens: string[] = line.trim().split(/\s+/);
333+
if (tokens.length < 3) {
334+
throw new InternalError(`Invalid process list entry: ${line}`);
335+
}
336+
const [ppidString, pidString, ...nameParts] = tokens;
337+
processName = nameParts.join(' ');
338+
commandLine = undefined;
339+
processId = parseInt(pidString, 10);
340+
parentProcessId = parseInt(ppidString, 10);
341+
}
342+
} else {
343+
const processListEntryRegex: RegExp = PROCESS_LIST_ENTRY_REGEX;
344+
const match: RegExpMatchArray | null = line.match(processListEntryRegex);
345+
if (!match?.groups) {
346+
throw new InternalError(`Invalid process list entry: ${line}`);
347+
}
348+
processName = match.groups[NAME_GROUP];
349+
const parsedCommandLine: string | undefined = match.groups[COMMAND_LINE_GROUP];
350+
commandLine = parsedCommandLine && parsedCommandLine.length > 0 ? parsedCommandLine : undefined;
351+
processId = parseInt(match.groups[PROCESS_ID_GROUP], 10);
352+
parentProcessId = parseInt(match.groups[PARENT_PROCESS_ID_GROUP], 10);
306353
}
307354

308-
const processName: string = match.groups[NAME_GROUP];
309-
const processId: number = parseInt(match.groups[PROCESS_ID_GROUP], 10);
310-
const parentProcessId: number = parseInt(match.groups[PARENT_PROCESS_ID_GROUP], 10);
311-
312355
// Only care about the parent process if it is not the same as the current process.
313356
let parentProcessInfo: IProcessInfo | undefined;
314357
if (parentProcessId !== processId) {
@@ -334,10 +377,16 @@ function parseProcessInfoEntry(
334377
parentProcessInfo,
335378
childProcessInfos: []
336379
};
380+
if (commandLine !== undefined) {
381+
processInfo.commandLine = commandLine;
382+
}
337383
existingProcessInfoById.set(processId, processInfo);
338384
} else {
339385
// Update placeholder entry
340386
processInfo.processName = processName;
387+
if (commandLine !== undefined) {
388+
processInfo.commandLine = commandLine;
389+
}
341390
processInfo.parentProcessInfo = parentProcessInfo;
342391
}
343392

@@ -368,11 +417,11 @@ function getProcessListProcessOptions(): ICommandLineOptions {
368417
if (OS_PLATFORM === 'win32') {
369418
command = 'powershell.exe';
370419
// Order of declared properties sets the order of the output.
371-
// Put name last to simplify parsing, since it can contain spaces.
420+
// Emit tab-delimited columns to allow the command line to contain spaces.
372421
args = [
373422
'-NoProfile',
374423
'-Command',
375-
`'PPID PID Name'; Get-CimInstance Win32_Process | % { '{0} {1} {2}' -f $_.ParentProcessId, $_.ProcessId, $_.Name }`
424+
`'PPID\`tPID\`tName\`tCommandLine'; Get-CimInstance Win32_Process | % { '{0}\`t{1}\`t{2}\`t{3}' -f $_.ParentProcessId, $_.ProcessId, $_.Name, ($_.CommandLine -replace "\`t", " ") }`
376425
];
377426
} else {
378427
command = 'ps';
@@ -382,7 +431,8 @@ function getProcessListProcessOptions(): ICommandLineOptions {
382431
// Order of declared properties sets the order of the output. We will
383432
// need to request the "comm" property last in order to ensure that the
384433
// process names are not truncated on certain platforms
385-
args = ['-Awo', 'ppid,pid,comm'];
434+
// Include both the comm (thread name) and args (full command line) columns.
435+
args = ['-Awwxo', 'ppid,pid,comm,args'];
386436
}
387437
return { path: command, args };
388438
}

libraries/node-core-library/src/test/Executable.test.ts

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -386,22 +386,62 @@ describe('Executable process list', () => {
386386
' 1 3 process1\n'
387387
];
388388

389+
test('captures command line when present (win32)', () => {
390+
const processListMap: Map<number, IProcessInfo> = parseProcessListOutput(
391+
[
392+
'PPID PID NAME\r\n',
393+
'0 100 node.exe\tC:\\Program Files\\nodejs\\node.exe --foo --bar=baz\r\n',
394+
'100 101 helper.exe\thelper.exe --opt arg\r\n'
395+
],
396+
'win32'
397+
);
398+
const results: IProcessInfo[] = [...processListMap.values()].sort((a, b) => a.processId - b.processId);
399+
expect(results.length).toBe(3);
400+
expect(results).toMatchSnapshot();
401+
});
402+
403+
test('captures command line when present (linux)', () => {
404+
const processListMap: Map<number, IProcessInfo> = parseProcessListOutput(
405+
[
406+
'PPID PID COMMAND\n',
407+
'0 10 node /usr/bin/node --foo --bar=baz\n',
408+
'10 11 child /tmp/child.sh arg1\n'
409+
],
410+
'linux'
411+
);
412+
const results: IProcessInfo[] = [...processListMap.values()].sort((a, b) => a.processId - b.processId);
413+
expect(results.length).toBe(3);
414+
expect(results).toMatchSnapshot();
415+
});
416+
417+
test('contains the current command line (sync)', () => {
418+
const results: ReadonlyMap<number, IProcessInfo> = Executable.getProcessInfoById();
419+
const currentProcessInfo: IProcessInfo | undefined = results.get(process.pid);
420+
expect(currentProcessInfo).toBeDefined();
421+
// Ensure we recorded some command line text for the current process
422+
expect(currentProcessInfo?.commandLine?.length ?? 0).toBeGreaterThan(0);
423+
});
424+
389425
test('contains the current pid (sync)', () => {
390426
const results: ReadonlyMap<number, IProcessInfo> = Executable.getProcessInfoById();
391427
const currentProcessInfo: IProcessInfo | undefined = results.get(process.pid);
392428
expect(currentProcessInfo).toBeDefined();
393429
expect(currentProcessInfo?.parentProcessInfo?.processId).toEqual(process.ppid);
394-
// TODO: Fix parsing of process name as "MainThread" for Node 24
395-
expect(currentProcessInfo?.processName).toMatch(/(node(\.exe)|MainThread)?$/i);
430+
const processIdentity: string = `${currentProcessInfo?.processName ?? ''} ${
431+
currentProcessInfo?.commandLine ?? ''
432+
}`;
433+
expect(processIdentity).toMatch(/node(\.exe)?/i);
396434
});
397435

398436
test('contains the current pid (async)', async () => {
399437
const results: ReadonlyMap<number, IProcessInfo> = await Executable.getProcessInfoByIdAsync();
400438
const currentProcessInfo: IProcessInfo | undefined = results.get(process.pid);
401439
expect(currentProcessInfo).toBeDefined();
402440
expect(currentProcessInfo?.parentProcessInfo?.processId).toEqual(process.ppid);
403-
// TODO: Fix parsing of process name as "MainThread" for Node 24
404-
expect(currentProcessInfo?.processName).toMatch(/(node(\.exe)|MainThread)?$/i);
441+
const processIdentity: string = `${currentProcessInfo?.processName ?? ''} ${
442+
currentProcessInfo?.commandLine ?? ''
443+
}`;
444+
expect(processIdentity).toMatch(/node(\.exe)?/i);
405445
});
406446

407447
test('parses win32 output', () => {

0 commit comments

Comments
 (0)