diff --git a/common/changes/@rushstack/node-core-library/executable-modern-win11_2025-11-20-23-45.json b/common/changes/@rushstack/node-core-library/executable-modern-win11_2025-11-20-23-45.json new file mode 100644 index 00000000000..027def551f3 --- /dev/null +++ b/common/changes/@rushstack/node-core-library/executable-modern-win11_2025-11-20-23-45.json @@ -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" +} \ No newline at end of file diff --git a/libraries/node-core-library/src/Executable.ts b/libraries/node-core-library/src/Executable.ts index f7f73603205..97edafde57f 100644 --- a/libraries/node-core-library/src/Executable.ts +++ b/libraries/node-core-library/src/Executable.ts @@ -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 @@ -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*$` ); @@ -301,8 +299,7 @@ function parseProcessInfoEntry( existingProcessInfoById: Map, 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}`); @@ -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']; @@ -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> { @@ -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> { diff --git a/libraries/node-core-library/src/test/Executable.test.ts b/libraries/node-core-library/src/test/Executable.test.ts index 97da9ed6f95..2a6d6b6fac8 100644 --- a/libraries/node-core-library/src/test/Executable.test.ts +++ b/libraries/node-core-library/src/test/Executable.test.ts @@ -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)[] = [ @@ -388,6 +386,22 @@ describe('Executable process list', () => { ' 1 3 process1\n' ]; + 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); + expect(currentProcessInfo?.processName.startsWith('node')).toBe(true); + }); + + test('contains the current pid (async)', async () => { + const results: ReadonlyMap = 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 = parseProcessListOutput( WIN32_PROCESS_LIST_OUTPUT,