Skip to content

Commit af9fa7e

Browse files
authored
Merge pull request #4489 from D4N14L/user/danade/FixProcessList
[node-core-library] Fix parsing of process name on MacOS
2 parents 3347c2c + 6794a8b commit af9fa7e

File tree

3 files changed

+61
-26
lines changed

3 files changed

+61
-26
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": "Fix Executable.getProcessInfoBy* methods truncating the process name on MacOS",
6+
"type": "patch"
7+
}
8+
],
9+
"packageName": "@rushstack/node-core-library"
10+
}

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

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4-
import * as child_process from 'child_process';
54
import * as os from 'os';
5+
import * as child_process from 'child_process';
66
import * as path from 'path';
77
import { EnvironmentMap } from './EnvironmentMap';
88

@@ -11,6 +11,8 @@ import { PosixModeBits } from './PosixModeBits';
1111
import { Text } from './Text';
1212
import { InternalError } from './InternalError';
1313

14+
const OS_PLATFORM: NodeJS.Platform = os.platform();
15+
1416
/**
1517
* Typings for one of the streams inside IExecutableSpawnSyncOptions.stdio.
1618
* @public
@@ -221,29 +223,33 @@ export interface IProcessInfo {
221223
}
222224

223225
export async function parseProcessListOutputAsync(
224-
stream: NodeJS.ReadableStream
226+
stream: NodeJS.ReadableStream,
227+
platform: NodeJS.Platform = OS_PLATFORM
225228
): Promise<Map<number, IProcessInfo>> {
226229
const processInfoById: Map<number, IProcessInfo> = new Map<number, IProcessInfo>();
227230
let seenHeaders: boolean = false;
228231
for await (const line of Text.readLinesFromIterableAsync(stream, { ignoreEmptyLines: true })) {
229232
if (!seenHeaders) {
230233
seenHeaders = true;
231234
} else {
232-
parseProcessInfoEntry(line, processInfoById);
235+
parseProcessInfoEntry(line, processInfoById, platform);
233236
}
234237
}
235238
return processInfoById;
236239
}
237240

238-
// eslint-disable-next-line @rushstack/no-new-null
239-
export function parseProcessListOutput(output: Iterable<string | null>): Map<number, IProcessInfo> {
241+
export function parseProcessListOutput(
242+
// eslint-disable-next-line @rushstack/no-new-null
243+
output: Iterable<string | null>,
244+
platform: NodeJS.Platform = OS_PLATFORM
245+
): Map<number, IProcessInfo> {
240246
const processInfoById: Map<number, IProcessInfo> = new Map<number, IProcessInfo>();
241247
let seenHeaders: boolean = false;
242248
for (const line of Text.readLinesFromIterable(output, { ignoreEmptyLines: true })) {
243249
if (!seenHeaders) {
244250
seenHeaders = true;
245251
} else {
246-
parseProcessInfoEntry(line, processInfoById);
252+
parseProcessInfoEntry(line, processInfoById, platform);
247253
}
248254
}
249255
return processInfoById;
@@ -253,18 +259,28 @@ export function parseProcessListOutput(output: Iterable<string | null>): Map<num
253259
// Name ParentProcessId ProcessId
254260
// process name 1234 5678
255261
// unix format:
256-
// COMMAND PPID PID
257-
// process name 51234 56784
262+
// PPID PID COMMAND
263+
// 51234 56784 process name
258264
const NAME_GROUP: 'name' = 'name';
259265
const PROCESS_ID_GROUP: 'pid' = 'pid';
260266
const PARENT_PROCESS_ID_GROUP: 'ppid' = 'ppid';
261267
// eslint-disable-next-line @rushstack/security/no-unsafe-regexp
262-
const PROCESS_LIST_ENTRY_REGEX: RegExp = new RegExp(
268+
const PROCESS_LIST_ENTRY_REGEX_WIN32: RegExp = new RegExp(
263269
`^(?<${NAME_GROUP}>.+?)\\s+(?<${PARENT_PROCESS_ID_GROUP}>\\d+)\\s+(?<${PROCESS_ID_GROUP}>\\d+)\\s*$`
264270
);
271+
// eslint-disable-next-line @rushstack/security/no-unsafe-regexp
272+
const PROCESS_LIST_ENTRY_REGEX_UNIX: RegExp = new RegExp(
273+
`^\\s*(?<${PARENT_PROCESS_ID_GROUP}>\\d+)\\s+(?<${PROCESS_ID_GROUP}>\\d+)\\s+(?<${NAME_GROUP}>.+?)\\s*$`
274+
);
265275

266-
function parseProcessInfoEntry(line: string, existingProcessInfoById: Map<number, IProcessInfo>): void {
267-
const match: RegExpMatchArray | null = line.match(PROCESS_LIST_ENTRY_REGEX);
276+
function parseProcessInfoEntry(
277+
line: string,
278+
existingProcessInfoById: Map<number, IProcessInfo>,
279+
platform: NodeJS.Platform
280+
): void {
281+
const processListEntryRegex: RegExp =
282+
platform === 'win32' ? PROCESS_LIST_ENTRY_REGEX_WIN32 : PROCESS_LIST_ENTRY_REGEX_UNIX;
283+
const match: RegExpMatchArray | null = line.match(processListEntryRegex);
268284
if (!match?.groups) {
269285
throw new InternalError(`Invalid process list entry: ${line}`);
270286
}
@@ -326,8 +342,6 @@ function convertToProcessInfoByNameMap(
326342
return processInfoByNameMap;
327343
}
328344

329-
const OS_PLATFORM: NodeJS.Platform = os.platform();
330-
331345
function getProcessListProcessOptions(): ICommandLineOptions {
332346
let command: string;
333347
let args: string[];
@@ -338,10 +352,12 @@ function getProcessListProcessOptions(): ICommandLineOptions {
338352
} else {
339353
command = 'ps';
340354
// -A: Select all processes
355+
// -w: Wide format
341356
// -o: User-defined format
342-
// Order of declared properties impacts the order of the output, so match
343-
// the order of wmic.exe output
344-
args = ['-Ao', 'comm,ppid,pid'];
357+
// Order of declared properties impacts the order of the output. We will
358+
// need to request the "comm" property last in order to ensure that the
359+
// process names are not truncated on certain platforms
360+
args = ['-Awo', 'ppid,pid,comm'];
345361
}
346362
return { path: command, args };
347363
}

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

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -341,22 +341,26 @@ describe('Executable process list', () => {
341341
];
342342

343343
const UNIX_PROCESS_LIST_OUTPUT: (string | null)[] = [
344-
'COMMAND PPID PID\n',
344+
'PPID PID COMMAND\n',
345345
// Test that the parser can handle referencing a parent that doesn't exist
346-
'init 0 1\n',
347-
'process2 ',
346+
' 0 1 init\n',
348347
// Test that the parser can handle a line that is truncated in the middle of a field
349348
// Test that the parser can handle an entry referencing a parent that hasn't been seen yet
350-
' 2 4\n',
351-
'process0 1 2\n',
349+
// Test that the parser can handle whitespace at the end of the process name.
350+
' 2 4',
351+
' process2 \n',
352+
' 1 2 process0\n',
352353
// Test that the parser can handle empty strings
353354
'',
354355
// Test children handling when multiple entries reference the same parent
355-
'process1 1 3\n'
356+
' 1 3 process1\n'
356357
];
357358

358359
test('parses win32 output', () => {
359-
const processListMap: Map<number, IProcessInfo> = parseProcessListOutput(WIN32_PROCESS_LIST_OUTPUT);
360+
const processListMap: Map<number, IProcessInfo> = parseProcessListOutput(
361+
WIN32_PROCESS_LIST_OUTPUT,
362+
'win32'
363+
);
360364
const results: IProcessInfo[] = [...processListMap.values()].sort();
361365

362366
// Expect 7 because we reference a parent that doesn't exist
@@ -379,7 +383,8 @@ describe('Executable process list', () => {
379383

380384
test('parses win32 stream output', async () => {
381385
const processListMap: Map<number, IProcessInfo> = await parseProcessListOutputAsync(
382-
Readable.from(WIN32_PROCESS_LIST_OUTPUT)
386+
Readable.from(WIN32_PROCESS_LIST_OUTPUT),
387+
'win32'
383388
);
384389
const results: IProcessInfo[] = [...processListMap.values()].sort();
385390

@@ -402,7 +407,10 @@ describe('Executable process list', () => {
402407
});
403408

404409
test('parses unix output', () => {
405-
const processListMap: Map<number, IProcessInfo> = parseProcessListOutput(UNIX_PROCESS_LIST_OUTPUT);
410+
const processListMap: Map<number, IProcessInfo> = parseProcessListOutput(
411+
UNIX_PROCESS_LIST_OUTPUT,
412+
'linux'
413+
);
406414
const results: IProcessInfo[] = [...processListMap.values()].sort();
407415

408416
// Expect 5 because we reference a parent that doesn't exist
@@ -423,7 +431,8 @@ describe('Executable process list', () => {
423431

424432
test('parses unix stream output', async () => {
425433
const processListMap: Map<number, IProcessInfo> = await parseProcessListOutputAsync(
426-
Readable.from(UNIX_PROCESS_LIST_OUTPUT)
434+
Readable.from(UNIX_PROCESS_LIST_OUTPUT),
435+
'linux'
427436
);
428437
const results: IProcessInfo[] = [...processListMap.values()].sort();
429438

0 commit comments

Comments
 (0)