Repository navigation
Expand file tree
/
Copy pathexit.ts
More file actions
128 lines (116 loc) · 4.31 KB
/
Copy pathexit.ts
File metadata and controls
128 lines (116 loc) · 4.31 KB
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
export const exitCodes = {
success: 0,
testFailure: 1,
invalidArgs: 2,
auth: 3,
network: 4,
config: 5,
timeout: 6,
payment: 7,
notFound: 8,
} as const;
type ExitCode = (typeof exitCodes)[keyof typeof exitCodes];
type ExitProcess = {
readonly stderr: { readonly write: (chunk: string) => unknown };
readonly exit: (code: number) => never;
};
export function exit(
code: ExitCode,
message?: string,
proc: ExitProcess = process,
): never {
if (message !== undefined && message.length > 0) {
proc.stderr.write(`${message}\n`);
}
return proc.exit(code);
}
type ExitingProcess = {
exitCode: number | string | undefined;
readonly exit: (code: number) => never;
};
/**
* How long a process that cannot drain on its own is left to finish. Long
* enough for an outstanding socket teardown or a stdout write to a slow reader
* to land, and only ever waited out by a command that holds the loop open.
*/
const backstopMs = 2000;
/**
* Records `code` as the exit status and lets the event loop drain, rather than
* killing the process where the command finished. Tearing down mid-flight I/O
* aborts a Windows build of Node ("Assertion failed:
* !(handle->flags & UV_HANDLE_CLOSING), src\\win\\async.c"), which replaces the
* command's own exit code with a crash code — so a caller reads a successful
* command as a failure.
*
* The flow runtime still leaves browser processes and CDP sockets the loop
* never drains, so the backstop forces the exit those runs need. Its timer is
* unref'd: it fires only while something else holds the loop open, and a
* command that has nothing left in flight exits before it ever comes due.
*/
export function exitWhenIdle(
code: number,
proc: ExitingProcess = process,
scheduleBackstop: (fn: () => void) => void = (fn) => {
setTimeout(fn, backstopMs).unref();
},
): void {
proc.exitCode = code;
scheduleBackstop(() => proc.exit(code));
}
type SignalName = "SIGINT" | "SIGTERM";
const signalExitCodes: Record<SignalName, number> = {
SIGINT: 130,
SIGTERM: 143,
};
/**
* How long a signalled process is left to finish. Long enough for the I/O that
* was in flight when the signal landed, short enough to read as immediate.
*/
const signalGraceMs = 150;
/** Schedules the grace period between a signal and the exit it forces. */
export function scheduleSignalGrace(
fn: () => void,
): ReturnType<typeof setTimeout> {
// Holds the event loop open, unlike the backstop in `exitWhenIdle`. The exit
// status is one mutable value: a command that settles inside the grace
// overwrites the signal's code with its own, so only a timer certain to fire
// can exit on the code the signal chose.
return setTimeout(fn, signalGraceMs);
}
/**
* Builds the handler for `signal`, sharing one "already signalled" flag across
* every signal it builds.
*
* The first signal runs `shutdown`, then records the exit status and leaves a
* short moment for work in flight — killing the process here reaches the same
* win32 abort `exitWhenIdle` describes, and loses the 130 or 143 with it. The
* moment is short rather than absent because a signal, unlike a finished
* command, arrives while the event loop is still busy: waiting for it to drain
* would mean the command runs to completion and prints, which is not what an
* interrupt asks for.
*
* A second signal exits at once. That is the deliberate escape hatch for a
* process whose cleanup will not finish, so it keeps its hard exit.
*/
export function createSignalExit(deps: {
shutdown: (reason: string) => Promise<void>;
proc?: ExitingProcess;
scheduleGrace?: (fn: () => void) => void;
}): (signal: SignalName) => () => void {
const proc = deps.proc ?? process;
const scheduleGrace = deps.scheduleGrace ?? scheduleSignalGrace;
let signalled = false;
return (signal) => () => {
const code = signalExitCodes[signal];
if (signalled) proc.exit(code);
signalled = true;
void deps
.shutdown(signal)
// A shutdown that rejects would reach the process as an unhandled
// rejection, and the default handling replaces the signal's code with 1
// — the loss this function exists to prevent. The registry settles its
// own cleanups, so this guards the type rather than a caller we have.
.catch(() => {})
.finally(() => exitWhenIdle(code, proc, scheduleGrace));
};
}