@serverkgg/bridge 0.0.20 → 0.0.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/package.json
CHANGED
|
@@ -23,6 +23,7 @@ export interface BridgeInstallModule {
|
|
|
23
23
|
export interface BridgeLifecycleModule {
|
|
24
24
|
kind: BridgeKind.Lifecycle;
|
|
25
25
|
ready?: RegExp;
|
|
26
|
+
failed?: RegExp;
|
|
26
27
|
stopTimeoutSeconds?: number;
|
|
27
28
|
command(context: BridgeContext): Promise<string[]>;
|
|
28
29
|
stop?(context: BridgeContext): Promise<void>;
|
|
@@ -32,6 +32,14 @@ const TIGHT_LAST_LINE_CRASH = `sleep 0.2; echo "${LAST_LINE}"; exit 3`;
|
|
|
32
32
|
|
|
33
33
|
const INSTANT_EXIT = "false";
|
|
34
34
|
|
|
35
|
+
const FAILED_BOOT_LINE = "[main/FATAL] [minecraft/Main]: Failed to start the minecraft server";
|
|
36
|
+
|
|
37
|
+
const FAILED_BOOT = /Failed to start the minecraft server/;
|
|
38
|
+
|
|
39
|
+
const FAILED_BOOT_WRAPPER = `sleep 300 & echo child=$!; echo "${FAILED_BOOT_LINE}"; wait`;
|
|
40
|
+
|
|
41
|
+
const LATE_FAILED_LINE_WRAPPER = `echo ready; sleep 0.6; echo "${FAILED_BOOT_LINE}"; sleep 300`;
|
|
42
|
+
|
|
35
43
|
const silent: BridgeLeveledLogger = Object.assign(() => {}, {
|
|
36
44
|
warn: () => {},
|
|
37
45
|
error: () => {},
|
|
@@ -519,4 +527,85 @@ describe("createSupervisor", () => {
|
|
|
519
527
|
|
|
520
528
|
expect(await waitGone(pids.at(0) ?? 0, 5000)).toBe(true);
|
|
521
529
|
}, 30_000);
|
|
530
|
+
|
|
531
|
+
test("stops a game that reports a failed boot instead of waiting for a ready line that never comes", async () => {
|
|
532
|
+
const records: BridgeCrashRecord[] = [];
|
|
533
|
+
const pids: number[] = [];
|
|
534
|
+
|
|
535
|
+
const supervisor = createSupervisor({
|
|
536
|
+
cwd: tmpdir(),
|
|
537
|
+
uid: process.getuid?.() ?? 0,
|
|
538
|
+
gid: process.getgid?.() ?? 0,
|
|
539
|
+
stopTimeoutSeconds: 1,
|
|
540
|
+
ready: READY,
|
|
541
|
+
failed: FAILED_BOOT,
|
|
542
|
+
onLog: silent,
|
|
543
|
+
resolveCommand: async () => {
|
|
544
|
+
return [
|
|
545
|
+
"bash",
|
|
546
|
+
"-c",
|
|
547
|
+
FAILED_BOOT_WRAPPER,
|
|
548
|
+
];
|
|
549
|
+
},
|
|
550
|
+
onExit(record) {
|
|
551
|
+
records.push(record);
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
supervisor.subscribe((entry) => {
|
|
556
|
+
const match = CHILD_PATTERN.exec(entry.line);
|
|
557
|
+
|
|
558
|
+
if (match?.groups?.pid) {
|
|
559
|
+
pids.push(Number(match.groups.pid));
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
await supervisor.start();
|
|
564
|
+
|
|
565
|
+
expect(await waitFor(() => records.length > 0, 10_000)).toBe(true);
|
|
566
|
+
|
|
567
|
+
const record = records.at(0);
|
|
568
|
+
|
|
569
|
+
expect(record?.phase).toBe(BridgeCrashPhase.Booting);
|
|
570
|
+
expect(record?.outOfMemory).toBe(false);
|
|
571
|
+
expect(record?.restarting).toBe(false);
|
|
572
|
+
expect(record?.lastLine).toBe(FAILED_BOOT_LINE);
|
|
573
|
+
expect(supervisor.running).toBe(false);
|
|
574
|
+
expect(supervisor.runtime).toBe(BridgeRuntime.Crashed);
|
|
575
|
+
expect(await waitGone(pids.at(0) ?? 0, 5000)).toBe(true);
|
|
576
|
+
}, 30_000);
|
|
577
|
+
|
|
578
|
+
test("ignores the failed pattern once the game has been running long enough to count as stable", async () => {
|
|
579
|
+
const records: BridgeCrashRecord[] = [];
|
|
580
|
+
|
|
581
|
+
const supervisor = createSupervisor({
|
|
582
|
+
cwd: tmpdir(),
|
|
583
|
+
uid: process.getuid?.() ?? 0,
|
|
584
|
+
gid: process.getgid?.() ?? 0,
|
|
585
|
+
stopTimeoutSeconds: 1,
|
|
586
|
+
ready: READY,
|
|
587
|
+
failed: FAILED_BOOT,
|
|
588
|
+
stableAfterMs: 200,
|
|
589
|
+
onLog: silent,
|
|
590
|
+
resolveCommand: async () => {
|
|
591
|
+
return [
|
|
592
|
+
"bash",
|
|
593
|
+
"-c",
|
|
594
|
+
LATE_FAILED_LINE_WRAPPER,
|
|
595
|
+
];
|
|
596
|
+
},
|
|
597
|
+
onExit(record) {
|
|
598
|
+
records.push(record);
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
await supervisor.start();
|
|
603
|
+
|
|
604
|
+
await Bun.sleep(1500);
|
|
605
|
+
|
|
606
|
+
expect(records).toHaveLength(0);
|
|
607
|
+
expect(supervisor.running).toBe(true);
|
|
608
|
+
|
|
609
|
+
await supervisor.stop();
|
|
610
|
+
}, 30_000);
|
|
522
611
|
});
|
|
@@ -36,6 +36,7 @@ export interface SupervisorOptions {
|
|
|
36
36
|
gid: number;
|
|
37
37
|
stopTimeoutSeconds: number;
|
|
38
38
|
ready?: RegExp;
|
|
39
|
+
failed?: RegExp;
|
|
39
40
|
resolveCommand: () => Promise<string[]>;
|
|
40
41
|
gracefulStop?: () => Promise<void>;
|
|
41
42
|
stableAfterMs?: number;
|
|
@@ -166,6 +167,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
166
167
|
let launchedAt = performance.now();
|
|
167
168
|
let stable = false;
|
|
168
169
|
let stableTimer: ReturnType<typeof setTimeout> | null = null;
|
|
170
|
+
let bootFailure: string | null = null;
|
|
169
171
|
|
|
170
172
|
const serialize = chain();
|
|
171
173
|
const transition = chain();
|
|
@@ -298,6 +300,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
298
300
|
|
|
299
301
|
launchedAt = performance.now();
|
|
300
302
|
stable = false;
|
|
303
|
+
bootFailure = null;
|
|
301
304
|
|
|
302
305
|
if (stableTimer) {
|
|
303
306
|
clearTimeout(stableTimer);
|
|
@@ -396,6 +399,31 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
396
399
|
return;
|
|
397
400
|
}
|
|
398
401
|
|
|
402
|
+
if (options.failed) {
|
|
403
|
+
void watch(options.failed, options.stopTimeoutSeconds * 1000 * 10).then((match) => {
|
|
404
|
+
if (!match || launchStamp !== launchedAt || child === null || stable) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
bootFailure = (match.input ?? match[0]).trim().slice(0, BRIDGE_CRASH_LINE_LIMIT);
|
|
409
|
+
|
|
410
|
+
options.onLog.error("the game reported a failed boot, stopping it", {
|
|
411
|
+
line: bootFailure,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const failing = child;
|
|
415
|
+
const failingGroup = childGroup;
|
|
416
|
+
|
|
417
|
+
terminate(failing, failingGroup, "SIGTERM");
|
|
418
|
+
|
|
419
|
+
void Bun.sleep(KILL_GRACE_MS).then(() => {
|
|
420
|
+
if (child === failing) {
|
|
421
|
+
terminate(failing, failingGroup, "SIGKILL");
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
399
427
|
if (!options.ready) {
|
|
400
428
|
runtime = BridgeRuntime.Running;
|
|
401
429
|
|
|
@@ -489,7 +517,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
489
517
|
const uptimeMs = performance.now() - launchedAt;
|
|
490
518
|
const phase = stable ? BridgeCrashPhase.Running : BridgeCrashPhase.Booting;
|
|
491
519
|
|
|
492
|
-
outOfMemory = code === SIGKILL_EXIT_CODE;
|
|
520
|
+
outOfMemory = code === SIGKILL_EXIT_CODE && bootFailure === null;
|
|
493
521
|
|
|
494
522
|
if (outOfMemory) {
|
|
495
523
|
options.onLog.error("the game was killed for running out of memory", {
|
|
@@ -512,7 +540,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
512
540
|
|
|
513
541
|
options.onExit?.({
|
|
514
542
|
...settled,
|
|
515
|
-
lastLine: lastLine(),
|
|
543
|
+
lastLine: bootFailure ?? lastLine(),
|
|
516
544
|
});
|
|
517
545
|
};
|
|
518
546
|
|
package/src/runtime/serve.ts
CHANGED
|
@@ -83,6 +83,7 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
83
83
|
gid: config.gameGid,
|
|
84
84
|
stopTimeoutSeconds: driver.lifecycle.stopTimeoutSeconds ?? DEFAULT_STOP_TIMEOUT_SECONDS,
|
|
85
85
|
ready: driver.lifecycle.ready,
|
|
86
|
+
failed: driver.lifecycle.failed,
|
|
86
87
|
onLog: log,
|
|
87
88
|
async resolveCommand() {
|
|
88
89
|
return await driver.lifecycle.command(context);
|