gcf-board 0.2.16 → 0.2.17
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/server/app.js
CHANGED
|
@@ -90,8 +90,13 @@ async function sendPage(res, web, pathname) {
|
|
|
90
90
|
}
|
|
91
91
|
export function createApp(o) {
|
|
92
92
|
const watchers = new Map();
|
|
93
|
+
let closed = false;
|
|
93
94
|
const watchAll = async () => {
|
|
94
|
-
|
|
95
|
+
const projects = await listProjects();
|
|
96
|
+
// A watcher set up after the server closed would keep the process alive.
|
|
97
|
+
if (closed)
|
|
98
|
+
return;
|
|
99
|
+
for (const p of projects)
|
|
95
100
|
if (!watchers.has(p.name))
|
|
96
101
|
watchers.set(p.name, watchProject(p));
|
|
97
102
|
};
|
|
@@ -303,15 +308,18 @@ export function createApp(o) {
|
|
|
303
308
|
const server = createServer((req, res) => {
|
|
304
309
|
handle(req, res).catch((e) => {
|
|
305
310
|
if (res.headersSent)
|
|
306
|
-
|
|
307
|
-
|
|
311
|
+
res.end();
|
|
312
|
+
else
|
|
313
|
+
send(res, e instanceof HttpError ? e.status : 500, { error: e.message });
|
|
308
314
|
});
|
|
309
315
|
});
|
|
310
316
|
// A broken projects.json must not take the server down: the page shows the error instead.
|
|
311
317
|
server.on('listening', () => watchAll().catch((e) => console.error(`gcf-board: ${e.message}`)));
|
|
312
318
|
server.on('close', () => {
|
|
319
|
+
closed = true;
|
|
313
320
|
for (const stop of watchers.values())
|
|
314
321
|
stop();
|
|
322
|
+
watchers.clear();
|
|
315
323
|
});
|
|
316
324
|
return server;
|
|
317
325
|
}
|
package/dist/server/auth.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// The board starts agents that commit and push: whoever talks to it runs code on this machine.
|
|
2
|
-
// It listens on 127.0.0.1 only
|
|
3
|
-
// cookie
|
|
4
|
-
// reach 127.0.0.1 (DNS rebinding, a
|
|
2
|
+
// It listens on 127.0.0.1 only. The URL printed at start carries the token; that visit gets a
|
|
3
|
+
// session cookie (one per server process, never the token itself), and every request needs it.
|
|
4
|
+
// Host and Origin are checked too: a page in another tab can reach 127.0.0.1 (DNS rebinding, a
|
|
5
|
+
// cross-site POST), but it cannot fake those headers.
|
|
5
6
|
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
6
7
|
import { existsSync } from 'node:fs';
|
|
7
8
|
import { mkdir, readFile } from 'node:fs/promises';
|
package/dist/server/cli.js
CHANGED
|
@@ -71,6 +71,8 @@ export function runCli(entry, cwd, args, timeoutMs = 120_000) {
|
|
|
71
71
|
spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
72
72
|
else
|
|
73
73
|
child.kill();
|
|
74
|
+
// A grandchild holding the pipes keeps close from firing: the answer does not wait for it.
|
|
75
|
+
setTimeout(() => resolve({ ok: false, code: null, out, err }), 5_000).unref();
|
|
74
76
|
}, timeoutMs);
|
|
75
77
|
child.on('error', (e) => {
|
|
76
78
|
clearTimeout(timer);
|
package/dist/server/runs.js
CHANGED
|
@@ -127,8 +127,9 @@ function schedule(run, at) {
|
|
|
127
127
|
catch (e) {
|
|
128
128
|
// The board's own run of an item takes it out of the schedule first, so this is rare;
|
|
129
129
|
// the queue still goes on.
|
|
130
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
130
131
|
if (s)
|
|
131
|
-
trace(project, 'resume-failed', `${s.run.ids.join(', ')}: ${
|
|
132
|
+
trace(project, 'resume-failed', `${s.run.ids.join(', ')}: ${why}`);
|
|
132
133
|
advance(project);
|
|
133
134
|
}
|
|
134
135
|
broadcast({ type: 'board', project });
|
|
@@ -142,9 +143,7 @@ function schedule(run, at) {
|
|
|
142
143
|
* report.
|
|
143
144
|
*/
|
|
144
145
|
function limited(run, result) {
|
|
145
|
-
const
|
|
146
|
-
?.items;
|
|
147
|
-
const hit = (items ?? []).filter((i) => i.limit_resets !== undefined);
|
|
146
|
+
const hit = (result?.items ?? []).filter((i) => i.limit_resets !== undefined);
|
|
148
147
|
if (!hit.length)
|
|
149
148
|
return '';
|
|
150
149
|
const project = run.p.name;
|
|
@@ -204,6 +203,34 @@ export function stalled(ok, result) {
|
|
|
204
203
|
const items = result?.items ?? [];
|
|
205
204
|
return (items.length > 0 && items.every((i) => /another run works in the main/.test(i.error ?? '')));
|
|
206
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* The harness report a run printed, or null for none. A report of another shape counts as none:
|
|
208
|
+
* the close listener that reads it would throw, and take the server down with it.
|
|
209
|
+
*/
|
|
210
|
+
export function harnessReport(out) {
|
|
211
|
+
let r;
|
|
212
|
+
try {
|
|
213
|
+
r = JSON.parse(out);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// No report: the error says why.
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
if (typeof r !== 'object' || r === null || Array.isArray(r))
|
|
220
|
+
return null;
|
|
221
|
+
const { gate, items, orphans } = r;
|
|
222
|
+
const list = (v) => v === undefined || (Array.isArray(v) && v.every(isItem));
|
|
223
|
+
return (gate === undefined || typeof gate === 'string') && list(items) && list(orphans)
|
|
224
|
+
? r
|
|
225
|
+
: null;
|
|
226
|
+
}
|
|
227
|
+
// The fields read as more than text: the id, and the reset a resume is scheduled at.
|
|
228
|
+
function isItem(i) {
|
|
229
|
+
if (typeof i !== 'object' || i === null)
|
|
230
|
+
return false;
|
|
231
|
+
const { id, limit_resets: resets } = i;
|
|
232
|
+
return typeof id === 'string' && (resets == null || typeof resets === 'string');
|
|
233
|
+
}
|
|
207
234
|
function spawnRun(q, mode) {
|
|
208
235
|
const { entry, p, ids } = q;
|
|
209
236
|
const run = { project: p.name, ids, started: new Date().toISOString(), mode };
|
|
@@ -224,13 +251,7 @@ function spawnRun(q, mode) {
|
|
|
224
251
|
// A spawn that fails fires both error and close: one end only.
|
|
225
252
|
if (!active.delete(run))
|
|
226
253
|
return;
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
result = JSON.parse(out);
|
|
230
|
-
}
|
|
231
|
-
catch {
|
|
232
|
-
// No report: the error says why.
|
|
233
|
-
}
|
|
254
|
+
const result = harnessReport(out);
|
|
234
255
|
// A run of a project removed meanwhile only reports: it pauses and schedules nothing.
|
|
235
256
|
const kept = !forgotten.has(run);
|
|
236
257
|
const stop = kept && mode === 'main' && queues.get(p.name)?.length && stalled(ok, result);
|
|
@@ -244,8 +265,7 @@ function spawnRun(q, mode) {
|
|
|
244
265
|
(stop ? 'the queue is paused: fix the cause, then resume it on the board' : '');
|
|
245
266
|
if (stop)
|
|
246
267
|
trace(p.name, 'queue-paused', `after ${ids.join(', ')}: ${note}`);
|
|
247
|
-
const
|
|
248
|
-
for (const i of handed ?? [])
|
|
268
|
+
for (const i of result?.items ?? [])
|
|
249
269
|
if (kept && i.handoffs)
|
|
250
270
|
trace(p.name, 'handoff', `${i.id}: ${i.handoffs} context handoff(s) to a fresh session`);
|
|
251
271
|
const brief = err && briefError(err);
|