@serverkgg/bridge 0.0.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/LICENSE.md +22 -0
- package/README.md +52 -0
- package/package.json +26 -0
- package/src/cli/compile.ts +75 -0
- package/src/cli/directorySource.ts +48 -0
- package/src/cli/main.ts +44 -0
- package/src/cli/validate.ts +94 -0
- package/src/codecs/index.ts +3 -0
- package/src/codecs/json.ts +42 -0
- package/src/codecs/properties.ts +75 -0
- package/src/codecs/ueIni.ts +235 -0
- package/src/context/bridgeContext.ts +120 -0
- package/src/context/index.ts +1 -0
- package/src/core/bridgeAction.ts +13 -0
- package/src/core/bridgeId.ts +3 -0
- package/src/core/bridgeKind.ts +11 -0
- package/src/core/bridgePort.ts +4 -0
- package/src/core/bridgeRuntime.ts +13 -0
- package/src/core/bridgeSecretError.ts +10 -0
- package/src/core/bridgeStream.ts +4 -0
- package/src/core/bridgeValue.ts +19 -0
- package/src/core/index.ts +8 -0
- package/src/main.ts +5 -0
- package/src/manifest/assets.ts +102 -0
- package/src/manifest/category.ts +10 -0
- package/src/manifest/checks.ts +50 -0
- package/src/manifest/dimensions.ts +106 -0
- package/src/manifest/guide.ts +36 -0
- package/src/manifest/image.ts +1 -0
- package/src/manifest/index.ts +9 -0
- package/src/manifest/localized.ts +8 -0
- package/src/manifest/manifest.ts +170 -0
- package/src/manifest/source.ts +89 -0
- package/src/modules/bridge.ts +53 -0
- package/src/modules/bridgeDriver.ts +19 -0
- package/src/modules/bridgeModule.ts +114 -0
- package/src/modules/index.ts +3 -0
- package/src/panel/bridgeConfirm.ts +5 -0
- package/src/panel/bridgeControl.ts +7 -0
- package/src/panel/bridgeFormTarget.ts +4 -0
- package/src/panel/bridgeIcon.ts +17 -0
- package/src/panel/bridgeLayout.ts +6 -0
- package/src/panel/bridgePanel.ts +107 -0
- package/src/panel/index.ts +6 -0
- package/src/probes/a2s.ts +170 -0
- package/src/probes/index.ts +1 -0
- package/src/protocol/bridgeConfig.ts +13 -0
- package/src/protocol/bridgeFrame.ts +91 -0
- package/src/protocol/bridgeLine.ts +40 -0
- package/src/protocol/bridgeManifest.ts +19 -0
- package/src/protocol/bridgeVersion.ts +11 -0
- package/src/protocol/index.ts +7 -0
- package/src/runtime/bridgeCodec.ts +78 -0
- package/src/runtime/bridgeConfigReader.ts +119 -0
- package/src/runtime/bridgeContext.ts +96 -0
- package/src/runtime/bridgeDriver.ts +179 -0
- package/src/runtime/bridgeEvents.ts +71 -0
- package/src/runtime/bridgeExec.ts +143 -0
- package/src/runtime/bridgeFiles.ts +412 -0
- package/src/runtime/bridgeLog.ts +15 -0
- package/src/runtime/bridgeNet.ts +59 -0
- package/src/runtime/bridgePath.ts +132 -0
- package/src/runtime/bridgePrivilege.ts +99 -0
- package/src/runtime/bridgeProbe.ts +14 -0
- package/src/runtime/bridgeRegistry.ts +311 -0
- package/src/runtime/bridgeServer.ts +233 -0
- package/src/runtime/bridgeState.ts +40 -0
- package/src/runtime/bridgeSupervisor.ts +498 -0
- package/src/runtime/bridgeZip.ts +122 -0
- package/src/runtime/index.ts +18 -0
- package/src/runtime/serve.ts +294 -0
- package/src/utils/glob.ts +62 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import type { BridgeCommandOptions, BridgeReply } from "../context";
|
|
2
|
+
import { BridgeRuntime, BridgeStream } from "../core";
|
|
3
|
+
import type { BridgeLogger } from "./bridgeLog";
|
|
4
|
+
import { errorText } from "./bridgeLog";
|
|
5
|
+
import { settleUid, unprivilegedArgv } from "./bridgePrivilege";
|
|
6
|
+
|
|
7
|
+
const TAIL_LIMIT = 500;
|
|
8
|
+
|
|
9
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 10_000;
|
|
10
|
+
|
|
11
|
+
const KILL_GRACE_MS = 5000;
|
|
12
|
+
|
|
13
|
+
const RESTART_DELAY_MS = 2000;
|
|
14
|
+
|
|
15
|
+
const RESTART_WINDOW_MS = 300_000;
|
|
16
|
+
|
|
17
|
+
const RESTART_LIMIT = 5;
|
|
18
|
+
|
|
19
|
+
const GAME_OOM_SCORE_ADJ = 500;
|
|
20
|
+
|
|
21
|
+
export interface SupervisorLine {
|
|
22
|
+
stream: BridgeStream;
|
|
23
|
+
line: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SupervisorOptions {
|
|
27
|
+
cwd: string;
|
|
28
|
+
uid: number;
|
|
29
|
+
gid: number;
|
|
30
|
+
stopTimeoutSeconds: number;
|
|
31
|
+
ready?: RegExp;
|
|
32
|
+
resolveCommand: () => Promise<string[]>;
|
|
33
|
+
gracefulStop?: () => Promise<void>;
|
|
34
|
+
onReady?: () => void;
|
|
35
|
+
onLog: BridgeLogger;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type LineWatcher = (entry: SupervisorLine) => void;
|
|
39
|
+
|
|
40
|
+
const preferOomVictim = async (pid: number) => {
|
|
41
|
+
try {
|
|
42
|
+
await Bun.write(`/proc/${pid}/oom_score_adj`, `${GAME_OOM_SCORE_ADJ}\n`);
|
|
43
|
+
} catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const SIGNAL_NUMBERS: Record<string, number> = {
|
|
49
|
+
SIGHUP: 1,
|
|
50
|
+
SIGINT: 2,
|
|
51
|
+
SIGQUIT: 3,
|
|
52
|
+
SIGABRT: 6,
|
|
53
|
+
SIGKILL: 9,
|
|
54
|
+
SIGSEGV: 11,
|
|
55
|
+
SIGTERM: 15,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const EARLY_OUTPUT_LIMIT = 2000;
|
|
59
|
+
|
|
60
|
+
const earlyOutput = async (spawned: { stdout: unknown; stderr: unknown }) => {
|
|
61
|
+
const read = async (stream: unknown) => {
|
|
62
|
+
if (!(stream instanceof ReadableStream)) {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return await new Response(stream).text().catch(() => "");
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const [stdout, stderr] = await Promise.all([
|
|
70
|
+
read(spawned.stdout),
|
|
71
|
+
read(spawned.stderr),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
const text = `${stdout}\n${stderr}`.trim();
|
|
75
|
+
|
|
76
|
+
return text.length === 0 ? "" : ` — ${text.slice(-EARLY_OUTPUT_LIMIT)}`;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const exitCodeOf = (code: number | null, signal: number | string | null) => {
|
|
80
|
+
if (code !== null) {
|
|
81
|
+
return code;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (typeof signal === "number") {
|
|
85
|
+
return 128 + signal;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (typeof signal === "string") {
|
|
89
|
+
return 128 + (SIGNAL_NUMBERS[signal] ?? 0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return 0;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const splitLines = (buffer: string, chunk: string) => {
|
|
96
|
+
const merged = buffer + chunk;
|
|
97
|
+
const parts = merged.split("\n");
|
|
98
|
+
const rest = parts.pop() ?? "";
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
lines: parts.map((line) => line.replace(/\r$/, "")),
|
|
102
|
+
rest,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const createSupervisor = (options: SupervisorOptions) => {
|
|
107
|
+
const tail: SupervisorLine[] = [];
|
|
108
|
+
const watchers = new Set<LineWatcher>();
|
|
109
|
+
const restartStamps: number[] = [];
|
|
110
|
+
|
|
111
|
+
let child: Bun.Subprocess<"pipe", "pipe", "pipe"> | null = null;
|
|
112
|
+
let runtime: BridgeRuntime = BridgeRuntime.Stopped;
|
|
113
|
+
let shouldRun = false;
|
|
114
|
+
let breaker = false;
|
|
115
|
+
let exitCode: number | null = null;
|
|
116
|
+
let restarts = 0;
|
|
117
|
+
let queue: Promise<unknown> = Promise.resolve();
|
|
118
|
+
|
|
119
|
+
const emit = (entry: SupervisorLine) => {
|
|
120
|
+
if (entry.stream === BridgeStream.Stderr) {
|
|
121
|
+
process.stderr.write(`${entry.line}\n`);
|
|
122
|
+
} else {
|
|
123
|
+
process.stdout.write(`${entry.line}\n`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
tail.push(entry);
|
|
127
|
+
|
|
128
|
+
while (tail.length > TAIL_LIMIT) {
|
|
129
|
+
tail.shift();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const watcher of watchers) {
|
|
133
|
+
watcher(entry);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const pump = async (stream: ReadableStream<Uint8Array>, kind: BridgeStream) => {
|
|
138
|
+
const decoder = new TextDecoder();
|
|
139
|
+
const reader = stream.getReader();
|
|
140
|
+
|
|
141
|
+
let buffer = "";
|
|
142
|
+
|
|
143
|
+
while (true) {
|
|
144
|
+
const { done, value } = await reader.read();
|
|
145
|
+
|
|
146
|
+
if (done) {
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const split = splitLines(
|
|
151
|
+
buffer,
|
|
152
|
+
decoder.decode(value, {
|
|
153
|
+
stream: true,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
buffer = split.rest;
|
|
158
|
+
|
|
159
|
+
for (const line of split.lines) {
|
|
160
|
+
emit({
|
|
161
|
+
stream: kind,
|
|
162
|
+
line,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (buffer.length > 0) {
|
|
168
|
+
emit({
|
|
169
|
+
stream: kind,
|
|
170
|
+
line: buffer,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const watch = (pattern: RegExp, timeoutMs: number) => {
|
|
176
|
+
return new Promise<RegExpMatchArray | null>((resolve) => {
|
|
177
|
+
const timer = setTimeout(() => {
|
|
178
|
+
watchers.delete(watcher);
|
|
179
|
+
resolve(null);
|
|
180
|
+
}, timeoutMs);
|
|
181
|
+
|
|
182
|
+
const watcher: LineWatcher = (entry) => {
|
|
183
|
+
const match = entry.line.match(pattern);
|
|
184
|
+
|
|
185
|
+
if (!match) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
watchers.delete(watcher);
|
|
191
|
+
resolve(match);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
watchers.add(watcher);
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const tripped = () => {
|
|
199
|
+
const cutoff = performance.now() - RESTART_WINDOW_MS;
|
|
200
|
+
|
|
201
|
+
while (restartStamps.length > 0 && (restartStamps[0] ?? 0) < cutoff) {
|
|
202
|
+
restartStamps.shift();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return restartStamps.length >= RESTART_LIMIT;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const launch = async () => {
|
|
209
|
+
const command = await options.resolveCommand();
|
|
210
|
+
const [executable, ...rest] = command;
|
|
211
|
+
|
|
212
|
+
if (!executable) {
|
|
213
|
+
throw new Error("the game start command is empty");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const argv = [
|
|
217
|
+
executable,
|
|
218
|
+
...rest,
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const { argv: spawnArgv } = await unprivilegedArgv(argv, options.uid, options.gid);
|
|
222
|
+
const spawned = Bun.spawn(spawnArgv, {
|
|
223
|
+
cwd: options.cwd,
|
|
224
|
+
stderr: "pipe",
|
|
225
|
+
stdin: "pipe",
|
|
226
|
+
stdout: "pipe",
|
|
227
|
+
onExit(_subprocess, code, signal) {
|
|
228
|
+
handleExit(exitCodeOf(code, signal));
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const observed = await settleUid(spawned.pid, options.uid);
|
|
233
|
+
|
|
234
|
+
if (observed !== options.uid) {
|
|
235
|
+
const finished = await Promise.race([
|
|
236
|
+
spawned.exited.then(() => true),
|
|
237
|
+
Bun.sleep(KILL_GRACE_MS).then(() => false),
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
if (observed === null && finished) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`the game exited immediately with code ${exitCodeOf(spawned.exitCode, spawned.signalCode)}${await earlyOutput(spawned)}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
spawned.kill("SIGKILL");
|
|
247
|
+
|
|
248
|
+
await spawned.exited;
|
|
249
|
+
|
|
250
|
+
throw new Error(
|
|
251
|
+
`the game process runs as uid ${String(observed)}, expected ${options.uid} — refusing to run it as the bridge`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await preferOomVictim(spawned.pid);
|
|
256
|
+
|
|
257
|
+
child = spawned;
|
|
258
|
+
exitCode = null;
|
|
259
|
+
|
|
260
|
+
void pump(spawned.stdout, BridgeStream.Stdout);
|
|
261
|
+
void pump(spawned.stderr, BridgeStream.Stderr);
|
|
262
|
+
|
|
263
|
+
options.onLog("game started", {
|
|
264
|
+
pid: spawned.pid,
|
|
265
|
+
uid: options.uid,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (!options.ready) {
|
|
269
|
+
runtime = BridgeRuntime.Running;
|
|
270
|
+
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
runtime = BridgeRuntime.Starting;
|
|
275
|
+
|
|
276
|
+
void watch(options.ready, options.stopTimeoutSeconds * 1000 * 10).then((match) => {
|
|
277
|
+
if (match && runtime === BridgeRuntime.Starting) {
|
|
278
|
+
runtime = BridgeRuntime.Running;
|
|
279
|
+
|
|
280
|
+
options.onLog("game ready");
|
|
281
|
+
options.onReady?.();
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const restart = async () => {
|
|
287
|
+
await Bun.sleep(RESTART_DELAY_MS);
|
|
288
|
+
|
|
289
|
+
if (!shouldRun || breaker || child !== null) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
await launch();
|
|
295
|
+
} catch (error) {
|
|
296
|
+
runtime = BridgeRuntime.Crashed;
|
|
297
|
+
|
|
298
|
+
options.onLog("game failed to restart", {
|
|
299
|
+
error: errorText(error),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
function handleExit(code: number) {
|
|
305
|
+
child = null;
|
|
306
|
+
exitCode = code;
|
|
307
|
+
|
|
308
|
+
if (!shouldRun) {
|
|
309
|
+
runtime = BridgeRuntime.Stopped;
|
|
310
|
+
|
|
311
|
+
options.onLog("game stopped", {
|
|
312
|
+
code,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
restarts += 1;
|
|
319
|
+
restartStamps.push(performance.now());
|
|
320
|
+
|
|
321
|
+
if (tripped()) {
|
|
322
|
+
breaker = true;
|
|
323
|
+
runtime = BridgeRuntime.Crashed;
|
|
324
|
+
|
|
325
|
+
options.onLog("game crash loop detected, not restarting", {
|
|
326
|
+
code,
|
|
327
|
+
restarts,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
runtime = BridgeRuntime.Starting;
|
|
334
|
+
|
|
335
|
+
options.onLog("game exited, restarting", {
|
|
336
|
+
code,
|
|
337
|
+
restarts,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
void restart();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const serialize = <Result>(run: () => Promise<Result>): Promise<Result> => {
|
|
344
|
+
const next = queue.then(run, run);
|
|
345
|
+
|
|
346
|
+
queue = next.then(
|
|
347
|
+
() => undefined,
|
|
348
|
+
() => undefined,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
return next;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const writeLine = async (input: string) => {
|
|
355
|
+
if (!child) {
|
|
356
|
+
throw new Error("the game is not running");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
child.stdin.write(`${input}\n`);
|
|
360
|
+
|
|
361
|
+
await child.stdin.flush();
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
get runtime() {
|
|
366
|
+
return runtime;
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
get running() {
|
|
370
|
+
return child !== null;
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
get exitCode() {
|
|
374
|
+
return exitCode;
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
get restarts() {
|
|
378
|
+
return restarts;
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
async start() {
|
|
382
|
+
shouldRun = true;
|
|
383
|
+
breaker = false;
|
|
384
|
+
restartStamps.length = 0;
|
|
385
|
+
|
|
386
|
+
if (child) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
await launch();
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
async stop() {
|
|
394
|
+
shouldRun = false;
|
|
395
|
+
|
|
396
|
+
if (!child) {
|
|
397
|
+
runtime = BridgeRuntime.Stopped;
|
|
398
|
+
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const active = child;
|
|
403
|
+
|
|
404
|
+
runtime = BridgeRuntime.Stopping;
|
|
405
|
+
|
|
406
|
+
if (options.gracefulStop) {
|
|
407
|
+
const graceful = options.gracefulStop().catch((error: unknown) => {
|
|
408
|
+
options.onLog("graceful stop failed", {
|
|
409
|
+
error: errorText(error),
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
await Promise.race([
|
|
414
|
+
graceful,
|
|
415
|
+
active.exited,
|
|
416
|
+
]);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const settled = await Promise.race([
|
|
420
|
+
active.exited,
|
|
421
|
+
Bun.sleep(options.stopTimeoutSeconds * 1000).then(() => null),
|
|
422
|
+
]);
|
|
423
|
+
|
|
424
|
+
if (settled === null) {
|
|
425
|
+
active.kill("SIGTERM");
|
|
426
|
+
|
|
427
|
+
const afterTerm = await Promise.race([
|
|
428
|
+
active.exited,
|
|
429
|
+
Bun.sleep(KILL_GRACE_MS).then(() => null),
|
|
430
|
+
]);
|
|
431
|
+
|
|
432
|
+
if (afterTerm === null) {
|
|
433
|
+
active.kill("SIGKILL");
|
|
434
|
+
|
|
435
|
+
await active.exited;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
runtime = BridgeRuntime.Stopped;
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
async command(input: string, commandOptions: BridgeCommandOptions = {}): Promise<BridgeReply> {
|
|
443
|
+
return await serialize(async () => {
|
|
444
|
+
const pattern = commandOptions.expect;
|
|
445
|
+
|
|
446
|
+
if (!pattern) {
|
|
447
|
+
await writeLine(input);
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
sent: input,
|
|
451
|
+
line: null,
|
|
452
|
+
groups: {},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const pending = watch(pattern, commandOptions.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);
|
|
457
|
+
|
|
458
|
+
await writeLine(input);
|
|
459
|
+
|
|
460
|
+
const match = await pending;
|
|
461
|
+
|
|
462
|
+
if (!match) {
|
|
463
|
+
throw new Error(`"${input}" produced no reply matching ${String(pattern)}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
sent: input,
|
|
468
|
+
line: match[0],
|
|
469
|
+
groups: {
|
|
470
|
+
...match.groups,
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
tail(lines: number) {
|
|
477
|
+
return tail.slice(-lines).map((entry) => entry.line);
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
watch(pattern: RegExp, timeoutMs: number) {
|
|
481
|
+
return watch(pattern, timeoutMs);
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
subscribe(watcher: LineWatcher) {
|
|
485
|
+
watchers.add(watcher);
|
|
486
|
+
|
|
487
|
+
return () => {
|
|
488
|
+
watchers.delete(watcher);
|
|
489
|
+
};
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
setRuntime(next: BridgeRuntime) {
|
|
493
|
+
runtime = next;
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
export type BridgeSupervisor = ReturnType<typeof createSupervisor>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { inflateRawSync } from "node:zlib";
|
|
2
|
+
|
|
3
|
+
const EOCD_SIGNATURE = 0x06_05_4b_50;
|
|
4
|
+
|
|
5
|
+
const CENTRAL_SIGNATURE = 0x02_01_4b_50;
|
|
6
|
+
|
|
7
|
+
const LOCAL_SIGNATURE = 0x04_03_4b_50;
|
|
8
|
+
|
|
9
|
+
const EOCD_MIN_LENGTH = 22;
|
|
10
|
+
|
|
11
|
+
const EOCD_SEARCH_LIMIT = 65_557;
|
|
12
|
+
|
|
13
|
+
const LOCAL_HEADER_LENGTH = 30;
|
|
14
|
+
|
|
15
|
+
const STORED = 0;
|
|
16
|
+
|
|
17
|
+
const DEFLATED = 8;
|
|
18
|
+
|
|
19
|
+
const ENCRYPTED_FLAG = 0x1;
|
|
20
|
+
|
|
21
|
+
const MAX_ENTRIES = 2048;
|
|
22
|
+
|
|
23
|
+
const MAX_ENTRY_BYTES = 128 * 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
export interface ZipEntry {
|
|
26
|
+
name: string;
|
|
27
|
+
data: Uint8Array;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const findEndOfCentralDirectory = (buffer: Buffer) => {
|
|
31
|
+
const start = Math.max(0, buffer.length - EOCD_SEARCH_LIMIT);
|
|
32
|
+
|
|
33
|
+
for (let offset = buffer.length - EOCD_MIN_LENGTH; offset >= start; offset--) {
|
|
34
|
+
if (buffer.readUInt32LE(offset) === EOCD_SIGNATURE) {
|
|
35
|
+
return offset;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return -1;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const readZip = (buffer: Buffer): ZipEntry[] => {
|
|
43
|
+
const eocd = findEndOfCentralDirectory(buffer);
|
|
44
|
+
|
|
45
|
+
if (eocd === -1) {
|
|
46
|
+
throw new Error("archive has no zip central directory");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const total = buffer.readUInt16LE(eocd + 10);
|
|
50
|
+
const directoryOffset = buffer.readUInt32LE(eocd + 16);
|
|
51
|
+
|
|
52
|
+
if (total > MAX_ENTRIES) {
|
|
53
|
+
throw new Error(`archive has too many entries (${total})`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const entries: ZipEntry[] = [];
|
|
57
|
+
|
|
58
|
+
let cursor = directoryOffset;
|
|
59
|
+
|
|
60
|
+
for (let index = 0; index < total; index++) {
|
|
61
|
+
if (cursor + 46 > buffer.length || buffer.readUInt32LE(cursor) !== CENTRAL_SIGNATURE) {
|
|
62
|
+
throw new Error("archive central directory is corrupt");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const flags = buffer.readUInt16LE(cursor + 8);
|
|
66
|
+
const method = buffer.readUInt16LE(cursor + 10);
|
|
67
|
+
const compressedSize = buffer.readUInt32LE(cursor + 20);
|
|
68
|
+
const uncompressedSize = buffer.readUInt32LE(cursor + 24);
|
|
69
|
+
const nameLength = buffer.readUInt16LE(cursor + 28);
|
|
70
|
+
const extraLength = buffer.readUInt16LE(cursor + 30);
|
|
71
|
+
const commentLength = buffer.readUInt16LE(cursor + 32);
|
|
72
|
+
const localOffset = buffer.readUInt32LE(cursor + 42);
|
|
73
|
+
const name = buffer.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8");
|
|
74
|
+
|
|
75
|
+
cursor += 46 + nameLength + extraLength + commentLength;
|
|
76
|
+
|
|
77
|
+
if (name.endsWith("/") || (flags & ENCRYPTED_FLAG) !== 0) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (uncompressedSize > MAX_ENTRY_BYTES) {
|
|
82
|
+
throw new Error(`archive entry "${name}" is too large`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (localOffset + LOCAL_HEADER_LENGTH > buffer.length || buffer.readUInt32LE(localOffset) !== LOCAL_SIGNATURE) {
|
|
86
|
+
throw new Error(`archive entry "${name}" has a corrupt local header`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const localNameLength = buffer.readUInt16LE(localOffset + 26);
|
|
90
|
+
const localExtraLength = buffer.readUInt16LE(localOffset + 28);
|
|
91
|
+
const dataOffset = localOffset + LOCAL_HEADER_LENGTH + localNameLength + localExtraLength;
|
|
92
|
+
const compressed = buffer.subarray(dataOffset, dataOffset + compressedSize);
|
|
93
|
+
|
|
94
|
+
if (method === STORED) {
|
|
95
|
+
entries.push({
|
|
96
|
+
name,
|
|
97
|
+
data: compressed,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (method !== DEFLATED) {
|
|
104
|
+
throw new Error(`archive entry "${name}" uses unsupported compression ${method}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const data = inflateRawSync(compressed, {
|
|
108
|
+
maxOutputLength: MAX_ENTRY_BYTES,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (data.length !== uncompressedSize) {
|
|
112
|
+
throw new Error(`archive entry "${name}" did not inflate to its declared size`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
entries.push({
|
|
116
|
+
name,
|
|
117
|
+
data,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return entries;
|
|
122
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "./bridgeCodec";
|
|
2
|
+
export * from "./bridgeConfigReader";
|
|
3
|
+
export * from "./bridgeContext";
|
|
4
|
+
export * from "./bridgeDriver";
|
|
5
|
+
export * from "./bridgeEvents";
|
|
6
|
+
export * from "./bridgeExec";
|
|
7
|
+
export * from "./bridgeFiles";
|
|
8
|
+
export * from "./bridgeLog";
|
|
9
|
+
export * from "./bridgeNet";
|
|
10
|
+
export * from "./bridgePath";
|
|
11
|
+
export * from "./bridgePrivilege";
|
|
12
|
+
export * from "./bridgeProbe";
|
|
13
|
+
export * from "./bridgeRegistry";
|
|
14
|
+
export * from "./bridgeServer";
|
|
15
|
+
export * from "./bridgeState";
|
|
16
|
+
export * from "./bridgeSupervisor";
|
|
17
|
+
export * from "./bridgeZip";
|
|
18
|
+
export * from "./serve";
|