Skip to content

Commit

Permalink
feat: promisify responses
Browse files Browse the repository at this point in the history
  • Loading branch information
antongolub committed Mar 26, 2024
1 parent 528ed85 commit c4ff62d
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 28 deletions.
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions src/main/ts/index.ts
Original file line number Diff line number Diff line change
@@ -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 }
85 changes: 59 additions & 26 deletions src/main/ts/ps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 = {}) => {
Expand Down Expand Up @@ -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<void> => {
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'
Expand All @@ -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
Expand All @@ -142,6 +158,7 @@ export const kill = (pid: string | number, opts?: TPsNext | TPsKillOptions | TPs

if (err) {
clearTimeout(checkTimeoutTimer)
reject(err)
finishCallback?.(err)
}

Expand All @@ -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)
Expand All @@ -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) =>
Expand All @@ -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({
Expand All @@ -197,4 +219,15 @@ export const formatOutput = (data: TIngridResponse): TPsLookupEntry[] =>
return m
}, [])

export default { lookup, kill }
export type PromiseResolve<T = any> = (value?: T | PromiseLike<T>) => void

export const makeDeferred = <T = any, E = any>(): { promise: Promise<T>, resolve: PromiseResolve<T>, reject: PromiseResolve<E> } => {
let resolve
let reject
const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej })
return { resolve, reject, promise } as any
}

export const noop = () => {/* noop */}

export const identity = <T>(v: T): T => v
4 changes: 3 additions & 1 deletion src/test/ts/index.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
Expand Down
64 changes: 64 additions & 0 deletions src/test/ts/ps.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})

0 comments on commit c4ff62d

Please sign in to comment.