@serverkgg/bridge 0.1.4 → 0.1.6
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 -3
- package/src/bot/botCauses.test.ts +292 -0
- package/src/bot/botCauses.ts +58 -0
- package/src/bot/botDeploy.test.ts +260 -1
- package/src/bot/botDeploy.ts +125 -6
- package/src/bot/botState.ts +6 -0
- package/src/bot/detect/botFrameworks.ts +1 -0
- package/src/bot/detect/botInspectWarning.ts +4 -1
- package/src/bot/detect/botPackageManager.ts +3 -0
- package/src/bot/detect/corpus/alexmubarakshin-ts-telegraf-boilerplate.json +37 -0
- package/src/bot/detect/corpus/alsarmad-discord-quran-bot.json +40 -0
- package/src/bot/detect/corpus/anasty17-mirror-leech-telegram-bot.json +50 -0
- package/src/bot/detect/corpus/anidiotsguide-guidebot.json +36 -0
- package/src/bot/detect/corpus/anonymousx1025-anonxmusic.json +45 -0
- package/src/bot/detect/corpus/bot-base-telegram-bot-template.json +35 -0
- package/src/bot/detect/corpus/bracketed-templates-discordjs.json +40 -0
- package/src/bot/detect/corpus/cog-creators-red-discordbot.json +49 -0
- package/src/bot/detect/corpus/cranberriez-discord.ts-bot-template.json +35 -0
- package/src/bot/detect/corpus/crazycat256-nextcord-bot-template.json +38 -0
- package/src/bot/detect/corpus/discord-tickets-bot.json +45 -0
- package/src/bot/detect/corpus/edjopato-telegram-typescript-bot-template.json +37 -0
- package/src/bot/detect/corpus/haniali3161-bot-adhkar-telegram.json +33 -0
- package/src/bot/detect/corpus/hellosverre-discord-ts-template.json +34 -0
- package/src/bot/detect/corpus/kevinnovak-discord-bot-typescript-template.json +43 -0
- package/src/bot/detect/corpus/kkrypt0nn-python-discord-bot-template.json +41 -0
- package/src/bot/detect/corpus/kolezka-telegram-grammy-template.json +37 -0
- package/src/bot/detect/corpus/latand-tgbot_template_v3.json +37 -0
- package/src/bot/detect/corpus/mahmoudbm77-zekr-telegram-bot.json +36 -0
- package/src/bot/detect/corpus/mericcan41-discordjs-v14-template-ts.json +35 -0
- package/src/bot/detect/corpus/modmail-dev-modmail.json +52 -0
- package/src/bot/detect/corpus/nezdemkovski-telegram-bot-boilerplate.json +39 -0
- package/src/bot/detect/corpus/notchum-discord-bot-template.json +39 -0
- package/src/bot/detect/corpus/python-discord-bot.json +44 -0
- package/src/bot/detect/corpus/python-telegram-bot-rules-bot.json +39 -0
- package/src/bot/detect/corpus/rajdave69-pycord-bot-template.json +31 -0
- package/src/bot/detect/corpus/rapptz-robodanny.json +36 -0
- package/src/bot/detect/corpus/ry2x-typescript-discord.js-v.14-template.json +35 -0
- package/src/bot/detect/corpus/saiteja-madha-discord-js-bot.json +45 -0
- package/src/bot/detect/corpus/seegreatness-discord.js-for-heroku.json +37 -0
- package/src/bot/detect/corpus/serverk-discord-node.json +36 -0
- package/src/bot/detect/corpus/serverk-discord-python.json +35 -0
- package/src/bot/detect/corpus/serverk-telegram-node.json +36 -0
- package/src/bot/detect/corpus/serverk-telegram-python.json +35 -0
- package/src/bot/detect/corpus/shuruhatik-quran-radio-bot-discord.json +33 -0
- package/src/bot/detect/corpus/skyra-project-skyra.json +46 -0
- package/src/bot/detect/corpus/tgbot-collection-ytdlbot.json +41 -0
- package/src/bot/detect/corpus/zeriodev-music-bot.json +38 -0
- package/src/bot/detect/detect.corpus.test.ts +77 -0
- package/src/bot/detect/detect.test.ts +443 -17
- package/src/bot/detect/detect.ts +375 -34
- package/src/bot/detect/detectResult.ts +4 -0
- package/src/bot/detect/detectTable.test.ts +22 -6
- package/src/bot/detect/detectTable.ts +187 -19
- package/src/bot/detect/detectedVersion.ts +5 -0
- package/src/bot/detect/envExample.test.ts +55 -1
- package/src/bot/detect/envExample.ts +14 -0
- package/src/bot/detect/index.ts +1 -0
- package/src/bot/detect/keys.test.ts +32 -1
- package/src/bot/detect/keys.ts +15 -0
- package/src/bot/detect/serverkJson.test.ts +2 -0
- package/src/bot/detect/serverkJson.ts +4 -1
- package/src/bot/index.ts +1 -0
- package/src/manifest/manifest.ts +2 -1
- package/src/modules/bridge.ts +2 -0
- package/src/modules/bridgeModule.ts +5 -0
- package/src/probes/minecraftPing.test.ts +2 -1
- package/src/protocol/bridgeCrash.ts +49 -0
- package/src/protocol/bridgeEvents.ts +9 -0
- package/src/rcon/rconClient.test.ts +3 -2
- package/src/runtime/bridgeDriver.test.ts +45 -0
- package/src/runtime/bridgeDriver.ts +25 -0
- package/src/runtime/bridgeEvents.ts +2 -0
- package/src/runtime/bridgeSupervisor.test.ts +163 -3
- package/src/runtime/bridgeSupervisor.ts +42 -1
- package/src/runtime/serve.ts +26 -5
|
@@ -19,6 +19,7 @@ import { createRconWriter, type RconWriter } from "./rconWriter";
|
|
|
19
19
|
const HOST = "127.0.0.1";
|
|
20
20
|
|
|
21
21
|
const CLOSED_PORT = 59_433;
|
|
22
|
+
const CLOSED_HOST = "::1";
|
|
22
23
|
|
|
23
24
|
const PASSWORD = "kX7-forbidden-in-errors";
|
|
24
25
|
|
|
@@ -1005,7 +1006,7 @@ describe("RconClient failures", () => {
|
|
|
1005
1006
|
|
|
1006
1007
|
it("reports a port nothing listens on", async () => {
|
|
1007
1008
|
const client = new RconClient({
|
|
1008
|
-
host:
|
|
1009
|
+
host: CLOSED_HOST,
|
|
1009
1010
|
port: CLOSED_PORT,
|
|
1010
1011
|
password: PASSWORD,
|
|
1011
1012
|
timeoutMs: 1000,
|
|
@@ -1475,7 +1476,7 @@ describe("RconClient secrecy", () => {
|
|
|
1475
1476
|
});
|
|
1476
1477
|
|
|
1477
1478
|
const unreachable = new RconClient({
|
|
1478
|
-
host:
|
|
1479
|
+
host: CLOSED_HOST,
|
|
1479
1480
|
port: CLOSED_PORT,
|
|
1480
1481
|
password: PASSWORD,
|
|
1481
1482
|
timeoutMs: 500,
|
|
@@ -1899,6 +1899,51 @@ describe("the lifecycle module", () => {
|
|
|
1899
1899
|
}
|
|
1900
1900
|
});
|
|
1901
1901
|
|
|
1902
|
+
it("accepts a grace from one second to ten minutes and a stable line", () => {
|
|
1903
|
+
for (const graceSeconds of [
|
|
1904
|
+
1,
|
|
1905
|
+
20,
|
|
1906
|
+
600,
|
|
1907
|
+
]) {
|
|
1908
|
+
expect(() =>
|
|
1909
|
+
validateDriver(
|
|
1910
|
+
lifecycleDriver({
|
|
1911
|
+
graceSeconds,
|
|
1912
|
+
stableLine: /logged in as/i,
|
|
1913
|
+
}),
|
|
1914
|
+
),
|
|
1915
|
+
).not.toThrow();
|
|
1916
|
+
}
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
it("refuses a grace outside one second to ten minutes", () => {
|
|
1920
|
+
for (const graceSeconds of [
|
|
1921
|
+
0,
|
|
1922
|
+
601,
|
|
1923
|
+
2.5,
|
|
1924
|
+
]) {
|
|
1925
|
+
expect(() =>
|
|
1926
|
+
validateDriver(
|
|
1927
|
+
lifecycleDriver({
|
|
1928
|
+
graceSeconds,
|
|
1929
|
+
}),
|
|
1930
|
+
),
|
|
1931
|
+
).toThrow(
|
|
1932
|
+
`the lifecycle module declares a grace of ${graceSeconds} seconds — it must be a whole number from 1 to 600`,
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
it("refuses a stable line that is not a regular expression", () => {
|
|
1938
|
+
expect(() =>
|
|
1939
|
+
validateDriver(
|
|
1940
|
+
lifecycleDriver({
|
|
1941
|
+
stableLine: "logged in as",
|
|
1942
|
+
}),
|
|
1943
|
+
),
|
|
1944
|
+
).toThrow("the lifecycle module declares a stable line that is not a regular expression");
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1902
1947
|
it("accepts each lifecycle subject the bridge names", () => {
|
|
1903
1948
|
for (const subject of Object.values(BridgeLifecycleSubject)) {
|
|
1904
1949
|
expect(() =>
|
|
@@ -62,6 +62,10 @@ const READY_TIMEOUT_MIN_SECONDS = 1;
|
|
|
62
62
|
|
|
63
63
|
const READY_TIMEOUT_MAX_SECONDS = 3600;
|
|
64
64
|
|
|
65
|
+
const GRACE_MIN_SECONDS = 1;
|
|
66
|
+
|
|
67
|
+
const GRACE_MAX_SECONDS = 600;
|
|
68
|
+
|
|
65
69
|
const RESERVED_MODULE_IDS = new Set([
|
|
66
70
|
"announce",
|
|
67
71
|
"backup",
|
|
@@ -635,6 +639,27 @@ const validateLifecycle = (driver: BridgeDriver) => {
|
|
|
635
639
|
);
|
|
636
640
|
}
|
|
637
641
|
|
|
642
|
+
const graceSeconds: unknown = driver.lifecycle.graceSeconds;
|
|
643
|
+
|
|
644
|
+
if (
|
|
645
|
+
graceSeconds !== undefined
|
|
646
|
+
&& !(
|
|
647
|
+
Number.isInteger(graceSeconds)
|
|
648
|
+
&& (graceSeconds as number) >= GRACE_MIN_SECONDS
|
|
649
|
+
&& (graceSeconds as number) <= GRACE_MAX_SECONDS
|
|
650
|
+
)
|
|
651
|
+
) {
|
|
652
|
+
fail(
|
|
653
|
+
`the lifecycle module declares a grace of ${String(graceSeconds)} seconds — it must be a whole number from ${GRACE_MIN_SECONDS} to ${GRACE_MAX_SECONDS}`,
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const stableLine: unknown = driver.lifecycle.stableLine;
|
|
658
|
+
|
|
659
|
+
if (stableLine !== undefined && !(stableLine instanceof RegExp)) {
|
|
660
|
+
fail("the lifecycle module declares a stable line that is not a regular expression");
|
|
661
|
+
}
|
|
662
|
+
|
|
638
663
|
const subject: unknown = driver.lifecycle.subject;
|
|
639
664
|
|
|
640
665
|
if (subject !== undefined && !isBridgeLifecycleSubject(subject)) {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import {
|
|
3
|
+
import { BOT_CRASH_EVIDENCE, LOGIN_LINE } from "../bot";
|
|
4
|
+
import { BridgeKind, BridgeRuntime } from "../core";
|
|
4
5
|
import { BridgeLifecycleSubject, BridgeStopSignal } from "../modules";
|
|
5
|
-
import { BridgeCrashPhase, type BridgeCrashRecord } from "../protocol";
|
|
6
|
+
import { BridgeCrashCause, BridgeCrashPhase, type BridgeCrashRecord } from "../protocol";
|
|
7
|
+
import { createEventRing } from "./bridgeEvents";
|
|
6
8
|
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
7
9
|
import { createBridgeRedaction, REDACTED } from "./bridgeRedaction";
|
|
8
10
|
import { readProcessGroup } from "./bridgeSignal";
|
|
9
|
-
import { type BridgeSupervisor, createSupervisor } from "./bridgeSupervisor";
|
|
11
|
+
import { type BridgeSupervisor, createSupervisor, type SupervisorOptions } from "./bridgeSupervisor";
|
|
10
12
|
|
|
11
13
|
const CHILD_PATTERN = /child=(?<pid>\d+)/;
|
|
12
14
|
|
|
@@ -1277,3 +1279,161 @@ describe("the lifecycle subject", () => {
|
|
|
1277
1279
|
await expect(supervisor.start()).rejects.toThrow('the bot environment names "BAD-KEY"');
|
|
1278
1280
|
});
|
|
1279
1281
|
});
|
|
1282
|
+
|
|
1283
|
+
const GRACE_SECONDS = 1;
|
|
1284
|
+
|
|
1285
|
+
const READY_THEN_ALIVE = "echo ready; exec sleep 300";
|
|
1286
|
+
|
|
1287
|
+
const READY_THEN_LOGIN = "echo ready; sleep 0.2; echo 'Logged in as serverk-bot#0001'; exec sleep 300";
|
|
1288
|
+
|
|
1289
|
+
const READY_THEN_TOKEN_CRASH =
|
|
1290
|
+
"echo ready; sleep 0.3; echo 'Error [TokenInvalid]: An invalid token was provided.' >&2; exit 1";
|
|
1291
|
+
|
|
1292
|
+
const graceSupervisor = (script: string, grace: Partial<Pick<SupervisorOptions, "graceSeconds" | "stableLine">>) => {
|
|
1293
|
+
const records: BridgeCrashRecord[] = [];
|
|
1294
|
+
const causes: BridgeCrashCause[] = [];
|
|
1295
|
+
|
|
1296
|
+
let stables = 0;
|
|
1297
|
+
|
|
1298
|
+
const supervisor: BridgeSupervisor = createSupervisor({
|
|
1299
|
+
cwd: tmpdir(),
|
|
1300
|
+
uid: process.getuid?.() ?? 0,
|
|
1301
|
+
gid: process.getgid?.() ?? 0,
|
|
1302
|
+
stopTimeoutSeconds: 1,
|
|
1303
|
+
ready: READY,
|
|
1304
|
+
...grace,
|
|
1305
|
+
onLog: silent,
|
|
1306
|
+
resolveCommand: async () => {
|
|
1307
|
+
return [
|
|
1308
|
+
"bash",
|
|
1309
|
+
"-c",
|
|
1310
|
+
script,
|
|
1311
|
+
];
|
|
1312
|
+
},
|
|
1313
|
+
onReady() {
|
|
1314
|
+
ring.discard();
|
|
1315
|
+
},
|
|
1316
|
+
onStable() {
|
|
1317
|
+
stables += 1;
|
|
1318
|
+
},
|
|
1319
|
+
onExit(record) {
|
|
1320
|
+
records.push(record);
|
|
1321
|
+
causes.push(ring.crash(record));
|
|
1322
|
+
},
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
const ring = createEventRing(
|
|
1326
|
+
{
|
|
1327
|
+
kind: BridgeKind.Events,
|
|
1328
|
+
patterns: [
|
|
1329
|
+
...BOT_CRASH_EVIDENCE,
|
|
1330
|
+
],
|
|
1331
|
+
},
|
|
1332
|
+
supervisor,
|
|
1333
|
+
);
|
|
1334
|
+
|
|
1335
|
+
return {
|
|
1336
|
+
supervisor,
|
|
1337
|
+
records,
|
|
1338
|
+
causes,
|
|
1339
|
+
stables: () => stables,
|
|
1340
|
+
};
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
describe("the grace window", () => {
|
|
1344
|
+
it("marks a driver from an older bridge that declares no grace stable at its ready line", async () => {
|
|
1345
|
+
const { supervisor, stables } = graceSupervisor(READY_THEN_ALIVE, {});
|
|
1346
|
+
|
|
1347
|
+
await supervisor.start();
|
|
1348
|
+
|
|
1349
|
+
expect(await waitFor(() => stables() === 1, 5000)).toBe(true);
|
|
1350
|
+
expect(supervisor.runtime).toBe(BridgeRuntime.Running);
|
|
1351
|
+
|
|
1352
|
+
await supervisor.stop();
|
|
1353
|
+
}, 30_000);
|
|
1354
|
+
|
|
1355
|
+
it("holds a bot that printed no login line at booting until the grace passes", async () => {
|
|
1356
|
+
const { supervisor, stables } = graceSupervisor(READY_THEN_ALIVE, {
|
|
1357
|
+
graceSeconds: GRACE_SECONDS,
|
|
1358
|
+
stableLine: LOGIN_LINE,
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
await supervisor.start();
|
|
1362
|
+
|
|
1363
|
+
expect(await waitFor(() => supervisor.runtime === BridgeRuntime.Running, 5000)).toBe(true);
|
|
1364
|
+
|
|
1365
|
+
await Bun.sleep(400);
|
|
1366
|
+
|
|
1367
|
+
expect(stables(), "the bot was stable before the grace passed").toBe(0);
|
|
1368
|
+
expect(await waitFor(() => stables() === 1, 5000)).toBe(true);
|
|
1369
|
+
|
|
1370
|
+
await Bun.sleep(GRACE_SECONDS * 1000);
|
|
1371
|
+
|
|
1372
|
+
expect(stables(), "the bot was reported stable twice").toBe(1);
|
|
1373
|
+
|
|
1374
|
+
await supervisor.stop();
|
|
1375
|
+
}, 30_000);
|
|
1376
|
+
|
|
1377
|
+
it("marks a bot stable at its login line, before the grace passes", async () => {
|
|
1378
|
+
const { supervisor, stables } = graceSupervisor(READY_THEN_LOGIN, {
|
|
1379
|
+
graceSeconds: 30,
|
|
1380
|
+
stableLine: LOGIN_LINE,
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
await supervisor.start();
|
|
1384
|
+
|
|
1385
|
+
expect(await waitFor(() => stables() === 1, 5000)).toBe(true);
|
|
1386
|
+
|
|
1387
|
+
await supervisor.stop();
|
|
1388
|
+
}, 30_000);
|
|
1389
|
+
|
|
1390
|
+
it("counts an exit inside the grace as a boot failure with the cause the bot printed", async () => {
|
|
1391
|
+
const { supervisor, records, causes, stables } = graceSupervisor(READY_THEN_TOKEN_CRASH, {
|
|
1392
|
+
graceSeconds: 30,
|
|
1393
|
+
stableLine: LOGIN_LINE,
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
await supervisor.start();
|
|
1397
|
+
|
|
1398
|
+
expect(await waitFor(() => records.length > 0, 10_000)).toBe(true);
|
|
1399
|
+
expect(records.at(0)?.phase).toBe(BridgeCrashPhase.Booting);
|
|
1400
|
+
expect(records.at(0)?.restarting).toBe(false);
|
|
1401
|
+
expect(causes).toEqual([
|
|
1402
|
+
BridgeCrashCause.TokenInvalid,
|
|
1403
|
+
]);
|
|
1404
|
+
expect(stables()).toBe(0);
|
|
1405
|
+
expect(supervisor.runtime).toBe(BridgeRuntime.Crashed);
|
|
1406
|
+
}, 30_000);
|
|
1407
|
+
|
|
1408
|
+
it("forgets what the install printed once the launcher hands over, so only the bot's own lines name the cause", async () => {
|
|
1409
|
+
const { supervisor, records, causes } = graceSupervisor(
|
|
1410
|
+
"echo 'gyp ERR! stack Error: not found: make'; echo ready; sleep 0.3; exit 1",
|
|
1411
|
+
{
|
|
1412
|
+
graceSeconds: 30,
|
|
1413
|
+
stableLine: LOGIN_LINE,
|
|
1414
|
+
},
|
|
1415
|
+
);
|
|
1416
|
+
|
|
1417
|
+
await supervisor.start();
|
|
1418
|
+
|
|
1419
|
+
expect(await waitFor(() => records.length > 0, 10_000)).toBe(true);
|
|
1420
|
+
expect(records.at(0)?.phase).toBe(BridgeCrashPhase.Booting);
|
|
1421
|
+
expect(causes).toEqual([
|
|
1422
|
+
BridgeCrashCause.Unknown,
|
|
1423
|
+
]);
|
|
1424
|
+
}, 30_000);
|
|
1425
|
+
|
|
1426
|
+
it("restarts the same exit when it comes after the grace", async () => {
|
|
1427
|
+
const { supervisor, records } = graceSupervisor("echo ready; sleep 0.5; exit 1", {
|
|
1428
|
+
graceSeconds: 0.2,
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
await supervisor.start();
|
|
1432
|
+
|
|
1433
|
+
expect(await waitFor(() => records.length > 0, 10_000)).toBe(true);
|
|
1434
|
+
expect(records.at(0)?.phase).toBe(BridgeCrashPhase.Running);
|
|
1435
|
+
expect(records.at(0)?.restarting).toBe(true);
|
|
1436
|
+
|
|
1437
|
+
await supervisor.stop();
|
|
1438
|
+
}, 30_000);
|
|
1439
|
+
});
|
|
@@ -46,6 +46,8 @@ export interface SupervisorOptions {
|
|
|
46
46
|
subject?: BridgeLifecycleSubject;
|
|
47
47
|
ready?: RegExp;
|
|
48
48
|
failed?: RegExp;
|
|
49
|
+
graceSeconds?: number;
|
|
50
|
+
stableLine?: RegExp;
|
|
49
51
|
resolveCommand: () => Promise<string[]>;
|
|
50
52
|
resolveEnvironment?: () => Promise<Record<string, string>>;
|
|
51
53
|
redact?: (text: string) => string;
|
|
@@ -208,6 +210,11 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
208
210
|
|
|
209
211
|
const readyTimeoutMs = (options.readyTimeoutSeconds ?? options.stopTimeoutSeconds * READY_TIMEOUT_STOP_FACTOR) * 1000;
|
|
210
212
|
|
|
213
|
+
const graceMs =
|
|
214
|
+
options.graceSeconds === undefined ? (options.stableAfterMs ?? STABLE_UPTIME_MS) : options.graceSeconds * 1000;
|
|
215
|
+
|
|
216
|
+
const deferred = options.graceSeconds !== undefined || options.stableLine !== undefined;
|
|
217
|
+
|
|
211
218
|
const awaitingReady = () => {
|
|
212
219
|
return options.readyTimeoutSeconds !== undefined && runtime === BridgeRuntime.Starting;
|
|
213
220
|
};
|
|
@@ -241,6 +248,36 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
241
248
|
options.onStable?.();
|
|
242
249
|
};
|
|
243
250
|
|
|
251
|
+
const settle = (launchStamp: number) => {
|
|
252
|
+
if (!deferred) {
|
|
253
|
+
markStable();
|
|
254
|
+
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const current = () => launchStamp === launchedAt && child !== null;
|
|
259
|
+
|
|
260
|
+
if (stableTimer) {
|
|
261
|
+
clearTimeout(stableTimer);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
stableTimer = setTimeout(() => {
|
|
265
|
+
if (current()) {
|
|
266
|
+
markStable();
|
|
267
|
+
}
|
|
268
|
+
}, graceMs);
|
|
269
|
+
|
|
270
|
+
if (!options.stableLine) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
void watch(options.stableLine, graceMs).then((match) => {
|
|
275
|
+
if (match && current()) {
|
|
276
|
+
markStable();
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
};
|
|
280
|
+
|
|
244
281
|
const emit = (raw: SupervisorLine) => {
|
|
245
282
|
const entry = options.redact
|
|
246
283
|
? {
|
|
@@ -491,6 +528,10 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
491
528
|
if (!options.ready) {
|
|
492
529
|
runtime = BridgeRuntime.Running;
|
|
493
530
|
|
|
531
|
+
if (deferred) {
|
|
532
|
+
settle(launchStamp);
|
|
533
|
+
}
|
|
534
|
+
|
|
494
535
|
return;
|
|
495
536
|
}
|
|
496
537
|
|
|
@@ -509,7 +550,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
|
|
|
509
550
|
options.onReady?.();
|
|
510
551
|
}
|
|
511
552
|
|
|
512
|
-
|
|
553
|
+
settle(launchStamp);
|
|
513
554
|
|
|
514
555
|
return;
|
|
515
556
|
}
|
package/src/runtime/serve.ts
CHANGED
|
@@ -98,6 +98,14 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
98
98
|
|
|
99
99
|
const setup = createSetupHandlers(driver, log, redact);
|
|
100
100
|
|
|
101
|
+
const runHook = (hook: string, run: () => Promise<void>) => {
|
|
102
|
+
void run().catch((error: unknown) => {
|
|
103
|
+
log.warn(`lifecycle ${hook} hook failed`, {
|
|
104
|
+
error: errorText(error),
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
101
109
|
const supervisor = createSupervisor({
|
|
102
110
|
cwd: config.cwd,
|
|
103
111
|
uid: config.gameUid,
|
|
@@ -108,6 +116,8 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
108
116
|
subject: driver.lifecycle.subject,
|
|
109
117
|
ready: driver.lifecycle.ready,
|
|
110
118
|
failed: driver.lifecycle.failed,
|
|
119
|
+
graceSeconds: driver.lifecycle.graceSeconds,
|
|
120
|
+
stableLine: driver.lifecycle.stableLine,
|
|
111
121
|
redact,
|
|
112
122
|
onLog: log,
|
|
113
123
|
async resolveCommand() {
|
|
@@ -124,20 +134,31 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
124
134
|
await driver.lifecycle.stop?.(context);
|
|
125
135
|
},
|
|
126
136
|
onReady() {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
ring.discard();
|
|
138
|
+
|
|
139
|
+
runHook("ready", async () => {
|
|
140
|
+
await driver.lifecycle.onReady?.(context);
|
|
131
141
|
});
|
|
132
142
|
},
|
|
133
143
|
onStable() {
|
|
134
144
|
ring.discard();
|
|
145
|
+
|
|
146
|
+
runHook("stable", async () => {
|
|
147
|
+
await driver.lifecycle.onStable?.(context);
|
|
148
|
+
});
|
|
135
149
|
},
|
|
136
150
|
onStopped() {
|
|
137
151
|
ring.discard();
|
|
138
152
|
},
|
|
139
153
|
onExit(record) {
|
|
140
|
-
ring.crash(record);
|
|
154
|
+
const cause = ring.crash(record);
|
|
155
|
+
|
|
156
|
+
runHook("exit", async () => {
|
|
157
|
+
await driver.lifecycle.onExit?.(context, {
|
|
158
|
+
...record,
|
|
159
|
+
cause,
|
|
160
|
+
});
|
|
161
|
+
});
|
|
141
162
|
},
|
|
142
163
|
});
|
|
143
164
|
|