@mindexec/cli 0.2.152 → 0.2.153
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/package.json +6 -5
- package/scripts/remote-fleet-render-smoke.mjs +38 -14
- package/scripts/remote-input-ws-smoke.mjs +263 -0
- package/server.js +249 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +314 -45
- package/wwwroot/service-worker-assets.js +2 -2
- package/wwwroot/service-worker.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindexec/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.153",
|
|
4
4
|
"description": "MindExec local runtime and bridge CLI",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -21,14 +21,15 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"start": "node launch-bridge.cjs",
|
|
23
23
|
"dev": "node launch-bridge.cjs --watch",
|
|
24
|
-
"test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-registry-follower-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
|
|
24
|
+
"test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-input-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-registry-follower-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
|
|
25
25
|
"test:auth": "node scripts/auth-session-smoke.mjs",
|
|
26
26
|
"test:remote": "node scripts/remote-hub-smoke.mjs",
|
|
27
27
|
"test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
|
|
28
28
|
"test:remote:render": "node scripts/remote-fleet-render-smoke.mjs",
|
|
29
|
-
"test:remote:http": "node scripts/remote-http-smoke.mjs",
|
|
30
|
-
"test:remote:frame-ws": "node scripts/remote-frame-ws-smoke.mjs",
|
|
31
|
-
"test:remote:
|
|
29
|
+
"test:remote:http": "node scripts/remote-http-smoke.mjs",
|
|
30
|
+
"test:remote:frame-ws": "node scripts/remote-frame-ws-smoke.mjs",
|
|
31
|
+
"test:remote:input-ws": "node scripts/remote-input-ws-smoke.mjs",
|
|
32
|
+
"test:remote:agent-ws": "node scripts/remote-agent-ws-smoke.mjs",
|
|
32
33
|
"test:remote:managed": "node scripts/remote-agent-managed-smoke.mjs",
|
|
33
34
|
"test:remote:registry": "node scripts/remote-registry-follower-smoke.mjs",
|
|
34
35
|
"test:remote:package": "node scripts/remote-agent-package-smoke.mjs",
|
|
@@ -528,7 +528,8 @@ async function loadCss3DManager() {
|
|
|
528
528
|
status: 200,
|
|
529
529
|
json: async () => ({
|
|
530
530
|
bridgeToken: 'render-smoke-bridge-token',
|
|
531
|
-
remoteFrameWsPath: '/api/remote/frames/ws'
|
|
531
|
+
remoteFrameWsPath: '/api/remote/frames/ws',
|
|
532
|
+
remoteInputWsPath: '/api/remote/input/ws'
|
|
532
533
|
})
|
|
533
534
|
};
|
|
534
535
|
},
|
|
@@ -1180,21 +1181,44 @@ try {
|
|
|
1180
1181
|
code: 'KeyA',
|
|
1181
1182
|
repeat: false
|
|
1182
1183
|
});
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1184
|
+
controlShell.dispatchEvent({
|
|
1185
|
+
type: 'keyup',
|
|
1186
|
+
key: 'A',
|
|
1187
|
+
code: 'KeyA',
|
|
1188
|
+
repeat: false
|
|
1189
|
+
});
|
|
1190
|
+
await wait(50);
|
|
1191
|
+
const inputWsSends = diagnostics.webSocketSends
|
|
1192
|
+
.filter(send => send.url.includes('/api/remote/input/ws'))
|
|
1193
|
+
.map(send => {
|
|
1194
|
+
try {
|
|
1195
|
+
return JSON.parse(send.payload);
|
|
1196
|
+
} catch {
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
})
|
|
1200
|
+
.filter(Boolean);
|
|
1201
|
+
assert.ok(inputWsSends.length >= 3, 'control popup should send pointer and keyboard input over the input WebSocket');
|
|
1202
|
+
const pointerDown = inputWsSends.find(message => message.input?.type === 'pointerDown');
|
|
1203
|
+
const keyDown = inputWsSends.find(message => message.input?.type === 'keyDown');
|
|
1204
|
+
const keyUp = inputWsSends.find(message => message.input?.type === 'keyUp');
|
|
1188
1205
|
assert.ok(pointerDown);
|
|
1189
1206
|
assert.ok(keyDown);
|
|
1190
|
-
assert.
|
|
1191
|
-
assert.equal(pointerDown.
|
|
1192
|
-
assert.equal(pointerDown.
|
|
1193
|
-
assert.
|
|
1194
|
-
assert.ok(pointerDown.
|
|
1195
|
-
assert.ok(pointerDown.
|
|
1196
|
-
assert.
|
|
1197
|
-
assert.equal(keyDown.
|
|
1207
|
+
assert.ok(keyUp);
|
|
1208
|
+
assert.equal(pointerDown.nodeId, 'remote-fleet-render-smoke');
|
|
1209
|
+
assert.equal(pointerDown.deviceId, patchTarget.DeviceId);
|
|
1210
|
+
assert.equal(pointerDown.input.button, 'left');
|
|
1211
|
+
assert.ok(pointerDown.input.controlLeaseId);
|
|
1212
|
+
assert.ok(pointerDown.input.normalizedX >= 0 && pointerDown.input.normalizedX <= 1);
|
|
1213
|
+
assert.ok(pointerDown.input.normalizedY >= 0 && pointerDown.input.normalizedY <= 1);
|
|
1214
|
+
assert.equal(keyDown.input.key, 'A');
|
|
1215
|
+
assert.equal(keyDown.input.code, 'KeyA');
|
|
1216
|
+
assert.equal(keyUp.input.key, 'A');
|
|
1217
|
+
assert.equal(keyUp.input.code, 'KeyA');
|
|
1218
|
+
assert.equal(
|
|
1219
|
+
dotNetCalls.filter(call => call.methodName === 'SendRemoteFleetInputFromJs').length,
|
|
1220
|
+
0,
|
|
1221
|
+
'WebSocket input path should not use the slower DotNet input fallback in the happy path');
|
|
1198
1222
|
manager.closeRemoteFleetControlPopupForTest('render-smoke');
|
|
1199
1223
|
await wait(20);
|
|
1200
1224
|
assert.equal(document.body.querySelector('[data-remote-fleet-control-popup="true"]'), null);
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { WebSocket } from 'ws';
|
|
9
|
+
|
|
10
|
+
const BRIDGE_TOKEN = 'remote-input-ws-smoke-token';
|
|
11
|
+
const PAIR_TOKEN = 'remote-input-ws-pair-token';
|
|
12
|
+
const DEVICE_ID = 'remote-input-ws-device';
|
|
13
|
+
|
|
14
|
+
function wait(ms) {
|
|
15
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function waitFor(predicate, timeoutMs = 10000, label = 'condition') {
|
|
19
|
+
const startedAt = Date.now();
|
|
20
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
21
|
+
const value = await predicate();
|
|
22
|
+
if (value) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
await wait(50);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw new Error(`Timed out waiting for ${label}.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function findFreePort() {
|
|
32
|
+
return await new Promise((resolve, reject) => {
|
|
33
|
+
const server = net.createServer();
|
|
34
|
+
server.unref();
|
|
35
|
+
server.once('error', reject);
|
|
36
|
+
server.listen(0, '127.0.0.1', () => {
|
|
37
|
+
const address = server.address();
|
|
38
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
39
|
+
server.close(() => resolve(port));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function fetchJson(url, options = {}) {
|
|
45
|
+
const response = await fetch(url, {
|
|
46
|
+
...options,
|
|
47
|
+
headers: {
|
|
48
|
+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
49
|
+
'X-Bridge-Token': BRIDGE_TOKEN,
|
|
50
|
+
...(options.headers || {})
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const payload = await response.json().catch(() => null);
|
|
54
|
+
return { ok: response.ok, status: response.status, payload };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function waitForBridge(baseUrl, details) {
|
|
58
|
+
const startedAt = Date.now();
|
|
59
|
+
while (Date.now() - startedAt < 30000) {
|
|
60
|
+
try {
|
|
61
|
+
const result = await fetchJson(`${baseUrl}/api/remote/status`);
|
|
62
|
+
if (result.ok && result.payload?.started === true) {
|
|
63
|
+
return result.payload;
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Server is still starting.
|
|
67
|
+
}
|
|
68
|
+
await wait(100);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
throw new Error(`Timed out waiting for LocalBridge.\n${details()}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function spawnBridge({ bridgePort, remoteHubPort }) {
|
|
75
|
+
const bridgeRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
76
|
+
const env = {
|
|
77
|
+
...process.env,
|
|
78
|
+
BRIDGE_PORT: String(bridgePort),
|
|
79
|
+
BRIDGE_TOKEN,
|
|
80
|
+
BRIDGE_REQUIRE_TOKEN: '1',
|
|
81
|
+
MINDEXEC_REMOTE_HUB: '1',
|
|
82
|
+
REMOTE_HUB_HOST: '127.0.0.1',
|
|
83
|
+
REMOTE_HUB_PORT: String(remoteHubPort),
|
|
84
|
+
REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
|
|
85
|
+
NO_COLOR: '1'
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const child = spawn(process.execPath, ['server.js'], {
|
|
89
|
+
cwd: bridgeRoot,
|
|
90
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
91
|
+
windowsHide: true,
|
|
92
|
+
env
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
let stdout = '';
|
|
96
|
+
let stderr = '';
|
|
97
|
+
child.stdout.on('data', chunk => {
|
|
98
|
+
stdout += chunk.toString();
|
|
99
|
+
});
|
|
100
|
+
child.stderr.on('data', chunk => {
|
|
101
|
+
stderr += chunk.toString();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const exitPromise = new Promise(resolve => child.once('exit', resolve));
|
|
105
|
+
const stop = async () => {
|
|
106
|
+
if (child.exitCode === null && !child.killed) {
|
|
107
|
+
child.kill('SIGTERM');
|
|
108
|
+
await Promise.race([exitPromise, wait(5000)]);
|
|
109
|
+
if (child.exitCode === null && !child.killed) {
|
|
110
|
+
child.kill('SIGKILL');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
baseUrl: `http://127.0.0.1:${bridgePort}`,
|
|
117
|
+
details: () => `stdout=${stdout}\nstderr=${stderr}`,
|
|
118
|
+
stop
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function openWebSocket(url) {
|
|
123
|
+
const ws = new WebSocket(url);
|
|
124
|
+
await new Promise((resolve, reject) => {
|
|
125
|
+
ws.once('open', resolve);
|
|
126
|
+
ws.once('error', reject);
|
|
127
|
+
});
|
|
128
|
+
return ws;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function main() {
|
|
132
|
+
const bridgePort = await findFreePort();
|
|
133
|
+
const remoteHubPort = await findFreePort();
|
|
134
|
+
const bridge = spawnBridge({ bridgePort, remoteHubPort });
|
|
135
|
+
let agentWs = null;
|
|
136
|
+
let inputWs = null;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await waitForBridge(bridge.baseUrl, bridge.details);
|
|
140
|
+
|
|
141
|
+
const receivedCommands = [];
|
|
142
|
+
agentWs = await openWebSocket(`ws://127.0.0.1:${remoteHubPort}/remote-agent`);
|
|
143
|
+
agentWs.on('message', data => {
|
|
144
|
+
const text = Buffer.isBuffer(data) ? data.toString('utf8') : String(data || '');
|
|
145
|
+
if (!text.trim().startsWith('{')) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const message = JSON.parse(text);
|
|
150
|
+
if (message.command === 'input.control') {
|
|
151
|
+
receivedCommands.push(message);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
agentWs.send(JSON.stringify({
|
|
155
|
+
type: 'hello',
|
|
156
|
+
pairToken: PAIR_TOKEN,
|
|
157
|
+
deviceId: DEVICE_ID,
|
|
158
|
+
deviceName: 'Remote Input WS Smoke',
|
|
159
|
+
hostname: 'remote-input-ws-smoke',
|
|
160
|
+
platform: process.platform,
|
|
161
|
+
arch: process.arch,
|
|
162
|
+
pid: process.pid,
|
|
163
|
+
agentVersion: '0.0.0-input-ws-smoke',
|
|
164
|
+
runtime: 'node-smoke',
|
|
165
|
+
capabilities: {
|
|
166
|
+
status: true,
|
|
167
|
+
thumbnail: true,
|
|
168
|
+
liveStream: true,
|
|
169
|
+
binaryFrames: true,
|
|
170
|
+
control: true,
|
|
171
|
+
computerAgent: true,
|
|
172
|
+
taskDispatch: true,
|
|
173
|
+
aiAssist: false
|
|
174
|
+
}
|
|
175
|
+
}));
|
|
176
|
+
agentWs.send(JSON.stringify({
|
|
177
|
+
type: 'status',
|
|
178
|
+
status: {
|
|
179
|
+
uptimeSec: 1,
|
|
180
|
+
totalMem: 2,
|
|
181
|
+
freeMem: 1,
|
|
182
|
+
timestamp: new Date().toISOString()
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
await waitFor(async () => {
|
|
187
|
+
const result = await fetchJson(`${bridge.baseUrl}/api/remote/devices`);
|
|
188
|
+
return result.payload?.devices?.find(device => device.deviceId === DEVICE_ID && device.connected === true);
|
|
189
|
+
}, 10000, 'device registration');
|
|
190
|
+
|
|
191
|
+
inputWs = await openWebSocket(`${bridge.baseUrl.replace(/^http:/, 'ws:')}/api/remote/input/ws?token=${encodeURIComponent(BRIDGE_TOKEN)}`);
|
|
192
|
+
inputWs.send(JSON.stringify({
|
|
193
|
+
type: 'input',
|
|
194
|
+
deviceId: DEVICE_ID,
|
|
195
|
+
requestId: 'pointer-move',
|
|
196
|
+
input: {
|
|
197
|
+
type: 'pointerMove',
|
|
198
|
+
normalizedX: 0.42,
|
|
199
|
+
normalizedY: 0.54,
|
|
200
|
+
controlLeaseId: 'input-ws-smoke-lease'
|
|
201
|
+
}
|
|
202
|
+
}));
|
|
203
|
+
inputWs.send(JSON.stringify({
|
|
204
|
+
type: 'input',
|
|
205
|
+
deviceId: DEVICE_ID,
|
|
206
|
+
requestId: 'key-down',
|
|
207
|
+
input: {
|
|
208
|
+
type: 'keyDown',
|
|
209
|
+
key: 'A',
|
|
210
|
+
code: 'KeyA',
|
|
211
|
+
repeat: false,
|
|
212
|
+
controlLeaseId: 'input-ws-smoke-lease'
|
|
213
|
+
}
|
|
214
|
+
}));
|
|
215
|
+
inputWs.send(JSON.stringify({
|
|
216
|
+
type: 'input',
|
|
217
|
+
deviceId: DEVICE_ID,
|
|
218
|
+
requestId: 'key-up',
|
|
219
|
+
input: {
|
|
220
|
+
type: 'keyUp',
|
|
221
|
+
key: 'A',
|
|
222
|
+
code: 'KeyA',
|
|
223
|
+
repeat: false,
|
|
224
|
+
controlLeaseId: 'input-ws-smoke-lease'
|
|
225
|
+
}
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
await waitFor(() => receivedCommands.length >= 3, 10000, 'input.control commands');
|
|
229
|
+
const commandTypes = receivedCommands.map(command => command.payload?.type);
|
|
230
|
+
assert.deepEqual(commandTypes.slice(0, 3), ['pointerMove', 'keyDown', 'keyUp']);
|
|
231
|
+
assert.equal(receivedCommands[0].payload.normalizedX, 0.42);
|
|
232
|
+
assert.equal(receivedCommands[0].payload.normalizedY, 0.54);
|
|
233
|
+
assert.equal(receivedCommands[1].payload.key, 'A');
|
|
234
|
+
assert.equal(receivedCommands[1].payload.code, 'KeyA');
|
|
235
|
+
assert.equal(receivedCommands[2].payload.key, 'A');
|
|
236
|
+
assert.equal(receivedCommands[2].payload.code, 'KeyA');
|
|
237
|
+
|
|
238
|
+
const status = await fetchJson(`${bridge.baseUrl}/api/status?remoteInput=ws`);
|
|
239
|
+
assert.equal(status.ok, true);
|
|
240
|
+
assert.equal(status.payload?.remoteInputWsPath, '/api/remote/input/ws');
|
|
241
|
+
assert.ok(status.payload?.remoteInputWs?.clientCount >= 1, JSON.stringify(status.payload?.remoteInputWs));
|
|
242
|
+
assert.ok(status.payload?.remoteInputWs?.inputsQueued >= 3, JSON.stringify(status.payload?.remoteInputWs));
|
|
243
|
+
assert.ok(status.payload?.remoteInputWs?.pointerMovesQueued >= 1, JSON.stringify(status.payload?.remoteInputWs));
|
|
244
|
+
assert.ok(status.payload?.remoteInputWs?.keyEventsQueued >= 2, JSON.stringify(status.payload?.remoteInputWs));
|
|
245
|
+
assert.equal(status.payload?.remoteInputWs?.lastDeviceId, DEVICE_ID);
|
|
246
|
+
assert.equal(status.payload?.remoteInputWs?.lastInputType, 'keyUp');
|
|
247
|
+
|
|
248
|
+
console.log('Remote input WebSocket smoke OK');
|
|
249
|
+
} finally {
|
|
250
|
+
if (inputWs && inputWs.readyState === WebSocket.OPEN) {
|
|
251
|
+
inputWs.close();
|
|
252
|
+
}
|
|
253
|
+
if (agentWs && agentWs.readyState === WebSocket.OPEN) {
|
|
254
|
+
agentWs.close();
|
|
255
|
+
}
|
|
256
|
+
await bridge.stop();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
main().catch(error => {
|
|
261
|
+
console.error(error);
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
});
|
package/server.js
CHANGED
|
@@ -2125,10 +2125,13 @@ app.get('/api/local-files/:id', async (req, res) => {
|
|
|
2125
2125
|
const httpServer = createServer(app);
|
|
2126
2126
|
const wss = new WebSocketServer({ noServer: true });
|
|
2127
2127
|
const remoteFrameWss = new WebSocketServer({ noServer: true });
|
|
2128
|
+
const remoteInputWss = new WebSocketServer({ noServer: true });
|
|
2128
2129
|
const wsClients = new Set();
|
|
2129
2130
|
const remoteFrameClients = new Set();
|
|
2131
|
+
const remoteInputClients = new Set();
|
|
2130
2132
|
const shellJobs = new Map();
|
|
2131
2133
|
let remoteFrameClientSeq = 0;
|
|
2134
|
+
let remoteInputClientSeq = 0;
|
|
2132
2135
|
const remoteFrameWsDiagnostics = {
|
|
2133
2136
|
opened: 0,
|
|
2134
2137
|
closed: 0,
|
|
@@ -2157,6 +2160,25 @@ const remoteFrameWsDiagnostics = {
|
|
|
2157
2160
|
lastAutoStartStarted: 0,
|
|
2158
2161
|
lastAutoStartSkipped: 0
|
|
2159
2162
|
};
|
|
2163
|
+
const remoteInputWsDiagnostics = {
|
|
2164
|
+
opened: 0,
|
|
2165
|
+
closed: 0,
|
|
2166
|
+
errors: 0,
|
|
2167
|
+
messagesReceived: 0,
|
|
2168
|
+
messagesInvalid: 0,
|
|
2169
|
+
inputsQueued: 0,
|
|
2170
|
+
inputsFailed: 0,
|
|
2171
|
+
pointerMovesQueued: 0,
|
|
2172
|
+
keyEventsQueued: 0,
|
|
2173
|
+
lastOpenAt: '',
|
|
2174
|
+
lastCloseAt: '',
|
|
2175
|
+
lastErrorAt: '',
|
|
2176
|
+
lastInputAt: '',
|
|
2177
|
+
lastDeviceId: '',
|
|
2178
|
+
lastInputType: '',
|
|
2179
|
+
lastCommandId: '',
|
|
2180
|
+
lastError: ''
|
|
2181
|
+
};
|
|
2160
2182
|
const REMOTE_FRAME_WS_AUTO_START_LIMIT = 120;
|
|
2161
2183
|
const REMOTE_FRAME_WS_DEFAULT_FPS = 12;
|
|
2162
2184
|
const REMOTE_FRAME_WS_DEFAULT_MAX_WIDTH = 960;
|
|
@@ -2183,6 +2205,14 @@ httpServer.on('upgrade', (req, socket, head) => {
|
|
|
2183
2205
|
return;
|
|
2184
2206
|
}
|
|
2185
2207
|
|
|
2208
|
+
if (parsed.pathname === '/api/remote/input/ws'
|
|
2209
|
+
&& (!bridgeAuthRequired || token === bridgeToken)) {
|
|
2210
|
+
remoteInputWss.handleUpgrade(req, socket, head, (ws) => {
|
|
2211
|
+
remoteInputWss.emit('connection', ws, req);
|
|
2212
|
+
});
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2186
2216
|
{
|
|
2187
2217
|
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
2188
2218
|
socket.destroy();
|
|
@@ -2552,6 +2582,195 @@ remoteFrameWss.on('connection', (ws, req) => {
|
|
|
2552
2582
|
});
|
|
2553
2583
|
});
|
|
2554
2584
|
|
|
2585
|
+
function sendRemoteInputClientJson(ws, payload) {
|
|
2586
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
2587
|
+
return false;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
try {
|
|
2591
|
+
ws.send(JSON.stringify(payload));
|
|
2592
|
+
return true;
|
|
2593
|
+
} catch {
|
|
2594
|
+
return false;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
function normalizeRemoteInputWsPayload(payload = {}) {
|
|
2599
|
+
const outer = payload && typeof payload === 'object' ? payload : {};
|
|
2600
|
+
const inputSource = outer.input && typeof outer.input === 'object'
|
|
2601
|
+
? outer.input
|
|
2602
|
+
: outer.Input && typeof outer.Input === 'object'
|
|
2603
|
+
? outer.Input
|
|
2604
|
+
: outer;
|
|
2605
|
+
const input = { ...inputSource };
|
|
2606
|
+
input.type = String(input.type ?? input.Type ?? '').trim();
|
|
2607
|
+
const deviceId = String(
|
|
2608
|
+
outer.deviceId
|
|
2609
|
+
?? outer.DeviceId
|
|
2610
|
+
?? input.deviceId
|
|
2611
|
+
?? input.DeviceId
|
|
2612
|
+
?? ''
|
|
2613
|
+
).trim();
|
|
2614
|
+
delete input.deviceId;
|
|
2615
|
+
delete input.DeviceId;
|
|
2616
|
+
|
|
2617
|
+
return {
|
|
2618
|
+
deviceId,
|
|
2619
|
+
input,
|
|
2620
|
+
requestId: String(outer.requestId ?? outer.RequestId ?? '').trim(),
|
|
2621
|
+
ack: outer.ack === true || outer.Ack === true
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
function serializeRemoteInputWsDiagnostics() {
|
|
2626
|
+
const clients = [];
|
|
2627
|
+
for (const client of remoteInputClients) {
|
|
2628
|
+
clients.push({
|
|
2629
|
+
id: client.remoteInputClientId || '',
|
|
2630
|
+
readyState: client.readyState,
|
|
2631
|
+
connectedAt: client.remoteInputConnectedAt || '',
|
|
2632
|
+
received: Number(client.remoteInputReceivedCount || 0),
|
|
2633
|
+
queued: Number(client.remoteInputQueuedCount || 0),
|
|
2634
|
+
failed: Number(client.remoteInputFailedCount || 0),
|
|
2635
|
+
bufferedAmount: Number(client.bufferedAmount || 0),
|
|
2636
|
+
lastInputAt: client.remoteInputLastInputAt || '',
|
|
2637
|
+
lastDeviceId: client.remoteInputLastDeviceId || '',
|
|
2638
|
+
lastInputType: client.remoteInputLastInputType || ''
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
return {
|
|
2643
|
+
protocol: 'mindexec.remote.input.json.v1',
|
|
2644
|
+
path: '/api/remote/input/ws',
|
|
2645
|
+
clientCount: remoteInputClients.size,
|
|
2646
|
+
...remoteInputWsDiagnostics,
|
|
2647
|
+
clients
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
remoteInputWss.on('connection', (ws) => {
|
|
2652
|
+
remoteInputClients.add(ws);
|
|
2653
|
+
ws.remoteInputClientId = `riws-${++remoteInputClientSeq}`;
|
|
2654
|
+
ws.remoteInputConnectedAt = new Date().toISOString();
|
|
2655
|
+
ws.remoteInputReceivedCount = 0;
|
|
2656
|
+
ws.remoteInputQueuedCount = 0;
|
|
2657
|
+
ws.remoteInputFailedCount = 0;
|
|
2658
|
+
remoteInputWsDiagnostics.opened += 1;
|
|
2659
|
+
remoteInputWsDiagnostics.lastOpenAt = ws.remoteInputConnectedAt;
|
|
2660
|
+
|
|
2661
|
+
ws.on('message', data => {
|
|
2662
|
+
const receivedAt = new Date().toISOString();
|
|
2663
|
+
remoteInputWsDiagnostics.messagesReceived += 1;
|
|
2664
|
+
ws.remoteInputReceivedCount = (ws.remoteInputReceivedCount || 0) + 1;
|
|
2665
|
+
|
|
2666
|
+
let payload = null;
|
|
2667
|
+
try {
|
|
2668
|
+
const text = Buffer.isBuffer(data) ? data.toString('utf8') : String(data || '');
|
|
2669
|
+
payload = JSON.parse(text);
|
|
2670
|
+
} catch {
|
|
2671
|
+
remoteInputWsDiagnostics.messagesInvalid += 1;
|
|
2672
|
+
sendRemoteInputClientJson(ws, {
|
|
2673
|
+
type: 'RemoteInputError',
|
|
2674
|
+
timestamp: receivedAt,
|
|
2675
|
+
error: 'invalid-json'
|
|
2676
|
+
});
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
const messageType = String(payload?.type || payload?.Type || '').trim().toLowerCase();
|
|
2681
|
+
if (messageType === 'ping') {
|
|
2682
|
+
sendRemoteInputClientJson(ws, {
|
|
2683
|
+
type: 'RemoteInputPong',
|
|
2684
|
+
timestamp: receivedAt
|
|
2685
|
+
});
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
if (messageType !== 'input' && messageType !== 'remote.input') {
|
|
2690
|
+
remoteInputWsDiagnostics.messagesInvalid += 1;
|
|
2691
|
+
sendRemoteInputClientJson(ws, {
|
|
2692
|
+
type: 'RemoteInputError',
|
|
2693
|
+
timestamp: receivedAt,
|
|
2694
|
+
error: 'unsupported-message'
|
|
2695
|
+
});
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
const normalized = normalizeRemoteInputWsPayload(payload);
|
|
2700
|
+
if (!normalized.deviceId || !normalized.input.type) {
|
|
2701
|
+
remoteInputWsDiagnostics.messagesInvalid += 1;
|
|
2702
|
+
sendRemoteInputClientJson(ws, {
|
|
2703
|
+
type: 'RemoteInputResult',
|
|
2704
|
+
timestamp: receivedAt,
|
|
2705
|
+
ok: false,
|
|
2706
|
+
requestId: normalized.requestId,
|
|
2707
|
+
deviceId: normalized.deviceId,
|
|
2708
|
+
inputType: normalized.input.type,
|
|
2709
|
+
error: !normalized.deviceId ? 'missing-device-id' : 'missing-input-type'
|
|
2710
|
+
});
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
const result = remoteHub.sendInputControl(normalized.deviceId, normalized.input);
|
|
2715
|
+
const ok = result?.ok === true;
|
|
2716
|
+
const inputType = String(normalized.input.type || '').trim();
|
|
2717
|
+
remoteInputWsDiagnostics.lastInputAt = receivedAt;
|
|
2718
|
+
remoteInputWsDiagnostics.lastDeviceId = normalized.deviceId;
|
|
2719
|
+
remoteInputWsDiagnostics.lastInputType = inputType;
|
|
2720
|
+
remoteInputWsDiagnostics.lastCommandId = String(result?.commandId || '');
|
|
2721
|
+
ws.remoteInputLastInputAt = receivedAt;
|
|
2722
|
+
ws.remoteInputLastDeviceId = normalized.deviceId;
|
|
2723
|
+
ws.remoteInputLastInputType = inputType;
|
|
2724
|
+
|
|
2725
|
+
if (ok) {
|
|
2726
|
+
remoteInputWsDiagnostics.inputsQueued += 1;
|
|
2727
|
+
ws.remoteInputQueuedCount = (ws.remoteInputQueuedCount || 0) + 1;
|
|
2728
|
+
if (/^pointermove$/i.test(inputType)) {
|
|
2729
|
+
remoteInputWsDiagnostics.pointerMovesQueued += 1;
|
|
2730
|
+
}
|
|
2731
|
+
if (/^key(up|down)$/i.test(inputType)) {
|
|
2732
|
+
remoteInputWsDiagnostics.keyEventsQueued += 1;
|
|
2733
|
+
}
|
|
2734
|
+
} else {
|
|
2735
|
+
remoteInputWsDiagnostics.inputsFailed += 1;
|
|
2736
|
+
remoteInputWsDiagnostics.lastError = String(result?.error || 'input-failed');
|
|
2737
|
+
ws.remoteInputFailedCount = (ws.remoteInputFailedCount || 0) + 1;
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
const shouldAck = normalized.ack === true || !ok || !/^pointermove$/i.test(inputType);
|
|
2741
|
+
if (shouldAck) {
|
|
2742
|
+
sendRemoteInputClientJson(ws, {
|
|
2743
|
+
type: 'RemoteInputResult',
|
|
2744
|
+
timestamp: receivedAt,
|
|
2745
|
+
ok,
|
|
2746
|
+
requestId: normalized.requestId,
|
|
2747
|
+
deviceId: normalized.deviceId,
|
|
2748
|
+
inputType,
|
|
2749
|
+
commandId: result?.commandId || '',
|
|
2750
|
+
error: result?.error || ''
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
});
|
|
2754
|
+
|
|
2755
|
+
ws.on('close', () => {
|
|
2756
|
+
remoteInputClients.delete(ws);
|
|
2757
|
+
remoteInputWsDiagnostics.closed += 1;
|
|
2758
|
+
remoteInputWsDiagnostics.lastCloseAt = new Date().toISOString();
|
|
2759
|
+
});
|
|
2760
|
+
ws.on('error', error => {
|
|
2761
|
+
remoteInputClients.delete(ws);
|
|
2762
|
+
remoteInputWsDiagnostics.errors += 1;
|
|
2763
|
+
remoteInputWsDiagnostics.lastErrorAt = new Date().toISOString();
|
|
2764
|
+
remoteInputWsDiagnostics.lastError = String(error?.message || error || 'input-ws-error');
|
|
2765
|
+
});
|
|
2766
|
+
sendRemoteInputClientJson(ws, {
|
|
2767
|
+
type: 'RemoteInputSocketReady',
|
|
2768
|
+
timestamp: new Date().toISOString(),
|
|
2769
|
+
protocol: 'mindexec.remote.input.json.v1',
|
|
2770
|
+
clientId: ws.remoteInputClientId
|
|
2771
|
+
});
|
|
2772
|
+
});
|
|
2773
|
+
|
|
2555
2774
|
// Helper: Validate path is within workspace
|
|
2556
2775
|
function validatePath(requestedPath) {
|
|
2557
2776
|
const fullPath = resolveWorkspaceBoundPath(requestedPath);
|
|
@@ -10279,11 +10498,13 @@ app.get('/api/status', async (req, res) => {
|
|
|
10279
10498
|
wsToken,
|
|
10280
10499
|
wsPath: '/events',
|
|
10281
10500
|
remoteFrameWsPath: '/api/remote/frames/ws',
|
|
10501
|
+
remoteInputWsPath: '/api/remote/input/ws',
|
|
10282
10502
|
bridgeToken,
|
|
10283
10503
|
bridgeTokenHeader,
|
|
10284
10504
|
bridgeAuthRequired,
|
|
10285
10505
|
remoteHub: remoteHub.getStatus({ includeSecrets: false }),
|
|
10286
10506
|
remoteFrameWs: serializeRemoteFrameWsDiagnostics(),
|
|
10507
|
+
remoteInputWs: serializeRemoteInputWsDiagnostics(),
|
|
10287
10508
|
remoteAgent: serializeRemoteAgentState(),
|
|
10288
10509
|
remoteRegistryFollower: serializeRemoteRegistryFollowerState(),
|
|
10289
10510
|
remoteRegistryRealtime: serializeRemoteRegistryRealtimeState(),
|
|
@@ -12127,6 +12348,34 @@ async function shutdownBridge(signal) {
|
|
|
12127
12348
|
// Ignore if already closed
|
|
12128
12349
|
}
|
|
12129
12350
|
|
|
12351
|
+
for (const client of remoteFrameClients) {
|
|
12352
|
+
try {
|
|
12353
|
+
client.close();
|
|
12354
|
+
} catch {
|
|
12355
|
+
// Ignore close race
|
|
12356
|
+
}
|
|
12357
|
+
}
|
|
12358
|
+
|
|
12359
|
+
for (const client of remoteInputClients) {
|
|
12360
|
+
try {
|
|
12361
|
+
client.close();
|
|
12362
|
+
} catch {
|
|
12363
|
+
// Ignore close race
|
|
12364
|
+
}
|
|
12365
|
+
}
|
|
12366
|
+
|
|
12367
|
+
try {
|
|
12368
|
+
remoteFrameWss.close();
|
|
12369
|
+
} catch {
|
|
12370
|
+
// Ignore if already closed
|
|
12371
|
+
}
|
|
12372
|
+
|
|
12373
|
+
try {
|
|
12374
|
+
remoteInputWss.close();
|
|
12375
|
+
} catch {
|
|
12376
|
+
// Ignore if already closed
|
|
12377
|
+
}
|
|
12378
|
+
|
|
12130
12379
|
try {
|
|
12131
12380
|
stopRemoteRegistryFollower();
|
|
12132
12381
|
} catch {
|
|
@@ -13494,6 +13494,8 @@
|
|
|
13494
13494
|
const REMOTE_FLEET_LIVE_FRAME_REFRESH_MS = Math.round(1000 / REMOTE_FLEET_MONITOR_LIVE_FPS);
|
|
13495
13495
|
const REMOTE_FLEET_CONTROL_LIVE_FPS = 20;
|
|
13496
13496
|
const REMOTE_FLEET_CONTROL_FRAME_REFRESH_MS = Math.round(1000 / REMOTE_FLEET_CONTROL_LIVE_FPS);
|
|
13497
|
+
const REMOTE_FLEET_CONTROL_INPUT_MOVE_MS = 16;
|
|
13498
|
+
const REMOTE_FLEET_CONTROL_INPUT_QUEUE_LIMIT = 64;
|
|
13497
13499
|
const REMOTE_FLEET_THUMBNAIL_FRAME_REFRESH_MS = 2000;
|
|
13498
13500
|
const REMOTE_FLEET_FRAME_CANVAS_MAX_DPR = 2;
|
|
13499
13501
|
const REMOTE_FLEET_FRAME_BLOB_CACHE_MS = 10000;
|
|
@@ -13829,6 +13831,22 @@
|
|
|
13829
13831
|
return url.toString();
|
|
13830
13832
|
}
|
|
13831
13833
|
|
|
13834
|
+
function buildRemoteFleetInputWsUrl(status) {
|
|
13835
|
+
const baseUrl = String(status?.__remoteFleetBridgeBaseUrl || window.location?.origin || '').trim();
|
|
13836
|
+
const path = String(status?.remoteInputWsPath || '/api/remote/input/ws').trim() || '/api/remote/input/ws';
|
|
13837
|
+
if (!baseUrl) {
|
|
13838
|
+
return '';
|
|
13839
|
+
}
|
|
13840
|
+
|
|
13841
|
+
const url = new URL(path, baseUrl);
|
|
13842
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
13843
|
+
const bridgeToken = String(status?.bridgeToken || '').trim();
|
|
13844
|
+
if (bridgeToken) {
|
|
13845
|
+
url.searchParams.set('token', bridgeToken);
|
|
13846
|
+
}
|
|
13847
|
+
return url.toString();
|
|
13848
|
+
}
|
|
13849
|
+
|
|
13832
13850
|
async function parseRemoteFleetBinaryFrameMessage(data) {
|
|
13833
13851
|
let buffer = null;
|
|
13834
13852
|
if (data instanceof ArrayBuffer) {
|
|
@@ -15729,6 +15747,8 @@
|
|
|
15729
15747
|
activeRemoteFleetControlPopup = null;
|
|
15730
15748
|
session.active = false;
|
|
15731
15749
|
detachRemoteFleetControlBinaryFrameSession(session);
|
|
15750
|
+
closeRemoteFleetControlInputSocket(session, 'close');
|
|
15751
|
+
session.inputCleanup?.();
|
|
15732
15752
|
if (session.timer) {
|
|
15733
15753
|
clearTimeout(session.timer);
|
|
15734
15754
|
session.timer = null;
|
|
@@ -15910,34 +15930,268 @@
|
|
|
15910
15930
|
}
|
|
15911
15931
|
}
|
|
15912
15932
|
|
|
15913
|
-
function
|
|
15914
|
-
if (!session
|
|
15933
|
+
function closeRemoteFleetControlInputSocket(session, reason = 'close') {
|
|
15934
|
+
if (!session) {
|
|
15915
15935
|
return;
|
|
15916
15936
|
}
|
|
15917
15937
|
|
|
15918
|
-
|
|
15919
|
-
|
|
15920
|
-
|
|
15938
|
+
if (session.inputWsOpenTimer) {
|
|
15939
|
+
clearTimeout(session.inputWsOpenTimer);
|
|
15940
|
+
session.inputWsOpenTimer = null;
|
|
15941
|
+
}
|
|
15942
|
+
|
|
15943
|
+
const ws = session.inputWs;
|
|
15944
|
+
session.inputWs = null;
|
|
15945
|
+
session.inputWsConnecting = false;
|
|
15946
|
+
if (ws && ws.readyState !== 3) {
|
|
15947
|
+
try {
|
|
15948
|
+
ws.close();
|
|
15949
|
+
} catch {
|
|
15950
|
+
// Best-effort input socket cleanup.
|
|
15921
15951
|
}
|
|
15922
|
-
|
|
15923
|
-
|
|
15924
|
-
|
|
15925
|
-
|
|
15926
|
-
|
|
15952
|
+
}
|
|
15953
|
+
|
|
15954
|
+
if (reason === 'close') {
|
|
15955
|
+
session.inputQueue = [];
|
|
15956
|
+
}
|
|
15957
|
+
}
|
|
15958
|
+
|
|
15959
|
+
function createRemoteFleetControlInput(session, payload) {
|
|
15960
|
+
return {
|
|
15961
|
+
...payload,
|
|
15962
|
+
controlLeaseId: session.controlLeaseId,
|
|
15963
|
+
issuedAt: new Date().toISOString()
|
|
15964
|
+
};
|
|
15965
|
+
}
|
|
15966
|
+
|
|
15967
|
+
function sendRemoteFleetControlInputFallback(session, input) {
|
|
15968
|
+
if (!session?.active || !input?.type) {
|
|
15969
|
+
return;
|
|
15970
|
+
}
|
|
15971
|
+
|
|
15972
|
+
invokeDotNetAsync('SendRemoteFleetInputFromJs', session.nodeId, session.deviceId, {
|
|
15973
|
+
...input,
|
|
15974
|
+
controlLeaseId: input.controlLeaseId || session.controlLeaseId,
|
|
15975
|
+
issuedAt: input.issuedAt || new Date().toISOString()
|
|
15976
|
+
}).catch(error => {
|
|
15977
|
+
if (session.active) {
|
|
15927
15978
|
session.status.textContent = error?.message || 'Input failed';
|
|
15979
|
+
}
|
|
15980
|
+
});
|
|
15981
|
+
}
|
|
15982
|
+
|
|
15983
|
+
function drainRemoteFleetControlInputQueueToFallback(session) {
|
|
15984
|
+
if (!session?.inputQueue?.length) {
|
|
15985
|
+
return;
|
|
15986
|
+
}
|
|
15987
|
+
|
|
15988
|
+
const queued = session.inputQueue.splice(0);
|
|
15989
|
+
queued.forEach(envelope => sendRemoteFleetControlInputFallback(session, envelope.input));
|
|
15990
|
+
}
|
|
15991
|
+
|
|
15992
|
+
function enqueueRemoteFleetControlInput(session, envelope) {
|
|
15993
|
+
if (!session) {
|
|
15994
|
+
return;
|
|
15995
|
+
}
|
|
15996
|
+
|
|
15997
|
+
if (!Array.isArray(session.inputQueue)) {
|
|
15998
|
+
session.inputQueue = [];
|
|
15999
|
+
}
|
|
16000
|
+
|
|
16001
|
+
const inputType = String(envelope?.input?.type || '').toLowerCase();
|
|
16002
|
+
if (inputType === 'pointermove') {
|
|
16003
|
+
for (let i = session.inputQueue.length - 1; i >= 0; i -= 1) {
|
|
16004
|
+
if (String(session.inputQueue[i]?.input?.type || '').toLowerCase() === 'pointermove') {
|
|
16005
|
+
session.inputQueue.splice(i, 1);
|
|
16006
|
+
}
|
|
16007
|
+
}
|
|
16008
|
+
}
|
|
16009
|
+
|
|
16010
|
+
session.inputQueue.push(envelope);
|
|
16011
|
+
while (session.inputQueue.length > REMOTE_FLEET_CONTROL_INPUT_QUEUE_LIMIT) {
|
|
16012
|
+
const moveIndex = session.inputQueue.findIndex(item =>
|
|
16013
|
+
String(item?.input?.type || '').toLowerCase() === 'pointermove');
|
|
16014
|
+
session.inputQueue.splice(moveIndex >= 0 ? moveIndex : 0, 1);
|
|
16015
|
+
}
|
|
16016
|
+
}
|
|
16017
|
+
|
|
16018
|
+
function flushRemoteFleetControlInputQueue(session) {
|
|
16019
|
+
const ws = session?.inputWs;
|
|
16020
|
+
if (!session?.active || !ws || ws.readyState !== 1 || !session.inputQueue?.length) {
|
|
16021
|
+
return;
|
|
16022
|
+
}
|
|
16023
|
+
|
|
16024
|
+
const queued = session.inputQueue.splice(0);
|
|
16025
|
+
for (const envelope of queued) {
|
|
16026
|
+
if (!sendRemoteFleetControlInputEnvelope(session, envelope)) {
|
|
16027
|
+
enqueueRemoteFleetControlInput(session, envelope);
|
|
16028
|
+
break;
|
|
16029
|
+
}
|
|
16030
|
+
}
|
|
16031
|
+
}
|
|
16032
|
+
|
|
16033
|
+
function sendRemoteFleetControlInputEnvelope(session, envelope) {
|
|
16034
|
+
const ws = session?.inputWs;
|
|
16035
|
+
if (!session?.active || !ws || ws.readyState !== 1) {
|
|
16036
|
+
return false;
|
|
16037
|
+
}
|
|
16038
|
+
|
|
16039
|
+
const inputType = String(envelope?.input?.type || '').toLowerCase();
|
|
16040
|
+
if (inputType === 'pointermove' && Number(ws.bufferedAmount || 0) > 256 * 1024) {
|
|
16041
|
+
window.RuntimeTrace?.emit?.('remote.control.input.drop', {
|
|
16042
|
+
reason: 'ws-buffered',
|
|
16043
|
+
deviceId: session.deviceId,
|
|
16044
|
+
bufferedAmount: Number(ws.bufferedAmount || 0)
|
|
15928
16045
|
});
|
|
16046
|
+
return true;
|
|
16047
|
+
}
|
|
16048
|
+
|
|
16049
|
+
try {
|
|
16050
|
+
ws.send(JSON.stringify(envelope));
|
|
16051
|
+
return true;
|
|
16052
|
+
} catch {
|
|
16053
|
+
return false;
|
|
16054
|
+
}
|
|
16055
|
+
}
|
|
16056
|
+
|
|
16057
|
+
function handleRemoteFleetControlInputSocketFailure(session) {
|
|
16058
|
+
if (!session?.active) {
|
|
16059
|
+
return;
|
|
16060
|
+
}
|
|
16061
|
+
|
|
16062
|
+
closeRemoteFleetControlInputSocket(session, 'failed');
|
|
16063
|
+
session.inputWsUnavailableUntil = Date.now() + 1000;
|
|
16064
|
+
drainRemoteFleetControlInputQueueToFallback(session);
|
|
16065
|
+
}
|
|
16066
|
+
|
|
16067
|
+
function ensureRemoteFleetControlInputSocket(session) {
|
|
16068
|
+
if (!session?.active || typeof WebSocket !== 'function') {
|
|
16069
|
+
return false;
|
|
16070
|
+
}
|
|
16071
|
+
|
|
16072
|
+
const ws = session.inputWs;
|
|
16073
|
+
if (ws && (ws.readyState === 0 || ws.readyState === 1)) {
|
|
16074
|
+
return true;
|
|
16075
|
+
}
|
|
16076
|
+
|
|
16077
|
+
if (session.inputWsConnecting === true) {
|
|
16078
|
+
return true;
|
|
16079
|
+
}
|
|
16080
|
+
|
|
16081
|
+
if ((session.inputWsUnavailableUntil || 0) > Date.now()) {
|
|
16082
|
+
return false;
|
|
16083
|
+
}
|
|
16084
|
+
|
|
16085
|
+
const connectToken = `input-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
16086
|
+
session.inputWsConnecting = true;
|
|
16087
|
+
session.inputWsConnectToken = connectToken;
|
|
16088
|
+
getRemoteFleetBridgeStatusForFrames()
|
|
16089
|
+
.then(status => {
|
|
16090
|
+
if (!session.active || session.inputWsConnectToken !== connectToken) {
|
|
16091
|
+
return;
|
|
16092
|
+
}
|
|
16093
|
+
|
|
16094
|
+
const wsUrl = buildRemoteFleetInputWsUrl(status);
|
|
16095
|
+
if (!wsUrl) {
|
|
16096
|
+
throw new Error('input-ws-url-unavailable');
|
|
16097
|
+
}
|
|
16098
|
+
|
|
16099
|
+
const inputWs = new WebSocket(wsUrl);
|
|
16100
|
+
session.inputWs = inputWs;
|
|
16101
|
+
inputWs.onopen = () => {
|
|
16102
|
+
if (!session.active || session.inputWs !== inputWs) {
|
|
16103
|
+
return;
|
|
16104
|
+
}
|
|
16105
|
+
|
|
16106
|
+
session.inputWsConnecting = false;
|
|
16107
|
+
session.inputWsUnavailableUntil = 0;
|
|
16108
|
+
if (session.inputWsOpenTimer) {
|
|
16109
|
+
clearTimeout(session.inputWsOpenTimer);
|
|
16110
|
+
session.inputWsOpenTimer = null;
|
|
16111
|
+
}
|
|
16112
|
+
flushRemoteFleetControlInputQueue(session);
|
|
16113
|
+
};
|
|
16114
|
+
inputWs.onmessage = event => {
|
|
16115
|
+
try {
|
|
16116
|
+
const message = JSON.parse(String(event?.data || ''));
|
|
16117
|
+
if (message?.ok === false && session.active) {
|
|
16118
|
+
session.status.textContent = message.error || 'Input failed';
|
|
16119
|
+
}
|
|
16120
|
+
} catch {
|
|
16121
|
+
// Ignore input ack parse errors.
|
|
16122
|
+
}
|
|
16123
|
+
};
|
|
16124
|
+
inputWs.onerror = () => {
|
|
16125
|
+
if (session.inputWs === inputWs) {
|
|
16126
|
+
handleRemoteFleetControlInputSocketFailure(session);
|
|
16127
|
+
}
|
|
16128
|
+
};
|
|
16129
|
+
inputWs.onclose = () => {
|
|
16130
|
+
if (session.inputWs === inputWs) {
|
|
16131
|
+
handleRemoteFleetControlInputSocketFailure(session);
|
|
16132
|
+
}
|
|
16133
|
+
};
|
|
16134
|
+
session.inputWsOpenTimer = setTimeout(() => {
|
|
16135
|
+
if (session.active && session.inputWs === inputWs && inputWs.readyState === 0) {
|
|
16136
|
+
handleRemoteFleetControlInputSocketFailure(session);
|
|
16137
|
+
}
|
|
16138
|
+
}, 900);
|
|
16139
|
+
})
|
|
16140
|
+
.catch(() => {
|
|
16141
|
+
if (!session.active || session.inputWsConnectToken !== connectToken) {
|
|
16142
|
+
return;
|
|
16143
|
+
}
|
|
16144
|
+
|
|
16145
|
+
session.inputWsConnecting = false;
|
|
16146
|
+
session.inputWsUnavailableUntil = Date.now() + 1000;
|
|
16147
|
+
drainRemoteFleetControlInputQueueToFallback(session);
|
|
16148
|
+
});
|
|
16149
|
+
|
|
16150
|
+
return true;
|
|
16151
|
+
}
|
|
16152
|
+
|
|
16153
|
+
function sendRemoteFleetControlInputNow(session, payload) {
|
|
16154
|
+
if (!session?.active || !payload?.type) {
|
|
16155
|
+
return;
|
|
16156
|
+
}
|
|
16157
|
+
|
|
16158
|
+
const input = createRemoteFleetControlInput(session, payload);
|
|
16159
|
+
const envelope = {
|
|
16160
|
+
type: 'input',
|
|
16161
|
+
nodeId: session.nodeId,
|
|
16162
|
+
deviceId: session.deviceId,
|
|
16163
|
+
requestId: `input-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
16164
|
+
input
|
|
15929
16165
|
};
|
|
15930
16166
|
|
|
16167
|
+
const wsReady = ensureRemoteFleetControlInputSocket(session);
|
|
16168
|
+
if (wsReady && sendRemoteFleetControlInputEnvelope(session, envelope)) {
|
|
16169
|
+
return;
|
|
16170
|
+
}
|
|
16171
|
+
|
|
16172
|
+
if (wsReady) {
|
|
16173
|
+
enqueueRemoteFleetControlInput(session, envelope);
|
|
16174
|
+
return;
|
|
16175
|
+
}
|
|
16176
|
+
|
|
16177
|
+
sendRemoteFleetControlInputFallback(session, input);
|
|
16178
|
+
}
|
|
16179
|
+
|
|
16180
|
+
function sendRemoteFleetControlInput(session, payload, throttleMove = false) {
|
|
16181
|
+
if (!session?.active || !payload?.type) {
|
|
16182
|
+
return;
|
|
16183
|
+
}
|
|
16184
|
+
|
|
15931
16185
|
if (!throttleMove) {
|
|
15932
|
-
|
|
16186
|
+
sendRemoteFleetControlInputNow(session, payload);
|
|
15933
16187
|
return;
|
|
15934
16188
|
}
|
|
15935
16189
|
|
|
15936
16190
|
const now = performance.now();
|
|
15937
16191
|
const elapsed = now - (session.lastMoveSentAt || 0);
|
|
15938
|
-
if (elapsed >=
|
|
16192
|
+
if (elapsed >= REMOTE_FLEET_CONTROL_INPUT_MOVE_MS) {
|
|
15939
16193
|
session.lastMoveSentAt = now;
|
|
15940
|
-
|
|
16194
|
+
sendRemoteFleetControlInputNow(session, payload);
|
|
15941
16195
|
return;
|
|
15942
16196
|
}
|
|
15943
16197
|
|
|
@@ -15952,9 +16206,9 @@
|
|
|
15952
16206
|
session.pendingMove = null;
|
|
15953
16207
|
if (next) {
|
|
15954
16208
|
session.lastMoveSentAt = performance.now();
|
|
15955
|
-
|
|
16209
|
+
sendRemoteFleetControlInputNow(session, next);
|
|
15956
16210
|
}
|
|
15957
|
-
}, Math.max(1,
|
|
16211
|
+
}, Math.max(1, REMOTE_FLEET_CONTROL_INPUT_MOVE_MS - elapsed));
|
|
15958
16212
|
}
|
|
15959
16213
|
|
|
15960
16214
|
function bindRemoteFleetControlInput(session) {
|
|
@@ -15965,6 +16219,40 @@
|
|
|
15965
16219
|
overlay.addEventListener(eventName, event => event.stopPropagation(), true);
|
|
15966
16220
|
});
|
|
15967
16221
|
|
|
16222
|
+
const shouldIgnoreKeyboardEvent = event =>
|
|
16223
|
+
event?.target?.closest?.('[data-remote-fleet-control-close="true"]');
|
|
16224
|
+
|
|
16225
|
+
const handleKeyboardEvent = (event, type) => {
|
|
16226
|
+
if (!session.active || shouldIgnoreKeyboardEvent(event) || event.__remoteFleetControlHandled === true) {
|
|
16227
|
+
return;
|
|
16228
|
+
}
|
|
16229
|
+
|
|
16230
|
+
event.__remoteFleetControlHandled = true;
|
|
16231
|
+
event.preventDefault();
|
|
16232
|
+
event.stopPropagation();
|
|
16233
|
+
shell.focus({ preventScroll: true });
|
|
16234
|
+
sendRemoteFleetControlInput(session, {
|
|
16235
|
+
type,
|
|
16236
|
+
key: event.key,
|
|
16237
|
+
code: event.code,
|
|
16238
|
+
repeat: event.repeat === true
|
|
16239
|
+
});
|
|
16240
|
+
};
|
|
16241
|
+
|
|
16242
|
+
const handleWindowKeyDown = event => handleKeyboardEvent(event, 'keyDown');
|
|
16243
|
+
const handleWindowKeyUp = event => handleKeyboardEvent(event, 'keyUp');
|
|
16244
|
+
if (typeof window.addEventListener === 'function') {
|
|
16245
|
+
window.addEventListener('keydown', handleWindowKeyDown, true);
|
|
16246
|
+
window.addEventListener('keyup', handleWindowKeyUp, true);
|
|
16247
|
+
}
|
|
16248
|
+
session.inputCleanup = () => {
|
|
16249
|
+
if (typeof window.removeEventListener === 'function') {
|
|
16250
|
+
window.removeEventListener('keydown', handleWindowKeyDown, true);
|
|
16251
|
+
window.removeEventListener('keyup', handleWindowKeyUp, true);
|
|
16252
|
+
}
|
|
16253
|
+
session.inputCleanup = null;
|
|
16254
|
+
};
|
|
16255
|
+
|
|
15968
16256
|
stage.addEventListener('pointerdown', event => {
|
|
15969
16257
|
const point = getRemoteFleetControlPoint(event, canvas, false);
|
|
15970
16258
|
if (!point) {
|
|
@@ -16041,35 +16329,8 @@
|
|
|
16041
16329
|
event.stopPropagation();
|
|
16042
16330
|
});
|
|
16043
16331
|
|
|
16044
|
-
shell.addEventListener('keydown', event =>
|
|
16045
|
-
|
|
16046
|
-
return;
|
|
16047
|
-
}
|
|
16048
|
-
|
|
16049
|
-
event.preventDefault();
|
|
16050
|
-
event.stopPropagation();
|
|
16051
|
-
sendRemoteFleetControlInput(session, {
|
|
16052
|
-
type: 'keyDown',
|
|
16053
|
-
key: event.key,
|
|
16054
|
-
code: event.code,
|
|
16055
|
-
repeat: event.repeat === true
|
|
16056
|
-
});
|
|
16057
|
-
});
|
|
16058
|
-
|
|
16059
|
-
shell.addEventListener('keyup', event => {
|
|
16060
|
-
if (event.target?.closest?.('[data-remote-fleet-control-close="true"]')) {
|
|
16061
|
-
return;
|
|
16062
|
-
}
|
|
16063
|
-
|
|
16064
|
-
event.preventDefault();
|
|
16065
|
-
event.stopPropagation();
|
|
16066
|
-
sendRemoteFleetControlInput(session, {
|
|
16067
|
-
type: 'keyUp',
|
|
16068
|
-
key: event.key,
|
|
16069
|
-
code: event.code,
|
|
16070
|
-
repeat: event.repeat === true
|
|
16071
|
-
});
|
|
16072
|
-
});
|
|
16332
|
+
shell.addEventListener('keydown', event => handleKeyboardEvent(event, 'keyDown'));
|
|
16333
|
+
shell.addEventListener('keyup', event => handleKeyboardEvent(event, 'keyUp'));
|
|
16073
16334
|
}
|
|
16074
16335
|
|
|
16075
16336
|
function openRemoteFleetControlPopup(bodyView, nodeModel, deviceId) {
|
|
@@ -16233,10 +16494,18 @@
|
|
|
16233
16494
|
lastMoveSentAt: 0,
|
|
16234
16495
|
lastControlFrameAt: 0,
|
|
16235
16496
|
lastControlFrameSeq: 0,
|
|
16236
|
-
binaryFramePreferred: false
|
|
16497
|
+
binaryFramePreferred: false,
|
|
16498
|
+
inputWs: null,
|
|
16499
|
+
inputWsConnecting: false,
|
|
16500
|
+
inputWsConnectToken: '',
|
|
16501
|
+
inputWsOpenTimer: null,
|
|
16502
|
+
inputWsUnavailableUntil: 0,
|
|
16503
|
+
inputQueue: [],
|
|
16504
|
+
inputCleanup: null
|
|
16237
16505
|
};
|
|
16238
16506
|
activeRemoteFleetControlPopup = session;
|
|
16239
16507
|
bindRemoteFleetControlInput(session);
|
|
16508
|
+
ensureRemoteFleetControlInputSocket(session);
|
|
16240
16509
|
attachRemoteFleetControlBinaryFrameSession(session, bodyView);
|
|
16241
16510
|
|
|
16242
16511
|
const refresh = async () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
self.assetsManifest = {
|
|
2
|
-
"version": "
|
|
2
|
+
"version": "uSz5Xt0z",
|
|
3
3
|
"assets": [
|
|
4
4
|
{
|
|
5
5
|
"hash": "sha256-+CSYMcqLNTsq3VnH11jgYyOCCdxvHzL74CBmo4sCmMU=",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"url": "_content/MindExecution.Shared/js/mind-map-core.js.backup"
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
|
-
"hash": "sha256-
|
|
89
|
+
"hash": "sha256-4BdT/RnBOnh3Mm/hTZilg3zo9az6W9oYf8h3br2aDko=",
|
|
90
90
|
"url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
|
|
91
91
|
},
|
|
92
92
|
{
|