livedesk 0.1.384 → 0.1.385

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.
@@ -927,9 +927,10 @@ function serializeDevice(device, options = {}) {
927
927
  platform: device.platform,
928
928
  arch: device.arch,
929
929
  slotNumber: device.slotNumber || 0,
930
- pid: device.pid,
931
- agentVersion: device.agentVersion,
932
- protocol: device.protocol,
930
+ pid: device.pid,
931
+ agentVersion: device.agentVersion,
932
+ productVersion: device.productVersion,
933
+ protocol: device.protocol,
933
934
  protocolVersion: device.protocolVersion,
934
935
  frameProtocol: device.frameProtocol ? { ...device.frameProtocol } : null,
935
936
  frameModes: Array.isArray(device.frameModes)
@@ -2646,9 +2647,10 @@ export function createRemoteHub(options = {}) {
2646
2647
  platform: safeString(hello.platform, 80),
2647
2648
  arch: safeString(hello.arch, 40),
2648
2649
  slotNumber: normalizeSlotNumber(hello.slotNumber ?? hello.slot ?? hello.computerNumber),
2649
- pid: Number.isFinite(Number(hello.pid)) ? Number(hello.pid) : 0,
2650
- agentVersion: safeString(hello.agentVersion, 40),
2651
- protocol: safeString(hello.protocol, 80) || REMOTE_AGENT_PROTOCOL,
2650
+ pid: Number.isFinite(Number(hello.pid)) ? Number(hello.pid) : 0,
2651
+ agentVersion: safeString(hello.agentVersion, 40),
2652
+ productVersion: safeString(hello.productVersion, 40),
2653
+ protocol: safeString(hello.protocol, 80) || REMOTE_AGENT_PROTOCOL,
2652
2654
  protocolVersion: Number.isFinite(Number(hello.protocolVersion)) ? Math.max(1, Math.floor(Number(hello.protocolVersion))) : 1,
2653
2655
  frameProtocol: frameProtocol || buildRemoteFrameProtocolDescriptor(),
2654
2656
  frameModes: frameModes.length > 0 ? frameModes : getSupportedRemoteFrameModeProfiles(),
package/hub/src/server.js CHANGED
@@ -67,7 +67,12 @@ const mode5FrameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_MODE5_WS_BA
67
67
  const mode5FrameQueuePackets = readPositiveIntegerEnv('LIVEDESK_MODE5_WS_QUEUE_PACKETS', 2);
68
68
  const audioBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_AUDIO_WS_BACKPRESSURE_BYTES', 96 * 1024);
69
69
  const webSocketHeartbeatMs = readPositiveIntegerEnv('LIVEDESK_BROWSER_WS_HEARTBEAT_MS', 15_000);
70
- const frameClients = new Set();
70
+ const hubShutdownGraceMs = readPositiveIntegerEnv('LIVEDESK_HUB_SHUTDOWN_GRACE_MS', 1_500);
71
+ const hubShutdownTimeoutMs = Math.max(
72
+ hubShutdownGraceMs + 1_000,
73
+ readPositiveIntegerEnv('LIVEDESK_HUB_SHUTDOWN_TIMEOUT_MS', 8_000)
74
+ );
75
+ const frameClients = new Set();
71
76
  const frameClientsByDeviceId = new Map();
72
77
  const frameWildcardClients = new Set();
73
78
  const pendingFrameStreamStops = new Map();
@@ -507,7 +512,7 @@ const remoteHub = createRemoteHub({
507
512
  }
508
513
  });
509
514
 
510
- function requestHubRestart(request = {}) {
515
+ function requestHubRestart(request = {}) {
511
516
  const requestPath = String(process.env.LIVEDESK_HUB_UPDATE_REQUEST_PATH || '').trim();
512
517
  if (!requestPath) {
513
518
  return { ok: false, error: 'hub-launcher-restart-unavailable' };
@@ -524,10 +529,73 @@ function requestHubRestart(request = {}) {
524
529
  return { ok: true };
525
530
  } catch (error) {
526
531
  return { ok: false, error: `hub-restart-request-failed: ${error instanceof Error ? error.message : String(error)}` };
527
- }
528
- }
529
-
530
- if (process.env.LIVEDESK_DESKTOP_HOST !== '1') {
532
+ }
533
+ }
534
+
535
+ const HUB_RESTART_RESULT_STAGES = new Set([
536
+ 'supervisor-starting',
537
+ 'waiting-for-old-launcher',
538
+ 'old-launcher-exited',
539
+ 'target-attempt',
540
+ 'target-failed',
541
+ 'fallback-attempt',
542
+ 'fallback-failed',
543
+ 'updated',
544
+ 'restored',
545
+ 'failed'
546
+ ]);
547
+ const HUB_RESTART_RESULT_OUTCOMES = new Set(['in-progress', 'updated', 'restored', 'failed']);
548
+
549
+ function readHubRestartResult() {
550
+ const resultPath = String(process.env.LIVEDESK_HUB_UPDATE_RESULT_PATH || '').trim();
551
+ if (!resultPath || !existsSync(resultPath)) return null;
552
+ try {
553
+ const raw = JSON.parse(readFileSync(resultPath, 'utf8'));
554
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
555
+ const operationId = String(raw.operationId || '').trim().slice(0, 160);
556
+ const stage = String(raw.stage || '').trim();
557
+ const outcome = String(raw.outcome || '').trim();
558
+ if (!operationId || !HUB_RESTART_RESULT_STAGES.has(stage) || !HUB_RESTART_RESULT_OUTCOMES.has(outcome)) {
559
+ return null;
560
+ }
561
+ const text = (value, maximum = 200) => String(value || '').trim().slice(0, maximum);
562
+ const positiveInteger = value => {
563
+ const numeric = Number(value);
564
+ return Number.isInteger(numeric) && numeric > 0 ? numeric : undefined;
565
+ };
566
+ return {
567
+ operationId,
568
+ stage,
569
+ outcome,
570
+ targetVersion: text(raw.targetVersion),
571
+ fallbackVersion: text(raw.fallbackVersion),
572
+ appVersion: text(raw.appVersion),
573
+ attempt: positiveInteger(raw.attempt),
574
+ previousRuntimePid: positiveInteger(raw.previousRuntimePid),
575
+ supervisorPid: positiveInteger(raw.supervisorPid),
576
+ launcherPid: positiveInteger(raw.launcherPid),
577
+ runtimePid: positiveInteger(raw.runtimePid),
578
+ startedAt: text(raw.startedAt, 64),
579
+ updatedAt: text(raw.updatedAt, 64),
580
+ completedAt: text(raw.completedAt, 64),
581
+ error: text(raw.error, 4_000)
582
+ };
583
+ } catch {
584
+ return null;
585
+ }
586
+ }
587
+
588
+ function getLiveDeskUpdateStatus() {
589
+ return {
590
+ ...(liveDeskUpdateManager?.getStatus() || {
591
+ updateAvailable: false,
592
+ state: 'unavailable'
593
+ }),
594
+ restartResult: readHubRestartResult()
595
+ };
596
+ }
597
+
598
+ if (process.env.LIVEDESK_DESKTOP_HOST !== '1') {
531
599
  liveDeskUpdateManager = createLiveDeskUpdateManager({
532
600
  remoteHub,
533
601
  currentManagerVersion: process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version,
@@ -1062,14 +1130,20 @@ const hubSharedFolders = createHubSharedFolders({
1062
1130
  remoteHub
1063
1131
  });
1064
1132
 
1065
- const app = express();
1066
- const httpServer = createServer(app);
1133
+ const app = express();
1134
+ const httpServer = createServer(app);
1135
+ const httpConnections = new Set();
1067
1136
  const frameWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
1068
1137
  const atlasWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
1069
1138
  const inputWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
1070
1139
  const audioWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
1071
1140
  const browserWebSocketServers = [frameWss, atlasWss, inputWss, audioWss];
1072
1141
 
1142
+ httpServer.on('connection', socket => {
1143
+ httpConnections.add(socket);
1144
+ socket.once('close', () => httpConnections.delete(socket));
1145
+ });
1146
+
1073
1147
  for (const wss of browserWebSocketServers) {
1074
1148
  wss.on('connection', ws => {
1075
1149
  ws.liveDeskHeartbeatAlive = true;
@@ -2765,8 +2839,8 @@ app.get('/api/remote/status', (_req, res) => {
2765
2839
  deviceId: runtimeDeviceId,
2766
2840
  deviceName: runtimeDeviceName,
2767
2841
  roleSource: runtimeRoleSource,
2768
- agentPackage: '@livedesk/client',
2769
- update: liveDeskUpdateManager?.getStatus() || null
2842
+ agentPackage: '@livedesk/client',
2843
+ update: getLiveDeskUpdateStatus()
2770
2844
  });
2771
2845
  });
2772
2846
 
@@ -2780,19 +2854,27 @@ app.get('/api/runtime/events', (_req, res) => {
2780
2854
  res.json({ ok: true, events: runtimeManager.getEvents() });
2781
2855
  });
2782
2856
 
2783
- app.post('/api/runtime/restart', (_req, res) => {
2784
- noStore(res);
2785
- runtimeManager.emit('runtime.restart.requested', { role: runtimeRole });
2786
- res.json({ ok: true, restarting: true, role: runtimeRole });
2787
- setTimeout(() => process.kill(process.pid, 'SIGTERM'), 150);
2788
- });
2789
-
2790
- app.post('/api/runtime/shutdown', (_req, res) => {
2791
- noStore(res);
2792
- runtimeManager.emit('runtime.shutdown.requested', { role: runtimeRole });
2793
- res.json({ ok: true, shuttingDown: true, role: runtimeRole });
2794
- setTimeout(() => process.kill(process.pid, 'SIGTERM'), 150);
2795
- });
2857
+ app.post('/api/runtime/restart', (_req, res) => {
2858
+ noStore(res);
2859
+ runtimeManager.emit('runtime.restart.requested', { role: runtimeRole });
2860
+ res.json({ ok: true, restarting: true, role: runtimeRole });
2861
+ setTimeout(() => {
2862
+ void shutdownHub('API_RESTART')
2863
+ .then(() => process.exit(0))
2864
+ .catch(() => process.exit(1));
2865
+ }, 150);
2866
+ });
2867
+
2868
+ app.post('/api/runtime/shutdown', (_req, res) => {
2869
+ noStore(res);
2870
+ runtimeManager.emit('runtime.shutdown.requested', { role: runtimeRole });
2871
+ res.json({ ok: true, shuttingDown: true, role: runtimeRole });
2872
+ setTimeout(() => {
2873
+ void shutdownHub('API_SHUTDOWN')
2874
+ .then(() => process.exit(0))
2875
+ .catch(() => process.exit(1));
2876
+ }, 150);
2877
+ });
2796
2878
 
2797
2879
  app.post('/api/runtime/switch-role', (req, res) => {
2798
2880
  noStore(res);
@@ -3137,7 +3219,7 @@ app.get('/api/hub/status', (_req, res) => {
3137
3219
  roleSource: runtimeRoleSource,
3138
3220
  runtimeStarted: true,
3139
3221
  hostTargetLease: getHubHostTargetLeaseStatus(),
3140
- update: liveDeskUpdateManager?.getStatus() || null
3222
+ update: getLiveDeskUpdateStatus()
3141
3223
  });
3142
3224
  });
3143
3225
 
@@ -3248,14 +3330,20 @@ app.post('/api/runtime/role', async (req, res) => {
3248
3330
  }
3249
3331
  });
3250
3332
 
3251
- app.get('/api/update/status', (_req, res) => {
3252
- noStore(res);
3253
- if (process.env.LIVEDESK_DESKTOP_HOST === '1') {
3254
- res.json({ updateAvailable: false, state: 'electron-managed', canApply: false, disabled: true });
3255
- return;
3256
- }
3257
- res.json(liveDeskUpdateManager?.getStatus() || { updateAvailable: false, state: 'unavailable' });
3258
- });
3333
+ app.get('/api/update/status', (_req, res) => {
3334
+ noStore(res);
3335
+ if (process.env.LIVEDESK_DESKTOP_HOST === '1') {
3336
+ res.json({
3337
+ updateAvailable: false,
3338
+ state: 'electron-managed',
3339
+ canApply: false,
3340
+ disabled: true,
3341
+ restartResult: readHubRestartResult()
3342
+ });
3343
+ return;
3344
+ }
3345
+ res.json(getLiveDeskUpdateStatus());
3346
+ });
3259
3347
 
3260
3348
  app.post('/api/update/apply', async (_req, res) => {
3261
3349
  noStore(res);
@@ -4037,18 +4125,130 @@ const roleWatchTimer = runtimeRole === 'hub'
4037
4125
  : null;
4038
4126
  roleWatchTimer?.unref?.();
4039
4127
 
4040
- for (const signal of ['SIGINT', 'SIGTERM']) {
4041
- process.once(signal, async () => {
4042
- if (roleWatchTimer) {
4043
- clearInterval(roleWatchTimer);
4128
+ function waitForShutdownDelay(milliseconds) {
4129
+ return new Promise(resolveWait => {
4130
+ const timer = setTimeout(resolveWait, milliseconds);
4131
+ timer.unref?.();
4132
+ });
4133
+ }
4134
+
4135
+ async function boundedShutdownStep(label, action, timeoutMs) {
4136
+ let timer;
4137
+ try {
4138
+ await Promise.race([
4139
+ Promise.resolve().then(action),
4140
+ new Promise((_, reject) => {
4141
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
4142
+ timer.unref?.();
4143
+ })
4144
+ ]);
4145
+ console.log(`[LiveDesk Hub] Shutdown stage completed: ${label}.`);
4146
+ } catch (error) {
4147
+ console.warn(`[LiveDesk Hub] Shutdown stage warning: ${error instanceof Error ? error.message : String(error)}`);
4148
+ } finally {
4149
+ if (timer) clearTimeout(timer);
4150
+ }
4151
+ }
4152
+
4153
+ function runSynchronousShutdownStep(label, action) {
4154
+ try {
4155
+ action();
4156
+ } catch (error) {
4157
+ console.warn(`[LiveDesk Hub] Shutdown stage warning: ${label} failed: ${error instanceof Error ? error.message : String(error)}`);
4158
+ }
4159
+ }
4160
+
4161
+ function closeBrowserWebSockets() {
4162
+ let closeRequested = 0;
4163
+ for (const wss of browserWebSocketServers) {
4164
+ for (const ws of wss.clients) {
4165
+ closeRequested += 1;
4166
+ try {
4167
+ ws.close(1012, 'LiveDesk Hub restarting');
4168
+ } catch {
4169
+ try { ws.terminate(); } catch { /* socket already closed */ }
4170
+ }
4044
4171
  }
4172
+ }
4173
+ console.log(`[LiveDesk Hub] Shutdown stage: requested ${closeRequested} browser WebSocket connection(s) to close.`);
4174
+ }
4175
+
4176
+ function terminateOpenConnections() {
4177
+ let terminatedWebSockets = 0;
4178
+ for (const wss of browserWebSocketServers) {
4179
+ for (const ws of wss.clients) {
4180
+ terminatedWebSockets += 1;
4181
+ try { ws.terminate(); } catch { /* socket already closed */ }
4182
+ }
4183
+ try { wss.close(); } catch { /* no active WebSocket server state */ }
4184
+ }
4185
+ let destroyedHttpConnections = 0;
4186
+ for (const socket of httpConnections) {
4187
+ if (socket.destroyed) continue;
4188
+ destroyedHttpConnections += 1;
4189
+ try { socket.destroy(); } catch { /* socket already closed */ }
4190
+ }
4191
+ try { httpServer.closeAllConnections?.(); } catch { /* older Node runtime */ }
4192
+ console.log(`[LiveDesk Hub] Shutdown stage: force-closed ${terminatedWebSockets} WebSocket and ${destroyedHttpConnections} HTTP connection(s).`);
4193
+ }
4194
+
4195
+ let hubShutdownPromise = null;
4196
+ function shutdownHub(signal) {
4197
+ if (hubShutdownPromise) return hubShutdownPromise;
4198
+ hubShutdownPromise = (async () => {
4199
+ const startedAt = Date.now();
4200
+ console.log(`[LiveDesk Hub] Shutdown started signal=${signal} grace=${hubShutdownGraceMs}ms timeout=${hubShutdownTimeoutMs}ms.`);
4201
+ if (roleWatchTimer) clearInterval(roleWatchTimer);
4045
4202
  clearInterval(browserWebSocketHeartbeatTimer);
4046
- liveDeskUpdateManager?.close();
4047
- atlasPool.close();
4203
+ runSynchronousShutdownStep('update manager close', () => liveDeskUpdateManager?.close());
4204
+ runSynchronousShutdownStep('atlas pool close', () => atlasPool.close());
4048
4205
  atlasClients.clear();
4049
- hubSharedFolders.close();
4050
- await clearHubHostTarget(`process-${signal.toLowerCase()}`);
4051
- await remoteHub.close();
4052
- httpServer.close(() => process.exit(0));
4053
- });
4054
- }
4206
+ runSynchronousShutdownStep('shared folder close', () => hubSharedFolders.close());
4207
+
4208
+ const httpClosed = new Promise(resolveClose => {
4209
+ try {
4210
+ httpServer.close(error => {
4211
+ if (error) {
4212
+ console.warn(`[LiveDesk Hub] HTTP listener close warning: ${error.message}`);
4213
+ } else {
4214
+ console.log('[LiveDesk Hub] Shutdown stage completed: HTTP listener closed.');
4215
+ }
4216
+ resolveClose();
4217
+ });
4218
+ httpServer.closeIdleConnections?.();
4219
+ } catch (error) {
4220
+ console.warn(`[LiveDesk Hub] HTTP listener close warning: ${error instanceof Error ? error.message : String(error)}`);
4221
+ resolveClose();
4222
+ }
4223
+ });
4224
+
4225
+ closeBrowserWebSockets();
4226
+ const forceCloseTimer = setTimeout(terminateOpenConnections, hubShutdownGraceMs);
4227
+ forceCloseTimer.unref?.();
4228
+
4229
+ const cleanupBudgetMs = Math.max(500, hubShutdownTimeoutMs - hubShutdownGraceMs - 500);
4230
+ await Promise.all([
4231
+ boundedShutdownStep('host target lease cleared', () => clearHubHostTarget(`process-${String(signal).toLowerCase()}`), Math.min(3_000, cleanupBudgetMs)),
4232
+ boundedShutdownStep('remote transport closed', () => remoteHub.close(), cleanupBudgetMs)
4233
+ ]);
4234
+
4235
+ const remainingMs = Math.max(0, hubShutdownTimeoutMs - (Date.now() - startedAt));
4236
+ await Promise.race([httpClosed, waitForShutdownDelay(remainingMs)]);
4237
+ clearTimeout(forceCloseTimer);
4238
+ terminateOpenConnections();
4239
+ console.log(`[LiveDesk Hub] Shutdown complete in ${Date.now() - startedAt}ms.`);
4240
+ })();
4241
+ return hubShutdownPromise;
4242
+ }
4243
+
4244
+ for (const signal of ['SIGINT', 'SIGTERM']) {
4245
+ process.once(signal, () => {
4246
+ void shutdownHub(signal)
4247
+ .then(() => process.exit(0))
4248
+ .catch(error => {
4249
+ console.error(`[LiveDesk Hub] Shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
4250
+ terminateOpenConnections();
4251
+ process.exit(1);
4252
+ });
4253
+ });
4254
+ }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
- "name": "livedesk",
3
- "version": "0.1.384",
4
- "buildFlavor": "production",
2
+ "name": "livedesk",
3
+ "version": "0.1.385",
4
+ "livedeskClientVersion": "0.1.170",
5
+ "buildFlavor": "production",
5
6
  "description": "LiveDesk Hub and client launcher",
6
7
  "type": "module",
7
8
  "main": "electron/main.mjs",
@@ -48,10 +49,10 @@
48
49
  "ws": "^8.18.3"
49
50
  },
50
51
  "optionalDependencies": {
51
- "@livedesk/fast-linux-x64": "0.1.373",
52
- "@livedesk/fast-osx-arm64": "0.1.375",
53
- "@livedesk/fast-osx-x64": "0.1.375",
54
- "@livedesk/fast-win-x64": "0.1.373"
52
+ "@livedesk/fast-linux-x64": "0.1.374",
53
+ "@livedesk/fast-osx-arm64": "0.1.376",
54
+ "@livedesk/fast-osx-x64": "0.1.376",
55
+ "@livedesk/fast-win-x64": "0.1.374"
55
56
  },
56
57
  "publishConfig": {
57
58
  "access": "public"