@livedesk/hub 0.1.5 → 0.1.7
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/package.json +2 -2
- package/src/agents/agent-tool-registry.js +19 -19
- package/src/agents/codex-agent-runtime.js +2 -2
- package/src/filesystem/shared-folders.js +1 -1
- package/src/filesystem/transfer-jobs.js +90 -5
- package/src/live-desk-update.js +585 -302
- package/src/mode4-atlas-pool.js +123 -0
- package/src/mode4-atlas-sizing.js +32 -0
- package/src/mode4-atlas-worker.js +58 -11
- package/src/mode4-atlas.js +9 -1
- package/src/remote-hub.js +1259 -423
- package/src/server.js +776 -275
- package/src/settings/settings-schema.js +13 -6
package/src/server.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
import express from 'express';
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import { createServer } from 'node:http';
|
|
6
|
-
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { dirname, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import os from 'node:os';
|
|
10
10
|
import { WebSocketServer } from 'ws';
|
|
11
|
-
import { createRemoteHub } from './remote-hub.js';
|
|
12
|
-
import {
|
|
11
|
+
import { createRemoteHub } from './remote-hub.js';
|
|
12
|
+
import { buildMode4AtlasSessionKey, Mode4AtlasPool, planMode4AtlasInputTransitions } from './mode4-atlas-pool.js';
|
|
13
|
+
import { resolveMode4AtlasTileSize } from './mode4-atlas-sizing.js';
|
|
13
14
|
import { createHubFilesystem } from './filesystem/hub-filesystem.js';
|
|
14
15
|
import { createHubTransferJobs } from './filesystem/transfer-jobs.js';
|
|
15
16
|
import { createHubSharedFolders } from './filesystem/shared-folders.js';
|
|
@@ -59,19 +60,31 @@ const runtimeManager = createHubRuntime({
|
|
|
59
60
|
runtimePid: process.pid
|
|
60
61
|
});
|
|
61
62
|
runtimeManager.emit('runtime.started', { role: runtimeRole, pid: process.pid });
|
|
62
|
-
const frameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_BACKPRESSURE_BYTES',
|
|
63
|
-
const frameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_QUEUE_PACKETS',
|
|
64
|
-
const controlFrameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_CONTROL_FRAME_WS_QUEUE_PACKETS',
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
63
|
+
const frameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_BACKPRESSURE_BYTES', 8 * 1024 * 1024);
|
|
64
|
+
const frameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_QUEUE_PACKETS', 4);
|
|
65
|
+
const controlFrameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_CONTROL_FRAME_WS_QUEUE_PACKETS', 2);
|
|
66
|
+
const frameStreamStopGraceMs = readPositiveIntegerEnv('LIVEDESK_FRAME_STREAM_STOP_GRACE_MS', 1500);
|
|
67
|
+
const mode5FrameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_MODE5_WS_BACKPRESSURE_BYTES', 2 * 1024 * 1024);
|
|
68
|
+
const mode5FrameQueuePackets = readPositiveIntegerEnv('LIVEDESK_MODE5_WS_QUEUE_PACKETS', 2);
|
|
69
|
+
const audioBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_AUDIO_WS_BACKPRESSURE_BYTES', 96 * 1024);
|
|
70
|
+
const webSocketHeartbeatMs = readPositiveIntegerEnv('LIVEDESK_BROWSER_WS_HEARTBEAT_MS', 15_000);
|
|
71
|
+
const hubShutdownGraceMs = readPositiveIntegerEnv('LIVEDESK_HUB_SHUTDOWN_GRACE_MS', 1_500);
|
|
72
|
+
const hubShutdownTimeoutMs = Math.max(
|
|
73
|
+
hubShutdownGraceMs + 1_000,
|
|
74
|
+
readPositiveIntegerEnv('LIVEDESK_HUB_SHUTDOWN_TIMEOUT_MS', 8_000)
|
|
75
|
+
);
|
|
76
|
+
const frameClients = new Set();
|
|
71
77
|
const frameClientsByDeviceId = new Map();
|
|
72
78
|
const frameWildcardClients = new Set();
|
|
73
79
|
const pendingFrameStreamStops = new Map();
|
|
74
|
-
const atlasClients = new Map();
|
|
80
|
+
const atlasClients = new Map();
|
|
81
|
+
const atlasPool = new Mode4AtlasPool({
|
|
82
|
+
onStatus: status => {
|
|
83
|
+
if (status?.type !== 'log') return;
|
|
84
|
+
const logger = status.level === 'warn' ? console.warn : console.log;
|
|
85
|
+
logger(`[LiveDesk Hub] ${status.message}`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
75
88
|
const inputClients = new Set();
|
|
76
89
|
const audioClients = new Set();
|
|
77
90
|
const FREE_DEVICE_LIMIT = 5;
|
|
@@ -94,6 +107,10 @@ const supabasePublishableKey = String(configuredSupabaseKey || PRODUCTION_SUPABA
|
|
|
94
107
|
const SUPABASE_AUTH_TIMEOUT_MS = process.env.LIVEDESK_AUTH_TEST_MODE === '1'
|
|
95
108
|
? readPositiveIntegerEnv('LIVEDESK_AUTH_TEST_TIMEOUT_MS', AUTH_REQUEST_TIMEOUT_MS)
|
|
96
109
|
: AUTH_REQUEST_TIMEOUT_MS;
|
|
110
|
+
const CLIENT_AUTH_STORAGE_KEY = 'livedesk.client.supabase.auth';
|
|
111
|
+
const runtimeAuthStatePath = process.env.LIVEDESK_DESKTOP_HOST === '1'
|
|
112
|
+
? ''
|
|
113
|
+
: String(process.env.LIVEDESK_AUTH_STATE_PATH || '').trim();
|
|
97
114
|
const testLicensePlan = process.env.LIVEDESK_TEST_MODE === '1'
|
|
98
115
|
&& ['ltd', 'pro'].includes(String(process.env.LIVEDESK_TEST_LICENSE_PLAN || '').toLowerCase())
|
|
99
116
|
? String(process.env.LIVEDESK_TEST_LICENSE_PLAN).toLowerCase()
|
|
@@ -111,9 +128,10 @@ let verifiedLicense = {
|
|
|
111
128
|
verifiedAt: 0
|
|
112
129
|
};
|
|
113
130
|
let frameClientSeq = 0;
|
|
114
|
-
let inputClientSeq = 0;
|
|
115
|
-
let audioClientSeq = 0;
|
|
131
|
+
let inputClientSeq = 0;
|
|
132
|
+
let audioClientSeq = 0;
|
|
116
133
|
let liveDeskUpdateManager = null;
|
|
134
|
+
let hubTransferJobs = null;
|
|
117
135
|
const HUB_HOST_TARGET_LEASE_MS = Math.max(5000, readPositiveIntegerEnv('LIVEDESK_HOST_TARGET_LEASE_MS', 60_000));
|
|
118
136
|
const HUB_HOST_TARGET_RENEW_MS = Math.max(1000, Math.min(
|
|
119
137
|
readPositiveIntegerEnv('LIVEDESK_HOST_TARGET_RENEW_MS', 20_000),
|
|
@@ -124,6 +142,7 @@ const hubWakeBaseUrl = String(process.env.LIVEDESK_WAKE_URL || 'https://livedesk
|
|
|
124
142
|
const HUB_WAKE_NOTIFY_RETRY_MS = 60_000;
|
|
125
143
|
let hubHostTargetRenewTimer = null;
|
|
126
144
|
let hubHostTargetRenewInFlight = false;
|
|
145
|
+
let roleTransitionTakeoverPending = process.env.LIVEDESK_ROLE_TAKEOVER === '1';
|
|
127
146
|
let hubHostTargetLeaseState = {
|
|
128
147
|
state: 'idle',
|
|
129
148
|
lastAttemptAt: '',
|
|
@@ -148,8 +167,9 @@ function readPositiveIntegerEnv(name, fallback) {
|
|
|
148
167
|
return Number.isFinite(value) && value > 0 ? Math.round(value) : fallback;
|
|
149
168
|
}
|
|
150
169
|
|
|
151
|
-
function handleRemoteHubEvent(type, event) {
|
|
152
|
-
liveDeskUpdateManager?.handleRemoteEvent(type, event);
|
|
170
|
+
function handleRemoteHubEvent(type, event) {
|
|
171
|
+
liveDeskUpdateManager?.handleRemoteEvent(type, event);
|
|
172
|
+
hubTransferJobs?.handleRemoteEvent(type, event);
|
|
153
173
|
if (type === 'RemoteInputApplied') {
|
|
154
174
|
for (const ws of inputClients) {
|
|
155
175
|
sendJson(ws, {
|
|
@@ -159,16 +179,16 @@ function handleRemoteHubEvent(type, event) {
|
|
|
159
179
|
}
|
|
160
180
|
return;
|
|
161
181
|
}
|
|
162
|
-
if (type === 'RemoteInputError') {
|
|
182
|
+
if (type === 'RemoteInputError') {
|
|
163
183
|
for (const ws of inputClients) {
|
|
164
184
|
sendJson(ws, {
|
|
165
185
|
type: 'RemoteInputError',
|
|
166
186
|
...event
|
|
167
187
|
});
|
|
168
|
-
}
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
if (type === 'RemoteDeviceConnected' || type === 'RemoteDeviceDisconnected') {
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (type === 'RemoteDeviceConnected' || type === 'RemoteDeviceDisconnected') {
|
|
172
192
|
connectedDeviceCount = Number(remoteHub.getStatus({ includeSecrets: false }).connectedDeviceCount || 0);
|
|
173
193
|
}
|
|
174
194
|
if (type !== 'RemoteDeviceConnected') {
|
|
@@ -293,11 +313,82 @@ function normalizeSessionExpiryMs(value) {
|
|
|
293
313
|
return numeric < 100_000_000_000 ? Math.round(numeric * 1000) : Math.round(numeric);
|
|
294
314
|
}
|
|
295
315
|
|
|
316
|
+
function readRuntimeAuthState() {
|
|
317
|
+
if (!runtimeAuthStatePath) return {};
|
|
318
|
+
try {
|
|
319
|
+
const state = JSON.parse(readFileSync(runtimeAuthStatePath, 'utf8'));
|
|
320
|
+
return state && typeof state === 'object' ? state : {};
|
|
321
|
+
} catch {
|
|
322
|
+
return {};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function readPersistedRuntimeSession(state = readRuntimeAuthState()) {
|
|
327
|
+
try {
|
|
328
|
+
const raw = state?.[CLIENT_AUTH_STORAGE_KEY];
|
|
329
|
+
const session = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
330
|
+
return session && typeof session === 'object' ? session : null;
|
|
331
|
+
} catch {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function writePrivateRuntimeAuthState(state) {
|
|
337
|
+
if (!runtimeAuthStatePath) return false;
|
|
338
|
+
mkdirSync(dirname(runtimeAuthStatePath), { recursive: true });
|
|
339
|
+
const temporaryPath = `${runtimeAuthStatePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
340
|
+
try {
|
|
341
|
+
writeFileSync(temporaryPath, JSON.stringify(state, null, 2), 'utf8');
|
|
342
|
+
try { chmodSync(temporaryPath, 0o600); } catch { /* Windows profile ACLs apply instead. */ }
|
|
343
|
+
renameSync(temporaryPath, runtimeAuthStatePath);
|
|
344
|
+
try { chmodSync(runtimeAuthStatePath, 0o600); } catch { /* Windows profile ACLs apply instead. */ }
|
|
345
|
+
return true;
|
|
346
|
+
} catch (error) {
|
|
347
|
+
try { rmSync(temporaryPath, { force: true }); } catch { /* best effort */ }
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function persistRuntimeSession(session) {
|
|
353
|
+
if (!runtimeAuthStatePath) return false;
|
|
354
|
+
const state = readRuntimeAuthState();
|
|
355
|
+
const previous = readPersistedRuntimeSession(state) || {};
|
|
356
|
+
const incomingUserId = String(session?.user?.id || '').trim();
|
|
357
|
+
const previousUserId = String(previous?.user?.id || '').trim();
|
|
358
|
+
const sameUser = !incomingUserId || !previousUserId || incomingUserId === previousUserId;
|
|
359
|
+
const refreshToken = String(
|
|
360
|
+
session?.refresh_token
|
|
361
|
+
|| (sameUser ? previous?.refresh_token : '')
|
|
362
|
+
|| ''
|
|
363
|
+
).trim();
|
|
364
|
+
const normalized = normalizeRuntimeAuthSession({
|
|
365
|
+
...previous,
|
|
366
|
+
...session,
|
|
367
|
+
refresh_token: refreshToken,
|
|
368
|
+
user: {
|
|
369
|
+
...(sameUser && previous?.user && typeof previous.user === 'object' ? previous.user : {}),
|
|
370
|
+
...(session?.user && typeof session.user === 'object' ? session.user : {})
|
|
371
|
+
}
|
|
372
|
+
}, { requireRefreshToken: true });
|
|
373
|
+
if (!normalized.ok) return false;
|
|
374
|
+
state[CLIENT_AUTH_STORAGE_KEY] = JSON.stringify(normalized.session);
|
|
375
|
+
writePrivateRuntimeAuthState(state);
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function clearPersistedRuntimeSession() {
|
|
380
|
+
if (!runtimeAuthStatePath) return false;
|
|
381
|
+
const state = readRuntimeAuthState();
|
|
382
|
+
delete state[CLIENT_AUTH_STORAGE_KEY];
|
|
383
|
+
return writePrivateRuntimeAuthState(state);
|
|
384
|
+
}
|
|
385
|
+
|
|
296
386
|
function clearRuntimeSession() {
|
|
297
387
|
runtimeAccessToken = '';
|
|
298
388
|
runtimeRefreshToken = '';
|
|
299
389
|
runtimeAccessTokenExpiresAt = 0;
|
|
300
390
|
runtimeManager.setAuthenticated(false);
|
|
391
|
+
try { clearPersistedRuntimeSession(); } catch { /* runtime auth is already invalid */ }
|
|
301
392
|
}
|
|
302
393
|
|
|
303
394
|
async function getRuntimeAccessToken() {
|
|
@@ -343,6 +434,15 @@ async function getRuntimeAccessToken() {
|
|
|
343
434
|
runtimeRefreshToken = String(refreshed?.refresh_token || refreshToken).trim();
|
|
344
435
|
runtimeAccessTokenExpiresAt = normalizeSessionExpiryMs(refreshed?.expires_at)
|
|
345
436
|
|| (Number(refreshed?.expires_in) > 0 ? Date.now() + Number(refreshed.expires_in) * 1000 : 0);
|
|
437
|
+
try {
|
|
438
|
+
persistRuntimeSession({
|
|
439
|
+
access_token: runtimeAccessToken,
|
|
440
|
+
refresh_token: runtimeRefreshToken,
|
|
441
|
+
expires_at: Math.floor(runtimeAccessTokenExpiresAt / 1000)
|
|
442
|
+
});
|
|
443
|
+
} catch (error) {
|
|
444
|
+
console.warn(`[LiveDesk Hub] Refreshed session persistence failed: ${error?.message || error}`);
|
|
445
|
+
}
|
|
346
446
|
return runtimeAccessToken;
|
|
347
447
|
}
|
|
348
448
|
|
|
@@ -500,7 +600,7 @@ const remoteHub = createRemoteHub({
|
|
|
500
600
|
}
|
|
501
601
|
});
|
|
502
602
|
|
|
503
|
-
function requestHubRestart(request = {}) {
|
|
603
|
+
function requestHubRestart(request = {}) {
|
|
504
604
|
const requestPath = String(process.env.LIVEDESK_HUB_UPDATE_REQUEST_PATH || '').trim();
|
|
505
605
|
if (!requestPath) {
|
|
506
606
|
return { ok: false, error: 'hub-launcher-restart-unavailable' };
|
|
@@ -517,10 +617,76 @@ function requestHubRestart(request = {}) {
|
|
|
517
617
|
return { ok: true };
|
|
518
618
|
} catch (error) {
|
|
519
619
|
return { ok: false, error: `hub-restart-request-failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const HUB_RESTART_RESULT_STAGES = new Set([
|
|
624
|
+
'supervisor-starting',
|
|
625
|
+
'preflight-failed',
|
|
626
|
+
'waiting-for-old-launcher',
|
|
627
|
+
'old-launcher-exited',
|
|
628
|
+
'target-attempt',
|
|
629
|
+
'target-failed',
|
|
630
|
+
'fallback-attempt',
|
|
631
|
+
'fallback-failed',
|
|
632
|
+
'updated',
|
|
633
|
+
'restored',
|
|
634
|
+
'failed'
|
|
635
|
+
]);
|
|
636
|
+
const HUB_RESTART_RESULT_OUTCOMES = new Set(['in-progress', 'updated', 'restored', 'failed']);
|
|
637
|
+
|
|
638
|
+
function readHubRestartResult() {
|
|
639
|
+
const resultPath = String(process.env.LIVEDESK_HUB_UPDATE_RESULT_PATH || '').trim();
|
|
640
|
+
if (!resultPath || !existsSync(resultPath)) return null;
|
|
641
|
+
try {
|
|
642
|
+
const raw = JSON.parse(readFileSync(resultPath, 'utf8'));
|
|
643
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
644
|
+
const operationId = String(raw.operationId || '').trim().slice(0, 160);
|
|
645
|
+
const stage = String(raw.stage || '').trim();
|
|
646
|
+
const outcome = String(raw.outcome || '').trim();
|
|
647
|
+
if (!operationId || !HUB_RESTART_RESULT_STAGES.has(stage) || !HUB_RESTART_RESULT_OUTCOMES.has(outcome)) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
const text = (value, maximum = 200) => String(value || '').trim().slice(0, maximum);
|
|
651
|
+
const positiveInteger = value => {
|
|
652
|
+
const numeric = Number(value);
|
|
653
|
+
return Number.isInteger(numeric) && numeric > 0 ? numeric : undefined;
|
|
654
|
+
};
|
|
655
|
+
return {
|
|
656
|
+
operationId,
|
|
657
|
+
stage,
|
|
658
|
+
outcome,
|
|
659
|
+
targetVersion: text(raw.targetVersion),
|
|
660
|
+
fallbackVersion: text(raw.fallbackVersion),
|
|
661
|
+
appVersion: text(raw.appVersion),
|
|
662
|
+
attempt: positiveInteger(raw.attempt),
|
|
663
|
+
previousRuntimePid: positiveInteger(raw.previousRuntimePid),
|
|
664
|
+
supervisorPid: positiveInteger(raw.supervisorPid),
|
|
665
|
+
launcherPid: positiveInteger(raw.launcherPid),
|
|
666
|
+
runtimePid: positiveInteger(raw.runtimePid),
|
|
667
|
+
startedAt: text(raw.startedAt, 64),
|
|
668
|
+
updatedAt: text(raw.updatedAt, 64),
|
|
669
|
+
completedAt: text(raw.completedAt, 64),
|
|
670
|
+
error: text(raw.error, 4_000)
|
|
671
|
+
};
|
|
672
|
+
} catch {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function getLiveDeskUpdateStatus() {
|
|
678
|
+
const restartResult = readHubRestartResult();
|
|
679
|
+
liveDeskUpdateManager?.reconcileHubRestartResult(restartResult);
|
|
680
|
+
return {
|
|
681
|
+
...(liveDeskUpdateManager?.getStatus() || {
|
|
682
|
+
updateAvailable: false,
|
|
683
|
+
state: 'unavailable'
|
|
684
|
+
}),
|
|
685
|
+
restartResult
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (process.env.LIVEDESK_DESKTOP_HOST !== '1') {
|
|
524
690
|
liveDeskUpdateManager = createLiveDeskUpdateManager({
|
|
525
691
|
remoteHub,
|
|
526
692
|
currentManagerVersion: process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version,
|
|
@@ -1044,23 +1210,58 @@ async function synchronizeAgentEnablement() {
|
|
|
1044
1210
|
}
|
|
1045
1211
|
|
|
1046
1212
|
const hubFilesystem = createHubFilesystem();
|
|
1047
|
-
|
|
1048
|
-
filesystem: hubFilesystem,
|
|
1049
|
-
remoteHub,
|
|
1050
|
-
maxConcurrent: Number(process.env.LIVEDESK_MAX_FILE_JOBS || 2)
|
|
1051
|
-
|
|
1213
|
+
hubTransferJobs = createHubTransferJobs({
|
|
1214
|
+
filesystem: hubFilesystem,
|
|
1215
|
+
remoteHub,
|
|
1216
|
+
maxConcurrent: Number(process.env.LIVEDESK_MAX_FILE_JOBS || 2),
|
|
1217
|
+
commandResultTimeoutMs: readPositiveIntegerEnv('LIVEDESK_FILE_CHUNK_ACK_TIMEOUT_MS', 30_000)
|
|
1218
|
+
});
|
|
1052
1219
|
const hubSharedFolders = createHubSharedFolders({
|
|
1053
1220
|
filesystem: hubFilesystem,
|
|
1054
1221
|
transferJobs: hubTransferJobs,
|
|
1055
1222
|
remoteHub
|
|
1056
1223
|
});
|
|
1057
1224
|
|
|
1058
|
-
const app = express();
|
|
1059
|
-
const httpServer = createServer(app);
|
|
1060
|
-
const
|
|
1061
|
-
const
|
|
1062
|
-
const
|
|
1063
|
-
const
|
|
1225
|
+
const app = express();
|
|
1226
|
+
const httpServer = createServer(app);
|
|
1227
|
+
const httpConnections = new Set();
|
|
1228
|
+
const frameWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
|
1229
|
+
const atlasWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
|
1230
|
+
const inputWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
|
1231
|
+
const audioWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
|
1232
|
+
const browserWebSocketServers = [frameWss, atlasWss, inputWss, audioWss];
|
|
1233
|
+
|
|
1234
|
+
httpServer.on('connection', socket => {
|
|
1235
|
+
httpConnections.add(socket);
|
|
1236
|
+
socket.once('close', () => httpConnections.delete(socket));
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
for (const wss of browserWebSocketServers) {
|
|
1240
|
+
wss.on('connection', ws => {
|
|
1241
|
+
ws.liveDeskHeartbeatAlive = true;
|
|
1242
|
+
ws.on('pong', () => {
|
|
1243
|
+
ws.liveDeskHeartbeatAlive = true;
|
|
1244
|
+
});
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const browserWebSocketHeartbeatTimer = setInterval(() => {
|
|
1249
|
+
for (const wss of browserWebSocketServers) {
|
|
1250
|
+
for (const ws of wss.clients) {
|
|
1251
|
+
if (ws.liveDeskHeartbeatAlive === false) {
|
|
1252
|
+
ws.terminate();
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
ws.liveDeskHeartbeatAlive = false;
|
|
1256
|
+
try {
|
|
1257
|
+
ws.ping();
|
|
1258
|
+
} catch {
|
|
1259
|
+
ws.terminate();
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}, webSocketHeartbeatMs);
|
|
1264
|
+
browserWebSocketHeartbeatTimer.unref?.();
|
|
1064
1265
|
|
|
1065
1266
|
function normalizeHostname(value) {
|
|
1066
1267
|
return String(value || '').trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
@@ -1340,25 +1541,27 @@ function forgetWebSocketClient(ws) {
|
|
|
1340
1541
|
}
|
|
1341
1542
|
}
|
|
1342
1543
|
|
|
1343
|
-
function safeWebSocketSend(ws, payload, options = undefined) {
|
|
1344
|
-
if (!ws || ws.readyState !== 1) {
|
|
1345
|
-
return false;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
try {
|
|
1349
|
-
ws.send(payload, options, err => {
|
|
1350
|
-
if (err) {
|
|
1351
|
-
forgetWebSocketClient(ws);
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1544
|
+
function safeWebSocketSend(ws, payload, options = undefined, onComplete = undefined) {
|
|
1545
|
+
if (!ws || ws.readyState !== 1) {
|
|
1546
|
+
return false;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
try {
|
|
1550
|
+
ws.send(payload, options, err => {
|
|
1551
|
+
if (err) {
|
|
1552
|
+
forgetWebSocketClient(ws);
|
|
1553
|
+
}
|
|
1554
|
+
onComplete?.(err || null);
|
|
1555
|
+
});
|
|
1556
|
+
return true;
|
|
1557
|
+
} catch (error) {
|
|
1558
|
+
forgetWebSocketClient(ws);
|
|
1559
|
+
onComplete?.(error);
|
|
1560
|
+
return false;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1360
1563
|
|
|
1361
|
-
function unregisterFrameClient(ws) {
|
|
1564
|
+
function unregisterFrameClient(ws) {
|
|
1362
1565
|
if (!ws) {
|
|
1363
1566
|
return;
|
|
1364
1567
|
}
|
|
@@ -1373,10 +1576,16 @@ function unregisterFrameClient(ws) {
|
|
|
1373
1576
|
if (clients.size === 0) {
|
|
1374
1577
|
frameClientsByDeviceId.delete(deviceId);
|
|
1375
1578
|
}
|
|
1376
|
-
}
|
|
1377
|
-
frameWildcardClients.delete(ws);
|
|
1378
|
-
ws.
|
|
1379
|
-
|
|
1579
|
+
}
|
|
1580
|
+
frameWildcardClients.delete(ws);
|
|
1581
|
+
ws.liveDeskExpectedStreamBindingsByDeviceId?.clear?.();
|
|
1582
|
+
const lane = ws.liveDeskFrameSendLane;
|
|
1583
|
+
if (lane) {
|
|
1584
|
+
clearTimeout(lane.retryTimer);
|
|
1585
|
+
lane.retryTimer = null;
|
|
1586
|
+
lane.queue.splice(0);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1380
1589
|
|
|
1381
1590
|
function hasOtherFrameStreamOwner(ws, deviceId, streamId, streamPurpose = '') {
|
|
1382
1591
|
if (!streamId) {
|
|
@@ -1476,11 +1685,12 @@ function registerFrameClient(ws) {
|
|
|
1476
1685
|
function ensureFrameClientSendLane(ws) {
|
|
1477
1686
|
if (!ws.liveDeskFrameSendLane) {
|
|
1478
1687
|
ws.liveDeskFrameSendLane = {
|
|
1479
|
-
queue: [],
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1688
|
+
queue: [],
|
|
1689
|
+
sending: false,
|
|
1690
|
+
retryTimer: null,
|
|
1691
|
+
dropped: 0,
|
|
1692
|
+
awaitingKeyFrames: new Set()
|
|
1693
|
+
};
|
|
1484
1694
|
}
|
|
1485
1695
|
return ws.liveDeskFrameSendLane;
|
|
1486
1696
|
}
|
|
@@ -1528,39 +1738,48 @@ function dropQueuedMode5FrameForLane(lane, deviceId) {
|
|
|
1528
1738
|
return true;
|
|
1529
1739
|
}
|
|
1530
1740
|
|
|
1531
|
-
function drainFrameClientSendLane(ws) {
|
|
1532
|
-
const lane = ws.liveDeskFrameSendLane;
|
|
1533
|
-
if (!lane || lane.
|
|
1534
|
-
return;
|
|
1535
|
-
}
|
|
1536
|
-
lane.
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1741
|
+
function drainFrameClientSendLane(ws) {
|
|
1742
|
+
const lane = ws.liveDeskFrameSendLane;
|
|
1743
|
+
if (!lane || lane.sending) {
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
clearTimeout(lane.retryTimer);
|
|
1747
|
+
lane.retryTimer = null;
|
|
1748
|
+
if (!ws || ws.readyState !== ws.OPEN) {
|
|
1749
|
+
lane.queue.length = 0;
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
const item = lane.queue[0];
|
|
1753
|
+
if (!item) {
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
const backpressureBytes = item.isMode5 ? mode5FrameBackpressureBytes : frameBackpressureBytes;
|
|
1757
|
+
if (ws.bufferedAmount > backpressureBytes) {
|
|
1758
|
+
ws.liveDeskFrameBackpressured = true;
|
|
1759
|
+
lane.retryTimer = setTimeout(() => {
|
|
1760
|
+
lane.retryTimer = null;
|
|
1761
|
+
drainFrameClientSendLane(ws);
|
|
1762
|
+
}, 10);
|
|
1763
|
+
lane.retryTimer.unref?.();
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
lane.queue.shift();
|
|
1768
|
+
lane.sending = true;
|
|
1769
|
+
ws.liveDeskFrameBackpressured = false;
|
|
1770
|
+
const accepted = safeWebSocketSend(ws, item.packet, { binary: true }, error => {
|
|
1771
|
+
lane.sending = false;
|
|
1772
|
+
if (error || ws.readyState !== ws.OPEN) {
|
|
1773
|
+
lane.queue.length = 0;
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
drainFrameClientSendLane(ws);
|
|
1777
|
+
});
|
|
1778
|
+
if (!accepted) {
|
|
1779
|
+
lane.sending = false;
|
|
1780
|
+
lane.queue.length = 0;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1564
1783
|
|
|
1565
1784
|
function enqueueFramePacketForClient(ws, packet, meta) {
|
|
1566
1785
|
if (!ws || ws.readyState !== ws.OPEN) {
|
|
@@ -1610,7 +1829,7 @@ function enqueueFramePacketForClient(ws, packet, meta) {
|
|
|
1610
1829
|
return true;
|
|
1611
1830
|
}
|
|
1612
1831
|
|
|
1613
|
-
function updateFrameSubscription(ws, payload = {}) {
|
|
1832
|
+
function updateFrameSubscription(ws, payload = {}) {
|
|
1614
1833
|
const previousDeviceIds = ws.liveDeskDeviceIds instanceof Set
|
|
1615
1834
|
? new Set(ws.liveDeskDeviceIds)
|
|
1616
1835
|
: new Set();
|
|
@@ -1620,14 +1839,18 @@ function updateFrameSubscription(ws, payload = {}) {
|
|
|
1620
1839
|
registerFrameClient(ws);
|
|
1621
1840
|
ws.liveDeskAutoStart = /^(1|true|yes|on|live)$/i.test(String(payload.autoStartLive ?? payload.startLive ?? ''));
|
|
1622
1841
|
ws.liveDeskLiveOptions = normalizeLiveOptions(payload);
|
|
1623
|
-
if (!(ws.liveDeskStreamIdsByDeviceId instanceof Map)) {
|
|
1624
|
-
ws.liveDeskStreamIdsByDeviceId = new Map();
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1842
|
+
if (!(ws.liveDeskStreamIdsByDeviceId instanceof Map)) {
|
|
1843
|
+
ws.liveDeskStreamIdsByDeviceId = new Map();
|
|
1844
|
+
}
|
|
1845
|
+
if (!(ws.liveDeskExpectedStreamBindingsByDeviceId instanceof Map)) {
|
|
1846
|
+
ws.liveDeskExpectedStreamBindingsByDeviceId = new Map();
|
|
1847
|
+
}
|
|
1848
|
+
for (const subscribedDeviceId of ws.liveDeskStreamIdsByDeviceId.keys()) {
|
|
1849
|
+
if (!ws.liveDeskDeviceIds.has(subscribedDeviceId)) {
|
|
1850
|
+
ws.liveDeskStreamIdsByDeviceId.delete(subscribedDeviceId);
|
|
1851
|
+
ws.liveDeskExpectedStreamBindingsByDeviceId.delete(subscribedDeviceId);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1631
1854
|
sendJson(ws, {
|
|
1632
1855
|
type: 'RemoteFrameSubscription',
|
|
1633
1856
|
timestamp: new Date().toISOString(),
|
|
@@ -1681,15 +1904,46 @@ function startFrameSubscriptionLive(ws, reason = 'subscribe', onlyDeviceId = '',
|
|
|
1681
1904
|
reuseExisting: liveOptions.forceRestart !== true
|
|
1682
1905
|
&& (liveOptions.reuseExisting === true || reason === 'subscribe' || reason === 'watchdog'),
|
|
1683
1906
|
silentReuse: reason === 'watchdog' && liveOptions.forceRestart !== true
|
|
1684
|
-
});
|
|
1685
|
-
if (result?.ok) {
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1907
|
+
});
|
|
1908
|
+
if (result?.ok) {
|
|
1909
|
+
const expectedBinding = {
|
|
1910
|
+
deviceId,
|
|
1911
|
+
sessionId: String(result.sessionId || device.sessionId || ''),
|
|
1912
|
+
streamId: String(result.streamId || ''),
|
|
1913
|
+
streamPurpose: String(result.streamPurpose || liveOptions.streamPurpose || 'wall'),
|
|
1914
|
+
commandId: String(result.commandId || ''),
|
|
1915
|
+
captureGeneration: Number(result.captureGeneration || 0),
|
|
1916
|
+
monitorIndex: Number(result.monitorIndex || 0),
|
|
1917
|
+
readySent: false
|
|
1918
|
+
};
|
|
1919
|
+
const activeStream = device?.activeLiveStream;
|
|
1920
|
+
const reusedFrameReady = result.reused === true
|
|
1921
|
+
&& result.ready === true
|
|
1922
|
+
&& Number(result.framesReceived ?? activeStream?.framesReceived ?? 0) > 0
|
|
1923
|
+
&& String(activeStream?.streamId || expectedBinding.streamId) === expectedBinding.streamId
|
|
1924
|
+
&& String(activeStream?.commandId || expectedBinding.commandId) === expectedBinding.commandId
|
|
1925
|
+
&& Number(activeStream?.captureGeneration || expectedBinding.captureGeneration) === expectedBinding.captureGeneration;
|
|
1926
|
+
expectedBinding.readySent = reusedFrameReady;
|
|
1927
|
+
ws.liveDeskStreamIdsByDeviceId.set(deviceId, result.streamId);
|
|
1928
|
+
ws.liveDeskExpectedStreamBindingsByDeviceId.set(deviceId, expectedBinding);
|
|
1929
|
+
cancelPendingFrameStreamStop(deviceId, result.streamId, String(liveOptions.streamPurpose || 'wall'));
|
|
1930
|
+
started.push({
|
|
1931
|
+
deviceId: expectedBinding.deviceId,
|
|
1932
|
+
sessionId: expectedBinding.sessionId,
|
|
1933
|
+
streamId: expectedBinding.streamId,
|
|
1934
|
+
streamPurpose: expectedBinding.streamPurpose,
|
|
1935
|
+
commandId: expectedBinding.commandId,
|
|
1936
|
+
captureGeneration: expectedBinding.captureGeneration,
|
|
1937
|
+
monitorIndex: expectedBinding.monitorIndex,
|
|
1938
|
+
ready: reusedFrameReady,
|
|
1939
|
+
fps: result.fps,
|
|
1940
|
+
reused: result.reused === true
|
|
1941
|
+
});
|
|
1942
|
+
} else {
|
|
1943
|
+
ws.liveDeskStreamIdsByDeviceId.delete(deviceId);
|
|
1944
|
+
ws.liveDeskExpectedStreamBindingsByDeviceId.delete(deviceId);
|
|
1945
|
+
skipped.push({ deviceId, reason: result?.error || 'start-failed' });
|
|
1946
|
+
}
|
|
1693
1947
|
}
|
|
1694
1948
|
if (reason === 'watchdog' && !started.some(item => item.reused !== true)) {
|
|
1695
1949
|
return;
|
|
@@ -1749,10 +2003,13 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
|
|
|
1749
2003
|
kind: frameEvent.kind || 'live',
|
|
1750
2004
|
deviceId: frameEvent.deviceId || frame.deviceId || '',
|
|
1751
2005
|
frameSeq: Number(frame.frameSeq || 0) || 0,
|
|
1752
|
-
streamFrameSeq: Number(frame.streamFrameSeq || 0) || 0,
|
|
1753
|
-
streamId: frame.streamId || '',
|
|
1754
|
-
commandId: frame.commandId || '',
|
|
1755
|
-
|
|
2006
|
+
streamFrameSeq: Number(frame.streamFrameSeq || 0) || 0,
|
|
2007
|
+
streamId: frame.streamId || '',
|
|
2008
|
+
commandId: frame.commandId || '',
|
|
2009
|
+
sessionId: frame.sessionId || '',
|
|
2010
|
+
captureGeneration: Number(frame.captureGeneration || 0) || 0,
|
|
2011
|
+
streamPurpose: frame.streamPurpose || '',
|
|
2012
|
+
width: Number(frame.width || 0) || 0,
|
|
1756
2013
|
height: Number(frame.height || 0) || 0,
|
|
1757
2014
|
sourceWidth: Number(frame.sourceWidth || 0) || 0,
|
|
1758
2015
|
sourceHeight: Number(frame.sourceHeight || 0) || 0,
|
|
@@ -1812,9 +2069,13 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
|
|
|
1812
2069
|
sameContentStreak: Number(frame.sameContentStreak || 0) || 0,
|
|
1813
2070
|
layoutVersion: Number(frame.layoutVersion || 0) || 0,
|
|
1814
2071
|
atlasComposeMs: Number(frame.atlasComposeMs || 0) || 0,
|
|
1815
|
-
atlasPoolStarved: Number(frame.atlasPoolStarved || 0) || 0,
|
|
1816
|
-
atlasReadyTiles: Number(frame.atlasReadyTiles || 0) || 0,
|
|
1817
|
-
atlasTileCount: Number(frame.atlasTileCount || 0) || 0
|
|
2072
|
+
atlasPoolStarved: Number(frame.atlasPoolStarved || 0) || 0,
|
|
2073
|
+
atlasReadyTiles: Number(frame.atlasReadyTiles || 0) || 0,
|
|
2074
|
+
atlasTileCount: Number(frame.atlasTileCount || 0) || 0,
|
|
2075
|
+
atlasInputRevision: Number(frame.atlasInputRevision || 0) || 0,
|
|
2076
|
+
atlasSourceNewestAtEpochMs: Number(frame.atlasSourceNewestAtEpochMs || 0) || 0,
|
|
2077
|
+
atlasSourceOldestAtEpochMs: Number(frame.atlasSourceOldestAtEpochMs || 0) || 0,
|
|
2078
|
+
atlasSourceMaxAgeMs: Number(frame.atlasSourceMaxAgeMs || 0) || 0
|
|
1818
2079
|
};
|
|
1819
2080
|
const metaBuffer = Buffer.from(JSON.stringify(metadata), 'utf8');
|
|
1820
2081
|
const header = Buffer.allocUnsafe(4);
|
|
@@ -1860,10 +2121,8 @@ function buildRemoteAudioBinaryPacket(audioEvent) {
|
|
|
1860
2121
|
return Buffer.concat([header, metaBuffer, payload], 4 + metaBuffer.length + payload.length);
|
|
1861
2122
|
}
|
|
1862
2123
|
|
|
1863
|
-
function broadcastRemoteBinaryFrame(frameEvent) {
|
|
1864
|
-
|
|
1865
|
-
session.ingest(frameEvent);
|
|
1866
|
-
}
|
|
2124
|
+
function broadcastRemoteBinaryFrame(frameEvent) {
|
|
2125
|
+
atlasPool.ingest(frameEvent);
|
|
1867
2126
|
const deviceId = String(frameEvent?.deviceId || frameEvent?.frame?.deviceId || '').trim();
|
|
1868
2127
|
if (!deviceId || frameClients.size === 0) {
|
|
1869
2128
|
return;
|
|
@@ -1879,18 +2138,40 @@ function broadcastRemoteBinaryFrame(frameEvent) {
|
|
|
1879
2138
|
const isH264 = String(frame.codec || frame.mimeType || frame.format || '').toLowerCase().includes('h264')
|
|
1880
2139
|
|| String(frame.frameMode || frame.mode || '').toLowerCase() === 'mode3-h264-hw';
|
|
1881
2140
|
const isMode5 = String(frame.frameMode || frame.mode || '').toLowerCase() === 'mode5-lzo-delta';
|
|
1882
|
-
const isKeyFrame = frame.isKeyFrame === true || String(frame.chunkType || '').toLowerCase() === 'key';
|
|
1883
|
-
const frameSeq = Number(frame.frameSeq || 0) || 0;
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
2141
|
+
const isKeyFrame = frame.isKeyFrame === true || String(frame.chunkType || '').toLowerCase() === 'key';
|
|
2142
|
+
const frameSeq = Number(frame.frameSeq || 0) || 0;
|
|
2143
|
+
let sharedPacket = null;
|
|
2144
|
+
for (const client of targetClients) {
|
|
2145
|
+
if (client.readyState !== client.OPEN) {
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
const expectedBinding = client.liveDeskExpectedStreamBindingsByDeviceId instanceof Map
|
|
2149
|
+
? client.liveDeskExpectedStreamBindingsByDeviceId.get(deviceId)
|
|
2150
|
+
: null;
|
|
2151
|
+
if (!expectedBinding
|
|
2152
|
+
|| String(frame.streamId || '') !== expectedBinding.streamId
|
|
2153
|
+
|| String(frame.sessionId || '') !== expectedBinding.sessionId
|
|
2154
|
+
|| String(frame.commandId || '') !== expectedBinding.commandId
|
|
2155
|
+
|| Number(frame.captureGeneration || 0) !== expectedBinding.captureGeneration
|
|
2156
|
+
|| String(frame.streamPurpose || '') !== expectedBinding.streamPurpose
|
|
2157
|
+
|| Number(frame.monitorIndex || 0) !== expectedBinding.monitorIndex) {
|
|
2158
|
+
continue;
|
|
2159
|
+
}
|
|
2160
|
+
if (!expectedBinding.readySent) {
|
|
2161
|
+
expectedBinding.readySent = sendJson(client, {
|
|
2162
|
+
type: 'RemoteFrameStreamReady',
|
|
2163
|
+
deviceId,
|
|
2164
|
+
sessionId: expectedBinding.sessionId,
|
|
2165
|
+
streamId: expectedBinding.streamId,
|
|
2166
|
+
streamPurpose: expectedBinding.streamPurpose,
|
|
2167
|
+
commandId: expectedBinding.commandId,
|
|
2168
|
+
captureGeneration: expectedBinding.captureGeneration,
|
|
2169
|
+
monitorIndex: expectedBinding.monitorIndex
|
|
2170
|
+
});
|
|
2171
|
+
if (!expectedBinding.readySent) {
|
|
2172
|
+
continue;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
1894
2175
|
const lane = ensureFrameClientSendLane(client);
|
|
1895
2176
|
if (client.liveDeskFrameBackpressured && isH264 && !isKeyFrame) {
|
|
1896
2177
|
lane.dropped += 1;
|
|
@@ -1898,13 +2179,11 @@ function broadcastRemoteBinaryFrame(frameEvent) {
|
|
|
1898
2179
|
client.liveDeskFrameBackpressureDrops = Number(client.liveDeskFrameBackpressureDrops || 0) + 1;
|
|
1899
2180
|
continue;
|
|
1900
2181
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
}
|
|
1907
|
-
enqueueFramePacketForClient(client, packet, { deviceId, frameSeq, isH264, isMode5, isKeyFrame });
|
|
2182
|
+
sharedPacket ||= buildRemoteFrameBinaryPacket(frameEvent);
|
|
2183
|
+
if (!sharedPacket) {
|
|
2184
|
+
continue;
|
|
2185
|
+
}
|
|
2186
|
+
enqueueFramePacketForClient(client, sharedPacket, { deviceId, frameSeq, isH264, isMode5, isKeyFrame });
|
|
1908
2187
|
}
|
|
1909
2188
|
}
|
|
1910
2189
|
|
|
@@ -1977,9 +2256,13 @@ function sendMode4AtlasFrame(ws, output) {
|
|
|
1977
2256
|
encoderGeneration,
|
|
1978
2257
|
layoutVersion: Number(metadata.layoutVersion || 0),
|
|
1979
2258
|
atlasComposeMs: Number(metadata.composeMs || 0),
|
|
1980
|
-
atlasPoolStarved: Number(metadata.poolStarved || 0),
|
|
1981
|
-
atlasReadyTiles: Number(metadata.readyTileCount || 0),
|
|
1982
|
-
atlasTileCount: Number(metadata.tileCount || 0)
|
|
2259
|
+
atlasPoolStarved: Number(metadata.poolStarved || 0),
|
|
2260
|
+
atlasReadyTiles: Number(metadata.readyTileCount || 0),
|
|
2261
|
+
atlasTileCount: Number(metadata.tileCount || 0),
|
|
2262
|
+
atlasInputRevision: Number(metadata.inputRevision || 0),
|
|
2263
|
+
atlasSourceNewestAtEpochMs: Number(metadata.sourceNewestAtEpochMs || 0),
|
|
2264
|
+
atlasSourceOldestAtEpochMs: Number(metadata.sourceOldestAtEpochMs || 0),
|
|
2265
|
+
atlasSourceMaxAgeMs: Number(metadata.sourceMaxAgeMs || 0)
|
|
1983
2266
|
}
|
|
1984
2267
|
};
|
|
1985
2268
|
const lane = ensureFrameClientSendLane(ws);
|
|
@@ -1988,7 +2271,15 @@ function sendMode4AtlasFrame(ws, output) {
|
|
|
1988
2271
|
lane.awaitingKeyFrames.add('__livedesk_mode4_atlas__');
|
|
1989
2272
|
return;
|
|
1990
2273
|
}
|
|
1991
|
-
const
|
|
2274
|
+
const packetCacheKey = `${streamId}:${frameSeq}`;
|
|
2275
|
+
let packet = output.liveDeskBinaryPacketKey === packetCacheKey
|
|
2276
|
+
? output.liveDeskBinaryPacket
|
|
2277
|
+
: null;
|
|
2278
|
+
if (!packet) {
|
|
2279
|
+
packet = buildRemoteFrameBinaryPacket(frameEvent);
|
|
2280
|
+
output.liveDeskBinaryPacketKey = packetCacheKey;
|
|
2281
|
+
output.liveDeskBinaryPacket = packet;
|
|
2282
|
+
}
|
|
1992
2283
|
if (packet) {
|
|
1993
2284
|
enqueueFramePacketForClient(ws, packet, {
|
|
1994
2285
|
deviceId: '__livedesk_mode4_atlas__',
|
|
@@ -1999,67 +2290,114 @@ function sendMode4AtlasFrame(ws, output) {
|
|
|
1999
2290
|
}
|
|
2000
2291
|
}
|
|
2001
2292
|
|
|
2002
|
-
function configureMode4Atlas(ws, payload = {}) {
|
|
2003
|
-
const deviceIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId).slice(0, 100);
|
|
2004
|
-
const captureDeviceIds = normalizeDeviceIds(payload.captureDeviceIds ?? payload.inputDeviceIds ?? deviceIds)
|
|
2005
|
-
.filter(deviceId => deviceIds.includes(deviceId));
|
|
2006
|
-
const monitorSelections = normalizeMonitorSelections(payload.monitorSelections);
|
|
2007
|
-
const
|
|
2008
|
-
const
|
|
2009
|
-
const
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
:
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2293
|
+
function configureMode4Atlas(ws, payload = {}) {
|
|
2294
|
+
const deviceIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId).slice(0, 100);
|
|
2295
|
+
const captureDeviceIds = normalizeDeviceIds(payload.captureDeviceIds ?? payload.inputDeviceIds ?? deviceIds)
|
|
2296
|
+
.filter(deviceId => deviceIds.includes(deviceId));
|
|
2297
|
+
const monitorSelections = normalizeMonitorSelections(payload.monitorSelections);
|
|
2298
|
+
const width = clampNumber(payload.width, 640, 3840, 1920);
|
|
2299
|
+
const height = clampNumber(payload.height, 360, 2160, 1080);
|
|
2300
|
+
const tileSize = resolveMode4AtlasTileSize({
|
|
2301
|
+
requestedWidth: payload.tileWidth,
|
|
2302
|
+
requestedHeight: payload.tileHeight,
|
|
2303
|
+
deviceCount: deviceIds.length,
|
|
2304
|
+
atlasWidth: width,
|
|
2305
|
+
atlasHeight: height
|
|
2306
|
+
});
|
|
2307
|
+
const tileWidth = tileSize.width;
|
|
2308
|
+
const tileHeight = tileSize.height;
|
|
2309
|
+
const inputFps = clampNumber(payload.inputFps, 1, 30, 8);
|
|
2310
|
+
const previousInputDeviceIds = ws.liveDeskAtlasInputDeviceIds instanceof Set
|
|
2311
|
+
? new Set(ws.liveDeskAtlasInputDeviceIds)
|
|
2312
|
+
: new Set();
|
|
2313
|
+
const previousInputOptions = ws.liveDeskAtlasInputOptions || {};
|
|
2314
|
+
const atlasConfig = {
|
|
2315
|
+
deviceIds,
|
|
2316
|
+
// Composition ownership is independent from capture ownership. Keeping
|
|
2317
|
+
// every layout device ingestible lets a paused/reused input retain its
|
|
2318
|
+
// latest tile while only the changed device restarts capture.
|
|
2319
|
+
inputDeviceIds: deviceIds,
|
|
2320
|
+
width,
|
|
2321
|
+
height,
|
|
2021
2322
|
tileWidth,
|
|
2022
2323
|
tileHeight,
|
|
2023
|
-
fps: clampNumber(payload.fps, 1, 30, 20)
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2324
|
+
fps: clampNumber(payload.fps, 1, 30, 20),
|
|
2325
|
+
monitorSelections
|
|
2326
|
+
};
|
|
2327
|
+
const previousHandle = ws.liveDeskAtlasHandle;
|
|
2328
|
+
const nextKey = buildMode4AtlasSessionKey(atlasConfig);
|
|
2329
|
+
if (!previousHandle || previousHandle.key !== nextKey) {
|
|
2330
|
+
const nextHandle = atlasPool.acquire(atlasConfig, {
|
|
2331
|
+
onFrame: output => sendMode4AtlasFrame(ws, output),
|
|
2332
|
+
onLayout: layout => sendJson(ws, { ...layout, type: 'Mode4AtlasLayout' }),
|
|
2333
|
+
onStatus: status => sendJson(ws, { ...status, type: 'Mode4AtlasStatus' })
|
|
2334
|
+
});
|
|
2335
|
+
ws.liveDeskAtlasHandle = nextHandle;
|
|
2336
|
+
ws.liveDeskAtlasSession = nextHandle.session;
|
|
2337
|
+
ws.liveDeskAtlasStreamId = nextHandle.streamId;
|
|
2338
|
+
atlasClients.set(ws, nextHandle);
|
|
2339
|
+
previousHandle?.release?.();
|
|
2340
|
+
}
|
|
2341
|
+
ws.liveDeskAtlasDeviceIds = new Set(deviceIds);
|
|
2342
|
+
ws.liveDeskAtlasInputDeviceIds = new Set(captureDeviceIds);
|
|
2343
|
+
ws.liveDeskAtlasInputOptions = { tileWidth, tileHeight, inputFps, monitorSelections };
|
|
2344
|
+
const inputTransitions = planMode4AtlasInputTransitions({
|
|
2345
|
+
inputDeviceIds: [...previousInputDeviceIds],
|
|
2346
|
+
tileWidth: previousInputOptions.tileWidth,
|
|
2347
|
+
tileHeight: previousInputOptions.tileHeight,
|
|
2348
|
+
inputFps: previousInputOptions.inputFps,
|
|
2349
|
+
monitorSelections: previousInputOptions.monitorSelections
|
|
2350
|
+
}, {
|
|
2351
|
+
inputDeviceIds: captureDeviceIds,
|
|
2352
|
+
tileWidth,
|
|
2353
|
+
tileHeight,
|
|
2354
|
+
inputFps,
|
|
2355
|
+
monitorSelections
|
|
2356
|
+
});
|
|
2357
|
+
for (const deviceId of inputTransitions.stop) {
|
|
2358
|
+
stopMode4AtlasInput(ws, deviceId, 'atlas-input-paused');
|
|
2359
|
+
}
|
|
2360
|
+
for (const deviceId of inputTransitions.start) {
|
|
2361
|
+
startMode4AtlasInput(ws, deviceId, 'atlas-input-started');
|
|
2362
|
+
}
|
|
2363
|
+
for (const deviceId of inputTransitions.restart) {
|
|
2364
|
+
startMode4AtlasInput(ws, deviceId, 'atlas-input-changed', {
|
|
2365
|
+
forceRestart: true
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
sendJson(ws, {
|
|
2369
|
+
type: 'Mode4AtlasSubscription',
|
|
2370
|
+
streamId: ws.liveDeskAtlasStreamId,
|
|
2371
|
+
deviceIds,
|
|
2037
2372
|
captureDeviceIds,
|
|
2038
2373
|
inputMode: 'mode2-lzo',
|
|
2039
|
-
outputMode: 'mode4-h264-atlas',
|
|
2040
|
-
tileWidth,
|
|
2041
|
-
tileHeight,
|
|
2042
|
-
|
|
2374
|
+
outputMode: 'mode4-h264-atlas',
|
|
2375
|
+
tileWidth,
|
|
2376
|
+
tileHeight,
|
|
2377
|
+
inputFps,
|
|
2378
|
+
timestamp: new Date().toISOString()
|
|
2043
2379
|
});
|
|
2044
2380
|
}
|
|
2045
2381
|
|
|
2046
|
-
function startMode4AtlasInput(ws, deviceId, reason = 'atlas-configure') {
|
|
2047
|
-
const options = ws?.liveDeskAtlasInputOptions;
|
|
2048
|
-
if (!options || !ws.liveDeskAtlasInputDeviceIds?.has(deviceId)) return { ok: false, error: 'atlas-device-not-configured' };
|
|
2049
|
-
return remoteHub.startLiveStream(deviceId, {
|
|
2050
|
-
fps:
|
|
2382
|
+
function startMode4AtlasInput(ws, deviceId, reason = 'atlas-configure', { forceRestart = false } = {}) {
|
|
2383
|
+
const options = ws?.liveDeskAtlasInputOptions;
|
|
2384
|
+
if (!options || !ws.liveDeskAtlasInputDeviceIds?.has(deviceId)) return { ok: false, error: 'atlas-device-not-configured' };
|
|
2385
|
+
return remoteHub.startLiveStream(deviceId, {
|
|
2386
|
+
fps: options.inputFps,
|
|
2051
2387
|
maxWidth: options.tileWidth,
|
|
2052
2388
|
maxHeight: options.tileHeight,
|
|
2053
2389
|
quality: 60,
|
|
2054
2390
|
mode: 'mode2-lzo',
|
|
2055
2391
|
frameMode: 'mode2-lzo',
|
|
2056
|
-
streamPurpose: 'atlas',
|
|
2057
|
-
monitorIndex: Number(options.monitorSelections?.[deviceId] || 0),
|
|
2058
|
-
reuseExisting:
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2392
|
+
streamPurpose: 'atlas',
|
|
2393
|
+
monitorIndex: Number(options.monitorSelections?.[deviceId] || 0),
|
|
2394
|
+
reuseExisting: !forceRestart,
|
|
2395
|
+
forceRestart,
|
|
2396
|
+
restartToken: forceRestart ? `atlas-monitor-${deviceId}-${Date.now()}` : '',
|
|
2397
|
+
silentReuse: !forceRestart,
|
|
2398
|
+
restartReason: reason
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2063
2401
|
|
|
2064
2402
|
function hasOtherAtlasInputOwner(ws, deviceId) {
|
|
2065
2403
|
for (const candidate of atlasClients.keys()) {
|
|
@@ -2112,10 +2450,35 @@ function isCapabilityEnabled(device, key) {
|
|
|
2112
2450
|
return /^(1|true|yes|on)$/i.test(String(value || '').trim());
|
|
2113
2451
|
}
|
|
2114
2452
|
|
|
2115
|
-
app.get('/api/health', (_req, res) => {
|
|
2116
|
-
noStore(res);
|
|
2117
|
-
|
|
2118
|
-
|
|
2453
|
+
app.get('/api/health', (_req, res) => {
|
|
2454
|
+
noStore(res);
|
|
2455
|
+
const memory = process.memoryUsage();
|
|
2456
|
+
const webSockets = {
|
|
2457
|
+
frame: frameWss.clients.size,
|
|
2458
|
+
atlas: atlasWss.clients.size,
|
|
2459
|
+
input: inputWss.clients.size,
|
|
2460
|
+
audio: audioWss.clients.size
|
|
2461
|
+
};
|
|
2462
|
+
res.json({
|
|
2463
|
+
ok: true,
|
|
2464
|
+
product: 'LiveDesk',
|
|
2465
|
+
timestamp: new Date().toISOString(),
|
|
2466
|
+
memory: {
|
|
2467
|
+
rssBytes: memory.rss,
|
|
2468
|
+
heapUsedBytes: memory.heapUsed,
|
|
2469
|
+
heapTotalBytes: memory.heapTotal,
|
|
2470
|
+
externalBytes: memory.external,
|
|
2471
|
+
arrayBuffersBytes: memory.arrayBuffers
|
|
2472
|
+
},
|
|
2473
|
+
webSockets: {
|
|
2474
|
+
...webSockets,
|
|
2475
|
+
total: Object.values(webSockets).reduce((sum, count) => sum + count, 0),
|
|
2476
|
+
bufferedBytes: [...browserWebSocketServers]
|
|
2477
|
+
.flatMap(wss => [...wss.clients])
|
|
2478
|
+
.reduce((sum, ws) => sum + Math.max(0, Number(ws.bufferedAmount || 0)), 0)
|
|
2479
|
+
}
|
|
2480
|
+
});
|
|
2481
|
+
});
|
|
2119
2482
|
|
|
2120
2483
|
app.get('/api/settings', async (_req, res) => {
|
|
2121
2484
|
noStore(res);
|
|
@@ -2444,7 +2807,7 @@ app.post('/api/settings/agent/run', async (req, res) => {
|
|
|
2444
2807
|
const deviceIds = normalizeDeviceIds(req.body?.deviceIds).slice(0, 500);
|
|
2445
2808
|
const connectedDeviceIds = new Set(connectedAgentDeviceIds());
|
|
2446
2809
|
if (deviceIds.length === 0 || deviceIds.some(deviceId => !connectedDeviceIds.has(deviceId))) {
|
|
2447
|
-
throw new AgentProviderError('agent-target-not-connected', 'Every selected
|
|
2810
|
+
throw new AgentProviderError('agent-target-not-connected', 'Every selected Client must have an active Agent control channel.', { status: 409 });
|
|
2448
2811
|
}
|
|
2449
2812
|
const permissionMode = String(req.body?.permissionMode || 'ask').trim().toLowerCase();
|
|
2450
2813
|
if (!AGENT_PERMISSION_MODES.includes(permissionMode)) {
|
|
@@ -2580,8 +2943,8 @@ app.get('/api/remote/status', (_req, res) => {
|
|
|
2580
2943
|
deviceId: runtimeDeviceId,
|
|
2581
2944
|
deviceName: runtimeDeviceName,
|
|
2582
2945
|
roleSource: runtimeRoleSource,
|
|
2583
|
-
agentPackage: '@livedesk/client',
|
|
2584
|
-
update:
|
|
2946
|
+
agentPackage: '@livedesk/client',
|
|
2947
|
+
update: getLiveDeskUpdateStatus()
|
|
2585
2948
|
});
|
|
2586
2949
|
});
|
|
2587
2950
|
|
|
@@ -2595,19 +2958,27 @@ app.get('/api/runtime/events', (_req, res) => {
|
|
|
2595
2958
|
res.json({ ok: true, events: runtimeManager.getEvents() });
|
|
2596
2959
|
});
|
|
2597
2960
|
|
|
2598
|
-
app.post('/api/runtime/restart', (_req, res) => {
|
|
2599
|
-
noStore(res);
|
|
2600
|
-
runtimeManager.emit('runtime.restart.requested', { role: runtimeRole });
|
|
2601
|
-
res.json({ ok: true, restarting: true, role: runtimeRole });
|
|
2602
|
-
setTimeout(() =>
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2961
|
+
app.post('/api/runtime/restart', (_req, res) => {
|
|
2962
|
+
noStore(res);
|
|
2963
|
+
runtimeManager.emit('runtime.restart.requested', { role: runtimeRole });
|
|
2964
|
+
res.json({ ok: true, restarting: true, role: runtimeRole });
|
|
2965
|
+
setTimeout(() => {
|
|
2966
|
+
void shutdownHub('API_RESTART')
|
|
2967
|
+
.then(() => process.exit(0))
|
|
2968
|
+
.catch(() => process.exit(1));
|
|
2969
|
+
}, 150);
|
|
2970
|
+
});
|
|
2971
|
+
|
|
2972
|
+
app.post('/api/runtime/shutdown', (_req, res) => {
|
|
2973
|
+
noStore(res);
|
|
2974
|
+
runtimeManager.emit('runtime.shutdown.requested', { role: runtimeRole });
|
|
2975
|
+
res.json({ ok: true, shuttingDown: true, role: runtimeRole });
|
|
2976
|
+
setTimeout(() => {
|
|
2977
|
+
void shutdownHub('API_SHUTDOWN')
|
|
2978
|
+
.then(() => process.exit(0))
|
|
2979
|
+
.catch(() => process.exit(1));
|
|
2980
|
+
}, 150);
|
|
2981
|
+
});
|
|
2611
2982
|
|
|
2612
2983
|
app.post('/api/runtime/switch-role', (req, res) => {
|
|
2613
2984
|
noStore(res);
|
|
@@ -2642,13 +3013,19 @@ app.post('/api/auth/session', async (req, res) => {
|
|
|
2642
3013
|
runtimeRefreshToken = refreshToken;
|
|
2643
3014
|
runtimeAccessTokenExpiresAt = normalizeSessionExpiryMs(expiresAt);
|
|
2644
3015
|
runtimeManager.setAuthenticated(true, user.id);
|
|
3016
|
+
const persisted = persistRuntimeSession({
|
|
3017
|
+
access_token: accessToken,
|
|
3018
|
+
refresh_token: refreshToken,
|
|
3019
|
+
expires_at: expiresAt,
|
|
3020
|
+
user
|
|
3021
|
+
});
|
|
2645
3022
|
const hostTarget = runtimeRole === 'hub'
|
|
2646
|
-
? await
|
|
3023
|
+
? await publishHubHostTargetWithPendingRoleTakeover('session-received')
|
|
2647
3024
|
: { ok: true, active: false };
|
|
2648
3025
|
if (runtimeRole === 'hub') {
|
|
2649
3026
|
startHubHostTargetLeaseRenewal();
|
|
2650
3027
|
}
|
|
2651
|
-
res.json({ ok: true, authenticated: true, userId: user.id, role: runtimeRole, hostTarget });
|
|
3028
|
+
res.json({ ok: true, authenticated: true, persisted, userId: user.id, role: runtimeRole, hostTarget });
|
|
2652
3029
|
} catch (error) {
|
|
2653
3030
|
const message = error instanceof Error ? error.message : String(error);
|
|
2654
3031
|
const status = authVerificationHttpStatus(message);
|
|
@@ -2877,6 +3254,21 @@ async function publishHubHostTarget({ takeover = false, reason = 'renewal' } = {
|
|
|
2877
3254
|
}
|
|
2878
3255
|
}
|
|
2879
3256
|
|
|
3257
|
+
async function publishHubHostTargetWithPendingRoleTakeover(reason = 'renewal') {
|
|
3258
|
+
const takeover = roleTransitionTakeoverPending;
|
|
3259
|
+
const result = await publishHubHostTarget({
|
|
3260
|
+
takeover,
|
|
3261
|
+
reason: takeover ? `${reason}-role-takeover` : reason
|
|
3262
|
+
});
|
|
3263
|
+
if (takeover && result.ok) {
|
|
3264
|
+
// One explicit Client -> Hub selection authorizes one ownership handoff.
|
|
3265
|
+
// Normal renewals must never keep takeover authority after publication.
|
|
3266
|
+
roleTransitionTakeoverPending = false;
|
|
3267
|
+
delete process.env.LIVEDESK_ROLE_TAKEOVER;
|
|
3268
|
+
}
|
|
3269
|
+
return result;
|
|
3270
|
+
}
|
|
3271
|
+
|
|
2880
3272
|
async function clearHubHostTarget(reason = 'shutdown') {
|
|
2881
3273
|
if (hubHostTargetRenewTimer) {
|
|
2882
3274
|
clearInterval(hubHostTargetRenewTimer);
|
|
@@ -2924,7 +3316,7 @@ function startHubHostTargetLeaseRenewal() {
|
|
|
2924
3316
|
return;
|
|
2925
3317
|
}
|
|
2926
3318
|
hubHostTargetRenewTimer = setInterval(() => {
|
|
2927
|
-
void
|
|
3319
|
+
void publishHubHostTargetWithPendingRoleTakeover('renewal');
|
|
2928
3320
|
}, HUB_HOST_TARGET_RENEW_MS);
|
|
2929
3321
|
hubHostTargetRenewTimer.unref?.();
|
|
2930
3322
|
}
|
|
@@ -2952,7 +3344,7 @@ app.get('/api/hub/status', (_req, res) => {
|
|
|
2952
3344
|
roleSource: runtimeRoleSource,
|
|
2953
3345
|
runtimeStarted: true,
|
|
2954
3346
|
hostTargetLease: getHubHostTargetLeaseStatus(),
|
|
2955
|
-
update:
|
|
3347
|
+
update: getLiveDeskUpdateStatus()
|
|
2956
3348
|
});
|
|
2957
3349
|
});
|
|
2958
3350
|
|
|
@@ -3063,23 +3455,30 @@ app.post('/api/runtime/role', async (req, res) => {
|
|
|
3063
3455
|
}
|
|
3064
3456
|
});
|
|
3065
3457
|
|
|
3066
|
-
app.get('/api/update/status', (_req, res) => {
|
|
3067
|
-
noStore(res);
|
|
3068
|
-
if (process.env.LIVEDESK_DESKTOP_HOST === '1') {
|
|
3069
|
-
res.json({
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3458
|
+
app.get('/api/update/status', (_req, res) => {
|
|
3459
|
+
noStore(res);
|
|
3460
|
+
if (process.env.LIVEDESK_DESKTOP_HOST === '1') {
|
|
3461
|
+
res.json({
|
|
3462
|
+
updateAvailable: false,
|
|
3463
|
+
state: 'electron-managed',
|
|
3464
|
+
canApply: false,
|
|
3465
|
+
disabled: true,
|
|
3466
|
+
restartResult: readHubRestartResult()
|
|
3467
|
+
});
|
|
3468
|
+
return;
|
|
3469
|
+
}
|
|
3470
|
+
res.json(getLiveDeskUpdateStatus());
|
|
3471
|
+
});
|
|
3074
3472
|
|
|
3075
3473
|
app.post('/api/update/apply', async (_req, res) => {
|
|
3076
3474
|
noStore(res);
|
|
3077
3475
|
if (process.env.LIVEDESK_DESKTOP_HOST === '1') {
|
|
3078
3476
|
res.status(409).json({ ok: false, error: 'electron-updater-managed', disabled: true });
|
|
3079
3477
|
return;
|
|
3080
|
-
}
|
|
3081
|
-
try {
|
|
3082
|
-
|
|
3478
|
+
}
|
|
3479
|
+
try {
|
|
3480
|
+
liveDeskUpdateManager?.reconcileHubRestartResult(readHubRestartResult());
|
|
3481
|
+
const result = await liveDeskUpdateManager?.startUpdate();
|
|
3083
3482
|
res.status(result?.ok === false ? 409 : 200).json(result || { ok: false, error: 'update-manager-unavailable' });
|
|
3084
3483
|
} catch (error) {
|
|
3085
3484
|
res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
@@ -3468,8 +3867,8 @@ app.post('/api/remote/devices/:deviceId/quick-actions', requireHubFeatureAccess,
|
|
|
3468
3867
|
|
|
3469
3868
|
const label = action === 'shutdown' ? 'Shut down' : `${action.slice(0, 1).toUpperCase()}${action.slice(1)}`;
|
|
3470
3869
|
const result = remoteHub.requestAgentTask(req.params.deviceId, {
|
|
3471
|
-
instruction: `${label} this
|
|
3472
|
-
title: `${label}
|
|
3870
|
+
instruction: `${label} this Client. This action was explicitly requested and confirmed from the LiveDesk Hub action panel.`,
|
|
3871
|
+
title: `${label} Client`,
|
|
3473
3872
|
operation: 'system.power',
|
|
3474
3873
|
approvalLevel: 'task-only',
|
|
3475
3874
|
toolArguments: { action, delaySec: 0 },
|
|
@@ -3719,30 +4118,18 @@ frameWss.on('connection', (ws, req) => {
|
|
|
3719
4118
|
});
|
|
3720
4119
|
});
|
|
3721
4120
|
|
|
3722
|
-
atlasWss.on('connection', ws => {
|
|
3723
|
-
ws.liveDeskFrameClientId = `atlas-${++frameClientSeq}`;
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
onFrame: output => sendMode4AtlasFrame(ws, output),
|
|
3728
|
-
onLayout: layout => sendJson(ws, { ...layout, type: 'Mode4AtlasLayout' }),
|
|
3729
|
-
onStatus: status => {
|
|
3730
|
-
if (status?.type === 'log') {
|
|
3731
|
-
const logger = status.level === 'warn' ? console.warn : console.log;
|
|
3732
|
-
logger(`[LiveDesk Hub] ${status.message}`);
|
|
3733
|
-
}
|
|
3734
|
-
sendJson(ws, { ...status, type: 'Mode4AtlasStatus' });
|
|
3735
|
-
}
|
|
3736
|
-
});
|
|
3737
|
-
ws.liveDeskAtlasSession = session;
|
|
3738
|
-
atlasClients.set(ws, session);
|
|
3739
|
-
const cleanup = () => {
|
|
3740
|
-
atlasClients.delete(ws);
|
|
4121
|
+
atlasWss.on('connection', ws => {
|
|
4122
|
+
ws.liveDeskFrameClientId = `atlas-${++frameClientSeq}`;
|
|
4123
|
+
try { ws._socket?.setNoDelay?.(true); } catch {}
|
|
4124
|
+
const cleanup = () => {
|
|
4125
|
+
atlasClients.delete(ws);
|
|
3741
4126
|
for (const deviceId of ws.liveDeskAtlasInputDeviceIds || []) {
|
|
3742
4127
|
stopMode4AtlasInput(ws, deviceId, 'atlas-client-closed');
|
|
3743
|
-
}
|
|
3744
|
-
ws.liveDeskAtlasInputDeviceIds = new Set();
|
|
3745
|
-
|
|
4128
|
+
}
|
|
4129
|
+
ws.liveDeskAtlasInputDeviceIds = new Set();
|
|
4130
|
+
ws.liveDeskAtlasHandle?.release?.();
|
|
4131
|
+
ws.liveDeskAtlasHandle = null;
|
|
4132
|
+
ws.liveDeskAtlasSession = null;
|
|
3746
4133
|
const lane = ws.liveDeskFrameSendLane;
|
|
3747
4134
|
if (lane) {
|
|
3748
4135
|
lane.queue.length = 0;
|
|
@@ -3813,11 +4200,12 @@ inputWss.on('connection', ws => {
|
|
|
3813
4200
|
} catch {
|
|
3814
4201
|
sendJson(ws, { type: 'RemoteInputError', error: 'invalid-json' });
|
|
3815
4202
|
return;
|
|
3816
|
-
}
|
|
3817
|
-
const deviceId = String(payload?.deviceId || '').trim();
|
|
3818
|
-
const
|
|
3819
|
-
|
|
3820
|
-
|
|
4203
|
+
}
|
|
4204
|
+
const deviceId = String(payload?.deviceId || '').trim();
|
|
4205
|
+
const inputEventId = String(payload?.requestId || '').trim() || crypto.randomUUID();
|
|
4206
|
+
const input = payload?.input && typeof payload.input === 'object'
|
|
4207
|
+
? { ...payload.input, inputEventId, hubConnectionId: ws.liveDeskInputClientId, hubReceivedAtEpochMs: Date.now() }
|
|
4208
|
+
: { ...payload, inputEventId, hubConnectionId: ws.liveDeskInputClientId, hubReceivedAtEpochMs: Date.now() };
|
|
3821
4209
|
const result = remoteHub.sendInputControl(deviceId, input);
|
|
3822
4210
|
const inputType = String(input?.type || '').toLowerCase();
|
|
3823
4211
|
const fireAndForget = payload?.fireAndForget === true
|
|
@@ -3863,17 +4251,130 @@ const roleWatchTimer = runtimeRole === 'hub'
|
|
|
3863
4251
|
: null;
|
|
3864
4252
|
roleWatchTimer?.unref?.();
|
|
3865
4253
|
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
4254
|
+
function waitForShutdownDelay(milliseconds) {
|
|
4255
|
+
return new Promise(resolveWait => {
|
|
4256
|
+
const timer = setTimeout(resolveWait, milliseconds);
|
|
4257
|
+
timer.unref?.();
|
|
4258
|
+
});
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4261
|
+
async function boundedShutdownStep(label, action, timeoutMs) {
|
|
4262
|
+
let timer;
|
|
4263
|
+
try {
|
|
4264
|
+
await Promise.race([
|
|
4265
|
+
Promise.resolve().then(action),
|
|
4266
|
+
new Promise((_, reject) => {
|
|
4267
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
4268
|
+
timer.unref?.();
|
|
4269
|
+
})
|
|
4270
|
+
]);
|
|
4271
|
+
console.log(`[LiveDesk Hub] Shutdown stage completed: ${label}.`);
|
|
4272
|
+
} catch (error) {
|
|
4273
|
+
console.warn(`[LiveDesk Hub] Shutdown stage warning: ${error instanceof Error ? error.message : String(error)}`);
|
|
4274
|
+
} finally {
|
|
4275
|
+
if (timer) clearTimeout(timer);
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
function runSynchronousShutdownStep(label, action) {
|
|
4280
|
+
try {
|
|
4281
|
+
action();
|
|
4282
|
+
} catch (error) {
|
|
4283
|
+
console.warn(`[LiveDesk Hub] Shutdown stage warning: ${label} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
function closeBrowserWebSockets() {
|
|
4288
|
+
let closeRequested = 0;
|
|
4289
|
+
for (const wss of browserWebSocketServers) {
|
|
4290
|
+
for (const ws of wss.clients) {
|
|
4291
|
+
closeRequested += 1;
|
|
4292
|
+
try {
|
|
4293
|
+
ws.close(1012, 'LiveDesk Hub restarting');
|
|
4294
|
+
} catch {
|
|
4295
|
+
try { ws.terminate(); } catch { /* socket already closed */ }
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
console.log(`[LiveDesk Hub] Shutdown stage: requested ${closeRequested} browser WebSocket connection(s) to close.`);
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4302
|
+
function terminateOpenConnections() {
|
|
4303
|
+
let terminatedWebSockets = 0;
|
|
4304
|
+
for (const wss of browserWebSocketServers) {
|
|
4305
|
+
for (const ws of wss.clients) {
|
|
4306
|
+
terminatedWebSockets += 1;
|
|
4307
|
+
try { ws.terminate(); } catch { /* socket already closed */ }
|
|
3870
4308
|
}
|
|
3871
|
-
|
|
3872
|
-
|
|
4309
|
+
try { wss.close(); } catch { /* no active WebSocket server state */ }
|
|
4310
|
+
}
|
|
4311
|
+
let destroyedHttpConnections = 0;
|
|
4312
|
+
for (const socket of httpConnections) {
|
|
4313
|
+
if (socket.destroyed) continue;
|
|
4314
|
+
destroyedHttpConnections += 1;
|
|
4315
|
+
try { socket.destroy(); } catch { /* socket already closed */ }
|
|
4316
|
+
}
|
|
4317
|
+
try { httpServer.closeAllConnections?.(); } catch { /* older Node runtime */ }
|
|
4318
|
+
console.log(`[LiveDesk Hub] Shutdown stage: force-closed ${terminatedWebSockets} WebSocket and ${destroyedHttpConnections} HTTP connection(s).`);
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4321
|
+
let hubShutdownPromise = null;
|
|
4322
|
+
function shutdownHub(signal) {
|
|
4323
|
+
if (hubShutdownPromise) return hubShutdownPromise;
|
|
4324
|
+
hubShutdownPromise = (async () => {
|
|
4325
|
+
const startedAt = Date.now();
|
|
4326
|
+
console.log(`[LiveDesk Hub] Shutdown started signal=${signal} grace=${hubShutdownGraceMs}ms timeout=${hubShutdownTimeoutMs}ms.`);
|
|
4327
|
+
if (roleWatchTimer) clearInterval(roleWatchTimer);
|
|
4328
|
+
clearInterval(browserWebSocketHeartbeatTimer);
|
|
4329
|
+
runSynchronousShutdownStep('update manager close', () => liveDeskUpdateManager?.close());
|
|
4330
|
+
runSynchronousShutdownStep('atlas pool close', () => atlasPool.close());
|
|
3873
4331
|
atlasClients.clear();
|
|
3874
|
-
hubSharedFolders.close();
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
4332
|
+
runSynchronousShutdownStep('shared folder close', () => hubSharedFolders.close());
|
|
4333
|
+
|
|
4334
|
+
const httpClosed = new Promise(resolveClose => {
|
|
4335
|
+
try {
|
|
4336
|
+
httpServer.close(error => {
|
|
4337
|
+
if (error) {
|
|
4338
|
+
console.warn(`[LiveDesk Hub] HTTP listener close warning: ${error.message}`);
|
|
4339
|
+
} else {
|
|
4340
|
+
console.log('[LiveDesk Hub] Shutdown stage completed: HTTP listener closed.');
|
|
4341
|
+
}
|
|
4342
|
+
resolveClose();
|
|
4343
|
+
});
|
|
4344
|
+
httpServer.closeIdleConnections?.();
|
|
4345
|
+
} catch (error) {
|
|
4346
|
+
console.warn(`[LiveDesk Hub] HTTP listener close warning: ${error instanceof Error ? error.message : String(error)}`);
|
|
4347
|
+
resolveClose();
|
|
4348
|
+
}
|
|
4349
|
+
});
|
|
4350
|
+
|
|
4351
|
+
closeBrowserWebSockets();
|
|
4352
|
+
const forceCloseTimer = setTimeout(terminateOpenConnections, hubShutdownGraceMs);
|
|
4353
|
+
forceCloseTimer.unref?.();
|
|
4354
|
+
|
|
4355
|
+
const cleanupBudgetMs = Math.max(500, hubShutdownTimeoutMs - hubShutdownGraceMs - 500);
|
|
4356
|
+
await Promise.all([
|
|
4357
|
+
boundedShutdownStep('host target lease cleared', () => clearHubHostTarget(`process-${String(signal).toLowerCase()}`), Math.min(3_000, cleanupBudgetMs)),
|
|
4358
|
+
boundedShutdownStep('remote transport closed', () => remoteHub.close(), cleanupBudgetMs)
|
|
4359
|
+
]);
|
|
4360
|
+
|
|
4361
|
+
const remainingMs = Math.max(0, hubShutdownTimeoutMs - (Date.now() - startedAt));
|
|
4362
|
+
await Promise.race([httpClosed, waitForShutdownDelay(remainingMs)]);
|
|
4363
|
+
clearTimeout(forceCloseTimer);
|
|
4364
|
+
terminateOpenConnections();
|
|
4365
|
+
console.log(`[LiveDesk Hub] Shutdown complete in ${Date.now() - startedAt}ms.`);
|
|
4366
|
+
})();
|
|
4367
|
+
return hubShutdownPromise;
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
4371
|
+
process.once(signal, () => {
|
|
4372
|
+
void shutdownHub(signal)
|
|
4373
|
+
.then(() => process.exit(0))
|
|
4374
|
+
.catch(error => {
|
|
4375
|
+
console.error(`[LiveDesk Hub] Shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4376
|
+
terminateOpenConnections();
|
|
4377
|
+
process.exit(1);
|
|
4378
|
+
});
|
|
4379
|
+
});
|
|
4380
|
+
}
|