@rome-os/node-core 0.1.1 → 0.1.3
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/README.md +59 -1
- package/dist/actions.d.ts +4 -0
- package/dist/client.d.ts +4 -1
- package/dist/client.js +30 -1
- package/dist/connect.js +2 -3
- package/dist/connector.d.ts +10 -1
- package/dist/connector.js +129 -7
- package/dist/console.d.ts +12 -0
- package/dist/console.js +101 -0
- package/dist/daemon-client.d.ts +10 -4
- package/dist/daemon-client.js +87 -2
- package/dist/daemon-control.js +13 -2
- package/dist/daemon-protocol.d.ts +22 -1
- package/dist/daemon-protocol.js +22 -1
- package/dist/daemon-rpc.d.ts +7 -2
- package/dist/daemon-rpc.js +49 -17
- package/dist/daemon.js +345 -29
- package/dist/executor.d.ts +4 -1
- package/dist/executor.js +180 -66
- package/dist/frame.d.ts +24 -0
- package/dist/frame.js +64 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/observe-protocol.d.ts +100 -0
- package/dist/observe-protocol.js +13 -0
- package/dist/observe.d.ts +67 -0
- package/dist/observe.js +457 -0
- package/dist/transfer.d.ts +102 -0
- package/dist/transfer.js +518 -0
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -1,13 +1,78 @@
|
|
|
1
1
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { join } from "node:path";
|
|
3
5
|
import WebSocket, { WebSocketServer } from "ws";
|
|
4
|
-
import { isRecord } from "./actions.js";
|
|
6
|
+
import { actionError, isRecord } from "./actions.js";
|
|
7
|
+
import { ObserveStore, QueryError } from "./observe.js";
|
|
5
8
|
import { DeviceConnector } from "./connector.js";
|
|
6
9
|
import { daemonStatePath, readCallerCredential } from "./local.js";
|
|
7
10
|
import { writePrivateJson } from "./storage.js";
|
|
8
|
-
import { DAEMON_PROTOCOL_VERSION } from "./daemon-protocol.js";
|
|
11
|
+
import { DAEMON_PROTOCOL_VERSION, encodeBinaryMessage, MAX_LOCAL_MESSAGE_BYTES, parseBinaryMessage, } from "./daemon-protocol.js";
|
|
9
12
|
import { DeviceStatusReader } from "./devices-status.js";
|
|
10
13
|
import { RpcError } from "./daemon-rpc.js";
|
|
14
|
+
import { isAbsolute } from "node:path";
|
|
15
|
+
import { isUuid } from "./frame.js";
|
|
16
|
+
import { copyWithDevice, TRANSFER_VERSION, TransferError } from "./transfer.js";
|
|
17
|
+
const PRUNE_INTERVAL_MS = 60 * 60 * 1000;
|
|
18
|
+
const { version: NODE_CORE_VERSION } = createRequire(import.meta.url)("../package.json");
|
|
19
|
+
function copyRequest(params) {
|
|
20
|
+
const { direction, localPath, deviceId, remotePath } = params;
|
|
21
|
+
if ((direction !== "push" && direction !== "pull") ||
|
|
22
|
+
typeof localPath !== "string" ||
|
|
23
|
+
!isAbsolute(localPath) ||
|
|
24
|
+
localPath.includes("\0") ||
|
|
25
|
+
!isUuid(deviceId) ||
|
|
26
|
+
typeof remotePath !== "string" ||
|
|
27
|
+
!remotePath ||
|
|
28
|
+
remotePath.includes("\0"))
|
|
29
|
+
return null;
|
|
30
|
+
return { direction, localPath, deviceId: deviceId.toLowerCase(), remotePath };
|
|
31
|
+
}
|
|
32
|
+
function byteCount(value) {
|
|
33
|
+
return typeof value === "string" ? Buffer.byteLength(value) : 0;
|
|
34
|
+
}
|
|
35
|
+
function actionStart(deviceId, action, args, mode, stdinBytes = 0) {
|
|
36
|
+
const exec = action === "exec" && isRecord(args) && typeof args.command === "string";
|
|
37
|
+
let text = null;
|
|
38
|
+
if (!exec)
|
|
39
|
+
try {
|
|
40
|
+
text = JSON.stringify(args ?? {});
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
return {
|
|
44
|
+
direction: "out",
|
|
45
|
+
peer: deviceId.toLowerCase(),
|
|
46
|
+
action,
|
|
47
|
+
mode,
|
|
48
|
+
command: exec ? args.command : null,
|
|
49
|
+
args: exec ? (Array.isArray(args.args) ? args.args.map(String) : []) : text,
|
|
50
|
+
cwd: exec && typeof args.cwd === "string" ? args.cwd : null,
|
|
51
|
+
stdinBytes,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function actionEnd(response, stdoutBytes) {
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const code = response.error.code;
|
|
57
|
+
return {
|
|
58
|
+
outcome: code === "unknown_outcome" ? "unknown_outcome" : code === "canceled" ? "aborted" : "error",
|
|
59
|
+
errorCode: code,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const result = isRecord(response.result) ? response.result : {};
|
|
63
|
+
return {
|
|
64
|
+
outcome: "ok",
|
|
65
|
+
exitCode: Number.isSafeInteger(result.exitCode) ? result.exitCode : null,
|
|
66
|
+
signal: typeof result.signal === "string" ? result.signal : null,
|
|
67
|
+
stdoutBytes: stdoutBytes ?? byteCount(result.stdout),
|
|
68
|
+
stderrBytes: byteCount(result.stderr),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function logQuery(params) {
|
|
72
|
+
if (params.kind !== "actions")
|
|
73
|
+
throw new RpcError(-32602, "log.query requires kind actions.");
|
|
74
|
+
return params;
|
|
75
|
+
}
|
|
11
76
|
export async function serveDaemon(config, signal) {
|
|
12
77
|
if (signal.aborted)
|
|
13
78
|
return;
|
|
@@ -19,14 +84,37 @@ export async function serveDaemon(config, signal) {
|
|
|
19
84
|
protocolVersion: DAEMON_PROTOCOL_VERSION,
|
|
20
85
|
token: `romenode_${randomBytes(32).toString("base64url")}`,
|
|
21
86
|
};
|
|
87
|
+
const startedAt = Date.now();
|
|
22
88
|
let closing = false;
|
|
89
|
+
const store = new ObserveStore({
|
|
90
|
+
path: join(config.directory, "observe.db"),
|
|
91
|
+
secrets: [credential.token, state.token],
|
|
92
|
+
});
|
|
93
|
+
const caller = {
|
|
94
|
+
connection: "stopped",
|
|
95
|
+
since: startedAt,
|
|
96
|
+
onlineOnce: false,
|
|
97
|
+
reconnects: 0,
|
|
98
|
+
running: 0,
|
|
99
|
+
};
|
|
23
100
|
const connector = new DeviceConnector({
|
|
24
101
|
credential,
|
|
25
|
-
onStatus: () => {
|
|
102
|
+
onStatus: (status) => {
|
|
26
103
|
deviceStatus.invalidate();
|
|
27
104
|
for (const peer of peers)
|
|
28
105
|
if (peer.subscribed)
|
|
29
106
|
notify(peer);
|
|
107
|
+
if (status === caller.connection)
|
|
108
|
+
return;
|
|
109
|
+
const at = Date.now();
|
|
110
|
+
caller.connection = status;
|
|
111
|
+
caller.since = at;
|
|
112
|
+
if (status === "online") {
|
|
113
|
+
if (caller.onlineOnce)
|
|
114
|
+
caller.reconnects++;
|
|
115
|
+
caller.onlineOnce = true;
|
|
116
|
+
}
|
|
117
|
+
broadcast({ type: "connection", role: "caller", status, at });
|
|
30
118
|
},
|
|
31
119
|
});
|
|
32
120
|
const deviceStatus = new DeviceStatusReader(connector);
|
|
@@ -35,6 +123,39 @@ export async function serveDaemon(config, signal) {
|
|
|
35
123
|
protocolVersion: DAEMON_PROTOCOL_VERSION,
|
|
36
124
|
connection: connector.getStatus(),
|
|
37
125
|
});
|
|
126
|
+
const nodeStatus = () => ({
|
|
127
|
+
version: NODE_CORE_VERSION,
|
|
128
|
+
startedAt,
|
|
129
|
+
roles: {
|
|
130
|
+
caller: {
|
|
131
|
+
auth: caller.connection === "revoked" ? "revoked" : "authorized",
|
|
132
|
+
connection: caller.connection,
|
|
133
|
+
since: caller.since,
|
|
134
|
+
reconnects: caller.reconnects,
|
|
135
|
+
running: caller.running,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
async function observed(start, work, end) {
|
|
140
|
+
if (!start)
|
|
141
|
+
return work();
|
|
142
|
+
caller.running++;
|
|
143
|
+
const row = store.startAction(start);
|
|
144
|
+
if (row)
|
|
145
|
+
broadcast({ type: "action", id: row.id, phase: "start", row });
|
|
146
|
+
let ended = { outcome: "error", errorCode: "daemon_error" };
|
|
147
|
+
try {
|
|
148
|
+
const value = await work();
|
|
149
|
+
ended = end(value);
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
caller.running--;
|
|
154
|
+
const done = row && store.endAction(row.id, ended);
|
|
155
|
+
if (done)
|
|
156
|
+
broadcast({ type: "action", id: done.id, phase: "end", row: done });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
38
159
|
function authorized(req) {
|
|
39
160
|
const actual = Buffer.from(req.headers.authorization ?? "");
|
|
40
161
|
const expected = Buffer.from(`Bearer ${state.token}`);
|
|
@@ -44,7 +165,11 @@ export async function serveDaemon(config, signal) {
|
|
|
44
165
|
res.writeHead(authorized(req) ? 426 : 401, { "cache-control": "no-store" }).end();
|
|
45
166
|
});
|
|
46
167
|
server.headersTimeout = 10_000;
|
|
47
|
-
const sockets = new WebSocketServer({
|
|
168
|
+
const sockets = new WebSocketServer({
|
|
169
|
+
noServer: true,
|
|
170
|
+
perMessageDeflate: false,
|
|
171
|
+
maxPayload: MAX_LOCAL_MESSAGE_BYTES,
|
|
172
|
+
});
|
|
48
173
|
server.on("upgrade", (req, socket, head) => {
|
|
49
174
|
if (closing || req.url !== "/rpc" || req.headers.origin || !authorized(req)) {
|
|
50
175
|
socket.end("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
@@ -60,12 +185,50 @@ export async function serveDaemon(config, signal) {
|
|
|
60
185
|
peer.socket.terminate();
|
|
61
186
|
});
|
|
62
187
|
}
|
|
188
|
+
function deliver(peer, reply) {
|
|
189
|
+
if (!reply.body) {
|
|
190
|
+
send(peer, reply.message);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (peer.socket.readyState !== WebSocket.OPEN)
|
|
194
|
+
return;
|
|
195
|
+
let data = encodeBinaryMessage(reply.message, reply.body);
|
|
196
|
+
if (data.byteLength > MAX_LOCAL_MESSAGE_BYTES) {
|
|
197
|
+
const id = isRecord(reply.message) ? reply.message.id : null;
|
|
198
|
+
data = encodeBinaryMessage({
|
|
199
|
+
jsonrpc: "2.0",
|
|
200
|
+
id,
|
|
201
|
+
result: actionError("message_too_large", "The response exceeds the local message limit. The action may have completed."),
|
|
202
|
+
}, new Uint8Array());
|
|
203
|
+
}
|
|
204
|
+
peer.socket.send(data, { binary: true }, (error) => {
|
|
205
|
+
if (error)
|
|
206
|
+
peer.socket.terminate();
|
|
207
|
+
});
|
|
208
|
+
}
|
|
63
209
|
function notify(peer) {
|
|
64
|
-
|
|
210
|
+
emit(peer, "events.connection", snapshot());
|
|
211
|
+
}
|
|
212
|
+
function broadcast(event) {
|
|
213
|
+
for (const peer of peers)
|
|
214
|
+
if (peer.observing)
|
|
215
|
+
emit(peer, "events.node", event);
|
|
216
|
+
}
|
|
217
|
+
function emit(peer, method, params) {
|
|
218
|
+
if (peer.eventBytes > 1024 * 1024) {
|
|
65
219
|
peer.socket.terminate();
|
|
66
220
|
return;
|
|
67
221
|
}
|
|
68
|
-
|
|
222
|
+
if (peer.socket.readyState !== WebSocket.OPEN)
|
|
223
|
+
return;
|
|
224
|
+
const text = JSON.stringify({ jsonrpc: "2.0", method, params });
|
|
225
|
+
const size = Buffer.byteLength(text);
|
|
226
|
+
peer.eventBytes += size;
|
|
227
|
+
peer.socket.send(text, (error) => {
|
|
228
|
+
peer.eventBytes -= size;
|
|
229
|
+
if (error)
|
|
230
|
+
peer.socket.terminate();
|
|
231
|
+
});
|
|
69
232
|
}
|
|
70
233
|
const close = () => {
|
|
71
234
|
if (closing)
|
|
@@ -83,10 +246,52 @@ export async function serveDaemon(config, signal) {
|
|
|
83
246
|
}, 1000);
|
|
84
247
|
timer.unref();
|
|
85
248
|
};
|
|
249
|
+
async function copyFile(peer, id, request) {
|
|
250
|
+
const controller = new AbortController();
|
|
251
|
+
peer.copies.add(controller);
|
|
252
|
+
if (peer.socket.readyState !== WebSocket.OPEN)
|
|
253
|
+
controller.abort();
|
|
254
|
+
const canceled = () => actionError("canceled", "The client disconnected. The copy did not start.");
|
|
255
|
+
let last = 0;
|
|
256
|
+
try {
|
|
257
|
+
if (controller.signal.aborted)
|
|
258
|
+
return canceled();
|
|
259
|
+
const info = await connector.run(request.deviceId, "system.info", {});
|
|
260
|
+
if (controller.signal.aborted)
|
|
261
|
+
return canceled();
|
|
262
|
+
if (!info.ok)
|
|
263
|
+
return info;
|
|
264
|
+
if (!isRecord(info.result) || info.result.transferVersion !== TRANSFER_VERSION)
|
|
265
|
+
return actionError("unsupported_device", "The device runs a rome-node version without file transfers. Update rome-node on that device.");
|
|
266
|
+
const summary = await copyWithDevice((events) => connector.openChannel(request.deviceId, events), request, {
|
|
267
|
+
signal: controller.signal,
|
|
268
|
+
onProgress: ({ bytes, total }) => {
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
if (now - last < 250 && bytes !== total)
|
|
271
|
+
return;
|
|
272
|
+
last = now;
|
|
273
|
+
send(peer, {
|
|
274
|
+
jsonrpc: "2.0",
|
|
275
|
+
method: "events.transfer",
|
|
276
|
+
params: { requestId: id, bytes, total },
|
|
277
|
+
});
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
return { type: "response", ok: true, result: summary };
|
|
281
|
+
}
|
|
282
|
+
catch (cause) {
|
|
283
|
+
if (!(cause instanceof TransferError))
|
|
284
|
+
throw cause;
|
|
285
|
+
return actionError(cause.code, cause.message);
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
peer.copies.delete(controller);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
86
291
|
function error(id, code, message) {
|
|
87
292
|
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
88
293
|
}
|
|
89
|
-
async function dispatch(peer, value) {
|
|
294
|
+
async function dispatch(peer, value, input) {
|
|
90
295
|
if (!isRecord(value) ||
|
|
91
296
|
value.jsonrpc !== "2.0" ||
|
|
92
297
|
typeof value.method !== "string" ||
|
|
@@ -94,7 +299,7 @@ export async function serveDaemon(config, signal) {
|
|
|
94
299
|
typeof value.id !== "string" &&
|
|
95
300
|
typeof value.id !== "number" &&
|
|
96
301
|
value.id !== null))
|
|
97
|
-
return error(null, -32600, "Invalid Request");
|
|
302
|
+
return { message: error(null, -32600, "Invalid Request") };
|
|
98
303
|
const hasId = Object.hasOwn(value, "id");
|
|
99
304
|
const id = value.id;
|
|
100
305
|
if (hasId && id !== null && peer.pending.has(id)) {
|
|
@@ -109,7 +314,10 @@ export async function serveDaemon(config, signal) {
|
|
|
109
314
|
const params = Object.hasOwn(value, "params") ? value.params : {};
|
|
110
315
|
if (!isRecord(params))
|
|
111
316
|
throw new RpcError(-32602, "Invalid params");
|
|
317
|
+
if ((value.method === "devices.runBinary") !== Boolean(input))
|
|
318
|
+
throw new RpcError(-32600, "Only devices.runBinary uses binary messages, and it requires one.");
|
|
112
319
|
let result;
|
|
320
|
+
let body;
|
|
113
321
|
if (value.method === "daemon.hello") {
|
|
114
322
|
if (params.protocolVersion !== DAEMON_PROTOCOL_VERSION)
|
|
115
323
|
throw new RpcError(-32002, "Incompatible daemon protocol.");
|
|
@@ -127,6 +335,29 @@ export async function serveDaemon(config, signal) {
|
|
|
127
335
|
result = { stopped: true };
|
|
128
336
|
setImmediate(close);
|
|
129
337
|
break;
|
|
338
|
+
case "node.status":
|
|
339
|
+
result = nodeStatus();
|
|
340
|
+
break;
|
|
341
|
+
case "log.query":
|
|
342
|
+
try {
|
|
343
|
+
result = store.query(logQuery(params));
|
|
344
|
+
}
|
|
345
|
+
catch (cause) {
|
|
346
|
+
if (cause instanceof QueryError)
|
|
347
|
+
throw new RpcError(-32602, cause.message);
|
|
348
|
+
throw cause;
|
|
349
|
+
}
|
|
350
|
+
break;
|
|
351
|
+
case "log.stats":
|
|
352
|
+
try {
|
|
353
|
+
result = store.stats(params);
|
|
354
|
+
}
|
|
355
|
+
catch (cause) {
|
|
356
|
+
if (cause instanceof QueryError)
|
|
357
|
+
throw new RpcError(-32602, cause.message);
|
|
358
|
+
throw cause;
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
130
361
|
case "devices.status":
|
|
131
362
|
result = await deviceStatus.read();
|
|
132
363
|
break;
|
|
@@ -136,29 +367,83 @@ export async function serveDaemon(config, signal) {
|
|
|
136
367
|
case "devices.run":
|
|
137
368
|
if (typeof params.deviceId !== "string" || typeof params.action !== "string")
|
|
138
369
|
throw new RpcError(-32602, "A device ID and action are required.");
|
|
139
|
-
|
|
370
|
+
{
|
|
371
|
+
const { deviceId, action } = params;
|
|
372
|
+
const args = params.args ?? {};
|
|
373
|
+
result = await observed(action === "system.info" ? null : actionStart(deviceId, action, args, "json"), () => connector.run(deviceId, action, args), (response) => actionEnd(response));
|
|
374
|
+
}
|
|
375
|
+
break;
|
|
376
|
+
case "devices.runBinary": {
|
|
377
|
+
if (typeof params.deviceId !== "string" || typeof params.action !== "string")
|
|
378
|
+
throw new RpcError(-32602, "A device ID and action are required.");
|
|
379
|
+
const { deviceId, action } = params;
|
|
380
|
+
const args = params.args ?? {};
|
|
381
|
+
const stdin = input ?? new Uint8Array();
|
|
382
|
+
const reply = await observed(actionStart(deviceId, action, args, "binary", stdin.byteLength), () => connector.runBinary(deviceId, action, args, stdin), ({ response, body }) => actionEnd(response, body.byteLength));
|
|
383
|
+
result = reply.response;
|
|
384
|
+
body = reply.body;
|
|
140
385
|
break;
|
|
386
|
+
}
|
|
387
|
+
case "files.copy": {
|
|
388
|
+
const request = copyRequest(params);
|
|
389
|
+
if (!request)
|
|
390
|
+
throw new RpcError(-32602, "files.copy requires direction push or pull, an absolute localPath, a device UUID, and remotePath.");
|
|
391
|
+
result = await observed({
|
|
392
|
+
direction: "out",
|
|
393
|
+
peer: request.deviceId,
|
|
394
|
+
action: "copy",
|
|
395
|
+
mode: "copy",
|
|
396
|
+
transfer: {
|
|
397
|
+
direction: request.direction,
|
|
398
|
+
localPath: request.localPath,
|
|
399
|
+
remotePath: request.remotePath,
|
|
400
|
+
},
|
|
401
|
+
}, () => copyFile(peer, id, request), (response) => {
|
|
402
|
+
const summary = response.ok && isRecord(response.result) ? response.result : {};
|
|
403
|
+
return {
|
|
404
|
+
...actionEnd(response),
|
|
405
|
+
transferBytes: typeof summary.bytes === "number" ? summary.bytes : null,
|
|
406
|
+
sha256: typeof summary.sha256 === "string" ? summary.sha256 : null,
|
|
407
|
+
};
|
|
408
|
+
});
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
141
411
|
case "events.subscribe":
|
|
142
|
-
case "events.unsubscribe":
|
|
143
|
-
|
|
412
|
+
case "events.unsubscribe": {
|
|
413
|
+
const on = value.method === "events.subscribe";
|
|
414
|
+
if (params.topic === "connection") {
|
|
415
|
+
peer.subscribed = on;
|
|
416
|
+
if (on)
|
|
417
|
+
notify(peer);
|
|
418
|
+
}
|
|
419
|
+
else if (params.topic === "node") {
|
|
420
|
+
peer.observing = on;
|
|
421
|
+
if (on)
|
|
422
|
+
emit(peer, "events.node", {
|
|
423
|
+
type: "snapshot",
|
|
424
|
+
status: nodeStatus(),
|
|
425
|
+
running: store.running(),
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
else
|
|
144
429
|
throw new RpcError(-32602, "Unknown event topic.");
|
|
145
|
-
|
|
146
|
-
if (peer.subscribed)
|
|
147
|
-
notify(peer);
|
|
148
|
-
result = { subscribed: peer.subscribed };
|
|
430
|
+
result = { subscribed: on };
|
|
149
431
|
break;
|
|
432
|
+
}
|
|
150
433
|
default:
|
|
151
434
|
throw new RpcError(-32601, "Method not found");
|
|
152
435
|
}
|
|
153
436
|
}
|
|
154
|
-
return hasId ? { jsonrpc: "2.0", id, result } : undefined;
|
|
437
|
+
return hasId ? { message: { jsonrpc: "2.0", id, result }, body } : undefined;
|
|
155
438
|
}
|
|
156
439
|
catch (cause) {
|
|
157
440
|
if (!hasId)
|
|
158
441
|
return;
|
|
159
|
-
return
|
|
160
|
-
|
|
161
|
-
|
|
442
|
+
return {
|
|
443
|
+
message: cause instanceof RpcError
|
|
444
|
+
? error(id, cause.code, cause.message)
|
|
445
|
+
: error(id, -32000, "The device service is unavailable."),
|
|
446
|
+
};
|
|
162
447
|
}
|
|
163
448
|
finally {
|
|
164
449
|
if (hasId && id !== null)
|
|
@@ -166,14 +451,42 @@ export async function serveDaemon(config, signal) {
|
|
|
166
451
|
}
|
|
167
452
|
}
|
|
168
453
|
sockets.on("connection", (socket) => {
|
|
169
|
-
const peer = {
|
|
454
|
+
const peer = {
|
|
455
|
+
socket,
|
|
456
|
+
ready: false,
|
|
457
|
+
subscribed: false,
|
|
458
|
+
observing: false,
|
|
459
|
+
alive: true,
|
|
460
|
+
pending: new Set(),
|
|
461
|
+
eventBytes: 0,
|
|
462
|
+
copies: new Set(),
|
|
463
|
+
};
|
|
170
464
|
peers.add(peer);
|
|
171
465
|
socket.on("error", () => socket.terminate());
|
|
172
|
-
socket.on("close", () =>
|
|
466
|
+
socket.on("close", () => {
|
|
467
|
+
peers.delete(peer);
|
|
468
|
+
for (const copy of peer.copies)
|
|
469
|
+
copy.abort();
|
|
470
|
+
});
|
|
173
471
|
socket.on("pong", () => {
|
|
174
472
|
peer.alive = true;
|
|
175
473
|
});
|
|
176
474
|
socket.on("message", (data, binary) => {
|
|
475
|
+
if (binary) {
|
|
476
|
+
const parsed = parseBinaryMessage(Buffer.isBuffer(data)
|
|
477
|
+
? data
|
|
478
|
+
: Array.isArray(data)
|
|
479
|
+
? Buffer.concat(data)
|
|
480
|
+
: Buffer.from(data));
|
|
481
|
+
if (!parsed) {
|
|
482
|
+
send(peer, error(null, -32600, "Invalid Request"));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
void dispatch(peer, parsed.message, parsed.body)
|
|
486
|
+
.then((reply) => reply && deliver(peer, reply))
|
|
487
|
+
.catch(() => socket.terminate());
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
177
490
|
let request;
|
|
178
491
|
try {
|
|
179
492
|
request = JSON.parse(data.toString());
|
|
@@ -182,20 +495,18 @@ export async function serveDaemon(config, signal) {
|
|
|
182
495
|
send(peer, error(null, -32700, "Parse error"));
|
|
183
496
|
return;
|
|
184
497
|
}
|
|
185
|
-
if (binary) {
|
|
186
|
-
send(peer, error(null, -32600, "Invalid Request"));
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
498
|
void (async () => {
|
|
190
499
|
if (Array.isArray(request) && request.length > 0) {
|
|
191
|
-
const responses = (await Promise.all(request.map((item) => dispatch(peer, item))))
|
|
500
|
+
const responses = (await Promise.all(request.map((item) => dispatch(peer, item))))
|
|
501
|
+
.filter((reply) => reply !== undefined)
|
|
502
|
+
.map((reply) => reply.message);
|
|
192
503
|
if (responses.length)
|
|
193
504
|
send(peer, responses);
|
|
194
505
|
}
|
|
195
506
|
else {
|
|
196
|
-
const
|
|
197
|
-
if (
|
|
198
|
-
send(peer,
|
|
507
|
+
const reply = await dispatch(peer, request);
|
|
508
|
+
if (reply !== undefined)
|
|
509
|
+
send(peer, reply.message);
|
|
199
510
|
}
|
|
200
511
|
})().catch(() => socket.terminate());
|
|
201
512
|
});
|
|
@@ -207,6 +518,9 @@ export async function serveDaemon(config, signal) {
|
|
|
207
518
|
resolve();
|
|
208
519
|
});
|
|
209
520
|
});
|
|
521
|
+
store.open();
|
|
522
|
+
const pruner = setInterval(() => store.prune(), PRUNE_INTERVAL_MS);
|
|
523
|
+
pruner.unref();
|
|
210
524
|
const heartbeat = setInterval(() => {
|
|
211
525
|
for (const peer of peers) {
|
|
212
526
|
if (!peer.alive) {
|
|
@@ -230,6 +544,8 @@ export async function serveDaemon(config, signal) {
|
|
|
230
544
|
finally {
|
|
231
545
|
close();
|
|
232
546
|
clearInterval(heartbeat);
|
|
547
|
+
clearInterval(pruner);
|
|
548
|
+
store.close();
|
|
233
549
|
signal.removeEventListener("abort", close);
|
|
234
550
|
}
|
|
235
551
|
}
|
package/dist/executor.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { type Frame } from "./frame.js";
|
|
1
2
|
import type { InboundEnvelope, OutboundEnvelope } from "./protocol.js";
|
|
2
|
-
export declare function
|
|
3
|
+
export declare function redactBytes(data: Buffer, secrets: string[]): Buffer;
|
|
4
|
+
export declare function createExecutor(name: string, send: (message: OutboundEnvelope) => boolean, secrets?: string[], sendFrame?: (frame: Frame) => boolean): {
|
|
3
5
|
receive(message: InboundEnvelope): Promise<void>;
|
|
6
|
+
receiveFrame(frame: Frame): Promise<void>;
|
|
4
7
|
disconnect(): void;
|
|
5
8
|
};
|