@serverkgg/bridge 0.0.21 → 0.1.2

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 (119) 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 +246 -0
  9. package/src/bot/botPanel.ts +129 -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 +283 -3
  50. package/src/cli/validate.ts +105 -7
  51. package/src/context/bridgeContext.ts +17 -1
  52. package/src/core/bridgeAction.ts +2 -0
  53. package/src/core/bridgeActivity.ts +9 -0
  54. package/src/core/bridgeAvailability.ts +10 -0
  55. package/src/core/bridgePort.test.ts +45 -0
  56. package/src/core/bridgePort.ts +26 -0
  57. package/src/core/bridgeUserError.test.ts +10 -0
  58. package/src/core/bridgeUserError.ts +6 -2
  59. package/src/core/describeDownloadFailure.test.ts +58 -0
  60. package/src/core/describeDownloadFailure.ts +69 -0
  61. package/src/core/index.ts +3 -0
  62. package/src/guides/compile.ts +2 -2
  63. package/src/manifest/index.ts +1 -0
  64. package/src/manifest/kind.ts +5 -0
  65. package/src/manifest/manifest.test.ts +630 -3
  66. package/src/manifest/manifest.ts +391 -87
  67. package/src/manifest/runtime.ts +2 -2
  68. package/src/manifest/source.ts +4 -4
  69. package/src/modules/bridge.ts +8 -1
  70. package/src/modules/bridgeLifecycleSubject.ts +8 -0
  71. package/src/modules/bridgeModule.ts +19 -1
  72. package/src/modules/bridgeStopSignal.ts +13 -0
  73. package/src/modules/index.ts +2 -0
  74. package/src/panel/bridgeDetail.ts +4 -1
  75. package/src/protocol/bridgeActivity.ts +54 -0
  76. package/src/protocol/bridgeConfig.ts +2 -0
  77. package/src/protocol/bridgeEvents.ts +30 -0
  78. package/src/protocol/bridgeFrame.ts +10 -1
  79. package/src/protocol/index.ts +1 -0
  80. package/src/runtime/bridgeActions.ts +19 -2
  81. package/src/runtime/bridgeActivity.test.ts +318 -0
  82. package/src/runtime/bridgeActivity.ts +268 -0
  83. package/src/runtime/bridgeConfigReader.test.ts +80 -0
  84. package/src/runtime/bridgeConfigReader.ts +20 -0
  85. package/src/runtime/bridgeContext.test.ts +247 -0
  86. package/src/runtime/bridgeContext.ts +43 -10
  87. package/src/runtime/bridgeDriver.test.ts +153 -1
  88. package/src/runtime/bridgeDriver.ts +64 -8
  89. package/src/runtime/bridgeFiles.test.ts +62 -0
  90. package/src/runtime/bridgeFiles.ts +12 -2
  91. package/src/runtime/bridgeLiveConfig.ts +18 -0
  92. package/src/runtime/bridgeLog.ts +10 -0
  93. package/src/runtime/bridgeRedaction.test.ts +410 -0
  94. package/src/runtime/bridgeRedaction.ts +214 -0
  95. package/src/runtime/bridgeRefusalError.ts +13 -0
  96. package/src/runtime/bridgeRegistry.test.ts +1056 -10
  97. package/src/runtime/bridgeRegistry.ts +464 -50
  98. package/src/runtime/bridgeScopedContext.ts +55 -0
  99. package/src/runtime/bridgeServer.test.ts +534 -4
  100. package/src/runtime/bridgeServer.ts +145 -38
  101. package/src/runtime/bridgeSetupHandlers.test.ts +156 -5
  102. package/src/runtime/bridgeSetupHandlers.ts +59 -14
  103. package/src/runtime/bridgeSupervisor.test.ts +670 -2
  104. package/src/runtime/bridgeSupervisor.ts +219 -67
  105. package/src/runtime/serve.ts +87 -11
  106. package/src/steam/steamcmd.test.ts +32 -1
  107. package/src/steam/steamcmd.ts +27 -4
  108. package/src/utils/changedSettingKeys.test.ts +65 -0
  109. package/src/utils/changedSettingKeys.ts +6 -0
  110. package/src/utils/describeExecFailure.test.ts +34 -0
  111. package/src/utils/describeExecFailure.ts +9 -0
  112. package/src/utils/index.ts +3 -0
  113. package/src/utils/sameSettingValue.test.ts +35 -0
  114. package/src/utils/sameSettingValue.ts +40 -0
  115. package/src/wire/activity.test.ts +95 -0
  116. package/src/wire/activity.ts +41 -0
  117. package/src/wire/index.ts +1 -0
  118. package/src/wire/panel.test.ts +67 -1
  119. package/src/wire/panel.ts +11 -0
@@ -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,9 +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;
39
48
  failed?: RegExp;
40
49
  resolveCommand: () => Promise<string[]>;
50
+ resolveEnvironment?: () => Promise<Record<string, string>>;
51
+ redact?: (text: string) => string;
41
52
  gracefulStop?: () => Promise<void>;
42
53
  stableAfterMs?: number;
43
54
  onReady?: () => void;
@@ -79,6 +90,27 @@ const SIGNAL_NUMBERS: Record<string, number> = {
79
90
  SIGTERM: 15,
80
91
  };
81
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
+
82
114
  const EARLY_OUTPUT_LIMIT = 2000;
83
115
 
84
116
  const earlyOutput = async (spawned: { stdout: unknown; stderr: unknown }) => {
@@ -152,6 +184,7 @@ const chain = () => {
152
184
  };
153
185
 
154
186
  export const createSupervisor = (options: SupervisorOptions) => {
187
+ const subject = options.subject ?? BridgeLifecycleSubject.Game;
155
188
  const tail: SupervisorLine[] = [];
156
189
  const watchers = new Set<LineWatcher>();
157
190
  const restartStamps: number[] = [];
@@ -168,10 +201,36 @@ export const createSupervisor = (options: SupervisorOptions) => {
168
201
  let stable = false;
169
202
  let stableTimer: ReturnType<typeof setTimeout> | null = null;
170
203
  let bootFailure: string | null = null;
204
+ let replacing: SpawnedGame | null = null;
171
205
 
172
206
  const serialize = chain();
173
207
  const transition = chain();
174
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
+
175
234
  const markStable = () => {
176
235
  if (stable) {
177
236
  return;
@@ -182,7 +241,14 @@ export const createSupervisor = (options: SupervisorOptions) => {
182
241
  options.onStable?.();
183
242
  };
184
243
 
185
- 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
+
186
252
  if (entry.stream === BridgeStream.Stderr) {
187
253
  process.stderr.write(`${entry.line}\n`);
188
254
  } else {
@@ -288,7 +354,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
288
354
  const [executable, ...rest] = command;
289
355
 
290
356
  if (!executable) {
291
- throw new Error("the game start command is empty");
357
+ throw new Error(`the ${subject} start command is empty`);
292
358
  }
293
359
 
294
360
  const argv = [
@@ -296,6 +362,10 @@ export const createSupervisor = (options: SupervisorOptions) => {
296
362
  ...rest,
297
363
  ];
298
364
 
365
+ const environment = options.resolveEnvironment
366
+ ? spawnEnvironment(subject, await options.resolveEnvironment())
367
+ : null;
368
+
299
369
  const { argv: spawnArgv, credentials } = await unprivilegedArgv(argv, options.uid, options.gid);
300
370
 
301
371
  launchedAt = performance.now();
@@ -320,7 +390,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
320
390
  reported = true;
321
391
 
322
392
  void handleExit(spawned, code, pumps).catch((error: unknown) => {
323
- options.onLog.error("the game exit was not reported", {
393
+ options.onLog.error(`the ${subject} exit was not reported`, {
324
394
  error: errorText(error),
325
395
  });
326
396
  });
@@ -328,6 +398,9 @@ export const createSupervisor = (options: SupervisorOptions) => {
328
398
 
329
399
  const spawned = Bun.spawn(spawnArgv, {
330
400
  ...credentials,
401
+ ...(environment && {
402
+ env: environment,
403
+ }),
331
404
  cwd: options.cwd,
332
405
  detached: true,
333
406
  stderr: "pipe",
@@ -349,7 +422,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
349
422
 
350
423
  if (observed === null && finished) {
351
424
  throw new Error(
352
- `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)}`,
353
426
  );
354
427
  }
355
428
 
@@ -360,7 +433,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
360
433
  sweep(spawned.pid, group);
361
434
 
362
435
  throw new Error(
363
- `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`,
364
437
  );
365
438
  }
366
439
 
@@ -373,7 +446,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
373
446
  const launchStamp = launchedAt;
374
447
 
375
448
  stableTimer = setTimeout(() => {
376
- if (launchStamp !== launchedAt || child === null) {
449
+ if (launchStamp !== launchedAt || child === null || awaitingReady()) {
377
450
  return;
378
451
  }
379
452
 
@@ -387,7 +460,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
387
460
 
388
461
  accepted = true;
389
462
 
390
- options.onLog("game started", {
463
+ options.onLog(`${subject} started`, {
391
464
  pid: spawned.pid,
392
465
  group,
393
466
  uid: options.uid,
@@ -400,27 +473,18 @@ export const createSupervisor = (options: SupervisorOptions) => {
400
473
  }
401
474
 
402
475
  if (options.failed) {
403
- void watch(options.failed, options.stopTimeoutSeconds * 1000 * 10).then((match) => {
476
+ void watch(options.failed, readyTimeoutMs).then((match) => {
404
477
  if (!match || launchStamp !== launchedAt || child === null || stable) {
405
478
  return;
406
479
  }
407
480
 
408
- bootFailure = (match.input ?? match[0]).trim().slice(0, BRIDGE_CRASH_LINE_LIMIT);
481
+ const line = match.input ?? match[0];
409
482
 
410
- options.onLog.error("the game reported a failed boot, stopping it", {
411
- line: bootFailure,
483
+ options.onLog.error(`the ${subject} reported a failed boot, stopping it`, {
484
+ line: line.trim().slice(0, BRIDGE_CRASH_LINE_LIMIT),
412
485
  });
413
486
 
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
- });
487
+ failBoot(line);
424
488
  });
425
489
  }
426
490
 
@@ -432,7 +496,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
432
496
 
433
497
  runtime = BridgeRuntime.Starting;
434
498
 
435
- void watch(options.ready, options.stopTimeoutSeconds * 1000 * 10).then((match) => {
499
+ void watch(options.ready, readyTimeoutMs).then((match) => {
436
500
  if (launchStamp !== launchedAt || child === null) {
437
501
  return;
438
502
  }
@@ -441,7 +505,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
441
505
  if (runtime === BridgeRuntime.Starting) {
442
506
  runtime = BridgeRuntime.Running;
443
507
 
444
- options.onLog("game ready");
508
+ options.onLog(`${subject} ready`);
445
509
  options.onReady?.();
446
510
  }
447
511
 
@@ -450,17 +514,27 @@ export const createSupervisor = (options: SupervisorOptions) => {
450
514
  return;
451
515
  }
452
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
+
453
527
  if (runtime === BridgeRuntime.Starting) {
454
528
  runtime = BridgeRuntime.Running;
455
529
 
456
- 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`);
457
531
  }
458
532
 
459
533
  markStable();
460
534
  });
461
535
  };
462
536
 
463
- const restart = async () => {
537
+ const restartAfterCrash = async () => {
464
538
  await Bun.sleep(RESTART_DELAY_MS);
465
539
 
466
540
  await transition(async () => {
@@ -473,7 +547,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
473
547
  } catch (error) {
474
548
  runtime = BridgeRuntime.Crashed;
475
549
 
476
- options.onLog.error("game failed to restart", {
550
+ options.onLog.error(`${subject} failed to restart`, {
477
551
  error: errorText(error),
478
552
  });
479
553
  }
@@ -502,12 +576,22 @@ export const createSupervisor = (options: SupervisorOptions) => {
502
576
 
503
577
  reap(finished, code);
504
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
+
505
589
  if (!shouldRun) {
506
590
  outOfMemory = false;
507
591
 
508
592
  runtime = BridgeRuntime.Stopped;
509
593
 
510
- options.onLog("game stopped", {
594
+ options.onLog(`${subject} stopped`, {
511
595
  code,
512
596
  });
513
597
 
@@ -520,7 +604,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
520
604
  outOfMemory = code === SIGKILL_EXIT_CODE && bootFailure === null;
521
605
 
522
606
  if (outOfMemory) {
523
- 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`, {
524
608
  code,
525
609
  });
526
610
  }
@@ -538,16 +622,18 @@ export const createSupervisor = (options: SupervisorOptions) => {
538
622
 
539
623
  await drainPumps(pumps);
540
624
 
625
+ const line = bootFailure ?? lastLine();
626
+
541
627
  options.onExit?.({
542
628
  ...settled,
543
- lastLine: bootFailure ?? lastLine(),
629
+ lastLine: line !== null && options.redact ? options.redact(line) : line,
544
630
  });
545
631
  };
546
632
 
547
633
  if (phase === BridgeCrashPhase.Booting) {
548
634
  runtime = BridgeRuntime.Crashed;
549
635
 
550
- options.onLog.error("game exited while booting, not restarting", {
636
+ options.onLog.error(`${subject} exited while booting, not restarting`, {
551
637
  code,
552
638
  phase,
553
639
  });
@@ -564,7 +650,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
564
650
  breaker = true;
565
651
  runtime = BridgeRuntime.Crashed;
566
652
 
567
- options.onLog.error("game crash loop detected, not restarting", {
653
+ options.onLog.error(`${subject} crash loop detected, not restarting`, {
568
654
  code,
569
655
  restarts,
570
656
  });
@@ -576,19 +662,80 @@ export const createSupervisor = (options: SupervisorOptions) => {
576
662
 
577
663
  runtime = BridgeRuntime.Starting;
578
664
 
579
- options.onLog.warn("game exited, restarting", {
665
+ options.onLog.warn(`${subject} exited, restarting`, {
580
666
  code,
581
667
  restarts,
582
668
  });
583
669
 
584
670
  await report(true, false);
585
671
 
586
- void restart();
672
+ void restartAfterCrash();
587
673
  }
588
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
+
589
736
  const writeLine = async (input: string) => {
590
737
  if (!child) {
591
- throw new Error("the game is not running");
738
+ throw new Error(`the ${subject} is not running`);
592
739
  }
593
740
 
594
741
  child.stdin.write(`${input}\n`);
@@ -652,54 +799,59 @@ export const createSupervisor = (options: SupervisorOptions) => {
652
799
 
653
800
  runtime = BridgeRuntime.Stopping;
654
801
 
655
- if (options.gracefulStop) {
656
- const graceful = options.gracefulStop().catch((error: unknown) => {
657
- options.onLog.warn("graceful stop failed", {
658
- error: errorText(error),
659
- });
802
+ const code = await halt(active, group);
803
+
804
+ if (code !== null) {
805
+ options.onLog(`${subject} stopped`, {
806
+ code,
660
807
  });
808
+ }
809
+
810
+ runtime = BridgeRuntime.Stopped;
811
+
812
+ options.onStopped?.();
813
+ });
814
+ },
661
815
 
662
- await Promise.race([
663
- graceful,
664
- active.exited,
665
- ]);
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`);
666
820
  }
667
821
 
668
- const settled = await Promise.race([
669
- active.exited,
670
- Bun.sleep(options.stopTimeoutSeconds * 1000).then(() => null),
671
- ]);
822
+ const active = child;
672
823
 
673
- if (settled === null) {
674
- terminate(active, group, "SIGTERM");
824
+ runtime = BridgeRuntime.Stopping;
675
825
 
676
- const afterTerm = await Promise.race([
677
- active.exited,
678
- Bun.sleep(KILL_GRACE_MS).then(() => null),
679
- ]);
826
+ if (active) {
827
+ replacing = active;
680
828
 
681
- if (afterTerm === null) {
682
- terminate(active, group, "SIGKILL");
829
+ try {
830
+ const code = await halt(active, childGroup);
683
831
 
684
- await active.exited;
832
+ if (code !== null) {
833
+ options.onLog(`${subject} stopped for a restart`, {
834
+ code,
835
+ });
836
+ }
837
+ } finally {
838
+ replacing = null;
685
839
  }
686
840
  }
687
841
 
688
- if (child === active) {
689
- const code = exitCodeOf(active.exitCode, active.signalCode);
842
+ if (!shouldRun) {
843
+ return;
844
+ }
690
845
 
691
- reap(active, code);
846
+ breaker = false;
692
847
 
693
- outOfMemory = false;
848
+ try {
849
+ await launch();
850
+ } catch (error) {
851
+ runtime = BridgeRuntime.Crashed;
694
852
 
695
- options.onLog("game stopped", {
696
- code,
697
- });
853
+ throw error;
698
854
  }
699
-
700
- runtime = BridgeRuntime.Stopped;
701
-
702
- options.onStopped?.();
703
855
  });
704
856
  },
705
857