@mindexec/cli 0.2.393 → 0.2.394
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 +1 -1
- package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
- package/remote-hub.js +74 -2
- package/scripts/remote-fast-live-rate-smoke.mjs +3 -0
- package/scripts/remote-hub-smoke.mjs +72 -20
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/remote-hub.js
CHANGED
|
@@ -23,6 +23,8 @@ const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 24 * 1024 * 1024;
|
|
|
23
23
|
const REMOTE_PROTOCOL_VERSION = 1;
|
|
24
24
|
const MAX_SYNTHETIC_DEVICES = 1000;
|
|
25
25
|
const DEFAULT_HOST_TARGET_LEASE_MS = 30000;
|
|
26
|
+
const DUPLICATE_DEVICE_ACTIVE_REJECT_MS = 15000;
|
|
27
|
+
const DUPLICATE_DEVICE_LOG_THROTTLE_MS = 5000;
|
|
26
28
|
const SYNTHETIC_FRAME_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC';
|
|
27
29
|
const SYNTHETIC_FRAME_PAYLOAD = Buffer.from(SYNTHETIC_FRAME_DATA_URL.split(',')[1], 'base64');
|
|
28
30
|
const SYNTHETIC_FRAME_HASH = crypto.createHash('sha256').update(SYNTHETIC_FRAME_PAYLOAD).digest('hex').slice(0, 16);
|
|
@@ -882,6 +884,7 @@ export function createRemoteHub(options = {}) {
|
|
|
882
884
|
const taskBatches = new Map();
|
|
883
885
|
const sockets = new Map();
|
|
884
886
|
const allSockets = new Set();
|
|
887
|
+
const duplicateDeviceLogAt = new Map();
|
|
885
888
|
let server = null;
|
|
886
889
|
let started = false;
|
|
887
890
|
let boundPort = requestedPort;
|
|
@@ -1281,6 +1284,63 @@ export function createRemoteHub(options = {}) {
|
|
|
1281
1284
|
existing.socket.destroy();
|
|
1282
1285
|
}
|
|
1283
1286
|
|
|
1287
|
+
function getDeviceActivityMs(device) {
|
|
1288
|
+
return Math.max(
|
|
1289
|
+
Date.parse(device?.lastSeenAt || '') || 0,
|
|
1290
|
+
Date.parse(device?.lastStatusAt || '') || 0,
|
|
1291
|
+
Date.parse(device?.connectedAt || '') || 0
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function isDeviceConnectionFresh(device, nowMs = Date.now()) {
|
|
1296
|
+
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
const lastActivityMs = getDeviceActivityMs(device);
|
|
1301
|
+
if (!lastActivityMs) {
|
|
1302
|
+
return true;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const freshnessMs = Math.max(DUPLICATE_DEVICE_ACTIVE_REJECT_MS, heartbeatMs * 3);
|
|
1306
|
+
return nowMs - lastActivityMs <= freshnessMs;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function rejectDuplicateActiveDevice(socket, existing, attemptedSessionId) {
|
|
1310
|
+
const retryAfterMs = Math.max(DUPLICATE_DEVICE_LOG_THROTTLE_MS, Math.min(30000, heartbeatMs));
|
|
1311
|
+
writeJsonLine(socket, {
|
|
1312
|
+
type: 'disconnect',
|
|
1313
|
+
reason: 'duplicate-device-active',
|
|
1314
|
+
retryAfterMs,
|
|
1315
|
+
activeSessionId: existing.sessionId,
|
|
1316
|
+
attemptedSessionId
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
existing.counters.duplicateConnectionsRejected = (existing.counters.duplicateConnectionsRejected || 0) + 1;
|
|
1320
|
+
const nowMs = Date.now();
|
|
1321
|
+
const lastLoggedAt = duplicateDeviceLogAt.get(existing.deviceId) || 0;
|
|
1322
|
+
if (nowMs - lastLoggedAt >= DUPLICATE_DEVICE_LOG_THROTTLE_MS) {
|
|
1323
|
+
duplicateDeviceLogAt.set(existing.deviceId, nowMs);
|
|
1324
|
+
logWarn(
|
|
1325
|
+
'remote',
|
|
1326
|
+
`duplicate device connection suppressed ${existing.deviceName} (${existing.deviceId}); keeping active session ${existing.sessionId}`);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
try {
|
|
1330
|
+
socket.end?.();
|
|
1331
|
+
} catch {
|
|
1332
|
+
// Best-effort graceful close before hard destroy.
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
setTimeout(() => {
|
|
1336
|
+
try {
|
|
1337
|
+
socket.destroy?.();
|
|
1338
|
+
} catch {
|
|
1339
|
+
// Ignore destroy failures.
|
|
1340
|
+
}
|
|
1341
|
+
}, 25).unref?.();
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1284
1344
|
function clearSyntheticFleet() {
|
|
1285
1345
|
let removed = 0;
|
|
1286
1346
|
for (const [deviceId, device] of devices.entries()) {
|
|
@@ -1470,9 +1530,16 @@ export function createRemoteHub(options = {}) {
|
|
|
1470
1530
|
}
|
|
1471
1531
|
|
|
1472
1532
|
function attachDevice(socket, hello) {
|
|
1473
|
-
const
|
|
1533
|
+
const nowMs = Date.now();
|
|
1534
|
+
const now = new Date(nowMs).toISOString();
|
|
1474
1535
|
const sessionId = crypto.randomUUID();
|
|
1475
1536
|
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
1537
|
+
const existing = devices.get(deviceId);
|
|
1538
|
+
|
|
1539
|
+
if (isDeviceConnectionFresh(existing, nowMs)) {
|
|
1540
|
+
rejectDuplicateActiveDevice(socket, existing, sessionId);
|
|
1541
|
+
return null;
|
|
1542
|
+
}
|
|
1476
1543
|
|
|
1477
1544
|
closeExistingDeviceSocket(deviceId, sessionId);
|
|
1478
1545
|
|
|
@@ -2116,8 +2183,13 @@ export function createRemoteHub(options = {}) {
|
|
|
2116
2183
|
return;
|
|
2117
2184
|
}
|
|
2118
2185
|
|
|
2186
|
+
const device = attachDevice(socket, message);
|
|
2187
|
+
if (!device) {
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2119
2191
|
state.authenticated = true;
|
|
2120
|
-
state.device =
|
|
2192
|
+
state.device = device;
|
|
2121
2193
|
return;
|
|
2122
2194
|
}
|
|
2123
2195
|
|
|
@@ -85,6 +85,9 @@ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, label, traceFra
|
|
|
85
85
|
BRIDGE_REQUIRE_TOKEN: '1',
|
|
86
86
|
MINDEXEC_REMOTE_HUB: '1',
|
|
87
87
|
MINDEXEC_REMOTE_TRACE_FRAMES: traceFrames ? '1' : '',
|
|
88
|
+
MINDEXEC_REMOTE_REGISTRY_FOLLOWER: '0',
|
|
89
|
+
MINDEXEC_REMOTE_REGISTRY_REALTIME: '0',
|
|
90
|
+
MINDEXEC_REMOTE_HOST_TARGET_AUTO_RENEW: '0',
|
|
88
91
|
REMOTE_HUB_HOST: '127.0.0.1',
|
|
89
92
|
REMOTE_HUB_PORT: String(remoteHubPort),
|
|
90
93
|
REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
|
|
@@ -515,6 +515,78 @@ try {
|
|
|
515
515
|
});
|
|
516
516
|
assert.equal(staleSessionTask.ok, true);
|
|
517
517
|
const oldSessionId = lateTimedOutDevice.sessionId;
|
|
518
|
+
|
|
519
|
+
const duplicateSocket = net.createConnection({ host: '127.0.0.1', port: status.port });
|
|
520
|
+
duplicateSocket.setEncoding('utf8');
|
|
521
|
+
const duplicateMessages = [];
|
|
522
|
+
let duplicateBuffer = '';
|
|
523
|
+
await new Promise((resolve, reject) => {
|
|
524
|
+
duplicateSocket.once('connect', resolve);
|
|
525
|
+
duplicateSocket.once('error', reject);
|
|
526
|
+
});
|
|
527
|
+
duplicateSocket.on('error', () => { });
|
|
528
|
+
duplicateSocket.on('data', chunk => {
|
|
529
|
+
duplicateBuffer += chunk;
|
|
530
|
+
let lineBreakIndex = duplicateBuffer.indexOf('\n');
|
|
531
|
+
while (lineBreakIndex >= 0) {
|
|
532
|
+
const line = duplicateBuffer.slice(0, lineBreakIndex).trim();
|
|
533
|
+
duplicateBuffer = duplicateBuffer.slice(lineBreakIndex + 1);
|
|
534
|
+
if (line) {
|
|
535
|
+
duplicateMessages.push(JSON.parse(line));
|
|
536
|
+
}
|
|
537
|
+
lineBreakIndex = duplicateBuffer.indexOf('\n');
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
writeJsonLine(duplicateSocket, {
|
|
541
|
+
type: 'hello',
|
|
542
|
+
pairToken: 'smoke-token',
|
|
543
|
+
deviceId: 'smoke-device',
|
|
544
|
+
deviceName: 'Smoke Device Duplicate',
|
|
545
|
+
hostname: 'smoke-host-duplicate',
|
|
546
|
+
platform: process.platform,
|
|
547
|
+
arch: process.arch,
|
|
548
|
+
pid: process.pid,
|
|
549
|
+
agentVersion: '0.0.0-smoke-duplicate',
|
|
550
|
+
capabilities: {
|
|
551
|
+
status: true,
|
|
552
|
+
thumbnail: false,
|
|
553
|
+
control: false,
|
|
554
|
+
liveStream: true,
|
|
555
|
+
computerAgent: true,
|
|
556
|
+
taskDispatch: true,
|
|
557
|
+
aiAssist: false
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const duplicateDisconnect = await waitFor(() => (
|
|
562
|
+
duplicateMessages.find(message => message?.type === 'disconnect')
|
|
563
|
+
), 1000);
|
|
564
|
+
assert.equal(duplicateDisconnect.reason, 'duplicate-device-active');
|
|
565
|
+
assert.equal(duplicateDisconnect.activeSessionId, oldSessionId);
|
|
566
|
+
assert.equal(duplicateDisconnect.retryAfterMs >= 1000, true);
|
|
567
|
+
await wait(100);
|
|
568
|
+
const duplicateRejectedDevice = hub.listDevices()[0];
|
|
569
|
+
assert.equal(duplicateRejectedDevice.sessionId, oldSessionId);
|
|
570
|
+
assert.equal(duplicateRejectedDevice.deviceName, 'Smoke Device');
|
|
571
|
+
assert.equal(duplicateRejectedDevice.connected, true);
|
|
572
|
+
assert.equal(duplicateRejectedDevice.counters.duplicateConnectionsRejected, 1);
|
|
573
|
+
assert.equal(duplicateRejectedDevice.latestTask.taskId, staleSessionTask.taskId);
|
|
574
|
+
duplicateSocket.destroy();
|
|
575
|
+
|
|
576
|
+
socket.destroy();
|
|
577
|
+
const disconnectedDevice = await waitFor(() => {
|
|
578
|
+
const current = hub.listDevices();
|
|
579
|
+
return current.length === 1
|
|
580
|
+
&& current[0]?.sessionId === oldSessionId
|
|
581
|
+
&& current[0]?.connected === false
|
|
582
|
+
? current[0]
|
|
583
|
+
: null;
|
|
584
|
+
});
|
|
585
|
+
assert.equal(disconnectedDevice.latestTask.status, 'failed');
|
|
586
|
+
assert.equal(
|
|
587
|
+
['device-disconnected', 'task-timeout'].includes(disconnectedDevice.latestTask.error),
|
|
588
|
+
true);
|
|
589
|
+
|
|
518
590
|
const replacementSocket = net.createConnection({ host: '127.0.0.1', port: status.port });
|
|
519
591
|
replacementSocket.setEncoding('utf8');
|
|
520
592
|
await new Promise((resolve, reject) => {
|
|
@@ -565,25 +637,6 @@ try {
|
|
|
565
637
|
assert.equal(reconnectedDevice.latestTask, null);
|
|
566
638
|
assert.equal(reconnectedDevice.counters.taskResultsReceived, 0);
|
|
567
639
|
|
|
568
|
-
writeJsonLine(socket, {
|
|
569
|
-
type: 'status',
|
|
570
|
-
status: {
|
|
571
|
-
uptimeSec: 999,
|
|
572
|
-
totalMem: 999,
|
|
573
|
-
freeMem: 0
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
writeJsonLine(socket, {
|
|
577
|
-
type: 'command.result',
|
|
578
|
-
commandId: staleSessionTask.commandId,
|
|
579
|
-
result: {
|
|
580
|
-
kind: 'agent.task',
|
|
581
|
-
taskId: staleSessionTask.taskId,
|
|
582
|
-
status: 'completed',
|
|
583
|
-
summary: 'This old-session result must not mutate the reconnected device.',
|
|
584
|
-
completedAt: new Date().toISOString()
|
|
585
|
-
}
|
|
586
|
-
});
|
|
587
640
|
await wait(100);
|
|
588
641
|
const staleSessionIgnoredDevice = hub.listDevices()[0];
|
|
589
642
|
assert.equal(staleSessionIgnoredDevice.sessionId, reconnectedDevice.sessionId);
|
|
@@ -592,7 +645,6 @@ try {
|
|
|
592
645
|
assert.equal(staleSessionIgnoredDevice.latestTask, null);
|
|
593
646
|
assert.equal(staleSessionIgnoredDevice.counters.taskResultsReceived, 0);
|
|
594
647
|
|
|
595
|
-
socket.destroy();
|
|
596
648
|
replacementSocket.destroy();
|
|
597
649
|
await waitFor(() => hub.listDevices()[0]?.connected === false);
|
|
598
650
|
console.log('RemoteHub smoke OK');
|