livedesk 0.1.458 → 0.1.459
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/bin/livedesk.js +18 -2
- package/client/package.json +5 -5
- package/diagnostics/livedesk-mode3-capture-smoke.mjs +183 -0
- package/diagnostics/livedesk-transport-physical-core.mjs +503 -0
- package/diagnostics/livedesk-transport-physical.mjs +508 -0
- package/hub/package.json +1 -1
- package/hub/src/remote-hub.js +813 -30
- package/hub/src/server.js +155 -5
- package/hub/src/transport/udp-hub-transport.js +110 -8
- package/hub/src/transport/udp-rendezvous.js +6 -2
- package/package.json +7 -7
package/bin/livedesk.js
CHANGED
|
@@ -334,6 +334,7 @@ Usage:
|
|
|
334
334
|
npx -y --prefer-online livedesk@latest --force-role hub
|
|
335
335
|
npx -y --prefer-online livedesk@latest --force-role client
|
|
336
336
|
npx -y --prefer-online livedesk@latest diagnose mac-physical
|
|
337
|
+
npx -y --prefer-online livedesk@latest diagnose transport-physical
|
|
337
338
|
|
|
338
339
|
Commands:
|
|
339
340
|
No command Resolve the signed-in device role and start the matching runtime.
|
|
@@ -341,6 +342,8 @@ Commands:
|
|
|
341
342
|
client Legacy alias for --force-role client.
|
|
342
343
|
diagnose mac-physical
|
|
343
344
|
Run the isolated two-display macOS capture/control physical gate.
|
|
345
|
+
diagnose transport-physical
|
|
346
|
+
Prove Direct, UDP P2P, encrypted relay, and Direct recovery on a current stream.
|
|
344
347
|
|
|
345
348
|
Hub options:
|
|
346
349
|
--no-open Do not open the browser automatically.
|
|
@@ -397,11 +400,24 @@ Options:
|
|
|
397
400
|
async function runDiagnosticCommand(args) {
|
|
398
401
|
const name = String(args[0] || '').trim().toLowerCase();
|
|
399
402
|
if (!name || name === '--help' || name === '-h' || name === 'help') {
|
|
400
|
-
|
|
403
|
+
printHelp();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (name === 'transport-physical') {
|
|
407
|
+
const diagnosticEntry = resolve(
|
|
408
|
+
packageRoot,
|
|
409
|
+
'diagnostics',
|
|
410
|
+
'livedesk-transport-physical.mjs'
|
|
411
|
+
);
|
|
412
|
+
if (!existsSync(diagnosticEntry)) {
|
|
413
|
+
throw new Error(`LiveDesk transport-physical diagnostic is missing from this package: ${diagnosticEntry}`);
|
|
414
|
+
}
|
|
415
|
+
const diagnostic = await import(pathToFileURL(diagnosticEntry).href);
|
|
416
|
+
await diagnostic.runTransportPhysicalDiagnosticCli(args.slice(1));
|
|
401
417
|
return;
|
|
402
418
|
}
|
|
403
419
|
if (name !== 'mac-physical') {
|
|
404
|
-
throw new Error(`Unknown LiveDesk diagnostic: ${name}. Supported
|
|
420
|
+
throw new Error(`Unknown LiveDesk diagnostic: ${name}. Supported diagnostics: mac-physical, transport-physical.`);
|
|
405
421
|
}
|
|
406
422
|
if (args.slice(1).some(argument => ['--help', '-h', 'help'].includes(String(argument).toLowerCase()))) {
|
|
407
423
|
printMacPhysicalDiagnosticHelp();
|
package/client/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.213",
|
|
4
4
|
"description": "LiveDesk local remote client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"ws": "^8.18.3"
|
|
41
41
|
},
|
|
42
42
|
"optionalDependencies": {
|
|
43
|
-
"@livedesk/fast-linux-x64": "0.1.
|
|
44
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
45
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
46
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
43
|
+
"@livedesk/fast-linux-x64": "0.1.417",
|
|
44
|
+
"@livedesk/fast-osx-arm64": "0.1.417",
|
|
45
|
+
"@livedesk/fast-osx-x64": "0.1.417",
|
|
46
|
+
"@livedesk/fast-win-x64": "0.1.417"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public"
|
|
@@ -129,7 +129,15 @@ const requestedHeight = heightArgumentIndex >= 0
|
|
|
129
129
|
const transitionToMode5 = process.argv.includes('--transition-mode5');
|
|
130
130
|
const switchMonitor = process.argv.includes('--switch-monitor') || isMacPhysicalGate;
|
|
131
131
|
const verifyMonitorPixels = process.argv.includes('--verify-monitor-pixels') || isMacPhysicalGate;
|
|
132
|
+
const windowsInputAck = process.argv.includes('--windows-input-ack');
|
|
133
|
+
const windowsInputHookLateStart = process.argv.includes('--windows-input-hook-late-start');
|
|
132
134
|
const legacyFfmpegFirst = process.argv.includes('--legacy-ffmpeg-first');
|
|
135
|
+
if (windowsInputAck && !isWindows) {
|
|
136
|
+
throw new Error('The Windows native input ACK diagnostic requires Windows hardware.');
|
|
137
|
+
}
|
|
138
|
+
if (windowsInputHookLateStart && !windowsInputAck) {
|
|
139
|
+
throw new Error('The Windows hook late-start diagnostic requires --windows-input-ack.');
|
|
140
|
+
}
|
|
133
141
|
const reportArgumentIndex = rawArguments.findIndex(
|
|
134
142
|
argument => argument === '--report' || argument === '--gate-report'
|
|
135
143
|
);
|
|
@@ -468,6 +476,150 @@ async function runMacModifierProbe(binding) {
|
|
|
468
476
|
};
|
|
469
477
|
}
|
|
470
478
|
|
|
479
|
+
async function runWindowsInputAckProbe(binding, { expectHookLateStartFailure = false } = {}) {
|
|
480
|
+
const socket = new WebSocket(`ws://127.0.0.1:${httpPort}/api/remote/input/ws`, {
|
|
481
|
+
perMessageDeflate: false
|
|
482
|
+
});
|
|
483
|
+
const sendInput = async (input, timeoutMs = 5_000) => {
|
|
484
|
+
const outcome = waitForWebSocketJson(
|
|
485
|
+
socket,
|
|
486
|
+
message => (
|
|
487
|
+
(message.type === 'RemoteInputApplied' || message.type === 'RemoteInputError')
|
|
488
|
+
&& Number(message.inputSeq) === Number(input.inputSeq)
|
|
489
|
+
),
|
|
490
|
+
timeoutMs
|
|
491
|
+
);
|
|
492
|
+
socket.send(JSON.stringify({
|
|
493
|
+
type: 'input',
|
|
494
|
+
requestId: input.inputEventId,
|
|
495
|
+
deviceId: binding.deviceId,
|
|
496
|
+
input: {
|
|
497
|
+
...input,
|
|
498
|
+
requestAck: true,
|
|
499
|
+
issuedAtEpochMs: Date.now(),
|
|
500
|
+
monitorIndex: binding.monitorIndex,
|
|
501
|
+
controlSessionId: binding.sessionId,
|
|
502
|
+
controlCommandId: binding.commandId,
|
|
503
|
+
captureGeneration: binding.captureGeneration
|
|
504
|
+
}
|
|
505
|
+
}));
|
|
506
|
+
return outcome;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
let resetApplied = false;
|
|
510
|
+
try {
|
|
511
|
+
const ready = waitForWebSocketJson(socket, message => message.type === 'RemoteInputSocketReady');
|
|
512
|
+
await Promise.all([waitForWebSocketOpen(socket), ready]);
|
|
513
|
+
const probe = await sendInput({
|
|
514
|
+
type: 'keyboard.probe',
|
|
515
|
+
inputSeq: 1,
|
|
516
|
+
inputEventId: `windows-physical-probe-${Date.now()}`,
|
|
517
|
+
pressedCodes: [],
|
|
518
|
+
shiftKey: false,
|
|
519
|
+
ctrlKey: false,
|
|
520
|
+
altKey: false,
|
|
521
|
+
metaKey: false,
|
|
522
|
+
repeat: false
|
|
523
|
+
});
|
|
524
|
+
if (expectHookLateStartFailure) {
|
|
525
|
+
assert.equal(
|
|
526
|
+
probe.type,
|
|
527
|
+
'RemoteInputError',
|
|
528
|
+
'The forced late hook start unexpectedly reached native input injection.'
|
|
529
|
+
);
|
|
530
|
+
assert.match(
|
|
531
|
+
String(probe.error || ''),
|
|
532
|
+
/diagnostic keyboard isolation hook did not become ready/i,
|
|
533
|
+
'The forced late hook start did not fail at the bounded ready deadline.'
|
|
534
|
+
);
|
|
535
|
+
const reset = await sendInput({
|
|
536
|
+
type: 'keyboard.reset',
|
|
537
|
+
inputSeq: 2,
|
|
538
|
+
inputEventId: `windows-late-start-reset-${Date.now()}`,
|
|
539
|
+
pressedCodes: [],
|
|
540
|
+
shiftKey: false,
|
|
541
|
+
ctrlKey: false,
|
|
542
|
+
altKey: false,
|
|
543
|
+
metaKey: false,
|
|
544
|
+
repeat: false,
|
|
545
|
+
reason: 'windows-hook-late-start-cleanup'
|
|
546
|
+
});
|
|
547
|
+
assert.equal(reset.type, 'RemoteInputApplied', String(reset.error || 'Windows late-start reset failed.'));
|
|
548
|
+
assert.equal(reset.duplicate, false, 'Windows late-start reset was acknowledged as a duplicate.');
|
|
549
|
+
resetApplied = true;
|
|
550
|
+
return {
|
|
551
|
+
lateStartFailure: true,
|
|
552
|
+
error: String(probe.error || ''),
|
|
553
|
+
resetApplied
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
assert.equal(probe.type, 'RemoteInputApplied', String(probe.error || 'Windows native input probe failed.'));
|
|
557
|
+
assert.equal(probe.duplicate, false, 'Windows native input probe was acknowledged as a duplicate.');
|
|
558
|
+
assert.equal(probe.nativeInputBackend, 'win32-sendinput-hook-isolated');
|
|
559
|
+
assert.equal(Number(probe.nativeEventsRequested), 8);
|
|
560
|
+
assert.equal(Number(probe.nativeEventsAccepted), 8);
|
|
561
|
+
assert.equal(Number(probe.hookEventsObserved), 8);
|
|
562
|
+
assert.equal(Number(probe.hookEventsBlocked), 8);
|
|
563
|
+
assert.equal(probe.focusChanged, false, 'Windows native input probe changed the foreground window.');
|
|
564
|
+
assert.equal(probe.inputDiagnostic, true);
|
|
565
|
+
|
|
566
|
+
const reset = await sendInput({
|
|
567
|
+
type: 'keyboard.reset',
|
|
568
|
+
inputSeq: 2,
|
|
569
|
+
inputEventId: `windows-physical-reset-${Date.now()}`,
|
|
570
|
+
pressedCodes: [],
|
|
571
|
+
shiftKey: false,
|
|
572
|
+
ctrlKey: false,
|
|
573
|
+
altKey: false,
|
|
574
|
+
metaKey: false,
|
|
575
|
+
repeat: false,
|
|
576
|
+
reason: 'windows-native-input-diagnostic-cleanup'
|
|
577
|
+
});
|
|
578
|
+
assert.equal(reset.type, 'RemoteInputApplied', String(reset.error || 'Windows native input reset failed.'));
|
|
579
|
+
assert.equal(reset.duplicate, false, 'Windows native input reset was acknowledged as a duplicate.');
|
|
580
|
+
resetApplied = true;
|
|
581
|
+
return {
|
|
582
|
+
backend: String(probe.nativeInputBackend || ''),
|
|
583
|
+
requested: Number(probe.nativeEventsRequested || 0),
|
|
584
|
+
accepted: Number(probe.nativeEventsAccepted || 0),
|
|
585
|
+
hookObserved: Number(probe.hookEventsObserved || 0),
|
|
586
|
+
hookBlocked: Number(probe.hookEventsBlocked || 0),
|
|
587
|
+
duplicate: probe.duplicate === true,
|
|
588
|
+
focusChanged: probe.focusChanged === true,
|
|
589
|
+
resetApplied
|
|
590
|
+
};
|
|
591
|
+
} finally {
|
|
592
|
+
if (!resetApplied && socket.readyState === WebSocket.OPEN) {
|
|
593
|
+
try {
|
|
594
|
+
socket.send(JSON.stringify({
|
|
595
|
+
type: 'input',
|
|
596
|
+
deviceId: binding.deviceId,
|
|
597
|
+
fireAndForget: true,
|
|
598
|
+
input: {
|
|
599
|
+
type: 'keyboard.reset',
|
|
600
|
+
inputSeq: 0,
|
|
601
|
+
requestAck: false,
|
|
602
|
+
pressedCodes: [],
|
|
603
|
+
shiftKey: false,
|
|
604
|
+
ctrlKey: false,
|
|
605
|
+
altKey: false,
|
|
606
|
+
metaKey: false,
|
|
607
|
+
repeat: false,
|
|
608
|
+
reason: 'windows-native-input-diagnostic-finally',
|
|
609
|
+
monitorIndex: binding.monitorIndex,
|
|
610
|
+
controlSessionId: binding.sessionId,
|
|
611
|
+
controlCommandId: binding.commandId,
|
|
612
|
+
captureGeneration: binding.captureGeneration
|
|
613
|
+
}
|
|
614
|
+
}));
|
|
615
|
+
} catch {
|
|
616
|
+
// Closing the exact input owner also triggers RemoteFast ReleaseAll.
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
try { socket.close(); } catch {}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
471
623
|
async function decodeKeyFrame(accessUnit, label) {
|
|
472
624
|
const directory = await mkdtemp(path.join(tmpdir(), `livedesk-${label}-`));
|
|
473
625
|
const inputPath = path.join(directory, 'frame.h264');
|
|
@@ -1044,6 +1196,10 @@ try {
|
|
|
1044
1196
|
LIVEDESK_UDP_PREFER_P2P: '0',
|
|
1045
1197
|
LIVEDESK_RELAY_ENABLED: '0'
|
|
1046
1198
|
} : {}),
|
|
1199
|
+
...(windowsInputAck ? { LIVEDESK_INPUT_DIAGNOSTIC: '1' } : {}),
|
|
1200
|
+
...(windowsInputHookLateStart ? {
|
|
1201
|
+
LIVEDESK_INPUT_DIAGNOSTIC_HOOK_START_DELAY_MS: '2500'
|
|
1202
|
+
} : {}),
|
|
1047
1203
|
LIVEDESK_FFMPEG: legacyFfmpegFirst ? ffmpegPath : ffmpegStaticPath,
|
|
1048
1204
|
LIVEDESK_FFMPEG_PATHS: (legacyFfmpegFirst
|
|
1049
1205
|
? [ffmpegPath, ffmpegStaticPath]
|
|
@@ -1126,6 +1282,7 @@ try {
|
|
|
1126
1282
|
}));
|
|
1127
1283
|
|
|
1128
1284
|
const frames = await withMacDiagnosticAbort(framePromise, cancelFrameWait);
|
|
1285
|
+
let activeInputBindingFrame = frames.at(-1);
|
|
1129
1286
|
const initialCaptureIdentity = switchMonitor && isWindows
|
|
1130
1287
|
? await waitFor(
|
|
1131
1288
|
() => findWindowsCaptureIdentity(agentLogs, requestedMonitorIndex),
|
|
@@ -1294,6 +1451,7 @@ try {
|
|
|
1294
1451
|
)
|
|
1295
1452
|
: null;
|
|
1296
1453
|
const firstSwitchedFrame = switchedFrames[0];
|
|
1454
|
+
activeInputBindingFrame = switchedFrames.at(-1);
|
|
1297
1455
|
const switchedFirstFrameMs = firstSwitchedFrame.receivedAt - switchedAt;
|
|
1298
1456
|
assert.equal(firstSwitchedFrame.metadata.isKeyFrame, true, 'Mode 3 monitor transition did not start on a key frame.');
|
|
1299
1457
|
assert.notEqual(String(firstSwitchedFrame.metadata.commandId || ''), previousCommandId, 'Mode 3 monitor transition reused the previous command binding.');
|
|
@@ -1405,6 +1563,31 @@ try {
|
|
|
1405
1563
|
console.log(`Mode 3 monitor transition OK: ${requestedMonitorIndex} -> ${nextMonitorIndex}, first frame ${switchedFirstFrameMs.toFixed(0)}ms, generation ${previousCaptureGeneration} -> ${firstSwitchedFrame.metadata.captureGeneration}.`);
|
|
1406
1564
|
}
|
|
1407
1565
|
|
|
1566
|
+
if (windowsInputAck) {
|
|
1567
|
+
const inputProbe = await runWindowsInputAckProbe({
|
|
1568
|
+
deviceId: connectedDeviceId,
|
|
1569
|
+
sessionId: String(activeInputBindingFrame.metadata.sessionId || ''),
|
|
1570
|
+
commandId: String(activeInputBindingFrame.metadata.commandId || ''),
|
|
1571
|
+
captureGeneration: Number(activeInputBindingFrame.metadata.captureGeneration || 0),
|
|
1572
|
+
monitorIndex: Number(activeInputBindingFrame.metadata.monitorIndex || 0)
|
|
1573
|
+
}, {
|
|
1574
|
+
expectHookLateStartFailure: windowsInputHookLateStart
|
|
1575
|
+
});
|
|
1576
|
+
if (windowsInputHookLateStart) {
|
|
1577
|
+
console.log(
|
|
1578
|
+
`Windows diagnostic hook late-start cleanup OK: expected timeout, `
|
|
1579
|
+
+ `Agent responsive, reset=${inputProbe.resetApplied}.`
|
|
1580
|
+
);
|
|
1581
|
+
} else {
|
|
1582
|
+
console.log(
|
|
1583
|
+
`Windows native modifier input ACK: ${inputProbe.accepted}/${inputProbe.requested} `
|
|
1584
|
+
+ `via ${inputProbe.backend}, hook=${inputProbe.hookObserved}/${inputProbe.hookBlocked}, `
|
|
1585
|
+
+ `duplicate=${inputProbe.duplicate}, `
|
|
1586
|
+
+ `focusChanged=${inputProbe.focusChanged}, reset=${inputProbe.resetApplied}.`
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1408
1591
|
if (transitionToMode5) {
|
|
1409
1592
|
const previousFrameSocket = frameSocket;
|
|
1410
1593
|
frameSocket = new WebSocket(`ws://127.0.0.1:${httpPort}/api/remote/frames/ws?devices=${encodeURIComponent(connectedDeviceId)}`);
|