From cf9b3e883d4a9f55f4b29edddeb8a15101bd6b99 Mon Sep 17 00:00:00 2001
From: Simone Primarosa
Date: Mon, 8 Jun 2026 13:17:05 +0200
Subject: [PATCH] feat!: rewrite as a pure ESM package (Node.js >= 18)
Modernizes the project for the first 1.0 release:
- Pure ESM with default and named exports (import pidtree from 'pidtree')
- Drops CommonJS and Node.js < 18
- Refreshed toolchain: ava 6, c8, xo 2, tsd; tests use dependency
injection and a shared parser instead of mockery and stream mocks
- Keeps the Windows wmic -> PowerShell fallback and zero runtime deps
BREAKING CHANGE: pidtree is now ESM-only and requires Node.js >= 18.
CommonJS consumers and older Node.js should stay on pidtree@0.6.
Co-Authored-By: Gavin Aiken
---
.github/workflows/lint.yml | 13 +--
.github/workflows/test-macos.yml | 28 ++----
.github/workflows/test-ubuntu.yml | 30 +++---
.github/workflows/test-windows.yml | 33 ++-----
bin/pidtree.js | 102 +++++++++----------
index.d.ts | 83 ++++++++--------
index.js | 48 +++------
index.test-d.ts | 15 ++-
lib/bin.js | 52 +++++-----
lib/get.js | 70 ++++++-------
lib/parse.js | 24 +++++
lib/pidtree.js | 106 +++++++++-----------
lib/powershell.js | 61 ++++--------
lib/ps.js | 51 ++++------
lib/wmic.js | 47 +++------
package.json | 52 +++++-----
readme.md | 62 ++++++------
test/backends.js | 60 +++++++++++
test/bench.js | 32 +++---
test/bin.js | 14 +++
test/get.js | 138 ++++++++++++--------------
test/helpers/exec/child.js | 8 +-
test/helpers/exec/parent.js | 32 +++---
test/helpers/graph.js | 15 ---
test/helpers/mocks.js | 32 ------
test/integration.js | 153 ++++++++++++-----------------
test/parse.js | 43 ++++++++
test/powershell.js | 66 -------------
test/ps.js | 136 -------------------------
test/wmic.js | 48 ---------
30 files changed, 665 insertions(+), 989 deletions(-)
create mode 100644 lib/parse.js
create mode 100644 test/backends.js
create mode 100644 test/bin.js
delete mode 100644 test/helpers/graph.js
delete mode 100644 test/helpers/mocks.js
create mode 100644 test/parse.js
delete mode 100644 test/powershell.js
delete mode 100644 test/ps.js
delete mode 100644 test/wmic.js
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index f98df47..f042379 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -9,19 +9,14 @@ on:
jobs:
lint:
runs-on: ubuntu-latest
- name: XO & Prettier
steps:
- name: Setup repo
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Setup node
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
- npm list --dev --depth=0
+ node-version: 22
+ - name: Install dependencies
+ run: npm install
- name: Run lint
run: npm run lint
diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml
index 0a8bf5b..5e20072 100644
--- a/.github/workflows/test-macos.yml
+++ b/.github/workflows/test-macos.yml
@@ -9,32 +9,20 @@ on:
jobs:
test:
runs-on: macos-latest
- name: AVA & TSD & Benchmark & Codecov
strategy:
fail-fast: false
+ # Node 26+ is not yet supported by the coverage tool (c8); revisit
+ # "current" once the toolchain catches up.
matrix:
- node: [current, 22, 20, 18]
+ node: [24, 22, 20, 18]
steps:
- name: Setup repo
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup node ${{ matrix.node }}
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- - name: Install lib dependencies
- run: |
- npm install --only=prod
- npm list --prod --depth=0
- - name: Install dev dependencies
- run: |
- npm install --only=dev
- npm list --dev --depth=0
+ - name: Install dependencies
+ run: npm install
- name: Run tests
- run: npm run test
- #- name: Run type checking
- # run: npm run types
- - name: Run benchmark
- run: |
- npm run bench
- - name: Upload coverage to Codecov
- uses: codecov/codecov-action@v2
+ run: npm test
diff --git a/.github/workflows/test-ubuntu.yml b/.github/workflows/test-ubuntu.yml
index 9f0cc2c..126caf1 100644
--- a/.github/workflows/test-ubuntu.yml
+++ b/.github/workflows/test-ubuntu.yml
@@ -9,32 +9,26 @@ on:
jobs:
test:
runs-on: ubuntu-latest
- name: AVA & TSD & Benchmark & Codecov
strategy:
fail-fast: false
+ # Node 26+ is not yet supported by the coverage tool (c8); revisit
+ # "current" once the toolchain catches up.
matrix:
- node: [current, 22, 20, 18]
+ node: [24, 22, 20, 18]
steps:
- name: Setup repo
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup node ${{ matrix.node }}
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- - name: Install lib dependencies
- run: |
- npm install --only=prod
- npm list --prod --depth=0
- - name: Install dev dependencies
- run: |
- npm install --only=dev
- npm list --dev --depth=0
+ - name: Install dependencies
+ run: npm install
- name: Run tests
- run: npm run test
- #- name: Run type checking
- # run: npm run types
+ run: npm test
+ - name: Run type checking
+ run: npm run types
- name: Run benchmark
- run: |
- npm run bench
+ run: npm run bench
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@v2
+ uses: codecov/codecov-action@v5
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 4fcb35c..ee9130f 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -11,37 +11,20 @@ jobs:
# 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
+ # Node 26+ is not yet supported by the coverage tool (c8); revisit
+ # "current" once the toolchain catches up.
matrix:
- node: [current, 22, 20, 18]
+ node: [24, 22, 20, 18]
steps:
- name: Setup repo
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup node ${{ matrix.node }}
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- - name: Install lib dependencies
- run: |
- npm install --only=prod
- npm list --prod --depth=0
- - name: Install dev dependencies
- run: |
- npm install --only=dev
- npm list --dev --depth=0
+ - name: Install dependencies
+ run: npm install
- name: Run tests
- if: ${{ matrix.node <= 6 }}
- run: npm run test
- - name: Run tests
- if: ${{ !(matrix.node <= 6) }}
- run: npm run test:windows
- #- name: Run type checking
- # run: npm run types
- - name: Run benchmark
- run: |
- npm run bench
- - name: Upload coverage to Codecov
- if: ${{ matrix.node <= 6 }}
- uses: codecov/codecov-action@v2
+ run: npm test
diff --git a/bin/pidtree.js b/bin/pidtree.js
index 542876e..3e54d36 100755
--- a/bin/pidtree.js
+++ b/bin/pidtree.js
@@ -1,36 +1,28 @@
#!/usr/bin/env node
-'use strict';
-
-var os = require('os');
-var pidtree = require('..');
-
-// The method startsWith is not defined on string objects in node 0.10
-// eslint-disable-next-line no-extend-native
-String.prototype.startsWith = function(suffix) {
- return this.substring(0, suffix.length) === suffix;
-};
+import os from 'node:os';
+import pidtree from '../index.js';
function help() {
- var help =
+ console.log(
' Usage\n' +
- ' $ pidtree \n' +
- '\n' +
- 'Options\n' +
- ' --list To print the pids as a list.\n' +
- '\n' +
- 'Examples\n' +
- ' $ pidtree\n' +
- ' $ pidtree --list\n' +
- ' $ pidtree 1\n' +
- ' $ pidtree 1 --list\n';
- console.log(help);
+ ' $ pidtree \n' +
+ '\n' +
+ 'Options\n' +
+ ' --list To print the pids as a list.\n' +
+ '\n' +
+ 'Examples\n' +
+ ' $ pidtree\n' +
+ ' $ pidtree --list\n' +
+ ' $ pidtree 1\n' +
+ ' $ pidtree 1 --list\n',
+ );
}
function list(ppid) {
- pidtree(ppid === undefined ? -1 : ppid, function(err, list) {
- if (err) {
- console.error(err.message);
+ pidtree(ppid === undefined ? -1 : ppid, (error, list) => {
+ if (error) {
+ console.error(error.message);
return;
}
@@ -39,16 +31,16 @@ function list(ppid) {
}
function tree(ppid) {
- pidtree(ppid, {advanced: true}, function(err, list) {
- if (err) {
- console.error(err.message);
+ pidtree(ppid, {advanced: true}, (error, list) => {
+ if (error) {
+ console.error(error.message);
return;
}
- var parents = {}; // Hash Map of parents
- var tree = {}; // Adiacency Hash Map
+ const parents = {}; // Hash Map of parents
+ const tree = {}; // Adjacency Hash Map
while (list.length > 0) {
- var element = list.pop();
+ const element = list.pop();
if (tree[element.ppid]) {
tree[element.ppid].push(element.pid);
} else {
@@ -60,45 +52,43 @@ function tree(ppid) {
}
}
- var roots = [ppid];
+ let roots = [ppid];
if (ppid === -1) {
- // Get all the roots
- roots = Object.keys(tree).filter(function(node) {
- return parents[node] === undefined;
- });
+ // Get all the roots.
+ roots = Object.keys(tree).filter((node) => parents[node] === undefined);
}
- roots.forEach(function(root) {
+ for (const root of roots) {
print(tree, root);
- });
+ }
});
function print(tree, start) {
function printBranch(node, branch) {
- var isGraphHead = branch.length === 0;
- var children = tree[node] || [];
+ const isGraphHead = branch.length === 0;
+ const children = tree[node] || [];
- var branchHead = '';
+ let branchHead = '';
if (!isGraphHead) {
branchHead = children.length > 0 ? '┬ ' : '─ ';
}
console.log(branch + branchHead + node);
- var baseBranch = branch;
+ let baseBranch = branch;
if (!isGraphHead) {
- var isChildOfLastBranch = branch.slice(-2) === '└─';
+ const isChildOfLastBranch = branch.slice(-2) === '└─';
baseBranch = branch.slice(0, -2) + (isChildOfLastBranch ? ' ' : '| ');
}
- var nextBranch = baseBranch + '├─';
- var lastBranch = baseBranch + '└─';
- children.forEach(function(child, index) {
+ const nextBranch = baseBranch + '├─';
+ const lastBranch = baseBranch + '└─';
+ for (const [index, child] of children.entries()) {
printBranch(
child,
- children.length - 1 === index ? lastBranch : nextBranch
+ children.length - 1 === index ? lastBranch : nextBranch,
);
- });
+ }
}
printBranch(start, '');
@@ -106,9 +96,9 @@ function tree(ppid) {
}
function run() {
- var flag;
- var ppid;
- for (var i = 2; i < process.argv.length; i++) {
+ let flag;
+ let ppid;
+ for (let i = 2; i < process.argv.length; i++) {
if (process.argv[i].startsWith('--')) {
flag = process.argv[i];
} else {
@@ -120,9 +110,13 @@ function run() {
ppid = -1;
}
- if (flag === '--list') list(ppid);
- else if (flag === undefined) tree(ppid);
- else help();
+ if (flag === '--list') {
+ list(ppid);
+ } else if (flag === undefined) {
+ tree(ppid);
+ } else {
+ help();
+ }
}
run();
diff --git a/index.d.ts b/index.d.ts
index 1702ec6..e15cb2e 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,80 +1,79 @@
-declare namespace PidTree {
- export interface Options {
- /**
- * Include the provided PID in the list. Ignored if -1 is passed as PID.
- * @default false
- */
- root?: boolean;
- }
+export type Options = {
+ /**
+ * Include the provided PID in the list. Ignored if -1 is passed as PID.
+ * @default false
+ */
+ root?: boolean;
+};
- export interface AdvancedResult {
- /**
- * PID of the parent.
- */
- ppid: number;
- /**
- * PID
- */
- pid: number;
- }
+export type AdvancedResult = {
+ /**
+ * PID of the parent.
+ */
+ ppid: number;
+ /**
+ * PID.
+ */
+ pid: number;
+};
- export type Result = number;
-}
+export type Result = number;
/**
* Get the list of children pids of the given pid.
- * @param pid A PID. If -1 will return all the pids.
- * @param callback Called when the list is ready.
+ * @param pid A PID. If -1 will return all the pids.
+ * @param callback Called when the list is ready.
*/
declare function pidtree(
pid: string | number,
- callback: (error: Error | undefined, result: PidTree.Result[]) => void
+ callback: (error: Error | undefined, result: Result[]) => void,
): void;
/**
* Get the list of children pids of the given pid.
- * @param pid A PID. If -1 will return all the pids.
- * @param options Options object.
- * @param callback Called when the list is ready.
+ * @param pid A PID. If -1 will return all the pids.
+ * @param options Options object.
+ * @param callback Called when the list is ready.
*/
declare function pidtree(
pid: string | number,
- options: PidTree.Options,
- callback: (error: Error | undefined, result: PidTree.Result[]) => void
+ options: Options,
+ callback: (error: Error | undefined, result: Result[]) => void,
): void;
/**
* Get the list of children pids of the given pid.
- * @param pid A PID. If -1 will return all the pids.
- * @param options Options object.
- * @param callback Called when the list is ready.
+ * @param pid A PID. If -1 will return all the pids.
+ * @param options Options object.
+ * @param callback Called when the list is ready.
*/
declare function pidtree(
pid: string | number,
- options: PidTree.Options & {advanced: true},
- callback: (error: Error | undefined, result: PidTree.AdvancedResult[]) => void
+ options: Options & {advanced: true},
+ callback: (error: Error | undefined, result: AdvancedResult[]) => void,
): void;
/**
* Get the list of children pids of the given pid.
- * @param pid A PID. If -1 will return all the pids.
- * @param [options] Optional options object.
+ * @param pid A PID. If -1 will return all the pids.
+ * @param options Optional options object.
* @returns A promise containing the list.
*/
declare function pidtree(
pid: string | number,
- options?: PidTree.Options
-): Promise;
+ options?: Options,
+): Promise;
/**
* Get the list of children pids of the given pid.
- * @param pid A PID. If -1 will return all the pids.
- * @param options Options object.
+ * @param pid A PID. If -1 will return all the pids.
+ * @param options Options object.
* @returns A promise containing the list.
*/
declare function pidtree(
pid: string | number,
- options: PidTree.Options & {advanced: true}
-): Promise;
+ options: Options & {advanced: true},
+): Promise;
-export = pidtree;
+export default pidtree;
+export {pidtree};
diff --git a/index.js b/index.js
index b38d08f..6ba6a6e 100644
--- a/index.js
+++ b/index.js
@@ -1,49 +1,33 @@
-'use strict';
+import {promisify} from 'node:util';
+import {pidtreeCallback} from './lib/pidtree.js';
-function pify(fn, arg1, arg2) {
- return new Promise(function(resolve, reject) {
- fn(arg1, arg2, function(err, data) {
- if (err) return reject(err);
- resolve(data);
- });
- });
-}
-
-// Node versions prior to 4.0.0 do not define have `startsWith`.
-/* istanbul ignore if */
-if (!String.prototype.startsWith) {
- // eslint-disable-next-line no-extend-native
- String.prototype.startsWith = function(suffix) {
- return this.substring(0, suffix.length) === suffix;
- };
-}
-
-var pidtree = require('./lib/pidtree');
+const pidtreeAsync = promisify(pidtreeCallback);
/**
* Get the list of children pids of the given pid.
* @public
- * @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 {pid: X, ppid: Y}.
- * @param {Function} [callback=undefined] Called when the list is ready. If not
- * provided a promise is returned instead.
- * @returns {Promise.
@@ -83,39 +83,44 @@ Furthermore ps-tree is [unmaintained][gh:ps-tree-um].
Uuh, and a fancy [CLI](#cli) is also available!
+## Install
+
+```bash
+npm install pidtree
+```
+
+> **Requirements:** pidtree is an [ESM-only][gh:esm] package and requires
+> **Node.js >= 18**. If you need CommonJS (`require`) or support for older
+> Node.js versions, stay on [`pidtree@0.6`](https://www.npmjs.com/package/pidtree/v/0.6.1).
+
## Usage
```js
-var pidtree = require('pidtree')
+import pidtree from 'pidtree'
+// The named import works too: import {pidtree} from 'pidtree'
-// Get childs of current process
-pidtree(process.pid, function (err, pids) {
- console.log(pids)
- // => []
-})
+// Get children of the current process (a promise is returned)
+const pids = await pidtree(process.pid)
+console.log(pids)
+// => []
// Include the given pid in the result array
-pidtree(process.pid, {root: true}, function (err, pids) {
- console.log(pids)
- // => [727]
-})
+console.log(await pidtree(process.pid, {root: true}))
+// => [727]
// Get all the processes of the System (-1 is a special value of this package)
-pidtree(-1, function (err, pids) {
- console.log(pids)
- // => [530, 42, ..., 41241]
-})
+console.log(await pidtree(-1))
+// => [530, 42, ..., 41241]
-// Include PPID in the results
-pidtree(1, {advanced: true}, function (err, pids) {
+// Include the PPID in the results
+console.log(await pidtree(1, {advanced: true}))
+// => [{ppid: 1, pid: 530}, {ppid: 1, pid: 42}, ..., {ppid: 1, pid: 41241}]
+
+// A Node-style callback is also supported instead of a promise
+pidtree(1, function (err, pids) {
console.log(pids)
- // => [{ppid: 1, pid: 530}, {ppid: 1, pid: 42}, ..., {ppid: 1, pid: 41241}]
+ // => [141, 42, ..., 15242]
})
-
-// If no callback is given it returns a promise instead
-const pids = await pidtree(1)
-console.log(pids)
-// => [141, 42, ..., 15242]
```
## Compatibility
@@ -192,6 +197,7 @@ This project is licensed under the MIT License - see the [license][license] file
[github:simonepri]: https://github.com/simonepri
+[gh:esm]: https://nodejs.org/api/esm.html
[gh:pidusage]: https://github.com/soyuka/pidusage
[gh:ps-tree]: https://github.com/indexzero/ps-tree
[gh:ps-tree-um]: https://github.com/indexzero/ps-tree/issues/30
diff --git a/test/backends.js b/test/backends.js
new file mode 100644
index 0000000..70ce8e7
--- /dev/null
+++ b/test/backends.js
@@ -0,0 +1,60 @@
+import test from 'ava';
+import {ps} from '../lib/ps.js';
+import {wmic} from '../lib/wmic.js';
+import {powershell} from '../lib/powershell.js';
+
+const backends = [
+ {name: 'ps', fn: ps},
+ {name: 'wmic', fn: wmic},
+ {name: 'powershell', fn: powershell},
+];
+
+// A drop-in replacement for lib/bin.js `run` that handles both the
+// (cmd, args, done) and (cmd, args, options, done) signatures.
+function fakeRun({err, stdout = '', code = 0} = {}) {
+ return (cmd, args, options, done) => {
+ if (typeof options === 'function') {
+ done = options;
+ }
+
+ if (err) {
+ done(err);
+ return;
+ }
+
+ done(null, stdout, code);
+ };
+}
+
+function call(backend, run) {
+ return new Promise((resolve, reject) => {
+ backend((error, list) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(list);
+ }
+ }, run);
+ });
+}
+
+for (const {name, fn} of backends) {
+ test(`${name} parses the spawned output`, async (t) => {
+ const list = await call(fn, fakeRun({stdout: '0 100\n100 101\n'}));
+ t.deepEqual(list, [
+ [0, 100],
+ [100, 101],
+ ]);
+ });
+
+ test(`${name} errors on a non-zero exit code`, async (t) => {
+ const error = await t.throwsAsync(call(fn, fakeRun({code: 1})));
+ t.true(error.message.includes('exited with code 1'));
+ });
+
+ test(`${name} propagates a spawn error`, async (t) => {
+ const boom = new Error('spawn failed');
+ const error = await t.throwsAsync(call(fn, fakeRun({err: boom})));
+ t.is(error, boom);
+ });
+}
diff --git a/test/bench.js b/test/bench.js
index b1f1123..2fb1941 100644
--- a/test/bench.js
+++ b/test/bench.js
@@ -1,39 +1,31 @@
import test from 'ava';
-
import tspan from 'time-span';
-
-import pidtree from '..';
+import pidtree from '../index.js';
async function execute(pid, times) {
const end = tspan();
- try {
- for (let i = 0; i < times; i++) {
- // eslint-disable-next-line no-await-in-loop
- await pidtree(pid);
- }
-
- const time = end();
- return Promise.resolve(time);
- } catch (error) {
- end();
- return Promise.reject(error);
+ for (let i = 0; i < times; i++) {
+ // eslint-disable-next-line no-await-in-loop
+ await pidtree(pid);
}
+
+ return end();
}
-test.serial('should execute the benchmark', async t => {
+test.serial('should execute the benchmark', async (t) => {
let time = await execute(-1, 100);
t.log(
`Get childs of all the system's pids 100 times done in ${time.toFixed(
- 3
- )} ms (${(1000 * 100 / time).toFixed(3)} op/s)`
+ 3,
+ )} ms (${((1000 * 100) / time).toFixed(3)} op/s)`,
);
time = await execute(process.pid, 100);
t.log(
`Get childs of pid:${process.pid} 100 times done in ${time.toFixed(
- 3
- )} ms (${(1000 * 100 / time).toFixed(3)} op/s)`
+ 3,
+ )} ms (${((1000 * 100) / time).toFixed(3)} op/s)`,
);
- t.pass();
+ t.true(time >= 0);
});
diff --git a/test/bin.js b/test/bin.js
new file mode 100644
index 0000000..719489b
--- /dev/null
+++ b/test/bin.js
@@ -0,0 +1,14 @@
+import test from 'ava';
+import {stripStderr} from '../lib/bin.js';
+
+test('returns undefined for empty stderr', (t) => {
+ t.is(stripStderr(''), undefined);
+});
+
+test('trims and passes through a real error message', (t) => {
+ t.is(stripStderr(' some error '), 'some error');
+});
+
+test('strips the bogus screen size warning', (t) => {
+ t.is(stripStderr('your 131072x1 screen size is bogus. expect trouble'), '');
+});
diff --git a/test/get.js b/test/get.js
index 2e69fc6..7feb582 100644
--- a/test/get.js
+++ b/test/get.js
@@ -1,83 +1,75 @@
import test from 'ava';
-import mockery from 'mockery';
-
-import pify from 'pify';
-
-test.before(() => {
- mockery.enable({
- warnOnReplace: false,
- warnOnUnregistered: false,
- useCleanCache: true,
+import {getWindows} from '../lib/get.js';
+
+function call(wmicFn, powershellFn) {
+ return new Promise((resolve, reject) => {
+ getWindows(
+ (error, list) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(list);
+ }
+ },
+ wmicFn,
+ powershellFn,
+ );
});
-});
-
-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('uses wmic on Windows when it is available', async (t) => {
+ let powershellUsed = false;
+ const list = await call(
+ (callback) =>
+ callback(null, [
+ [0, 100],
+ [100, 101],
+ ]),
+ (callback) => {
+ powershellUsed = true;
+ callback(null, []);
+ },
+ );
+ t.false(powershellUsed, 'powershell should not be used when wmic succeeds');
+ t.deepEqual(list, [
+ [0, 100],
+ [100, 101],
+ ]);
});
-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('falls back to powershell when wmic is missing', async (t) => {
+ const enoent = Object.assign(new Error('spawn wmic ENOENT'), {
+ code: 'ENOENT',
+ });
+ const list = await call(
+ (callback) => callback(enoent),
+ (callback) =>
+ callback(null, [
+ [0, 777],
+ [777, 778],
+ ]),
+ );
+ t.deepEqual(list, [
+ [0, 777],
+ [777, 778],
+ ]);
});
-test('should not fall back to powershell on a non ENOENT wmic error', async t => {
+test('does not fall back to powershell on a non-ENOENT wmic error', async (t) => {
+ let powershellUsed = false;
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');
+ const error = await t.throwsAsync(
+ call(
+ (callback) => callback(boom),
+ (callback) => {
+ powershellUsed = true;
+ callback(null, []);
+ },
+ ),
+ );
+ t.false(
+ powershellUsed,
+ 'powershell should not be used on a generic wmic error',
+ );
+ t.is(error, boom);
});
diff --git a/test/helpers/exec/child.js b/test/helpers/exec/child.js
index 9a5a307..50ee9e7 100644
--- a/test/helpers/exec/child.js
+++ b/test/helpers/exec/child.js
@@ -1,8 +1,8 @@
-'use strict';
+let started = false;
-var started = false;
-setInterval(function() {
+// Keeps the process alive and prints its pid once, so the test knows it is up.
+setInterval(() => {
if (started) return;
console.log(process.pid);
started = true;
-}, 100); // Does nothing, but prevents exit
+}, 100);
diff --git a/test/helpers/exec/parent.js b/test/helpers/exec/parent.js
index 9e31bf1..fe5a3ff 100644
--- a/test/helpers/exec/parent.js
+++ b/test/helpers/exec/parent.js
@@ -1,25 +1,25 @@
-'use strict';
+import cp from 'node:child_process';
+import path from 'node:path';
+import {fileURLToPath} from 'node:url';
-var path = require('path');
-var cp = require('child_process');
+const dirname = path.dirname(fileURLToPath(import.meta.url));
+const script = path.join(dirname, 'child.js');
-var started = false;
-var spawned = {};
-var script = path.join('test', 'helpers', 'exec', 'child.js');
+let started = false;
+const spawned = {};
-for (var i = 0; i < 10; i++) {
- var child = cp.spawn('node', [script]);
- child.stdout.on(
- 'data',
- (child => {
- spawned[child.pid] = true;
- }).bind(this, child)
- );
+for (let i = 0; i < 10; i++) {
+ const child = cp.spawn('node', [script]);
+ child.stdout.on('data', () => {
+ spawned[child.pid] = true;
+ });
}
-setInterval(function() {
+// Prints this process's pid only once all ten children are up, so the test can
+// rely on exactly ten descendants being alive.
+setInterval(() => {
if (started) return;
if (Object.keys(spawned).length !== 10) return;
console.log(process.pid);
started = true;
-}, 100); // Does nothing, but prevents exit
+}, 100);
diff --git a/test/helpers/graph.js b/test/helpers/graph.js
deleted file mode 100644
index 033c949..0000000
--- a/test/helpers/graph.js
+++ /dev/null
@@ -1,15 +0,0 @@
-async function deepForEach(root, fn) {
- const queue = [root];
- while (queue.length > 0) {
- const cur = queue.pop();
- // eslint-disable-next-line no-await-in-loop
- await fn(cur);
- if (Array.isArray(cur.children)) {
- cur.children.forEach(c => queue.push(c));
- }
- }
-}
-
-module.exports = {
- deepForEach,
-};
diff --git a/test/helpers/mocks.js b/test/helpers/mocks.js
deleted file mode 100644
index 265c236..0000000
--- a/test/helpers/mocks.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import EventEmitter from 'events';
-import streamify from 'string-to-stream';
-import through from 'through';
-
-// eslint-disable-next-line max-params
-function spawn(stdout, stderr, error, code, signal) {
- const ee = new EventEmitter();
-
- ee.stdout = through(function(d) {
- this.queue(d);
- });
- ee.stderr = through(function(d) {
- this.queue(d);
- });
-
- streamify(stderr).pipe(ee.stderr);
- streamify(stdout).pipe(ee.stdout);
-
- if (error) {
- ee.emit('error', error);
- } else if (stderr) {
- ee.stderr.on('end', () => ee.emit('close', code, signal));
- } else {
- ee.stdout.on('end', () => ee.emit('close', code, signal));
- }
-
- return ee;
-}
-
-export default {
- spawn,
-};
diff --git a/test/integration.js b/test/integration.js
index 6950add..610f36f 100644
--- a/test/integration.js
+++ b/test/integration.js
@@ -1,129 +1,100 @@
-import cp from 'child_process';
-import path from 'path';
-
+import cp from 'node:child_process';
+import path from 'node:path';
+import {fileURLToPath} from 'node:url';
+import {promisify} from 'node:util';
import test from 'ava';
+import treeKill from 'tree-kill';
+import pidtree from '../index.js';
-import pify from 'pify';
-import treek from 'tree-kill';
-
-import pidtree from '..';
+const dirname = path.dirname(fileURLToPath(import.meta.url));
+const kill = promisify(treeKill);
const scripts = {
- parent: path.join(__dirname, 'helpers', 'exec', 'parent.js'),
- child: path.join(__dirname, 'helpers', 'exec', 'child.js'),
+ parent: path.join(dirname, 'helpers', 'exec', 'parent.js'),
+ child: path.join(dirname, 'helpers', 'exec', 'child.js'),
};
-test('should work with a single pid', async t => {
- let result = await pidtree(-1, {advanced: true});
- t.log(result);
+// Resolves once the spawned helper has printed its pid, meaning it (and all of
+// its own children) are up and running.
+function waitForReady(child) {
+ return new Promise((resolve, reject) => {
+ child.stdout.on('data', (data) => resolve(data.toString()));
+ child.stderr.on('data', (data) => reject(new Error(data.toString())));
+ child.on('error', reject);
+ child.on('exit', () => reject(new Error('the helper exited early')));
+ });
+}
+test('should work with a single pid', async (t) => {
+ let result = await pidtree(-1, {advanced: true});
t.true(Array.isArray(result));
- result.forEach((p, i) => {
- t.is(typeof p, 'object', i);
- t.is(typeof p.ppid, 'number', i);
- t.false(isNaN(p.ppid), i);
- t.is(typeof p.pid, 'number', i);
- t.false(isNaN(p.pid), i);
- });
+ for (const entry of result) {
+ t.is(typeof entry.ppid, 'number');
+ t.false(Number.isNaN(entry.ppid));
+ t.is(typeof entry.pid, 'number');
+ t.false(Number.isNaN(entry.pid));
+ }
result = await pidtree(-1);
-
t.true(Array.isArray(result));
- result.forEach((p, i) => {
- t.is(typeof p, 'number', i);
- t.false(isNaN(p), i);
- });
+ for (const entry of result) {
+ t.is(typeof entry, 'number');
+ t.false(Number.isNaN(entry));
+ }
});
-test('show work with a Parent process which has zero Child processes', async t => {
+test('should work with a parent which has zero child processes', async (t) => {
const child = cp.spawn('node', [scripts.child]);
-
- try {
- await new Promise((resolve, reject) => {
- child.stdout.on('data', d => resolve(d.toString()));
- child.stderr.on('data', d => reject(d.toString()));
- child.on('error', reject);
- child.on('exit', reject);
- });
- } catch (error) {
- await pify(treek)(child.pid);
- t.notThrows(() => {
- throw error;
- });
- }
+ await waitForReady(child);
const children = await pidtree(child.pid);
- await pify(treek)(child.pid);
+ await kill(child.pid);
t.is(children.length, 0, 'There should be no active child processes');
});
-test('show work with a Parent process which has ten Child processes', async t => {
+test('should work with a parent which has ten child processes', async (t) => {
const parent = cp.spawn('node', [scripts.parent]);
-
- try {
- await new Promise((resolve, reject) => {
- parent.stdout.on('data', d => resolve(d.toString()));
- parent.stderr.on('data', d => reject(d.toString()));
- parent.on('error', reject);
- parent.on('exit', reject);
- });
- } catch (error) {
- await pify(treek)(parent.pid);
- t.notThrows(() => {
- throw error;
- });
- }
+ await waitForReady(parent);
const children = await pidtree(parent.pid);
- await pify(treek)(parent.pid);
+ await kill(parent.pid);
t.is(children.length, 10, 'There should be 10 active child processes');
});
-test('show include the root if the root option is passsed', async t => {
+test('should include the root when the root option is passed', async (t) => {
const child = cp.spawn('node', [scripts.child]);
-
- try {
- await new Promise((resolve, reject) => {
- child.stdout.on('data', d => resolve(d.toString()));
- child.stderr.on('data', d => reject(d.toString()));
- child.on('error', reject);
- child.on('exit', reject);
- });
- } catch (error) {
- await pify(treek)(child.pid);
- t.notThrows(() => {
- throw error;
- });
- }
+ await waitForReady(child);
const children = await pidtree(child.pid, {root: true, advanced: true});
- await pify(treek)(child.pid);
+ await kill(child.pid);
- t.deepEqual(
- children,
- [{ppid: process.pid, pid: child.pid}],
- 'There should be the root pid in the array'
- );
+ t.deepEqual(children, [{ppid: process.pid, pid: child.pid}]);
});
-test('should throw an error if an invalid pid is provided', async t => {
- let err = await t.throws(pidtree(null));
- t.is(err.message, 'The pid provided is invalid');
- err = await t.throws(pidtree([]));
- t.is(err.message, 'The pid provided is invalid');
- err = await t.throws(pidtree('invalid'));
- t.is(err.message, 'The pid provided is invalid');
- err = await t.throws(pidtree(-2));
- t.is(err.message, 'The pid provided is invalid');
+test('should throw an error if an invalid pid is provided', async (t) => {
+ for (const bad of [null, [], 'invalid', -2]) {
+ // eslint-disable-next-line no-await-in-loop
+ const error = await t.throwsAsync(pidtree(bad));
+ t.is(error.message, 'The pid provided is invalid');
+ }
});
-test('should throw an error if the pid does not exists', async t => {
- const err = await t.throws(pidtree(65535));
- t.is(err.message, 'No matching pid found');
+test('should throw an error if the pid does not exist', async (t) => {
+ const error = await t.throwsAsync(pidtree(65_535));
+ t.is(error.message, 'No matching pid found');
});
-test.cb("should use the callback if it's provided", t => {
- pidtree(process.pid, t.end);
+test('should use the callback when one is provided', async (t) => {
+ const list = await new Promise((resolve, reject) => {
+ pidtree(process.pid, (error, result) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(result);
+ }
+ });
+ });
+ t.true(Array.isArray(list));
});
diff --git a/test/parse.js b/test/parse.js
new file mode 100644
index 0000000..f7e912c
--- /dev/null
+++ b/test/parse.js
@@ -0,0 +1,43 @@
+import test from 'ava';
+import {parse} from '../lib/parse.js';
+
+test('parses ps style output and skips the header', (t) => {
+ const stdout = 'PPID PID\n 1 430\n 430 432\n 1 7166\n';
+ t.deepEqual(parse(stdout), [
+ [1, 430],
+ [430, 432],
+ [1, 7166],
+ ]);
+});
+
+test(String.raw`parses wmic style output with \r\r\n line endings`, (t) => {
+ const stdout =
+ 'ParentProcessId ProcessId\r\r\n' +
+ '0 777\r\r\n' +
+ '777 778\r\r\n';
+ t.deepEqual(parse(stdout), [
+ [0, 777],
+ [777, 778],
+ ]);
+});
+
+test('parses powershell style output without a header', (t) => {
+ const stdout = '0 777\r\n777 778\r\n0 779\r\n';
+ t.deepEqual(parse(stdout), [
+ [0, 777],
+ [777, 778],
+ [0, 779],
+ ]);
+});
+
+test('skips blank and non-numeric lines', (t) => {
+ const stdout = '\n0 777\nsome banner line\n777 778\n';
+ t.deepEqual(parse(stdout), [
+ [0, 777],
+ [777, 778],
+ ]);
+});
+
+test('returns an empty list for empty output', (t) => {
+ t.deepEqual(parse(''), []);
+});
diff --git a/test/powershell.js b/test/powershell.js
deleted file mode 100644
index 734f3b5..0000000
--- a/test/powershell.js
+++ /dev/null
@@ -1,66 +0,0 @@
-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');
-});
diff --git a/test/ps.js b/test/ps.js
deleted file mode 100644
index a1be0d2..0000000
--- a/test/ps.js
+++ /dev/null
@@ -1,136 +0,0 @@
-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 ps output on Darwin', async t => {
- const stdout =
- 'PPID PID\n' +
- ' 1 430\n' +
- ' 430 432\n' +
- ' 1 727\n' +
- ' 1 7166\n';
-
- mockery.registerMock('child_process', {
- spawn: () => mocks.spawn(stdout, '', null, 0, null),
- });
- mockery.registerMock('os', {
- EOL: '\n',
- platform: () => 'darwin',
- type: () => 'type',
- release: () => 'release',
- });
-
- const ps = require('../lib/ps');
-
- const result = await pify(ps)();
- t.deepEqual(result, [[1, 430], [430, 432], [1, 727], [1, 7166]]);
-
- mockery.deregisterMock('child_process');
- mockery.deregisterMock('os');
-});
-
-test('should parse ps output on *nix', async t => {
- const stdout =
- 'PPID PID\n' +
- ' 1 430\n' +
- ' 430 432\n' +
- ' 1 727\n' +
- ' 1 7166\n';
-
- mockery.registerMock('child_process', {
- spawn: () => mocks.spawn(stdout, '', null, 0, null),
- });
- mockery.registerMock('os', {
- EOL: '\n',
- platform: () => 'linux',
- type: () => 'type',
- release: () => 'release',
- });
-
- const ps = require('../lib/ps');
-
- const result = await pify(ps)();
- t.deepEqual(result, [[1, 430], [430, 432], [1, 727], [1, 7166]]);
-
- mockery.deregisterMock('child_process');
- mockery.deregisterMock('os');
-});
-
-test('should throw if stderr contains an error', async t => {
- const stdout =
- 'PPID PID\n' +
- ' 1 430\n' +
- ' 430 432\n' +
- ' 1 727\n' +
- ' 1 7166\n';
-
- mockery.registerMock('child_process', {
- spawn: () => mocks.spawn(stdout, 'Some error', null, 0, null),
- });
- mockery.registerMock('os', {
- EOL: '\n',
- platform: () => 'linux',
- type: () => 'type',
- release: () => 'release',
- });
-
- const ps = require('../lib/ps');
-
- await t.throws(pify(ps)());
-
- mockery.deregisterMock('child_process');
- mockery.deregisterMock('os');
-});
-
-test('should not throw if stderr contains the "bogus screen" error message', async t => {
- const stdout =
- 'PPID PID\n' +
- ' 1 430\n' +
- ' 430 432\n' +
- ' 1 727\n' +
- ' 1 7166\n';
-
- mockery.registerMock('child_process', {
- spawn: () =>
- mocks.spawn(
- stdout,
- 'your 131072x1 screen size is bogus. expect trouble',
- null,
- 0,
- null
- ),
- });
- mockery.registerMock('os', {
- EOL: '\n',
- platform: () => 'linux',
- type: () => 'type',
- release: () => 'release',
- });
-
- const ps = require('../lib/ps');
-
- const result = await pify(ps)();
- t.deepEqual(result, [[1, 430], [430, 432], [1, 727], [1, 7166]]);
-
- mockery.deregisterMock('child_process');
- mockery.deregisterMock('os');
-});
diff --git a/test/wmic.js b/test/wmic.js
deleted file mode 100644
index 506cedb..0000000
--- a/test/wmic.js
+++ /dev/null
@@ -1,48 +0,0 @@
-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 wmic output on Windows', async t => {
- const stdout =
- `ParentProcessId ProcessId\r\r\n` +
- `0 777 \r\r\n` +
- `777 778 \r\r\n` +
- `0 779 \r\r\n\r\r\n`;
-
- mockery.registerMock('child_process', {
- spawn: () => mocks.spawn(stdout, '', null, 0, null),
- });
- mockery.registerMock('os', {
- EOL: '\r\n',
- platform: () => 'linux',
- type: () => 'type',
- release: () => 'release',
- });
-
- const wmic = require('../lib/wmic');
-
- const result = await pify(wmic)();
- t.deepEqual(result, [[0, 777], [777, 778], [0, 779]]);
-
- mockery.deregisterMock('child_process');
- mockery.deregisterMock('os');
-});