Skip to content

Commit c4ff62d

Browse files
committedMar 26, 2024··
feat: promisify responses
1 parent 528ed85 commit c4ff62d

File tree

5 files changed

+130
-28
lines changed

5 files changed

+130
-28
lines changed
 

‎readme.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* [x] Rewritten in TypeScript
77
* [x] CJS and ESM package entry points
88
* [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)
9-
* [ ] Provides promisified responses
9+
* [x] Provides promisified responses
1010
* [ ] Brings sync API
1111
* [ ] Builds a process tree
1212

‎src/main/ts/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1+
import { kill, lookup } from './ps.ts'
2+
13
export type * from './ps.ts'
24
export { kill, lookup } from './ps.ts'
5+
export default { lookup, kill }

‎src/main/ts/ps.ts

+59-26
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ import { EOL as SystemEOL } from 'node:os'
66

77
const EOL = /(\r\n)|(\n\r)|\n|\r/
88
const IS_WIN = process.platform === 'win32'
9-
const isBin = (f:string): boolean => fs.existsSync(f) && fs.lstatSync(f).isFile()
9+
const isBin = (f: string): boolean => {
10+
if (f === '') return false
11+
if (!f.includes('/')) return true
12+
if (!fs.existsSync(f)) return false
13+
14+
const stat = fs.lstatSync(f)
15+
return stat.isFile() || stat.isSymbolicLink()
16+
}
1017

1118
export type TPsLookupCallback = (err: any, processList?: TPsLookupEntry[]) => void
1219

@@ -39,38 +46,44 @@ export type TPsNext = (err?: any) => void
3946
* @param {String} query.command RegExp String
4047
* @param {String} query.arguments RegExp String
4148
* @param {String|String[]} query.psargs
42-
* @param {Function} callback
43-
* @param {Object=null} callback.err
44-
* @param {Object[]} callback.processList
49+
* @param {Function} cb
50+
* @param {Object=null} cb.err
51+
* @param {Object[]} cb.processList
4552
* @return {Object}
4653
*/
47-
export const lookup = (query: TPsLookupQuery, callback: TPsLookupCallback) => {
48-
// add 'lx' as default ps arguments, since the default ps output in linux like "ubuntu", wont include command arguments
49-
const { psargs = ['-lx'] } = query
54+
export const lookup = (query: TPsLookupQuery = {}, cb: TPsLookupCallback = noop) => {
55+
const { promise, resolve, reject } = makeDeferred()
56+
const { psargs = ['-lx'] } = query // add 'lx' as default ps arguments, since the default ps output in linux like "ubuntu", wont include command arguments
5057
const args = typeof psargs === 'string' ? psargs.split(/\s+/) : psargs
58+
const extract = IS_WIN ? extractWmic : identity
59+
const callback: TSpawnCtx['callback'] = (err, {stdout}) => {
60+
if (err) {
61+
reject(err)
62+
cb(err)
63+
return
64+
}
65+
66+
const list = parseProcessList(extract(stdout), query)
67+
resolve(list)
68+
cb(null, list)
69+
}
5170
const ctx: TSpawnCtx = IS_WIN
5271
? {
5372
cmd: 'cmd',
5473
input: 'wmic process get ProcessId,ParentProcessId,CommandLine \n',
55-
callback(err, {stdout}) {
56-
if (err) return callback(err)
57-
58-
callback(null, parseProcessList(extractWmic(stdout), query))
59-
},
74+
callback,
6075
run(cb) {cb()}
6176
}
6277
: {
6378
cmd: 'ps',
6479
args,
6580
run(cb) {cb()},
66-
callback(err, {stdout}) {
67-
if (err) return callback(err)
68-
69-
return callback(null, parseProcessList(stdout, query))
70-
}
81+
callback,
7182
}
7283

7384
exec(ctx)
85+
86+
return promise
7487
}
7588

7689
export const parseProcessList = (output: string, query: TPsLookupQuery = {}) => {
@@ -112,15 +125,15 @@ export const extractWmic = (stdout: string): string => {
112125
* @param next
113126
*/
114127
// eslint-disable-next-line sonarjs/cognitive-complexity
115-
export const kill = (pid: string | number, opts?: TPsNext | TPsKillOptions | TPsKillOptions['signal'], next?: TPsNext ) => {
128+
export const kill = (pid: string | number, opts?: TPsNext | TPsKillOptions | TPsKillOptions['signal'], next?: TPsNext ): Promise<void> => {
116129
if (typeof opts == 'function') {
117-
kill(pid, undefined, opts)
118-
return
130+
return kill(pid, undefined, opts)
119131
}
120132
if (typeof opts == 'string' || typeof opts == 'number') {
121-
kill(pid, { signal: opts }, next)
122-
return
133+
return kill(pid, { signal: opts }, next)
123134
}
135+
136+
const { promise, resolve, reject } = makeDeferred()
124137
const {
125138
timeout = 30,
126139
signal = 'SIGTERM'
@@ -129,7 +142,10 @@ export const kill = (pid: string | number, opts?: TPsNext | TPsKillOptions | TPs
129142
try {
130143
process.kill(+pid, signal)
131144
} catch(e) {
132-
return next?.(e)
145+
reject(e)
146+
next?.(e)
147+
148+
return promise
133149
}
134150

135151
let checkConfident = 0
@@ -142,6 +158,7 @@ export const kill = (pid: string | number, opts?: TPsNext | TPsKillOptions | TPs
142158

143159
if (err) {
144160
clearTimeout(checkTimeoutTimer)
161+
reject(err)
145162
finishCallback?.(err)
146163
}
147164

@@ -154,6 +171,7 @@ export const kill = (pid: string | number, opts?: TPsNext | TPsKillOptions | TPs
154171
checkConfident++
155172
if (checkConfident === 5) {
156173
clearTimeout(checkTimeoutTimer)
174+
resolve()
157175
finishCallback?.()
158176
} else {
159177
checkKilled(finishCallback)
@@ -167,7 +185,11 @@ export const kill = (pid: string | number, opts?: TPsNext | TPsKillOptions | TPs
167185
checkIsTimeout = true
168186
next(new Error('Kill process timeout'))
169187
}, timeout * 1000)
188+
} else {
189+
resolve()
170190
}
191+
192+
return promise
171193
}
172194

173195
export const parseGrid = (output: string) =>
@@ -182,8 +204,8 @@ export const formatOutput = (data: TIngridResponse): TPsLookupEntry[] =>
182204
const cmd = d.CMD || d.CommandLine || d.COMMAND || []
183205

184206
if (pid && cmd.length > 0) {
185-
const c = cmd.findIndex((_v, i) => isBin(cmd.slice(0, i).join(''))) - 1
186-
const command = cmd.slice(0, c).join('')
207+
const c = (cmd.findIndex((_v, i) => isBin(cmd.slice(0, i).join(' '))))
208+
const command = cmd.slice(0, c).join(' ')
187209
const args = cmd.length > 1 ? cmd.slice(c) : []
188210

189211
m.push({
@@ -197,4 +219,15 @@ export const formatOutput = (data: TIngridResponse): TPsLookupEntry[] =>
197219
return m
198220
}, [])
199221

200-
export default { lookup, kill }
222+
export type PromiseResolve<T = any> = (value?: T | PromiseLike<T>) => void
223+
224+
export const makeDeferred = <T = any, E = any>(): { promise: Promise<T>, resolve: PromiseResolve<T>, reject: PromiseResolve<E> } => {
225+
let resolve
226+
let reject
227+
const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej })
228+
return { resolve, reject, promise } as any
229+
}
230+
231+
export const noop = () => {/* noop */}
232+
233+
export const identity = <T>(v: T): T => v

‎src/test/ts/index.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import * as assert from 'node:assert'
22
import { describe, it } from 'node:test'
3-
import { kill, lookup } from '../../main/ts/index.ts'
3+
import ps, { kill, lookup } from '../../main/ts/index.ts'
44

55
describe('index', () => {
66
it('has proper exports', () => {
7+
assert.equal(ps.lookup, lookup)
8+
assert.equal(ps.kill, kill)
79
assert.equal(typeof lookup, 'function')
810
assert.equal(typeof kill, 'function')
911
})

‎src/test/ts/ps.test.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as assert from 'node:assert'
2+
import { describe, it, before, after } from 'node:test'
3+
import process from 'node:process'
4+
import * as cp from 'node:child_process'
5+
import * as path from 'node:path'
6+
import { kill, lookup } from '../../main/ts/ps.ts'
7+
8+
const __dirname = new URL('.', import.meta.url).pathname
9+
const testScript = path.resolve(__dirname, '../legacy/node_process_for_test.cjs')
10+
const testScriptArgs = ['--foo', '--bar', Math.random().toString(16).slice(2)]
11+
12+
describe('lookup()', () => {
13+
let pid: number
14+
before(() => {
15+
pid = cp.fork(testScript, testScriptArgs).pid as number
16+
})
17+
18+
after(() => {
19+
try {
20+
process.kill(pid)
21+
} catch (err) { void err }
22+
})
23+
24+
it('returns a process list', async () => {
25+
const list = await lookup()
26+
assert.ok(list.length > 0)
27+
})
28+
29+
it('searches process by pid', async () => {
30+
const list = await lookup({ pid })
31+
const found = list[0]
32+
33+
assert.equal(list.length, 1)
34+
assert.equal(found.pid, pid)
35+
})
36+
37+
it('filters by args', async () => {
38+
const list = await lookup({ arguments: testScriptArgs[2] })
39+
40+
assert.equal(list.length, 1)
41+
assert.equal(list[0].pid, pid)
42+
})
43+
})
44+
45+
describe('kill()', () => {
46+
it('kills a process', async () => {
47+
const pid = cp.fork(testScript, testScriptArgs).pid as number
48+
assert.equal((await lookup({ pid })).length, 1)
49+
await kill(pid)
50+
assert.equal((await lookup({ pid })).length, 0)
51+
})
52+
53+
it('kills with check', async () => {
54+
let cheked = false
55+
const cb = () => cheked = true
56+
57+
const pid = cp.fork(testScript, testScriptArgs).pid as number
58+
assert.equal((await lookup({ pid })).length, 1)
59+
60+
await kill(pid, {timeout: 1}, cb)
61+
assert.equal((await lookup({ pid })).length, 0)
62+
assert.equal(cheked, true)
63+
})
64+
})

0 commit comments

Comments
 (0)
Please sign in to comment.