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 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.toString();
12
+ stdout += data;
11
13
  });
12
14
  child.stderr.on('data', (data) => {
13
- stderr += data.toString();
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
- for (const pid of processTree.keys()) {
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
- throw err;
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 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
- });
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
- 'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress',
91
+ '[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); ' +
92
+ 'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress',
95
93
  ]);
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
- }));
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 Array.from(stdout.matchAll(socketOwnerPattern), (match) => Number(match[1]));
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
- 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;
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
- `@(${query}) | Select-Object -ExpandProperty OwningProcess | ` +
134
+ '[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); ' +
135
+ `@(${query}) | Select-Object -ExpandProperty OwningProcess | ` +
162
136
  'ConvertTo-Json -Compress',
163
137
  ]);
164
- if (stdout.trim() === '') {
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tinyps",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
4
  "description": "A library of utilities for dealing with system processes.",
5
5
  "main": "./lib/main.js",
6
6
  "type": "module",