@livedesk/client 0.1.173 → 0.1.174

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.
@@ -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' };
@@ -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.174",
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.380",
43
+ "@livedesk/fast-osx-arm64": "0.1.380",
44
+ "@livedesk/fast-osx-x64": "0.1.380",
45
+ "@livedesk/fast-win-x64": "0.1.380"
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())),