tinyps 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/lib/main.d.ts +2 -6
- package/lib/main.js +32 -59
- package/lib/parse.d.ts +11 -0
- package/lib/parse.js +63 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,6 +41,9 @@ Note that `signal` is not supported on Windows, and will be ignored.
|
|
|
41
41
|
- `pid` (`number`) - process to check
|
|
42
42
|
- Returns `boolean`
|
|
43
43
|
|
|
44
|
+
A zombie process is still in the process table, so this returns `true` for a
|
|
45
|
+
process which has exited but has not yet been released by its parent.
|
|
46
|
+
|
|
44
47
|
### `getDescendants(pid)`
|
|
45
48
|
|
|
46
49
|
- `pid` (`number`) - process to collect descendants of
|
|
@@ -61,6 +64,9 @@ Note that `signal` is not supported on Windows, and will be ignored.
|
|
|
61
64
|
- `options.loose` (`boolean`, optional) - match any name containing `name`, ignoring case
|
|
62
65
|
- Returns `Promise<ProcessInfo[]>`
|
|
63
66
|
|
|
67
|
+
Names come from the process' `argv[0]`, so a process which has renamed itself
|
|
68
|
+
(e.g. via `process.title`) matches its new name rather than its executable.
|
|
69
|
+
|
|
64
70
|
### `findProcessesByPort(port, options?)`
|
|
65
71
|
|
|
66
72
|
- `port` (`number`) - local port to match against
|
package/lib/main.d.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
+
import { type ProcessInfo } from './parse.js';
|
|
2
|
+
export type { ProcessInfo };
|
|
1
3
|
export type ProcessTree = Map<number, number[]>;
|
|
2
4
|
export type Protocol = 'tcp' | 'udp';
|
|
3
|
-
export interface ProcessInfo {
|
|
4
|
-
pid: number;
|
|
5
|
-
ppid: number;
|
|
6
|
-
name: string;
|
|
7
|
-
command: string;
|
|
8
|
-
}
|
|
9
5
|
export declare function killTree(pid: number, signal?: NodeJS.Signals): Promise<void>;
|
|
10
6
|
export declare function listProcesses(): Promise<ProcessInfo[]>;
|
|
11
7
|
export declare function getProcessInfo(pid: number): Promise<ProcessInfo | undefined>;
|
package/lib/main.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { basename } from 'node:path';
|
|
3
2
|
import { platform } from 'node:process';
|
|
3
|
+
import { parseLsofOutput, parseOwningProcessJson, parsePsOutput, parseSsOutput, parseWin32ProcessJson, } from './parse.js';
|
|
4
4
|
async function spawnAsync(command, args, allowedExitCodes) {
|
|
5
5
|
return new Promise((resolve, reject) => {
|
|
6
6
|
const child = spawn(command, args);
|
|
7
7
|
let stdout = '';
|
|
8
8
|
let stderr = '';
|
|
9
|
+
child.stdout.setEncoding('utf8');
|
|
10
|
+
child.stderr.setEncoding('utf8');
|
|
9
11
|
child.stdout.on('data', (data) => {
|
|
10
|
-
stdout += data
|
|
12
|
+
stdout += data;
|
|
11
13
|
});
|
|
12
14
|
child.stderr.on('data', (data) => {
|
|
13
|
-
stderr += data
|
|
15
|
+
stderr += data;
|
|
14
16
|
});
|
|
15
17
|
child.on('close', (code) => {
|
|
16
18
|
const allowed = allowedExitCodes === undefined || code === null
|
|
@@ -29,21 +31,31 @@ async function spawnAsync(command, args, allowedExitCodes) {
|
|
|
29
31
|
});
|
|
30
32
|
}
|
|
31
33
|
function killAll(processTree, signal) {
|
|
32
|
-
|
|
34
|
+
const errors = [];
|
|
35
|
+
const pids = Array.from(processTree.keys()).reverse();
|
|
36
|
+
for (const pid of pids) {
|
|
33
37
|
try {
|
|
34
38
|
process.kill(pid, signal);
|
|
35
39
|
}
|
|
36
40
|
catch (err) {
|
|
37
41
|
if (err.code !== 'ESRCH') {
|
|
38
|
-
|
|
42
|
+
errors.push(err);
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
}
|
|
46
|
+
if (errors.length > 0) {
|
|
47
|
+
throw new AggregateError(errors, 'Failed to kill one or more processes');
|
|
48
|
+
}
|
|
42
49
|
}
|
|
43
50
|
function buildProcessTree(pid, processes) {
|
|
44
51
|
const childrenByParent = new Map();
|
|
52
|
+
let exists = false;
|
|
45
53
|
for (const [childPid, parentPid] of processes) {
|
|
46
54
|
if (childPid === pid) {
|
|
55
|
+
exists = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (childPid <= 0) {
|
|
47
59
|
continue;
|
|
48
60
|
}
|
|
49
61
|
const children = childrenByParent.get(parentPid) ?? [];
|
|
@@ -51,6 +63,9 @@ function buildProcessTree(pid, processes) {
|
|
|
51
63
|
childrenByParent.set(parentPid, children);
|
|
52
64
|
}
|
|
53
65
|
const processTree = new Map();
|
|
66
|
+
if (!exists) {
|
|
67
|
+
return processTree;
|
|
68
|
+
}
|
|
54
69
|
const queue = [pid];
|
|
55
70
|
for (const current of queue) {
|
|
56
71
|
const children = childrenByParent.get(current) ?? [];
|
|
@@ -59,14 +74,6 @@ function buildProcessTree(pid, processes) {
|
|
|
59
74
|
}
|
|
60
75
|
return processTree;
|
|
61
76
|
}
|
|
62
|
-
// This matches `{ppid} {pid} {command}`, allowing spaces in the command
|
|
63
|
-
const processListPattern = /^\s*(\d+)\s+(\d+)\s+(.*)$/gm;
|
|
64
|
-
// `ss` reports socket owners as `users:(("name",pid=123,fd=4),...)`
|
|
65
|
-
const socketOwnerPattern = /pid=(\d+)/g;
|
|
66
|
-
// This matches the extensions windows includes in process names
|
|
67
|
-
const executableExtensionPattern = /\.exe$/i;
|
|
68
|
-
// Login shells are executed with a `-` prefixed to their argv[0]
|
|
69
|
-
const loginShellPattern = /^-/;
|
|
70
77
|
async function listProcessesUnix() {
|
|
71
78
|
const { stdout } = await spawnAsync('ps', [
|
|
72
79
|
'-A',
|
|
@@ -74,33 +81,17 @@ async function listProcessesUnix() {
|
|
|
74
81
|
'-o',
|
|
75
82
|
'ppid=,pid=,args=',
|
|
76
83
|
]);
|
|
77
|
-
return
|
|
78
|
-
const command = match[3].trim();
|
|
79
|
-
const end = command.indexOf(' ');
|
|
80
|
-
const executable = end === -1 ? command : command.slice(0, end);
|
|
81
|
-
return {
|
|
82
|
-
pid: Number(match[2]),
|
|
83
|
-
ppid: Number(match[1]),
|
|
84
|
-
name: basename(executable.replace(loginShellPattern, '')),
|
|
85
|
-
command,
|
|
86
|
-
};
|
|
87
|
-
});
|
|
84
|
+
return parsePsOutput(stdout);
|
|
88
85
|
}
|
|
89
86
|
async function listProcessesWindows() {
|
|
90
87
|
const { stdout } = await spawnAsync('powershell.exe', [
|
|
91
88
|
'-NoProfile',
|
|
92
89
|
'-NonInteractive',
|
|
93
90
|
'-Command',
|
|
94
|
-
'
|
|
91
|
+
'[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); ' +
|
|
92
|
+
'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress',
|
|
95
93
|
]);
|
|
96
|
-
|
|
97
|
-
const processes = Array.isArray(parsed) ? parsed : [parsed];
|
|
98
|
-
return processes.map((proc) => ({
|
|
99
|
-
pid: proc.ProcessId,
|
|
100
|
-
ppid: proc.ParentProcessId,
|
|
101
|
-
name: proc.Name.replace(executableExtensionPattern, ''),
|
|
102
|
-
command: proc.CommandLine ?? proc.Name,
|
|
103
|
-
}));
|
|
94
|
+
return parseWin32ProcessJson(stdout);
|
|
104
95
|
}
|
|
105
96
|
async function findPidsByPortLinux(port, protocol) {
|
|
106
97
|
const protocolArgs = [];
|
|
@@ -115,7 +106,7 @@ async function findPidsByPortLinux(port, protocol) {
|
|
|
115
106
|
...protocolArgs,
|
|
116
107
|
`sport = :${port}`,
|
|
117
108
|
]);
|
|
118
|
-
return
|
|
109
|
+
return parseSsOutput(stdout);
|
|
119
110
|
}
|
|
120
111
|
async function findPidsByPortDarwin(port, protocol) {
|
|
121
112
|
const selector = protocol === undefined
|
|
@@ -123,25 +114,7 @@ async function findPidsByPortDarwin(port, protocol) {
|
|
|
123
114
|
: `-i${protocol.toUpperCase()}:${port}`;
|
|
124
115
|
// lsof exits 1 when nothing matches
|
|
125
116
|
const { stdout } = await spawnAsync('lsof', ['-nP', '-Fpn', selector], [0, 1]);
|
|
126
|
-
|
|
127
|
-
let currentPid;
|
|
128
|
-
// Field output is a `p{pid}` line followed by an `n{address}` line
|
|
129
|
-
for (const line of stdout.split('\n')) {
|
|
130
|
-
const value = line.slice(1);
|
|
131
|
-
if (line[0] === 'p') {
|
|
132
|
-
currentPid = Number(value);
|
|
133
|
-
}
|
|
134
|
-
else if (line[0] === 'n' && currentPid !== undefined) {
|
|
135
|
-
// Example connected output: 192.168.1.99:57253->1.2.3.4:443
|
|
136
|
-
// Example listening output: [::1]:57355
|
|
137
|
-
const separator = value.indexOf('->');
|
|
138
|
-
const local = separator === -1 ? value : value.slice(0, separator);
|
|
139
|
-
if (Number(local.slice(local.lastIndexOf(':') + 1)) === port) {
|
|
140
|
-
pids.push(currentPid);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return pids;
|
|
117
|
+
return parseLsofOutput(stdout, port);
|
|
145
118
|
}
|
|
146
119
|
async function findPidsByPortWindows(port, protocol) {
|
|
147
120
|
const queries = [];
|
|
@@ -158,14 +131,11 @@ async function findPidsByPortWindows(port, protocol) {
|
|
|
158
131
|
'-NoProfile',
|
|
159
132
|
'-NonInteractive',
|
|
160
133
|
'-Command',
|
|
161
|
-
|
|
134
|
+
'[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); ' +
|
|
135
|
+
`@(${query}) | Select-Object -ExpandProperty OwningProcess | ` +
|
|
162
136
|
'ConvertTo-Json -Compress',
|
|
163
137
|
]);
|
|
164
|
-
|
|
165
|
-
return [];
|
|
166
|
-
}
|
|
167
|
-
const parsed = JSON.parse(stdout);
|
|
168
|
-
return Array.isArray(parsed) ? parsed : [parsed];
|
|
138
|
+
return parseOwningProcessJson(stdout);
|
|
169
139
|
}
|
|
170
140
|
async function killTreeWindows(pid) {
|
|
171
141
|
// taskkill on windows exits 128 when its not found.
|
|
@@ -177,6 +147,9 @@ async function killTreeUnix(pid, signal) {
|
|
|
177
147
|
killAll(processTree, signal);
|
|
178
148
|
}
|
|
179
149
|
export async function killTree(pid, signal) {
|
|
150
|
+
if (pid <= 0) {
|
|
151
|
+
throw new TypeError(`Expected a positive pid, received: ${pid}`);
|
|
152
|
+
}
|
|
180
153
|
if (platform === 'win32') {
|
|
181
154
|
return killTreeWindows(pid);
|
|
182
155
|
}
|
package/lib/parse.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ProcessInfo {
|
|
2
|
+
pid: number;
|
|
3
|
+
ppid: number;
|
|
4
|
+
name: string;
|
|
5
|
+
command: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function parsePsOutput(stdout: string): ProcessInfo[];
|
|
8
|
+
export declare function parseWin32ProcessJson(stdout: string): ProcessInfo[];
|
|
9
|
+
export declare function parseSsOutput(stdout: string): number[];
|
|
10
|
+
export declare function parseLsofOutput(stdout: string, port: number): number[];
|
|
11
|
+
export declare function parseOwningProcessJson(stdout: string): number[];
|
package/lib/parse.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
// This matches `{ppid} {pid} {command}`, allowing spaces in the command
|
|
3
|
+
const processListPattern = /^\s*(\d+)\s+(\d+)\s+(.*)$/gm;
|
|
4
|
+
// `ss` reports socket owners as `users:(("name",pid=123,fd=4),...)`
|
|
5
|
+
const socketOwnerPattern = /pid=(\d+)/g;
|
|
6
|
+
// This matches the extensions windows includes in process names
|
|
7
|
+
const executableExtensionPattern = /\.exe$/i;
|
|
8
|
+
// Login shells are executed with a `-` prefixed to their argv[0]
|
|
9
|
+
const loginShellPattern = /^-/;
|
|
10
|
+
export function parsePsOutput(stdout) {
|
|
11
|
+
return Array.from(stdout.matchAll(processListPattern), (match) => {
|
|
12
|
+
const command = match[3].trim();
|
|
13
|
+
const end = command.indexOf(' ');
|
|
14
|
+
const executable = end === -1 ? command : command.slice(0, end);
|
|
15
|
+
return {
|
|
16
|
+
pid: Number(match[2]),
|
|
17
|
+
ppid: Number(match[1]),
|
|
18
|
+
name: basename(executable.replace(loginShellPattern, '')),
|
|
19
|
+
command,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function parseWin32ProcessJson(stdout) {
|
|
24
|
+
const parsed = JSON.parse(stdout);
|
|
25
|
+
const processes = Array.isArray(parsed) ? parsed : [parsed];
|
|
26
|
+
return processes.map((proc) => ({
|
|
27
|
+
pid: proc.ProcessId,
|
|
28
|
+
ppid: proc.ParentProcessId,
|
|
29
|
+
name: proc.Name.replace(executableExtensionPattern, ''),
|
|
30
|
+
command: proc.CommandLine ?? proc.Name,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
export function parseSsOutput(stdout) {
|
|
34
|
+
return Array.from(stdout.matchAll(socketOwnerPattern), (match) => Number(match[1]));
|
|
35
|
+
}
|
|
36
|
+
export function parseLsofOutput(stdout, port) {
|
|
37
|
+
const pids = [];
|
|
38
|
+
let currentPid;
|
|
39
|
+
// Field output is a `p{pid}` line followed by an `n{address}` line
|
|
40
|
+
for (const line of stdout.split('\n')) {
|
|
41
|
+
const value = line.slice(1);
|
|
42
|
+
if (line[0] === 'p') {
|
|
43
|
+
currentPid = Number(value);
|
|
44
|
+
}
|
|
45
|
+
else if (line[0] === 'n' && currentPid !== undefined) {
|
|
46
|
+
// Example connected output: 192.168.1.99:57253->1.2.3.4:443
|
|
47
|
+
// Example listening output: [::1]:57355
|
|
48
|
+
const separator = value.indexOf('->');
|
|
49
|
+
const local = separator === -1 ? value : value.slice(0, separator);
|
|
50
|
+
if (Number(local.slice(local.lastIndexOf(':') + 1)) === port) {
|
|
51
|
+
pids.push(currentPid);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return pids;
|
|
56
|
+
}
|
|
57
|
+
export function parseOwningProcessJson(stdout) {
|
|
58
|
+
if (stdout.trim() === '') {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const parsed = JSON.parse(stdout);
|
|
62
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
63
|
+
}
|