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
6 changes: 5 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ jobs:
- name: Setup repo
uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v1
uses: actions/setup-node@v4
with:
# Pinned: the legacy xo@0.20 toolchain crashes on modern Node
# (util.isDate was removed). Revisit when the toolchain is updated.
node-version: 14
- name: Install dev dependencies
run: |
npm install --only=dev
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node: [current, 16, 14, 12, 10, 8, 6, 4]
node: [current, 22, 20, 18]
steps:
- name: Setup repo
uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node: [current, 16, 14, 12, 10, 8, 6, 4]
node: [current, 22, 20, 18]
steps:
- name: Setup repo
uses: actions/checkout@v3
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/test-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ on:

jobs:
test:
# windows-latest is Windows Server 2025, which ships without wmic, so the
# integration tests here run against the PowerShell fallback in lib/get.js.
runs-on: windows-latest
name: AVA & TSD & Benchmark & Codecov
strategy:
fail-fast: false
matrix:
node: [current, 16, 14, 12, 10, 8, 6, 4]
node: [current, 22, 20, 18]
steps:
- name: Setup repo
uses: actions/checkout@v3
Expand Down
4 changes: 3 additions & 1 deletion lib/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ function run(cmd, args, options, done) {
ch.on('error', function(err) {
if (executed) return;
executed = true;
done(new Error(err));
// 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(err);
});

ch.on('close', function(code) {
Expand Down
19 changes: 19 additions & 0 deletions lib/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ var platformToMethod = {
var methodToRequireFn = {
ps: () => require('./ps'),
wmic: () => require('./wmic'),
powershell: () => require('./powershell'),
};

var platform = os.platform();
Expand All @@ -36,6 +37,24 @@ function get(callback) {
' is not supported yet, please open an issue (https://github.com/simonepri/pidtree)'
)
);
return;
}

// On Windows wmic has been removed from recent versions (e.g. Windows 11
// 24H2 and Windows Server 2025). Try it first since it is faster when
// present, and fall back to PowerShell when the binary cannot be found.
if (method === 'wmic') {
var wmic = methodToRequireFn.wmic();
wmic(function(err, list) {
if (err && err.code === 'ENOENT') {
var powershell = methodToRequireFn.powershell();
powershell(callback);
return;
}

callback(err, list);
});
return;
}

var list = methodToRequireFn[method]();
Expand Down
70 changes: 70 additions & 0 deletions lib/powershell.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use strict';

var os = require('os');
var bin = require('./bin');

// 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 (e.g. "Preparing modules for first use.") to stderr as
// CLIXML, which would otherwise be treated as an error.
var 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.
var 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(err, list)
*/
function powershell(callback) {
var args = ['-NoProfile', '-NonInteractive', '-EncodedCommand', ENCODED];
var options = {windowsHide: true};
bin('powershell', args, options, function(err, stdout, code) {
if (err) {
callback(err);
return;
}

if (code !== 0) {
callback(
new Error('pidtree powershell command exited with code ' + code)
);
return;
}

// Example of stdout
//
// 0 777
// 777 778
// 0 779

try {
stdout = stdout.split(os.EOL);

var list = [];
for (var i = 0; i < stdout.length; i++) {
stdout[i] = stdout[i].trim();
if (!stdout[i]) continue;
var parts = stdout[i].split(/\s+/);
var ppid = parseInt(parts[0], 10); // PPID
var pid = parseInt(parts[1], 10); // PID
if (isNaN(ppid) || isNaN(pid)) continue;
list.push([ppid, pid]);
}

callback(null, list);
} catch (error) {
callback(error);
}
});
}

module.exports = powershell;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pidtree",
"version": "0.6.0",
"version": "0.6.1",
"description": "Cross platform children list of a PID",
"license": "MIT",
"homepage": "http://github.com/simonepri/pidtree#readme",
Expand Down
83 changes: 83 additions & 0 deletions test/get.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import test from 'ava';
import mockery from 'mockery';

import pify from 'pify';

test.before(() => {
mockery.enable({
warnOnReplace: false,
warnOnUnregistered: false,
useCleanCache: true,
});
});

test.beforeEach(() => {
mockery.resetCache();
});

test.after(() => {
mockery.disable();
});

function osMock(platform) {
return {
EOL: '\n',
platform: () => platform,
type: () => 'type',
release: () => 'release',
};
}

test('should use wmic on Windows when it is available', async t => {
mockery.registerMock('os', osMock('win32'));
mockery.registerMock('./wmic', cb => cb(null, [[0, 100], [100, 101]]));
mockery.registerMock('./powershell', () => {
t.fail('powershell should not be used when wmic succeeds');
});

const get = require('../lib/get');

const result = await pify(get)();
t.deepEqual(result, [[0, 100], [100, 101]]);

mockery.deregisterMock('os');
mockery.deregisterMock('./wmic');
mockery.deregisterMock('./powershell');
});

test('should fall back to powershell when wmic is missing on Windows', async t => {
const enoent = new Error('spawn wmic ENOENT');
enoent.code = 'ENOENT';

mockery.registerMock('os', osMock('win32'));
mockery.registerMock('./wmic', cb => cb(enoent));
mockery.registerMock('./powershell', cb => cb(null, [[0, 777], [777, 778]]));

const get = require('../lib/get');

const result = await pify(get)();
t.deepEqual(result, [[0, 777], [777, 778]]);

mockery.deregisterMock('os');
mockery.deregisterMock('./wmic');
mockery.deregisterMock('./powershell');
});

test('should not fall back to powershell on a non ENOENT wmic error', async t => {
const boom = new Error('wmic exploded');

mockery.registerMock('os', osMock('win32'));
mockery.registerMock('./wmic', cb => cb(boom));
mockery.registerMock('./powershell', () => {
t.fail('powershell should not be used on a generic wmic error');
});

const get = require('../lib/get');

const err = await t.throws(pify(get)());
t.is(err.message, 'wmic exploded');

mockery.deregisterMock('os');
mockery.deregisterMock('./wmic');
mockery.deregisterMock('./powershell');
});
66 changes: 66 additions & 0 deletions test/powershell.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import test from 'ava';
import mockery from 'mockery';

import pify from 'pify';

import mocks from './helpers/mocks';

test.before(() => {
mockery.enable({
warnOnReplace: false,
warnOnUnregistered: false,
useCleanCache: true,
});
});

test.beforeEach(() => {
mockery.resetCache();
});

test.after(() => {
mockery.disable();
});

test('should parse powershell output on Windows', async t => {
const stdout = '0 777\r\n777 778\r\n0 779\r\n';

mockery.registerMock('child_process', {
spawn: () => mocks.spawn(stdout, '', null, 0, null),
});
mockery.registerMock('os', {
EOL: '\n',
platform: () => 'win32',
type: () => 'type',
release: () => 'release',
});

const powershell = require('../lib/powershell');

const result = await pify(powershell)();
t.deepEqual(result, [[0, 777], [777, 778], [0, 779]]);

mockery.deregisterMock('child_process');
mockery.deregisterMock('os');
});

test('should ignore non numeric lines in powershell output', async t => {
const stdout = '\r\n0 777\r\nsome banner line\r\n777 778\r\n';

mockery.registerMock('child_process', {
spawn: () => mocks.spawn(stdout, '', null, 0, null),
});
mockery.registerMock('os', {
EOL: '\n',
platform: () => 'win32',
type: () => 'type',
release: () => 'release',
});

const powershell = require('../lib/powershell');

const result = await pify(powershell)();
t.deepEqual(result, [[0, 777], [777, 778]]);

mockery.deregisterMock('child_process');
mockery.deregisterMock('os');
});
Loading