File size: 2,213 Bytes
3144483
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
 * Child-process output cleanup for commands whose descendants inherit pipes.
 */
import type { ChildProcess } from "node:child_process";

const EXIT_STDIO_GRACE_MS = 100;
const EXIT_STDIO_MAX_DRAIN_MS = 1_000;

/**
 * Execa waits for stdout/stderr after the direct child exits. Bound that wait
 * when detached descendants keep inherited pipes open, while still draining
 * short output tails. The returned cleanup must run after awaiting the child.
 */
export function releaseChildProcessOutputAfterExit(child: ChildProcess): () => void {
  let idleTimer: NodeJS.Timeout | undefined;
  let releaseTimer: NodeJS.Timeout | undefined;
  let deadlineTimer: NodeJS.Timeout | undefined;

  const cleanup = () => {
    clearTimeout(idleTimer);
    clearTimeout(releaseTimer);
    clearTimeout(deadlineTimer);
    child.removeListener("exit", onExit);
    child.stdout?.removeListener("data", onData);
    child.stderr?.removeListener("data", onData);
  };
  const release = () => {
    cleanup();
    child.stdout?.destroy();
    child.stderr?.destroy();
  };
  const scheduleRelease = () => {
    // Either timer may run before already-buffered pipe data on a loaded loop.
    // Defer to the next timers phase so both Node and Bun poll the pipes first.
    releaseTimer ??= setTimeout(release, 0);
    releaseTimer.unref();
  };
  const armIdleTimer = () => {
    clearTimeout(idleTimer);
    clearTimeout(releaseTimer);
    releaseTimer = undefined;
    idleTimer = setTimeout(scheduleRelease, EXIT_STDIO_GRACE_MS);
    idleTimer.unref();
  };
  const onData = () => {
    if (deadlineTimer) {
      armIdleTimer();
    }
  };
  const onExit = () => {
    deadlineTimer = setTimeout(() => {
      // Post-deadline data must not cancel release and extend the hard bound.
      deadlineTimer = undefined;
      scheduleRelease();
    }, EXIT_STDIO_MAX_DRAIN_MS);
    deadlineTimer.unref();
    armIdleTimer();
  };

  child.stdout?.on("data", onData);
  child.stderr?.on("data", onData);
  // A command deadline can transfer output here after the root has already exited.
  if (child.exitCode != null || child.signalCode != null) {
    onExit();
  } else {
    child.once("exit", onExit);
  }
  return cleanup;
}