hammerkit 1.27.0-next.2 → 1.27.0-next.21
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/build.schema.json +27 -0
- package/dist/cache/backends/registry-cache-backend.js +2 -2
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +1 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/event-cache.js +20 -2
- package/dist/executer/execute-work-task.js +5 -3
- package/dist/executer/execution-steps.js +25 -4
- package/dist/executer/reset-work-tree.js +4 -0
- package/dist/executer/scheduler/enqueue-next.js +7 -0
- package/dist/executer/scheduler/state-key.js +42 -5
- package/dist/harness/connect.js +20 -6
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +63 -114
- package/dist/harness/opencode.js +158 -50
- package/dist/harness/pi.js +29 -0
- package/dist/harness/sandbox-client.js +207 -65
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +27 -16
- package/dist/kubernetes/claim-layout.js +71 -0
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +5 -9
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
- package/dist/kubernetes/ensure-persistent-data.js +30 -20
- package/dist/kubernetes/get-service-ip.js +2 -2
- package/dist/kubernetes/job-secrets.js +33 -19
- package/dist/kubernetes/kept-claims.js +22 -10
- package/dist/kubernetes/network-policy.js +85 -27
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/resources.js +25 -3
- package/dist/kubernetes/restore-kubernetes-data.js +9 -3
- package/dist/kubernetes/sandbox-runner.js +626 -0
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/volumes.js +6 -3
- package/dist/network/egress-proxy.js +45 -6
- package/dist/optimizer/calculate-checksum.js +3 -2
- package/dist/optimizer/check-key.js +94 -0
- package/dist/optimizer/check-keys.js +64 -0
- package/dist/optimizer/get-work-cache-stats.js +17 -3
- package/dist/optimizer/work-task-cache-description.js +51 -1
- package/dist/planner/utils/append-work-dependencies.js +10 -5
- package/dist/planner/utils/append-work-service.js +1 -0
- package/dist/planner/utils/append-work-task.js +15 -2
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/planner/utils/task-arguments.js +3 -0
- package/dist/planner/validate.js +22 -0
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-runtime-docker.js +2 -1
- package/dist/planner/work-runtime-kubernetes.js +97 -48
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +35 -13
- package/dist/remote/annotations.js +13 -1
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +64 -3
- package/dist/remote/remote-commands.js +41 -9
- package/dist/remote/remote-work-commands.js +246 -1
- package/dist/remote/server-client.js +15 -2
- package/dist/remote/token-source.js +64 -26
- package/dist/run-program.js +9 -0
- package/dist/schema/build-file-base-task-schema.js +5 -1
- package/dist/schema/build-file-container-service-schema.js +2 -0
- package/dist/schema/build-file-include-schema.js +10 -4
- package/dist/schema/reference-parser.js +9 -1
- package/dist/schema/schema-parser.js +36 -4
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/server/accounts-api.js +3 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/definitions-api.js +4 -2
- package/dist/server/endpoints.js +612 -0
- package/dist/server/executor.js +56 -5
- package/dist/server/kubernetes-backend.js +303 -64
- package/dist/server/kubernetes-sandbox-storage.js +48 -3
- package/dist/server/oauth.js +12 -1
- package/dist/server/plan.js +126 -62
- package/dist/server/policy.js +32 -2
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-plan.js +50 -19
- package/dist/server/run-request.js +64 -3
- package/dist/server/run-worker.js +146 -10
- package/dist/server/runs.js +347 -27
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes-api.js +10 -4
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +213 -14
- package/dist/server/server.js +268 -15
- package/dist/server/service-runs.js +82 -0
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/tar.js +36 -0
- package/package.json +1 -1
package/dist/server/server.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.startServer = exports.getDiscoveryDocument = exports.SERVER_PROTOCOL = exports.DISCOVERY_PATH = void 0;
|
|
3
|
+
exports.startServer = exports.refuseWake = exports.getDiscoveryDocument = exports.SERVER_PROTOCOL = exports.DISCOVERY_PATH = void 0;
|
|
4
4
|
const http_1 = require("http");
|
|
5
5
|
const https_1 = require("https");
|
|
6
|
+
const tls_1 = require("tls");
|
|
6
7
|
const version_1 = require("../version");
|
|
7
8
|
const server_config_1 = require("./server-config");
|
|
8
9
|
const discovery_1 = require("./discovery");
|
|
9
10
|
const oauth_1 = require("./oauth");
|
|
10
11
|
const policy_1 = require("./policy");
|
|
12
|
+
const repo_1 = require("./repo");
|
|
11
13
|
const run_store_1 = require("./run-store");
|
|
12
14
|
const runs_1 = require("./runs");
|
|
13
15
|
const git_checkout_1 = require("./git-checkout");
|
|
@@ -32,10 +34,14 @@ const sandbox_storage_1 = require("./sandbox-storage");
|
|
|
32
34
|
const kubernetes_sandbox_storage_1 = require("./kubernetes-sandbox-storage");
|
|
33
35
|
const sandbox_docker_1 = require("./sandbox-docker");
|
|
34
36
|
const sandboxes_api_1 = require("./sandboxes-api");
|
|
37
|
+
const endpoints_1 = require("./endpoints");
|
|
38
|
+
const service_runs_1 = require("./service-runs");
|
|
35
39
|
const units_1 = require("../utils/units");
|
|
36
40
|
const run_logs_1 = require("./run-logs");
|
|
37
41
|
const get_cache_directory_1 = require("../optimizer/get-cache-directory");
|
|
38
42
|
const path_1 = require("path");
|
|
43
|
+
const checks_1 = require("./checks");
|
|
44
|
+
const checks_api_1 = require("./checks-api");
|
|
39
45
|
var discovery_2 = require("./discovery");
|
|
40
46
|
Object.defineProperty(exports, "DISCOVERY_PATH", { enumerable: true, get: function () { return discovery_2.DISCOVERY_PATH; } });
|
|
41
47
|
Object.defineProperty(exports, "SERVER_PROTOCOL", { enumerable: true, get: function () { return discovery_2.SERVER_PROTOCOL; } });
|
|
@@ -64,6 +70,8 @@ const HEARTBEAT_MS = 15000;
|
|
|
64
70
|
// the longest `?wait=` of a run: below the 60 seconds after which a proxy in front of the
|
|
65
71
|
// server (nginx by default) closes a connection that says nothing
|
|
66
72
|
const MAX_WAIT_MS = 50000;
|
|
73
|
+
// how often checks older than `runs.checksKeep` are removed
|
|
74
|
+
const CHECK_SWEEP_MS = 6 * 60 * 60 * 1000;
|
|
67
75
|
// Server-sent events: `id` is the sequence number, so a client that lost the
|
|
68
76
|
// connection continues with Last-Event-ID (or ?after=) and misses nothing.
|
|
69
77
|
function streamEvents(registry, config, principal, id, req, res, url) {
|
|
@@ -96,8 +104,8 @@ function streamEvents(registry, config, principal, id, req, res, url) {
|
|
|
96
104
|
watch.stop();
|
|
97
105
|
});
|
|
98
106
|
}
|
|
99
|
-
async function handleRuns(config, registry, definitions, sources, sandboxes, planning, repos, log, principal, req, res, url) {
|
|
100
|
-
const match = /^\/v1\/runs(?:\/([^/]+)(?:\/(cancel|events|logs))?)?$/.exec(url.pathname);
|
|
107
|
+
async function handleRuns(config, registry, definitions, sources, sandboxes, planning, repos, endpointUrl, log, principal, req, res, url) {
|
|
108
|
+
const match = /^\/v1\/runs(?:\/([^/]+)(?:\/(cancel|events|logs|extend))?)?$/.exec(url.pathname);
|
|
101
109
|
if (!match) {
|
|
102
110
|
return false;
|
|
103
111
|
}
|
|
@@ -115,7 +123,7 @@ async function handleRuns(config, registry, definitions, sources, sandboxes, pla
|
|
|
115
123
|
});
|
|
116
124
|
}
|
|
117
125
|
else if (method === 'POST') {
|
|
118
|
-
await submitRun(config, registry, definitions, sources, sandboxes, planning, repos, log, principal, req, res);
|
|
126
|
+
await submitRun(config, registry, definitions, sources, sandboxes, planning, repos, endpointUrl, log, principal, req, res);
|
|
119
127
|
}
|
|
120
128
|
else {
|
|
121
129
|
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET, POST' });
|
|
@@ -134,6 +142,13 @@ async function handleRuns(config, registry, definitions, sources, sandboxes, pla
|
|
|
134
142
|
log(`cancel iss=${principal.iss} sub=${principal.sub} run=${id} found=${Boolean(run)}`);
|
|
135
143
|
run ? (0, http_2.send)(res, 200, toRunResponse(run)) : (0, http_2.send)(res, 404, { error: 'not found' });
|
|
136
144
|
}
|
|
145
|
+
else if (action === 'extend') {
|
|
146
|
+
if (method !== 'POST') {
|
|
147
|
+
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'POST' });
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
await extendRun(config, registry, log, principal, id, req, res);
|
|
151
|
+
}
|
|
137
152
|
else if (method !== 'GET') {
|
|
138
153
|
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
|
|
139
154
|
}
|
|
@@ -181,12 +196,75 @@ function accountsOf(config, principal) {
|
|
|
181
196
|
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
182
197
|
return (account) => (0, policy_1.canUse)(grant, account);
|
|
183
198
|
}
|
|
199
|
+
async function extendRun(config, registry, log, principal, id, req, res) {
|
|
200
|
+
const body = (await (0, http_2.readJson)(req));
|
|
201
|
+
let ttl;
|
|
202
|
+
try {
|
|
203
|
+
if (typeof body !== 'object' || body === null || typeof body.ttl !== 'string') {
|
|
204
|
+
throw new Error();
|
|
205
|
+
}
|
|
206
|
+
ttl = (0, units_1.parseDuration)(body.ttl);
|
|
207
|
+
if (!(ttl > 0)) {
|
|
208
|
+
throw new Error();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
(0, http_2.send)(res, 400, { error: 'ttl: expected a duration like 30m, 8h or 1d' });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
216
|
+
const result = registry.extend(principal, id, ttl, (run) => (0, service_runs_1.mayOpenEndpoints)(grant, run));
|
|
217
|
+
log(`extend iss=${principal.iss} sub=${principal.sub} run=${id} result=${result.status}`);
|
|
218
|
+
if (result.status === 'not-found') {
|
|
219
|
+
(0, http_2.send)(res, 404, { error: 'not found' });
|
|
220
|
+
}
|
|
221
|
+
else if (result.status === 'not-up') {
|
|
222
|
+
(0, http_2.send)(res, 409, { error: `the run is ${result.state}, only a service run that is up can be extended` });
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
(0, http_2.send)(res, 200, toRunResponse(result.run));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Why the owner of a service run that sleeps may no longer run it, with the entitlements of
|
|
229
|
+
// the moment: a wake is started by who opens an endpoint, but runs as the owner, as a new
|
|
230
|
+
// run of theirs would.
|
|
231
|
+
function refuseWake(config, repos, run) {
|
|
232
|
+
if (run.source.type !== 'ref') {
|
|
233
|
+
return 'a run from a sandbox does not wake';
|
|
234
|
+
}
|
|
235
|
+
const grant = (0, policy_1.getGrant)(config.entitlements, {
|
|
236
|
+
iss: run.owner.iss,
|
|
237
|
+
sub: run.owner.sub,
|
|
238
|
+
groups: run.owner.groups ?? [],
|
|
239
|
+
claims: run.owner.claims ?? {},
|
|
240
|
+
});
|
|
241
|
+
const decision = (0, policy_1.decideRun)({
|
|
242
|
+
grant,
|
|
243
|
+
accounts: run.accounts,
|
|
244
|
+
catalog: config.accounts,
|
|
245
|
+
source: { type: 'ref', repo: run.source.repo, ref: run.source.ref },
|
|
246
|
+
// an account it may only request is refused, the wake has nobody to approve it
|
|
247
|
+
approvable: () => true,
|
|
248
|
+
readable: (repo) => repos.readable(repo),
|
|
249
|
+
});
|
|
250
|
+
if (decision.type === 'refuse') {
|
|
251
|
+
return `its owner ${run.owner.sub} may no longer run it: ${decision.error.replace(/^you /, 'they ')}`;
|
|
252
|
+
}
|
|
253
|
+
if (decision.type === 'approval') {
|
|
254
|
+
return `its owner ${run.owner.sub} may only request ${decision.accounts.join(', ')} now, start it again`;
|
|
255
|
+
}
|
|
256
|
+
if (run.up?.endpoints.length && (0, policy_1.endpointLevel)(grant, run.source.repo) !== 'publish') {
|
|
257
|
+
return `its owner ${run.owner.sub} may no longer publish endpoints of ${run.source.repo}`;
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
exports.refuseWake = refuseWake;
|
|
184
262
|
function toRunResponse(run) {
|
|
185
263
|
const { owner, ...visible } = run;
|
|
186
264
|
void owner;
|
|
187
265
|
return { ...visible, results: run.results ?? {} };
|
|
188
266
|
}
|
|
189
|
-
async function submitRun(config, registry, definitions, sources, sandboxes, planning, repos, log, principal, req, res) {
|
|
267
|
+
async function submitRun(config, registry, definitions, sources, sandboxes, planning, repos, endpointUrl, log, principal, req, res) {
|
|
190
268
|
const parsed = run_request_1.runRequestSchema.safeParse(await (0, http_2.readJson)(req));
|
|
191
269
|
if (!parsed.success) {
|
|
192
270
|
const issue = parsed.error.issues[0];
|
|
@@ -218,6 +296,27 @@ async function submitRun(config, registry, definitions, sources, sandboxes, plan
|
|
|
218
296
|
(0, http_2.send)(res, 400, { error: `${version.hash} has no task ${unknown.map(oauth_1.quote).join(', ')}` });
|
|
219
297
|
return;
|
|
220
298
|
}
|
|
299
|
+
const notServices = request.services.filter((service) => !version.services.includes(service));
|
|
300
|
+
if (notServices.length > 0) {
|
|
301
|
+
(0, http_2.send)(res, 400, { error: `${version.hash} has no service ${notServices.map(oauth_1.quote).join(', ')}` });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (request.endpoints.length > 0 && !config.endpoints) {
|
|
305
|
+
(0, http_2.send)(res, 400, { error: 'this server publishes no endpoints (endpoints is not configured)' });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const ttl = request.services.length > 0 ? (0, units_1.parseDuration)(request.ttl ?? config.runs.up.ttl) : 0;
|
|
309
|
+
if (ttl > (0, units_1.parseDuration)(config.runs.up.maxTtl)) {
|
|
310
|
+
(0, http_2.send)(res, 400, { error: `ttl: at most ${config.runs.up.maxTtl} on this server (runs.up.maxTtl)` });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// the files of a sandbox change under a run that sleeps, it would wake to others
|
|
314
|
+
if (request.sleepTtl !== undefined && request.source.type === 'sandbox') {
|
|
315
|
+
(0, http_2.send)(res, 400, { error: 'sleepTtl: a service run from a sandbox does not sleep, start it from a branch' });
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const sleepTtlText = request.source.type === 'ref' ? request.sleepTtl ?? config.runs.up.sleepTtl : undefined;
|
|
319
|
+
const sleepTtl = request.services.length > 0 && sleepTtlText ? (0, units_1.parseDuration)(sleepTtlText) : undefined;
|
|
221
320
|
const argued = (request.options.args ?? []).length > 0 ? request.tasks[0] : undefined;
|
|
222
321
|
if (argued !== undefined && version.services.includes(argued)) {
|
|
223
322
|
(0, http_2.send)(res, 400, { error: `${argued} is a service, arguments go to a task` });
|
|
@@ -239,6 +338,12 @@ async function submitRun(config, registry, definitions, sources, sandboxes, plan
|
|
|
239
338
|
return;
|
|
240
339
|
}
|
|
241
340
|
}
|
|
341
|
+
if (request.services.length > 0 && request.source.type === 'sandbox' && config.backend.type === 'kubernetes') {
|
|
342
|
+
(0, http_2.send)(res, 400, {
|
|
343
|
+
error: 'on a cluster a service run starts from a branch, push the sandbox and start it from there',
|
|
344
|
+
});
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
242
347
|
const sandbox = request.source.type === 'sandbox' ? sandboxes.registry.get(principal, request.source.id) : undefined;
|
|
243
348
|
if (request.source.type === 'sandbox') {
|
|
244
349
|
if (!sandbox) {
|
|
@@ -257,6 +362,24 @@ async function submitRun(config, registry, definitions, sources, sandboxes, plan
|
|
|
257
362
|
(0, http_2.send)(res, 413, { error: over });
|
|
258
363
|
return;
|
|
259
364
|
}
|
|
365
|
+
// a check counts like a run from a commit of the repository, and leaves what it made in
|
|
366
|
+
// the repository's cache, which runs from main restore from: the caller must be allowed
|
|
367
|
+
// to start runs from it, and to push a branch of it, which with a run from that branch
|
|
368
|
+
// leaves the same
|
|
369
|
+
if (request.source.check && !(0, repo_1.matchesAnyRepo)(grant.repos, sandbox.repo)) {
|
|
370
|
+
log(`refuse run iss=${principal.iss} sub=${principal.sub}: check on ${sandbox.repo} without the repository`);
|
|
371
|
+
(0, http_2.send)(res, 403, {
|
|
372
|
+
error: `a check counts like a run from ${sandbox.repo}, which no entitlement of yours lists in repos`,
|
|
373
|
+
});
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (request.source.check && (0, policy_1.sandboxPushBranches)(grant, sandbox.repo).length === 0) {
|
|
377
|
+
log(`refuse run iss=${principal.iss} sub=${principal.sub}: check on ${sandbox.repo} without a branch to push`);
|
|
378
|
+
(0, http_2.send)(res, 403, {
|
|
379
|
+
error: `a check counts like a run from a branch you pushed, and you may push no branch of ${sandbox.repo}`,
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
260
383
|
}
|
|
261
384
|
// the accounts of a run are read from the files it is pinned to, with the engine
|
|
262
385
|
const bundle = version.plan ? await definitions.files(version) : undefined;
|
|
@@ -275,12 +398,20 @@ async function submitRun(config, registry, definitions, sources, sandboxes, plan
|
|
|
275
398
|
(0, http_2.send)(res, 400, { error: `the run does not load: ${e instanceof Error ? e.message : String(e)}` });
|
|
276
399
|
return;
|
|
277
400
|
}
|
|
278
|
-
const refused = (0, run_plan_1.refuseArguments)(request, planned.plan)
|
|
401
|
+
const refused = (0, run_plan_1.refuseArguments)(request, planned.plan, sandbox ? sandboxes.policy : planning.policy) ??
|
|
402
|
+
(0, service_runs_1.refuseEndpoints)(request, planned.plan, config.runs.secretNetwork === 'open');
|
|
279
403
|
if (refused) {
|
|
280
404
|
log(`refuse run iss=${principal.iss} sub=${principal.sub}: ${refused}`);
|
|
281
405
|
(0, http_2.send)(res, 400, { error: refused });
|
|
282
406
|
return;
|
|
283
407
|
}
|
|
408
|
+
const runRepo = request.source.type === 'ref' ? request.source.repo : sandbox?.repo;
|
|
409
|
+
if (request.endpoints.length > 0 && (!runRepo || (0, policy_1.endpointLevel)(grant, runRepo) !== 'publish')) {
|
|
410
|
+
const error = `you may not publish endpoints of ${runRepo ?? 'this source'}, an entitlement needs endpoints with level publish for it`;
|
|
411
|
+
log(`refuse run iss=${principal.iss} sub=${principal.sub}: ${error}`);
|
|
412
|
+
(0, http_2.send)(res, 403, { error });
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
284
415
|
const decision = (0, policy_1.decideRun)({
|
|
285
416
|
grant,
|
|
286
417
|
accounts: planned.accounts,
|
|
@@ -304,6 +435,12 @@ async function submitRun(config, registry, definitions, sources, sandboxes, plan
|
|
|
304
435
|
if (request.source.type === 'ref') {
|
|
305
436
|
try {
|
|
306
437
|
const commit = await repos.resolve(request.source.repo, request.source.ref);
|
|
438
|
+
if (request.source.commit && commit !== request.source.commit) {
|
|
439
|
+
const error = `${request.source.ref} is at ${commit} now, not at ${request.source.commit}`;
|
|
440
|
+
log(`refuse run iss=${principal.iss} sub=${principal.sub}: ${error}`);
|
|
441
|
+
(0, http_2.send)(res, 409, { error });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
307
444
|
source = { type: 'ref', repo: request.source.repo, ref: request.source.ref, commit };
|
|
308
445
|
}
|
|
309
446
|
catch (e) {
|
|
@@ -318,13 +455,34 @@ async function submitRun(config, registry, definitions, sources, sandboxes, plan
|
|
|
318
455
|
else if (request.source.type === 'upload') {
|
|
319
456
|
source = request.source;
|
|
320
457
|
}
|
|
321
|
-
|
|
458
|
+
// a service run is named after its branch unless it says, and its endpoints after that
|
|
459
|
+
const serviceName = request.services.length > 0
|
|
460
|
+
? request.name ??
|
|
461
|
+
(0, service_runs_1.getServiceRunName)(request.source.type === 'ref' ? request.source.ref : sandbox?.ref)
|
|
462
|
+
: '';
|
|
463
|
+
const up = request.services.length > 0
|
|
464
|
+
? {
|
|
465
|
+
services: request.services,
|
|
466
|
+
ttl,
|
|
467
|
+
...(sleepTtl ? { sleepTtl } : {}),
|
|
468
|
+
name: serviceName,
|
|
469
|
+
endpoints: (0, service_runs_1.toRunEndpoints)(request.endpoints, (0, service_runs_1.getEndpointHosts)(serviceName, principal, request.endpoints), endpointUrl),
|
|
470
|
+
}
|
|
471
|
+
: undefined;
|
|
472
|
+
// a run that may sleep keeps what its owner is entitled by, for the check of a wake
|
|
473
|
+
const claimNames = new Set(config.entitlements.flatMap((entitlement) => Object.keys(entitlement.claims?.match ?? {})));
|
|
474
|
+
const owner = {
|
|
475
|
+
...principal,
|
|
476
|
+
claims: Object.fromEntries(Object.entries(principal.claims).filter(([name]) => claimNames.has(name))),
|
|
477
|
+
};
|
|
478
|
+
const submit = (runSource) => registry.submit(owner, {
|
|
322
479
|
definition: version.hash,
|
|
323
480
|
tasks: request.tasks,
|
|
324
481
|
accounts: planned.accounts,
|
|
325
482
|
plan: planned.plan,
|
|
326
483
|
source: runSource,
|
|
327
484
|
options: request.options,
|
|
485
|
+
...(up ? { up } : {}),
|
|
328
486
|
}, approvals);
|
|
329
487
|
try {
|
|
330
488
|
// a sandbox is read and the run submitted while nothing else changes it: the run
|
|
@@ -340,9 +498,12 @@ async function submitRun(config, registry, definitions, sources, sandboxes, plan
|
|
|
340
498
|
commit: state.commit,
|
|
341
499
|
tree: state.tree,
|
|
342
500
|
clean: state.clean,
|
|
501
|
+
...(request.source.type === 'sandbox' && request.source.check ? { check: true } : {}),
|
|
343
502
|
});
|
|
344
503
|
});
|
|
345
|
-
log(`run ${run.id} iss=${principal.iss} sub=${principal.sub} accounts=[${planned.accounts.join(',')}] source=${run.source.type}${run.source.type === 'sandbox' ? ` sandbox=${run.source.id}` : ''} tasks=${run.tasks.length}${
|
|
504
|
+
log(`run ${run.id} iss=${principal.iss} sub=${principal.sub} accounts=[${planned.accounts.join(',')}] source=${run.source.type}${run.source.type === 'sandbox' ? ` sandbox=${run.source.id}${run.source.check ? ' check' : ''}` : ''} tasks=${run.tasks.length}${run.up
|
|
505
|
+
? ` services=[${run.up.services.join(',')}] name=${run.up.name} endpoints=${run.up.endpoints.length}`
|
|
506
|
+
: ''}${approvals.length > 0 ? ` approval=[${approvals.join(',')}]` : ''}`);
|
|
346
507
|
(0, http_2.send)(res, 202, toRunResponse(run));
|
|
347
508
|
}
|
|
348
509
|
catch (e) {
|
|
@@ -379,7 +540,13 @@ async function authenticate(verifier, log, req, res) {
|
|
|
379
540
|
return null;
|
|
380
541
|
}
|
|
381
542
|
}
|
|
382
|
-
async function handle(config, registry, definitions, proposals, sources, sandboxes, planning, repos, verifier, log, req, res) {
|
|
543
|
+
async function handle(config, registry, gateway, definitions, proposals, sources, sandboxes, planning, repos, checks, verifier, log, req, res) {
|
|
544
|
+
// the hosts of endpoints are the apps of service runs, not the API
|
|
545
|
+
const label = gateway?.hostOf(req);
|
|
546
|
+
if (gateway && label) {
|
|
547
|
+
await gateway.handle(label, req, res);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
383
550
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
384
551
|
const path = url.pathname;
|
|
385
552
|
if (path === discovery_1.DISCOVERY_PATH || path === '/healthz') {
|
|
@@ -406,7 +573,7 @@ async function handle(config, registry, definitions, proposals, sources, sandbox
|
|
|
406
573
|
if (await (0, sandboxes_api_1.handleSandboxes)(config, sandboxes.registry, repos, log, principal, req, res, url)) {
|
|
407
574
|
return;
|
|
408
575
|
}
|
|
409
|
-
if (await handleRuns(config, registry, definitions, sources, sandboxes, planning, repos, log, principal, req, res, url)) {
|
|
576
|
+
if (await handleRuns(config, registry, definitions, sources, sandboxes, planning, repos, (host) => gateway?.url(host) ?? host, log, principal, req, res, url)) {
|
|
410
577
|
return;
|
|
411
578
|
}
|
|
412
579
|
if (await (0, approvals_api_1.handleApprovals)(config, registry, log, principal, req, res, url)) {
|
|
@@ -415,6 +582,9 @@ async function handle(config, registry, definitions, proposals, sources, sandbox
|
|
|
415
582
|
if (await (0, proposals_api_1.handleProposals)(config, definitions, proposals, log, principal, req, res, url)) {
|
|
416
583
|
return;
|
|
417
584
|
}
|
|
585
|
+
if (await (0, checks_api_1.handleChecks)(config, checks, definitions, log, principal, req, res, url)) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
418
588
|
(0, http_2.send)(res, 404, { error: 'not found' });
|
|
419
589
|
}
|
|
420
590
|
async function startServer(config, environment, options = {}) {
|
|
@@ -436,14 +606,22 @@ async function startServer(config, environment, options = {}) {
|
|
|
436
606
|
environment: {
|
|
437
607
|
...environment,
|
|
438
608
|
processEnvs: (0, run_environment_1.runEnvironment)(config, environment.processEnvs),
|
|
439
|
-
|
|
609
|
+
// on a cluster a local task runs in a pod of a pool's tool image
|
|
610
|
+
hosted: { catalog: (0, server_config_1.getSecretCatalog)(config), localTasksInContainers: config.backend.type === 'kubernetes' },
|
|
440
611
|
},
|
|
441
612
|
policy: {
|
|
442
613
|
envs: config.runs.env.allow,
|
|
443
|
-
reserved: (0, server_config_1.
|
|
614
|
+
reserved: (0, server_config_1.getServerAccounts)(config),
|
|
444
615
|
runtimes: config.runtimes,
|
|
445
616
|
backend: config.backend.type,
|
|
446
617
|
network: (0, work_network_1.parseWorkNetwork)(config.runs.network),
|
|
618
|
+
secretNetwork: (0, work_network_1.parseWorkNetwork)(config.runs.secretNetwork),
|
|
619
|
+
args: config.runs.args,
|
|
620
|
+
proxiedProviders: config.backend.type === 'kubernetes'
|
|
621
|
+
? Object.entries(config.backend.providers)
|
|
622
|
+
.filter(([, provider]) => provider.allow)
|
|
623
|
+
.map(([name]) => name)
|
|
624
|
+
: [],
|
|
447
625
|
},
|
|
448
626
|
};
|
|
449
627
|
const storage = config.storage.directory ?? (0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'server');
|
|
@@ -506,6 +684,11 @@ async function startServer(config, environment, options = {}) {
|
|
|
506
684
|
? new kubernetes_backend_1.KubernetesExecutor({ ...config, backend }, environment.processEnvs, executorOptions)
|
|
507
685
|
: new executor_1.HostExecutor(executorOptions));
|
|
508
686
|
const sources = new sources_1.SourceStore((0, path_1.join)(storage, 'sources'));
|
|
687
|
+
const checks = new checks_1.CheckStore((0, path_1.join)(storage, 'checks'), { keep: (0, units_1.parseDuration)(config.runs.checksKeep) });
|
|
688
|
+
const sweepChecks = () => checks.sweep().catch((e) => log(`the old checks could not be removed: ${e instanceof Error ? e.message : e}`));
|
|
689
|
+
void sweepChecks();
|
|
690
|
+
const sweeping = setInterval(sweepChecks, CHECK_SWEEP_MS);
|
|
691
|
+
sweeping.unref();
|
|
509
692
|
const registry = new runs_1.RunRegistry({
|
|
510
693
|
executor,
|
|
511
694
|
maxConcurrent: config.runs.maxConcurrent,
|
|
@@ -515,10 +698,44 @@ async function startServer(config, environment, options = {}) {
|
|
|
515
698
|
limits: config.runs.limits,
|
|
516
699
|
maxDuration: (0, units_1.parseDuration)(config.runs.maxDuration),
|
|
517
700
|
maxLogBytes: config.runs.maxLogBytes,
|
|
701
|
+
up: {
|
|
702
|
+
maxTtl: (0, units_1.parseDuration)(config.runs.up.maxTtl),
|
|
703
|
+
perCaller: config.runs.up.limits.up,
|
|
704
|
+
total: config.runs.up.total,
|
|
705
|
+
},
|
|
518
706
|
store: new run_store_1.FileRunStore((0, path_1.join)(storage, 'records')),
|
|
707
|
+
// what a check run passed, for `remote verify` to find
|
|
708
|
+
onFinished: (run, keys) => {
|
|
709
|
+
const passed = (0, checks_1.getCheckRecords)(run, keys);
|
|
710
|
+
if (passed.length > 0) {
|
|
711
|
+
checks
|
|
712
|
+
.save(passed)
|
|
713
|
+
.then(() => log(`checks run=${run.id} passed=[${passed.map((record) => record.task).join(',')}]`))
|
|
714
|
+
.catch((e) => log(`the checks of run ${run.id} were not kept: ${e instanceof Error ? e.message : e}`));
|
|
715
|
+
}
|
|
716
|
+
},
|
|
519
717
|
log,
|
|
520
718
|
});
|
|
521
719
|
await registry.restore();
|
|
720
|
+
if (executor instanceof kubernetes_backend_1.KubernetesExecutor) {
|
|
721
|
+
// the services of the runs of a server that stopped are gone with it
|
|
722
|
+
executor.sweepServiceRuns();
|
|
723
|
+
}
|
|
724
|
+
const login = config.endpoints?.login;
|
|
725
|
+
const gateway = config.endpoints
|
|
726
|
+
? new endpoints_1.EndpointGateway(config, registry, {
|
|
727
|
+
keyFile: (0, path_1.join)(storage, 'endpoints', 'session.key'),
|
|
728
|
+
verifier,
|
|
729
|
+
...(login?.clientSecret
|
|
730
|
+
? { clientSecret: () => repos.serverSecret(login.clientSecret) }
|
|
731
|
+
: {}),
|
|
732
|
+
log,
|
|
733
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
734
|
+
...(options.now ? { now: options.now } : {}),
|
|
735
|
+
mayWake: (run) => refuseWake(config, repos, run),
|
|
736
|
+
})
|
|
737
|
+
: undefined;
|
|
738
|
+
await gateway?.load();
|
|
522
739
|
sandboxes.registry.useRuns({
|
|
523
740
|
holder: (id) => registry.holder(id),
|
|
524
741
|
stop: (id) => registry.stopSandbox(id, 'the sandbox was deleted'),
|
|
@@ -529,7 +746,7 @@ async function startServer(config, environment, options = {}) {
|
|
|
529
746
|
cooldown: (0, units_1.parseDuration)(config.approvals.cooldown),
|
|
530
747
|
});
|
|
531
748
|
const listener = (req, res) => {
|
|
532
|
-
handle(config, registry, definitions, proposals, sources, sandboxes, planning, repos, verifier, log, req, res).catch((e) => {
|
|
749
|
+
handle(config, registry, gateway, definitions, proposals, sources, sandboxes, planning, repos, checks, verifier, log, req, res).catch((e) => {
|
|
533
750
|
if (e instanceof http_2.HttpError) {
|
|
534
751
|
(0, http_2.send)(res, e.status, { error: e.message });
|
|
535
752
|
return;
|
|
@@ -549,11 +766,44 @@ async function startServer(config, environment, options = {}) {
|
|
|
549
766
|
environment.file.read(config.tls.cert),
|
|
550
767
|
environment.file.read(config.tls.key),
|
|
551
768
|
]);
|
|
552
|
-
|
|
769
|
+
// the hosts of endpoints have a wildcard certificate of their own
|
|
770
|
+
const endpointsTls = config.endpoints?.tls;
|
|
771
|
+
const endpointContext = endpointsTls
|
|
772
|
+
? (0, tls_1.createSecureContext)({
|
|
773
|
+
cert: await environment.file.read(endpointsTls.cert),
|
|
774
|
+
key: await environment.file.read(endpointsTls.key),
|
|
775
|
+
})
|
|
776
|
+
: undefined;
|
|
777
|
+
const domain = config.endpoints?.domain;
|
|
778
|
+
server = (0, https_1.createServer)({
|
|
779
|
+
cert,
|
|
780
|
+
key,
|
|
781
|
+
...(endpointContext && domain
|
|
782
|
+
? {
|
|
783
|
+
SNICallback: (servername, done) => done(null, servername.toLowerCase().endsWith(`.${domain}`) ? endpointContext : undefined),
|
|
784
|
+
}
|
|
785
|
+
: {}),
|
|
786
|
+
}, listener);
|
|
553
787
|
}
|
|
554
788
|
else {
|
|
555
789
|
server = (0, http_1.createServer)(listener);
|
|
556
790
|
}
|
|
791
|
+
// WebSockets of the apps behind endpoints; the API has none. An upgraded connection is
|
|
792
|
+
// no longer the HTTP server's, it goes when the server closes.
|
|
793
|
+
const upgraded = new Set();
|
|
794
|
+
server.on('upgrade', (req, socket, head) => {
|
|
795
|
+
upgraded.add(socket);
|
|
796
|
+
socket.once('close', () => upgraded.delete(socket));
|
|
797
|
+
const label = gateway?.hostOf(req);
|
|
798
|
+
if (!gateway || !label) {
|
|
799
|
+
socket.end('HTTP/1.1 404 Not Found\r\nconnection: close\r\ncontent-length: 0\r\n\r\n');
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
gateway.upgrade(label, req, socket, head).catch((e) => {
|
|
803
|
+
log(`error upgrade ${label}: ${e instanceof Error ? e.message : String(e)}`);
|
|
804
|
+
socket.destroy();
|
|
805
|
+
});
|
|
806
|
+
});
|
|
557
807
|
await new Promise((resolve, reject) => {
|
|
558
808
|
server.once('error', reject);
|
|
559
809
|
server.listen(config.listen.port, config.listen.host, () => {
|
|
@@ -562,11 +812,12 @@ async function startServer(config, environment, options = {}) {
|
|
|
562
812
|
});
|
|
563
813
|
});
|
|
564
814
|
const address = server.address();
|
|
815
|
+
gateway?.listening(address.port, Boolean(config.tls));
|
|
565
816
|
const host = address.family === 'IPv6' ? `[${address.address}]` : address.address;
|
|
566
817
|
return {
|
|
567
818
|
url: `${config.tls ? 'https' : 'http'}://${host}:${address.port}`,
|
|
568
819
|
reload: (next) => {
|
|
569
|
-
const readers = (0, server_config_1.
|
|
820
|
+
const readers = (0, server_config_1.getServerAccounts)(config);
|
|
570
821
|
next.entitlements.forEach((entitlement, index) => {
|
|
571
822
|
const iss = (entitlement.subject ?? entitlement.group ?? entitlement.claims)?.iss;
|
|
572
823
|
if (iss && !config.issuers.some((issuer) => issuer.issuer === iss)) {
|
|
@@ -586,11 +837,13 @@ async function startServer(config, environment, options = {}) {
|
|
|
586
837
|
return { restart };
|
|
587
838
|
},
|
|
588
839
|
close: async () => {
|
|
840
|
+
clearInterval(sweeping);
|
|
589
841
|
proposals.close();
|
|
590
842
|
await registry.close();
|
|
591
843
|
await new Promise((resolve, reject) => {
|
|
592
844
|
server.close((err) => (err ? reject(err) : resolve()));
|
|
593
845
|
server.closeAllConnections();
|
|
846
|
+
upgraded.forEach((socket) => socket.destroy());
|
|
594
847
|
});
|
|
595
848
|
},
|
|
596
849
|
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mayOpenEndpoints = exports.getRunRepo = exports.toRunEndpoints = exports.refuseEndpoints = exports.getEndpointHosts = exports.getServiceRunName = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const plan_1 = require("./plan");
|
|
6
|
+
const policy_1 = require("./policy");
|
|
7
|
+
const run_request_1 = require("./run-request");
|
|
8
|
+
// The name of a service run when the request names none: the branch it runs from, as a DNS
|
|
9
|
+
// label, so the runs of several branches stand side by side and each keeps its hosts.
|
|
10
|
+
function getServiceRunName(branch) {
|
|
11
|
+
const name = (branch ?? '')
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
14
|
+
.replace(/-+/g, '-')
|
|
15
|
+
.slice(0, 40)
|
|
16
|
+
.replace(/^-+|-+$/g, '');
|
|
17
|
+
return run_request_1.SERVICE_RUN_NAME_PATTERN.test(name) ? name : 'run';
|
|
18
|
+
}
|
|
19
|
+
exports.getServiceRunName = getServiceRunName;
|
|
20
|
+
/**
|
|
21
|
+
* The host labels of a run's endpoints: its name, the port and twelve hex digits of its
|
|
22
|
+
* owner (at most 40 + 5 + 2 + 12 and three dashes: a DNS label), so two callers with a branch of the same name get hosts of their own, and a run
|
|
23
|
+
* that replaces another of the same name takes over its hosts. Two endpoints on the same
|
|
24
|
+
* port (of two services) are told apart by their place.
|
|
25
|
+
*/
|
|
26
|
+
function getEndpointHosts(name, owner, endpoints) {
|
|
27
|
+
const of = (0, crypto_1.createHash)('sha256').update(`${owner.iss}\0${owner.sub}`).digest('hex').slice(0, 12);
|
|
28
|
+
return endpoints.map((endpoint, index) => endpoints.findIndex((other) => other.port === endpoint.port) === index
|
|
29
|
+
? `${name}-${endpoint.port}-${of}`
|
|
30
|
+
: `${name}-${endpoint.port}-${index}-${of}`);
|
|
31
|
+
}
|
|
32
|
+
exports.getEndpointHosts = getEndpointHosts;
|
|
33
|
+
// The container port of each `host:container` port of a planned service.
|
|
34
|
+
function containerPorts(plan) {
|
|
35
|
+
return plan.ports.map((port) => Number(port.split(':').pop()));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Why the endpoints of a service run can't be published, or null. Each is a port its
|
|
39
|
+
* service declares, of a service the run starts. Under a `runs.secretNetwork` below `open`
|
|
40
|
+
* a service behind an endpoint may not read a secret or a credential of the server: a page
|
|
41
|
+
* is a way out that no mask of the log covers.
|
|
42
|
+
*/
|
|
43
|
+
function refuseEndpoints(request, plan, secretNetworkOpen) {
|
|
44
|
+
for (const endpoint of request.endpoints) {
|
|
45
|
+
const item = plan[endpoint.service];
|
|
46
|
+
if (!item || item.kind !== 'service') {
|
|
47
|
+
return `the endpoint ${endpoint.service}:${endpoint.port} names no service the run starts`;
|
|
48
|
+
}
|
|
49
|
+
if (!containerPorts(item).includes(endpoint.port)) {
|
|
50
|
+
const ports = containerPorts(item);
|
|
51
|
+
return `${endpoint.service} has no port ${endpoint.port}${ports.length > 0 ? `, its ports are ${ports.join(', ')}` : ', it declares none'}`;
|
|
52
|
+
}
|
|
53
|
+
const reads = (0, plan_1.getCredentialReads)(item);
|
|
54
|
+
if (!secretNetworkOpen && reads.length > 0) {
|
|
55
|
+
return `${endpoint.service} reads ${reads.join(', ')}, it can not be behind an endpoint on this server (runs.secretNetwork): a page could show it`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
exports.refuseEndpoints = refuseEndpoints;
|
|
61
|
+
function toRunEndpoints(endpoints, hosts, url) {
|
|
62
|
+
return endpoints.map((endpoint, index) => ({
|
|
63
|
+
service: endpoint.service,
|
|
64
|
+
port: endpoint.port,
|
|
65
|
+
host: hosts[index],
|
|
66
|
+
url: url(hosts[index]),
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
exports.toRunEndpoints = toRunEndpoints;
|
|
70
|
+
// The repository of a run's source, which the endpoints grants are written for.
|
|
71
|
+
function getRunRepo(run) {
|
|
72
|
+
return run.source.type === 'upload' ? null : run.source.repo;
|
|
73
|
+
}
|
|
74
|
+
exports.getRunRepo = getRunRepo;
|
|
75
|
+
// Who may open the endpoints of a service run, and extend it, besides its owner: those
|
|
76
|
+
// with an `open` or `publish` level for its repository.
|
|
77
|
+
function mayOpenEndpoints(grant, run) {
|
|
78
|
+
const repo = getRunRepo(run);
|
|
79
|
+
return repo !== null && (0, policy_1.endpointLevel)(grant, repo) !== null;
|
|
80
|
+
}
|
|
81
|
+
exports.mayOpenEndpoints = mayOpenEndpoints;
|
|
82
|
+
//# sourceMappingURL=service-runs.js.map
|
|
@@ -1,17 +1,57 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.abortOnSignals = exports.ABORT_SIGNALS = void 0;
|
|
3
|
+
exports.takeOverSignal = exports.abortOnSignals = exports.ABORT_SIGNALS = void 0;
|
|
4
4
|
// Ctrl-C (SIGINT), a CI job being cancelled or a supervisor stopping hammerkit
|
|
5
5
|
// (SIGTERM) and a closed terminal (SIGHUP) all stop the run the same way: abort,
|
|
6
6
|
// so services are removed and local tasks' process groups are killed. Without a
|
|
7
7
|
// handler Node exits on SIGTERM/SIGHUP at once and leaves both behind.
|
|
8
8
|
exports.ABORT_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
// signals a running command gives a meaning of its own (the server reloads its
|
|
10
|
+
// entitlements on SIGHUP), per source and counted by how many hold them: they don't
|
|
11
|
+
// abort the run
|
|
12
|
+
const takenOver = new WeakMap();
|
|
13
|
+
function heldOn(source) {
|
|
14
|
+
let held = takenOver.get(source);
|
|
15
|
+
if (!held) {
|
|
16
|
+
held = new Map();
|
|
17
|
+
takenOver.set(source, held);
|
|
14
18
|
}
|
|
19
|
+
return held;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Stops the run on the abort signals, all but those a command took over with
|
|
23
|
+
* `takeOverSignal`. Returns what removes the listeners again.
|
|
24
|
+
*/
|
|
25
|
+
function abortOnSignals(source, abortCtrl) {
|
|
26
|
+
const listeners = exports.ABORT_SIGNALS.map((signal) => {
|
|
27
|
+
const listener = () => {
|
|
28
|
+
if (!takenOver.get(source)?.get(signal)) {
|
|
29
|
+
abortCtrl.abort();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
source.on(signal, listener);
|
|
33
|
+
return { signal, listener };
|
|
34
|
+
});
|
|
35
|
+
return () => listeners.forEach(({ signal, listener }) => source.off(signal, listener));
|
|
15
36
|
}
|
|
16
37
|
exports.abortOnSignals = abortOnSignals;
|
|
38
|
+
/**
|
|
39
|
+
* Calls `handler` on `signal` instead of stopping the run, until the returned function
|
|
40
|
+
* gives the signal back.
|
|
41
|
+
*/
|
|
42
|
+
function takeOverSignal(source, signal, handler) {
|
|
43
|
+
const held = heldOn(source);
|
|
44
|
+
held.set(signal, (held.get(signal) ?? 0) + 1);
|
|
45
|
+
source.on(signal, handler);
|
|
46
|
+
let released = false;
|
|
47
|
+
return () => {
|
|
48
|
+
if (released) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
released = true;
|
|
52
|
+
source.off(signal, handler);
|
|
53
|
+
held.set(signal, (held.get(signal) ?? 1) - 1);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
exports.takeOverSignal = takeOverSignal;
|
|
17
57
|
//# sourceMappingURL=abort-on-signals.js.map
|