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
31 changes: 19 additions & 12 deletions src/processTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,8 @@ export function getProcesses(one: (pid: number, ppid: number, command: string, a
function lines(callback: (a: string) => void) {
let unfinished = ''; // unfinished last line of chunk
return (data: string | Buffer) => {
const lines = data.toString().split(/\r?\n/);
const lines = (unfinished + data.toString()).split(/\r?\n/);
const finishedLines = lines.slice(0, lines.length - 1);
finishedLines[0] = unfinished + finishedLines[0]; // complete previous unfinished line
unfinished = lines[lines.length - 1]; // remember unfinished last line of this chunk for next round
for (const s of finishedLines) {
callback(s);
Expand All @@ -72,18 +71,26 @@ export function getProcesses(one: (pid: number, ppid: number, command: string, a

if (process.platform === 'win32') {

// attributes columns are in alphabetic order!
const CMD_PAT = /^(.*)\s+([0-9]+)\.[0-9]+[+-][0-9]+\s+([0-9]+)\s+([0-9]+)$/;

const wmic = join(process.env['WINDIR'] || 'C:\\Windows', 'System32', 'wbem', 'WMIC.exe');
proc = spawn(wmic, [ 'process', 'get', 'CommandLine,CreationDate,ParentProcessId,ProcessId' ]);
const CMD_PAT = /^(.*)\s+([0-9]+)\s+([0-9]+)$/;

const command = [
"$ErrorActionPreference = 'Stop';",
'if ([Console]::OutputEncoding.CodePage -ne 65001) {',
"if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') {",
"throw 'UTF-8 process discovery is restricted by policy. Attach using hostName and port instead.';",
'}',
'[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;',
'}',
'Get-CimInstance Win32_Process | ForEach-Object { "$($_.CommandLine) $($_.ParentProcessId) $($_.ProcessId)" }'
].join(' ');
const powershell = join(process.env['WINDIR'] || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
proc = spawn(powershell, [ '-NoLogo', '-NoProfile', '-NonInteractive', '-Command', command ], { windowsHide: true });
proc.stdout.setEncoding('utf8');
proc.stdout.on('data', lines(line => {
let matches = CMD_PAT.exec(line.trim());
if (matches && matches.length === 5) {
const pid = Number(matches[4]);
const ppid = Number(matches[3]);
const date = Number(matches[2]);
if (matches && matches.length === 4) {
const pid = Number(matches[3]);
const ppid = Number(matches[2]);
let args = matches[1].trim();
if (!isNaN(pid) && !isNaN(ppid) && args) {
let command = args;
Expand All @@ -102,7 +109,7 @@ export function getProcesses(one: (pid: number, ppid: number, command: string, a
args = '';
}
}
one(pid, ppid, command, args, date);
one(pid, ppid, command, args);
}
}
}));
Expand Down
146 changes: 146 additions & 0 deletions test/processTree.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

import * as assert from "assert";
import * as childProcess from "child_process";
import { EventEmitter } from "events";
import { join } from "path";
import { PassThrough } from "stream";
import { getProcesses } from "../src/processTree";

suite("Windows process discovery", () => {
const windowsTest = process.platform === "win32" ? test : test.skip;
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform")!;
const spawnDescriptor = Object.getOwnPropertyDescriptor(childProcess, "spawn")!;
const commandLine = '"C:\\\u5de5\u5177\\jdk\\bin\\java.exe" -agentlib:jdwp=transport=dt_socket,address=5005 \u5e94\u7528';
let child: EventEmitter & { stdout: PassThrough; stderr: PassThrough };
let executable: string;
let spawnArgs: string[];
let spawnOptions: childProcess.SpawnOptions | undefined;

setup(() => {
child = Object.assign(new EventEmitter(), { stdout: new PassThrough(), stderr: new PassThrough() });
executable = "";
spawnArgs = [];
spawnOptions = undefined;
Object.defineProperty(process, "platform", { ...platformDescriptor, value: "win32" });
Object.defineProperty(childProcess, "spawn", {
...spawnDescriptor,
value: (command: string, args: string[], options?: childProcess.SpawnOptions) => {
executable = command;
spawnArgs = args;
spawnOptions = options;
return child;
},
});
});

teardown(() => {
Object.defineProperty(childProcess, "spawn", spawnDescriptor);
Object.defineProperty(process, "platform", platformDescriptor);
child.stdout.destroy();
child.stderr.destroy();
});

test("runs Windows PowerShell without profiles, banners, or interaction", async () => {
const discovery = getProcesses(() => { });
child.emit("close", 0, null);
await discovery;

assert.strictEqual(executable, join(process.env.WINDIR || "C:\\Windows",
"System32", "WindowsPowerShell", "v1.0", "powershell.exe"));
assert.deepStrictEqual(spawnArgs.slice(0, -1), ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command"]);
assert.deepStrictEqual(spawnOptions, { windowsHide: true });
});

test("preserves Unicode command lines split across UTF-8 chunks", async () => {
const processes: { pid: number; ppid: number; command: string; args: string }[] = [];
const discovery = getProcesses((pid, ppid, command, args) => processes.push({ pid, ppid, command, args }));
const output = Buffer.from(`${commandLine} 7 42\r\n`);
child.stdout.write(output.slice(0, 5));
child.stdout.write(output.slice(5, output.length - 1));
child.stdout.write(output.slice(output.length - 1));
child.emit("close", 0, null);
await discovery;

assert.deepStrictEqual(processes, [{
pid: 42,
ppid: 7,
command: "C:\\\u5de5\u5177\\jdk\\bin\\java.exe",
args: "-agentlib:jdwp=transport=dt_socket,address=5005 \u5e94\u7528",
}]);
});

test("surfaces PowerShell errors instead of reporting an empty process list", async () => {
const discovery = getProcesses(() => assert.fail("Unexpected process"));
child.stderr.write("UTF-8 process discovery is restricted by policy.");
child.emit("close", 1, null);
await assert.rejects(discovery, /UTF-8 process discovery is restricted by policy/);
});

test("rejects unsuccessful PowerShell exits without stderr", async () => {
const discovery = getProcesses(() => { });
child.emit("close", 1, null);
await assert.rejects(discovery, /process terminated with exit code: 1/);
});

test("rejects PowerShell launch failures", async () => {
const discovery = getProcesses(() => { });
const error = new Error("PowerShell could not be started");
child.emit("error", error);
await assert.rejects(discovery, (actual: unknown) => actual === error);
});

async function runScript(prefix: string): Promise<childProcess.SpawnSyncReturns<string>> {
const discovery = getProcesses(() => { });
child.emit("close", 0, null);
await discovery;
const result = childProcess.spawnSync(executable,
[...spawnArgs.slice(0, -1), prefix + spawnArgs[spawnArgs.length - 1]],
{ encoding: "utf8", timeout: 10000, windowsHide: true });
assert.ifError(result.error);
return result;
}

function fixture(codePage: number, languageMode: "FullLanguage" | "ConstrainedLanguage"): string {
return [
`[Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding(${codePage});`,
`$record = [pscustomobject]@{ CommandLine = '${commandLine}'; ParentProcessId = 7; ProcessId = 42 };`,
"function Get-CimInstance { $record };",
`$ExecutionContext.SessionState.LanguageMode = '${languageMode}'; `,
].join(" ");
}

windowsTest("converts a legacy code page to UTF-8 in FullLanguage mode", async function() {
this.timeout(15000);
const result = await runScript(fixture(437, "FullLanguage"));
assert.strictEqual(result.status, 0, result.stderr);
assert.strictEqual(result.stderr, "");
assert.strictEqual(result.stdout, `${commandLine} 7 42\r\n`);
});

windowsTest("preserves UTF-8 output in ConstrainedLanguage mode", async function() {
this.timeout(15000);
const result = await runScript(fixture(65001, "ConstrainedLanguage"));
assert.strictEqual(result.status, 0, result.stderr);
assert.strictEqual(result.stderr, "");
assert.strictEqual(result.stdout, `${commandLine} 7 42\r\n`);
});

windowsTest("rejects legacy encoding in ConstrainedLanguage mode with actionable guidance", async function() {
this.timeout(15000);
const result = await runScript(fixture(437, "ConstrainedLanguage"));
assert.strictEqual(result.status, 1);
assert.strictEqual(result.stdout, "");
assert.ok(result.stderr.replace(/\s+/g, " ").includes(
"UTF-8 process discovery is restricted by policy. Attach using hostName and port instead."), result.stderr);
});

windowsTest("terminates on non-terminating CIM errors", async function() {
this.timeout(15000);
const result = await runScript("function Get-CimInstance { Write-Error 'CIM discovery failed' }; ");
assert.strictEqual(result.status, 1);
assert.strictEqual(result.stdout, "");
assert.ok(result.stderr.includes("CIM discovery failed"), result.stderr);
});
});
Loading