diff --git a/README.md b/README.md index f794bbb..9be05c9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ * [x] `table-parser` replaced with `@webpod/ingrid` to handle some issues: [neekey/ps#76](https://github.com/neekey/ps/issues/76), [neekey/ps#62](https://github.com/neekey/ps/issues/62), [neekey/table-parser#11](https://github.com/neekey/table-parser/issues/11), [neekey/table-parser#18](https://github.com/neekey/table-parser/issues/18) * [x] Provides promisified responses * [ ] Brings sync API -* [ ] Builds a process tree +* [x] Builds a process tree ## Install ```bash @@ -79,6 +79,38 @@ lookup({ }) ``` +### tree() +Returns a child processes list by the specified parent `pid`. Some kind of shortcut for `lookup({ppid: pid})`. +```ts +import { tree } from '@webpod/ps' + +const children = await tree(123) +/** +[ + {pid: 124, ppid: 123}, + {pid: 125, ppid: 123} +] +*/ +``` + +To obtain all nested children, set `recursive` option to `true`: +```ts +const children = await tree({pid: 123, recursive: true}) +/** +[ + {pid: 124, ppid: 123}, + {pid: 125, ppid: 123}, + + {pid: 126, ppid: 124}, + {pid: 127, ppid: 124}, + {pid: 128, ppid: 124}, + + {pid: 129, ppid: 125}, + {pid: 130, ppid: 125}, +] +*/ +``` + ### kill() Eliminates the process by its `pid`. diff --git a/src/main/ts/index.ts b/src/main/ts/index.ts index 1f62b73..66b7ede 100644 --- a/src/main/ts/index.ts +++ b/src/main/ts/index.ts @@ -1,5 +1,5 @@ -import { kill, lookup } from './ps.ts' +import { kill, lookup, tree } from './ps.ts' export type * from './ps.ts' -export { kill, lookup } from './ps.ts' -export default { lookup, kill } +export { kill, lookup, tree } from './ps.ts' +export default { lookup, kill, tree } diff --git a/src/main/ts/ps.ts b/src/main/ts/ps.ts index edbbd52..6e401e5 100644 --- a/src/main/ts/ps.ts +++ b/src/main/ts/ps.ts @@ -28,7 +28,7 @@ export type TPsLookupQuery = { pid?: number | string | (string | number)[] command?: string arguments?: string - ppid?: string + ppid?: number | string psargs?: string | string[] } @@ -52,7 +52,7 @@ export type TPsNext = (err?: any, data?: any) => void * @return {Object} */ export const lookup = (query: TPsLookupQuery = {}, cb: TPsLookupCallback = noop) => { - const { promise, resolve, reject } = makeDeferred() + const { promise, resolve, reject } = makeDeferred() const { psargs = ['-lx'] } = query // add 'lx' as default ps arguments, since the default ps output in linux like "ubuntu", wont include command arguments const args = typeof psargs === 'string' ? psargs.split(/\s+/) : psargs const extract = IS_WIN ? extractWmic : identity @@ -94,7 +94,7 @@ export const parseProcessList = (output: string, query: TPsLookupQuery = {}) => const filter = (['command', 'arguments', 'ppid'] as Array) .reduce((m, k) => { const param = query[k] - if (param) m[k] = new RegExp(param, 'i') + if (param) m[k] = new RegExp(param + '', 'i') return m }, {} as Record) @@ -116,6 +116,36 @@ export const extractWmic = (stdout: string): string => { return _stdout.join(SystemEOL) } +export type TPsTreeOpts = { + pid: string | number + recursive?: boolean +} + +export const pickTree = (list: TPsLookupEntry[], pid: string | number, recursive = false): TPsLookupEntry[] => { + const children = list.filter(p => p.ppid === pid + '') + return [ + ...children, + ...children.flatMap(p => recursive ? pickTree(list, p.pid, true) : []) + ] +} + +export const tree = async (opts: string | number | TPsTreeOpts, cb: TPsLookupCallback = noop): Promise => { + if (typeof opts === 'string' || typeof opts === 'number') { + return tree({ pid: opts }, cb) + } + + try { + const {pid, recursive = false} = opts + const list = pickTree(await lookup(), pid, recursive) + + cb(null, list) + return list + } catch (err) { + cb(err) + throw err + } +} + /** * Kill process * @param pid diff --git a/src/test/legacy/node_process_for_test.cjs b/src/test/legacy/node_process_for_test.cjs index da7c0cd..3d761d7 100644 --- a/src/test/legacy/node_process_for_test.cjs +++ b/src/test/legacy/node_process_for_test.cjs @@ -1,10 +1,26 @@ +var cp = require('node:child_process') +var process = require('node:process') var now = Date.now(); -console.log('[child] child process start!'); +var argv = process.argv.slice(2); +var marker = argv[0] +var fork = +argv.find(v => v.startsWith('--fork='))?.slice(7) || 0; +var depth = +argv.find(v => v.startsWith('--depth='))?.slice(8) || 0; -function doSomething() { - return null; +while(depth) { + depth-- + const _fork = fork + while (fork) { + fork-- + cp.fork(__filename, [marker, `--depth=${depth}`, `--fork=${_fork}`]) + } } +console.log('[child] child process start!', 'argv=', argv); + setInterval(function () { doSomething(); }, 50); + +function doSomething() { + return null; +} diff --git a/src/test/ts/index.test.ts b/src/test/ts/index.test.ts index d259803..0635062 100644 --- a/src/test/ts/index.test.ts +++ b/src/test/ts/index.test.ts @@ -1,12 +1,14 @@ import * as assert from 'node:assert' import { describe, it } from 'node:test' -import ps, { kill, lookup } from '../../main/ts/index.ts' +import ps, { kill, lookup, tree } from '../../main/ts/index.ts' describe('index', () => { it('has proper exports', () => { assert.equal(ps.lookup, lookup) assert.equal(ps.kill, kill) + assert.equal(ps.tree, tree) assert.equal(typeof lookup, 'function') assert.equal(typeof kill, 'function') + assert.equal(typeof tree, 'function') }) }) diff --git a/src/test/ts/ps.test.ts b/src/test/ts/ps.test.ts index 4b86300..843709d 100644 --- a/src/test/ts/ps.test.ts +++ b/src/test/ts/ps.test.ts @@ -3,11 +3,12 @@ import { describe, it, before, after } from 'node:test' import process from 'node:process' import * as cp from 'node:child_process' import * as path from 'node:path' -import { kill, lookup } from '../../main/ts/ps.ts' +import { kill, lookup, tree } from '../../main/ts/ps.ts' const __dirname = new URL('.', import.meta.url).pathname +const marker = Math.random().toString(16).slice(2) const testScript = path.resolve(__dirname, '../legacy/node_process_for_test.cjs') -const testScriptArgs = ['--foo', '--bar', Math.random().toString(16).slice(2)] +const testScriptArgs = [marker, '--foo', '--bar'] describe('lookup()', () => { let pid: number @@ -35,7 +36,7 @@ describe('lookup()', () => { }) it('filters by args', async () => { - const list = await lookup({ arguments: testScriptArgs[2] }) + const list = await lookup({ arguments: marker }) assert.equal(list.length, 1) assert.equal(list[0].pid, pid) @@ -63,3 +64,23 @@ describe('kill()', () => { assert.equal(cheked, true) }) }) + +describe('tree()', () => { + it('returns 1st level child', async () => { + const pid = cp.fork(testScript, [...testScriptArgs, '--fork=1', '--depth=2']).pid as number + await new Promise(resolve => setTimeout(resolve, 2000)) // wait for child process to spawn + + const list = await lookup({ arguments: marker }) + const children = await tree(pid) + const childrenAll = await tree({pid, recursive: true}) + + await Promise.all(list.map(p => kill(p.pid))) + await kill(pid) + + assert.equal(children.length, 1) + assert.equal(childrenAll.length, 2) + assert.equal(list.length, 3) + + assert.equal((await lookup({ arguments: marker })).length, 0) + }) +})