livedesk 0.1.496 → 0.1.498

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
@@ -31,8 +31,15 @@ import {
31
31
  isKnownLiveDeskCaptureHelperProcess,
32
32
  isKnownLiveDeskClientAgentProcess
33
33
  } from '../bootstrap/client-process-identity.js';
34
- import { runLegacyClientUpdateSupervisorCli } from '../bootstrap/legacy-client-update.js';
35
- import { transferSavedClientSessionToHub } from '../bootstrap/hub-auth-handoff.js';
34
+ import { runLegacyClientUpdateSupervisorCli } from '../bootstrap/legacy-client-update.js';
35
+ import { transferSavedClientSessionToHub } from '../bootstrap/hub-auth-handoff.js';
36
+ import {
37
+ cancelUpdateHostJob,
38
+ ensureUpdateHost,
39
+ getUpdateHostJob,
40
+ submitUpdateHostJob,
41
+ updateHostEnvironment
42
+ } from '../bootstrap/update-host-client.mjs';
36
43
  import {
37
44
  consumeDeadWindowsOwnedProcessManifest,
38
45
  readDeadWindowsOwnedProcessManifest,
@@ -220,108 +227,80 @@ function readBoundedMilliseconds(value, fallback, minimum = 1) {
220
227
  return Number.isFinite(parsed) && parsed >= minimum ? Math.round(parsed) : fallback;
221
228
  }
222
229
 
223
- async function stopUnclaimedHubRestartSupervisor(child) {
224
- const supervisorPid = Number(child?.pid || 0);
225
- if (supervisorPid <= 1 || !isPidAlive(supervisorPid)) return;
226
- try {
227
- child.kill('SIGKILL');
228
- } catch {
229
- // The bounded liveness check below decides whether recovery is safe.
230
- }
231
- const deadline = Date.now() + 2_000;
232
- while (Date.now() < deadline && isPidAlive(supervisorPid)) {
233
- await delay(HUB_UPDATE_SUPERVISOR_HANDSHAKE_POLL_MS);
234
- }
235
- if (isPidAlive(supervisorPid)) {
236
- throw new Error(
237
- `Unclaimed Hub restart supervisor pid=${supervisorPid} could not be stopped; `
238
- + 'the current launcher will remain alive and keep the unchanged Hub running.'
239
- );
240
- }
241
- }
242
-
243
- async function waitForHubRestartSupervisorClaim(child, expected) {
244
- let exit = null;
245
- let spawnError = null;
246
- let spawned = false;
247
- child.once('spawn', () => {
248
- spawned = true;
249
- });
250
- child.once('error', error => {
251
- spawnError = error;
252
- });
253
- child.once('exit', (code, signal) => {
254
- exit = { code, signal };
255
- });
256
-
257
- const spawnDeadline = Date.now() + expected.timeoutMs;
258
- while (!spawned && !spawnError && !exit && Date.now() < spawnDeadline) {
259
- await delay(HUB_UPDATE_SUPERVISOR_HANDSHAKE_POLL_MS);
260
- }
261
- if (spawnError) {
262
- throw new Error(`Hub restart supervisor spawn failed: ${spawnError.message}`);
263
- }
264
- if (!spawned) {
265
- const detail = exit
266
- ? `exited code=${exit.code ?? 'none'} signal=${exit.signal || 'none'}`
267
- : `did not emit spawn within ${expected.timeoutMs}ms`;
268
- throw new Error(`Hub restart supervisor ${detail} before claiming the handoff.`);
269
- }
270
-
271
- const supervisorPid = Number(child.pid || 0);
272
- const claimDeadline = Date.now() + expected.timeoutMs;
273
- let matchingClaimSince = 0;
274
- while (Date.now() < claimDeadline) {
275
- if (spawnError) {
276
- throw new Error(`Hub restart supervisor failed before handoff: ${spawnError.message}`);
277
- }
278
- if (exit || !isPidAlive(supervisorPid)) {
279
- throw new Error(
280
- `Hub restart supervisor pid=${supervisorPid || 'unknown'} exited before handoff `
281
- + `(code=${exit?.code ?? 'none'} signal=${exit?.signal || 'none'}).`
282
- );
283
- }
284
- const claim = readJsonFile(expected.resultPath);
285
- if (
286
- claim?.operationId === expected.operationId
287
- && claim?.stage === 'waiting-for-old-launcher'
288
- && claim?.outcome === 'in-progress'
289
- && Number(claim?.supervisorPid || 0) === supervisorPid
290
- && Number(claim?.launcherPid || 0) === expected.launcherPid
291
- && claim?.targetVersion === expected.targetVersion
292
- && claim?.handoffToken === expected.handoffToken
293
- ) {
294
- if (!matchingClaimSince) matchingClaimSince = Date.now();
295
- if (Date.now() - matchingClaimSince >= expected.stabilityMs) {
296
- await delay(HUB_UPDATE_SUPERVISOR_HANDSHAKE_POLL_MS);
297
- const stableClaim = readJsonFile(expected.resultPath);
298
- if (
299
- !exit
300
- && isPidAlive(supervisorPid)
301
- && stableClaim?.operationId === expected.operationId
302
- && stableClaim?.stage === 'waiting-for-old-launcher'
303
- && stableClaim?.outcome === 'in-progress'
304
- && Number(stableClaim?.supervisorPid || 0) === supervisorPid
305
- && Number(stableClaim?.launcherPid || 0) === expected.launcherPid
306
- && stableClaim?.targetVersion === expected.targetVersion
307
- && stableClaim?.handoffToken === expected.handoffToken
308
- ) {
309
- return stableClaim;
310
- }
311
- matchingClaimSince = 0;
312
- }
313
- } else {
314
- matchingClaimSince = 0;
315
- }
316
- await delay(HUB_UPDATE_SUPERVISOR_HANDSHAKE_POLL_MS);
317
- }
318
- throw new Error(
319
- `Hub restart supervisor pid=${supervisorPid || 'unknown'} did not atomically claim `
320
- + `operation ${expected.operationId} within ${expected.timeoutMs}ms.`
321
- );
322
- }
323
-
324
- function writeJsonAtomic(filePath, value) {
230
+ async function cancelUnclaimedHubUpdateHostJob(updateHost, jobId, reason) {
231
+ try {
232
+ await cancelUpdateHostJob(updateHost, jobId, reason);
233
+ } catch (error) {
234
+ const job = await getUpdateHostJob(updateHost, jobId).catch(() => null);
235
+ if (job && ['accepted', 'running', 'cancelling'].includes(String(job.state || ''))) {
236
+ throw new Error(
237
+ `Update Host job ${jobId} could not be cancelled: ${error instanceof Error ? error.message : String(error)}`
238
+ );
239
+ }
240
+ }
241
+ }
242
+
243
+ async function waitForHubRestartUpdateHostClaim(updateHost, jobId, expected) {
244
+ const claimDeadline = Date.now() + expected.timeoutMs;
245
+ let matchingClaimSince = 0;
246
+ let workerPid = 0;
247
+ while (Date.now() < claimDeadline) {
248
+ const job = await getUpdateHostJob(updateHost, jobId, {
249
+ timeoutMs: Math.min(2_000, Math.max(250, claimDeadline - Date.now()))
250
+ });
251
+ if (!job) {
252
+ throw new Error(`LiveDesk Update Host lost job ${jobId} before handoff.`);
253
+ }
254
+ if (['failed', 'cancelled'].includes(String(job.state || ''))) {
255
+ throw new Error(
256
+ `LiveDesk Update Host job ${jobId} failed before handoff: ${job.error || `state=${job.state}`}`
257
+ );
258
+ }
259
+ workerPid = Number(job.workerPid || 0);
260
+ const claim = readJsonFile(expected.resultPath);
261
+ if (
262
+ job.state === 'running'
263
+ && workerPid > 1
264
+ && claim?.operationId === expected.operationId
265
+ && claim?.stage === 'waiting-for-old-launcher'
266
+ && claim?.outcome === 'in-progress'
267
+ && Number(claim?.supervisorPid || 0) === workerPid
268
+ && Number(claim?.launcherPid || 0) === expected.launcherPid
269
+ && claim?.targetVersion === expected.targetVersion
270
+ && claim?.handoffToken === expected.handoffToken
271
+ ) {
272
+ if (!matchingClaimSince) matchingClaimSince = Date.now();
273
+ if (Date.now() - matchingClaimSince >= expected.stabilityMs) {
274
+ await delay(HUB_UPDATE_SUPERVISOR_HANDSHAKE_POLL_MS);
275
+ const stableJob = await getUpdateHostJob(updateHost, jobId, { timeoutMs: 2_000 });
276
+ const stableClaim = readJsonFile(expected.resultPath);
277
+ if (
278
+ stableJob?.state === 'running'
279
+ && Number(stableJob?.workerPid || 0) === workerPid
280
+ && stableClaim?.operationId === expected.operationId
281
+ && stableClaim?.stage === 'waiting-for-old-launcher'
282
+ && stableClaim?.outcome === 'in-progress'
283
+ && Number(stableClaim?.supervisorPid || 0) === workerPid
284
+ && Number(stableClaim?.launcherPid || 0) === expected.launcherPid
285
+ && stableClaim?.targetVersion === expected.targetVersion
286
+ && stableClaim?.handoffToken === expected.handoffToken
287
+ ) {
288
+ return stableClaim;
289
+ }
290
+ matchingClaimSince = 0;
291
+ }
292
+ } else {
293
+ matchingClaimSince = 0;
294
+ }
295
+ await delay(Math.max(50, HUB_UPDATE_SUPERVISOR_HANDSHAKE_POLL_MS));
296
+ }
297
+ throw new Error(
298
+ `LiveDesk Update Host job ${jobId} did not publish an atomic handoff for `
299
+ + `operation ${expected.operationId} within ${expected.timeoutMs}ms.`
300
+ );
301
+ }
302
+
303
+ function writeJsonAtomic(filePath, value) {
325
304
  if (!filePath) return;
326
305
  mkdirSync(dirname(filePath), { recursive: true });
327
306
  const previous = readJsonFile(filePath);
@@ -2465,7 +2444,7 @@ function buildHubRestartBootstrapScript() {
2465
2444
  ].join('\n');
2466
2445
  }
2467
2446
 
2468
- async function runManager(args, resolvedRole = null, runtimeLock = null) {
2447
+ async function runManager(args, resolvedRole = null, runtimeLock = null, updateHost = null) {
2469
2448
  const runtimeOwner = runtimeLock?.payload || null;
2470
2449
  const options = parseManagerArgs(args);
2471
2450
  const httpPort = normalizePort(
@@ -2513,7 +2492,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null) {
2513
2492
  LIVEDESK_HUB_HTTP_PORT: String(httpPort),
2514
2493
  LIVEDESK_MANAGER_VERSION: readVersion(),
2515
2494
  LIVEDESK_CLIENT_PACKAGE_VERSION: readClientVersion(),
2516
- LIVEDESK_HUB_UPDATE_REQUEST_PATH: updateRequestPath,
2495
+ LIVEDESK_HUB_UPDATE_REQUEST_PATH: updateHost?.available ? updateRequestPath : '',
2517
2496
  LIVEDESK_HUB_UPDATE_RESULT_PATH: hubUpdateResultPath,
2518
2497
  LIVEDESK_UDP_ENABLED: String(process.env.LIVEDESK_UDP_ENABLED || '1'),
2519
2498
  LIVEDESK_UDP_PREFER_P2P: String(process.env.LIVEDESK_UDP_PREFER_P2P || '0'),
@@ -2641,7 +2620,10 @@ async function runManager(args, resolvedRole = null, runtimeLock = null) {
2641
2620
  process.on(signal, handler);
2642
2621
  }
2643
2622
 
2644
- const restartWithLatest = async request => {
2623
+ const restartWithLatest = async request => {
2624
+ if (!updateHost?.available) {
2625
+ throw new Error('The independent LiveDesk Update Host is unavailable. The unchanged Hub remains running.');
2626
+ }
2645
2627
  const requestedVersion = normalizeExactUpdateVersion(request?.latestVersion);
2646
2628
  if (!requestedVersion) {
2647
2629
  throw new Error('The Hub update target version must be one exact semantic version.');
@@ -2698,67 +2680,83 @@ async function runManager(args, resolvedRole = null, runtimeLock = null) {
2698
2680
  reportLauncherUpdate(`[LiveDesk Hub] Could not persist the restart supervisor state: ${error instanceof Error ? error.message : String(error)}.`, true);
2699
2681
  throw error;
2700
2682
  }
2701
- const restartBootstrap = spawn(process.execPath, ['-e', buildHubRestartBootstrapScript()], {
2702
- cwd: neutralCwd,
2703
- env: {
2704
- ...process.env,
2705
- LIVEDESK_RESTART_WAIT_PID: String(process.pid),
2706
- LIVEDESK_RESTART_PREVIOUS_RUNTIME_PID: String(request?.pid || 0),
2707
- LIVEDESK_RESTART_OPERATION_ID: operationId,
2708
- LIVEDESK_RESTART_STARTED_AT: restartStartedAt,
2709
- LIVEDESK_RESTART_HANDOFF_TOKEN: handoffToken,
2710
- LIVEDESK_RESTART_RESULT_PATH: hubUpdateResultPath,
2711
- LIVEDESK_RESTART_COMMAND: restartInvocation.command,
2712
- LIVEDESK_RESTART_ARGS_BASE64: Buffer.from(JSON.stringify(restartInvocation.args), 'utf8').toString('base64'),
2713
- LIVEDESK_RESTART_FALLBACK_COMMAND: fallbackInvocation.command,
2714
- LIVEDESK_RESTART_FALLBACK_ARGS_BASE64: Buffer.from(JSON.stringify(fallbackInvocation.args), 'utf8').toString('base64'),
2715
- LIVEDESK_RESTART_EXPECTED_VERSION: requestedVersion,
2716
- LIVEDESK_RESTART_FALLBACK_VERSION: readVersion(),
2717
- LIVEDESK_RESTART_HEALTH_URL: hubProbeBaseUrl,
2718
- LIVEDESK_RESTART_REMOTE_HOST: '127.0.0.1',
2719
- LIVEDESK_RESTART_REMOTE_PORT: String(remotePort),
2720
- LIVEDESK_RESTART_NEUTRAL_CWD: neutralCwd,
2721
- LIVEDESK_RESTART_FALLBACK_CWD: originalCwd,
2722
- LIVEDESK_UPDATE_ORIGINAL_CWD: originalCwd,
2723
- LIVEDESK_RESTART_LOG_PATH: hubLogPath
2724
- },
2725
- stdio: 'ignore',
2726
- detached: true,
2727
- windowsHide: true
2728
- });
2729
- try {
2730
- await waitForHubRestartSupervisorClaim(restartBootstrap, {
2731
- operationId,
2732
- targetVersion: requestedVersion,
2733
- resultPath: hubUpdateResultPath,
2683
+ const workerEnvironment = {
2684
+ ...process.env,
2685
+ LIVEDESK_RESTART_WAIT_PID: String(process.pid),
2686
+ LIVEDESK_RESTART_PREVIOUS_RUNTIME_PID: String(request?.pid || 0),
2687
+ LIVEDESK_RESTART_OPERATION_ID: operationId,
2688
+ LIVEDESK_RESTART_STARTED_AT: restartStartedAt,
2689
+ LIVEDESK_RESTART_HANDOFF_TOKEN: handoffToken,
2690
+ LIVEDESK_RESTART_RESULT_PATH: hubUpdateResultPath,
2691
+ LIVEDESK_RESTART_COMMAND: restartInvocation.command,
2692
+ LIVEDESK_RESTART_ARGS_BASE64: Buffer.from(JSON.stringify(restartInvocation.args), 'utf8').toString('base64'),
2693
+ LIVEDESK_RESTART_FALLBACK_COMMAND: fallbackInvocation.command,
2694
+ LIVEDESK_RESTART_FALLBACK_ARGS_BASE64: Buffer.from(JSON.stringify(fallbackInvocation.args), 'utf8').toString('base64'),
2695
+ LIVEDESK_RESTART_EXPECTED_VERSION: requestedVersion,
2696
+ LIVEDESK_RESTART_FALLBACK_VERSION: readVersion(),
2697
+ LIVEDESK_RESTART_HEALTH_URL: hubProbeBaseUrl,
2698
+ LIVEDESK_RESTART_REMOTE_HOST: '127.0.0.1',
2699
+ LIVEDESK_RESTART_REMOTE_PORT: String(remotePort),
2700
+ LIVEDESK_RESTART_NEUTRAL_CWD: neutralCwd,
2701
+ LIVEDESK_RESTART_FALLBACK_CWD: originalCwd,
2702
+ LIVEDESK_UPDATE_ORIGINAL_CWD: originalCwd,
2703
+ LIVEDESK_RESTART_LOG_PATH: hubLogPath
2704
+ };
2705
+ const hostJobId = `hub-${safeUpdatePathSegment(operationId)}`;
2706
+ let hostJob;
2707
+ try {
2708
+ const submitted = await submitUpdateHostJob(updateHost, {
2709
+ jobId: hostJobId,
2710
+ kind: 'hub-update',
2711
+ operationId,
2712
+ command: process.execPath,
2713
+ args: ['-e', buildHubRestartBootstrapScript()],
2714
+ cwd: neutralCwd,
2715
+ env: workerEnvironment,
2716
+ resultPath: hubUpdateResultPath,
2717
+ requestedAt: restartStartedAt,
2718
+ expiresAt: new Date(Date.parse(restartStartedAt) + 90 * 60_000).toISOString()
2719
+ }, { timeoutMs: handshakeTimeoutMs });
2720
+ hostJob = submitted?.job || null;
2721
+ if (!hostJob || Number(hostJob.workerPid || 0) <= 1) {
2722
+ throw new Error('LiveDesk Update Host accepted the request without a worker identity.');
2723
+ }
2724
+ } catch (error) {
2725
+ throw new Error(`LiveDesk Update Host rejected the Hub replacement: ${error instanceof Error ? error.message : String(error)}`);
2726
+ }
2727
+ try {
2728
+ await waitForHubRestartUpdateHostClaim(updateHost, hostJobId, {
2729
+ operationId,
2730
+ targetVersion: requestedVersion,
2731
+ resultPath: hubUpdateResultPath,
2734
2732
  launcherPid: process.pid,
2735
2733
  handoffToken,
2736
2734
  timeoutMs: handshakeTimeoutMs,
2737
2735
  stabilityMs: HUB_UPDATE_SUPERVISOR_CLAIM_STABILITY_MS
2738
2736
  });
2739
- } catch (handshakeError) {
2740
- let stopError = null;
2741
- try {
2742
- await stopUnclaimedHubRestartSupervisor(restartBootstrap);
2743
- } catch (error) {
2744
- stopError = error;
2745
- }
2737
+ } catch (handshakeError) {
2738
+ let stopError = null;
2739
+ try {
2740
+ await cancelUnclaimedHubUpdateHostJob(updateHost, hostJobId, 'Hub handoff was not proven.');
2741
+ } catch (error) {
2742
+ stopError = error;
2743
+ }
2746
2744
  const error = new Error(
2747
2745
  `Hub restart supervisor handoff failed: `
2748
2746
  + `${handshakeError instanceof Error ? handshakeError.message : String(handshakeError)}`
2749
2747
  + `${stopError ? ` ${stopError.message}` : ''}`
2750
2748
  );
2751
- error.supervisorPid = Number(restartBootstrap.pid || 0);
2752
- error.handoffToken = handoffToken;
2753
- error.handoffCancelled = true;
2754
- throw error;
2755
- }
2756
- restartBootstrap.unref();
2757
- reportLauncherUpdate(
2758
- `[LiveDesk Hub] Update handoff claimed target=${requestedVersion} `
2759
- + `supervisorPid=${restartBootstrap.pid || 0} operation=${operationId}. `
2760
- + `The current launcher will now exit. Restart diagnostics: ${hubLogPath}`
2761
- );
2749
+ error.supervisorPid = Number(hostJob?.workerPid || 0);
2750
+ error.updateHostPid = Number(updateHost?.host?.pid || 0);
2751
+ error.handoffToken = handoffToken;
2752
+ error.handoffCancelled = true;
2753
+ throw error;
2754
+ }
2755
+ reportLauncherUpdate(
2756
+ `[LiveDesk Hub] Update handoff claimed target=${requestedVersion} `
2757
+ + `updateHostPid=${updateHost.host?.pid || 0} workerPid=${hostJob.workerPid || 0} operation=${operationId}. `
2758
+ + `The current launcher will now exit. Restart diagnostics: ${hubLogPath}`
2759
+ );
2762
2760
  process.exit(0);
2763
2761
  };
2764
2762
 
@@ -3287,11 +3285,30 @@ async function main() {
3287
3285
  });
3288
3286
  }
3289
3287
  }
3290
- if (!lock.acquired) {
3291
- reportExistingRuntime(lock.existing || { role: resolvedRole.role, pid: 'unknown' });
3292
- return;
3293
- }
3294
- process.once('exit', lock.release);
3288
+ if (!lock.acquired) {
3289
+ reportExistingRuntime(lock.existing || { role: resolvedRole.role, pid: 'unknown' });
3290
+ return;
3291
+ }
3292
+ process.once('exit', lock.release);
3293
+
3294
+ const updateHost = await ensureUpdateHost({
3295
+ stateDir: MANAGER_STATE_DIR,
3296
+ entryPath: resolve(packageRoot, 'bootstrap', 'update-host.mjs'),
3297
+ sourceVersion: readVersion(),
3298
+ env: process.env
3299
+ });
3300
+ Object.assign(process.env, updateHostEnvironment(updateHost));
3301
+ if (updateHost.available) {
3302
+ console.log(
3303
+ `[LiveDesk] Independent Update Host ready pid=${updateHost.host?.pid || 'unknown'} `
3304
+ + `source=${updateHost.host?.sourceVersion || readVersion()}.`
3305
+ );
3306
+ } else if (!updateHost.disabled) {
3307
+ console.warn(
3308
+ `[LiveDesk] Independent Update Host is unavailable; updates are disabled for this run: `
3309
+ + `${updateHost.error || 'unknown error'}`
3310
+ );
3311
+ }
3295
3312
 
3296
3313
  // Only the contender that won the replacement lock may clean residual
3297
3314
  // listener ports. A simultaneous losing launcher must never kill the newly
@@ -3335,16 +3352,16 @@ async function main() {
3335
3352
  await runClient(runtimeArgs, resolvedRole, lock);
3336
3353
  return;
3337
3354
  }
3338
- if (command === 'hub' || command === 'manager') {
3339
- await runManager(runtimeArgs, resolvedRole, lock);
3355
+ if (command === 'hub' || command === 'manager') {
3356
+ await runManager(runtimeArgs, resolvedRole, lock, updateHost);
3340
3357
  return;
3341
3358
  }
3342
3359
  if (resolvedRole.role === 'client') {
3343
3360
  await runClient(runtimeArgs, resolvedRole, lock);
3344
3361
  return;
3345
3362
  }
3346
- await runManager(runtimeArgs, resolvedRole, lock);
3347
- }
3363
+ await runManager(runtimeArgs, resolvedRole, lock, updateHost);
3364
+ }
3348
3365
 
3349
3366
  main().catch(error => {
3350
3367
  console.error(error?.message || error);