
























@@ -31,6 +31,7 @@ const DEFAULT_MAX_COMMAND_RSS_MIB = 8192;
3131const DEFAULT_OUTPUT_CAPTURE_CHARS = 1024 * 1024;
3232const GATEWAY_TEARDOWN_GRACE_MS = 10000;
3333const GATEWAY_TEARDOWN_KILL_GRACE_MS = 2000;
34+const COMMAND_PROCESS_TREE_EXIT_POLL_MS = 50;
3435const LOG_SCAN_CHUNK_BYTES = 64 * 1024;
3536const LOG_SCAN_MAX_LINE_CHARS = 16 * 1024;
3637const LOG_TAIL_BYTES = 256 * 1024;
@@ -380,49 +381,94 @@ export function runCommand(command, args, options = {}) {
380381});
381382child.on("close", (status, signal) => {
382383clearTimeout(timer);
383-clearTimeout(forceKillTimer);
384-void stopResourceSampling().then((resourceSampleFailure) => {
385-if (!timedOut && status === 0) {
386-if (resourceSampleFailure) {
387-reject(resourceSampleFailure);
384+const finish = () => {
385+clearTimeout(forceKillTimer);
386+void stopResourceSampling().then((resourceSampleFailure) => {
387+if (!timedOut && status === 0) {
388+if (resourceSampleFailure) {
389+reject(resourceSampleFailure);
390+return;
391+}
392+resolve({
393+stdout: stdout.text,
394+stderr: stderr.text,
395+stdoutTruncatedChars: stdout.truncatedChars,
396+stderrTruncatedChars: stderr.truncatedChars,
397+});
388398return;
389399}
390-resolve({
391-stdout: stdout.text,
392-stderr: stderr.text,
393-stdoutTruncatedChars: stdout.truncatedChars,
394-stderrTruncatedChars: stderr.truncatedChars,
395-});
396-return;
397-}
398-const detail = [
399-formatCapturedOutput("stdout", stdout),
400-formatCapturedOutput("stderr", stderr),
401-]
402-.filter(Boolean)
403-.join("\n")
404-.trim();
405-const failure = timedOut
406- ? `timed out after ${timeoutMs}ms`
407- : `failed with ${signal || status}`;
408-reject(
409-Object.assign(
410-new Error(
411-`${command} ${args.join(" ")} ${failure}${detail ? `\n${tailText(detail)}` : ""}`,
400+const detail = [
401+formatCapturedOutput("stdout", stdout),
402+formatCapturedOutput("stderr", stderr),
403+]
404+.filter(Boolean)
405+.join("\n")
406+.trim();
407+const failure = timedOut
408+ ? `timed out after ${timeoutMs}ms`
409+ : `failed with ${signal || status}`;
410+reject(
411+Object.assign(
412+new Error(
413+`${command} ${args.join(" ")} ${failure}${detail ? `\n${tailText(detail)}` : ""}`,
414+),
415+{
416+ signal,
417+ status,
418+stderr: stderr.text,
419+stdout: stdout.text,
420+},
412421),
413-{
414- signal,
415- status,
416-stderr: stderr.text,
417-stdout: stdout.text,
418-},
419-),
420-);
421-});
422+);
423+});
424+};
425+426+if (timedOut) {
427+void finishTimedOutCommandProcessTree(child, timeoutKillGraceMs).then(finish, finish);
428+return;
429+}
430+431+finish();
422432});
423433});
424434}
425435436+async function finishTimedOutCommandProcessTree(child, timeoutKillGraceMs) {
437+if (!commandProcessTreeIsAlive(child)) {
438+return;
439+}
440+signalProcessGroup(child, "SIGKILL");
441+await waitForCommandProcessTreeExit(child, timeoutKillGraceMs);
442+}
443+444+async function waitForCommandProcessTreeExit(child, timeoutMs) {
445+const deadlineAt = Date.now() + timeoutMs;
446+while (Date.now() < deadlineAt) {
447+if (!commandProcessTreeIsAlive(child)) {
448+return true;
449+}
450+await new Promise((resolvePoll) => {
451+setTimeout(resolvePoll, COMMAND_PROCESS_TREE_EXIT_POLL_MS);
452+});
453+}
454+return !commandProcessTreeIsAlive(child);
455+}
456+457+function commandProcessTreeIsAlive(child) {
458+if (process.platform === "win32" || typeof child.pid !== "number") {
459+return !hasChildExited(child);
460+}
461+try {
462+process.kill(-child.pid, 0);
463+return true;
464+} catch (error) {
465+if (error?.code === "EPERM") {
466+return true;
467+}
468+return false;
469+}
470+}
471+426472function signalProcessGroup(child, signal) {
427473if (process.platform !== "win32" && typeof child.pid === "number") {
428474try {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。