Skip to content

Commit 3129595

Browse files
authored
Harden against process lifetime issues (microsoft#75)
* Use rather than events * Move kill-children-of into node for greater control * Handle communication errors in the server harness
1 parent bb3b107 commit 3129595

File tree

6 files changed

+83
-104
lines changed

6 files changed

+83
-104
lines changed

package-lock.json

Lines changed: 16 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
"dependencies": {
1414
"@octokit/rest": "^16.43.2",
1515
"@typescript/github-link": "^0.2.1",
16-
"@typescript/server-harness": "^0.3.0",
17-
"@typescript/server-replay": "^0.2.6",
16+
"@typescript/server-harness": "^0.3.3",
17+
"@typescript/server-replay": "^0.2.7",
1818
"glob": "^7.2.3",
1919
"json5": "^2.2.1",
2020
"random-seed": "^0.3.0",

scripts/kill-children-of

Lines changed: 0 additions & 66 deletions
This file was deleted.

src/utils/execUtils.ts

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function spawnWithTimeoutAsync(cwd: string, command: string, args: readon
4848
let stdout = "";
4949
let stderr = "";
5050

51-
childProcess.on("close", (code, signal) => {
51+
childProcess.once("close", (code, signal) => {
5252
if (!timedOut) {
5353
clearTimeout(timeout);
5454
resolve({ stdout, stderr, code, signal });
@@ -65,10 +65,59 @@ export function spawnWithTimeoutAsync(cwd: string, command: string, args: readon
6565

6666
const timeout = setTimeout(async () => {
6767
timedOut = true;
68-
// Note that killing childProcess resets the PPID of each of its children to 1, so this has to happen first
69-
await execAsync(path.join(__dirname, "..", "..", "scripts"), `./kill-children-of ${childProcess.pid}`);
70-
childProcess.kill("SIGKILL"); // This may fail if the process exited when its children were killed
68+
await killTree(childProcess);
7169
resolve(undefined);
7270
}, timeoutMs | 0); // Truncate to int
7371
});
7472
}
73+
74+
function killTree(childProcess: cp.ChildProcessWithoutNullStreams): Promise<void> {
75+
return new Promise<void>((resolve, reject) => {
76+
// Ideally, we would wait for all of the processes to close, but we only get events for
77+
// this one, so we'll kill it last and hope for the best.
78+
childProcess.once("close", () => {
79+
resolve();
80+
});
81+
82+
cp.exec("ps -e -o pid,ppid --no-headers", (err, stdout) => {
83+
if (err) {
84+
reject (err);
85+
return;
86+
}
87+
88+
const childMap: Record<number, number[]> = {};
89+
const pidList = stdout.trim().split(/\s+/);
90+
for (let i = 0; i + 1 < pidList.length; i += 2) {
91+
const childPid = +pidList[i];
92+
const parentPid = +pidList[i + 1];
93+
childMap[parentPid] ||= [];
94+
childMap[parentPid].push(childPid);
95+
}
96+
97+
if (!childMap[childProcess.pid]) {
98+
// Descendent processes may still be alive, but we have no way to identify them
99+
resolve();
100+
return;
101+
}
102+
103+
const strictDescendentPids: number[] = [];
104+
const stack: number[] = [ childProcess.pid ];
105+
while (stack.length) {
106+
const pid = stack.pop()!;
107+
if (pid !== childProcess.pid) {
108+
strictDescendentPids.push(pid);
109+
}
110+
const children = childMap[pid];
111+
if (children) {
112+
stack.push(...children);
113+
}
114+
}
115+
116+
console.log(`Killing process ${childProcess.pid} and its descendents: ${strictDescendentPids.join(", ")}`);
117+
118+
strictDescendentPids.forEach(pid => process.kill(pid, "SIGKILL"));
119+
childProcess.kill("SIGKILL");
120+
// Resolve when we detect that childProcess has closed (above)
121+
});
122+
});
123+
}

src/utils/exerciseServer.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import path = require("path");
77
import glob = require("glob");
88
import { performance } from "perf_hooks";
99
import randomSeed = require("random-seed");
10-
import { EXIT_BAD_ARGS, EXIT_UNHANDLED_EXCEPTION, EXIT_SERVER_EXIT_FAILED, EXIT_SERVER_CRASH, EXIT_SERVER_ERROR, EXIT_LANGUAGE_SERVICE_DISABLED } from "./exerciseServerConstants";
10+
import { EXIT_BAD_ARGS, EXIT_UNHANDLED_EXCEPTION, EXIT_SERVER_EXIT_FAILED, EXIT_SERVER_CRASH, EXIT_SERVER_ERROR, EXIT_LANGUAGE_SERVICE_DISABLED, EXIT_SERVER_COMMUNICATION_ERROR } from "./exerciseServerConstants";
1111

1212
const testDirPlaceholder = "@PROJECT_ROOT@";
1313

@@ -105,10 +105,17 @@ async function exerciseServerWorker(testDir: string, tsserverPath: string, repla
105105
}
106106
});
107107

108+
server.on("communicationError", async (err: any) => {
109+
console.error(`Error communicating with server:\n${err}`);
110+
exitExpected = true;
111+
await server.kill();
112+
process.exit(EXIT_SERVER_COMMUNICATION_ERROR);
113+
});
114+
108115
let exitExpected = false;
109-
server.on("exit", (code: number | null) => {
116+
server.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
110117
if (!exitExpected) {
111-
console.log(`Server exited prematurely with code ${code ?? "unknown"}`);
118+
console.log(`Server exited prematurely with code ${code ?? "unknown"} and signal ${signal ?? "unknown"}`);
112119
process.exit(EXIT_SERVER_CRASH);
113120
}
114121
});

src/utils/exerciseServerConstants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export const EXIT_UNHANDLED_EXCEPTION = 2;
33
export const EXIT_SERVER_EXIT_FAILED = 3;
44
export const EXIT_SERVER_CRASH = 4; // The server process actually exited
55
export const EXIT_SERVER_ERROR = 5; // The server process sent an error response
6-
export const EXIT_LANGUAGE_SERVICE_DISABLED = 6;
6+
export const EXIT_LANGUAGE_SERVICE_DISABLED = 6;
7+
export const EXIT_SERVER_COMMUNICATION_ERROR = 7;

0 commit comments

Comments
 (0)