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 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
- printMacPhysicalDiagnosticHelp();
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 diagnostic: mac-physical.`);
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();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.212",
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.416",
44
- "@livedesk/fast-osx-arm64": "0.1.416",
45
- "@livedesk/fast-osx-x64": "0.1.416",
46
- "@livedesk/fast-win-x64": "0.1.416"
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)}`);