Files
orca/src/main/windows/windows-process-table-cim-scan.ts
T
Neil 7f034a182f docs(windows): correct why the process-tree addon is not installed on relay hosts (#16565)
The note said the package "ships no prebuilds". It does: the published 0.8.0
tarball carries build/Release/windows_process_tree.node, apparently an
accidentally published MSVC build directory (.obj and .tlog files ship with it).
The conclusion was right and the reason was wrong, so record what was actually
measured on a Windows SSH host with 1486 processes.

Installing it normally rebuilds from source, because the tarball carries a
binding.gyp and npm runs node-gyp regardless of what is already compiled inside.
That build fails with MSB8040 (Spectre-mitigated libraries) even on a host that
already has MSVC Build Tools 2022 -- the requirement our binding.gyp patch
deletes, and patches do not cross SSH.

Skipping the build keeps the tarball binary, which loads (it is N-API) but
predates the src/process.cc patch and still caps enumeration at 1024. On that
host it returned exactly 1024 rows with the querying process among the missing,
which the self-presence guard rejects -- so it would work on a quiet machine and
fail only under load, the shape of bug that survives testing.

Also records the measured cost of the fallback, since the table's 706ms figure
is from a 1050-process host and reads as more headroom than there is, and names
the fix for the tracked gap: ship our own patched .node as a relay asset, as
config/relay-assets already does for node-pty.
2026-08-26 02:59:08 -07:00

108 lines
4.0 KiB
TypeScript

import { runProcess } from '../../shared/child-process/run-process'
import { windowsPowerShellPath } from '../../shared/child-process/windows-system-binary'
import type { WindowsProcessRow } from './windows-process-table'
/**
* The `Get-CimInstance Win32_Process` scan, kept only for hosts where the
* native Toolhelp32 binding is not installed.
*
* Why this still exists after #15749 retired it: the relay bundle is deployed to
* SSH hosts that only ever receive `node-pty` and `@parcel/watcher`, so
* `@vscode/windows-process-tree` is absent there and every native read rejects.
* Callers read that as "no evidence", so a pane keeps whatever name node-pty
* reported -- the shell, usually -- instead of the agent running under it, for
* the life of the relay process. This restores the v1.4.188 answer on exactly
* those hosts; the local app ships the addon and never reaches this path.
*
* Measured on a Windows 11 SSH host with 1486 processes: 1.36s and 4.8MiB of
* JSON per scan, against the 3s / 8MiB limits below. Both limits match the
* pre-#15749 reader, so this is parity, but the headroom is thinner than the
* 706ms figure in docs/reference/windows-process-enumeration.md suggests.
*/
const WINDOWS_CIM_QUERY_TIMEOUT_MS = 3_000
const WINDOWS_CIM_MAX_OUTPUT_BYTES = 8 * 1024 * 1024
// Why JSON and not the `Key=Value` list form: CommandLine can itself contain
// CR/LF, so an argument could otherwise masquerade as another row's field.
const POWERSHELL_PROCESS_QUERY =
'[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ' +
'Get-CimInstance -ClassName Win32_Process -Property CommandLine,Name,ParentProcessId,ProcessId | ' +
'Select-Object CommandLine,Name,ParentProcessId,ProcessId | ' +
'ConvertTo-Json -Compress'
type CimProcessRow = {
CommandLine?: unknown
Name?: unknown
ParentProcessId?: unknown
ProcessId?: unknown
}
function fieldAsString(value: unknown): string {
if (typeof value === 'string') {
return value
}
return value === null || value === undefined ? '' : String(value)
}
function fieldAsNumber(value: unknown): number {
if (typeof value === 'number') {
return value
}
return typeof value === 'string' ? Number.parseInt(value, 10) : Number.NaN
}
export function parseWindowsCimProcessRows(stdout: string): WindowsProcessRow[] | null {
const trimmed = stdout.trim()
if (!trimmed) {
return []
}
let parsed: unknown
try {
parsed = JSON.parse(trimmed)
} catch {
return null
}
const items = Array.isArray(parsed) ? parsed : [parsed]
return items.flatMap((item) => {
if (!item || typeof item !== 'object') {
return []
}
const row = item as CimProcessRow
const pid = fieldAsNumber(row.ProcessId)
const ppid = fieldAsNumber(row.ParentProcessId)
if (!Number.isFinite(pid) || !Number.isFinite(ppid)) {
return []
}
const name = fieldAsString(row.Name)
// memoryBytes stays undefined: Win32_Process reports WorkingSetSize, but no
// caller reads it off this table and asking widens an already costly scan.
return [{ pid, ppid, name, command: fieldAsString(row.CommandLine) || name }]
})
}
/**
* Read the whole process table through PowerShell.
*
* Throws rather than returning `[]` on any failure: an empty table is a claim
* that nothing is running, and callers act on that by declaring a tree dead.
*/
export async function readWindowsProcessRowsWithCim(): Promise<WindowsProcessRow[]> {
const result = await runProcess({
program: windowsPowerShellPath(),
args: ['-NoProfile', '-NonInteractive', '-Command', POWERSHELL_PROCESS_QUERY],
timeoutMs: WINDOWS_CIM_QUERY_TIMEOUT_MS,
maxOutputBytes: WINDOWS_CIM_MAX_OUTPUT_BYTES
})
if (result.timedOut || result.code !== 0) {
throw new Error(
`windows process table CIM scan failed (code=${result.code} timedOut=${result.timedOut})`
)
}
const rows = parseWindowsCimProcessRows(result.stdout)
if (!rows || rows.length === 0) {
throw new Error('windows process table CIM scan returned no rows')
}
return rows
}