Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/node-core-library",
"comment": "Update `Executable.getProcessInfoBy*` APIs to use PowerShell on Windows to support latest Windows 11 versions.",
"type": "minor"
}
],
"packageName": "@rushstack/node-core-library"
}
32 changes: 17 additions & 15 deletions libraries/node-core-library/src/Executable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ interface ICommandLineOptions {
/**
* Process information sourced from the system. This process info is sourced differently depending
* on the operating system:
* - On Windows, this uses the `wmic.exe` utility.
* - On Windows, this uses `powershell.exe` and a scriptlet to retrieve process information.
* The wmic utility that was previously used is no longer present on the latest Windows versions.
* - On Unix, this uses the `ps` utility.
*
* @public
Expand Down Expand Up @@ -281,18 +282,15 @@ export function parseProcessListOutput(
}

// win32 format:
// Name ParentProcessId ProcessId
// process name 1234 5678
// PPID PID NAME
// 51234 56784 process name
// unix format:
// PPID PID COMMAND
// 51234 56784 process name
const NAME_GROUP: 'name' = 'name';
const PROCESS_ID_GROUP: 'pid' = 'pid';
const PARENT_PROCESS_ID_GROUP: 'ppid' = 'ppid';
const PROCESS_LIST_ENTRY_REGEX_WIN32: RegExp = new RegExp(
`^(?<${NAME_GROUP}>.+?)\\s+(?<${PARENT_PROCESS_ID_GROUP}>\\d+)\\s+(?<${PROCESS_ID_GROUP}>\\d+)\\s*$`
);
const PROCESS_LIST_ENTRY_REGEX_UNIX: RegExp = new RegExp(
const PROCESS_LIST_ENTRY_REGEX: RegExp = new RegExp(
`^\\s*(?<${PARENT_PROCESS_ID_GROUP}>\\d+)\\s+(?<${PROCESS_ID_GROUP}>\\d+)\\s+(?<${NAME_GROUP}>.+?)\\s*$`
);

Expand All @@ -301,8 +299,7 @@ function parseProcessInfoEntry(
existingProcessInfoById: Map<number, IProcessInfo>,
platform: NodeJS.Platform
): void {
const processListEntryRegex: RegExp =
platform === 'win32' ? PROCESS_LIST_ENTRY_REGEX_WIN32 : PROCESS_LIST_ENTRY_REGEX_UNIX;
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}`);
Expand Down Expand Up @@ -369,15 +366,20 @@ function getProcessListProcessOptions(): ICommandLineOptions {
let command: string;
let args: string[];
if (OS_PLATFORM === 'win32') {
command = 'wmic.exe';
// Order of declared properties does not impact the order of the output
args = ['process', 'get', 'Name,ParentProcessId,ProcessId'];
command = 'powershell.exe';
// Order of declared properties sets the order of the output.
// Put name last to simplify parsing, since it can contain spaces.
args = [
'-NoProfile',
'-Command',
`'PPID PID Name'; Get-CimInstance Win32_Process | % { '{0} {1} {2}' -f $_.ParentProcessId, $_.ProcessId, $_.Name }`
];
} else {
command = 'ps';
// -A: Select all processes
// -w: Wide format
// -o: User-defined format
// Order of declared properties impacts the order of the output. We will
// 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'];
Expand Down Expand Up @@ -654,7 +656,7 @@ export class Executable {
* Get the list of processes currently running on the system, keyed by the process ID.
*
* @remarks The underlying implementation depends on the operating system:
* - On Windows, this uses the `wmic.exe` utility.
* - On Windows, this uses `powershell.exe` and the `Get-CimInstance` cmdlet.
* - On Unix, this uses the `ps` utility.
*/
public static async getProcessInfoByIdAsync(): Promise<Map<number, IProcessInfo>> {
Expand Down Expand Up @@ -693,7 +695,7 @@ export class Executable {
* with the same name will be grouped.
*
* @remarks The underlying implementation depends on the operating system:
* - On Windows, this uses the `wmic.exe` utility.
* - On Windows, this uses `powershell.exe` and the `Get-CimInstance` cmdlet.
* - On Unix, this uses the `ps` utility.
*/
public static async getProcessInfoByNameAsync(): Promise<Map<string, IProcessInfo[]>> {
Expand Down
32 changes: 23 additions & 9 deletions libraries/node-core-library/src/test/Executable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,22 +354,20 @@ describe('Executable process tests', () => {

describe('Executable process list', () => {
const WIN32_PROCESS_LIST_OUTPUT: (string | null)[] = [
'Name ParentProcessId ProcessId\r\r\n',
'PPID PID NAME\r\n',
// Test that the parser can handle referencing a parent that is the same as the current process
// Test that the parser can handle multiple return characters
'System Idle Process 0 0\r\r\n',
'System 0 1\r\r\n',
'executable2.exe ',
// Test that the parser can handle a line that is truncated in the middle of a field
'0 0 System Idle Process\r\n',
'0 1 System\r\n',
// Test that the parser can handle an entry referencing a parent that hasn't been seen yet
' 2 4\r\r\n',
'executable0.exe 1 2\r\r\n',
'2 4 executable2.exe\r\n',
'1 2 executable0.exe\r\n',
// Test children handling when multiple entries reference the same parent
'executable1.exe 1 3\r\r\n',
'1 3 executable1.exe\r\n',
// Test that the parser can handle empty strings
'',
// Test that the parser can handle referencing a parent that doesn't exist
'executable3.exe 6 5\r\r\n'
'6 5 executable3.exe\r\n'
];

const UNIX_PROCESS_LIST_OUTPUT: (string | null)[] = [
Expand All @@ -388,6 +386,22 @@ describe('Executable process list', () => {
' 1 3 process1\n'
];

test('contains the current pid (sync)', () => {
const results: ReadonlyMap<number, IProcessInfo> = Executable.getProcessInfoById();
const currentProcessInfo: IProcessInfo | undefined = results.get(process.pid);
expect(currentProcessInfo).toBeDefined();
expect(currentProcessInfo?.parentProcessInfo?.processId).toEqual(process.ppid);
expect(currentProcessInfo?.processName.startsWith('node')).toBe(true);
});

test('contains the current pid (async)', async () => {
const results: ReadonlyMap<number, IProcessInfo> = await Executable.getProcessInfoByIdAsync();
const currentProcessInfo: IProcessInfo | undefined = results.get(process.pid);
expect(currentProcessInfo).toBeDefined();
expect(currentProcessInfo?.parentProcessInfo?.processId).toEqual(process.ppid);
expect(currentProcessInfo?.processName.startsWith('node')).toBe(true);
});

test('parses win32 output', () => {
const processListMap: Map<number, IProcessInfo> = parseProcessListOutput(
WIN32_PROCESS_LIST_OUTPUT,
Expand Down
Loading