@serverkgg/bridge 0.0.15 → 0.0.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/package.json +1 -1
- package/src/context/bridgeContext.ts +7 -1
- package/src/core/bridgeAction.ts +2 -0
- package/src/modules/bridgeModule.ts +21 -4
- package/src/protocol/bridgeCatalog.ts +6 -0
- package/src/protocol/bridgeCrash.ts +83 -0
- package/src/protocol/bridgeFrame.ts +14 -0
- package/src/protocol/bridgeManifest.ts +1 -0
- package/src/protocol/bridgePreview.ts +1 -0
- package/src/protocol/index.ts +1 -0
- package/src/runtime/bridgeActions.ts +10 -0
- package/src/runtime/bridgeBackupLease.test.ts +148 -0
- package/src/runtime/bridgeBackupLease.ts +74 -0
- package/src/runtime/bridgeEvents.test.ts +411 -9
- package/src/runtime/bridgeEvents.ts +53 -3
- package/src/runtime/bridgeFiles.test.ts +330 -4
- package/src/runtime/bridgeFiles.ts +112 -18
- package/src/runtime/bridgeRegistry.test.ts +61 -0
- package/src/runtime/bridgeRegistry.ts +30 -2
- package/src/runtime/bridgeServer.test.ts +58 -0
- package/src/runtime/bridgeServer.ts +14 -6
- package/src/runtime/bridgeSupervisor.test.ts +289 -0
- package/src/runtime/bridgeSupervisor.ts +166 -9
- package/src/runtime/bridgeZip.ts +73 -6
- package/src/runtime/serve.ts +58 -14
- package/src/wire/panel.ts +1 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BridgeCommandOptions, BridgeReply } from "../context";
|
|
2
2
|
import { BridgeRuntime, BridgeStream } from "../core";
|
|
3
|
+
import { BRIDGE_CRASH_LINE_LIMIT, BridgeCrashPhase, type BridgeCrashRecord } from "../protocol";
|
|
3
4
|
import { stripAnsi } from "../utils";
|
|
4
5
|
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
5
6
|
import { errorText } from "./bridgeLog";
|
|
@@ -14,9 +15,11 @@ const KILL_GRACE_MS = 5000;
|
|
|
14
15
|
|
|
15
16
|
const RESTART_DELAY_MS = 2000;
|
|
16
17
|
|
|
17
|
-
const RESTART_WINDOW_MS =
|
|
18
|
+
const RESTART_WINDOW_MS = 600_000;
|
|
18
19
|
|
|
19
|
-
const RESTART_LIMIT =
|
|
20
|
+
const RESTART_LIMIT = 3;
|
|
21
|
+
|
|
22
|
+
const STABLE_UPTIME_MS = 180_000;
|
|
20
23
|
|
|
21
24
|
const GAME_OOM_SCORE_ADJ = 500;
|
|
22
25
|
|
|
@@ -35,7 +38,11 @@ export interface SupervisorOptions {
|
|
|
35
38
|
ready?: RegExp;
|
|
36
39
|
resolveCommand: () => Promise<string[]>;
|
|
37
40
|
gracefulStop?: () => Promise<void>;
|
|
41
|
+
stableAfterMs?: number;
|
|
38
42
|
onReady?: () => void;
|
|
43
|
+
onStable?: () => void;
|
|
44
|
+
onStopped?: () => void;
|
|
45
|
+
onExit?: (record: BridgeCrashRecord) => void;
|
|
39
46
|
onLog: BridgeLeveledLogger;
|
|
40
47
|
}
|
|
41
48
|
|
|
@@ -119,6 +126,15 @@ const splitLines = (buffer: string, chunk: string) => {
|
|
|
119
126
|
};
|
|
120
127
|
};
|
|
121
128
|
|
|
129
|
+
const PUMP_DRAIN_MS = 500;
|
|
130
|
+
|
|
131
|
+
const drainPumps = async (pumps: readonly Promise<unknown>[]) => {
|
|
132
|
+
await Promise.race([
|
|
133
|
+
Promise.all(pumps),
|
|
134
|
+
Bun.sleep(PUMP_DRAIN_MS),
|
|
135
|
+
]);
|
|
136
|
+
};
|
|
137
|
+
|
|
122
138
|
export const createSupervisor = (options: SupervisorOptions) => {
|
|
123
139
|
const tail: SupervisorLine[] = [];
|
|
124
140
|
const watchers = new Set<LineWatcher>();
|
|
@@ -132,8 +148,21 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
132
148
|
let exitCode: number | null = null;
|
|
133
149
|
let outOfMemory = false;
|
|
134
150
|
let restarts = 0;
|
|
151
|
+
let launchedAt = performance.now();
|
|
152
|
+
let stable = false;
|
|
153
|
+
let stableTimer: ReturnType<typeof setTimeout> | null = null;
|
|
135
154
|
let queue: Promise<unknown> = Promise.resolve();
|
|
136
155
|
|
|
156
|
+
const markStable = () => {
|
|
157
|
+
if (stable) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
stable = true;
|
|
162
|
+
|
|
163
|
+
options.onStable?.();
|
|
164
|
+
};
|
|
165
|
+
|
|
137
166
|
const emit = (entry: SupervisorLine) => {
|
|
138
167
|
if (entry.stream === BridgeStream.Stderr) {
|
|
139
168
|
process.stderr.write(`${entry.line}\n`);
|
|
@@ -223,6 +252,18 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
223
252
|
return restartStamps.length >= RESTART_LIMIT;
|
|
224
253
|
};
|
|
225
254
|
|
|
255
|
+
const lastLine = () => {
|
|
256
|
+
for (let index = tail.length - 1; index >= 0; index -= 1) {
|
|
257
|
+
const line = stripAnsi(tail.at(index)?.line ?? "").trim();
|
|
258
|
+
|
|
259
|
+
if (line.length > 0) {
|
|
260
|
+
return line.slice(0, BRIDGE_CRASH_LINE_LIMIT);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return null;
|
|
265
|
+
};
|
|
266
|
+
|
|
226
267
|
const launch = async () => {
|
|
227
268
|
const command = await options.resolveCommand();
|
|
228
269
|
const [executable, ...rest] = command;
|
|
@@ -237,6 +278,34 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
237
278
|
];
|
|
238
279
|
|
|
239
280
|
const { argv: spawnArgv, credentials } = await unprivilegedArgv(argv, options.uid, options.gid);
|
|
281
|
+
|
|
282
|
+
launchedAt = performance.now();
|
|
283
|
+
stable = false;
|
|
284
|
+
|
|
285
|
+
if (stableTimer) {
|
|
286
|
+
clearTimeout(stableTimer);
|
|
287
|
+
|
|
288
|
+
stableTimer = null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let accepted = false;
|
|
292
|
+
let reported = false;
|
|
293
|
+
let pumps: Promise<unknown>[] = [];
|
|
294
|
+
|
|
295
|
+
const finish = (code: number) => {
|
|
296
|
+
if (!accepted || reported) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
reported = true;
|
|
301
|
+
|
|
302
|
+
void handleExit(code, pumps).catch((error: unknown) => {
|
|
303
|
+
options.onLog.error("the game exit was not reported", {
|
|
304
|
+
error: errorText(error),
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
|
|
240
309
|
const spawned = Bun.spawn(spawnArgv, {
|
|
241
310
|
...credentials,
|
|
242
311
|
cwd: options.cwd,
|
|
@@ -245,7 +314,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
245
314
|
stdin: "pipe",
|
|
246
315
|
stdout: "pipe",
|
|
247
316
|
onExit(_subprocess, code, signal) {
|
|
248
|
-
|
|
317
|
+
finish(exitCodeOf(code, signal));
|
|
249
318
|
},
|
|
250
319
|
});
|
|
251
320
|
|
|
@@ -281,8 +350,22 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
281
350
|
childGroup = group;
|
|
282
351
|
exitCode = null;
|
|
283
352
|
|
|
284
|
-
|
|
285
|
-
|
|
353
|
+
const launchStamp = launchedAt;
|
|
354
|
+
|
|
355
|
+
stableTimer = setTimeout(() => {
|
|
356
|
+
if (launchStamp !== launchedAt || child === null) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
markStable();
|
|
361
|
+
}, options.stableAfterMs ?? STABLE_UPTIME_MS);
|
|
362
|
+
|
|
363
|
+
pumps = [
|
|
364
|
+
pump(spawned.stdout, BridgeStream.Stdout).catch(() => undefined),
|
|
365
|
+
pump(spawned.stderr, BridgeStream.Stderr).catch(() => undefined),
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
accepted = true;
|
|
286
369
|
|
|
287
370
|
options.onLog("game started", {
|
|
288
371
|
pid: spawned.pid,
|
|
@@ -290,6 +373,12 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
290
373
|
uid: options.uid,
|
|
291
374
|
});
|
|
292
375
|
|
|
376
|
+
if (spawned.exitCode !== null || spawned.signalCode !== null) {
|
|
377
|
+
finish(exitCodeOf(spawned.exitCode, spawned.signalCode));
|
|
378
|
+
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
293
382
|
if (!options.ready) {
|
|
294
383
|
runtime = BridgeRuntime.Running;
|
|
295
384
|
|
|
@@ -299,12 +388,30 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
299
388
|
runtime = BridgeRuntime.Starting;
|
|
300
389
|
|
|
301
390
|
void watch(options.ready, options.stopTimeoutSeconds * 1000 * 10).then((match) => {
|
|
302
|
-
if (
|
|
391
|
+
if (launchStamp !== launchedAt || child === null) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (match) {
|
|
396
|
+
if (runtime === BridgeRuntime.Starting) {
|
|
397
|
+
runtime = BridgeRuntime.Running;
|
|
398
|
+
|
|
399
|
+
options.onLog("game ready");
|
|
400
|
+
options.onReady?.();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
markStable();
|
|
404
|
+
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (runtime === BridgeRuntime.Starting) {
|
|
303
409
|
runtime = BridgeRuntime.Running;
|
|
304
410
|
|
|
305
|
-
options.onLog("game ready");
|
|
306
|
-
options.onReady?.();
|
|
411
|
+
options.onLog.warn("game ready line not seen, treating it as running");
|
|
307
412
|
}
|
|
413
|
+
|
|
414
|
+
markStable();
|
|
308
415
|
});
|
|
309
416
|
};
|
|
310
417
|
|
|
@@ -326,12 +433,18 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
326
433
|
}
|
|
327
434
|
};
|
|
328
435
|
|
|
329
|
-
function handleExit(code: number) {
|
|
436
|
+
async function handleExit(code: number, pumps: readonly Promise<unknown>[]) {
|
|
330
437
|
const finished = child;
|
|
331
438
|
|
|
332
439
|
child = null;
|
|
333
440
|
exitCode = code;
|
|
334
441
|
|
|
442
|
+
if (stableTimer) {
|
|
443
|
+
clearTimeout(stableTimer);
|
|
444
|
+
|
|
445
|
+
stableTimer = null;
|
|
446
|
+
}
|
|
447
|
+
|
|
335
448
|
if (finished) {
|
|
336
449
|
sweep(finished.pid, childGroup);
|
|
337
450
|
}
|
|
@@ -350,6 +463,9 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
350
463
|
return;
|
|
351
464
|
}
|
|
352
465
|
|
|
466
|
+
const uptimeMs = performance.now() - launchedAt;
|
|
467
|
+
const phase = stable ? BridgeCrashPhase.Running : BridgeCrashPhase.Booting;
|
|
468
|
+
|
|
353
469
|
outOfMemory = code === SIGKILL_EXIT_CODE;
|
|
354
470
|
|
|
355
471
|
if (outOfMemory) {
|
|
@@ -358,6 +474,38 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
358
474
|
});
|
|
359
475
|
}
|
|
360
476
|
|
|
477
|
+
const report = async (restarting: boolean, looping: boolean) => {
|
|
478
|
+
const settled = {
|
|
479
|
+
exitCode: code,
|
|
480
|
+
outOfMemory,
|
|
481
|
+
phase,
|
|
482
|
+
uptimeSeconds: Math.floor(uptimeMs / 1000),
|
|
483
|
+
restarting,
|
|
484
|
+
looping,
|
|
485
|
+
restarts,
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
await drainPumps(pumps);
|
|
489
|
+
|
|
490
|
+
options.onExit?.({
|
|
491
|
+
...settled,
|
|
492
|
+
lastLine: lastLine(),
|
|
493
|
+
});
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
if (phase === BridgeCrashPhase.Booting) {
|
|
497
|
+
runtime = BridgeRuntime.Crashed;
|
|
498
|
+
|
|
499
|
+
options.onLog.error("game exited while booting, not restarting", {
|
|
500
|
+
code,
|
|
501
|
+
phase,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
await report(false, false);
|
|
505
|
+
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
361
509
|
restarts += 1;
|
|
362
510
|
restartStamps.push(performance.now());
|
|
363
511
|
|
|
@@ -370,6 +518,8 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
370
518
|
restarts,
|
|
371
519
|
});
|
|
372
520
|
|
|
521
|
+
await report(false, true);
|
|
522
|
+
|
|
373
523
|
return;
|
|
374
524
|
}
|
|
375
525
|
|
|
@@ -380,6 +530,8 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
380
530
|
restarts,
|
|
381
531
|
});
|
|
382
532
|
|
|
533
|
+
await report(true, false);
|
|
534
|
+
|
|
383
535
|
void restart();
|
|
384
536
|
}
|
|
385
537
|
|
|
@@ -429,6 +581,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
429
581
|
shouldRun = true;
|
|
430
582
|
breaker = false;
|
|
431
583
|
outOfMemory = false;
|
|
584
|
+
restarts = 0;
|
|
432
585
|
restartStamps.length = 0;
|
|
433
586
|
|
|
434
587
|
if (child) {
|
|
@@ -444,6 +597,8 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
444
597
|
if (!child) {
|
|
445
598
|
runtime = BridgeRuntime.Stopped;
|
|
446
599
|
|
|
600
|
+
options.onStopped?.();
|
|
601
|
+
|
|
447
602
|
return;
|
|
448
603
|
}
|
|
449
604
|
|
|
@@ -488,6 +643,8 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
488
643
|
sweep(active.pid, group);
|
|
489
644
|
|
|
490
645
|
runtime = BridgeRuntime.Stopped;
|
|
646
|
+
|
|
647
|
+
options.onStopped?.();
|
|
491
648
|
},
|
|
492
649
|
|
|
493
650
|
async command(input: string, commandOptions: BridgeCommandOptions = {}): Promise<BridgeReply> {
|
package/src/runtime/bridgeZip.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { inflateRawSync } from "node:zlib";
|
|
1
|
+
import { crc32, inflateRawSync } from "node:zlib";
|
|
2
2
|
|
|
3
3
|
const EOCD_SIGNATURE = 0x06_05_4b_50;
|
|
4
4
|
|
|
@@ -32,11 +32,14 @@ const DEFLATED = 8;
|
|
|
32
32
|
|
|
33
33
|
const ENCRYPTED_FLAG = 0x1;
|
|
34
34
|
|
|
35
|
+
const DATA_DESCRIPTOR_FLAG = 0x8;
|
|
36
|
+
|
|
35
37
|
export const ZIP_ENTRY_LIMIT = 2048;
|
|
36
38
|
|
|
37
39
|
export const ZIP_TREE_ENTRY_LIMIT = 16_384;
|
|
38
40
|
|
|
39
41
|
const MAX_ENTRY_BYTES = 128 * 1024 * 1024;
|
|
42
|
+
const MAX_CONFIGURED_ENTRY_BYTES = 512 * 1024 * 1024;
|
|
40
43
|
|
|
41
44
|
const UNIX_MODE_FORMAT_MASK = 0xf0_00;
|
|
42
45
|
|
|
@@ -48,6 +51,8 @@ export interface ZipEntry {
|
|
|
48
51
|
sizeBytes: number;
|
|
49
52
|
compressedSize: number;
|
|
50
53
|
method: number;
|
|
54
|
+
flags: number;
|
|
55
|
+
checksum: number;
|
|
51
56
|
localOffset: number;
|
|
52
57
|
}
|
|
53
58
|
|
|
@@ -155,7 +160,14 @@ const readZip64Extra = (extra: Buffer, sizeBytes: number, compressedSize: number
|
|
|
155
160
|
return extended;
|
|
156
161
|
};
|
|
157
162
|
|
|
158
|
-
export const openZip = async (
|
|
163
|
+
export const openZip = async (
|
|
164
|
+
path: string,
|
|
165
|
+
maxEntries: number = ZIP_ENTRY_LIMIT,
|
|
166
|
+
maxEntryBytes: number = MAX_ENTRY_BYTES,
|
|
167
|
+
): Promise<ZipArchive> => {
|
|
168
|
+
if (!Number.isSafeInteger(maxEntryBytes) || maxEntryBytes <= 0 || maxEntryBytes > MAX_CONFIGURED_ENTRY_BYTES) {
|
|
169
|
+
throw new Error("archive entry byte limit is invalid");
|
|
170
|
+
}
|
|
159
171
|
const size = Bun.file(path).size;
|
|
160
172
|
const tailStart = Math.max(0, size - (EOCD_SEARCH_LIMIT + ZIP64_LOCATOR_LENGTH));
|
|
161
173
|
const tail = await readRange(path, tailStart, size);
|
|
@@ -174,6 +186,9 @@ export const openZip = async (path: string, maxEntries: number = ZIP_ENTRY_LIMIT
|
|
|
174
186
|
if (directory.total > maxEntries) {
|
|
175
187
|
throw new Error(`archive has too many entries (${directory.total})`);
|
|
176
188
|
}
|
|
189
|
+
if (directory.sizeBytes > 16 * 1024 * 1024 || directory.offset + directory.sizeBytes > size) {
|
|
190
|
+
throw new Error("archive central directory exceeds its bounds");
|
|
191
|
+
}
|
|
177
192
|
|
|
178
193
|
const central = await readRange(path, directory.offset, directory.offset + directory.sizeBytes);
|
|
179
194
|
const entries: ZipEntry[] = [];
|
|
@@ -187,6 +202,7 @@ export const openZip = async (path: string, maxEntries: number = ZIP_ENTRY_LIMIT
|
|
|
187
202
|
|
|
188
203
|
const flags = central.readUInt16LE(cursor + 8);
|
|
189
204
|
const method = central.readUInt16LE(cursor + 10);
|
|
205
|
+
const checksum = central.readUInt32LE(cursor + 16);
|
|
190
206
|
const compressedSize = central.readUInt32LE(cursor + 20);
|
|
191
207
|
const sizeBytes = central.readUInt32LE(cursor + 24);
|
|
192
208
|
const nameLength = central.readUInt16LE(cursor + 28);
|
|
@@ -200,8 +216,14 @@ export const openZip = async (path: string, maxEntries: number = ZIP_ENTRY_LIMIT
|
|
|
200
216
|
const extended = readZip64Extra(extra, sizeBytes, compressedSize, localOffset);
|
|
201
217
|
|
|
202
218
|
cursor = nameStart + nameLength + extraLength + commentLength;
|
|
219
|
+
if (cursor > central.length) {
|
|
220
|
+
throw new Error("archive central entry exceeds its directory");
|
|
221
|
+
}
|
|
203
222
|
|
|
204
|
-
if (
|
|
223
|
+
if ((flags & ENCRYPTED_FLAG) !== 0) {
|
|
224
|
+
throw new Error("encrypted archive entries are not supported");
|
|
225
|
+
}
|
|
226
|
+
if (name.endsWith("/")) {
|
|
205
227
|
continue;
|
|
206
228
|
}
|
|
207
229
|
|
|
@@ -211,6 +233,8 @@ export const openZip = async (path: string, maxEntries: number = ZIP_ENTRY_LIMIT
|
|
|
211
233
|
sizeBytes: extended.sizeBytes ?? sizeBytes,
|
|
212
234
|
compressedSize: extended.compressedSize ?? compressedSize,
|
|
213
235
|
method,
|
|
236
|
+
flags,
|
|
237
|
+
checksum,
|
|
214
238
|
localOffset: extended.localOffset ?? localOffset,
|
|
215
239
|
});
|
|
216
240
|
}
|
|
@@ -219,20 +243,59 @@ export const openZip = async (path: string, maxEntries: number = ZIP_ENTRY_LIMIT
|
|
|
219
243
|
entries,
|
|
220
244
|
|
|
221
245
|
async read(entry) {
|
|
222
|
-
if (entry.sizeBytes >
|
|
246
|
+
if (entry.sizeBytes > maxEntryBytes || entry.compressedSize > maxEntryBytes) {
|
|
223
247
|
throw new Error(`archive entry "${entry.name}" is too large`);
|
|
224
248
|
}
|
|
225
249
|
|
|
250
|
+
if (entry.localOffset + LOCAL_HEADER_LENGTH > directory.offset) {
|
|
251
|
+
throw new Error(`archive entry "${entry.name}" has an out-of-bounds local header`);
|
|
252
|
+
}
|
|
253
|
+
|
|
226
254
|
const header = await readRange(path, entry.localOffset, entry.localOffset + LOCAL_HEADER_LENGTH);
|
|
227
255
|
|
|
228
256
|
if (header.length < LOCAL_HEADER_LENGTH || header.readUInt32LE(0) !== LOCAL_SIGNATURE) {
|
|
229
257
|
throw new Error(`archive entry "${entry.name}" has a corrupt local header`);
|
|
230
258
|
}
|
|
231
259
|
|
|
232
|
-
|
|
260
|
+
if (header.readUInt16LE(8) !== entry.method || header.readUInt16LE(6) !== entry.flags) {
|
|
261
|
+
throw new Error(`archive entry "${entry.name}" has inconsistent local compression flags or method`);
|
|
262
|
+
}
|
|
263
|
+
if ((entry.flags & DATA_DESCRIPTOR_FLAG) === 0 && header.readUInt32LE(14) !== entry.checksum) {
|
|
264
|
+
throw new Error(`archive entry "${entry.name}" has inconsistent local checksum`);
|
|
265
|
+
}
|
|
266
|
+
if ((entry.flags & DATA_DESCRIPTOR_FLAG) === 0) {
|
|
267
|
+
const localCompressed = header.readUInt32LE(18);
|
|
268
|
+
const localSize = header.readUInt32LE(22);
|
|
269
|
+
if (
|
|
270
|
+
(localCompressed !== SIZE_SENTINEL && localCompressed !== entry.compressedSize)
|
|
271
|
+
|| (localSize !== SIZE_SENTINEL && localSize !== entry.sizeBytes)
|
|
272
|
+
) {
|
|
273
|
+
throw new Error(`archive entry "${entry.name}" has inconsistent local sizes`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const nameOffset = entry.localOffset + LOCAL_HEADER_LENGTH;
|
|
278
|
+
const nameLength = header.readUInt16LE(26);
|
|
279
|
+
const dataOffset = nameOffset + nameLength + header.readUInt16LE(28);
|
|
280
|
+
if (dataOffset > directory.offset || entry.compressedSize > directory.offset - dataOffset) {
|
|
281
|
+
throw new Error(`archive entry "${entry.name}" data exceeds its local bounds`);
|
|
282
|
+
}
|
|
283
|
+
const name = await readRange(path, nameOffset, nameOffset + nameLength);
|
|
284
|
+
if (name.toString("utf8") !== entry.name) {
|
|
285
|
+
throw new Error(`archive entry "${entry.name}" has an inconsistent local name`);
|
|
286
|
+
}
|
|
233
287
|
const compressed = await readRange(path, dataOffset, dataOffset + entry.compressedSize);
|
|
288
|
+
if (compressed.length !== entry.compressedSize) {
|
|
289
|
+
throw new Error(`archive entry "${entry.name}" has truncated compressed data`);
|
|
290
|
+
}
|
|
234
291
|
|
|
235
292
|
if (entry.method === STORED) {
|
|
293
|
+
if (compressed.length !== entry.sizeBytes || compressed.length !== entry.compressedSize) {
|
|
294
|
+
throw new Error(`archive entry "${entry.name}" does not match its declared size`);
|
|
295
|
+
}
|
|
296
|
+
if (crc32(compressed) !== entry.checksum) {
|
|
297
|
+
throw new Error(`archive entry "${entry.name}" failed its CRC32 checksum`);
|
|
298
|
+
}
|
|
236
299
|
return compressed;
|
|
237
300
|
}
|
|
238
301
|
|
|
@@ -241,13 +304,17 @@ export const openZip = async (path: string, maxEntries: number = ZIP_ENTRY_LIMIT
|
|
|
241
304
|
}
|
|
242
305
|
|
|
243
306
|
const data = inflateRawSync(compressed, {
|
|
244
|
-
maxOutputLength:
|
|
307
|
+
maxOutputLength: maxEntryBytes,
|
|
245
308
|
});
|
|
246
309
|
|
|
247
310
|
if (data.length !== entry.sizeBytes) {
|
|
248
311
|
throw new Error(`archive entry "${entry.name}" did not inflate to its declared size`);
|
|
249
312
|
}
|
|
250
313
|
|
|
314
|
+
if (crc32(data) !== entry.checksum) {
|
|
315
|
+
throw new Error(`archive entry "${entry.name}" failed its CRC32 checksum`);
|
|
316
|
+
}
|
|
317
|
+
|
|
251
318
|
return data;
|
|
252
319
|
},
|
|
253
320
|
};
|
package/src/runtime/serve.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type BridgeSampleResult,
|
|
10
10
|
type BridgeStatus,
|
|
11
11
|
} from "../protocol";
|
|
12
|
+
import { createBackupLease } from "./bridgeBackupLease";
|
|
12
13
|
import { configPath, readConfig, socketPath, statePath } from "./bridgeConfigReader";
|
|
13
14
|
import { createConsoleHandler } from "./bridgeConsoleHandler";
|
|
14
15
|
import { createContext } from "./bridgeContext";
|
|
@@ -96,6 +97,15 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
96
97
|
});
|
|
97
98
|
});
|
|
98
99
|
},
|
|
100
|
+
onStable() {
|
|
101
|
+
ring.discard();
|
|
102
|
+
},
|
|
103
|
+
onStopped() {
|
|
104
|
+
ring.discard();
|
|
105
|
+
},
|
|
106
|
+
onExit(record) {
|
|
107
|
+
ring.crash(record);
|
|
108
|
+
},
|
|
99
109
|
});
|
|
100
110
|
|
|
101
111
|
const ring = createEventRing(driver.events, supervisor);
|
|
@@ -111,6 +121,18 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
111
121
|
});
|
|
112
122
|
|
|
113
123
|
const registry = createRegistry(driver, context, supervisor, ring);
|
|
124
|
+
const backupLease = createBackupLease(async () => {
|
|
125
|
+
if (supervisor.running) {
|
|
126
|
+
await driver.backup?.release?.(context);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
setInterval(() => {
|
|
130
|
+
void backupLease.expire().catch((error: unknown) => {
|
|
131
|
+
log.error("could not resume saves after the backup lease expired", {
|
|
132
|
+
error: errorText(error),
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}, 10_000).unref();
|
|
114
136
|
|
|
115
137
|
const runConsole = createConsoleHandler({
|
|
116
138
|
driver,
|
|
@@ -130,6 +152,7 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
130
152
|
exitCode: supervisor.exitCode,
|
|
131
153
|
outOfMemory: supervisor.outOfMemory,
|
|
132
154
|
restarts: supervisor.restarts,
|
|
155
|
+
crashRecords: true,
|
|
133
156
|
installError,
|
|
134
157
|
installErrorText,
|
|
135
158
|
installHold,
|
|
@@ -219,8 +242,8 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
219
242
|
timeoutMs,
|
|
220
243
|
});
|
|
221
244
|
},
|
|
222
|
-
async onInvoke(module, action, args) {
|
|
223
|
-
return await registry.invoke(module, action, args);
|
|
245
|
+
async onInvoke(module, action, args, recoveryOperationId) {
|
|
246
|
+
return await registry.invoke(module, action, args, recoveryOperationId);
|
|
224
247
|
},
|
|
225
248
|
onEvents(cursor) {
|
|
226
249
|
return ring.drain(cursor);
|
|
@@ -228,7 +251,18 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
228
251
|
async onSample() {
|
|
229
252
|
return await sample();
|
|
230
253
|
},
|
|
231
|
-
async onQuiesce() {
|
|
254
|
+
async onQuiesce(supportsSnapshot, leaseId) {
|
|
255
|
+
if (leaseId) {
|
|
256
|
+
return {
|
|
257
|
+
supported: true,
|
|
258
|
+
quiesced: true,
|
|
259
|
+
settleSeconds: 0,
|
|
260
|
+
leaseId: backupLease.renew(leaseId),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (backupLease.active) {
|
|
264
|
+
throw new Error("a backup already owns the save hold");
|
|
265
|
+
}
|
|
232
266
|
const backup = driver.backup;
|
|
233
267
|
|
|
234
268
|
if (!backup?.quiesce) {
|
|
@@ -247,19 +281,29 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
247
281
|
};
|
|
248
282
|
}
|
|
249
283
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
284
|
+
const quiesce = backup.quiesce;
|
|
285
|
+
const owner = backupLease.begin(!supportsSnapshot);
|
|
286
|
+
try {
|
|
287
|
+
const prepared = await backupLease.prepare(owner, () => quiesce(context));
|
|
288
|
+
if (prepared?.snapshot && !supportsSnapshot) {
|
|
289
|
+
throw new Error("this backup requires an agent that supports bounded snapshots");
|
|
290
|
+
}
|
|
291
|
+
backupLease.renew(owner);
|
|
292
|
+
return {
|
|
293
|
+
supported: true,
|
|
294
|
+
quiesced: true,
|
|
295
|
+
settleSeconds: backup.settleSeconds ?? 0,
|
|
296
|
+
snapshot: prepared?.snapshot,
|
|
297
|
+
leaseId: supportsSnapshot ? owner : undefined,
|
|
298
|
+
};
|
|
299
|
+
} catch (error) {
|
|
300
|
+
await backupLease.finish(owner);
|
|
301
|
+
throw error;
|
|
261
302
|
}
|
|
262
303
|
},
|
|
304
|
+
async onRelease(leaseId) {
|
|
305
|
+
await backupLease.finish(leaseId);
|
|
306
|
+
},
|
|
263
307
|
async onDescribe() {
|
|
264
308
|
const describe = driver.install.describe;
|
|
265
309
|
|
package/src/wire/panel.ts
CHANGED
|
@@ -337,6 +337,7 @@ export const bridgeModuleManifestSchema = z.object({
|
|
|
337
337
|
pageSize: z.int().positive().nullable().default(null),
|
|
338
338
|
dependsOn: z.array(zKey).max(8),
|
|
339
339
|
actions: z.array(z.string().min(1).max(64)).max(16),
|
|
340
|
+
protectedActions: z.array(z.string().min(1).max(64)).max(16).optional(),
|
|
340
341
|
});
|
|
341
342
|
|
|
342
343
|
export const bridgeManifestSchema: z.ZodType<BridgeManifest> = z.object({
|