@serverkgg/bridge 0.0.20 → 0.1.1

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.
Files changed (86) hide show
  1. package/package.json +3 -1
  2. package/src/bot/botDeploy.test.ts +363 -0
  3. package/src/bot/botDeploy.ts +136 -0
  4. package/src/bot/botEnvironment.test.ts +189 -0
  5. package/src/bot/botEnvironment.ts +141 -0
  6. package/src/bot/botIdentity.test.ts +251 -0
  7. package/src/bot/botIdentity.ts +140 -0
  8. package/src/bot/botPanel.test.ts +216 -0
  9. package/src/bot/botPanel.ts +121 -0
  10. package/src/bot/botState.test.ts +45 -0
  11. package/src/bot/botState.ts +54 -0
  12. package/src/bot/botTerminal.test.ts +79 -0
  13. package/src/bot/botTerminal.ts +62 -0
  14. package/src/bot/botVariables.test.ts +193 -0
  15. package/src/bot/botVariables.ts +73 -0
  16. package/src/bot/detect/botFramework.ts +23 -0
  17. package/src/bot/detect/botFrameworks.ts +198 -0
  18. package/src/bot/detect/botInspectWarning.ts +15 -0
  19. package/src/bot/detect/botPackageManager.ts +6 -0
  20. package/src/bot/detect/botPlatform.ts +10 -0
  21. package/src/bot/detect/botRuntime.ts +15 -0
  22. package/src/bot/detect/botToken.test.ts +71 -0
  23. package/src/bot/detect/botToken.ts +126 -0
  24. package/src/bot/detect/detect.test.ts +543 -0
  25. package/src/bot/detect/detect.ts +842 -0
  26. package/src/bot/detect/detectResult.ts +40 -0
  27. package/src/bot/detect/detectTable.test.ts +72 -0
  28. package/src/bot/detect/detectTable.ts +166 -0
  29. package/src/bot/detect/envExample.test.ts +29 -0
  30. package/src/bot/detect/envExample.ts +48 -0
  31. package/src/bot/detect/envKey.ts +7 -0
  32. package/src/bot/detect/envScan.test.ts +39 -0
  33. package/src/bot/detect/envScan.ts +87 -0
  34. package/src/bot/detect/index.ts +17 -0
  35. package/src/bot/detect/inferBotPlatform.test.ts +60 -0
  36. package/src/bot/detect/inferBotPlatform.ts +28 -0
  37. package/src/bot/detect/keys.test.ts +28 -0
  38. package/src/bot/detect/keys.ts +64 -0
  39. package/src/bot/detect/sensitiveKey.test.ts +91 -0
  40. package/src/bot/detect/sensitiveKey.ts +44 -0
  41. package/src/bot/detect/serverkJson.test.ts +88 -0
  42. package/src/bot/detect/serverkJson.ts +174 -0
  43. package/src/bot/index.ts +8 -0
  44. package/src/cli/detectTable.test.ts +66 -0
  45. package/src/cli/detectTable.ts +43 -0
  46. package/src/cli/main.ts +12 -4
  47. package/src/cli/renderedFile.ts +35 -0
  48. package/src/cli/schema.ts +7 -26
  49. package/src/cli/validate.test.ts +210 -2
  50. package/src/cli/validate.ts +71 -7
  51. package/src/context/bridgeContext.ts +11 -0
  52. package/src/core/bridgePort.test.ts +45 -0
  53. package/src/core/bridgePort.ts +26 -0
  54. package/src/guides/compile.ts +2 -2
  55. package/src/manifest/index.ts +1 -0
  56. package/src/manifest/kind.ts +5 -0
  57. package/src/manifest/manifest.test.ts +630 -3
  58. package/src/manifest/manifest.ts +391 -87
  59. package/src/manifest/runtime.ts +2 -2
  60. package/src/manifest/source.ts +4 -4
  61. package/src/modules/bridgeLifecycleSubject.ts +8 -0
  62. package/src/modules/bridgeModule.ts +7 -0
  63. package/src/modules/bridgeStopSignal.ts +13 -0
  64. package/src/modules/index.ts +2 -0
  65. package/src/panel/bridgeDetail.ts +2 -0
  66. package/src/protocol/bridgeConfig.ts +2 -0
  67. package/src/runtime/bridgeConfigReader.test.ts +80 -0
  68. package/src/runtime/bridgeConfigReader.ts +20 -0
  69. package/src/runtime/bridgeContext.test.ts +241 -0
  70. package/src/runtime/bridgeContext.ts +35 -9
  71. package/src/runtime/bridgeDriver.test.ts +132 -1
  72. package/src/runtime/bridgeDriver.ts +54 -1
  73. package/src/runtime/bridgeFiles.test.ts +62 -0
  74. package/src/runtime/bridgeFiles.ts +12 -2
  75. package/src/runtime/bridgeLiveConfig.ts +18 -0
  76. package/src/runtime/bridgeRedaction.test.ts +410 -0
  77. package/src/runtime/bridgeRedaction.ts +214 -0
  78. package/src/runtime/bridgeRegistry.test.ts +3 -0
  79. package/src/runtime/bridgeRegistry.ts +15 -0
  80. package/src/runtime/bridgeServer.test.ts +130 -0
  81. package/src/runtime/bridgeServer.ts +24 -7
  82. package/src/runtime/bridgeSetupHandlers.test.ts +88 -0
  83. package/src/runtime/bridgeSetupHandlers.ts +16 -6
  84. package/src/runtime/bridgeSupervisor.test.ts +759 -2
  85. package/src/runtime/bridgeSupervisor.ts +234 -54
  86. package/src/runtime/serve.ts +48 -13
@@ -1,5 +1,7 @@
1
1
  import type { BridgeCommandOptions, BridgeReply } from "../context";
2
2
  import { BridgeRuntime, BridgeStream } from "../core";
3
+ import { BridgeLifecycleSubject } from "../modules/bridgeLifecycleSubject";
4
+ import { BRIDGE_STOP_SIGNALS, type BridgeStopSignal } from "../modules/bridgeStopSignal";
3
5
  import { BRIDGE_CRASH_LINE_LIMIT, BridgeCrashPhase, type BridgeCrashRecord } from "../protocol";
4
6
  import { stripAnsi } from "../utils";
5
7
  import type { BridgeLeveledLogger } from "./bridgeLog";
@@ -21,10 +23,14 @@ const RESTART_LIMIT = 3;
21
23
 
22
24
  const STABLE_UPTIME_MS = 180_000;
23
25
 
26
+ const READY_TIMEOUT_STOP_FACTOR = 10;
27
+
24
28
  const GAME_OOM_SCORE_ADJ = 500;
25
29
 
26
30
  const SIGKILL_EXIT_CODE = 128 + 9;
27
31
 
32
+ const ENVIRONMENT_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
33
+
28
34
  export interface SupervisorLine {
29
35
  stream: BridgeStream;
30
36
  line: string;
@@ -35,8 +41,14 @@ export interface SupervisorOptions {
35
41
  uid: number;
36
42
  gid: number;
37
43
  stopTimeoutSeconds: number;
44
+ readyTimeoutSeconds?: number;
45
+ stopSignal?: BridgeStopSignal;
46
+ subject?: BridgeLifecycleSubject;
38
47
  ready?: RegExp;
48
+ failed?: RegExp;
39
49
  resolveCommand: () => Promise<string[]>;
50
+ resolveEnvironment?: () => Promise<Record<string, string>>;
51
+ redact?: (text: string) => string;
40
52
  gracefulStop?: () => Promise<void>;
41
53
  stableAfterMs?: number;
42
54
  onReady?: () => void;
@@ -78,6 +90,27 @@ const SIGNAL_NUMBERS: Record<string, number> = {
78
90
  SIGTERM: 15,
79
91
  };
80
92
 
93
+ const spawnEnvironment = (subject: BridgeLifecycleSubject, resolved: Record<string, string>) => {
94
+ for (const [key, value] of Object.entries(resolved)) {
95
+ if (!ENVIRONMENT_KEY.test(key)) {
96
+ throw new Error(`the ${subject} environment names "${key}", which is not a valid variable name`);
97
+ }
98
+
99
+ if (typeof value !== "string") {
100
+ throw new Error(`the ${subject} environment value for "${key}" is not a string`);
101
+ }
102
+
103
+ if (value.includes("\0")) {
104
+ throw new Error(`the ${subject} environment value for "${key}" contains a NUL byte`);
105
+ }
106
+ }
107
+
108
+ return {
109
+ ...process.env,
110
+ ...resolved,
111
+ };
112
+ };
113
+
81
114
  const EARLY_OUTPUT_LIMIT = 2000;
82
115
 
83
116
  const earlyOutput = async (spawned: { stdout: unknown; stderr: unknown }) => {
@@ -151,6 +184,7 @@ const chain = () => {
151
184
  };
152
185
 
153
186
  export const createSupervisor = (options: SupervisorOptions) => {
187
+ const subject = options.subject ?? BridgeLifecycleSubject.Game;
154
188
  const tail: SupervisorLine[] = [];
155
189
  const watchers = new Set<LineWatcher>();
156
190
  const restartStamps: number[] = [];
@@ -166,10 +200,37 @@ export const createSupervisor = (options: SupervisorOptions) => {
166
200
  let launchedAt = performance.now();
167
201
  let stable = false;
168
202
  let stableTimer: ReturnType<typeof setTimeout> | null = null;
203
+ let bootFailure: string | null = null;
204
+ let replacing: SpawnedGame | null = null;
169
205
 
170
206
  const serialize = chain();
171
207
  const transition = chain();
172
208
 
209
+ const readyTimeoutMs = (options.readyTimeoutSeconds ?? options.stopTimeoutSeconds * READY_TIMEOUT_STOP_FACTOR) * 1000;
210
+
211
+ const awaitingReady = () => {
212
+ return options.readyTimeoutSeconds !== undefined && runtime === BridgeRuntime.Starting;
213
+ };
214
+
215
+ const failBoot = (line: string) => {
216
+ const failing = child;
217
+ const failingGroup = childGroup;
218
+
219
+ if (failing === null) {
220
+ return;
221
+ }
222
+
223
+ bootFailure = line.trim().slice(0, BRIDGE_CRASH_LINE_LIMIT);
224
+
225
+ terminate(failing, failingGroup, "SIGTERM");
226
+
227
+ void Bun.sleep(KILL_GRACE_MS).then(() => {
228
+ if (child === failing) {
229
+ terminate(failing, failingGroup, "SIGKILL");
230
+ }
231
+ });
232
+ };
233
+
173
234
  const markStable = () => {
174
235
  if (stable) {
175
236
  return;
@@ -180,7 +241,14 @@ export const createSupervisor = (options: SupervisorOptions) => {
180
241
  options.onStable?.();
181
242
  };
182
243
 
183
- const emit = (entry: SupervisorLine) => {
244
+ const emit = (raw: SupervisorLine) => {
245
+ const entry = options.redact
246
+ ? {
247
+ stream: raw.stream,
248
+ line: options.redact(raw.line),
249
+ }
250
+ : raw;
251
+
184
252
  if (entry.stream === BridgeStream.Stderr) {
185
253
  process.stderr.write(`${entry.line}\n`);
186
254
  } else {
@@ -286,7 +354,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
286
354
  const [executable, ...rest] = command;
287
355
 
288
356
  if (!executable) {
289
- throw new Error("the game start command is empty");
357
+ throw new Error(`the ${subject} start command is empty`);
290
358
  }
291
359
 
292
360
  const argv = [
@@ -294,10 +362,15 @@ export const createSupervisor = (options: SupervisorOptions) => {
294
362
  ...rest,
295
363
  ];
296
364
 
365
+ const environment = options.resolveEnvironment
366
+ ? spawnEnvironment(subject, await options.resolveEnvironment())
367
+ : null;
368
+
297
369
  const { argv: spawnArgv, credentials } = await unprivilegedArgv(argv, options.uid, options.gid);
298
370
 
299
371
  launchedAt = performance.now();
300
372
  stable = false;
373
+ bootFailure = null;
301
374
 
302
375
  if (stableTimer) {
303
376
  clearTimeout(stableTimer);
@@ -317,7 +390,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
317
390
  reported = true;
318
391
 
319
392
  void handleExit(spawned, code, pumps).catch((error: unknown) => {
320
- options.onLog.error("the game exit was not reported", {
393
+ options.onLog.error(`the ${subject} exit was not reported`, {
321
394
  error: errorText(error),
322
395
  });
323
396
  });
@@ -325,6 +398,9 @@ export const createSupervisor = (options: SupervisorOptions) => {
325
398
 
326
399
  const spawned = Bun.spawn(spawnArgv, {
327
400
  ...credentials,
401
+ ...(environment && {
402
+ env: environment,
403
+ }),
328
404
  cwd: options.cwd,
329
405
  detached: true,
330
406
  stderr: "pipe",
@@ -346,7 +422,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
346
422
 
347
423
  if (observed === null && finished) {
348
424
  throw new Error(
349
- `the game exited immediately with code ${exitCodeOf(spawned.exitCode, spawned.signalCode)}${await earlyOutput(spawned)}`,
425
+ `the ${subject} exited immediately with code ${exitCodeOf(spawned.exitCode, spawned.signalCode)}${await earlyOutput(spawned)}`,
350
426
  );
351
427
  }
352
428
 
@@ -357,7 +433,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
357
433
  sweep(spawned.pid, group);
358
434
 
359
435
  throw new Error(
360
- `the game process runs as uid ${String(observed)}, expected ${options.uid} — refusing to run it as the bridge`,
436
+ `the ${subject} process runs as uid ${String(observed)}, expected ${options.uid} — refusing to run it as the bridge`,
361
437
  );
362
438
  }
363
439
 
@@ -370,7 +446,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
370
446
  const launchStamp = launchedAt;
371
447
 
372
448
  stableTimer = setTimeout(() => {
373
- if (launchStamp !== launchedAt || child === null) {
449
+ if (launchStamp !== launchedAt || child === null || awaitingReady()) {
374
450
  return;
375
451
  }
376
452
 
@@ -384,7 +460,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
384
460
 
385
461
  accepted = true;
386
462
 
387
- options.onLog("game started", {
463
+ options.onLog(`${subject} started`, {
388
464
  pid: spawned.pid,
389
465
  group,
390
466
  uid: options.uid,
@@ -396,6 +472,22 @@ export const createSupervisor = (options: SupervisorOptions) => {
396
472
  return;
397
473
  }
398
474
 
475
+ if (options.failed) {
476
+ void watch(options.failed, readyTimeoutMs).then((match) => {
477
+ if (!match || launchStamp !== launchedAt || child === null || stable) {
478
+ return;
479
+ }
480
+
481
+ const line = match.input ?? match[0];
482
+
483
+ options.onLog.error(`the ${subject} reported a failed boot, stopping it`, {
484
+ line: line.trim().slice(0, BRIDGE_CRASH_LINE_LIMIT),
485
+ });
486
+
487
+ failBoot(line);
488
+ });
489
+ }
490
+
399
491
  if (!options.ready) {
400
492
  runtime = BridgeRuntime.Running;
401
493
 
@@ -404,7 +496,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
404
496
 
405
497
  runtime = BridgeRuntime.Starting;
406
498
 
407
- void watch(options.ready, options.stopTimeoutSeconds * 1000 * 10).then((match) => {
499
+ void watch(options.ready, readyTimeoutMs).then((match) => {
408
500
  if (launchStamp !== launchedAt || child === null) {
409
501
  return;
410
502
  }
@@ -413,7 +505,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
413
505
  if (runtime === BridgeRuntime.Starting) {
414
506
  runtime = BridgeRuntime.Running;
415
507
 
416
- options.onLog("game ready");
508
+ options.onLog(`${subject} ready`);
417
509
  options.onReady?.();
418
510
  }
419
511
 
@@ -422,17 +514,27 @@ export const createSupervisor = (options: SupervisorOptions) => {
422
514
  return;
423
515
  }
424
516
 
517
+ if (runtime === BridgeRuntime.Starting && options.readyTimeoutSeconds !== undefined) {
518
+ options.onLog.error(`the ${subject} did not report ready in time, stopping it`, {
519
+ readyTimeoutSeconds: options.readyTimeoutSeconds,
520
+ });
521
+
522
+ failBoot(`the ${subject} did not report ready within ${options.readyTimeoutSeconds} seconds`);
523
+
524
+ return;
525
+ }
526
+
425
527
  if (runtime === BridgeRuntime.Starting) {
426
528
  runtime = BridgeRuntime.Running;
427
529
 
428
- options.onLog.warn("game ready line not seen, treating it as running");
530
+ options.onLog.warn(`${subject} ready line not seen, treating it as running`);
429
531
  }
430
532
 
431
533
  markStable();
432
534
  });
433
535
  };
434
536
 
435
- const restart = async () => {
537
+ const restartAfterCrash = async () => {
436
538
  await Bun.sleep(RESTART_DELAY_MS);
437
539
 
438
540
  await transition(async () => {
@@ -445,7 +547,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
445
547
  } catch (error) {
446
548
  runtime = BridgeRuntime.Crashed;
447
549
 
448
- options.onLog.error("game failed to restart", {
550
+ options.onLog.error(`${subject} failed to restart`, {
449
551
  error: errorText(error),
450
552
  });
451
553
  }
@@ -474,12 +576,22 @@ export const createSupervisor = (options: SupervisorOptions) => {
474
576
 
475
577
  reap(finished, code);
476
578
 
579
+ if (finished === replacing) {
580
+ outOfMemory = false;
581
+
582
+ options.onLog(`${subject} stopped for a restart`, {
583
+ code,
584
+ });
585
+
586
+ return;
587
+ }
588
+
477
589
  if (!shouldRun) {
478
590
  outOfMemory = false;
479
591
 
480
592
  runtime = BridgeRuntime.Stopped;
481
593
 
482
- options.onLog("game stopped", {
594
+ options.onLog(`${subject} stopped`, {
483
595
  code,
484
596
  });
485
597
 
@@ -489,10 +601,10 @@ export const createSupervisor = (options: SupervisorOptions) => {
489
601
  const uptimeMs = performance.now() - launchedAt;
490
602
  const phase = stable ? BridgeCrashPhase.Running : BridgeCrashPhase.Booting;
491
603
 
492
- outOfMemory = code === SIGKILL_EXIT_CODE;
604
+ outOfMemory = code === SIGKILL_EXIT_CODE && bootFailure === null;
493
605
 
494
606
  if (outOfMemory) {
495
- options.onLog.error("the game was killed for running out of memory", {
607
+ options.onLog.error(`the ${subject} was killed for running out of memory`, {
496
608
  code,
497
609
  });
498
610
  }
@@ -510,16 +622,18 @@ export const createSupervisor = (options: SupervisorOptions) => {
510
622
 
511
623
  await drainPumps(pumps);
512
624
 
625
+ const line = bootFailure ?? lastLine();
626
+
513
627
  options.onExit?.({
514
628
  ...settled,
515
- lastLine: lastLine(),
629
+ lastLine: line !== null && options.redact ? options.redact(line) : line,
516
630
  });
517
631
  };
518
632
 
519
633
  if (phase === BridgeCrashPhase.Booting) {
520
634
  runtime = BridgeRuntime.Crashed;
521
635
 
522
- options.onLog.error("game exited while booting, not restarting", {
636
+ options.onLog.error(`${subject} exited while booting, not restarting`, {
523
637
  code,
524
638
  phase,
525
639
  });
@@ -536,7 +650,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
536
650
  breaker = true;
537
651
  runtime = BridgeRuntime.Crashed;
538
652
 
539
- options.onLog.error("game crash loop detected, not restarting", {
653
+ options.onLog.error(`${subject} crash loop detected, not restarting`, {
540
654
  code,
541
655
  restarts,
542
656
  });
@@ -548,19 +662,80 @@ export const createSupervisor = (options: SupervisorOptions) => {
548
662
 
549
663
  runtime = BridgeRuntime.Starting;
550
664
 
551
- options.onLog.warn("game exited, restarting", {
665
+ options.onLog.warn(`${subject} exited, restarting`, {
552
666
  code,
553
667
  restarts,
554
668
  });
555
669
 
556
670
  await report(true, false);
557
671
 
558
- void restart();
672
+ void restartAfterCrash();
559
673
  }
560
674
 
675
+ const escalate = async (active: SpawnedGame, group: number | null) => {
676
+ if (!options.stopSignal) {
677
+ terminate(active, group, "SIGTERM");
678
+
679
+ const afterTerm = await Promise.race([
680
+ active.exited,
681
+ Bun.sleep(KILL_GRACE_MS).then(() => null),
682
+ ]);
683
+
684
+ if (afterTerm !== null) {
685
+ return;
686
+ }
687
+ }
688
+
689
+ terminate(active, group, "SIGKILL");
690
+
691
+ await active.exited;
692
+ };
693
+
694
+ const halt = async (active: SpawnedGame, group: number | null) => {
695
+ if (options.gracefulStop) {
696
+ const graceful = options.gracefulStop().catch((error: unknown) => {
697
+ options.onLog.warn("graceful stop failed", {
698
+ error: errorText(error),
699
+ });
700
+ });
701
+
702
+ await Promise.race([
703
+ graceful,
704
+ active.exited,
705
+ ]);
706
+ }
707
+
708
+ const exited = active.exitCode !== null || active.signalCode !== null;
709
+
710
+ if (options.stopSignal && !exited) {
711
+ terminate(active, group, BRIDGE_STOP_SIGNALS[options.stopSignal]);
712
+ }
713
+
714
+ const settled = await Promise.race([
715
+ active.exited,
716
+ Bun.sleep(options.stopTimeoutSeconds * 1000).then(() => null),
717
+ ]);
718
+
719
+ if (settled === null) {
720
+ await escalate(active, group);
721
+ }
722
+
723
+ if (child !== active) {
724
+ return null;
725
+ }
726
+
727
+ const code = exitCodeOf(active.exitCode, active.signalCode);
728
+
729
+ reap(active, code);
730
+
731
+ outOfMemory = false;
732
+
733
+ return code;
734
+ };
735
+
561
736
  const writeLine = async (input: string) => {
562
737
  if (!child) {
563
- throw new Error("the game is not running");
738
+ throw new Error(`the ${subject} is not running`);
564
739
  }
565
740
 
566
741
  child.stdin.write(`${input}\n`);
@@ -624,54 +799,59 @@ export const createSupervisor = (options: SupervisorOptions) => {
624
799
 
625
800
  runtime = BridgeRuntime.Stopping;
626
801
 
627
- if (options.gracefulStop) {
628
- const graceful = options.gracefulStop().catch((error: unknown) => {
629
- options.onLog.warn("graceful stop failed", {
630
- error: errorText(error),
631
- });
802
+ const code = await halt(active, group);
803
+
804
+ if (code !== null) {
805
+ options.onLog(`${subject} stopped`, {
806
+ code,
632
807
  });
808
+ }
809
+
810
+ runtime = BridgeRuntime.Stopped;
811
+
812
+ options.onStopped?.();
813
+ });
814
+ },
633
815
 
634
- await Promise.race([
635
- graceful,
636
- active.exited,
637
- ]);
816
+ async restart() {
817
+ await transition(async () => {
818
+ if (!shouldRun) {
819
+ throw new Error(`the ${subject} is not meant to be running, so there is nothing to restart`);
638
820
  }
639
821
 
640
- const settled = await Promise.race([
641
- active.exited,
642
- Bun.sleep(options.stopTimeoutSeconds * 1000).then(() => null),
643
- ]);
822
+ const active = child;
644
823
 
645
- if (settled === null) {
646
- terminate(active, group, "SIGTERM");
824
+ runtime = BridgeRuntime.Stopping;
647
825
 
648
- const afterTerm = await Promise.race([
649
- active.exited,
650
- Bun.sleep(KILL_GRACE_MS).then(() => null),
651
- ]);
826
+ if (active) {
827
+ replacing = active;
652
828
 
653
- if (afterTerm === null) {
654
- terminate(active, group, "SIGKILL");
829
+ try {
830
+ const code = await halt(active, childGroup);
655
831
 
656
- await active.exited;
832
+ if (code !== null) {
833
+ options.onLog(`${subject} stopped for a restart`, {
834
+ code,
835
+ });
836
+ }
837
+ } finally {
838
+ replacing = null;
657
839
  }
658
840
  }
659
841
 
660
- if (child === active) {
661
- const code = exitCodeOf(active.exitCode, active.signalCode);
842
+ if (!shouldRun) {
843
+ return;
844
+ }
662
845
 
663
- reap(active, code);
846
+ breaker = false;
664
847
 
665
- outOfMemory = false;
848
+ try {
849
+ await launch();
850
+ } catch (error) {
851
+ runtime = BridgeRuntime.Crashed;
666
852
 
667
- options.onLog("game stopped", {
668
- code,
669
- });
853
+ throw error;
670
854
  }
671
-
672
- runtime = BridgeRuntime.Stopped;
673
-
674
- options.onStopped?.();
675
855
  });
676
856
  },
677
857
 
@@ -1,6 +1,6 @@
1
1
  import { chmod, chown, mkdir, rm } from "node:fs/promises";
2
2
  import { BridgeDesired, type BridgeInstallHold, BridgeRuntime, BridgeUserError, type LocalizedText } from "../core";
3
- import type { BridgeDriver } from "../modules";
3
+ import { type BridgeDriver, BridgeLifecycleSubject } from "../modules";
4
4
  import {
5
5
  BRIDGE_INSTALL_ERROR_LIMIT,
6
6
  BRIDGE_VERSION,
@@ -16,8 +16,10 @@ import { createContext } from "./bridgeContext";
16
16
  import { validateDriver } from "./bridgeDriver";
17
17
  import { createEventRing } from "./bridgeEvents";
18
18
  import { decideInstall, recountHold } from "./bridgeInstall";
19
- import { errorText, log } from "./bridgeLog";
19
+ import { createBridgeLiveConfig } from "./bridgeLiveConfig";
20
+ import { errorText, log as stdoutLog } from "./bridgeLog";
20
21
  import { pendingResult, rejectedPending, unsupportedPending } from "./bridgePending";
22
+ import { createBridgeRedaction, redactLocalized, redactLogger, secretFieldKeys } from "./bridgeRedaction";
21
23
  import { createRegistry } from "./bridgeRegistry";
22
24
  import { createServer } from "./bridgeServer";
23
25
  import { setupSummaryOf } from "./bridgeSetup";
@@ -69,24 +71,42 @@ export const serve = async (driver: BridgeDriver) => {
69
71
 
70
72
  const desired = await readDesired(statePath(config));
71
73
 
74
+ const live = createBridgeLiveConfig(configPath(), config);
75
+
76
+ const redact = createBridgeRedaction(secretFieldKeys(driver), live);
77
+
78
+ const log = redactLogger(stdoutLog, redact);
79
+
72
80
  let installError: string | null = null;
73
81
 
74
82
  let installErrorText: LocalizedText | null = null;
75
83
 
76
84
  let installHold: BridgeInstallHold | null = null;
77
85
 
78
- const setup = createSetupHandlers(driver, log);
86
+ const setup = createSetupHandlers(driver, log, redact);
79
87
 
80
88
  const supervisor = createSupervisor({
81
89
  cwd: config.cwd,
82
90
  uid: config.gameUid,
83
91
  gid: config.gameGid,
84
92
  stopTimeoutSeconds: driver.lifecycle.stopTimeoutSeconds ?? DEFAULT_STOP_TIMEOUT_SECONDS,
93
+ readyTimeoutSeconds: driver.lifecycle.readyTimeoutSeconds,
94
+ stopSignal: driver.lifecycle.stopSignal,
95
+ subject: driver.lifecycle.subject,
85
96
  ready: driver.lifecycle.ready,
97
+ failed: driver.lifecycle.failed,
98
+ redact,
86
99
  onLog: log,
87
100
  async resolveCommand() {
101
+ live.reload();
102
+
88
103
  return await driver.lifecycle.command(context);
89
104
  },
105
+ resolveEnvironment: driver.lifecycle.environment
106
+ ? async () => {
107
+ return (await driver.lifecycle.environment?.(context)) ?? {};
108
+ }
109
+ : undefined,
90
110
  async gracefulStop() {
91
111
  await driver.lifecycle.stop?.(context);
92
112
  },
@@ -110,15 +130,29 @@ export const serve = async (driver: BridgeDriver) => {
110
130
 
111
131
  const ring = createEventRing(driver.events, supervisor);
112
132
 
113
- const context = createContext(config, supervisor, {
114
- onLog: log,
115
- onEmit(event, payload) {
116
- ring.emit(event, payload);
117
- },
118
- onSetup(runtime) {
119
- setup.onSetup(runtime);
133
+ const context = createContext(
134
+ config,
135
+ supervisor,
136
+ {
137
+ onLog: log,
138
+ onEmit(event, payload) {
139
+ ring.emit(event, payload);
140
+ },
141
+ onSetup(runtime) {
142
+ setup.onSetup(runtime);
143
+ },
144
+ async onRestart() {
145
+ if (current !== BridgeDesired.Running) {
146
+ throw new Error(
147
+ `the ${driver.lifecycle.subject ?? BridgeLifecycleSubject.Game} is not meant to be running, so there is nothing to restart`,
148
+ );
149
+ }
150
+
151
+ await supervisor.restart();
152
+ },
120
153
  },
121
- });
154
+ live,
155
+ );
122
156
 
123
157
  const registry = createRegistry(driver, context, supervisor, ring);
124
158
  const backupLease = createBackupLease(async () => {
@@ -219,6 +253,7 @@ export const serve = async (driver: BridgeDriver) => {
219
253
  const server = createServer({
220
254
  socket: socketPath(config),
221
255
  token: config.token,
256
+ redact,
222
257
  onLog: log,
223
258
  onStatus: status,
224
259
  onManifest() {
@@ -427,8 +462,8 @@ export const serve = async (driver: BridgeDriver) => {
427
462
 
428
463
  log("install complete");
429
464
  } catch (error) {
430
- installError = errorText(error).slice(0, BRIDGE_INSTALL_ERROR_LIMIT);
431
- installErrorText = error instanceof BridgeUserError ? error.text : null;
465
+ installError = redact(errorText(error)).slice(0, BRIDGE_INSTALL_ERROR_LIMIT);
466
+ installErrorText = error instanceof BridgeUserError ? redactLocalized(error.text, redact) : null;
432
467
 
433
468
  supervisor.setRuntime(BridgeRuntime.Crashed);
434
469