@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.
- package/package.json +3 -1
- package/src/bot/botDeploy.test.ts +363 -0
- package/src/bot/botDeploy.ts +136 -0
- package/src/bot/botEnvironment.test.ts +189 -0
- package/src/bot/botEnvironment.ts +141 -0
- package/src/bot/botIdentity.test.ts +251 -0
- package/src/bot/botIdentity.ts +140 -0
- package/src/bot/botPanel.test.ts +216 -0
- package/src/bot/botPanel.ts +121 -0
- package/src/bot/botState.test.ts +45 -0
- package/src/bot/botState.ts +54 -0
- package/src/bot/botTerminal.test.ts +79 -0
- package/src/bot/botTerminal.ts +62 -0
- package/src/bot/botVariables.test.ts +193 -0
- package/src/bot/botVariables.ts +73 -0
- package/src/bot/detect/botFramework.ts +23 -0
- package/src/bot/detect/botFrameworks.ts +198 -0
- package/src/bot/detect/botInspectWarning.ts +15 -0
- package/src/bot/detect/botPackageManager.ts +6 -0
- package/src/bot/detect/botPlatform.ts +10 -0
- package/src/bot/detect/botRuntime.ts +15 -0
- package/src/bot/detect/botToken.test.ts +71 -0
- package/src/bot/detect/botToken.ts +126 -0
- package/src/bot/detect/detect.test.ts +543 -0
- package/src/bot/detect/detect.ts +842 -0
- package/src/bot/detect/detectResult.ts +40 -0
- package/src/bot/detect/detectTable.test.ts +72 -0
- package/src/bot/detect/detectTable.ts +166 -0
- package/src/bot/detect/envExample.test.ts +29 -0
- package/src/bot/detect/envExample.ts +48 -0
- package/src/bot/detect/envKey.ts +7 -0
- package/src/bot/detect/envScan.test.ts +39 -0
- package/src/bot/detect/envScan.ts +87 -0
- package/src/bot/detect/index.ts +17 -0
- package/src/bot/detect/inferBotPlatform.test.ts +60 -0
- package/src/bot/detect/inferBotPlatform.ts +28 -0
- package/src/bot/detect/keys.test.ts +28 -0
- package/src/bot/detect/keys.ts +64 -0
- package/src/bot/detect/sensitiveKey.test.ts +91 -0
- package/src/bot/detect/sensitiveKey.ts +44 -0
- package/src/bot/detect/serverkJson.test.ts +88 -0
- package/src/bot/detect/serverkJson.ts +174 -0
- package/src/bot/index.ts +8 -0
- package/src/cli/detectTable.test.ts +66 -0
- package/src/cli/detectTable.ts +43 -0
- package/src/cli/main.ts +12 -4
- package/src/cli/renderedFile.ts +35 -0
- package/src/cli/schema.ts +7 -26
- package/src/cli/validate.test.ts +210 -2
- package/src/cli/validate.ts +71 -7
- package/src/context/bridgeContext.ts +11 -0
- package/src/core/bridgePort.test.ts +45 -0
- package/src/core/bridgePort.ts +26 -0
- package/src/guides/compile.ts +2 -2
- package/src/manifest/index.ts +1 -0
- package/src/manifest/kind.ts +5 -0
- package/src/manifest/manifest.test.ts +630 -3
- package/src/manifest/manifest.ts +391 -87
- package/src/manifest/runtime.ts +2 -2
- package/src/manifest/source.ts +4 -4
- package/src/modules/bridgeLifecycleSubject.ts +8 -0
- package/src/modules/bridgeModule.ts +7 -0
- package/src/modules/bridgeStopSignal.ts +13 -0
- package/src/modules/index.ts +2 -0
- package/src/panel/bridgeDetail.ts +2 -0
- package/src/protocol/bridgeConfig.ts +2 -0
- package/src/runtime/bridgeConfigReader.test.ts +80 -0
- package/src/runtime/bridgeConfigReader.ts +20 -0
- package/src/runtime/bridgeContext.test.ts +241 -0
- package/src/runtime/bridgeContext.ts +35 -9
- package/src/runtime/bridgeDriver.test.ts +132 -1
- package/src/runtime/bridgeDriver.ts +54 -1
- package/src/runtime/bridgeFiles.test.ts +62 -0
- package/src/runtime/bridgeFiles.ts +12 -2
- package/src/runtime/bridgeLiveConfig.ts +18 -0
- package/src/runtime/bridgeRedaction.test.ts +410 -0
- package/src/runtime/bridgeRedaction.ts +214 -0
- package/src/runtime/bridgeRegistry.test.ts +3 -0
- package/src/runtime/bridgeRegistry.ts +15 -0
- package/src/runtime/bridgeServer.test.ts +130 -0
- package/src/runtime/bridgeServer.ts +24 -7
- package/src/runtime/bridgeSetupHandlers.test.ts +88 -0
- package/src/runtime/bridgeSetupHandlers.ts +16 -6
- package/src/runtime/bridgeSupervisor.test.ts +759 -2
- package/src/runtime/bridgeSupervisor.ts +234 -54
- 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 = (
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
|
641
|
-
active.exited,
|
|
642
|
-
Bun.sleep(options.stopTimeoutSeconds * 1000).then(() => null),
|
|
643
|
-
]);
|
|
822
|
+
const active = child;
|
|
644
823
|
|
|
645
|
-
|
|
646
|
-
terminate(active, group, "SIGTERM");
|
|
824
|
+
runtime = BridgeRuntime.Stopping;
|
|
647
825
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
Bun.sleep(KILL_GRACE_MS).then(() => null),
|
|
651
|
-
]);
|
|
826
|
+
if (active) {
|
|
827
|
+
replacing = active;
|
|
652
828
|
|
|
653
|
-
|
|
654
|
-
|
|
829
|
+
try {
|
|
830
|
+
const code = await halt(active, childGroup);
|
|
655
831
|
|
|
656
|
-
|
|
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 (
|
|
661
|
-
|
|
842
|
+
if (!shouldRun) {
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
662
845
|
|
|
663
|
-
|
|
846
|
+
breaker = false;
|
|
664
847
|
|
|
665
|
-
|
|
848
|
+
try {
|
|
849
|
+
await launch();
|
|
850
|
+
} catch (error) {
|
|
851
|
+
runtime = BridgeRuntime.Crashed;
|
|
666
852
|
|
|
667
|
-
|
|
668
|
-
code,
|
|
669
|
-
});
|
|
853
|
+
throw error;
|
|
670
854
|
}
|
|
671
|
-
|
|
672
|
-
runtime = BridgeRuntime.Stopped;
|
|
673
|
-
|
|
674
|
-
options.onStopped?.();
|
|
675
855
|
});
|
|
676
856
|
},
|
|
677
857
|
|
package/src/runtime/serve.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|