Skip to content

Commit 0d065cf

Browse files
committed
Extract process functions from general utils
1 parent 100c665 commit 0d065cf

File tree

5 files changed

+200
-195
lines changed

5 files changed

+200
-195
lines changed

src/interceptors/chromium-based-interceptors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
Browser,
1111
LaunchOptions
1212
} from '../browsers';
13-
import { delay, readFile, deleteFolder, listRunningProcesses, windowsClose, waitForExit } from '../util';
13+
import { delay, readFile, deleteFolder } from '../util';
14+
import { listRunningProcesses, windowsClose, waitForExit } from '../process-management';
1415
import { HideWarningServer } from '../hide-warning-server';
1516
import { Interceptor } from '.';
1617
import { reportError } from '../error-tracking';

src/interceptors/fresh-firefox.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { HtkConfig } from '../config';
77
import { reportError } from '../error-tracking';
88

99
import { getAvailableBrowsers, launchBrowser, BrowserInstance } from '../browsers';
10-
import { delay, windowsKill, readFile, canAccess, deleteFolder, spawnToResult } from '../util';
10+
import { delay, readFile, canAccess, deleteFolder } from '../util';
11+
import { windowsKill, spawnToResult } from '../process-management';
1112
import { MessageServer } from '../message-server';
1213
import { CertCheckServer } from '../cert-check-server';
1314
import { Interceptor } from '.';

src/interceptors/terminal/fresh-terminal-interceptor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { findExecutableById } from '@httptoolkit/osx-find-executable';
77
import { Interceptor } from '..';
88
import { HtkConfig } from '../../config';
99
import { reportError, addBreadcrumb } from '../../error-tracking';
10-
import { spawnToResult, canAccess, commandExists } from '../../util';
10+
import { canAccess, commandExists } from '../../util';
11+
import { spawnToResult } from '../../process-management';
1112

1213
import { getTerminalEnvVars } from './terminal-env-overrides';
1314
import { editShellStartupScripts, resetShellStartupScripts } from './terminal-scripts';

src/process-management.ts

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import * as _ from 'lodash';
2+
import * as path from 'path';
3+
import { spawn } from 'child_process';
4+
5+
import { delay } from './util';
6+
7+
// Spawn a command, and resolve with all output as strings when it terminates
8+
export function spawnToResult(command: string, args: string[] = [], options = {}, inheritOutput = false): Promise<{
9+
exitCode?: number,
10+
stdout: string,
11+
stderr: string
12+
}> {
13+
return new Promise((resolve, reject) => {
14+
const childProc = spawn(command, args, Object.assign({ stdio: 'pipe' } as const, options));
15+
const { stdout, stderr } = childProc;
16+
17+
const stdoutData: Buffer[] = [];
18+
stdout.on('data', (d) => stdoutData.push(d));
19+
const stderrData: Buffer[] = [];
20+
stderr.on('data', (d) => stderrData.push(d));
21+
22+
if (inheritOutput) {
23+
stdout.pipe(process.stdout);
24+
stderr.pipe(process.stderr);
25+
}
26+
27+
childProc.once('error', reject);
28+
childProc.once('close', (code?: number) => {
29+
// Note that we do _not_ check the error code, we just return it
30+
resolve({
31+
exitCode: code,
32+
stdout: Buffer.concat(stdoutData).toString(),
33+
stderr: Buffer.concat(stderrData).toString()
34+
});
35+
});
36+
});
37+
};
38+
39+
type Proc = {
40+
pid: number,
41+
command: string,
42+
bin: string | undefined,
43+
args: string | undefined
44+
};
45+
46+
const getOutputLines = (stdout: string) =>
47+
stdout
48+
.split('\n')
49+
.map(line => line.trim())
50+
.filter(line => !!line);
51+
52+
/**
53+
* Attempts to get a list of pid + command + binary + args for every process running
54+
* on the machine owned by the current user (not *all* processes!).
55+
*
56+
* This is best efforts, due to the lack of guarantees on 'ps'. Notably args may be
57+
* undefined, if we're unable to work out which part of the command is the command
58+
* and which is args.
59+
*/
60+
export async function listRunningProcesses(): Promise<Array<Proc>> {
61+
if (process.platform !== 'win32') {
62+
const [psCommResult, psFullResult] = await Promise.all([
63+
spawnToResult('ps', ['xo', 'pid=,comm=']), // Prints pid + bin only
64+
spawnToResult('ps', ['xo', 'pid=,command=']), // Prints pid + command + args
65+
]);
66+
67+
if (psCommResult.exitCode !== 0 || psFullResult.exitCode !== 0) {
68+
throw new Error(`Could not list running processes, ps exited with code ${
69+
psCommResult.exitCode || psFullResult.exitCode
70+
}`);
71+
}
72+
73+
const processes = getOutputLines(psCommResult.stdout).map(line => {
74+
const firstSpaceIndex = line.indexOf(' ');
75+
if (firstSpaceIndex === -1) {
76+
throw new Error('No space in PS output');
77+
}
78+
79+
const pid = parseInt(line.substring(0, firstSpaceIndex), 10);
80+
const command = line.substring(firstSpaceIndex + 1);
81+
82+
return { pid, command } as Proc;
83+
});
84+
85+
const processesByPid = _.keyBy(processes, p => p.pid);
86+
87+
getOutputLines(psFullResult.stdout).forEach(line => {
88+
const firstSpaceIndex = line.indexOf(' ');
89+
if (firstSpaceIndex === -1) throw new Error('No space in PS output');
90+
91+
const pid = parseInt(line.substring(0, firstSpaceIndex), 10);
92+
const binAndArgs = line.substring(firstSpaceIndex + 1);
93+
94+
const proc = processesByPid[pid];
95+
if (!proc) return;
96+
97+
if (proc.command.includes(path.sep)) {
98+
// Proc.command is a fully qualified path (as on Mac)
99+
if (binAndArgs.startsWith(proc.command)) {
100+
proc.bin = proc.command;
101+
proc.args = binAndArgs.substring(proc.bin.length + 1);
102+
}
103+
} else {
104+
// Proc.command is a plain binary name (as on Linux)
105+
const commandMatch = binAndArgs.match(
106+
// Best guess: first instance of the command name followed by a space
107+
new RegExp(_.escapeRegExp(proc.command) + '( |$)')
108+
);
109+
110+
if (!commandMatch) {
111+
// We can't work out which bit is the command, don't set args, treat
112+
// the whole command line as the command and give up
113+
proc.command = binAndArgs;
114+
return;
115+
}
116+
117+
const commandIndex = commandMatch.index!;
118+
proc.bin = binAndArgs.substring(0, commandIndex + proc.command.length);
119+
proc.args = binAndArgs.substring(proc.bin.length + 1);
120+
}
121+
});
122+
123+
return processes;
124+
} else {
125+
const wmicOutput = await spawnToResult('wmic', [
126+
'Process', 'Get', 'processid,commandline'
127+
]);
128+
129+
if (wmicOutput.exitCode !== 0) {
130+
throw new Error(`WMIC exited with unexpected error code ${wmicOutput.exitCode}`);
131+
}
132+
133+
return getOutputLines(wmicOutput.stdout)
134+
.slice(1) // Skip the header line
135+
.filter((line) => line.includes(' ')) // Skip lines where the command line isn't available (just pids)
136+
.map((line) => {
137+
const pidIndex = line.lastIndexOf(' ') + 1;
138+
const pid = parseInt(line.substring(pidIndex), 10);
139+
140+
const command = line.substring(0, pidIndex).trim();
141+
const bin = command[0] === '"'
142+
? command.substring(1, command.substring(1).indexOf('"') + 1)
143+
: command.substring(0, command.indexOf(' '));
144+
const args = command[0] === '"'
145+
? command.substring(bin.length + 3)
146+
: command.substring(bin.length + 1);
147+
148+
return {
149+
pid,
150+
command,
151+
bin,
152+
args
153+
};
154+
});
155+
}
156+
}
157+
158+
export async function waitForExit(pid: number, timeout: number = 5000): Promise<void> {
159+
const startTime = Date.now();
160+
161+
while (true) {
162+
try {
163+
process.kill(pid, 0) as void | boolean;
164+
165+
// Didn't throw. If we haven't timed out, check again after 250ms:
166+
if (Date.now() - startTime > timeout) {
167+
throw new Error("Process did not exit before timeout");
168+
}
169+
await delay(250);
170+
} catch (e) {
171+
if ((e as Error & { code?: string }).code === 'ESRCH') {
172+
return; // Process doesn't exist! We're done.
173+
}
174+
else throw e;
175+
}
176+
}
177+
}
178+
179+
// Cleanly close (simulate closing the main window) on a specific windows process
180+
export async function windowsClose(pid: number) {
181+
await spawnToResult('taskkill', [
182+
'/pid', pid.toString(),
183+
]);
184+
}
185+
186+
// Harshly kill a windows process by some WMIC matching string e.g.
187+
// "processId=..." or "CommandLine Like '%...%'"
188+
export async function windowsKill(processMatcher: string) {
189+
await spawnToResult('wmic', [
190+
'Path', 'win32_process',
191+
'Where', processMatcher,
192+
'Call', 'Terminate'
193+
], { }, true);
194+
}

0 commit comments

Comments
 (0)