hammerkit 1.23.0 → 1.25.0
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 +10 -0
- package/dist/cli.js +6 -0
- package/dist/executer/execute-command.js +7 -1
- package/dist/optimizer/get-cache-directory.js +5 -0
- package/dist/planner/utils/append-work-task.js +1 -0
- package/dist/planner/work-secret.js +6 -2
- package/dist/program.js +6 -0
- package/dist/remote/bundle.js +59 -0
- package/dist/remote/collect-bundle.js +41 -0
- package/dist/remote/credentials.js +93 -0
- package/dist/remote/device-login.js +177 -0
- package/dist/remote/host-config.js +170 -0
- package/dist/remote/location.js +46 -0
- package/dist/remote/remote-commands.js +158 -0
- package/dist/remote/remote-work-commands.js +224 -0
- package/dist/remote/server-client.js +88 -0
- package/dist/remote/source-manifest.js +79 -0
- package/dist/remote/text-diff.js +89 -0
- package/dist/schema/build-file-base-task-schema.js +4 -0
- package/dist/schema/build-file-schema.js +3 -0
- package/dist/schema/reference-parser.js +9 -2
- package/dist/schema/runner-schema.js +13 -0
- package/dist/secrets/provider-values.js +4 -0
- package/dist/server/accounts-api.js +42 -0
- package/dist/server/approvals-api.js +104 -0
- package/dist/server/definitions-api.js +163 -0
- package/dist/server/definitions.js +160 -0
- package/dist/server/discovery.js +9 -0
- package/dist/server/executor.js +237 -0
- package/dist/server/git-checkout.js +115 -0
- package/dist/server/http.js +39 -0
- package/dist/server/oauth.js +226 -0
- package/dist/server/plan.js +296 -0
- package/dist/server/policy.js +138 -0
- package/dist/server/proposals-api.js +145 -0
- package/dist/server/proposals.js +173 -0
- package/dist/server/repo-access.js +127 -0
- package/dist/server/repo.js +94 -0
- package/dist/server/run-environment.js +20 -0
- package/dist/server/run-plan.js +44 -0
- package/dist/server/run-request.js +85 -0
- package/dist/server/run-store.js +61 -0
- package/dist/server/run-worker-command.js +17 -0
- package/dist/server/run-worker-main.js +7 -0
- package/dist/server/run-worker.js +179 -0
- package/dist/server/runs.js +432 -0
- package/dist/server/server-command.js +61 -0
- package/dist/server/server-config.js +394 -0
- package/dist/server/server.js +434 -0
- package/dist/server/sources-api.js +106 -0
- package/dist/server/sources.js +258 -0
- package/package.json +1 -1
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.unavailableExecutor = exports.RunRegistry = exports.RUN_ID_PATTERN = exports.QuotaError = exports.RunError = exports.isFinal = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const units_1 = require("../utils/units");
|
|
6
|
+
const FINAL_STATES = ['succeeded', 'failed', 'cancelled', 'rejected'];
|
|
7
|
+
function isFinal(state) {
|
|
8
|
+
return FINAL_STATES.includes(state);
|
|
9
|
+
}
|
|
10
|
+
exports.isFinal = isFinal;
|
|
11
|
+
// The server can not take the run now; `status` is what the caller is told
|
|
12
|
+
class RunError extends Error {
|
|
13
|
+
constructor() {
|
|
14
|
+
super(...arguments);
|
|
15
|
+
this.status = 503;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.RunError = RunError;
|
|
19
|
+
// A caller is over one of their limits, others may still submit
|
|
20
|
+
class QuotaError extends RunError {
|
|
21
|
+
constructor() {
|
|
22
|
+
super(...arguments);
|
|
23
|
+
this.status = 429;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.QuotaError = QuotaError;
|
|
27
|
+
exports.RUN_ID_PATTERN = /^run_[A-Za-z0-9_-]{16}$/;
|
|
28
|
+
// Where runs are kept while the server is up. Everything is scoped to the
|
|
29
|
+
// caller that submitted the run: the same id asked for by someone else is
|
|
30
|
+
// "not found", so ids can't be used to find out which runs exist.
|
|
31
|
+
class RunRegistry {
|
|
32
|
+
constructor(options) {
|
|
33
|
+
this.options = options;
|
|
34
|
+
this.entries = new Map();
|
|
35
|
+
this.queue = [];
|
|
36
|
+
this.running = 0;
|
|
37
|
+
this.closed = false;
|
|
38
|
+
this.maxConcurrent = options.maxConcurrent ?? 4;
|
|
39
|
+
this.keep = options.keep ?? 200;
|
|
40
|
+
this.maxEvents = options.maxEvents ?? 5000;
|
|
41
|
+
this.maxWaiting = options.maxWaiting ?? 100;
|
|
42
|
+
this.approvalTimeout = options.approvalTimeout ?? 60 * 60 * 1000;
|
|
43
|
+
this.now = options.now ?? Date.now;
|
|
44
|
+
this.maxLogBytes = options.maxLogBytes ?? Infinity;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Takes back the runs of a previous start. Those that waited for an approval wait
|
|
48
|
+
* again, for what is left of their time; those that were queued or running when the
|
|
49
|
+
* server stopped failed with it, and say so.
|
|
50
|
+
*/
|
|
51
|
+
async restore() {
|
|
52
|
+
const store = this.options.store;
|
|
53
|
+
if (!store) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const { records, skipped } = await store.load();
|
|
57
|
+
skipped.forEach((name) => this.options.log?.(`skip run record ${name}: it can not be read`));
|
|
58
|
+
for (const { run, events } of records) {
|
|
59
|
+
const entry = {
|
|
60
|
+
run,
|
|
61
|
+
events,
|
|
62
|
+
listeners: new Set(),
|
|
63
|
+
abort: new AbortController(),
|
|
64
|
+
truncated: events.some((event) => event.type === 'truncated'),
|
|
65
|
+
bytes: 0,
|
|
66
|
+
saving: Promise.resolve(),
|
|
67
|
+
};
|
|
68
|
+
this.entries.set(run.id, entry);
|
|
69
|
+
if (run.state === 'awaiting-approval') {
|
|
70
|
+
const left = Date.parse(run.approvals?.[0]?.expiresAt ?? '') - this.now();
|
|
71
|
+
if (Number.isFinite(left) && left > 0) {
|
|
72
|
+
entry.timer = setTimeout(() => this.expire(entry), left);
|
|
73
|
+
entry.timer.unref();
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
this.finish(entry, 'rejected', 'nobody approved it in time');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (run.state === 'queued' || run.state === 'running') {
|
|
80
|
+
this.finish(entry, 'failed', `the server stopped while the run was ${run.state}, submit it again`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
this.evict();
|
|
84
|
+
}
|
|
85
|
+
// A run that needs approvals (for the accounts named) waits for them, the others
|
|
86
|
+
// are queued at once.
|
|
87
|
+
submit(owner, spec, approvalAccounts = []) {
|
|
88
|
+
if (this.closed) {
|
|
89
|
+
throw new RunError('the server is shutting down');
|
|
90
|
+
}
|
|
91
|
+
const waiting = approvalAccounts.length > 0;
|
|
92
|
+
if (waiting && this.waiting().length >= this.maxWaiting) {
|
|
93
|
+
throw new RunError('too many runs are waiting for an approval');
|
|
94
|
+
}
|
|
95
|
+
this.checkLimits(owner, waiting);
|
|
96
|
+
const run = {
|
|
97
|
+
...spec,
|
|
98
|
+
id: `run_${(0, crypto_1.randomBytes)(12).toString('base64url')}`,
|
|
99
|
+
owner: { iss: owner.iss, sub: owner.sub },
|
|
100
|
+
state: waiting ? 'awaiting-approval' : 'queued',
|
|
101
|
+
createdAt: this.timestamp(),
|
|
102
|
+
};
|
|
103
|
+
const entry = {
|
|
104
|
+
run,
|
|
105
|
+
events: [],
|
|
106
|
+
listeners: new Set(),
|
|
107
|
+
abort: new AbortController(),
|
|
108
|
+
truncated: false,
|
|
109
|
+
bytes: 0,
|
|
110
|
+
saving: Promise.resolve(),
|
|
111
|
+
};
|
|
112
|
+
this.entries.set(run.id, entry);
|
|
113
|
+
if (waiting) {
|
|
114
|
+
// before the first event, which is written down with the run
|
|
115
|
+
const expiresAt = new Date(this.now() + this.approvalTimeout).toISOString();
|
|
116
|
+
run.approvals = approvalAccounts.map((account) => ({ account, expiresAt }));
|
|
117
|
+
}
|
|
118
|
+
this.record(entry, { type: 'state', state: run.state });
|
|
119
|
+
if (waiting) {
|
|
120
|
+
entry.timer = setTimeout(() => this.expire(entry), this.approvalTimeout);
|
|
121
|
+
entry.timer.unref();
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.queue.push(entry);
|
|
125
|
+
}
|
|
126
|
+
this.evict();
|
|
127
|
+
// what the caller is told is the run as accepted, even when it starts at once
|
|
128
|
+
const accepted = this.copy(run);
|
|
129
|
+
this.dispatch();
|
|
130
|
+
return accepted;
|
|
131
|
+
}
|
|
132
|
+
// `canUse` also lets who uses an account the run needed approved read it: they
|
|
133
|
+
// decided about it, so they may follow it and stop it.
|
|
134
|
+
get(owner, id, canUse) {
|
|
135
|
+
const entry = this.find(owner, id, canUse);
|
|
136
|
+
return entry ? this.copy(entry.run) : undefined;
|
|
137
|
+
}
|
|
138
|
+
// newest first
|
|
139
|
+
list(owner) {
|
|
140
|
+
return [...this.entries.values()]
|
|
141
|
+
.filter((entry) => this.owns(owner, entry))
|
|
142
|
+
.map((entry) => this.copy(entry.run))
|
|
143
|
+
.reverse();
|
|
144
|
+
}
|
|
145
|
+
// A queued run is cancelled at once; a running one when its executor stops.
|
|
146
|
+
cancel(owner, id, canUse) {
|
|
147
|
+
const entry = this.find(owner, id, canUse);
|
|
148
|
+
if (!entry) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
if (entry.run.state === 'queued') {
|
|
152
|
+
this.queue.splice(this.queue.indexOf(entry), 1);
|
|
153
|
+
this.finish(entry, 'cancelled');
|
|
154
|
+
}
|
|
155
|
+
else if (entry.run.state === 'awaiting-approval') {
|
|
156
|
+
this.finish(entry, 'cancelled');
|
|
157
|
+
}
|
|
158
|
+
else if (entry.run.state === 'running') {
|
|
159
|
+
entry.abort.abort();
|
|
160
|
+
}
|
|
161
|
+
return this.copy(entry.run);
|
|
162
|
+
}
|
|
163
|
+
// The runs that wait for an approval, of anybody, oldest first. Who may see
|
|
164
|
+
// them is for the caller to decide.
|
|
165
|
+
waiting() {
|
|
166
|
+
return [...this.entries.values()]
|
|
167
|
+
.filter((entry) => entry.run.state === 'awaiting-approval')
|
|
168
|
+
.map((entry) => this.copy(entry.run));
|
|
169
|
+
}
|
|
170
|
+
// One run that waits (or waited), if `canDecide` says the decider uses one of the
|
|
171
|
+
// accounts it still needs approved.
|
|
172
|
+
getForApproval(id, canDecide) {
|
|
173
|
+
const entry = this.entries.get(id);
|
|
174
|
+
if (!entry || !this.mayDecide(entry, canDecide)) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
return this.copy(entry.run);
|
|
178
|
+
}
|
|
179
|
+
// An approval covers the accounts the approver uses; the run is queued when all
|
|
180
|
+
// of its accounts are covered. A rejection by anybody who covers one of them ends
|
|
181
|
+
// it. Nobody decides on their own run.
|
|
182
|
+
decide(decider, id, outcome, canDecide, reason) {
|
|
183
|
+
const entry = this.entries.get(id);
|
|
184
|
+
if (!entry || !this.mayDecide(entry, canDecide)) {
|
|
185
|
+
return { status: 'not-found' };
|
|
186
|
+
}
|
|
187
|
+
if (this.owns(decider, entry)) {
|
|
188
|
+
return { status: 'own-run' };
|
|
189
|
+
}
|
|
190
|
+
if (entry.run.state !== 'awaiting-approval' || !entry.run.approvals) {
|
|
191
|
+
return { status: 'not-waiting', state: entry.run.state };
|
|
192
|
+
}
|
|
193
|
+
const by = { iss: decider.iss, sub: decider.sub };
|
|
194
|
+
const covered = entry.run.approvals.filter((approval) => !approval.approvedBy && canDecide(approval.account));
|
|
195
|
+
if (outcome === 'approved' && covered.length === 0) {
|
|
196
|
+
return { status: 'already-approved' };
|
|
197
|
+
}
|
|
198
|
+
const accounts = entry.run.approvals
|
|
199
|
+
.filter((approval) => canDecide(approval.account))
|
|
200
|
+
.map((approval) => approval.account);
|
|
201
|
+
this.record(entry, { type: 'approval', outcome, by, accounts, ...(reason ? { reason } : {}) });
|
|
202
|
+
if (outcome === 'rejected') {
|
|
203
|
+
this.finish(entry, 'rejected', `rejected by ${by.sub}${reason ? `: ${reason}` : ''}`);
|
|
204
|
+
return { status: 'ok', run: this.copy(entry.run) };
|
|
205
|
+
}
|
|
206
|
+
const approvedAt = this.timestamp();
|
|
207
|
+
for (const approval of covered) {
|
|
208
|
+
approval.approvedBy = by;
|
|
209
|
+
approval.approvedAt = approvedAt;
|
|
210
|
+
}
|
|
211
|
+
if (entry.run.approvals.some((approval) => !approval.approvedBy)) {
|
|
212
|
+
// some accounts still wait for somebody who uses them
|
|
213
|
+
return { status: 'ok', run: this.copy(entry.run) };
|
|
214
|
+
}
|
|
215
|
+
this.clearTimer(entry);
|
|
216
|
+
entry.run.state = 'queued';
|
|
217
|
+
this.record(entry, { type: 'state', state: 'queued' });
|
|
218
|
+
this.queue.push(entry);
|
|
219
|
+
const approved = this.copy(entry.run);
|
|
220
|
+
this.dispatch();
|
|
221
|
+
return { status: 'ok', run: approved };
|
|
222
|
+
}
|
|
223
|
+
// The events after `after` (0 for all), and a way to follow new ones. The
|
|
224
|
+
// listener is not called for a run that is already over.
|
|
225
|
+
watch(owner, id, after, listener, canUse) {
|
|
226
|
+
const entry = this.find(owner, id, canUse);
|
|
227
|
+
if (!entry) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
const events = entry.events.filter((event) => event.seq > after);
|
|
231
|
+
const finished = FINAL_STATES.includes(entry.run.state);
|
|
232
|
+
if (!finished) {
|
|
233
|
+
entry.listeners.add(listener);
|
|
234
|
+
}
|
|
235
|
+
return { events, finished, stop: () => entry.listeners.delete(listener) };
|
|
236
|
+
}
|
|
237
|
+
// Stops accepting runs and aborts the ones in flight.
|
|
238
|
+
async close() {
|
|
239
|
+
this.closed = true;
|
|
240
|
+
for (const entry of this.queue.splice(0)) {
|
|
241
|
+
this.finish(entry, 'cancelled');
|
|
242
|
+
}
|
|
243
|
+
for (const entry of this.entries.values()) {
|
|
244
|
+
if (entry.run.state === 'awaiting-approval') {
|
|
245
|
+
if (this.options.store) {
|
|
246
|
+
// the record keeps it waiting, the next start takes it back
|
|
247
|
+
this.clearTimer(entry);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
this.finish(entry, 'cancelled');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const entry of this.entries.values()) {
|
|
255
|
+
if (entry.run.state === 'running') {
|
|
256
|
+
entry.abort.abort();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
while (this.running > 0) {
|
|
260
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
261
|
+
}
|
|
262
|
+
// the runs that were stopped are written down before the server is gone
|
|
263
|
+
await Promise.all([...this.entries.values()].map((entry) => entry.saving));
|
|
264
|
+
}
|
|
265
|
+
find(owner, id, canUse) {
|
|
266
|
+
const entry = this.entries.get(id);
|
|
267
|
+
return entry && (this.owns(owner, entry) || (canUse && this.mayDecide(entry, canUse))) ? entry : undefined;
|
|
268
|
+
}
|
|
269
|
+
// Whoever uses an account the run needed approved is to see it and decide.
|
|
270
|
+
mayDecide(entry, canDecide) {
|
|
271
|
+
return (entry.run.approvals ?? []).some((approval) => canDecide(approval.account));
|
|
272
|
+
}
|
|
273
|
+
expire(entry) {
|
|
274
|
+
if (entry.run.state === 'awaiting-approval') {
|
|
275
|
+
this.finish(entry, 'rejected', 'nobody approved it in time');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
clearTimer(entry) {
|
|
279
|
+
if (entry.timer) {
|
|
280
|
+
clearTimeout(entry.timer);
|
|
281
|
+
entry.timer = undefined;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
owns(owner, entry) {
|
|
285
|
+
return entry.run.owner.iss === owner.iss && entry.run.owner.sub === owner.sub;
|
|
286
|
+
}
|
|
287
|
+
copy(run) {
|
|
288
|
+
return {
|
|
289
|
+
...run,
|
|
290
|
+
tasks: [...run.tasks],
|
|
291
|
+
definition: { ...run.definition },
|
|
292
|
+
source: { ...run.source },
|
|
293
|
+
options: JSON.parse(JSON.stringify(run.options)),
|
|
294
|
+
owner: { ...run.owner },
|
|
295
|
+
accounts: [...run.accounts],
|
|
296
|
+
...(run.approvals ? { approvals: JSON.parse(JSON.stringify(run.approvals)) } : {}),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
timestamp() {
|
|
300
|
+
return new Date(this.now()).toISOString();
|
|
301
|
+
}
|
|
302
|
+
// What a caller has now, against what they may have.
|
|
303
|
+
checkLimits(owner, waiting) {
|
|
304
|
+
const limits = this.options.limits;
|
|
305
|
+
if (!limits) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const mine = [...this.entries.values()].filter((entry) => this.owns(owner, entry));
|
|
309
|
+
const count = (state) => mine.filter((entry) => entry.run.state === state).length;
|
|
310
|
+
if (waiting && count('awaiting-approval') >= limits.waiting) {
|
|
311
|
+
throw new QuotaError(`you have ${limits.waiting} runs waiting for an approval, wait for a decision or cancel one`);
|
|
312
|
+
}
|
|
313
|
+
if (!waiting && count('queued') >= limits.queued) {
|
|
314
|
+
throw new QuotaError(`you have ${limits.queued} runs in the queue, wait for one to start or cancel one`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
runningOf(owner) {
|
|
318
|
+
return [...this.entries.values()].filter((entry) => entry.run.state === 'running' && this.owns(owner, entry)).length;
|
|
319
|
+
}
|
|
320
|
+
// The first run in the queue whose caller has a place left: one caller with many runs
|
|
321
|
+
// does not keep the others from the machine.
|
|
322
|
+
dispatch() {
|
|
323
|
+
while (this.running < this.maxConcurrent) {
|
|
324
|
+
const index = this.queue.findIndex((entry) => !this.options.limits || this.runningOf(entry.run.owner) < this.options.limits.running);
|
|
325
|
+
if (index < 0) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const [entry] = this.queue.splice(index, 1);
|
|
329
|
+
this.running++;
|
|
330
|
+
this.execute(entry).finally(() => {
|
|
331
|
+
this.running--;
|
|
332
|
+
this.dispatch();
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async execute(entry) {
|
|
337
|
+
entry.run.state = 'running';
|
|
338
|
+
entry.run.startedAt = this.timestamp();
|
|
339
|
+
this.record(entry, { type: 'state', state: 'running' });
|
|
340
|
+
const limit = this.options.maxDuration;
|
|
341
|
+
const timer = limit
|
|
342
|
+
? setTimeout(() => {
|
|
343
|
+
entry.timedOut = true;
|
|
344
|
+
entry.abort.abort();
|
|
345
|
+
}, limit)
|
|
346
|
+
: undefined;
|
|
347
|
+
timer?.unref();
|
|
348
|
+
try {
|
|
349
|
+
await this.options.executor.execute(this.copy(entry.run), {
|
|
350
|
+
emit: (event) => this.record(entry, event),
|
|
351
|
+
signal: entry.abort.signal,
|
|
352
|
+
});
|
|
353
|
+
this.finish(entry, entry.timedOut ? 'failed' : entry.abort.signal.aborted ? 'cancelled' : 'succeeded', entry.timedOut ? this.tooLong() : undefined);
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
const cancelled = entry.abort.signal.aborted && !entry.timedOut;
|
|
357
|
+
this.finish(entry, cancelled ? 'cancelled' : 'failed', entry.timedOut ? this.tooLong() : cancelled ? undefined : e instanceof Error ? e.message : String(e));
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
clearTimeout(timer);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
tooLong() {
|
|
364
|
+
return `the run took longer than ${(0, units_1.formatDuration)(this.options.maxDuration)} and was stopped`;
|
|
365
|
+
}
|
|
366
|
+
finish(entry, state, error) {
|
|
367
|
+
this.clearTimer(entry);
|
|
368
|
+
entry.run.state = state;
|
|
369
|
+
entry.run.finishedAt = this.timestamp();
|
|
370
|
+
if (error) {
|
|
371
|
+
entry.run.error = error;
|
|
372
|
+
}
|
|
373
|
+
this.record(entry, { type: 'state', state, ...(error ? { error } : {}) }, true);
|
|
374
|
+
entry.listeners.clear();
|
|
375
|
+
this.evict();
|
|
376
|
+
}
|
|
377
|
+
record(entry, event, force = false) {
|
|
378
|
+
// an executor that keeps talking can't fill the memory of the server or its disk; the
|
|
379
|
+
// state changes, which are few, are always kept
|
|
380
|
+
if (!force && event.type !== 'state') {
|
|
381
|
+
const size = event.type === 'log' ? Buffer.byteLength(event.text) + 64 : 64;
|
|
382
|
+
if (entry.events.length >= this.maxEvents || entry.bytes + size > this.maxLogBytes) {
|
|
383
|
+
if (!entry.truncated) {
|
|
384
|
+
entry.truncated = true;
|
|
385
|
+
this.push(entry, { type: 'truncated' });
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
entry.bytes += size;
|
|
390
|
+
}
|
|
391
|
+
this.push(entry, event);
|
|
392
|
+
}
|
|
393
|
+
push(entry, event) {
|
|
394
|
+
const stored = { ...event, seq: entry.events.length + 1, time: this.timestamp() };
|
|
395
|
+
entry.events.push(stored);
|
|
396
|
+
for (const listener of entry.listeners) {
|
|
397
|
+
listener(stored);
|
|
398
|
+
}
|
|
399
|
+
// what a restart must not lose: the runs that wait, and how a run ended. A running
|
|
400
|
+
// run is not kept up to date, it fails with the server.
|
|
401
|
+
if (event.type === 'state' || event.type === 'approval') {
|
|
402
|
+
this.persist(entry);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
persist(entry) {
|
|
406
|
+
const store = this.options.store;
|
|
407
|
+
if (!store) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const record = { run: this.copy(entry.run), events: [...entry.events] };
|
|
411
|
+
entry.saving = entry.saving
|
|
412
|
+
.then(() => store.save(record))
|
|
413
|
+
.catch((e) => this.options.log?.(`could not keep run ${record.run.id}: ${e instanceof Error ? e.message : e}`));
|
|
414
|
+
}
|
|
415
|
+
// Only finished runs are dropped, oldest first.
|
|
416
|
+
evict() {
|
|
417
|
+
const finished = [...this.entries.values()].filter((entry) => FINAL_STATES.includes(entry.run.state));
|
|
418
|
+
for (const entry of finished.slice(0, Math.max(0, finished.length - this.keep))) {
|
|
419
|
+
this.entries.delete(entry.run.id);
|
|
420
|
+
void entry.saving.then(() => this.options.store?.remove(entry.run.id)).catch(() => undefined);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
exports.RunRegistry = RunRegistry;
|
|
425
|
+
// Used until a backend is wired in: runs are accepted, recorded and failed with
|
|
426
|
+
// a reason, so the API can be used and tested without pretending to execute.
|
|
427
|
+
exports.unavailableExecutor = {
|
|
428
|
+
async execute() {
|
|
429
|
+
throw new Error('this server has no backend that can execute runs yet');
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
//# sourceMappingURL=runs.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerServerCommand = void 0;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const log_1 = require("../log");
|
|
6
|
+
const server_config_1 = require("./server-config");
|
|
7
|
+
const server_1 = require("./server");
|
|
8
|
+
// Runs this machine as a hammerkit server until interrupted. Like `remote`, it
|
|
9
|
+
// only needs its own config file, not a build file.
|
|
10
|
+
function registerServerCommand(program, environment) {
|
|
11
|
+
program
|
|
12
|
+
.command('server')
|
|
13
|
+
.description('serve this machine to remote runs (experimental)')
|
|
14
|
+
.addOption(new commander_1.Option('-c, --config <file>', 'server config file').env('HAMMERKIT_SERVER_CONFIG'))
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
try {
|
|
17
|
+
if (!options.config) {
|
|
18
|
+
throw new Error('pass the server config with --config or HAMMERKIT_SERVER_CONFIG');
|
|
19
|
+
}
|
|
20
|
+
const config = await (0, server_config_1.readServerConfig)(environment, options.config);
|
|
21
|
+
const server = await (0, server_1.startServer)(config, environment, {
|
|
22
|
+
log: (line) => environment.stdout.write(`${line}\n`),
|
|
23
|
+
});
|
|
24
|
+
environment.stdout.write(`hammerkit server listening on ${server.url}\n`);
|
|
25
|
+
// SIGHUP reads the entitlements again, so who may do what changes without
|
|
26
|
+
// a restart; the rest of the config needs one
|
|
27
|
+
const reload = async () => {
|
|
28
|
+
try {
|
|
29
|
+
const result = server.reload(await (0, server_config_1.readServerConfig)(environment, options.config));
|
|
30
|
+
if (result.restart.length > 0) {
|
|
31
|
+
environment.stdout.write(`changed but not reloaded, restart for: ${result.restart.join(', ')}\n`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
environment.stdout.write(`entitlements were not reloaded: ${(0, log_1.getErrorMessage)(e)}\n`);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const onReload = () => void reload();
|
|
39
|
+
if (process.platform !== 'win32') {
|
|
40
|
+
process.on('SIGHUP', onReload);
|
|
41
|
+
}
|
|
42
|
+
await new Promise((resolve) => {
|
|
43
|
+
if (environment.abortCtrl.signal.aborted) {
|
|
44
|
+
resolve();
|
|
45
|
+
}
|
|
46
|
+
environment.abortCtrl.signal.addEventListener('abort', () => resolve(), { once: true });
|
|
47
|
+
});
|
|
48
|
+
process.off('SIGHUP', onReload);
|
|
49
|
+
await server.close();
|
|
50
|
+
environment.stdout.write('hammerkit server stopped\n');
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
if (e instanceof commander_1.CommanderError) {
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
program.error(`Server was not successful: ${(0, log_1.getErrorMessage)(e)}`, { exitCode: 1 });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
exports.registerServerCommand = registerServerCommand;
|
|
61
|
+
//# sourceMappingURL=server-command.js.map
|