quotacap 0.0.49 → 0.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/spawn.d.ts +6 -0
- package/dist/runtime/spawn.js +160 -33
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/runtime/spawn.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** Fallback bound when a probe passes no timeout. Every production caller
|
|
2
|
+
* passes its own (8–20s); this only binds ad-hoc callers. */
|
|
3
|
+
export declare const DEFAULT_PROBE_TIMEOUT_MS = 30000;
|
|
1
4
|
export declare function installAdapterSignal(id: string, signal: AbortSignal): void;
|
|
2
5
|
export declare function clearAdapterSignal(id: string): void;
|
|
3
6
|
export declare function adapterSignal(id: string): AbortSignal | undefined;
|
|
@@ -5,6 +8,9 @@ export declare function registerTrackedChild(kill: (signal?: NodeJS.Signals) =>
|
|
|
5
8
|
export declare function liveChildCount(): number;
|
|
6
9
|
export declare function killAll(signal?: NodeJS.Signals, escalateMs?: number): void;
|
|
7
10
|
export interface TrackedExecOptions {
|
|
11
|
+
/** Bound in ms; defaults to DEFAULT_PROBE_TIMEOUT_MS. On expiry the whole
|
|
12
|
+
* process group gets SIGTERM (SIGKILL escalation after 1s) and the promise
|
|
13
|
+
* rejects promptly without waiting for the child to exit. */
|
|
8
14
|
timeout?: number;
|
|
9
15
|
maxBuffer?: number;
|
|
10
16
|
cwd?: string;
|
package/dist/runtime/spawn.js
CHANGED
|
@@ -2,8 +2,40 @@
|
|
|
2
2
|
// Adapter.poll() signatures stay unchanged: the registry installs one
|
|
3
3
|
// AbortController per adapter id before invoking poll(); tracked exec/PTY
|
|
4
4
|
// wrappers read the controller for their adapter id.
|
|
5
|
-
|
|
5
|
+
//
|
|
6
|
+
// Every probe runs in its own process group (detached) with stdin closed,
|
|
7
|
+
// a bounded timeout, and SIGTERM → SIGKILL escalation delivered to the
|
|
8
|
+
// whole group — so a hung CLI and the grandchildren it booted (MCP servers)
|
|
9
|
+
// can never outlive the probe. execFile cannot do this: it forwards neither
|
|
10
|
+
// `detached` nor `stdio`, its timeout sends one SIGTERM with no escalation,
|
|
11
|
+
// and its callback never fires while a SIGTERM-ignoring child lives, so the
|
|
12
|
+
// probe promise pends forever. Hence the spawn-based implementation below.
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
6
14
|
import { diagnosticError } from "../diagnostics/failure.js";
|
|
15
|
+
/** Fallback bound when a probe passes no timeout. Every production caller
|
|
16
|
+
* passes its own (8–20s); this only binds ad-hoc callers. */
|
|
17
|
+
export const DEFAULT_PROBE_TIMEOUT_MS = 30000;
|
|
18
|
+
/** SIGTERM → SIGKILL escalation delay for a probe that ignores SIGTERM. */
|
|
19
|
+
const PROBE_ESCALATE_MS = 1000;
|
|
20
|
+
/** execFile parity: default cap on captured stdout/stderr each. */
|
|
21
|
+
const DEFAULT_MAX_BUFFER = 1024 * 1024;
|
|
22
|
+
/** Kill the probe's whole process group (it leads its group via detached).
|
|
23
|
+
* Falls back to a direct kill where group kill is unsupported (Windows) or
|
|
24
|
+
* the group is already gone. Never throws. */
|
|
25
|
+
function killProbeGroup(child, signal) {
|
|
26
|
+
const pid = child.pid;
|
|
27
|
+
if (pid !== undefined) {
|
|
28
|
+
try {
|
|
29
|
+
process.kill(-pid, signal);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
catch { }
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
child.kill(signal);
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
}
|
|
7
39
|
const installed = new Map();
|
|
8
40
|
export function installAdapterSignal(id, signal) {
|
|
9
41
|
installed.set(id, signal);
|
|
@@ -61,47 +93,142 @@ export function trackedExecFile(adapterId, file, args, opts) {
|
|
|
61
93
|
if (signal?.aborted)
|
|
62
94
|
return Promise.reject(diagnosticError(abortError(adapterId), { source: "exec", checkpoint: "abort" }));
|
|
63
95
|
return new Promise((resolve, reject) => {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
96
|
+
const timeoutMs = opts?.timeout ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
97
|
+
const maxBuffer = opts?.maxBuffer ?? DEFAULT_MAX_BUFFER;
|
|
98
|
+
let settled = false;
|
|
99
|
+
let stdout = "";
|
|
100
|
+
let stderr = "";
|
|
101
|
+
let stdoutLen = 0;
|
|
102
|
+
let stderrLen = 0;
|
|
103
|
+
const child = spawn(file, args ?? [], {
|
|
104
|
+
cwd: opts?.cwd,
|
|
105
|
+
env: opts?.env,
|
|
106
|
+
detached: true,
|
|
107
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
108
|
+
windowsHide: true,
|
|
109
|
+
});
|
|
110
|
+
const cleanup = () => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
// The escalation timer is NOT cleared here: on the timeout path the
|
|
113
|
+
// promise settles promptly while the child may still be alive, and
|
|
114
|
+
// the SIGKILL must still land. It is cleared in close (child dead).
|
|
67
115
|
if (signal)
|
|
68
116
|
signal.removeEventListener("abort", onAbort);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
117
|
+
};
|
|
118
|
+
const settleResolve = (v) => {
|
|
119
|
+
if (settled)
|
|
120
|
+
return;
|
|
121
|
+
settled = true;
|
|
122
|
+
cleanup();
|
|
123
|
+
resolve(v);
|
|
124
|
+
};
|
|
125
|
+
const settleReject = (e) => {
|
|
126
|
+
if (settled)
|
|
127
|
+
return;
|
|
128
|
+
settled = true;
|
|
129
|
+
cleanup();
|
|
130
|
+
reject(e);
|
|
131
|
+
};
|
|
83
132
|
const unregister = registerTrackedChild((sig) => {
|
|
84
|
-
|
|
85
|
-
child.kill(sig ?? "SIGTERM");
|
|
86
|
-
}
|
|
87
|
-
catch { }
|
|
133
|
+
killProbeGroup(child, sig ?? "SIGTERM");
|
|
88
134
|
}, adapterId);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const t = setTimeout(() => {
|
|
135
|
+
let escalateTimer;
|
|
136
|
+
const armEscalation = () => {
|
|
137
|
+
if (escalateTimer !== undefined)
|
|
138
|
+
return;
|
|
139
|
+
escalateTimer = setTimeout(() => {
|
|
95
140
|
try {
|
|
96
|
-
if (child.exitCode === null)
|
|
97
|
-
child
|
|
141
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
142
|
+
killProbeGroup(child, "SIGKILL");
|
|
143
|
+
}
|
|
98
144
|
}
|
|
99
145
|
catch { }
|
|
100
|
-
},
|
|
101
|
-
if (typeof
|
|
102
|
-
|
|
146
|
+
}, PROBE_ESCALATE_MS);
|
|
147
|
+
if (typeof escalateTimer.unref === "function")
|
|
148
|
+
escalateTimer.unref();
|
|
149
|
+
};
|
|
150
|
+
const timer = setTimeout(() => {
|
|
151
|
+
killProbeGroup(child, "SIGTERM");
|
|
152
|
+
armEscalation();
|
|
153
|
+
// Reject promptly: unlike execFile, never wait for a SIGTERM-ignoring
|
|
154
|
+
// child to exit before settling. The tracked entry stays until the
|
|
155
|
+
// child actually dies, so killAll still reaches stragglers.
|
|
156
|
+
settleReject(diagnosticError(new Error(`${adapterId}: probe timed out after ${timeoutMs}ms`), {
|
|
157
|
+
source: "exec",
|
|
158
|
+
}));
|
|
159
|
+
}, timeoutMs);
|
|
160
|
+
const onAbort = () => {
|
|
161
|
+
if (settled)
|
|
162
|
+
return;
|
|
163
|
+
killProbeGroup(child, "SIGTERM");
|
|
164
|
+
armEscalation();
|
|
165
|
+
// Settles in the close handler with the abort checkpoint.
|
|
103
166
|
};
|
|
104
167
|
if (signal)
|
|
105
168
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
169
|
+
const onOverflow = (kind) => {
|
|
170
|
+
killProbeGroup(child, "SIGTERM");
|
|
171
|
+
armEscalation();
|
|
172
|
+
settleReject(diagnosticError(new Error(`${kind} maxBuffer length exceeded`), {
|
|
173
|
+
source: "exec",
|
|
174
|
+
stdout,
|
|
175
|
+
stderr,
|
|
176
|
+
}));
|
|
177
|
+
};
|
|
178
|
+
child.stdout?.on("data", (d) => {
|
|
179
|
+
if (settled)
|
|
180
|
+
return;
|
|
181
|
+
stdoutLen += d.length;
|
|
182
|
+
if (stdoutLen > maxBuffer) {
|
|
183
|
+
onOverflow("stdout");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
stdout += d.toString();
|
|
187
|
+
});
|
|
188
|
+
child.stderr?.on("data", (d) => {
|
|
189
|
+
if (settled)
|
|
190
|
+
return;
|
|
191
|
+
stderrLen += d.length;
|
|
192
|
+
if (stderrLen > maxBuffer) {
|
|
193
|
+
onOverflow("stderr");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
stderr += d.toString();
|
|
197
|
+
});
|
|
198
|
+
child.on("error", (err) => {
|
|
199
|
+
// Spawn failure (e.g. ENOENT): no child will ever run. Unregister
|
|
200
|
+
// now (close still fires later; unregister is idempotent) so the
|
|
201
|
+
// entry is gone by the time the rejection is observed.
|
|
202
|
+
unregister();
|
|
203
|
+
settleReject(diagnosticError(err, { source: "exec", stdout, stderr }));
|
|
204
|
+
});
|
|
205
|
+
child.on("close", (code) => {
|
|
206
|
+
unregister();
|
|
207
|
+
clearTimeout(escalateTimer);
|
|
208
|
+
if (settled)
|
|
209
|
+
return;
|
|
210
|
+
if (signal?.aborted) {
|
|
211
|
+
settleReject(diagnosticError(abortError(adapterId), {
|
|
212
|
+
source: "exec",
|
|
213
|
+
checkpoint: "abort",
|
|
214
|
+
stdout,
|
|
215
|
+
stderr,
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
else if (code === 0) {
|
|
219
|
+
settleResolve({ stdout, stderr });
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
const err = new Error(`Command failed: ${file}${code === null ? "" : ` (code ${code})`}`);
|
|
223
|
+
if (typeof code === "number")
|
|
224
|
+
err.code = code;
|
|
225
|
+
settleReject(diagnosticError(err, {
|
|
226
|
+
source: "exec",
|
|
227
|
+
exitCode: code ?? undefined,
|
|
228
|
+
stderr,
|
|
229
|
+
stdout,
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
});
|
|
106
233
|
});
|
|
107
234
|
}
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// generated by scripts/build-embed.mjs — do not edit
|
|
2
|
-
export const VERSION = "0.0.
|
|
2
|
+
export const VERSION = "0.0.50";
|