tinyps 0.2.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/LICENSE +21 -0
- package/README.md +76 -0
- package/lib/main.d.ts +20 -0
- package/lib/main.js +240 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tinylibs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# ⚙️ tinyps
|
|
2
|
+
|
|
3
|
+
A cross-platform library of utilities for dealing with system processes.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install tinyps
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { killTree, getProcessTree } from 'tinyps';
|
|
15
|
+
|
|
16
|
+
// kill a process and everything it spawned
|
|
17
|
+
await killTree(child.pid);
|
|
18
|
+
|
|
19
|
+
// or inspect the tree first
|
|
20
|
+
const tree = await getProcessTree(child.pid);
|
|
21
|
+
console.log(tree.get(child.pid)); // [ 1234, 1235 ]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## API
|
|
25
|
+
|
|
26
|
+
### `killTree(pid, signal?)`
|
|
27
|
+
|
|
28
|
+
- `pid` (`number`) - process to kill, along with all of its descendants
|
|
29
|
+
- `signal` (`NodeJS.Signals`, optional) - signal to send, defaults to `SIGTERM`
|
|
30
|
+
- Returns `Promise<void>`
|
|
31
|
+
|
|
32
|
+
Note that `signal` is not supported on Windows, and will be ignored.
|
|
33
|
+
|
|
34
|
+
### `getProcessTree(pid)`
|
|
35
|
+
|
|
36
|
+
- `pid` (`number`) - process to build the tree from
|
|
37
|
+
- Returns `Promise<ProcessTree>`
|
|
38
|
+
|
|
39
|
+
### `isRunning(pid)`
|
|
40
|
+
|
|
41
|
+
- `pid` (`number`) - process to check
|
|
42
|
+
- Returns `boolean`
|
|
43
|
+
|
|
44
|
+
### `getDescendants(pid)`
|
|
45
|
+
|
|
46
|
+
- `pid` (`number`) - process to collect descendants of
|
|
47
|
+
- Returns `Promise<number[]>`
|
|
48
|
+
|
|
49
|
+
### `listProcesses()`
|
|
50
|
+
|
|
51
|
+
- Returns `Promise<ProcessInfo[]>`
|
|
52
|
+
|
|
53
|
+
### `getProcessInfo(pid)`
|
|
54
|
+
|
|
55
|
+
- `pid` (`number`) - process to look up
|
|
56
|
+
- Returns `Promise<ProcessInfo | undefined>`
|
|
57
|
+
|
|
58
|
+
### `findProcessesByName(name, options?)`
|
|
59
|
+
|
|
60
|
+
- `name` (`string`) - name to match against
|
|
61
|
+
- `options.loose` (`boolean`, optional) - match any name containing `name`, ignoring case
|
|
62
|
+
- Returns `Promise<ProcessInfo[]>`
|
|
63
|
+
|
|
64
|
+
### `findProcessesByPort(port, options?)`
|
|
65
|
+
|
|
66
|
+
- `port` (`number`) - local port to match against
|
|
67
|
+
- `options.protocol` (`'tcp' | 'udp'`, optional) - only match sockets of this
|
|
68
|
+
protocol, defaults to matching both
|
|
69
|
+
- Returns `Promise<ProcessInfo[]>`
|
|
70
|
+
|
|
71
|
+
Matches any process holding a socket bound to `port` locally, whether it is
|
|
72
|
+
listening or connected.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
package/lib/main.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type ProcessTree = Map<number, number[]>;
|
|
2
|
+
export type Protocol = 'tcp' | 'udp';
|
|
3
|
+
export interface ProcessInfo {
|
|
4
|
+
pid: number;
|
|
5
|
+
ppid: number;
|
|
6
|
+
name: string;
|
|
7
|
+
command: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function killTree(pid: number, signal?: NodeJS.Signals): Promise<void>;
|
|
10
|
+
export declare function listProcesses(): Promise<ProcessInfo[]>;
|
|
11
|
+
export declare function getProcessInfo(pid: number): Promise<ProcessInfo | undefined>;
|
|
12
|
+
export declare function findProcessesByName(name: string, options?: {
|
|
13
|
+
loose?: boolean;
|
|
14
|
+
}): Promise<ProcessInfo[]>;
|
|
15
|
+
export declare function findProcessesByPort(port: number, options?: {
|
|
16
|
+
protocol?: Protocol;
|
|
17
|
+
}): Promise<ProcessInfo[]>;
|
|
18
|
+
export declare function getProcessTree(pid: number): Promise<ProcessTree>;
|
|
19
|
+
export declare function isRunning(pid: number): boolean;
|
|
20
|
+
export declare function getDescendants(pid: number): Promise<number[]>;
|
package/lib/main.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { platform } from 'node:process';
|
|
4
|
+
async function spawnAsync(command, args, allowedExitCodes) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const child = spawn(command, args);
|
|
7
|
+
let stdout = '';
|
|
8
|
+
let stderr = '';
|
|
9
|
+
child.stdout.on('data', (data) => {
|
|
10
|
+
stdout += data.toString();
|
|
11
|
+
});
|
|
12
|
+
child.stderr.on('data', (data) => {
|
|
13
|
+
stderr += data.toString();
|
|
14
|
+
});
|
|
15
|
+
child.on('close', (code) => {
|
|
16
|
+
const allowed = allowedExitCodes === undefined || code === null
|
|
17
|
+
? code === 0
|
|
18
|
+
: allowedExitCodes.includes(code);
|
|
19
|
+
if (allowed) {
|
|
20
|
+
resolve({ stdout, stderr });
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
reject(new Error(`Command failed with exit code ${code}: ${stderr}`));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
child.on('error', (err) => {
|
|
27
|
+
reject(err);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function killAll(processTree, signal) {
|
|
32
|
+
for (const pid of processTree.keys()) {
|
|
33
|
+
try {
|
|
34
|
+
process.kill(pid, signal);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (err.code !== 'ESRCH') {
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function buildProcessTree(pid, processes) {
|
|
44
|
+
const childrenByParent = new Map();
|
|
45
|
+
for (const [childPid, parentPid] of processes) {
|
|
46
|
+
if (childPid === pid) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const children = childrenByParent.get(parentPid) ?? [];
|
|
50
|
+
children.push(childPid);
|
|
51
|
+
childrenByParent.set(parentPid, children);
|
|
52
|
+
}
|
|
53
|
+
const processTree = new Map();
|
|
54
|
+
const queue = [pid];
|
|
55
|
+
for (const current of queue) {
|
|
56
|
+
const children = childrenByParent.get(current) ?? [];
|
|
57
|
+
processTree.set(current, children);
|
|
58
|
+
queue.push(...children);
|
|
59
|
+
}
|
|
60
|
+
return processTree;
|
|
61
|
+
}
|
|
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
|
+
async function listProcessesUnix() {
|
|
71
|
+
const { stdout } = await spawnAsync('ps', [
|
|
72
|
+
'-A',
|
|
73
|
+
'-ww',
|
|
74
|
+
'-o',
|
|
75
|
+
'ppid=,pid=,args=',
|
|
76
|
+
]);
|
|
77
|
+
return Array.from(stdout.matchAll(processListPattern), (match) => {
|
|
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
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async function listProcessesWindows() {
|
|
90
|
+
const { stdout } = await spawnAsync('powershell.exe', [
|
|
91
|
+
'-NoProfile',
|
|
92
|
+
'-NonInteractive',
|
|
93
|
+
'-Command',
|
|
94
|
+
'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress',
|
|
95
|
+
]);
|
|
96
|
+
const parsed = JSON.parse(stdout);
|
|
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
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
async function findPidsByPortLinux(port, protocol) {
|
|
106
|
+
const protocolArgs = [];
|
|
107
|
+
if (protocol !== 'udp') {
|
|
108
|
+
protocolArgs.push('-t');
|
|
109
|
+
}
|
|
110
|
+
if (protocol !== 'tcp') {
|
|
111
|
+
protocolArgs.push('-u');
|
|
112
|
+
}
|
|
113
|
+
const { stdout } = await spawnAsync('ss', [
|
|
114
|
+
'-nHpa',
|
|
115
|
+
...protocolArgs,
|
|
116
|
+
`sport = :${port}`,
|
|
117
|
+
]);
|
|
118
|
+
return Array.from(stdout.matchAll(socketOwnerPattern), (match) => Number(match[1]));
|
|
119
|
+
}
|
|
120
|
+
async function findPidsByPortDarwin(port, protocol) {
|
|
121
|
+
const selector = protocol === undefined
|
|
122
|
+
? `-i:${port}`
|
|
123
|
+
: `-i${protocol.toUpperCase()}:${port}`;
|
|
124
|
+
// lsof exits 1 when nothing matches
|
|
125
|
+
const { stdout } = await spawnAsync('lsof', ['-nP', '-Fpn', selector], [0, 1]);
|
|
126
|
+
const pids = [];
|
|
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;
|
|
145
|
+
}
|
|
146
|
+
async function findPidsByPortWindows(port, protocol) {
|
|
147
|
+
const queries = [];
|
|
148
|
+
if (protocol !== 'udp') {
|
|
149
|
+
queries.push(`Get-NetTCPConnection -LocalPort ${port}`);
|
|
150
|
+
}
|
|
151
|
+
if (protocol !== 'tcp') {
|
|
152
|
+
queries.push(`Get-NetUDPEndpoint -LocalPort ${port}`);
|
|
153
|
+
}
|
|
154
|
+
const query = queries
|
|
155
|
+
.map((cmd) => `${cmd} -ErrorAction SilentlyContinue`)
|
|
156
|
+
.join('; ');
|
|
157
|
+
const { stdout } = await spawnAsync('powershell.exe', [
|
|
158
|
+
'-NoProfile',
|
|
159
|
+
'-NonInteractive',
|
|
160
|
+
'-Command',
|
|
161
|
+
`@(${query}) | Select-Object -ExpandProperty OwningProcess | ` +
|
|
162
|
+
'ConvertTo-Json -Compress',
|
|
163
|
+
]);
|
|
164
|
+
if (stdout.trim() === '') {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
const parsed = JSON.parse(stdout);
|
|
168
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
169
|
+
}
|
|
170
|
+
async function killTreeWindows(pid) {
|
|
171
|
+
// taskkill on windows exits 128 when its not found.
|
|
172
|
+
// unix/macos exit with 0 when the process is not found.
|
|
173
|
+
await spawnAsync('taskkill', ['/pid', String(pid), '/T', '/F'], [0, 128]);
|
|
174
|
+
}
|
|
175
|
+
async function killTreeUnix(pid, signal) {
|
|
176
|
+
const processTree = await getProcessTree(pid);
|
|
177
|
+
killAll(processTree, signal);
|
|
178
|
+
}
|
|
179
|
+
export async function killTree(pid, signal) {
|
|
180
|
+
if (platform === 'win32') {
|
|
181
|
+
return killTreeWindows(pid);
|
|
182
|
+
}
|
|
183
|
+
return killTreeUnix(pid, signal);
|
|
184
|
+
}
|
|
185
|
+
export async function listProcesses() {
|
|
186
|
+
if (platform === 'win32') {
|
|
187
|
+
return listProcessesWindows();
|
|
188
|
+
}
|
|
189
|
+
return listProcessesUnix();
|
|
190
|
+
}
|
|
191
|
+
export async function getProcessInfo(pid) {
|
|
192
|
+
const processes = await listProcesses();
|
|
193
|
+
return processes.find((proc) => proc.pid === pid);
|
|
194
|
+
}
|
|
195
|
+
export async function findProcessesByName(name, options) {
|
|
196
|
+
const processes = await listProcesses();
|
|
197
|
+
if (options?.loose) {
|
|
198
|
+
const needle = name.toLowerCase();
|
|
199
|
+
return processes.filter((proc) => proc.name.toLowerCase().includes(needle));
|
|
200
|
+
}
|
|
201
|
+
return processes.filter((proc) => proc.name === name);
|
|
202
|
+
}
|
|
203
|
+
export async function findProcessesByPort(port, options) {
|
|
204
|
+
let pids;
|
|
205
|
+
if (platform === 'win32') {
|
|
206
|
+
pids = await findPidsByPortWindows(port, options?.protocol);
|
|
207
|
+
}
|
|
208
|
+
else if (platform === 'linux') {
|
|
209
|
+
pids = await findPidsByPortLinux(port, options?.protocol);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
pids = await findPidsByPortDarwin(port, options?.protocol);
|
|
213
|
+
}
|
|
214
|
+
const matched = new Set(pids);
|
|
215
|
+
if (matched.size === 0) {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
const processes = await listProcesses();
|
|
219
|
+
return processes.filter((proc) => matched.has(proc.pid));
|
|
220
|
+
}
|
|
221
|
+
export async function getProcessTree(pid) {
|
|
222
|
+
const processes = await listProcesses();
|
|
223
|
+
return buildProcessTree(pid, processes.map((proc) => [proc.pid, proc.ppid]));
|
|
224
|
+
}
|
|
225
|
+
export function isRunning(pid) {
|
|
226
|
+
if (pid <= 0) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
process.kill(pid, 0);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
return err.code === 'EPERM';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export async function getDescendants(pid) {
|
|
238
|
+
const tree = await getProcessTree(pid);
|
|
239
|
+
return Array.from(tree.keys()).filter((current) => current !== pid);
|
|
240
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tinyps",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A library of utilities for dealing with system processes.",
|
|
5
|
+
"main": "./lib/main.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./lib/main.d.ts",
|
|
10
|
+
"default": "./lib/main.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"lib",
|
|
15
|
+
"!lib/**/*.test.js",
|
|
16
|
+
"!lib/**/*.test.d.ts"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build:clean": "node -e \"require('fs').rmSync('./lib', { recursive: true, force: true })\"",
|
|
20
|
+
"build": "npm run build:clean && tsgo",
|
|
21
|
+
"lint:js": "oxlint src",
|
|
22
|
+
"lint:format": "oxfmt --check src",
|
|
23
|
+
"lint": "npm run lint:js && npm run lint:format",
|
|
24
|
+
"format": "oxfmt --write src",
|
|
25
|
+
"test": "vitest run"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"proc",
|
|
29
|
+
"process",
|
|
30
|
+
"tree-kill",
|
|
31
|
+
"ps-tree",
|
|
32
|
+
"tiny",
|
|
33
|
+
"kill",
|
|
34
|
+
"tree"
|
|
35
|
+
],
|
|
36
|
+
"author": "James Garbutt (https://github.com/43081j)",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/tinylibs/tinyps#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/tinylibs/tinyps/issues"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/tinylibs/tinyps.git"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^26.5.1",
|
|
51
|
+
"@typescript/native-preview": "^7.0.0-dev.20260509.2",
|
|
52
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
53
|
+
"oxfmt": "^0.67.0",
|
|
54
|
+
"oxlint": "^1.63.0",
|
|
55
|
+
"vitest": "^5.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|