livedesk 0.1.261 → 0.1.263
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 +81 -19
- package/hub/src/live-desk-update.js +411 -0
- package/hub/src/remote-hub.js +11 -9
- package/hub/src/server.js +67 -20
- package/package.json +2 -2
- package/web/dist/assets/{index-DkJSqCbg.css → index-CSfyaj6A.css} +1 -1
- package/web/dist/assets/{index-D4ErD-xm.js → index-DeZmdVR5.js} +13 -13
- package/web/dist/index.html +2 -2
package/bin/livedesk.js
CHANGED
|
@@ -5,7 +5,7 @@ import net from 'node:net';
|
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { execFile, spawn } from 'node:child_process';
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { randomBytes } from 'node:crypto';
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
|
|
@@ -17,7 +17,8 @@ const DEFAULT_REMOTE_HUB_PORT = 5197;
|
|
|
17
17
|
const DEFAULT_MANAGER_URL = `http://127.0.0.1:${DEFAULT_MANAGER_HTTP_PORT}`;
|
|
18
18
|
const PORT_CLEANUP_TIMEOUT_MS = 3000;
|
|
19
19
|
const PORT_CLEANUP_POLL_MS = 100;
|
|
20
|
-
const HUB_STARTUP_RETRY_LIMIT = 1;
|
|
20
|
+
const HUB_STARTUP_RETRY_LIMIT = 1;
|
|
21
|
+
const HUB_UPDATE_POLL_MS = 250;
|
|
21
22
|
const MANAGER_STATE_DIR = join(os.homedir(), '.livedesk');
|
|
22
23
|
const MANAGER_STATE_PATH = join(MANAGER_STATE_DIR, 'manager.json');
|
|
23
24
|
|
|
@@ -75,13 +76,21 @@ Common options:
|
|
|
75
76
|
`.trimStart());
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
function readVersion() {
|
|
79
|
+
function readVersion() {
|
|
79
80
|
try {
|
|
80
81
|
return require('../package.json').version || '0.0.0';
|
|
81
82
|
} catch {
|
|
82
83
|
return '0.0.0';
|
|
83
84
|
}
|
|
84
|
-
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readClientVersion() {
|
|
88
|
+
try {
|
|
89
|
+
return require('../package.json').dependencies?.['@livedesk/client'] || '';
|
|
90
|
+
} catch {
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
85
94
|
|
|
86
95
|
function parseManagerArgs(args) {
|
|
87
96
|
const forwarded = [];
|
|
@@ -501,7 +510,7 @@ function resolvePackageEntry(packageName, fallback) {
|
|
|
501
510
|
}
|
|
502
511
|
}
|
|
503
512
|
|
|
504
|
-
async function runManager(args) {
|
|
513
|
+
async function runManager(args) {
|
|
505
514
|
const options = parseManagerArgs(args);
|
|
506
515
|
const httpPort = normalizePort(
|
|
507
516
|
options.port || process.env.LIVEDESK_HUB_HTTP_PORT || process.env.PORT,
|
|
@@ -514,10 +523,13 @@ async function runManager(args) {
|
|
|
514
523
|
const openUrl = options.openUrlExplicit
|
|
515
524
|
? (options.openUrl || `http://127.0.0.1:${httpPort}`)
|
|
516
525
|
: `http://127.0.0.1:${httpPort}`;
|
|
517
|
-
const pairToken = process.env.REMOTE_HUB_PAIR_TOKEN
|
|
518
|
-
|| process.env.LIVEDESK_CLIENT_PAIR_TOKEN
|
|
519
|
-
|| process.env.MINDEXEC_REMOTE_PAIR_TOKEN
|
|
520
|
-
|| getStablePairToken();
|
|
526
|
+
const pairToken = process.env.REMOTE_HUB_PAIR_TOKEN
|
|
527
|
+
|| process.env.LIVEDESK_CLIENT_PAIR_TOKEN
|
|
528
|
+
|| process.env.MINDEXEC_REMOTE_PAIR_TOKEN
|
|
529
|
+
|| getStablePairToken();
|
|
530
|
+
mkdirSync(MANAGER_STATE_DIR, { recursive: true });
|
|
531
|
+
const updateRequestPath = join(MANAGER_STATE_DIR, `hub-update-${httpPort}-${remotePort}.json`);
|
|
532
|
+
rmSync(updateRequestPath, { force: true });
|
|
521
533
|
|
|
522
534
|
if (options.cleanPortsOnStart) {
|
|
523
535
|
await stopProcessesOnPorts([httpPort, remotePort]);
|
|
@@ -531,21 +543,61 @@ async function runManager(args) {
|
|
|
531
543
|
const env = {
|
|
532
544
|
...process.env,
|
|
533
545
|
LIVEDESK_HUB_HTTP_HOST: options.host || process.env.LIVEDESK_HUB_HTTP_HOST || '127.0.0.1',
|
|
534
|
-
LIVEDESK_HUB_HTTP_PORT: String(httpPort),
|
|
535
|
-
LIVEDESK_MANAGER_VERSION: readVersion(),
|
|
546
|
+
LIVEDESK_HUB_HTTP_PORT: String(httpPort),
|
|
547
|
+
LIVEDESK_MANAGER_VERSION: readVersion(),
|
|
548
|
+
LIVEDESK_CLIENT_PACKAGE_VERSION: readClientVersion(),
|
|
549
|
+
LIVEDESK_HUB_UPDATE_REQUEST_PATH: updateRequestPath,
|
|
536
550
|
REMOTE_HUB_PORT: String(remotePort),
|
|
537
551
|
REMOTE_HUB_PAIR_TOKEN: pairToken,
|
|
538
552
|
LIVEDESK_WEB_DIST: existsSync(resolve(packagedWebDist, 'index.html')) ? packagedWebDist : (process.env.LIVEDESK_WEB_DIST || '')
|
|
539
553
|
};
|
|
540
554
|
|
|
541
|
-
let startupRetryCount = 0;
|
|
542
|
-
|
|
555
|
+
let startupRetryCount = 0;
|
|
556
|
+
let restartRequested = false;
|
|
557
|
+
let activeChild = null;
|
|
558
|
+
let updatePollId = null;
|
|
559
|
+
|
|
560
|
+
const restartWithLatest = () => {
|
|
561
|
+
const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
562
|
+
const next = spawn(npxCommand, ['-y', '--prefer-online', 'livedesk@latest', 'hub', ...args], {
|
|
563
|
+
env: process.env,
|
|
564
|
+
stdio: 'inherit',
|
|
565
|
+
detached: true,
|
|
566
|
+
windowsHide: false
|
|
567
|
+
});
|
|
568
|
+
next.once('error', error => {
|
|
569
|
+
console.error(`Failed to relaunch LiveDesk Hub from npm: ${error.message}`);
|
|
570
|
+
});
|
|
571
|
+
next.unref();
|
|
572
|
+
console.log('[LiveDesk Hub] Update approved. The latest Hub launcher is starting.');
|
|
573
|
+
setTimeout(() => process.exit(0), 100);
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const pollUpdateRequest = () => {
|
|
577
|
+
if (!existsSync(updateRequestPath)) return;
|
|
578
|
+
try {
|
|
579
|
+
const request = JSON.parse(readFileSync(updateRequestPath, 'utf8'));
|
|
580
|
+
rmSync(updateRequestPath, { force: true });
|
|
581
|
+
if (request?.pid && Number(request.pid) !== Number(activeChild?.pid || 0)) {
|
|
582
|
+
restartRequested = true;
|
|
583
|
+
console.log(`[LiveDesk Hub] Restart requested for update ${request.operationId || 'unknown'}.`);
|
|
584
|
+
if (activeChild && !activeChild.killed) {
|
|
585
|
+
activeChild.kill('SIGTERM');
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
} catch {
|
|
589
|
+
// The Hub writes the request atomically; a partial/stale file is retried.
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const startHubProcess = () => {
|
|
543
594
|
const stderrChunks = [];
|
|
544
|
-
const child = spawn(process.execPath, [hubEntry, ...options.forwarded], {
|
|
595
|
+
const child = spawn(process.execPath, [hubEntry, ...options.forwarded], {
|
|
545
596
|
env,
|
|
546
597
|
stdio: ['inherit', 'inherit', 'pipe'],
|
|
547
598
|
windowsHide: false
|
|
548
|
-
});
|
|
599
|
+
});
|
|
600
|
+
activeChild = child;
|
|
549
601
|
|
|
550
602
|
child.stderr.on('data', chunk => {
|
|
551
603
|
const text = chunk.toString();
|
|
@@ -560,8 +612,17 @@ async function runManager(args) {
|
|
|
560
612
|
console.error(`Failed to start LiveDesk Hub: ${error.message}`);
|
|
561
613
|
process.exitCode = 1;
|
|
562
614
|
});
|
|
563
|
-
child.once('exit', (code, signal) => {
|
|
564
|
-
const startupOutput = stderrChunks.join('');
|
|
615
|
+
child.once('exit', (code, signal) => {
|
|
616
|
+
const startupOutput = stderrChunks.join('');
|
|
617
|
+
if (restartRequested) {
|
|
618
|
+
restartRequested = false;
|
|
619
|
+
if (updatePollId) {
|
|
620
|
+
clearInterval(updatePollId);
|
|
621
|
+
updatePollId = null;
|
|
622
|
+
}
|
|
623
|
+
restartWithLatest();
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
565
626
|
if (!signal
|
|
566
627
|
&& code !== 0
|
|
567
628
|
&& options.cleanPortsOnStart
|
|
@@ -585,10 +646,11 @@ async function runManager(args) {
|
|
|
585
646
|
});
|
|
586
647
|
};
|
|
587
648
|
|
|
588
|
-
startHubProcess();
|
|
649
|
+
startHubProcess();
|
|
650
|
+
updatePollId = setInterval(pollUpdateRequest, HUB_UPDATE_POLL_MS);
|
|
589
651
|
|
|
590
652
|
if (options.openBrowserOnStart) {
|
|
591
|
-
void waitForManager(openUrl).then(ok => {
|
|
653
|
+
void waitForManager(openUrl).then(ok => {
|
|
592
654
|
if (ok) {
|
|
593
655
|
openBrowser(openUrl);
|
|
594
656
|
} else {
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const LIVE_DESK_UPDATE_COMMAND = 'livedesk.client-update';
|
|
4
|
+
export const LIVE_DESK_UPDATE_TIMEOUT_MS = 120_000;
|
|
5
|
+
export const LIVE_DESK_UPDATE_CHECK_INTERVAL_MS = 5 * 60_000;
|
|
6
|
+
|
|
7
|
+
function cleanVersion(value) {
|
|
8
|
+
return String(value || '').trim().replace(/^v/i, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function compareVersions(left, right) {
|
|
12
|
+
const a = cleanVersion(left).split(/[+-]/, 1)[0].split('.').map(part => Number(part) || 0);
|
|
13
|
+
const b = cleanVersion(right).split(/[+-]/, 1)[0].split('.').map(part => Number(part) || 0);
|
|
14
|
+
const length = Math.max(a.length, b.length, 3);
|
|
15
|
+
for (let index = 0; index < length; index += 1) {
|
|
16
|
+
const difference = (a[index] || 0) - (b[index] || 0);
|
|
17
|
+
if (difference !== 0) return difference;
|
|
18
|
+
}
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isVersionAtLeast(candidate, required) {
|
|
23
|
+
return !!cleanVersion(candidate) && !!cleanVersion(required) && compareVersions(candidate, required) >= 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function encodePowerShell(value) {
|
|
27
|
+
return Buffer.from(String(value || ''), 'utf16le').toString('base64');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function quotePowerShell(value) {
|
|
31
|
+
return `'${String(value ?? '').replaceAll("'", "''")}'`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildWindowsLegacyUpdateCommand({ manager, pair, name, slot, targetVersion }) {
|
|
35
|
+
const script = [
|
|
36
|
+
'$ErrorActionPreference = "Stop"',
|
|
37
|
+
'$cursor = Get-CimInstance Win32_Process -Filter ("ProcessId={0}" -f $PID)',
|
|
38
|
+
'$launcherPid = 0',
|
|
39
|
+
'for ($depth = 0; $depth -lt 12 -and $cursor; $depth++) {',
|
|
40
|
+
' $parent = Get-CimInstance Win32_Process -Filter ("ProcessId={0}" -f [int]$cursor.ParentProcessId)',
|
|
41
|
+
' if (-not $parent) { break }',
|
|
42
|
+
' $commandLine = [string]$parent.CommandLine',
|
|
43
|
+
' if ($commandLine -match "(?i)(livedesk-client\\.js|livedesk\\.js)") { $launcherPid = [int]$parent.ProcessId; break }',
|
|
44
|
+
' $cursor = $parent',
|
|
45
|
+
'}',
|
|
46
|
+
'if ($launcherPid -le 0) { throw "LiveDesk client launcher process was not found." }',
|
|
47
|
+
`$env:LIVEDESK_CLIENT_MANAGER = ${quotePowerShell(manager)}`,
|
|
48
|
+
`$env:LIVEDESK_CLIENT_PAIR_TOKEN = ${quotePowerShell(pair)}`,
|
|
49
|
+
`$env:LIVEDESK_CLIENT_NAME = ${quotePowerShell(name)}`,
|
|
50
|
+
`$env:LIVEDESK_CLIENT_SLOT = ${quotePowerShell(slot || '')}`,
|
|
51
|
+
`$env:LIVEDESK_CLIENT_UPDATE_TARGET_VERSION = ${quotePowerShell(targetVersion)}`,
|
|
52
|
+
'$npx = (Get-Command npx.cmd -ErrorAction SilentlyContinue).Source',
|
|
53
|
+
'if (-not $npx) { $npx = "npx.cmd" }',
|
|
54
|
+
'Start-Process -FilePath $npx -ArgumentList @("-y", "--prefer-online", "livedesk@latest", "client", "--no-login", "--update-wait-pid", [string]$launcherPid) -WindowStyle Hidden',
|
|
55
|
+
'Start-Sleep -Milliseconds 500',
|
|
56
|
+
'Start-Process -FilePath "taskkill.exe" -ArgumentList @("/PID", [string]$launcherPid, "/T", "/F") -WindowStyle Hidden',
|
|
57
|
+
''
|
|
58
|
+
].join('\r\n');
|
|
59
|
+
return `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodePowerShell(script)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildUnixLegacyUpdateCommand({ manager, pair, name, slot, targetVersion }) {
|
|
63
|
+
const script = [
|
|
64
|
+
'const { execFileSync, spawn } = require("node:child_process");',
|
|
65
|
+
'const fs = require("node:fs");',
|
|
66
|
+
'const os = require("node:os");',
|
|
67
|
+
'const path = require("node:path");',
|
|
68
|
+
'const self = process.pid;',
|
|
69
|
+
'const parentOf = pid => { try { return Number(execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8" }).trim()) || 0; } catch { return 0; } };',
|
|
70
|
+
'const commandOf = pid => { try { return execFileSync("ps", ["-o", "command=", "-p", String(pid)], { encoding: "utf8" }); } catch { return ""; } };',
|
|
71
|
+
'let cursor = parentOf(self); let launcherPid = 0;',
|
|
72
|
+
'for (let depth = 0; depth < 12 && cursor; depth++) { const command = commandOf(cursor); if (/livedesk-client\\.js|livedesk\\.js/i.test(command)) { launcherPid = cursor; break; } cursor = parentOf(cursor); }',
|
|
73
|
+
'if (!launcherPid) throw new Error("LiveDesk client launcher process was not found.");',
|
|
74
|
+
`process.env.LIVEDESK_CLIENT_MANAGER = ${JSON.stringify(String(manager || ''))};`,
|
|
75
|
+
`process.env.LIVEDESK_CLIENT_PAIR_TOKEN = ${JSON.stringify(String(pair || ''))};`,
|
|
76
|
+
`process.env.LIVEDESK_CLIENT_NAME = ${JSON.stringify(String(name || ''))};`,
|
|
77
|
+
`process.env.LIVEDESK_CLIENT_SLOT = ${JSON.stringify(String(slot || ''))};`,
|
|
78
|
+
`process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION = ${JSON.stringify(String(targetVersion || ''))};`,
|
|
79
|
+
'const updater = spawn("npx", ["-y", "--prefer-online", "livedesk@latest", "client", "--no-login", "--update-wait-pid", String(launcherPid)], { detached: true, stdio: "ignore" });',
|
|
80
|
+
'updater.unref();',
|
|
81
|
+
'setTimeout(() => { try { process.kill(launcherPid, "SIGTERM"); } catch {} }, 500);',
|
|
82
|
+
''
|
|
83
|
+
].join('\n');
|
|
84
|
+
return `node -e ${JSON.stringify(script)}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildLegacyUpdateCommand(device, credentials, targetVersion) {
|
|
88
|
+
const payload = {
|
|
89
|
+
manager: credentials.manager,
|
|
90
|
+
pair: credentials.pairToken,
|
|
91
|
+
name: device.deviceName || device.hostname || '',
|
|
92
|
+
slot: device.slotNumber || '',
|
|
93
|
+
targetVersion
|
|
94
|
+
};
|
|
95
|
+
return ['win32', 'windows'].includes(String(device.platform || '').toLowerCase())
|
|
96
|
+
? buildWindowsLegacyUpdateCommand(payload)
|
|
97
|
+
: buildUnixLegacyUpdateCommand(payload);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function fetchLatestPackage(packageName, fetchImpl) {
|
|
101
|
+
const encodedName = packageName.startsWith('@') ? packageName.replace('/', '%2F') : packageName;
|
|
102
|
+
const response = await fetchImpl(`https://registry.npmjs.org/${encodedName}/latest`, {
|
|
103
|
+
headers: { Accept: 'application/json' },
|
|
104
|
+
signal: AbortSignal.timeout(12_000)
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
throw new Error(`npm registry returned HTTP ${response.status} for ${packageName}.`);
|
|
108
|
+
}
|
|
109
|
+
const payload = await response.json();
|
|
110
|
+
const version = cleanVersion(payload?.version);
|
|
111
|
+
if (!version) throw new Error(`npm registry returned no version for ${packageName}.`);
|
|
112
|
+
return { version, package: payload };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function fetchLatestLiveDeskRelease(fetchImpl = globalThis.fetch) {
|
|
116
|
+
if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable for LiveDesk update checks.');
|
|
117
|
+
const [manager, client] = await Promise.all([
|
|
118
|
+
fetchLatestPackage('livedesk', fetchImpl),
|
|
119
|
+
fetchLatestPackage('@livedesk/client', fetchImpl)
|
|
120
|
+
]);
|
|
121
|
+
return {
|
|
122
|
+
latestManagerVersion: manager.version,
|
|
123
|
+
latestClientVersion: client.version,
|
|
124
|
+
managerPackage: manager.package,
|
|
125
|
+
clientPackage: client.package,
|
|
126
|
+
checkedAt: new Date().toISOString()
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function statusForRun(run) {
|
|
131
|
+
if (!run) return null;
|
|
132
|
+
return {
|
|
133
|
+
operationId: run.operationId,
|
|
134
|
+
state: run.state,
|
|
135
|
+
startedAt: run.startedAt,
|
|
136
|
+
updatedAt: run.updatedAt,
|
|
137
|
+
targetCount: run.targets.length,
|
|
138
|
+
completedCount: run.targets.filter(target => target.state === 'completed').length,
|
|
139
|
+
waitingCount: run.targets.filter(target => target.state === 'waiting').length,
|
|
140
|
+
failedCount: run.targets.filter(target => target.state === 'failed').length,
|
|
141
|
+
error: run.error || '',
|
|
142
|
+
targets: run.targets.map(target => ({ ...target }))
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function connectedDevices(remoteHub) {
|
|
147
|
+
return remoteHub.listDevices({ includeDataUrl: false })
|
|
148
|
+
.filter(device => device.connected === true && device.synthetic !== true)
|
|
149
|
+
.slice(0, 500);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function createLiveDeskUpdateManager({
|
|
153
|
+
remoteHub,
|
|
154
|
+
currentManagerVersion,
|
|
155
|
+
currentClientVersion,
|
|
156
|
+
restartSupported = false,
|
|
157
|
+
requestHubRestart,
|
|
158
|
+
fetchImpl = globalThis.fetch,
|
|
159
|
+
now = () => Date.now()
|
|
160
|
+
}) {
|
|
161
|
+
let latestRelease = null;
|
|
162
|
+
let checkError = '';
|
|
163
|
+
let checkPromise = null;
|
|
164
|
+
let run = null;
|
|
165
|
+
let checkTimer = null;
|
|
166
|
+
let runTimer = null;
|
|
167
|
+
|
|
168
|
+
const touch = () => {
|
|
169
|
+
if (run) run.updatedAt = new Date(now()).toISOString();
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const checkLatest = async () => {
|
|
173
|
+
if (checkPromise) return checkPromise;
|
|
174
|
+
checkPromise = fetchLatestLiveDeskRelease(fetchImpl)
|
|
175
|
+
.then(release => {
|
|
176
|
+
latestRelease = release;
|
|
177
|
+
checkError = '';
|
|
178
|
+
return release;
|
|
179
|
+
})
|
|
180
|
+
.catch(error => {
|
|
181
|
+
checkError = error instanceof Error ? error.message : String(error);
|
|
182
|
+
return null;
|
|
183
|
+
})
|
|
184
|
+
.finally(() => {
|
|
185
|
+
checkPromise = null;
|
|
186
|
+
});
|
|
187
|
+
return checkPromise;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const getStatus = () => {
|
|
191
|
+
const managerUpdateAvailable = !!latestRelease
|
|
192
|
+
&& compareVersions(latestRelease.latestManagerVersion, currentManagerVersion) > 0;
|
|
193
|
+
const clientDevices = connectedDevices(remoteHub);
|
|
194
|
+
const outdatedClientDevices = latestRelease
|
|
195
|
+
? clientDevices.filter(device => !isVersionAtLeast(device.agentVersion, latestRelease.latestClientVersion))
|
|
196
|
+
: [];
|
|
197
|
+
const clientPackageUpdateAvailable = !!latestRelease
|
|
198
|
+
&& compareVersions(latestRelease.latestClientVersion, currentClientVersion) > 0;
|
|
199
|
+
const clientUpdateAvailable = clientPackageUpdateAvailable || outdatedClientDevices.length > 0;
|
|
200
|
+
const updateAvailable = managerUpdateAvailable || clientUpdateAvailable;
|
|
201
|
+
const activeRun = statusForRun(run);
|
|
202
|
+
return {
|
|
203
|
+
currentVersion: String(currentManagerVersion || ''),
|
|
204
|
+
currentClientVersion: String(currentClientVersion || ''),
|
|
205
|
+
latestVersion: latestRelease?.latestManagerVersion || '',
|
|
206
|
+
latestClientVersion: latestRelease?.latestClientVersion || '',
|
|
207
|
+
updateAvailable,
|
|
208
|
+
managerUpdateAvailable,
|
|
209
|
+
clientUpdateAvailable,
|
|
210
|
+
clientPackageUpdateAvailable,
|
|
211
|
+
outdatedClientCount: outdatedClientDevices.length,
|
|
212
|
+
checkedAt: latestRelease?.checkedAt || '',
|
|
213
|
+
checkError,
|
|
214
|
+
restartSupported: !!restartSupported,
|
|
215
|
+
canApply: updateAvailable && (!managerUpdateAvailable && !clientPackageUpdateAvailable || !!restartSupported),
|
|
216
|
+
...(activeRun || { state: 'idle', operationId: '', targetCount: 0, completedCount: 0, waitingCount: 0, failedCount: 0, targets: [] })
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const failRun = (message) => {
|
|
221
|
+
if (!run) return;
|
|
222
|
+
run.state = 'failed';
|
|
223
|
+
run.error = String(message || 'LiveDesk update failed.');
|
|
224
|
+
touch();
|
|
225
|
+
if (runTimer) {
|
|
226
|
+
clearInterval(runTimer);
|
|
227
|
+
runTimer = null;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const requestRestart = () => {
|
|
232
|
+
if (!restartSupported) {
|
|
233
|
+
failRun('Hub launcher restart is unavailable. Start LiveDesk through npx livedesk@latest.');
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
const result = requestHubRestart?.({
|
|
237
|
+
operationId: run?.operationId || '',
|
|
238
|
+
latestVersion: latestRelease?.latestManagerVersion || '',
|
|
239
|
+
latestClientVersion: latestRelease?.latestClientVersion || ''
|
|
240
|
+
});
|
|
241
|
+
if (result?.ok !== true) {
|
|
242
|
+
failRun(result?.error || 'Hub launcher restart request failed.');
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
if (run) {
|
|
246
|
+
run.state = 'hub-restart-requested';
|
|
247
|
+
touch();
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const verifyTargets = () => {
|
|
253
|
+
if (!run || run.state !== 'waiting-for-clients') return;
|
|
254
|
+
const devices = new Map(connectedDevices(remoteHub).map(device => [String(device.deviceId), device]));
|
|
255
|
+
for (const target of run.targets) {
|
|
256
|
+
if (target.state === 'failed' || target.state === 'completed') continue;
|
|
257
|
+
const device = devices.get(target.deviceId);
|
|
258
|
+
if (device?.connected === true && isVersionAtLeast(device.agentVersion, run.latestClientVersion)) {
|
|
259
|
+
target.state = 'completed';
|
|
260
|
+
target.currentVersion = String(device.agentVersion || '');
|
|
261
|
+
target.completedAt = new Date(now()).toISOString();
|
|
262
|
+
} else {
|
|
263
|
+
target.state = 'waiting';
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
touch();
|
|
267
|
+
if (run.targets.every(target => target.state === 'completed')) {
|
|
268
|
+
if (runTimer) {
|
|
269
|
+
clearInterval(runTimer);
|
|
270
|
+
runTimer = null;
|
|
271
|
+
}
|
|
272
|
+
if (run.needsHubRestart) {
|
|
273
|
+
requestRestart();
|
|
274
|
+
} else {
|
|
275
|
+
run.state = 'clients-updated';
|
|
276
|
+
touch();
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (now() - Date.parse(run.startedAt) >= LIVE_DESK_UPDATE_TIMEOUT_MS) {
|
|
281
|
+
failRun(`Timed out waiting for ${run.targets.filter(target => target.state !== 'completed').length} client(s) to reconnect with ${run.latestClientVersion}.`);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const dispatchTarget = (target, credentials) => {
|
|
286
|
+
const device = target.device;
|
|
287
|
+
const supportsDedicated = device.capabilities?.clientUpdate === true;
|
|
288
|
+
const payload = {
|
|
289
|
+
targetVersion: run.latestClientVersion,
|
|
290
|
+
managerVersion: run.latestManagerVersion,
|
|
291
|
+
operationId: run.operationId
|
|
292
|
+
};
|
|
293
|
+
const result = supportsDedicated
|
|
294
|
+
? remoteHub.sendCommand(device.deviceId, { command: LIVE_DESK_UPDATE_COMMAND, payload })
|
|
295
|
+
: remoteHub.requestAgentTask(device.deviceId, {
|
|
296
|
+
operation: 'command.run',
|
|
297
|
+
title: 'Update LiveDesk client',
|
|
298
|
+
instruction: 'Update and restart this LiveDesk client.',
|
|
299
|
+
toolArguments: {
|
|
300
|
+
command: buildLegacyUpdateCommand(device, credentials, run.latestClientVersion),
|
|
301
|
+
timeoutMs: 30_000
|
|
302
|
+
},
|
|
303
|
+
permissionMode: 'full-access',
|
|
304
|
+
approvalLevel: 'once'
|
|
305
|
+
});
|
|
306
|
+
if (result?.ok !== true) {
|
|
307
|
+
target.state = 'failed';
|
|
308
|
+
target.error = String(result?.error || 'client-update-dispatch-failed');
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
target.state = 'waiting';
|
|
312
|
+
target.method = supportsDedicated ? 'dedicated' : 'legacy-command-run';
|
|
313
|
+
target.commandId = String(result.commandId || result.taskId || '');
|
|
314
|
+
return true;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const startUpdate = async () => {
|
|
318
|
+
if (run && ['dispatching', 'waiting-for-clients', 'hub-restart-requested'].includes(run.state)) {
|
|
319
|
+
return { ok: true, ...getStatus() };
|
|
320
|
+
}
|
|
321
|
+
const release = latestRelease || await checkLatest();
|
|
322
|
+
if (!release) return { ok: false, error: checkError || 'LiveDesk update check failed.', ...getStatus() };
|
|
323
|
+
const managerNeedsUpdate = compareVersions(release.latestManagerVersion, currentManagerVersion) > 0;
|
|
324
|
+
const clientPackageNeedsUpdate = compareVersions(release.latestClientVersion, currentClientVersion) > 0;
|
|
325
|
+
const connected = connectedDevices(remoteHub);
|
|
326
|
+
const outdatedClients = connected.filter(device => !isVersionAtLeast(device.agentVersion, release.latestClientVersion));
|
|
327
|
+
const needsHubRestart = managerNeedsUpdate || clientPackageNeedsUpdate;
|
|
328
|
+
if (!managerNeedsUpdate && !clientPackageNeedsUpdate && outdatedClients.length === 0) {
|
|
329
|
+
return { ok: false, error: 'LiveDesk is already up to date.', ...getStatus() };
|
|
330
|
+
}
|
|
331
|
+
if (needsHubRestart && !restartSupported) {
|
|
332
|
+
return { ok: false, error: 'Hub launcher restart is unavailable. Start LiveDesk through npx livedesk@latest.', ...getStatus() };
|
|
333
|
+
}
|
|
334
|
+
const targets = managerNeedsUpdate || clientPackageNeedsUpdate ? connected : outdatedClients;
|
|
335
|
+
const credentials = remoteHub.getStatus({ includeSecrets: true });
|
|
336
|
+
run = {
|
|
337
|
+
operationId: randomUUID(),
|
|
338
|
+
state: 'dispatching',
|
|
339
|
+
startedAt: new Date(now()).toISOString(),
|
|
340
|
+
updatedAt: new Date(now()).toISOString(),
|
|
341
|
+
latestManagerVersion: release.latestManagerVersion,
|
|
342
|
+
latestClientVersion: release.latestClientVersion,
|
|
343
|
+
needsHubRestart,
|
|
344
|
+
error: '',
|
|
345
|
+
targets: targets.map(device => ({
|
|
346
|
+
deviceId: String(device.deviceId || ''),
|
|
347
|
+
deviceName: String(device.deviceName || device.hostname || device.deviceId || ''),
|
|
348
|
+
oldVersion: String(device.agentVersion || ''),
|
|
349
|
+
currentVersion: '',
|
|
350
|
+
state: 'queued',
|
|
351
|
+
method: '',
|
|
352
|
+
commandId: '',
|
|
353
|
+
error: '',
|
|
354
|
+
device
|
|
355
|
+
}))
|
|
356
|
+
};
|
|
357
|
+
for (const target of run.targets) dispatchTarget(target, credentials);
|
|
358
|
+
run.targets = run.targets.map(({ device, ...target }) => target);
|
|
359
|
+
touch();
|
|
360
|
+
if (run.targets.some(target => target.state === 'failed')) {
|
|
361
|
+
failRun('One or more clients could not be scheduled for update.');
|
|
362
|
+
return { ok: false, error: run.error, ...getStatus() };
|
|
363
|
+
}
|
|
364
|
+
if (run.targets.length === 0) {
|
|
365
|
+
if (run.needsHubRestart) {
|
|
366
|
+
requestRestart();
|
|
367
|
+
} else {
|
|
368
|
+
run.state = 'clients-updated';
|
|
369
|
+
touch();
|
|
370
|
+
}
|
|
371
|
+
return { ok: true, ...getStatus() };
|
|
372
|
+
}
|
|
373
|
+
run.state = 'waiting-for-clients';
|
|
374
|
+
runTimer = setInterval(verifyTargets, 1000);
|
|
375
|
+
runTimer.unref?.();
|
|
376
|
+
verifyTargets();
|
|
377
|
+
return { ok: true, ...getStatus() };
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const handleRemoteEvent = (type, event) => {
|
|
381
|
+
if (!run || run.state !== 'waiting-for-clients' || type !== 'RemoteCommandResult') return;
|
|
382
|
+
const deviceId = String(event?.device?.deviceId || '');
|
|
383
|
+
const commandId = String(event?.commandId || '');
|
|
384
|
+
const target = run.targets.find(item => item.deviceId === deviceId && item.commandId === commandId);
|
|
385
|
+
if (!target || target.method !== 'dedicated') return;
|
|
386
|
+
const result = event?.result;
|
|
387
|
+
if (event?.error || result?.ok === false || result?.status === 'failed') {
|
|
388
|
+
target.state = 'failed';
|
|
389
|
+
target.error = String(event.error || result?.error || 'client-update-command-failed');
|
|
390
|
+
touch();
|
|
391
|
+
failRun('A client rejected the update request. Hub was kept running.');
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
checkTimer = setInterval(() => { void checkLatest(); }, LIVE_DESK_UPDATE_CHECK_INTERVAL_MS);
|
|
396
|
+
checkTimer.unref?.();
|
|
397
|
+
void checkLatest();
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
checkLatest,
|
|
401
|
+
getStatus,
|
|
402
|
+
startUpdate,
|
|
403
|
+
handleRemoteEvent,
|
|
404
|
+
close() {
|
|
405
|
+
if (checkTimer) clearInterval(checkTimer);
|
|
406
|
+
if (runTimer) clearInterval(runTimer);
|
|
407
|
+
checkTimer = null;
|
|
408
|
+
runTimer = null;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
package/hub/src/remote-hub.js
CHANGED
|
@@ -4490,15 +4490,17 @@ export function createRemoteHub(options = {}) {
|
|
|
4490
4490
|
function sendCommand(deviceId, command) {
|
|
4491
4491
|
const device = devices.get(String(deviceId || ''));
|
|
4492
4492
|
const commandName = safeString(command?.command || 'ping', 80);
|
|
4493
|
-
const requiredPermission = commandName === 'input.control'
|
|
4494
|
-
? 'allowControl'
|
|
4495
|
-
: commandName.startsWith('file.transfer')
|
|
4496
|
-
? 'allowFileTransfer'
|
|
4497
|
-
: commandName === 'audio.start'
|
|
4498
|
-
? 'allowRemoteAudio'
|
|
4499
|
-
: commandName
|
|
4500
|
-
? 'allowAgent'
|
|
4501
|
-
|
|
4493
|
+
const requiredPermission = commandName === 'input.control'
|
|
4494
|
+
? 'allowControl'
|
|
4495
|
+
: commandName.startsWith('file.transfer')
|
|
4496
|
+
? 'allowFileTransfer'
|
|
4497
|
+
: commandName === 'audio.start'
|
|
4498
|
+
? 'allowRemoteAudio'
|
|
4499
|
+
: commandName === 'livedesk.client-update'
|
|
4500
|
+
? 'allowAgent'
|
|
4501
|
+
: commandName.startsWith('agent.')
|
|
4502
|
+
? 'allowAgent'
|
|
4503
|
+
: '';
|
|
4502
4504
|
const denied = policyError(device, requiredPermission);
|
|
4503
4505
|
if (denied) return { ok: false, error: denied };
|
|
4504
4506
|
if (device?.synthetic === true && device.connected) {
|