@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-client.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { actionError, isRecord, parseResponse } from "./actions.js";
|
|
1
|
+
import { actionError, isRecord, parseResponse, } from "./actions.js";
|
|
2
2
|
import { validId } from "./protocol.js";
|
|
3
|
-
import {
|
|
3
|
+
import { encodeMeta, frameFits, isUuid, MAX_FRAME_BYTES } from "./frame.js";
|
|
4
|
+
import { parseDaemonStatus, } from "./daemon-protocol.js";
|
|
5
|
+
import { isAbsolute } from "node:path";
|
|
4
6
|
import { DaemonRequestError, DaemonVersionError, RpcError, } from "./daemon-rpc.js";
|
|
5
7
|
import { ensureDaemon, findDaemon } from "./daemon-control.js";
|
|
6
8
|
export { createNodeConfig, nodeConfigFromEnvironment, readOptionalCallerCredential, CallerConfigurationError, } from "./local.js";
|
|
7
9
|
export { daemonStatus, startDaemon, stopDaemon } from "./daemon-control.js";
|
|
8
10
|
export { DaemonRequestError, DaemonVersionError } from "./daemon-rpc.js";
|
|
11
|
+
export { openConsoleBridge, MAX_CONSOLE_MESSAGE_BYTES, } from "./console.js";
|
|
9
12
|
export class DeviceActionError extends Error {
|
|
10
13
|
response;
|
|
11
14
|
code;
|
|
@@ -143,6 +146,86 @@ export function createNodeClient(config, options = {}) {
|
|
|
143
146
|
throw error;
|
|
144
147
|
}
|
|
145
148
|
}
|
|
149
|
+
async function runBinary(deviceId, action, args = {}, body = new Uint8Array()) {
|
|
150
|
+
assertOpen();
|
|
151
|
+
const empty = new Uint8Array();
|
|
152
|
+
const failure = (code, message) => ({
|
|
153
|
+
response: actionError(code, message),
|
|
154
|
+
body: empty,
|
|
155
|
+
});
|
|
156
|
+
if (!isUuid(deviceId) || typeof action !== "string" || !action || action.length > 128)
|
|
157
|
+
return failure("invalid_request", "A device UUID and action are required.");
|
|
158
|
+
let params;
|
|
159
|
+
try {
|
|
160
|
+
params = JSON.parse(JSON.stringify({ deviceId, action, args }));
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return failure("invalid_request", "Action arguments must be JSON.");
|
|
164
|
+
}
|
|
165
|
+
const meta = encodeMeta({ type: "request", action, args: params.args ?? {} });
|
|
166
|
+
if (!frameFits(meta.byteLength, body.byteLength))
|
|
167
|
+
return failure("message_too_large", `The input does not fit in one ${MAX_FRAME_BYTES / 1024 / 1024} MiB frame. Nothing was sent. Use rome-node cp for large files.`);
|
|
168
|
+
try {
|
|
169
|
+
const rpc = await connect(true);
|
|
170
|
+
assertOpen();
|
|
171
|
+
if (!rpc)
|
|
172
|
+
throw new DaemonRequestError();
|
|
173
|
+
const reply = await rpc.requestBinary("devices.runBinary", params, body);
|
|
174
|
+
const response = parseResponse(reply.result);
|
|
175
|
+
if (!response)
|
|
176
|
+
return failure("unknown_outcome", "The action response was invalid. Do not automatically retry.");
|
|
177
|
+
return { response, body: response.ok ? reply.body : empty };
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error instanceof DaemonRequestError)
|
|
181
|
+
return failure(error.code, error.message);
|
|
182
|
+
if (error instanceof RpcError)
|
|
183
|
+
return failure(error.code === -32602 ? "invalid_request" : "gateway_unavailable", error.message);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function copy(request, options = {}) {
|
|
188
|
+
assertOpen();
|
|
189
|
+
const failure = (code, message) => new DeviceActionError({ type: "response", ok: false, error: { code, message } });
|
|
190
|
+
if ((request.direction !== "push" && request.direction !== "pull") ||
|
|
191
|
+
typeof request.localPath !== "string" ||
|
|
192
|
+
!isAbsolute(request.localPath) ||
|
|
193
|
+
!isUuid(request.deviceId) ||
|
|
194
|
+
typeof request.remotePath !== "string" ||
|
|
195
|
+
!request.remotePath)
|
|
196
|
+
throw failure("invalid_request", "A copy needs direction push or pull, an absolute localPath, a device UUID, and remotePath.");
|
|
197
|
+
const { direction, localPath, deviceId, remotePath } = request;
|
|
198
|
+
let value;
|
|
199
|
+
try {
|
|
200
|
+
const rpc = await connect(true);
|
|
201
|
+
assertOpen();
|
|
202
|
+
if (!rpc)
|
|
203
|
+
throw new DaemonRequestError();
|
|
204
|
+
value = await rpc.request("files.copy", { direction, localPath, deviceId, remotePath }, Number.POSITIVE_INFINITY, (params) => {
|
|
205
|
+
if (typeof params.bytes === "number" && typeof params.total === "number")
|
|
206
|
+
options.onProgress?.({ bytes: params.bytes, total: params.total });
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (error instanceof DaemonRequestError)
|
|
211
|
+
throw failure(error.code, error.message);
|
|
212
|
+
if (error instanceof RpcError)
|
|
213
|
+
throw failure(error.code === -32602 ? "invalid_request" : "gateway_unavailable", error.message);
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
const response = parseResponse(value);
|
|
217
|
+
if (!response)
|
|
218
|
+
throw failure("unknown_outcome", "The copy response was invalid. Run the copy again.");
|
|
219
|
+
if (!response.ok)
|
|
220
|
+
throw new DeviceActionError(response);
|
|
221
|
+
const summary = response.result;
|
|
222
|
+
if (!isRecord(summary) ||
|
|
223
|
+
typeof summary.bytes !== "number" ||
|
|
224
|
+
typeof summary.ms !== "number" ||
|
|
225
|
+
typeof summary.sha256 !== "string")
|
|
226
|
+
throw failure("unknown_outcome", "The copy response was invalid. Run the copy again.");
|
|
227
|
+
return { bytes: summary.bytes, ms: summary.ms, sha256: summary.sha256 };
|
|
228
|
+
}
|
|
146
229
|
return {
|
|
147
230
|
async getConnectionStatus() {
|
|
148
231
|
const rpc = await connect(false);
|
|
@@ -199,6 +282,8 @@ export function createNodeClient(config, options = {}) {
|
|
|
199
282
|
return body;
|
|
200
283
|
},
|
|
201
284
|
run,
|
|
285
|
+
runBinary,
|
|
286
|
+
copy,
|
|
202
287
|
describe: (deviceId) => run(deviceId, "system.info"),
|
|
203
288
|
async subscribe(topic, listener) {
|
|
204
289
|
assertOpen();
|
package/dist/daemon-control.js
CHANGED
|
@@ -117,8 +117,19 @@ export async function stopDaemon(config) {
|
|
|
117
117
|
if (legacy?.pid === state.pid) {
|
|
118
118
|
await legacyRequest(state, "/stop");
|
|
119
119
|
}
|
|
120
|
-
else
|
|
121
|
-
|
|
120
|
+
else {
|
|
121
|
+
const older = typeof state.protocolVersion === "number"
|
|
122
|
+
? await connectRpc(state, undefined, state.protocolVersion).catch(() => null)
|
|
123
|
+
: null;
|
|
124
|
+
if (!older)
|
|
125
|
+
return;
|
|
126
|
+
try {
|
|
127
|
+
await older.rpc.request("daemon.stop", {}, 5000);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
older.rpc.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
122
133
|
}
|
|
123
134
|
else {
|
|
124
135
|
const found = await findDaemon(config);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ConnectionStatus } from "./client.js";
|
|
2
|
-
export declare const DAEMON_PROTOCOL_VERSION =
|
|
2
|
+
export declare const DAEMON_PROTOCOL_VERSION = 4;
|
|
3
|
+
export declare const MAX_LOCAL_MESSAGE_BYTES: number;
|
|
3
4
|
export interface DaemonStatus {
|
|
4
5
|
pid: number;
|
|
5
6
|
protocolVersion: number;
|
|
@@ -14,3 +15,23 @@ export type ConnectionEvent = {
|
|
|
14
15
|
daemon: null;
|
|
15
16
|
reason: "connection_lost" | "incompatible";
|
|
16
17
|
};
|
|
18
|
+
export declare function encodeBinaryMessage(message: unknown, body: Uint8Array): Buffer;
|
|
19
|
+
export declare function parseBinaryMessage(data: Buffer): {
|
|
20
|
+
message: unknown;
|
|
21
|
+
body: Uint8Array;
|
|
22
|
+
} | null;
|
|
23
|
+
export interface CopyRequest {
|
|
24
|
+
direction: "push" | "pull";
|
|
25
|
+
localPath: string;
|
|
26
|
+
deviceId: string;
|
|
27
|
+
remotePath: string;
|
|
28
|
+
}
|
|
29
|
+
export interface CopySummary {
|
|
30
|
+
bytes: number;
|
|
31
|
+
ms: number;
|
|
32
|
+
sha256: string;
|
|
33
|
+
}
|
|
34
|
+
export interface CopyProgress {
|
|
35
|
+
bytes: number;
|
|
36
|
+
total: number;
|
|
37
|
+
}
|
package/dist/daemon-protocol.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isRecord } from "./actions.js";
|
|
2
|
-
export const DAEMON_PROTOCOL_VERSION =
|
|
2
|
+
export const DAEMON_PROTOCOL_VERSION = 4;
|
|
3
|
+
export const MAX_LOCAL_MESSAGE_BYTES = 64 * 1024 * 1024;
|
|
3
4
|
export function parseDaemonStatus(value) {
|
|
4
5
|
if (!isRecord(value) ||
|
|
5
6
|
!Number.isSafeInteger(value.pid) ||
|
|
@@ -13,3 +14,23 @@ export function parseDaemonStatus(value) {
|
|
|
13
14
|
connection: value.connection,
|
|
14
15
|
};
|
|
15
16
|
}
|
|
17
|
+
export function encodeBinaryMessage(message, body) {
|
|
18
|
+
const json = Buffer.from(JSON.stringify(message));
|
|
19
|
+
const header = Buffer.alloc(4);
|
|
20
|
+
header.writeUInt32BE(json.byteLength);
|
|
21
|
+
return Buffer.concat([header, json, body]);
|
|
22
|
+
}
|
|
23
|
+
export function parseBinaryMessage(data) {
|
|
24
|
+
if (data.byteLength < 4)
|
|
25
|
+
return null;
|
|
26
|
+
const end = 4 + data.readUInt32BE(0);
|
|
27
|
+
if (end > data.byteLength)
|
|
28
|
+
return null;
|
|
29
|
+
try {
|
|
30
|
+
const message = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(data.subarray(4, end)));
|
|
31
|
+
return { message, body: data.subarray(end) };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
package/dist/daemon-rpc.d.ts
CHANGED
|
@@ -25,10 +25,15 @@ export declare class RpcConnection {
|
|
|
25
25
|
onClose?: () => void;
|
|
26
26
|
constructor(socket: WebSocket);
|
|
27
27
|
get open(): boolean;
|
|
28
|
-
request(method: string, params?: unknown, timeoutMs?: number): Promise<unknown>;
|
|
28
|
+
request(method: string, params?: unknown, timeoutMs?: number, progress?: (params: Record<string, unknown>) => void): Promise<unknown>;
|
|
29
|
+
requestBinary(method: string, params: unknown, body: Uint8Array, timeoutMs?: number): Promise<{
|
|
30
|
+
result: unknown;
|
|
31
|
+
body: Uint8Array;
|
|
32
|
+
}>;
|
|
33
|
+
private submit;
|
|
29
34
|
close(): void;
|
|
30
35
|
}
|
|
31
|
-
export declare function connectRpc(state: DaemonState, signal?: AbortSignal): Promise<{
|
|
36
|
+
export declare function connectRpc(state: DaemonState, signal?: AbortSignal, protocolVersion?: number): Promise<{
|
|
32
37
|
rpc: RpcConnection;
|
|
33
38
|
status: DaemonStatus;
|
|
34
39
|
}>;
|
package/dist/daemon-rpc.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
2
|
import { isRecord } from "./actions.js";
|
|
3
|
-
import { DAEMON_PROTOCOL_VERSION, parseDaemonStatus, } from "./daemon-protocol.js";
|
|
3
|
+
import { DAEMON_PROTOCOL_VERSION, encodeBinaryMessage, MAX_LOCAL_MESSAGE_BYTES, parseBinaryMessage, parseDaemonStatus, } from "./daemon-protocol.js";
|
|
4
4
|
export class DaemonVersionError extends Error {
|
|
5
5
|
constructor() {
|
|
6
6
|
super("The caller daemon uses an incompatible protocol. Stop it explicitly before restarting with this version.");
|
|
@@ -52,26 +52,47 @@ export class RpcConnection {
|
|
|
52
52
|
});
|
|
53
53
|
socket.on("message", (data, binary) => {
|
|
54
54
|
let message;
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
let body = new Uint8Array();
|
|
56
|
+
if (binary) {
|
|
57
|
+
const parsed = parseBinaryMessage(Buffer.isBuffer(data)
|
|
58
|
+
? data
|
|
59
|
+
: Array.isArray(data)
|
|
60
|
+
? Buffer.concat(data)
|
|
61
|
+
: Buffer.from(data));
|
|
62
|
+
if (!parsed) {
|
|
63
|
+
this.close();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
({ message, body } = parsed);
|
|
57
67
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
else {
|
|
69
|
+
try {
|
|
70
|
+
message = JSON.parse(data.toString());
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
this.close();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
61
76
|
}
|
|
62
|
-
if (
|
|
77
|
+
if (!isRecord(message) || message.jsonrpc !== "2.0") {
|
|
63
78
|
this.close();
|
|
64
79
|
return;
|
|
65
80
|
}
|
|
66
81
|
if (typeof message.method === "string" && !Object.hasOwn(message, "id")) {
|
|
67
|
-
|
|
82
|
+
const params = message.params;
|
|
83
|
+
if (message.method === "events.transfer") {
|
|
84
|
+
if (isRecord(params) && typeof params.requestId === "string")
|
|
85
|
+
this.pending.get(params.requestId)?.progress?.(params);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.onNotification?.(message.method, params);
|
|
68
89
|
return;
|
|
69
90
|
}
|
|
70
91
|
const pending = typeof message.id === "string" ? this.pending.get(message.id) : undefined;
|
|
71
92
|
if (!pending)
|
|
72
93
|
return;
|
|
73
94
|
if (Object.hasOwn(message, "result") && !Object.hasOwn(message, "error"))
|
|
74
|
-
pending.resolve(message.result);
|
|
95
|
+
pending.resolve(message.result, body);
|
|
75
96
|
else if (!Object.hasOwn(message, "result") &&
|
|
76
97
|
isRecord(message.error) &&
|
|
77
98
|
Number.isInteger(message.error.code) &&
|
|
@@ -84,11 +105,18 @@ export class RpcConnection {
|
|
|
84
105
|
get open() {
|
|
85
106
|
return this.socket.readyState === WebSocket.OPEN;
|
|
86
107
|
}
|
|
87
|
-
request(method, params = {}, timeoutMs = 85_000) {
|
|
108
|
+
async request(method, params = {}, timeoutMs = 85_000, progress) {
|
|
109
|
+
return (await this.submit(method, params, undefined, timeoutMs, progress)).result;
|
|
110
|
+
}
|
|
111
|
+
async requestBinary(method, params, body, timeoutMs = 85_000) {
|
|
112
|
+
return this.submit(method, params, body, timeoutMs);
|
|
113
|
+
}
|
|
114
|
+
submit(method, params, body, timeoutMs, progress) {
|
|
88
115
|
if (!this.open)
|
|
89
116
|
return Promise.reject(new DaemonRequestError());
|
|
90
117
|
const id = String(++this.nextId);
|
|
91
|
-
const
|
|
118
|
+
const message = { jsonrpc: "2.0", id, method, params };
|
|
119
|
+
const data = body ? encodeBinaryMessage(message, body) : JSON.stringify(message);
|
|
92
120
|
return new Promise((resolve, reject) => {
|
|
93
121
|
const finish = (error, value) => {
|
|
94
122
|
clearTimeout(timer);
|
|
@@ -98,13 +126,16 @@ export class RpcConnection {
|
|
|
98
126
|
else
|
|
99
127
|
resolve(value);
|
|
100
128
|
};
|
|
101
|
-
const timer =
|
|
129
|
+
const timer = Number.isFinite(timeoutMs)
|
|
130
|
+
? setTimeout(() => finish(new DaemonRequestError()), timeoutMs)
|
|
131
|
+
: undefined;
|
|
102
132
|
this.pending.set(id, {
|
|
103
|
-
resolve: (
|
|
133
|
+
resolve: (result, body) => finish(undefined, { result, body }),
|
|
104
134
|
reject: (error) => finish(error),
|
|
135
|
+
progress,
|
|
105
136
|
});
|
|
106
137
|
try {
|
|
107
|
-
this.socket.send(
|
|
138
|
+
this.socket.send(data, { binary: Boolean(body) }, (error) => {
|
|
108
139
|
if (error)
|
|
109
140
|
finish(new DaemonRequestError());
|
|
110
141
|
});
|
|
@@ -118,12 +149,13 @@ export class RpcConnection {
|
|
|
118
149
|
this.socket.terminate();
|
|
119
150
|
}
|
|
120
151
|
}
|
|
121
|
-
export async function connectRpc(state, signal) {
|
|
152
|
+
export async function connectRpc(state, signal, protocolVersion = DAEMON_PROTOCOL_VERSION) {
|
|
122
153
|
signal?.throwIfAborted();
|
|
123
154
|
const socket = new WebSocket(`ws://127.0.0.1:${state.port}/rpc`, {
|
|
124
155
|
headers: { authorization: `Bearer ${state.token}` },
|
|
125
156
|
handshakeTimeout: 1500,
|
|
126
157
|
perMessageDeflate: false,
|
|
158
|
+
maxPayload: MAX_LOCAL_MESSAGE_BYTES,
|
|
127
159
|
followRedirects: false,
|
|
128
160
|
});
|
|
129
161
|
const rpc = new RpcConnection(socket);
|
|
@@ -150,8 +182,8 @@ export async function connectRpc(state, signal) {
|
|
|
150
182
|
if (signal?.aborted)
|
|
151
183
|
abort();
|
|
152
184
|
});
|
|
153
|
-
const value = await rpc.request("daemon.hello", { protocolVersion
|
|
154
|
-
if (isRecord(value) && value.protocolVersion !==
|
|
185
|
+
const value = await rpc.request("daemon.hello", { protocolVersion }, 1500);
|
|
186
|
+
if (isRecord(value) && value.protocolVersion !== protocolVersion)
|
|
155
187
|
throw new DaemonVersionError();
|
|
156
188
|
const status = parseDaemonStatus(value);
|
|
157
189
|
if (!status || status.pid !== state.pid)
|