@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverkgg/bridge",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
4
4
  "description": "Serverk game bridge and driver runtime, wire protocol, manifest schema, and maintainer CLI",
5
5
  "homepage": "https://serverk.gg",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -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
 
@@ -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);