@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,14 @@
|
|
|
1
|
+
import type { BridgeProbes } from "../context";
|
|
2
|
+
import { probeA2s } from "../probes";
|
|
3
|
+
|
|
4
|
+
const PROBE_HOST = "127.0.0.1";
|
|
5
|
+
|
|
6
|
+
const PROBE_TIMEOUT_MS = 5000;
|
|
7
|
+
|
|
8
|
+
export const createBridgeProbes = (): BridgeProbes => {
|
|
9
|
+
return {
|
|
10
|
+
async a2s(port) {
|
|
11
|
+
return await probeA2s(PROBE_HOST, port, PROBE_TIMEOUT_MS);
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import type { BridgeContext } from "../context";
|
|
2
|
+
import { BridgeAction, BridgeKind, type BridgeRow, type BridgeValues } from "../core";
|
|
3
|
+
import type { BridgeDriver, BridgePanelModule } from "../modules";
|
|
4
|
+
import { BRIDGE_VERSION, type BridgeManifest, type BridgeModuleManifest } from "../protocol";
|
|
5
|
+
import { BridgeDriverError } from "./bridgeDriver";
|
|
6
|
+
import type { BridgeEventRing } from "./bridgeEvents";
|
|
7
|
+
import type { BridgeSupervisor } from "./bridgeSupervisor";
|
|
8
|
+
|
|
9
|
+
interface CacheEntry {
|
|
10
|
+
value: unknown;
|
|
11
|
+
expiresAt: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const actionsOf = (module: BridgePanelModule) => {
|
|
15
|
+
switch (module.kind) {
|
|
16
|
+
case BridgeKind.Options: {
|
|
17
|
+
return [
|
|
18
|
+
BridgeAction.List,
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
case BridgeKind.Settings: {
|
|
23
|
+
return [
|
|
24
|
+
BridgeAction.Read,
|
|
25
|
+
...(module.write
|
|
26
|
+
? [
|
|
27
|
+
BridgeAction.Write,
|
|
28
|
+
]
|
|
29
|
+
: []),
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
case BridgeKind.Collection: {
|
|
34
|
+
return [
|
|
35
|
+
BridgeAction.List,
|
|
36
|
+
...(module.add
|
|
37
|
+
? [
|
|
38
|
+
BridgeAction.Add,
|
|
39
|
+
]
|
|
40
|
+
: []),
|
|
41
|
+
...Object.keys(module.actions ?? {}),
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case BridgeKind.Catalog: {
|
|
46
|
+
return [
|
|
47
|
+
BridgeAction.Install,
|
|
48
|
+
BridgeAction.Installed,
|
|
49
|
+
BridgeAction.Remove,
|
|
50
|
+
BridgeAction.Search,
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const requiresRunning = (module: BridgePanelModule) => {
|
|
57
|
+
return module.kind === BridgeKind.Collection ? (module.requiresRunning ?? false) : false;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const refreshOf = (module: BridgePanelModule) => {
|
|
61
|
+
return module.kind === BridgeKind.Collection ? (module.refreshSeconds ?? null) : null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const dependsOf = (module: BridgePanelModule) => {
|
|
65
|
+
return module.kind === BridgeKind.Options ? (module.dependsOn ?? []) : [];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const pageSizeOf = (module: BridgePanelModule) => {
|
|
69
|
+
return module.kind === BridgeKind.Catalog ? (module.pageSize ?? null) : null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const withVariables = (context: BridgeContext, overlay: Record<string, string>): BridgeContext => {
|
|
73
|
+
return {
|
|
74
|
+
get server() {
|
|
75
|
+
return context.server;
|
|
76
|
+
},
|
|
77
|
+
variable(key) {
|
|
78
|
+
return Object.hasOwn(overlay, key) ? (overlay[key] ?? null) : context.variable(key);
|
|
79
|
+
},
|
|
80
|
+
secret(key) {
|
|
81
|
+
return context.secret(key);
|
|
82
|
+
},
|
|
83
|
+
port(key) {
|
|
84
|
+
return context.port(key);
|
|
85
|
+
},
|
|
86
|
+
files: context.files,
|
|
87
|
+
codec: context.codec,
|
|
88
|
+
net: context.net,
|
|
89
|
+
probe: context.probe,
|
|
90
|
+
command(input, options) {
|
|
91
|
+
return context.command(input, options);
|
|
92
|
+
},
|
|
93
|
+
exec(command, options) {
|
|
94
|
+
return context.exec(command, options);
|
|
95
|
+
},
|
|
96
|
+
logs: context.logs,
|
|
97
|
+
emit(event, payload) {
|
|
98
|
+
context.emit(event, payload);
|
|
99
|
+
},
|
|
100
|
+
log(message, fields) {
|
|
101
|
+
context.log(message, fields);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const asString = (args: BridgeValues, key: string) => {
|
|
107
|
+
const value = args[key];
|
|
108
|
+
|
|
109
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
110
|
+
throw new BridgeDriverError(`"${key}" must be a non-empty string`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return value;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const asText = (args: BridgeValues, key: string) => {
|
|
117
|
+
const value = args[key];
|
|
118
|
+
|
|
119
|
+
return typeof value === "string" ? value : "";
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const asPage = (args: BridgeValues) => {
|
|
123
|
+
const value = args.page;
|
|
124
|
+
|
|
125
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const createRegistry = (
|
|
129
|
+
driver: BridgeDriver,
|
|
130
|
+
context: BridgeContext,
|
|
131
|
+
supervisor: BridgeSupervisor,
|
|
132
|
+
ring: BridgeEventRing,
|
|
133
|
+
) => {
|
|
134
|
+
const cache = new Map<string, CacheEntry>();
|
|
135
|
+
const inflight = new Map<string, Promise<unknown>>();
|
|
136
|
+
const modules = driver.modules ?? {};
|
|
137
|
+
|
|
138
|
+
const cached = async (key: string, ttlSeconds: number | undefined, run: () => Promise<unknown>) => {
|
|
139
|
+
if (!ttlSeconds) {
|
|
140
|
+
return await run();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const hit = cache.get(key);
|
|
144
|
+
const stamp = Math.floor(performance.now());
|
|
145
|
+
|
|
146
|
+
if (hit && hit.expiresAt > stamp) {
|
|
147
|
+
return hit.value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const pending = inflight.get(key);
|
|
151
|
+
|
|
152
|
+
if (pending) {
|
|
153
|
+
return await pending;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const promise = run().then((value) => {
|
|
157
|
+
if (!Array.isArray(value) || value.length > 0) {
|
|
158
|
+
cache.set(key, {
|
|
159
|
+
value,
|
|
160
|
+
expiresAt: stamp + ttlSeconds * 1000,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return value;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
inflight.set(key, promise);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
return await promise;
|
|
171
|
+
} finally {
|
|
172
|
+
inflight.delete(key);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const dispatch = async (module: BridgePanelModule, name: string, action: string, args: BridgeValues) => {
|
|
177
|
+
switch (module.kind) {
|
|
178
|
+
case BridgeKind.Options: {
|
|
179
|
+
if (action === BridgeAction.List) {
|
|
180
|
+
const overlay: Record<string, string> = {};
|
|
181
|
+
|
|
182
|
+
for (const variable of dependsOf(module)) {
|
|
183
|
+
const value = args[variable];
|
|
184
|
+
|
|
185
|
+
if (typeof value === "string") {
|
|
186
|
+
overlay[variable] = value;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const scoped = withVariables(context, overlay);
|
|
191
|
+
|
|
192
|
+
const key = [
|
|
193
|
+
name,
|
|
194
|
+
...dependsOf(module).map((variable) => `${variable}=${scoped.variable(variable) ?? ""}`),
|
|
195
|
+
].join("|");
|
|
196
|
+
|
|
197
|
+
return await cached(key, module.ttlSeconds, () => module.list(scoped));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
case BridgeKind.Settings: {
|
|
204
|
+
if (action === BridgeAction.Read) {
|
|
205
|
+
return await module.read(context);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (action === BridgeAction.Write && module.write) {
|
|
209
|
+
await module.write(context, args);
|
|
210
|
+
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
case BridgeKind.Collection: {
|
|
218
|
+
if (action === BridgeAction.List) {
|
|
219
|
+
return await module.list(context);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (action === BridgeAction.Add && module.add) {
|
|
223
|
+
await module.add(context, asString(args, "input"));
|
|
224
|
+
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const rowAction = module.actions?.[action];
|
|
229
|
+
|
|
230
|
+
if (rowAction) {
|
|
231
|
+
const rows = await module.list(context);
|
|
232
|
+
const id = asString(args, "id");
|
|
233
|
+
const row = rows.find((entry) => entry.id === id);
|
|
234
|
+
|
|
235
|
+
if (!row) {
|
|
236
|
+
throw new BridgeDriverError(`module "${name}" has no row "${id}"`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await rowAction(context, row as BridgeRow);
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
case BridgeKind.Catalog: {
|
|
248
|
+
if (action === BridgeAction.Search) {
|
|
249
|
+
return await module.search(context, asText(args, "query"), asPage(args));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (action === BridgeAction.Installed) {
|
|
253
|
+
return await module.installed(context);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (action === BridgeAction.Install) {
|
|
257
|
+
return await module.install(context, asString(args, "id"));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (action === BridgeAction.Remove) {
|
|
261
|
+
await module.remove(context, asString(args, "id"));
|
|
262
|
+
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
throw new BridgeDriverError(`module "${name}" does not support "${action}"`);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
manifest(): BridgeManifest {
|
|
275
|
+
const entries: BridgeModuleManifest[] = Object.entries(modules).map(([id, module]) => {
|
|
276
|
+
return {
|
|
277
|
+
id,
|
|
278
|
+
kind: module.kind,
|
|
279
|
+
requiresRunning: requiresRunning(module),
|
|
280
|
+
refreshSeconds: refreshOf(module),
|
|
281
|
+
pageSize: pageSizeOf(module),
|
|
282
|
+
dependsOn: dependsOf(module),
|
|
283
|
+
actions: actionsOf(module),
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
version: BRIDGE_VERSION,
|
|
289
|
+
panel: driver.panel ?? null,
|
|
290
|
+
modules: entries,
|
|
291
|
+
events: ring.names(),
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
async invoke(name: string, action: string, args: BridgeValues = {}) {
|
|
296
|
+
const module = modules[name];
|
|
297
|
+
|
|
298
|
+
if (!module) {
|
|
299
|
+
throw new BridgeDriverError(`no module named "${name}"`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (requiresRunning(module) && !supervisor.running) {
|
|
303
|
+
throw new BridgeDriverError(`module "${name}" needs the game to be running`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return await dispatch(module, name, action, args);
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export type BridgeRegistry = ReturnType<typeof createRegistry>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { BridgeSecretError, type BridgeValues } from "../core";
|
|
3
|
+
import {
|
|
4
|
+
BRIDGE_FRAME_LIMIT,
|
|
5
|
+
type BridgeCommandResult,
|
|
6
|
+
type BridgeEventsResult,
|
|
7
|
+
type BridgeManifest,
|
|
8
|
+
BridgeMethod,
|
|
9
|
+
type BridgeQuiesceResult,
|
|
10
|
+
type BridgeResponse,
|
|
11
|
+
type BridgeSampleResult,
|
|
12
|
+
type BridgeStatus,
|
|
13
|
+
} from "../protocol";
|
|
14
|
+
import { type BridgeLogger, errorText } from "./bridgeLog";
|
|
15
|
+
|
|
16
|
+
export interface ServerOptions {
|
|
17
|
+
socket: string;
|
|
18
|
+
token: string;
|
|
19
|
+
onStatus: () => BridgeStatus;
|
|
20
|
+
onManifest: () => BridgeManifest;
|
|
21
|
+
onStart: () => Promise<void>;
|
|
22
|
+
onStop: () => Promise<void>;
|
|
23
|
+
onCommand: (input: string, expect: string | undefined, timeoutMs: number | undefined) => Promise<BridgeCommandResult>;
|
|
24
|
+
onInvoke: (module: string, action: string, args: BridgeValues) => Promise<unknown>;
|
|
25
|
+
onEvents: (cursor: number) => BridgeEventsResult;
|
|
26
|
+
onSample: () => Promise<BridgeSampleResult>;
|
|
27
|
+
onQuiesce: () => Promise<BridgeQuiesceResult>;
|
|
28
|
+
onRelease: () => Promise<void>;
|
|
29
|
+
onLog: BridgeLogger;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface IncomingFrame {
|
|
33
|
+
id?: unknown;
|
|
34
|
+
token?: unknown;
|
|
35
|
+
method?: unknown;
|
|
36
|
+
input?: unknown;
|
|
37
|
+
expect?: unknown;
|
|
38
|
+
timeoutMs?: unknown;
|
|
39
|
+
module?: unknown;
|
|
40
|
+
action?: unknown;
|
|
41
|
+
args?: unknown;
|
|
42
|
+
cursor?: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const matches = (left: string, right: string) => {
|
|
46
|
+
const a = Buffer.from(left);
|
|
47
|
+
const b = Buffer.from(right);
|
|
48
|
+
|
|
49
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const encode = (response: BridgeResponse) => {
|
|
53
|
+
return `${JSON.stringify(response)}\n`;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const asArgs = (value: unknown): BridgeValues => {
|
|
57
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const out: BridgeValues = {};
|
|
62
|
+
|
|
63
|
+
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
64
|
+
if (entry === null || typeof entry === "boolean" || typeof entry === "number" || typeof entry === "string") {
|
|
65
|
+
out[key] = entry;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return out;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const createServer = (options: ServerOptions) => {
|
|
73
|
+
const handle = async (method: string, request: IncomingFrame): Promise<unknown> => {
|
|
74
|
+
switch (method) {
|
|
75
|
+
case BridgeMethod.Status: {
|
|
76
|
+
return options.onStatus();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
case BridgeMethod.Manifest: {
|
|
80
|
+
return options.onManifest();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case BridgeMethod.Start: {
|
|
84
|
+
await options.onStart();
|
|
85
|
+
|
|
86
|
+
return options.onStatus();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case BridgeMethod.Stop: {
|
|
90
|
+
await options.onStop();
|
|
91
|
+
|
|
92
|
+
return options.onStatus();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case BridgeMethod.Command: {
|
|
96
|
+
return await options.onCommand(
|
|
97
|
+
String(request.input ?? ""),
|
|
98
|
+
typeof request.expect === "string" ? request.expect : undefined,
|
|
99
|
+
typeof request.timeoutMs === "number" ? request.timeoutMs : undefined,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
case BridgeMethod.Invoke: {
|
|
104
|
+
return await options.onInvoke(String(request.module ?? ""), String(request.action ?? ""), asArgs(request.args));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case BridgeMethod.Events: {
|
|
108
|
+
return options.onEvents(typeof request.cursor === "number" ? request.cursor : 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
case BridgeMethod.Sample: {
|
|
112
|
+
return await options.onSample();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case BridgeMethod.Quiesce: {
|
|
116
|
+
return await options.onQuiesce();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case BridgeMethod.Release: {
|
|
120
|
+
await options.onRelease();
|
|
121
|
+
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
default: {
|
|
126
|
+
throw new Error(`unknown method "${method}"`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return Bun.listen<{
|
|
132
|
+
buffer: string;
|
|
133
|
+
}>({
|
|
134
|
+
unix: options.socket,
|
|
135
|
+
socket: {
|
|
136
|
+
open(socket) {
|
|
137
|
+
socket.data = {
|
|
138
|
+
buffer: "",
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
data(socket, chunk) {
|
|
143
|
+
socket.data.buffer += chunk.toString();
|
|
144
|
+
|
|
145
|
+
if (socket.data.buffer.length > BRIDGE_FRAME_LIMIT) {
|
|
146
|
+
socket.data.buffer = "";
|
|
147
|
+
socket.write(
|
|
148
|
+
encode({
|
|
149
|
+
id: null,
|
|
150
|
+
ok: false,
|
|
151
|
+
error: "request is too large",
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const frames = socket.data.buffer.split("\n");
|
|
159
|
+
|
|
160
|
+
socket.data.buffer = frames.pop() ?? "";
|
|
161
|
+
|
|
162
|
+
for (const frame of frames) {
|
|
163
|
+
if (frame.trim().length === 0) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let request: IncomingFrame;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
request = JSON.parse(frame) as IncomingFrame;
|
|
171
|
+
} catch {
|
|
172
|
+
socket.write(
|
|
173
|
+
encode({
|
|
174
|
+
id: null,
|
|
175
|
+
ok: false,
|
|
176
|
+
error: "request is not valid json",
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const id = typeof request.id === "number" ? request.id : null;
|
|
184
|
+
|
|
185
|
+
if (typeof request.token !== "string" || !matches(request.token, options.token)) {
|
|
186
|
+
socket.write(
|
|
187
|
+
encode({
|
|
188
|
+
id,
|
|
189
|
+
ok: false,
|
|
190
|
+
error: "unauthorized",
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
void handle(String(request.method ?? ""), request)
|
|
198
|
+
.then((result) => {
|
|
199
|
+
socket.write(
|
|
200
|
+
encode({
|
|
201
|
+
id,
|
|
202
|
+
ok: true,
|
|
203
|
+
result,
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
})
|
|
207
|
+
.catch((error: unknown) => {
|
|
208
|
+
socket.write(
|
|
209
|
+
encode({
|
|
210
|
+
id,
|
|
211
|
+
ok: false,
|
|
212
|
+
error: errorText(error),
|
|
213
|
+
...(error instanceof BridgeSecretError
|
|
214
|
+
? {
|
|
215
|
+
secret: error.key,
|
|
216
|
+
}
|
|
217
|
+
: {}),
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
error(_socket, error) {
|
|
225
|
+
options.onLog("bridge socket error", {
|
|
226
|
+
error: error.message,
|
|
227
|
+
});
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export type BridgeSocketServer = ReturnType<typeof createServer>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { BridgeDesired } from "../core";
|
|
2
|
+
|
|
3
|
+
const isDesired = (value: unknown): value is BridgeDesired => {
|
|
4
|
+
return value === BridgeDesired.Running || value === BridgeDesired.Stopped;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const readDesired = async (path: string): Promise<BridgeDesired> => {
|
|
8
|
+
const file = Bun.file(path);
|
|
9
|
+
|
|
10
|
+
if (!(await file.exists())) {
|
|
11
|
+
return BridgeDesired.Running;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const parsed: unknown = JSON.parse(await file.text());
|
|
16
|
+
|
|
17
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
18
|
+
return BridgeDesired.Running;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const desired = (
|
|
22
|
+
parsed as {
|
|
23
|
+
desired?: unknown;
|
|
24
|
+
}
|
|
25
|
+
).desired;
|
|
26
|
+
|
|
27
|
+
return isDesired(desired) ? desired : BridgeDesired.Running;
|
|
28
|
+
} catch {
|
|
29
|
+
return BridgeDesired.Running;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const writeDesired = async (path: string, desired: BridgeDesired) => {
|
|
34
|
+
await Bun.write(
|
|
35
|
+
path,
|
|
36
|
+
`${JSON.stringify({
|
|
37
|
+
desired,
|
|
38
|
+
})}\n`,
|
|
39
|
+
);
|
|
40
|
+
};
|