first commit
This commit is contained in:
+65
@@ -0,0 +1,65 @@
|
||||
import {spawn} from 'node:child_process';
|
||||
|
||||
/**
|
||||
* Strips known-benign noise from a command's stderr.
|
||||
* @param {string} stderr
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
export function stripStderr(stderr) {
|
||||
if (!stderr) return;
|
||||
stderr = stderr.trim();
|
||||
// Strip bogus screen size error.
|
||||
// See https://github.com/microsoft/vscode/issues/98590
|
||||
const regex = /your \d+x\d+ screen size is bogus\. expect trouble/gi;
|
||||
stderr = stderr.replaceAll(regex, '');
|
||||
|
||||
return stderr.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a binary and read its stdout.
|
||||
* @param {string} cmd The name of the binary to spawn.
|
||||
* @param {string[]} args The arguments for the binary.
|
||||
* @param {object} [options] Optional options for the spawn function.
|
||||
* @param {Function} done done(err, stdout, code)
|
||||
*/
|
||||
export function run(cmd, args, options, done) {
|
||||
if (typeof options === 'function') {
|
||||
done = options;
|
||||
options = undefined;
|
||||
}
|
||||
|
||||
let executed = false;
|
||||
const child = spawn(cmd, args, options);
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
if (executed) return;
|
||||
executed = true;
|
||||
// Pass the original error through so callers can inspect err.code
|
||||
// (e.g. 'ENOENT' when the binary is missing) to decide on a fallback.
|
||||
done(error);
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (executed) return;
|
||||
executed = true;
|
||||
|
||||
stderr = stripStderr(stderr);
|
||||
if (stderr) {
|
||||
done(new Error(stderr));
|
||||
return;
|
||||
}
|
||||
|
||||
done(null, stdout, code);
|
||||
});
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import os from 'node:os';
|
||||
import {ps} from './ps.js';
|
||||
import {wmic} from './wmic.js';
|
||||
import {powershell} from './powershell.js';
|
||||
|
||||
const platformToMethod = {
|
||||
darwin: 'ps',
|
||||
sunos: 'ps',
|
||||
freebsd: 'ps',
|
||||
netbsd: 'ps',
|
||||
win: 'win',
|
||||
linux: 'ps',
|
||||
aix: 'ps',
|
||||
};
|
||||
|
||||
let platform = os.platform();
|
||||
if (platform.startsWith('win')) {
|
||||
platform = 'win';
|
||||
}
|
||||
|
||||
const method = platformToMethod[platform];
|
||||
|
||||
/**
|
||||
* Lists the system pids on Windows. wmic has been removed from recent versions
|
||||
* (Windows 11 24H2, Windows Server 2025), so it is tried first (it is faster
|
||||
* when present) and PowerShell is used as a fallback when it is missing.
|
||||
* Exported for testing.
|
||||
* @param {Function} callback callback(err, list)
|
||||
* @param {Function} [wmicFn] Injectable wmic backend, used for testing.
|
||||
* @param {Function} [powershellFn] Injectable PowerShell backend, for testing.
|
||||
*/
|
||||
export function getWindows(callback, wmicFn = wmic, powershellFn = powershell) {
|
||||
wmicFn((error, list) => {
|
||||
if (error && error.code === 'ENOENT') {
|
||||
powershellFn(callback);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(error, list);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of all the pids of the system.
|
||||
* @param {Function} callback Called when the list is ready.
|
||||
*/
|
||||
export function get(callback) {
|
||||
if (method === undefined) {
|
||||
callback(
|
||||
new Error(
|
||||
os.platform() +
|
||||
' is not supported yet, please open an issue (https://github.com/simonepri/pidtree)',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'win') {
|
||||
getWindows(callback);
|
||||
return;
|
||||
}
|
||||
|
||||
ps(callback);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Parses the `<ppid> <pid>` output produced by the ps/wmic/powershell backends
|
||||
* into a list of `[ppid, pid]` pairs. The header row and any non-numeric lines
|
||||
* are skipped, and all common line endings (\n, \r\n, \r\r\n) are handled.
|
||||
* @param {string} stdout The raw command output.
|
||||
* @returns {Array<[number, number]>}
|
||||
*/
|
||||
export function parse(stdout) {
|
||||
const list = [];
|
||||
for (const rawLine of stdout.split(/\r*\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line) continue;
|
||||
|
||||
const [rawPpid, rawPid] = line.split(/\s+/);
|
||||
const ppid = Number.parseInt(rawPpid, 10);
|
||||
const pid = Number.parseInt(rawPid, 10);
|
||||
// Skips the header row and any stray output.
|
||||
if (Number.isNaN(ppid) || Number.isNaN(pid)) continue;
|
||||
|
||||
list.push([ppid, pid]);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import {get} from './get.js';
|
||||
|
||||
/**
|
||||
* Get the list of children and grandchildren pids of the given PID.
|
||||
* @param {number|string} pid A PID. If -1 will return all the pids.
|
||||
* @param {object} [options] Optional options object.
|
||||
* @param {boolean} [options.root=false] Include the provided PID in the list.
|
||||
* @param {boolean} [options.advanced=false] Returns a list of objects in the
|
||||
* format {ppid: X, pid: Y}.
|
||||
* @param {Function} callback callback(err, list) Called when the list is ready.
|
||||
*/
|
||||
export function pidtreeCallback(pid, options, callback) {
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
if (typeof options !== 'object' || options === null) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
pid = Number.parseInt(pid, 10);
|
||||
if (Number.isNaN(pid) || pid < -1) {
|
||||
callback(new TypeError('The pid provided is invalid'));
|
||||
return;
|
||||
}
|
||||
|
||||
get((error, list) => {
|
||||
if (error) {
|
||||
callback(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user wants the whole list just return it.
|
||||
if (pid === -1) {
|
||||
const all = list.map((entry) =>
|
||||
options.advanced ? {ppid: entry[0], pid: entry[1]} : entry[1],
|
||||
);
|
||||
callback(null, all);
|
||||
return;
|
||||
}
|
||||
|
||||
let root;
|
||||
for (const entry of list) {
|
||||
if (entry[1] === pid) {
|
||||
root = options.advanced ? {ppid: entry[0], pid} : pid;
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry[0] === pid) {
|
||||
// Special pids like 0 on *nix.
|
||||
root = options.advanced ? {pid} : pid;
|
||||
}
|
||||
}
|
||||
|
||||
if (root === undefined) {
|
||||
callback(new Error('No matching pid found'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the adjacency Hash Map (pid -> [children of pid]).
|
||||
const tree = {};
|
||||
for (const [parentPid, childPid] of list) {
|
||||
if (tree[parentPid]) {
|
||||
tree[parentPid].push(childPid);
|
||||
} else {
|
||||
tree[parentPid] = [childPid];
|
||||
}
|
||||
}
|
||||
|
||||
// Starting from the PID provided by the user, traverse the tree using the
|
||||
// adjacency Hash Map until the whole subtree is visited. Each pid
|
||||
// encountered while visiting is added to the pids array.
|
||||
const pids = [root];
|
||||
let index = 0;
|
||||
while (index < pids.length) {
|
||||
const current = options.advanced ? pids[index].pid : pids[index];
|
||||
index++;
|
||||
const children = tree[current];
|
||||
if (!children) continue;
|
||||
for (const childPid of children) {
|
||||
pids.push(options.advanced ? {ppid: current, pid: childPid} : childPid);
|
||||
}
|
||||
|
||||
delete tree[current];
|
||||
}
|
||||
|
||||
if (!options.root) {
|
||||
pids.shift(); // Remove root.
|
||||
}
|
||||
|
||||
callback(null, pids);
|
||||
});
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import {run as defaultRun} from './bin.js';
|
||||
import {parse} from './parse.js';
|
||||
|
||||
// PowerShell command used to list every process as "<ppid> <pid>" lines.
|
||||
// Get-CimInstance is the supported replacement for the removed wmic utility
|
||||
// (see https://github.com/simonepri/pidtree/issues/20). $ProgressPreference is
|
||||
// silenced so PowerShell does not serialize its progress stream to stderr.
|
||||
const COMMAND =
|
||||
"$ProgressPreference = 'SilentlyContinue'; " +
|
||||
'Get-CimInstance -ClassName Win32_Process | ' +
|
||||
'ForEach-Object { "$($_.ParentProcessId) $($_.ProcessId)" }';
|
||||
|
||||
// The command is passed through -EncodedCommand (Base64 of the UTF-16LE
|
||||
// string) to avoid any cross-shell argument quoting issues on Windows.
|
||||
const ENCODED = Buffer.from(COMMAND, 'utf16le').toString('base64');
|
||||
|
||||
/**
|
||||
* Gets the list of all the pids of the system through PowerShell.
|
||||
* Used as a fallback on Windows installations where wmic is not available
|
||||
* (e.g. Windows 11 24H2 and Windows Server 2025).
|
||||
* @param {Function} callback callback(err, list)
|
||||
* @param {Function} [run] Injectable spawner, used for testing.
|
||||
*/
|
||||
export function powershell(callback, run = defaultRun) {
|
||||
const args = ['-NoProfile', '-NonInteractive', '-EncodedCommand', ENCODED];
|
||||
const options = {windowsHide: true};
|
||||
|
||||
// Example of stdout
|
||||
//
|
||||
// 0 777
|
||||
// 777 778
|
||||
run('powershell', args, options, (error, stdout, code) => {
|
||||
if (error) {
|
||||
callback(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
callback(
|
||||
new Error('pidtree powershell command exited with code ' + code),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
callback(null, parse(stdout));
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import {run as defaultRun} from './bin.js';
|
||||
import {parse} from './parse.js';
|
||||
|
||||
/**
|
||||
* Gets the list of all the pids of the system through the ps command.
|
||||
* @param {Function} callback callback(err, list)
|
||||
* @param {Function} [run] Injectable spawner, used for testing.
|
||||
*/
|
||||
export function ps(callback, run = defaultRun) {
|
||||
// Example of stdout
|
||||
//
|
||||
// PPID PID
|
||||
// 1 430
|
||||
// 430 432
|
||||
run('ps', ['-A', '-o', 'ppid,pid'], (error, stdout, code) => {
|
||||
if (error) {
|
||||
callback(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
callback(new Error('pidtree ps command exited with code ' + code));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
callback(null, parse(stdout));
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import {run as defaultRun} from './bin.js';
|
||||
import {parse} from './parse.js';
|
||||
|
||||
/**
|
||||
* Gets the list of all the pids of the system through the wmic command.
|
||||
* @param {Function} callback callback(err, list)
|
||||
* @param {Function} [run] Injectable spawner, used for testing.
|
||||
*/
|
||||
export function wmic(callback, run = defaultRun) {
|
||||
const args = ['PROCESS', 'get', 'ParentProcessId,ProcessId'];
|
||||
const options = {windowsHide: true, windowsVerbatimArguments: true};
|
||||
|
||||
// Example of stdout
|
||||
//
|
||||
// ParentProcessId ProcessId
|
||||
// 0 777
|
||||
run('wmic', args, options, (error, stdout, code) => {
|
||||
if (error) {
|
||||
callback(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
callback(new Error('pidtree wmic command exited with code ' + code));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
callback(null, parse(stdout));
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user