tirtc-devtools-cli 0.0.6 → 0.0.8
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 +8 -1
- package/USAGE.md +45 -7
- package/dist/devtools/cli/src/config.js +2 -2
- package/dist/devtools/cli/src/facade.d.ts +19 -3
- package/dist/devtools/cli/src/facade.js +12 -3
- package/dist/devtools/cli/src/guide.js +1 -1
- package/dist/devtools/cli/src/index.js +68 -23
- package/dist/devtools/cli/src/progress.d.ts +19 -0
- package/dist/devtools/cli/src/progress.js +63 -0
- package/package.json +1 -1
- package/vendor/app-server/bin/native/macos-arm64/runtime_host_napi.node +0 -0
- package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/error.h +1 -0
- package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/transport.h +2 -0
- package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_audio.a +0 -0
- package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_credential.a +0 -0
- package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_facade.a +0 -0
- package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_foundation_http.a +0 -0
- package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_foundation_logging.a +0 -0
- package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_media.a +0 -0
- package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_transport.a +0 -0
- package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_video.a +0 -0
- package/vendor/app-server/bin/runtime/macos-arm64/manifest.txt +11 -11
- package/vendor/app-server/dist/host/ArtifactManager.js +17 -2
- package/vendor/app-server/dist/host/HostProtocol.d.ts +7 -3
- package/vendor/app-server/dist/host/HostProtocol.js +9 -3
- package/vendor/app-server/dist/host/HostServer.d.ts +6 -0
- package/vendor/app-server/dist/host/HostServer.js +172 -2
- package/vendor/app-server/dist/host/HostState.d.ts +14 -0
- package/vendor/app-server/dist/host/HostState.js +1 -0
- package/vendor/app-server/dist/host/RuntimeAdapter.d.ts +4 -2
- package/vendor/app-server/dist/host/RuntimeAdapter.js +51 -13
- package/vendor/app-server/dist/host/RuntimeSendWorker.d.ts +12 -0
- package/vendor/app-server/dist/host/RuntimeSendWorker.js +147 -0
- package/vendor/app-server/dist/host/native/RuntimeHostBridge.d.ts +13 -0
- package/vendor/app-server/dist/host/native/RuntimeHostBridge.js +16 -0
- package/vendor/app-server/dist/protocol/contract.d.ts +158 -12
- package/vendor/app-server/dist/protocol/contract.js +30 -5
|
@@ -64,6 +64,39 @@ function resolveLocalAssetsDir(source) {
|
|
|
64
64
|
}
|
|
65
65
|
return assetsDir;
|
|
66
66
|
}
|
|
67
|
+
function normalizeCommandPayload(payload) {
|
|
68
|
+
if (typeof payload === 'string') {
|
|
69
|
+
return {
|
|
70
|
+
payloadEncoding: 'utf8',
|
|
71
|
+
payload,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (payload instanceof Uint8Array) {
|
|
75
|
+
return {
|
|
76
|
+
payloadEncoding: 'base64',
|
|
77
|
+
payload: Buffer.from(payload).toString('base64'),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(payload) && payload.every((value) => typeof value === 'number')) {
|
|
81
|
+
return {
|
|
82
|
+
payloadEncoding: 'base64',
|
|
83
|
+
payload: Buffer.from(payload).toString('base64'),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (typeof payload === 'object' && payload !== null && 'type' in payload && payload.type === 'Buffer') {
|
|
87
|
+
const data = payload.data;
|
|
88
|
+
if (Array.isArray(data) && data.every((value) => typeof value === 'number')) {
|
|
89
|
+
return {
|
|
90
|
+
payloadEncoding: 'base64',
|
|
91
|
+
payload: Buffer.from(data).toString('base64'),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
payloadEncoding: 'utf8',
|
|
97
|
+
payload: '',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
67
100
|
class HostServer {
|
|
68
101
|
state;
|
|
69
102
|
artifactManager;
|
|
@@ -175,6 +208,19 @@ class HostServer {
|
|
|
175
208
|
if (family !== 'runtime') {
|
|
176
209
|
return;
|
|
177
210
|
}
|
|
211
|
+
if (kind === 'connection.accepted') {
|
|
212
|
+
this.noteIncomingConnection(payload);
|
|
213
|
+
this.activatePendingStreamsOnConnected();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (kind === 'connection.disconnected') {
|
|
217
|
+
this.handleIncomingConnectionDisconnected(payload);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (kind === 'command.remote.requested') {
|
|
221
|
+
this.handleInboundRemoteCommandRequest(payload);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
178
224
|
if (kind === 'transport.remote.request_audio' ||
|
|
179
225
|
kind === 'transport.remote.request_video' ||
|
|
180
226
|
kind === 'transport.remote.request_iframe') {
|
|
@@ -187,6 +233,53 @@ class HostServer {
|
|
|
187
233
|
this.maybeAutoStartStreamFromRemoteDemand(streamId, media, payload);
|
|
188
234
|
}
|
|
189
235
|
}
|
|
236
|
+
snapshotPendingConnectionSummary() {
|
|
237
|
+
return {
|
|
238
|
+
state: this.state.connection.state,
|
|
239
|
+
direction: this.state.connection.direction,
|
|
240
|
+
peerId: this.state.connection.peerId,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
handleInboundRemoteCommandRequest(payload) {
|
|
244
|
+
const remoteRequestId = Number(payload.remoteRequestId);
|
|
245
|
+
const commandId = Number(payload.commandId);
|
|
246
|
+
if (!Number.isInteger(remoteRequestId) || remoteRequestId <= 0 || !Number.isInteger(commandId)) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
this.noteIncomingConnection(payload);
|
|
250
|
+
const normalizedPayload = normalizeCommandPayload(payload.payload);
|
|
251
|
+
const entry = {
|
|
252
|
+
remoteRequestId,
|
|
253
|
+
commandId,
|
|
254
|
+
payloadEncoding: normalizedPayload.payloadEncoding,
|
|
255
|
+
payload: normalizedPayload.payload,
|
|
256
|
+
receivedAt: new Date().toISOString(),
|
|
257
|
+
connection: this.snapshotPendingConnectionSummary(),
|
|
258
|
+
};
|
|
259
|
+
this.state.pendingRemoteCommands.set(remoteRequestId, entry);
|
|
260
|
+
this.writeEvent('command', 'command.remote.requested', {
|
|
261
|
+
...entry,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
clearPendingRemoteCommands(reason) {
|
|
265
|
+
if (this.state.pendingRemoteCommands.size === 0) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const clearedAt = new Date().toISOString();
|
|
269
|
+
for (const entry of this.state.pendingRemoteCommands.values()) {
|
|
270
|
+
this.writeEvent('command', 'command.remote.cleared', {
|
|
271
|
+
remoteRequestId: entry.remoteRequestId,
|
|
272
|
+
commandId: entry.commandId,
|
|
273
|
+
payloadEncoding: entry.payloadEncoding,
|
|
274
|
+
payload: entry.payload,
|
|
275
|
+
receivedAt: entry.receivedAt,
|
|
276
|
+
connection: entry.connection,
|
|
277
|
+
reason,
|
|
278
|
+
clearedAt,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
this.state.pendingRemoteCommands.clear();
|
|
282
|
+
}
|
|
190
283
|
noteIncomingConnection(payload) {
|
|
191
284
|
if (this.state.connection.state === 'connected') {
|
|
192
285
|
return;
|
|
@@ -198,6 +291,39 @@ class HostServer {
|
|
|
198
291
|
connectedAt: new Date().toISOString(),
|
|
199
292
|
};
|
|
200
293
|
}
|
|
294
|
+
handleIncomingConnectionDisconnected(payload) {
|
|
295
|
+
if (this.state.connection.state === 'idle' || this.state.connection.state === 'disconnected') {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
this.clearPendingRemoteCommands('connection_disconnect');
|
|
299
|
+
const disconnectedAt = new Date().toISOString();
|
|
300
|
+
this.state.connection = {
|
|
301
|
+
state: 'disconnected',
|
|
302
|
+
direction: 'incoming',
|
|
303
|
+
peerId: this.state.connection.peerId,
|
|
304
|
+
connectedAt: this.state.connection.connectedAt,
|
|
305
|
+
disconnectedAt,
|
|
306
|
+
reasonCode: typeof payload.reasonCode === 'string' && payload.reasonCode.length > 0
|
|
307
|
+
? payload.reasonCode
|
|
308
|
+
: undefined,
|
|
309
|
+
};
|
|
310
|
+
for (const stream of this.state.streams.values()) {
|
|
311
|
+
if (stream.direction !== 'send') {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (stream.state !== 'active') {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
stream.state = 'pending_on_connection';
|
|
318
|
+
stream.updatedAt = disconnectedAt;
|
|
319
|
+
this.writeEvent('stream', 'state.pending_on_reconnect', {
|
|
320
|
+
streamId: stream.streamId,
|
|
321
|
+
media: stream.media,
|
|
322
|
+
direction: stream.direction,
|
|
323
|
+
at: disconnectedAt,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
201
327
|
maybeAutoStartStreamFromRemoteDemand(streamId, media, payload) {
|
|
202
328
|
const key = streamBindingKey(streamId, media);
|
|
203
329
|
const bound = this.state.streamSourceBindings.get(key);
|
|
@@ -270,8 +396,13 @@ class HostServer {
|
|
|
270
396
|
return this.handleOutputAttach((0, HostProtocol_1.parseOutputAttachParams)(params));
|
|
271
397
|
case 'output/detach':
|
|
272
398
|
return this.handleOutputDetach((0, HostProtocol_1.parseOutputDetachParams)(params));
|
|
399
|
+
case 'command/request':
|
|
273
400
|
case 'command/send':
|
|
274
401
|
return this.handleCommandSend((0, HostProtocol_1.parseCommandSendParams)(params));
|
|
402
|
+
case 'command/reply':
|
|
403
|
+
return this.handleCommandReply((0, HostProtocol_1.parseCommandReplyParams)(params));
|
|
404
|
+
case 'command/pending/list':
|
|
405
|
+
return this.handleCommandPendingList();
|
|
275
406
|
case 'events/subscribe':
|
|
276
407
|
return this.handleEventsSubscribe((0, HostProtocol_1.parseEventsSubscribeParams)(params));
|
|
277
408
|
case 'artifact/readReport':
|
|
@@ -311,6 +442,7 @@ class HostServer {
|
|
|
311
442
|
};
|
|
312
443
|
}
|
|
313
444
|
async handleHostShutdown() {
|
|
445
|
+
this.clearPendingRemoteCommands('host_shutdown');
|
|
314
446
|
return {
|
|
315
447
|
acceptedAt: new Date().toISOString(),
|
|
316
448
|
service: this.state.service,
|
|
@@ -381,6 +513,7 @@ class HostServer {
|
|
|
381
513
|
};
|
|
382
514
|
}
|
|
383
515
|
await this.runtimeAdapter.stopService();
|
|
516
|
+
this.clearPendingRemoteCommands('service_stop');
|
|
384
517
|
this.state.service.state = 'stopped';
|
|
385
518
|
this.state.service.stoppedAt = new Date().toISOString();
|
|
386
519
|
return {
|
|
@@ -507,6 +640,7 @@ class HostServer {
|
|
|
507
640
|
};
|
|
508
641
|
}
|
|
509
642
|
await this.runtimeAdapter.disconnect();
|
|
643
|
+
this.clearPendingRemoteCommands('connection_disconnect');
|
|
510
644
|
this.state.connection.state = 'disconnected';
|
|
511
645
|
this.state.connection.disconnectedAt = new Date().toISOString();
|
|
512
646
|
return {
|
|
@@ -631,15 +765,51 @@ class HostServer {
|
|
|
631
765
|
};
|
|
632
766
|
}
|
|
633
767
|
async handleCommandSend(params) {
|
|
634
|
-
const response = await this.runtimeAdapter.sendCommand(params.commandId, params.
|
|
768
|
+
const response = await this.runtimeAdapter.sendCommand(params.commandId, params.payloadEncoding, params.payload, params.timeoutMs);
|
|
635
769
|
return {
|
|
636
770
|
sequenceNumber: this.commandSequence++,
|
|
637
771
|
commandId: params.commandId,
|
|
638
|
-
kind: params.kind,
|
|
639
772
|
acceptedAt: new Date().toISOString(),
|
|
640
773
|
response,
|
|
641
774
|
};
|
|
642
775
|
}
|
|
776
|
+
async handleCommandReply(params) {
|
|
777
|
+
const entry = this.state.pendingRemoteCommands.get(params.remoteRequestId);
|
|
778
|
+
if (!entry) {
|
|
779
|
+
throw new HostProtocol_1.HostProtocolError('invalid_request', `remoteRequestId ${params.remoteRequestId} is not pending`, false);
|
|
780
|
+
}
|
|
781
|
+
if (entry.commandId !== params.commandId) {
|
|
782
|
+
throw new HostProtocol_1.HostProtocolError('invalid_request', 'commandId does not match pending entry', false);
|
|
783
|
+
}
|
|
784
|
+
if (this.state.connection.state !== 'connected') {
|
|
785
|
+
throw new HostProtocol_1.HostProtocolError('connection_not_ready', 'connection is not established', true);
|
|
786
|
+
}
|
|
787
|
+
await this.runtimeAdapter.replyRemoteCommand(params.remoteRequestId, params.commandId, params.payloadEncoding, params.payload);
|
|
788
|
+
this.state.pendingRemoteCommands.delete(params.remoteRequestId);
|
|
789
|
+
const repliedAt = new Date().toISOString();
|
|
790
|
+
this.writeEvent('command', 'command.remote.replied', {
|
|
791
|
+
remoteRequestId: params.remoteRequestId,
|
|
792
|
+
commandId: params.commandId,
|
|
793
|
+
payloadEncoding: entry.payloadEncoding,
|
|
794
|
+
payload: entry.payload,
|
|
795
|
+
receivedAt: entry.receivedAt,
|
|
796
|
+
connection: entry.connection,
|
|
797
|
+
repliedAt,
|
|
798
|
+
});
|
|
799
|
+
return {
|
|
800
|
+
remoteRequestId: params.remoteRequestId,
|
|
801
|
+
commandId: params.commandId,
|
|
802
|
+
repliedAt,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
async handleCommandPendingList() {
|
|
806
|
+
const items = Array.from(this.state.pendingRemoteCommands.values())
|
|
807
|
+
.sort((left, right) => left.receivedAt.localeCompare(right.receivedAt));
|
|
808
|
+
return {
|
|
809
|
+
items,
|
|
810
|
+
listedAt: new Date().toISOString(),
|
|
811
|
+
};
|
|
812
|
+
}
|
|
643
813
|
async handleEventsSubscribe(params) {
|
|
644
814
|
const families = params.families ?? [];
|
|
645
815
|
for (const family of families) {
|
|
@@ -35,6 +35,19 @@ export interface StreamSourceBinding {
|
|
|
35
35
|
source: StreamSource;
|
|
36
36
|
updatedAt: string;
|
|
37
37
|
}
|
|
38
|
+
export interface CommandPendingConnectionSummary {
|
|
39
|
+
state: 'idle' | 'connecting' | 'accepting' | 'connected' | 'disconnecting' | 'disconnected' | 'failed' | 'missed';
|
|
40
|
+
direction?: 'incoming' | 'outgoing';
|
|
41
|
+
peerId?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface CommandPendingEntry {
|
|
44
|
+
remoteRequestId: number;
|
|
45
|
+
commandId: number;
|
|
46
|
+
payloadEncoding: 'utf8' | 'base64';
|
|
47
|
+
payload: string;
|
|
48
|
+
receivedAt: string;
|
|
49
|
+
connection: CommandPendingConnectionSummary;
|
|
50
|
+
}
|
|
38
51
|
export declare class HostState {
|
|
39
52
|
service: ServiceSnapshot;
|
|
40
53
|
connection: ConnectionSnapshot;
|
|
@@ -42,6 +55,7 @@ export declare class HostState {
|
|
|
42
55
|
subscriptions: Set<string>;
|
|
43
56
|
internalConnections: Set<string>;
|
|
44
57
|
streamSourceBindings: Map<string, StreamSourceBinding>;
|
|
58
|
+
pendingRemoteCommands: Map<number, CommandPendingEntry>;
|
|
45
59
|
protocolVersion: string;
|
|
46
60
|
serverVersion: string;
|
|
47
61
|
runtimeTarget: string;
|
|
@@ -39,7 +39,8 @@ export interface RuntimeAdapter {
|
|
|
39
39
|
stopReceiveStream(streamId: number): Promise<void>;
|
|
40
40
|
attachOutput(streamId: number, consumer: string, mediaView: string, format: string, delivery: string, targetPath?: string, maxFiles?: number): Promise<OutputAttachResult>;
|
|
41
41
|
detachOutput(outputId: string): Promise<void>;
|
|
42
|
-
sendCommand(commandId: number,
|
|
42
|
+
sendCommand(commandId: number, payloadEncoding: string, payload: string, timeoutMs?: number): Promise<RuntimeCommandResponse | undefined>;
|
|
43
|
+
replyRemoteCommand(remoteRequestId: number, commandId: number, payloadEncoding: string, payload: string): Promise<void>;
|
|
43
44
|
}
|
|
44
45
|
type RuntimeBackedAdapterOptions = {
|
|
45
46
|
runtimeBundleRoot: string;
|
|
@@ -88,6 +89,7 @@ export declare class RuntimeBackedAdapter implements RuntimeAdapter {
|
|
|
88
89
|
stopReceiveStream(streamId: number): Promise<void>;
|
|
89
90
|
attachOutput(streamId: number, consumer: string, mediaView: string, format: string, delivery: string, targetPath?: string, maxFiles?: number): Promise<OutputAttachResult>;
|
|
90
91
|
detachOutput(outputId: string): Promise<void>;
|
|
91
|
-
sendCommand(commandId: number,
|
|
92
|
+
sendCommand(commandId: number, payloadEncoding: string, payload: string, timeoutMs?: number): Promise<RuntimeCommandResponse | undefined>;
|
|
93
|
+
replyRemoteCommand(remoteRequestId: number, commandId: number, payloadEncoding: string, payload: string): Promise<void>;
|
|
92
94
|
}
|
|
93
95
|
export {};
|
|
@@ -133,6 +133,26 @@ function resolveStreamAssetsDir(source) {
|
|
|
133
133
|
}
|
|
134
134
|
return assetsDir;
|
|
135
135
|
}
|
|
136
|
+
function decodePayload(payloadEncoding, payload) {
|
|
137
|
+
if (payloadEncoding === 'base64') {
|
|
138
|
+
return Uint8Array.from(Buffer.from(payload, 'base64'));
|
|
139
|
+
}
|
|
140
|
+
return Uint8Array.from(Buffer.from(payload, 'utf8'));
|
|
141
|
+
}
|
|
142
|
+
function encodePayload(payload) {
|
|
143
|
+
const buffer = Buffer.from(payload);
|
|
144
|
+
const utf8Text = buffer.toString('utf8');
|
|
145
|
+
if (Buffer.from(utf8Text, 'utf8').equals(buffer)) {
|
|
146
|
+
return {
|
|
147
|
+
payloadEncoding: 'utf8',
|
|
148
|
+
payload: utf8Text,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
payloadEncoding: 'base64',
|
|
153
|
+
payload: buffer.toString('base64'),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
136
156
|
class RuntimeBackedAdapter {
|
|
137
157
|
runtimeTarget;
|
|
138
158
|
hostEndpoint = 'stdio://runtime-backed';
|
|
@@ -163,6 +183,15 @@ class RuntimeBackedAdapter {
|
|
|
163
183
|
onLogLine: (payload) => {
|
|
164
184
|
this.emit('logs', 'runtime.line', payload);
|
|
165
185
|
},
|
|
186
|
+
onRuntimeEvent: (kind, payload) => {
|
|
187
|
+
if (kind === 'connection.accepted') {
|
|
188
|
+
this.connected = true;
|
|
189
|
+
}
|
|
190
|
+
else if (kind === 'connection.disconnected') {
|
|
191
|
+
this.connected = false;
|
|
192
|
+
}
|
|
193
|
+
this.emit('runtime', kind, payload);
|
|
194
|
+
},
|
|
166
195
|
});
|
|
167
196
|
this.receiveWorker = new RuntimeReceiveWorker_1.RuntimeReceiveWorkerManager({
|
|
168
197
|
repoRoot: this.repoRoot,
|
|
@@ -262,6 +291,7 @@ class RuntimeBackedAdapter {
|
|
|
262
291
|
async startService(serviceEntry, license, timeoutMs) {
|
|
263
292
|
this.assertRuntimeBundleReady();
|
|
264
293
|
const resolvedServiceEntry = this.resolveServiceEntry(serviceEntry);
|
|
294
|
+
const keepaliveSession = await this.sendWorker.ensureServiceSession(resolvedServiceEntry, license);
|
|
265
295
|
this.activeService = {
|
|
266
296
|
serviceEntry: resolvedServiceEntry,
|
|
267
297
|
license,
|
|
@@ -271,7 +301,8 @@ class RuntimeBackedAdapter {
|
|
|
271
301
|
serviceEntry: resolvedServiceEntry,
|
|
272
302
|
licensePresent: license.length > 0,
|
|
273
303
|
timeoutMs,
|
|
274
|
-
senderBootstrapStarted:
|
|
304
|
+
senderBootstrapStarted: keepaliveSession.started,
|
|
305
|
+
workerPid: keepaliveSession.pid,
|
|
275
306
|
at: nowIsoString(),
|
|
276
307
|
});
|
|
277
308
|
return resolvedServiceEntry;
|
|
@@ -404,6 +435,7 @@ class RuntimeBackedAdapter {
|
|
|
404
435
|
if (!this.connected) {
|
|
405
436
|
throw new Error('connection is not established');
|
|
406
437
|
}
|
|
438
|
+
await this.sendWorker.sendStreamMessage(streamId, decodePayload('utf8', payload));
|
|
407
439
|
this.emit('stream', 'message.runtime.send', {
|
|
408
440
|
streamId,
|
|
409
441
|
payloadPreview: payload.slice(0, 120),
|
|
@@ -567,32 +599,38 @@ class RuntimeBackedAdapter {
|
|
|
567
599
|
await this.refreshReceiveWorker();
|
|
568
600
|
this.emit('stream', 'output.detach.requested', { outputId, at: nowIsoString() });
|
|
569
601
|
}
|
|
570
|
-
async sendCommand(commandId,
|
|
602
|
+
async sendCommand(commandId, payloadEncoding, payload, timeoutMs) {
|
|
571
603
|
if (!this.connected) {
|
|
572
604
|
throw new Error('connection is not established');
|
|
573
605
|
}
|
|
574
606
|
this.emit('command', 'send.requested', {
|
|
575
607
|
commandId,
|
|
576
|
-
kind,
|
|
577
608
|
payloadEncoding,
|
|
578
|
-
replyToSequenceNumber,
|
|
579
|
-
awaitResponse: Boolean(awaitResponse),
|
|
580
609
|
timeoutMs,
|
|
581
610
|
at: nowIsoString(),
|
|
582
611
|
});
|
|
583
|
-
if (!awaitResponse) {
|
|
584
|
-
return undefined;
|
|
585
|
-
}
|
|
586
612
|
const sequenceNumber = this.commandSequence;
|
|
587
613
|
this.commandSequence += 1;
|
|
614
|
+
const response = await this.sendWorker.requestCommand(commandId, decodePayload(payloadEncoding, payload), timeoutMs ?? 15000);
|
|
615
|
+
const encodedResponse = encodePayload(response.payload);
|
|
588
616
|
return {
|
|
589
617
|
sequenceNumber,
|
|
590
|
-
payloadEncoding:
|
|
591
|
-
payload:
|
|
592
|
-
evidence: 'runtime-backed-callbacks',
|
|
593
|
-
commandRxCount: 1,
|
|
594
|
-
}),
|
|
618
|
+
payloadEncoding: encodedResponse.payloadEncoding,
|
|
619
|
+
payload: encodedResponse.payload,
|
|
595
620
|
};
|
|
596
621
|
}
|
|
622
|
+
async replyRemoteCommand(remoteRequestId, commandId, payloadEncoding, payload) {
|
|
623
|
+
if (!this.connected) {
|
|
624
|
+
throw new Error('connection is not established');
|
|
625
|
+
}
|
|
626
|
+
this.emit('command', 'reply.requested', {
|
|
627
|
+
remoteRequestId,
|
|
628
|
+
commandId,
|
|
629
|
+
payloadEncoding,
|
|
630
|
+
payloadLength: payload.length,
|
|
631
|
+
at: nowIsoString(),
|
|
632
|
+
});
|
|
633
|
+
await this.sendWorker.replyRemoteCommand(remoteRequestId, commandId, decodePayload(payloadEncoding, payload));
|
|
634
|
+
}
|
|
597
635
|
}
|
|
598
636
|
exports.RuntimeBackedAdapter = RuntimeBackedAdapter;
|
|
@@ -7,6 +7,7 @@ type SendWorkerManagerOptions = {
|
|
|
7
7
|
line: string;
|
|
8
8
|
pid?: number;
|
|
9
9
|
}) => void;
|
|
10
|
+
onRuntimeEvent?: (kind: string, payload: Record<string, unknown>) => void;
|
|
10
11
|
};
|
|
11
12
|
type SendWorkerStartOptions = {
|
|
12
13
|
key: string;
|
|
@@ -20,9 +21,14 @@ export declare class RuntimeSendWorkerManager {
|
|
|
20
21
|
private readonly repoRoot;
|
|
21
22
|
private readonly bindings;
|
|
22
23
|
private readonly onLogLine?;
|
|
24
|
+
private readonly onRuntimeEvent?;
|
|
23
25
|
private readonly bridge;
|
|
24
26
|
private sharedSession?;
|
|
27
|
+
private nextCommandRequestId;
|
|
28
|
+
private readonly pendingCommandRequests;
|
|
25
29
|
constructor(options: SendWorkerManagerOptions);
|
|
30
|
+
private handleNativeEvent;
|
|
31
|
+
private rejectPendingCommandRequests;
|
|
26
32
|
private emitLog;
|
|
27
33
|
private resolveFixtureAssetsDir;
|
|
28
34
|
private buildDesiredConfig;
|
|
@@ -42,5 +48,11 @@ export declare class RuntimeSendWorkerManager {
|
|
|
42
48
|
}>;
|
|
43
49
|
stopStream(key: string): Promise<void>;
|
|
44
50
|
stopAll(): Promise<void>;
|
|
51
|
+
sendStreamMessage(streamId: number, payload: Uint8Array): Promise<void>;
|
|
52
|
+
requestCommand(commandId: number, payload: Uint8Array, timeoutMs: number): Promise<{
|
|
53
|
+
commandId: number;
|
|
54
|
+
payload: Uint8Array;
|
|
55
|
+
}>;
|
|
56
|
+
replyRemoteCommand(remoteRequestId: number, commandId: number, payload: Uint8Array): Promise<void>;
|
|
45
57
|
}
|
|
46
58
|
export {};
|
|
@@ -43,6 +43,25 @@ function pathExists(filePath) {
|
|
|
43
43
|
function nowIsoString() {
|
|
44
44
|
return new Date().toISOString();
|
|
45
45
|
}
|
|
46
|
+
function toSafeInteger(value) {
|
|
47
|
+
if (typeof value === 'number' && Number.isSafeInteger(value)) {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === 'bigint') {
|
|
51
|
+
const normalized = Number(value);
|
|
52
|
+
if (Number.isSafeInteger(normalized)) {
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
58
|
+
const normalized = Number(value);
|
|
59
|
+
if (Number.isSafeInteger(normalized)) {
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
46
65
|
function ensureValidAssetsDir(assetsDir) {
|
|
47
66
|
const resolved = path.resolve(assetsDir);
|
|
48
67
|
let stats;
|
|
@@ -125,15 +144,90 @@ class RuntimeSendWorkerManager {
|
|
|
125
144
|
repoRoot;
|
|
126
145
|
bindings = new Map();
|
|
127
146
|
onLogLine;
|
|
147
|
+
onRuntimeEvent;
|
|
128
148
|
bridge;
|
|
129
149
|
sharedSession;
|
|
150
|
+
nextCommandRequestId = 1;
|
|
151
|
+
pendingCommandRequests = new Map();
|
|
130
152
|
constructor(options) {
|
|
131
153
|
this.repoRoot = options.repoRoot;
|
|
132
154
|
this.onLogLine = options.onLogLine;
|
|
155
|
+
this.onRuntimeEvent = options.onRuntimeEvent;
|
|
133
156
|
this.bridge = new RuntimeHostBridge_1.RuntimeHostBridge({
|
|
134
157
|
repoRoot: options.repoRoot,
|
|
135
158
|
platform: options.platform,
|
|
136
159
|
});
|
|
160
|
+
this.bridge.setUplinkEventHandler((event) => {
|
|
161
|
+
this.handleNativeEvent(event);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
handleNativeEvent(event) {
|
|
165
|
+
this.emitLog('stdout', '[runtime-backed sender] native event kind=' + event.kind +
|
|
166
|
+
' requestId=' + String(event.requestId ?? '') +
|
|
167
|
+
' remoteRequestId=' + String(event.remoteRequestId ?? '') +
|
|
168
|
+
' commandId=' + String(event.commandId ?? '') +
|
|
169
|
+
' errorCode=' + String(event.errorCode ?? '') +
|
|
170
|
+
' reasonCode=' + String(event.reasonCode ?? '') +
|
|
171
|
+
' payloadLength=' + String(event.payload?.byteLength ?? 0));
|
|
172
|
+
if (event.kind === 'command.response') {
|
|
173
|
+
const requestId = Number(event.requestId);
|
|
174
|
+
if (!Number.isInteger(requestId)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const pending = this.pendingCommandRequests.get(requestId);
|
|
178
|
+
if (!pending) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.pendingCommandRequests.delete(requestId);
|
|
182
|
+
if (pending.timer) {
|
|
183
|
+
clearTimeout(pending.timer);
|
|
184
|
+
}
|
|
185
|
+
if ((event.errorCode ?? 0) !== 0) {
|
|
186
|
+
pending.reject(new Error(`uplink command request failed error=${String(event.errorCode ?? -1)}`));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
pending.resolve({
|
|
190
|
+
commandId: Number.isInteger(event.commandId) ? Number(event.commandId) : pending.commandId,
|
|
191
|
+
payload: event.payload ?? new Uint8Array(),
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!this.onRuntimeEvent) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (event.kind === 'connection.accepted') {
|
|
199
|
+
this.onRuntimeEvent(event.kind, { at: nowIsoString() });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (event.kind === 'connection.disconnected') {
|
|
203
|
+
this.onRuntimeEvent(event.kind, {
|
|
204
|
+
at: nowIsoString(),
|
|
205
|
+
reasonCode: String(event.reasonCode ?? 0),
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (event.kind === 'command.remote.requested') {
|
|
210
|
+
const remoteRequestId = toSafeInteger(event.remoteRequestId);
|
|
211
|
+
const commandId = toSafeInteger(event.commandId);
|
|
212
|
+
this.onRuntimeEvent(event.kind, {
|
|
213
|
+
at: nowIsoString(),
|
|
214
|
+
remoteRequestId: remoteRequestId ?? String(event.remoteRequestId ?? ''),
|
|
215
|
+
commandId: commandId ?? event.commandId,
|
|
216
|
+
payload: event.payload,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
rejectPendingCommandRequests(message) {
|
|
221
|
+
if (this.pendingCommandRequests.size === 0) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
for (const [requestId, pending] of this.pendingCommandRequests.entries()) {
|
|
225
|
+
if (pending.timer) {
|
|
226
|
+
clearTimeout(pending.timer);
|
|
227
|
+
}
|
|
228
|
+
pending.reject(new Error(`${message} requestId=${requestId}`));
|
|
229
|
+
}
|
|
230
|
+
this.pendingCommandRequests.clear();
|
|
137
231
|
}
|
|
138
232
|
emitLog(stream, line) {
|
|
139
233
|
const normalized = line.trim();
|
|
@@ -201,6 +295,7 @@ class RuntimeSendWorkerManager {
|
|
|
201
295
|
}
|
|
202
296
|
this.bridge.stopUplink();
|
|
203
297
|
this.sharedSession = undefined;
|
|
298
|
+
this.rejectPendingCommandRequests('uplink session stopped');
|
|
204
299
|
this.emitLog('stdout', '[runtime-backed sender] session stopped');
|
|
205
300
|
}
|
|
206
301
|
async ensureSharedSession(serviceEntry, license, keepAliveWhenNoStreams) {
|
|
@@ -287,5 +382,57 @@ class RuntimeSendWorkerManager {
|
|
|
287
382
|
this.bindings.clear();
|
|
288
383
|
await this.stopSharedSession();
|
|
289
384
|
}
|
|
385
|
+
async sendStreamMessage(streamId, payload) {
|
|
386
|
+
if (!this.sharedSession) {
|
|
387
|
+
throw new Error('uplink session is not started');
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
this.bridge.sendUplinkStreamMessage(streamId, payload);
|
|
391
|
+
this.emitLog('stdout', '[runtime-backed sender] send_stream_message accepted streamId=' +
|
|
392
|
+
String(streamId) + ' payloadLength=' + String(payload.byteLength));
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
396
|
+
this.emitLog('stderr', '[runtime-backed sender] send_stream_message failed streamId=' +
|
|
397
|
+
String(streamId) + ' payloadLength=' + String(payload.byteLength) + ' error=' + message);
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async requestCommand(commandId, payload, timeoutMs) {
|
|
402
|
+
if (!this.sharedSession) {
|
|
403
|
+
throw new Error('uplink session is not started');
|
|
404
|
+
}
|
|
405
|
+
const requestId = this.nextCommandRequestId++;
|
|
406
|
+
return await new Promise((resolve, reject) => {
|
|
407
|
+
const pending = {
|
|
408
|
+
commandId,
|
|
409
|
+
resolve,
|
|
410
|
+
reject,
|
|
411
|
+
};
|
|
412
|
+
pending.timer = setTimeout(() => {
|
|
413
|
+
if (!this.pendingCommandRequests.delete(requestId)) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
reject(new Error(`uplink command response timeout requestId=${requestId}`));
|
|
417
|
+
}, Math.max(timeoutMs * 2, timeoutMs + 2000));
|
|
418
|
+
this.pendingCommandRequests.set(requestId, pending);
|
|
419
|
+
try {
|
|
420
|
+
this.bridge.requestUplinkCommand(requestId, commandId, payload, timeoutMs);
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
this.pendingCommandRequests.delete(requestId);
|
|
424
|
+
if (pending.timer) {
|
|
425
|
+
clearTimeout(pending.timer);
|
|
426
|
+
}
|
|
427
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
async replyRemoteCommand(remoteRequestId, commandId, payload) {
|
|
432
|
+
if (!this.sharedSession) {
|
|
433
|
+
throw new Error('uplink session is not started');
|
|
434
|
+
}
|
|
435
|
+
this.bridge.replyUplinkRemoteCommand(remoteRequestId, commandId, payload);
|
|
436
|
+
}
|
|
290
437
|
}
|
|
291
438
|
exports.RuntimeSendWorkerManager = RuntimeSendWorkerManager;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
export type UplinkNativeEvent = {
|
|
2
|
+
kind: string;
|
|
3
|
+
requestId?: number;
|
|
4
|
+
remoteRequestId?: number;
|
|
5
|
+
commandId?: number;
|
|
6
|
+
errorCode?: number;
|
|
7
|
+
reasonCode?: number;
|
|
8
|
+
payload?: Uint8Array;
|
|
9
|
+
};
|
|
1
10
|
type RuntimeHostBridgeOptions = {
|
|
2
11
|
repoRoot: string;
|
|
3
12
|
platform: string;
|
|
@@ -13,6 +22,10 @@ export declare class RuntimeHostBridge {
|
|
|
13
22
|
private ensureBinding;
|
|
14
23
|
startUplink(args: string[]): number;
|
|
15
24
|
stopUplink(): void;
|
|
25
|
+
setUplinkEventHandler(handler: ((event: UplinkNativeEvent) => void) | undefined): void;
|
|
26
|
+
sendUplinkStreamMessage(streamId: number, payload: Uint8Array): void;
|
|
27
|
+
requestUplinkCommand(requestId: number, commandId: number, payload: Uint8Array, timeoutMs: number): void;
|
|
28
|
+
replyUplinkRemoteCommand(remoteRequestId: number, commandId: number, payload: Uint8Array): void;
|
|
16
29
|
startDownlink(args: string[]): number;
|
|
17
30
|
stopDownlink(): void;
|
|
18
31
|
}
|