livedesk 0.1.458 → 0.1.460

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();
@@ -2931,14 +2947,6 @@ async function main() {
2931
2947
  await runLegacyClientUpdateSupervisorCli();
2932
2948
  return;
2933
2949
  }
2934
- const roleStateOptions = {
2935
- stateDir: MANAGER_STATE_DIR,
2936
- legacyClientStateDir: process.env.LIVEDESK_LEGACY_CLIENT_STATE_DIR
2937
- };
2938
- migrateLegacyClientState({
2939
- stateDir: MANAGER_STATE_DIR,
2940
- legacyClientStateDir: process.env.LIVEDESK_LEGACY_CLIENT_STATE_DIR
2941
- });
2942
2950
  const topLevel = parseTopLevelArgs(rawArgs);
2943
2951
  if (topLevel.debug) {
2944
2952
  process.env.LIVEDESK_DEBUG = '1';
@@ -2953,10 +2961,37 @@ async function main() {
2953
2961
  console.log(readVersion());
2954
2962
  return;
2955
2963
  }
2956
- await assertNoActiveMacPhysicalDiagnosticLease();
2957
2964
  const explicitRoleCommand = command === 'hub' || command === 'manager' || command === 'client';
2958
- let resolvedRole;
2959
- let runtimeArgs = args;
2965
+ if (explicitRoleCommand) {
2966
+ const commandArgs = args.slice(1).map(value => String(value || '').trim().toLowerCase());
2967
+ if (commandArgs.some(value => value === '--help' || value === '-h' || value === 'help')) {
2968
+ if (command === 'client') {
2969
+ console.warn('[LiveDesk] Legacy client command detected. Use `npx -y --prefer-online livedesk@latest` for automatic role selection.');
2970
+ printClientHelp();
2971
+ } else {
2972
+ printHelp();
2973
+ }
2974
+ return;
2975
+ }
2976
+ if (commandArgs.some(value => value === '--version' || value === '-v' || value === 'version')) {
2977
+ if (command === 'client') {
2978
+ console.warn('[LiveDesk] Legacy client command detected. Use `npx -y --prefer-online livedesk@latest` for automatic role selection.');
2979
+ }
2980
+ console.log(readVersion());
2981
+ return;
2982
+ }
2983
+ }
2984
+ await assertNoActiveMacPhysicalDiagnosticLease();
2985
+ const roleStateOptions = {
2986
+ stateDir: MANAGER_STATE_DIR,
2987
+ legacyClientStateDir: process.env.LIVEDESK_LEGACY_CLIENT_STATE_DIR
2988
+ };
2989
+ migrateLegacyClientState({
2990
+ stateDir: MANAGER_STATE_DIR,
2991
+ legacyClientStateDir: process.env.LIVEDESK_LEGACY_CLIENT_STATE_DIR
2992
+ });
2993
+ let resolvedRole;
2994
+ let runtimeArgs = args;
2960
2995
  if (explicitRoleCommand) {
2961
2996
  const role = command === 'client' ? 'client' : 'hub';
2962
2997
  resolvedRole = await resolveDeviceRole({ forcedRole: role, ...roleStateOptions });
@@ -3075,20 +3110,12 @@ async function main() {
3075
3110
  }
3076
3111
 
3077
3112
  if (command === 'client') {
3078
- if (args.slice(1).some(arg => arg === '--help' || arg === '-h' || arg === 'help')) {
3079
- printClientHelp();
3080
- return;
3081
- }
3082
3113
  await runClient(runtimeArgs, resolvedRole, lock);
3083
- return;
3084
- }
3085
- if (command === 'hub' || command === 'manager') {
3086
- if (args.slice(1).some(arg => arg === '--help' || arg === '-h' || arg === 'help')) {
3087
- printHelp();
3088
- return;
3089
- }
3114
+ return;
3115
+ }
3116
+ if (command === 'hub' || command === 'manager') {
3090
3117
  await runManager(runtimeArgs, resolvedRole, lock);
3091
- return;
3118
+ return;
3092
3119
  }
3093
3120
  if (resolvedRole.role === 'client') {
3094
3121
  await runClient(runtimeArgs, resolvedRole, lock);
@@ -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)}`);