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.} Only when the callback is not provided. + * @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] Called when the list is ready. If not provided a + * promise is returned instead. + * @returns {Promise|void} Only when the callback is not provided. */ -function list(pid, options, callback) { +function pidtree(pid, options, callback) { if (typeof options === 'function') { callback = options; options = undefined; } if (typeof callback === 'function') { - pidtree(pid, options, callback); + pidtreeCallback(pid, options, callback); return; } - return pify(pidtree, pid, options); + return pidtreeAsync(pid, options); } -module.exports = list; +export default pidtree; +export {pidtree}; diff --git a/index.test-d.ts b/index.test-d.ts index f327c18..8f050e8 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,14 +1,13 @@ import {expectType} from 'tsd'; - -import pidtree = require('.'); +import pidtree, {pidtree as named, type AdvancedResult} from './index.js'; (async () => { expectType(await pidtree(1)); expectType(await pidtree('1')); - expectType>( - await pidtree(1, {advanced: true}) - ); - expectType>( - await pidtree('1', {advanced: true}) - ); + expectType(await pidtree(1, {advanced: true})); + expectType(await pidtree('1', {advanced: true})); + expectType(await pidtree(1, {root: true})); + + // The named export is the same function as the default export. + expectType(await named(1)); })(); diff --git a/lib/bin.js b/lib/bin.js index a74954d..b3d61d1 100644 --- a/lib/bin.js +++ b/lib/bin.js @@ -1,63 +1,65 @@ -'use strict'; +import {spawn} from 'node:child_process'; -var spawn = require('child_process').spawn; - -function stripStderr(stderr) { +/** + * 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 - var regex = /your \d+x\d+ screen size is bogus\. expect trouble/gi; - stderr = stderr.replace(regex, ''); + 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 option for the spawn function. - * @param {Function} done(err, 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) */ -function run(cmd, args, options, done) { +export function run(cmd, args, options, done) { if (typeof options === 'function') { done = options; options = undefined; } - var executed = false; - var ch = spawn(cmd, args, options); - var stdout = ''; - var stderr = ''; + let executed = false; + const child = spawn(cmd, args, options); + let stdout = ''; + let stderr = ''; - ch.stdout.on('data', function(d) { - stdout += d.toString(); + child.stdout.on('data', (data) => { + stdout += data.toString(); }); - ch.stderr.on('data', function(d) { - stderr += d.toString(); + child.stderr.on('data', (data) => { + stderr += data.toString(); }); - ch.on('error', function(err) { + 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(err); + done(error); }); - ch.on('close', function(code) { + child.on('close', (code) => { if (executed) return; executed = true; stderr = stripStderr(stderr); if (stderr) { - return done(new Error(stderr)); + done(new Error(stderr)); + return; } done(null, stdout, code); }); } - -module.exports = run; diff --git a/lib/get.js b/lib/get.js index 68abad1..6565ee2 100644 --- a/lib/get.js +++ b/lib/get.js @@ -1,64 +1,64 @@ -'use strict'; +import os from 'node:os'; +import {ps} from './ps.js'; +import {wmic} from './wmic.js'; +import {powershell} from './powershell.js'; -var os = require('os'); - -var platformToMethod = { +const platformToMethod = { darwin: 'ps', sunos: 'ps', freebsd: 'ps', netbsd: 'ps', - win: 'wmic', + win: 'win', linux: 'ps', aix: 'ps', }; -var methodToRequireFn = { - ps: () => require('./ps'), - wmic: () => require('./wmic'), - powershell: () => require('./powershell'), -}; - -var platform = os.platform(); +let platform = os.platform(); if (platform.startsWith('win')) { platform = 'win'; } -var method = platformToMethod[platform]; +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. + * @param {Function} callback Called when the list is ready. */ -function get(callback) { +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)' - ) + ' 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); - }); + if (method === 'win') { + getWindows(callback); return; } - var list = methodToRequireFn[method](); - list(callback); + ps(callback); } - -module.exports = get; diff --git a/lib/parse.js b/lib/parse.js new file mode 100644 index 0000000..96e1179 --- /dev/null +++ b/lib/parse.js @@ -0,0 +1,24 @@ +/** + * Parses the ` ` 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; +} diff --git a/lib/pidtree.js b/lib/pidtree.js index f0c8e78..ea6b924 100644 --- a/lib/pidtree.js +++ b/lib/pidtree.js @@ -1,104 +1,94 @@ -'use strict'; - -var getAll = require('./get'); +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 {pid: X, ppid: Y}. - * @param {Function} callback(err, list) Called when the list is ready. + * @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. */ -function list(PID, options, callback) { +export function pidtreeCallback(pid, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } - if (typeof options !== 'object') { + if (typeof options !== 'object' || options === null) { options = {}; } - PID = parseInt(PID, 10); - if (isNaN(PID) || PID < -1) { + pid = Number.parseInt(pid, 10); + if (Number.isNaN(pid) || pid < -1) { callback(new TypeError('The pid provided is invalid')); return; } - getAll(function(err, list) { - if (err) { - callback(err); + get((error, list) => { + if (error) { + callback(error); return; } - // If the user wants the whole list just return it - if (PID === -1) { - for (var i = 0; i < list.length; i++) { - list[i] = options.advanced - ? {ppid: list[i][0], pid: list[i][1]} - : (list[i] = list[i][1]); - } - - callback(null, list); + // 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; } - var root; - for (var l = 0; l < list.length; l++) { - if (list[l][1] === PID) { - root = options.advanced ? {ppid: list[l][0], pid: PID} : PID; + let root; + for (const entry of list) { + if (entry[1] === pid) { + root = options.advanced ? {ppid: entry[0], pid} : pid; break; } - if (list[l][0] === PID) { - root = options.advanced ? {pid: PID} : PID; // Special pids like 0 on *nix + if (entry[0] === pid) { + // Special pids like 0 on *nix. + root = options.advanced ? {pid} : pid; } } - if (!root) { + if (root === undefined) { callback(new Error('No matching pid found')); return; } - // Build the adiacency Hash Map (pid -> [children of pid]) - var tree = {}; - while (list.length > 0) { - var element = list.pop(); - if (tree[element[0]]) { - tree[element[0]].push(element[1]); + // 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[element[0]] = [element[1]]; + tree[parentPid] = [childPid]; } } - // Starting by the PID provided by the user, traverse the tree using the - // adiacency Hash Map until the whole subtree is visited. - // Each pid encountered while visiting is added to the pids array. - var idx = 0; - var pids = [root]; - while (idx < pids.length) { - var curpid = options.advanced ? pids[idx++].pid : pids[idx++]; - if (!tree[curpid]) continue; - var length = tree[curpid].length; - for (var j = 0; j < length; j++) { - pids.push( - options.advanced - ? {ppid: curpid, pid: tree[curpid][j]} - : tree[curpid][j] - ); + // 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[curpid]; + delete tree[current]; } if (!options.root) { - pids.shift(); // Remove root + pids.shift(); // Remove root. } callback(null, pids); }); } - -module.exports = list; diff --git a/lib/powershell.js b/lib/powershell.js index 2fabb69..5525f2a 100644 --- a/lib/powershell.js +++ b/lib/powershell.js @@ -1,70 +1,51 @@ -'use strict'; - -var os = require('os'); -var bin = require('./bin'); +import {run as defaultRun} from './bin.js'; +import {parse} from './parse.js'; // PowerShell command used to list every process as " " 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 = +// (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. -var ENCODED = Buffer.from(COMMAND, 'utf16le').toString('base64'); +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(err, list) + * @param {Function} callback callback(err, list) + * @param {Function} [run] Injectable spawner, used for testing. */ -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); +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) + 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); + callback(null, parse(stdout)); } catch (error) { callback(error); } }); } - -module.exports = powershell; diff --git a/lib/ps.js b/lib/ps.js index 6d9bb5f..d81ff8d 100644 --- a/lib/ps.js +++ b/lib/ps.js @@ -1,47 +1,32 @@ -'use strict'; - -var os = require('os'); -var bin = require('./bin'); +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(err, list) + * @param {Function} callback callback(err, list) + * @param {Function} [run] Injectable spawner, used for testing. */ -function ps(callback) { - var args = ['-A', '-o', 'ppid,pid']; +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; + } - bin('ps', args, function(err, stdout, code) { - if (err) return callback(err); if (code !== 0) { - return callback(new Error('pidtree ps command exited with code ' + code)); + callback(new Error('pidtree ps command exited with code ' + code)); + return; } - // Example of stdout - // - // PPID PID - // 1 430 - // 430 432 - // 1 727 - // 1 7166 - try { - stdout = stdout.split(os.EOL); - - var list = []; - for (var i = 1; i < stdout.length; i++) { - stdout[i] = stdout[i].trim(); - if (!stdout[i]) continue; - stdout[i] = stdout[i].split(/\s+/); - stdout[i][0] = parseInt(stdout[i][0], 10); // PPID - stdout[i][1] = parseInt(stdout[i][1], 10); // PID - list.push(stdout[i]); - } - - callback(null, list); + callback(null, parse(stdout)); } catch (error) { callback(error); } }); } - -module.exports = ps; diff --git a/lib/wmic.js b/lib/wmic.js index 0361728..8770c03 100644 --- a/lib/wmic.js +++ b/lib/wmic.js @@ -1,18 +1,22 @@ -'use strict'; - -var os = require('os'); -var bin = require('./bin'); +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(err, list) + * @param {Function} callback callback(err, list) + * @param {Function} [run] Injectable spawner, used for testing. */ -function wmic(callback) { - var args = ['PROCESS', 'get', 'ParentProcessId,ProcessId']; - var options = {windowsHide: true, windowsVerbatimArguments: true}; - bin('wmic', args, options, function(err, stdout, code) { - if (err) { - callback(err); +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; } @@ -21,29 +25,10 @@ function wmic(callback) { return; } - // Example of stdout - // - // ParentProcessId ProcessId - // 0 777 - try { - stdout = stdout.split(os.EOL); - - var list = []; - for (var i = 1; i < stdout.length; i++) { - stdout[i] = stdout[i].trim(); - if (!stdout[i]) continue; - stdout[i] = stdout[i].split(/\s+/); - stdout[i][0] = parseInt(stdout[i][0], 10); // PPID - stdout[i][1] = parseInt(stdout[i][1], 10); // PID - list.push(stdout[i]); - } - - callback(null, list); + callback(null, parse(stdout)); } catch (error) { callback(error); } }); } - -module.exports = wmic; diff --git a/package.json b/package.json index f23f4a6..b73b78b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pidtree", - "version": "0.6.1", + "version": "1.0.0", "description": "Cross platform children list of a PID", "license": "MIT", "homepage": "http://github.com/simonepri/pidtree#readme", @@ -27,6 +27,13 @@ "process", "processes" ], + "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, "main": "index.js", "types": "index.d.ts", "bin": { @@ -39,38 +46,26 @@ "index.d.ts" ], "engines": { - "node": ">=0.10" + "node": ">=18" }, "scripts": { "start": "node ./bin/pidtree.js", - "update": "npm-check -u", - "release": "np", "lint": "xo", - "test": "nyc ava -m \"!*benchmark*\"", - "test:windows": "ava -m \"!*benchmark*\"", + "test": "c8 ava -m \"!*benchmark*\"", "types": "tsd", "bench": "ava -m \"*benchmark*\"" }, "devDependencies": { - "ava": "~0.25.0", - "mockery": "^2.1.0", - "np": "^2.20.1", - "npm-check": "^5.9.2", - "nyc": "^11.6.0", - "pify": "^3.0.0", - "string-to-stream": "^1.1.0", - "through": "^2.3.8", - "time-span": "^2.0.0", - "tree-kill": "^1.1.0", - "tsd": "^0.11.0", - "xo": "~0.20.3" - }, - "ava": { - "verbose": true + "ava": "^6.4.1", + "c8": "^10.1.3", + "time-span": "^5.1.0", + "tree-kill": "^1.2.2", + "tsd": "^0.33.0", + "xo": "^2.0.2" }, - "nyc": { + "c8": { "reporter": [ - "lcovonly", + "lcov", "text" ] }, @@ -78,13 +73,10 @@ "prettier": true, "space": true, "rules": { - "prefer-destructuring": 0, - "prefer-arrow-callback": 0, - "no-var": 0, - "object-shorthand": 0, - "unicorn/no-for-loop": 0, - "unicorn/prefer-string-slice": 0, - "unicorn/string-content": 0 + "n/prefer-global/process": "off", + "n/prefer-global/buffer": "off", + "require-unicode-regexp": "off", + "unicorn/prefer-top-level-await": "off" } } } diff --git a/readme.md b/readme.md index 0d1bebf..f0ccb90 100644 --- a/readme.md +++ b/readme.md @@ -52,17 +52,17 @@ AVA Test Runner used - - - Istanbul Test Coverage used + + + c8 Test Coverage used NI Scaffolding System used - - - NP Release System used + + + release-please Release System used

@@ -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'); -});