@livedesk/client 0.1.173 → 0.1.175

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/README.md CHANGED
@@ -89,7 +89,7 @@ Frame pipeline roadmap:
89
89
 
90
90
  - Mode 1: `mode1-jpeg` - current test path using screen capture, resize, and JPEG binary frames.
91
91
  - Mode 2: `mode2-lzo` - cross-platform wall path using independent RGB565LE frames capped at 320x180 and compressed as LZO1X blocks. The wall defaults to 8 fps. macOS and Linux keep one persistent local capture helper per client process. Legacy `mode2-lz4` settings migrate to this mode.
92
- - Mode 3: `mode3-h264-hw` - OS-specific hardware H.264 path. Windows tries Media Foundation/NVENC/QSV/AMF, macOS prefers ScreenCaptureKit plus VideoToolbox, and Linux tries NVENC/VAAPI/QSV. macOS emits one startup key frame and then an approximately one-second GOP. The launcher uses bundled ffmpeg for fallback paths unless `LIVEDESK_FFMPEG` points to a custom binary. On macOS, set `LIVEDESK_FFMPEG_AVFOUNDATION_INPUT` only when the ScreenCaptureKit helper is unavailable and the AVFoundation fallback input is not `1:none`.
92
+ - Mode 3: `mode3-h264-hw` - OS-specific H.264 path. Windows tries Media Foundation/NVENC/QSV/AMF, macOS prefers ScreenCaptureKit plus VideoToolbox, and Linux tries NVENC/VAAPI/QSV before falling back to the bundled ffmpeg `libx264` encoder. Linux users do not need to install ffmpeg separately when starting LiveDesk through the published npm package. macOS emits one startup key frame and then an approximately one-second GOP. The launcher uses bundled ffmpeg for fallback paths unless `LIVEDESK_FFMPEG` points to a custom binary. On macOS, set `LIVEDESK_FFMPEG_AVFOUNDATION_INPUT` only when the ScreenCaptureKit helper is unavailable and the AVFoundation fallback input is not `1:none`.
93
93
 
94
94
  Legacy mode names such as `remote-fast` and `remote-quality` are treated as
95
95
  Mode 1 aliases.
@@ -14,6 +14,7 @@ import { createClientRuntimeServer } from '../src/runtime/client-runtime-server.
14
14
  import { installAgentTerminationHandlers } from '../src/runtime/agent-process-lifecycle.js';
15
15
  import { createHubWakeListener } from '../src/runtime/hub-wake-listener.js';
16
16
  import { normalizeRuntimeAuthSession } from '../../runtime-core/src/auth-session.js';
17
+ import { startRoleTransitionSupervisor } from '../../runtime-core/src/role-transition-supervisor.js';
17
18
 
18
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
20
  const require = createRequire(import.meta.url);
@@ -21,6 +22,7 @@ const packageRoot = resolve(__dirname, '..');
21
22
  const nodeAgentPath = join(__dirname, 'livedesk-client-node.js');
22
23
  const FAST_PREFLIGHT_TIMEOUT_MS = 5000;
23
24
  const DEFAULT_MANAGER = '127.0.0.1:5197';
25
+ const DEFAULT_HUB_CLIENT_PORT = 5197;
24
26
  const DEFAULT_AUTH_CALLBACK_HOST = '127.0.0.1';
25
27
  const DEFAULT_AUTH_CALLBACK_PORT = 5179;
26
28
  const ENDPOINT_PROBE_TIMEOUT_MS = 900;
@@ -112,30 +114,53 @@ function writeLocalRoleCache(role, deviceId, roleVersion = 0, assignedHubId = ''
112
114
  return true;
113
115
  }
114
116
 
115
- function spawnUnifiedRoleRestart(role) {
116
- if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) {
117
- console.log(`[LiveDesk] Desktop supervisor will restart the runtime as ${role}.`);
118
- return false;
119
- }
120
- const unifiedEntry = String(process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY || '').trim();
121
- const child = unifiedEntry && existsSync(unifiedEntry)
122
- ? spawn(process.execPath, [unifiedEntry, '--force-role', role, '--no-open'], {
123
- env: {
124
- ...process.env,
125
- LIVEDESK_ROLE_TRANSITION: '1',
126
- LIVEDESK_RESTART_WAIT_PID: String(process.ppid),
127
- LIVEDESK_SKIP_BROWSER_OPEN: '1'
128
- },
129
- detached: true,
130
- stdio: 'ignore',
131
- windowsHide: true
132
- })
133
- : process.platform === 'win32'
134
- ? spawn('npx.cmd', ['-y', '--prefer-online', 'livedesk@latest', '--force-role', role, '--no-open'], { detached: true, stdio: 'ignore', windowsHide: true })
135
- : spawn('npx', ['-y', '--prefer-online', 'livedesk@latest', '--force-role', role, '--no-open'], { detached: true, stdio: 'ignore' });
136
- child.unref();
137
- return true;
138
- }
117
+ export function buildUnifiedRoleRestartEnvironment(role, baseEnv = process.env, runtimePid = process.pid) {
118
+ const normalizedRole = String(role || '').trim().toLowerCase();
119
+ const waitPid = Number(runtimePid);
120
+ if (normalizedRole !== 'hub' && normalizedRole !== 'client') {
121
+ throw new Error(`Unsupported LiveDesk role restart: ${role}`);
122
+ }
123
+ if (!Number.isInteger(waitPid) || waitPid <= 1) {
124
+ throw new Error(`Invalid LiveDesk runtime PID for role restart: ${runtimePid}`);
125
+ }
126
+ return {
127
+ ...baseEnv,
128
+ LIVEDESK_ROLE_TRANSITION: '1',
129
+ // The replacement must wait only for this Client runtime. Waiting for
130
+ // npm, npx, PowerShell, or another terminal parent can deadlock the
131
+ // takeover even after Supabase has selected this device as Hub.
132
+ LIVEDESK_RESTART_WAIT_PID: String(waitPid),
133
+ LIVEDESK_ROLE_TAKEOVER: normalizedRole === 'hub' ? '1' : '',
134
+ LIVEDESK_SKIP_BROWSER_OPEN: '1'
135
+ };
136
+ }
137
+
138
+ async function spawnUnifiedRoleRestart(role) {
139
+ if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) {
140
+ console.log(`[LiveDesk] Desktop supervisor will restart the runtime as ${role}.`);
141
+ return false;
142
+ }
143
+ const unifiedEntry = String(process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY || '').trim();
144
+ const restartEnvironment = buildUnifiedRoleRestartEnvironment(role);
145
+ if (!unifiedEntry || !existsSync(unifiedEntry)) {
146
+ throw new Error(`LiveDesk unified role launcher is unavailable: ${unifiedEntry || 'missing path'}`);
147
+ }
148
+ const handoff = await startRoleTransitionSupervisor({
149
+ role,
150
+ previousPid: process.pid,
151
+ launcherEntry: unifiedEntry,
152
+ stateDir: UNIFIED_CLIENT_STATE_DIR,
153
+ runtimePort: Number(process.env.LIVEDESK_CLIENT_RUNTIME_PORT || DEFAULT_AUTH_CALLBACK_PORT),
154
+ remotePort: resolveHubClientPort(),
155
+ env: restartEnvironment,
156
+ cwd: process.cwd()
157
+ });
158
+ console.log(
159
+ `[LiveDesk] Role transition supervisor claimed target=${role} `
160
+ + `pid=${handoff.supervisorPid}. Diagnostics: ${handoff.logPath}`
161
+ );
162
+ return true;
163
+ }
139
164
 
140
165
  function printHelp() {
141
166
  process.stdout.write(`
@@ -349,15 +374,64 @@ function parseLauncherArgs(argv) {
349
374
  };
350
375
  }
351
376
 
352
- function normalizePort(value) {
377
+ function normalizePort(value) {
353
378
  const port = Number(String(value || '').trim());
354
379
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
355
380
  return 0;
356
381
  }
357
382
  return port;
358
- }
359
-
360
- function normalizeSlotNumber(value) {
383
+ }
384
+
385
+ export function resolveHubClientPort(env = process.env) {
386
+ return normalizePort(env.REMOTE_HUB_PORT || env.LIVEDESK_HUB_REMOTE_PORT) || DEFAULT_HUB_CLIENT_PORT;
387
+ }
388
+
389
+ export function preflightHubClientPort(port = resolveHubClientPort()) {
390
+ const normalizedPort = normalizePort(port);
391
+ if (!normalizedPort) {
392
+ return Promise.resolve({ ok: false, port: Number(port) || 0, code: 'invalid-hub-client-port' });
393
+ }
394
+ return new Promise(resolvePreflight => {
395
+ const probe = net.createServer();
396
+ let completed = false;
397
+ const finish = result => {
398
+ if (completed) return;
399
+ completed = true;
400
+ resolvePreflight({ port: normalizedPort, ...result });
401
+ };
402
+ probe.unref?.();
403
+ probe.once('error', error => {
404
+ finish({
405
+ ok: false,
406
+ code: error?.code === 'EADDRINUSE' ? 'hub-client-port-in-use' : 'hub-client-port-unavailable',
407
+ systemCode: String(error?.code || '')
408
+ });
409
+ });
410
+ probe.listen({ host: '0.0.0.0', port: normalizedPort, exclusive: true }, () => {
411
+ probe.close(error => {
412
+ if (error) {
413
+ finish({ ok: false, code: 'hub-client-port-unavailable', systemCode: String(error?.code || '') });
414
+ return;
415
+ }
416
+ finish({ ok: true, code: 'ok' });
417
+ });
418
+ });
419
+ });
420
+ }
421
+
422
+ function hubClientPortPreflightError(preflight) {
423
+ const port = Number(preflight?.port || resolveHubClientPort());
424
+ return {
425
+ ok: false,
426
+ code: String(preflight?.code || 'hub-client-port-unavailable'),
427
+ port,
428
+ error: preflight?.code === 'hub-client-port-in-use'
429
+ ? `LiveDesk cannot switch this computer to Hub because TCP ${port} is already in use. Change or stop the owning service, then try Switch to Hub again. The current Hub was not changed.`
430
+ : `LiveDesk cannot switch this computer to Hub because TCP ${port} is unavailable. Check the local port configuration, then try again. The current Hub was not changed.`
431
+ };
432
+ }
433
+
434
+ function normalizeSlotNumber(value) {
361
435
  const number = Number(String(value || '').trim());
362
436
  if (!Number.isInteger(number) || number < 1 || number > 999) {
363
437
  return '';
@@ -2628,12 +2702,18 @@ async function startConnectionChoiceServer(supabase, options = {}) {
2628
2702
  payload = Object.fromEntries(params.entries());
2629
2703
  }
2630
2704
  const nextRole = String(payload.role || '').trim().toLowerCase();
2631
- if (nextRole !== 'hub') {
2705
+ if (nextRole !== 'hub') {
2632
2706
  res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
2633
2707
  res.end(JSON.stringify({ ok: false, error: 'client-can-only-transition-to-hub' }));
2634
- return;
2635
- }
2636
- const session = await refreshSessionIfNeeded(supabase);
2708
+ return;
2709
+ }
2710
+ const portPreflight = await preflightHubClientPort();
2711
+ if (!portPreflight.ok) {
2712
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
2713
+ res.end(JSON.stringify(hubClientPortPreflightError(portPreflight)));
2714
+ return;
2715
+ }
2716
+ const session = await refreshSessionIfNeeded(supabase);
2637
2717
  const expectedRoleVersion = Number(dashboardState.roleVersion || 0);
2638
2718
  const { data, error } = await supabase.rpc('set_livedesk_device_role', {
2639
2719
  p_device_id: deviceId,
@@ -2928,6 +3008,10 @@ async function chooseClientConnection(supabase, options = {}) {
2928
3008
  if (!supabase) {
2929
3009
  return { ok: false, error: 'supabase-session-required' };
2930
3010
  }
3011
+ const portPreflight = await preflightHubClientPort();
3012
+ if (!portPreflight.ok) {
3013
+ return hubClientPortPreflightError(portPreflight);
3014
+ }
2931
3015
  const session = await refreshSessionIfNeeded(supabase);
2932
3016
  if (!session?.access_token) {
2933
3017
  return { ok: false, error: 'supabase-session-required' };
@@ -3688,7 +3772,7 @@ function buildFastEnvironment(prepared) {
3688
3772
  env.LIVEDESK_FFMPEG_PATHS = merged.join(separator);
3689
3773
  }
3690
3774
 
3691
- if (process.platform === 'darwin' || env.LIVEDESK_DEBUG_FFMPEG === '1') {
3775
+ if (process.platform === 'darwin' || process.platform === 'linux' || env.LIVEDESK_DEBUG_FFMPEG === '1') {
3692
3776
  const describe = path => path.includes('ffmpeg-static')
3693
3777
  ? 'ffmpeg-static'
3694
3778
  : path.includes('@ffmpeg-installer')
@@ -3930,12 +4014,12 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
3930
4014
  while (true) {
3931
4015
  const prepared = await prepareLoginConnection(parsed, connectionPage);
3932
4016
  connectionPage = prepared.connectionPage || connectionPage;
3933
- const roleRestartBeforeAgent = connectionPage?.getRoleRestartRequest?.() || roleRestartRequest;
3934
- if (roleRestartBeforeAgent?.role) {
3935
- connectionPage?.close?.();
3936
- if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) process.exit(43);
3937
- spawnUnifiedRoleRestart(roleRestartBeforeAgent.role);
3938
- process.exit(0);
4017
+ const roleRestartBeforeAgent = connectionPage?.getRoleRestartRequest?.() || roleRestartRequest;
4018
+ if (roleRestartBeforeAgent?.role) {
4019
+ if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) process.exit(43);
4020
+ await spawnUnifiedRoleRestart(roleRestartBeforeAgent.role);
4021
+ connectionPage?.close?.();
4022
+ process.exit(0);
3939
4023
  }
3940
4024
  const fastRuntime = getFastRuntime();
3941
4025
  const useFast = shouldTryFast(prepared, fastRuntime);
@@ -4024,12 +4108,12 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
4024
4108
  });
4025
4109
  });
4026
4110
  }
4027
- const requestedRoleRestart = connectionPage?.getRoleRestartRequest?.();
4028
- if (requestedRoleRestart?.role) {
4029
- connectionPage?.close?.();
4030
- if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) process.exit(43);
4031
- spawnUnifiedRoleRestart(requestedRoleRestart.role);
4032
- process.exit(0);
4111
+ const requestedRoleRestart = connectionPage?.getRoleRestartRequest?.();
4112
+ if (requestedRoleRestart?.role) {
4113
+ if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) process.exit(43);
4114
+ await spawnUnifiedRoleRestart(requestedRoleRestart.role);
4115
+ connectionPage?.close?.();
4116
+ process.exit(0);
4033
4117
  }
4034
4118
  if (result?.signal) {
4035
4119
  connectionPage?.close?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.173",
3
+ "version": "0.1.175",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,10 +39,10 @@
39
39
  "ws": "^8.18.3"
40
40
  },
41
41
  "optionalDependencies": {
42
- "@livedesk/fast-linux-x64": "0.1.379",
43
- "@livedesk/fast-osx-arm64": "0.1.379",
44
- "@livedesk/fast-osx-x64": "0.1.379",
45
- "@livedesk/fast-win-x64": "0.1.379"
42
+ "@livedesk/fast-linux-x64": "0.1.381",
43
+ "@livedesk/fast-osx-arm64": "0.1.381",
44
+ "@livedesk/fast-osx-x64": "0.1.381",
45
+ "@livedesk/fast-win-x64": "0.1.381"
46
46
  },
47
47
  "publishConfig": {
48
48
  "access": "public"
@@ -739,6 +739,7 @@ export function createClientRuntimeServer(options = {}) {
739
739
  osVersion: os.release(),
740
740
  pid: process.pid,
741
741
  cpuUsagePercent: Math.round(cpuUsagePercent * 10) / 10,
742
+ cpuLogicalCores: Math.max(1, os.cpus().length),
742
743
  memoryTotalBytes: totalMemoryBytes,
743
744
  memoryUsedBytes: Math.max(0, totalMemoryBytes - freeMemoryBytes),
744
745
  uptimeSeconds: Math.max(0, Math.floor(os.uptime())),