From c4ff62d4731adeaf89dd0e9e055c17a7bb477686 Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Tue, 26 Mar 2024 12:34:04 +0300 Subject: [PATCH] feat: promisify responses --- readme.md | 2 +- src/main/ts/index.ts | 3 ++ src/main/ts/ps.ts | 85 +++++++++++++++++++++++++++------------ src/test/ts/index.test.ts | 4 +- src/test/ts/ps.test.ts | 64 +++++++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 28 deletions(-) create mode 100644 src/test/ts/ps.test.ts diff --git a/readme.md b/readme.md index 61d984d..d563f18 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ * [x] Rewritten in TypeScript * [x] CJS and ESM package entry points * [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) -* [ ] Provides promisified responses +* [x] Provides promisified responses * [ ] Brings sync API * [ ] Builds a process tree diff --git a/src/main/ts/index.ts b/src/main/ts/index.ts index 24c8b33..1f62b73 100644 --- a/src/main/ts/index.ts +++ b/src/main/ts/index.ts @@ -1,2 +1,5 @@ +import { kill, lookup } from './ps.ts' + export type * from './ps.ts' export { kill, lookup } from './ps.ts' +export default { lookup, kill } diff --git a/src/main/ts/ps.ts b/src/main/ts/ps.ts index 4d0d9c9..75ee7b5 100644 --- a/src/main/ts/ps.ts +++ b/src/main/ts/ps.ts @@ -6,7 +6,14 @@ import { EOL as SystemEOL } from 'node:os' const EOL = /(\r\n)|(\n\r)|\n|\r/ const IS_WIN = process.platform === 'win32' -const isBin = (f:string): boolean => fs.existsSync(f) && fs.lstatSync(f).isFile() +const isBin = (f: string): boolean => { + if (f === '') return false + if (!f.includes('/')) return true + if (!fs.existsSync(f)) return false + + const stat = fs.lstatSync(f) + return stat.isFile() || stat.isSymbolicLink() +} export type TPsLookupCallback = (err: any, processList?: TPsLookupEntry[]) => void @@ -39,38 +46,44 @@ export type TPsNext = (err?: any) => void * @param {String} query.command RegExp String * @param {String} query.arguments RegExp String * @param {String|String[]} query.psargs - * @param {Function} callback - * @param {Object=null} callback.err - * @param {Object[]} callback.processList + * @param {Function} cb + * @param {Object=null} cb.err + * @param {Object[]} cb.processList * @return {Object} */ -export const lookup = (query: TPsLookupQuery, callback: TPsLookupCallback) => { - // add 'lx' as default ps arguments, since the default ps output in linux like "ubuntu", wont include command arguments - const { psargs = ['-lx'] } = query +export const lookup = (query: TPsLookupQuery = {}, cb: TPsLookupCallback = noop) => { + 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 + const callback: TSpawnCtx['callback'] = (err, {stdout}) => { + if (err) { + reject(err) + cb(err) + return + } + + const list = parseProcessList(extract(stdout), query) + resolve(list) + cb(null, list) + } const ctx: TSpawnCtx = IS_WIN ? { cmd: 'cmd', input: 'wmic process get ProcessId,ParentProcessId,CommandLine \n', - callback(err, {stdout}) { - if (err) return callback(err) - - callback(null, parseProcessList(extractWmic(stdout), query)) - }, + callback, run(cb) {cb()} } : { cmd: 'ps', args, run(cb) {cb()}, - callback(err, {stdout}) { - if (err) return callback(err) - - return callback(null, parseProcessList(stdout, query)) - } + callback, } exec(ctx) + + return promise } export const parseProcessList = (output: string, query: TPsLookupQuery = {}) => { @@ -112,15 +125,15 @@ export const extractWmic = (stdout: string): string => { * @param next */ // eslint-disable-next-line sonarjs/cognitive-complexity -export const kill = (pid: string | number, opts?: TPsNext | TPsKillOptions | TPsKillOptions['signal'], next?: TPsNext ) => { +export const kill = (pid: string | number, opts?: TPsNext | TPsKillOptions | TPsKillOptions['signal'], next?: TPsNext ): Promise => { if (typeof opts == 'function') { - kill(pid, undefined, opts) - return + return kill(pid, undefined, opts) } if (typeof opts == 'string' || typeof opts == 'number') { - kill(pid, { signal: opts }, next) - return + return kill(pid, { signal: opts }, next) } + + const { promise, resolve, reject } = makeDeferred() const { timeout = 30, signal = 'SIGTERM' @@ -129,7 +142,10 @@ export const kill = (pid: string | number, opts?: TPsNext | TPsKillOptions | TPs try { process.kill(+pid, signal) } catch(e) { - return next?.(e) + reject(e) + next?.(e) + + return promise } let checkConfident = 0 @@ -142,6 +158,7 @@ export const kill = (pid: string | number, opts?: TPsNext | TPsKillOptions | TPs if (err) { clearTimeout(checkTimeoutTimer) + reject(err) finishCallback?.(err) } @@ -154,6 +171,7 @@ export const kill = (pid: string | number, opts?: TPsNext | TPsKillOptions | TPs checkConfident++ if (checkConfident === 5) { clearTimeout(checkTimeoutTimer) + resolve() finishCallback?.() } else { checkKilled(finishCallback) @@ -167,7 +185,11 @@ export const kill = (pid: string | number, opts?: TPsNext | TPsKillOptions | TPs checkIsTimeout = true next(new Error('Kill process timeout')) }, timeout * 1000) + } else { + resolve() } + + return promise } export const parseGrid = (output: string) => @@ -182,8 +204,8 @@ export const formatOutput = (data: TIngridResponse): TPsLookupEntry[] => const cmd = d.CMD || d.CommandLine || d.COMMAND || [] if (pid && cmd.length > 0) { - const c = cmd.findIndex((_v, i) => isBin(cmd.slice(0, i).join(''))) - 1 - const command = cmd.slice(0, c).join('') + const c = (cmd.findIndex((_v, i) => isBin(cmd.slice(0, i).join(' ')))) + const command = cmd.slice(0, c).join(' ') const args = cmd.length > 1 ? cmd.slice(c) : [] m.push({ @@ -197,4 +219,15 @@ export const formatOutput = (data: TIngridResponse): TPsLookupEntry[] => return m }, []) -export default { lookup, kill } +export type PromiseResolve = (value?: T | PromiseLike) => void + +export const makeDeferred = (): { promise: Promise, resolve: PromiseResolve, reject: PromiseResolve } => { + let resolve + let reject + const promise = new Promise((res, rej) => { resolve = res; reject = rej }) + return { resolve, reject, promise } as any +} + +export const noop = () => {/* noop */} + +export const identity = (v: T): T => v diff --git a/src/test/ts/index.test.ts b/src/test/ts/index.test.ts index 483e0e9..d259803 100644 --- a/src/test/ts/index.test.ts +++ b/src/test/ts/index.test.ts @@ -1,9 +1,11 @@ import * as assert from 'node:assert' import { describe, it } from 'node:test' -import { kill, lookup } from '../../main/ts/index.ts' +import ps, { kill, lookup } from '../../main/ts/index.ts' describe('index', () => { it('has proper exports', () => { + assert.equal(ps.lookup, lookup) + assert.equal(ps.kill, kill) assert.equal(typeof lookup, 'function') assert.equal(typeof kill, 'function') }) diff --git a/src/test/ts/ps.test.ts b/src/test/ts/ps.test.ts new file mode 100644 index 0000000..8d93ecd --- /dev/null +++ b/src/test/ts/ps.test.ts @@ -0,0 +1,64 @@ +import * as assert from 'node:assert' +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' + +const __dirname = new URL('.', import.meta.url).pathname +const testScript = path.resolve(__dirname, '../legacy/node_process_for_test.cjs') +const testScriptArgs = ['--foo', '--bar', Math.random().toString(16).slice(2)] + +describe('lookup()', () => { + let pid: number + before(() => { + pid = cp.fork(testScript, testScriptArgs).pid as number + }) + + after(() => { + try { + process.kill(pid) + } catch (err) { void err } + }) + + it('returns a process list', async () => { + const list = await lookup() + assert.ok(list.length > 0) + }) + + it('searches process by pid', async () => { + const list = await lookup({ pid }) + const found = list[0] + + assert.equal(list.length, 1) + assert.equal(found.pid, pid) + }) + + it('filters by args', async () => { + const list = await lookup({ arguments: testScriptArgs[2] }) + + assert.equal(list.length, 1) + assert.equal(list[0].pid, pid) + }) +}) + +describe('kill()', () => { + it('kills a process', async () => { + const pid = cp.fork(testScript, testScriptArgs).pid as number + assert.equal((await lookup({ pid })).length, 1) + await kill(pid) + assert.equal((await lookup({ pid })).length, 0) + }) + + it('kills with check', async () => { + let cheked = false + const cb = () => cheked = true + + const pid = cp.fork(testScript, testScriptArgs).pid as number + assert.equal((await lookup({ pid })).length, 1) + + await kill(pid, {timeout: 1}, cb) + assert.equal((await lookup({ pid })).length, 0) + assert.equal(cheked, true) + }) +})