livedesk 0.1.384 → 0.1.385
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/livedesk.js +221 -70
- package/client/bin/livedesk-client-node.js +285 -79
- package/client/bin/livedesk-client.js +102 -12
- package/client/package.json +5 -5
- package/hub/src/live-desk-update.js +403 -186
- package/hub/src/remote-hub.js +8 -6
- package/hub/src/server.js +244 -44
- package/package.json +8 -7
- package/web/dist/assets/index-CXcKGcRR.js +16 -0
- package/web/dist/assets/{index-C09kUT0m.css → index-knuZbyFM.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-BfqVH3Qb.js +0 -16
|
@@ -1,25 +1,59 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
|
-
export const LIVE_DESK_UPDATE_COMMAND = 'livedesk.client-update';
|
|
4
|
-
// 0.1.143 contains the launcher-PID-aware updater. Older clients use the
|
|
5
|
-
// Hub-generated legacy command so they can still be upgraded safely.
|
|
6
|
-
export const LIVE_DESK_DEDICATED_CLIENT_UPDATE_MIN_VERSION = '0.1.143';
|
|
7
|
-
export const
|
|
8
|
-
export const
|
|
3
|
+
export const LIVE_DESK_UPDATE_COMMAND = 'livedesk.client-update';
|
|
4
|
+
// 0.1.143 contains the launcher-PID-aware updater. Older clients use the
|
|
5
|
+
// Hub-generated legacy command so they can still be upgraded safely.
|
|
6
|
+
export const LIVE_DESK_DEDICATED_CLIENT_UPDATE_MIN_VERSION = '0.1.143';
|
|
7
|
+
export const LIVE_DESK_UPDATE_CLIENT_BATCH_SIZE = 5;
|
|
8
|
+
export const LIVE_DESK_UPDATE_TARGET_TIMEOUT_MS = 300_000;
|
|
9
|
+
export const LIVE_DESK_UPDATE_TIMEOUT_MS = 60 * 60_000;
|
|
10
|
+
export const LIVE_DESK_UPDATE_CHECK_INTERVAL_MS = 60_000;
|
|
9
11
|
|
|
10
|
-
function cleanVersion(value) {
|
|
11
|
-
return String(value || '').trim().replace(/^v/i, '');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
12
|
+
function cleanVersion(value) {
|
|
13
|
+
return String(value || '').trim().replace(/^v/i, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseVersion(value) {
|
|
17
|
+
const cleaned = cleanVersion(value);
|
|
18
|
+
const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
19
|
+
if (!match) return null;
|
|
20
|
+
return {
|
|
21
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
22
|
+
prerelease: match[4] ? match[4].split('.') : []
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function comparePrerelease(left, right) {
|
|
27
|
+
if (left.length === 0 && right.length === 0) return 0;
|
|
28
|
+
if (left.length === 0) return 1;
|
|
29
|
+
if (right.length === 0) return -1;
|
|
30
|
+
const length = Math.max(left.length, right.length);
|
|
31
|
+
for (let index = 0; index < length; index += 1) {
|
|
32
|
+
if (left[index] === undefined) return -1;
|
|
33
|
+
if (right[index] === undefined) return 1;
|
|
34
|
+
const leftNumeric = /^\d+$/.test(left[index]);
|
|
35
|
+
const rightNumeric = /^\d+$/.test(right[index]);
|
|
36
|
+
if (leftNumeric && rightNumeric) {
|
|
37
|
+
const difference = Number(left[index]) - Number(right[index]);
|
|
38
|
+
if (difference !== 0) return difference;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
|
|
42
|
+
const difference = left[index].localeCompare(right[index], 'en');
|
|
43
|
+
if (difference !== 0) return difference;
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function compareVersions(left, right) {
|
|
49
|
+
const a = parseVersion(left);
|
|
50
|
+
const b = parseVersion(right);
|
|
51
|
+
if (!a || !b) return cleanVersion(left).localeCompare(cleanVersion(right), 'en');
|
|
52
|
+
for (let index = 0; index < 3; index += 1) {
|
|
53
|
+
const difference = a.core[index] - b.core[index];
|
|
54
|
+
if (difference !== 0) return difference;
|
|
55
|
+
}
|
|
56
|
+
return comparePrerelease(a.prerelease, b.prerelease);
|
|
23
57
|
}
|
|
24
58
|
|
|
25
59
|
export function isVersionAtLeast(candidate, required) {
|
|
@@ -34,7 +68,7 @@ function quotePowerShell(value) {
|
|
|
34
68
|
return `'${String(value ?? '').replaceAll("'", "''")}'`;
|
|
35
69
|
}
|
|
36
70
|
|
|
37
|
-
function buildWindowsLegacyUpdateCommand({ manager, pair, name, slot, targetVersion }) {
|
|
71
|
+
function buildWindowsLegacyUpdateCommand({ manager, pair, name, slot, targetVersion, targetProductVersion }) {
|
|
38
72
|
const script = [
|
|
39
73
|
'$ErrorActionPreference = "Stop"',
|
|
40
74
|
'$cursor = Get-CimInstance Win32_Process -Filter ("ProcessId={0}" -f $PID)',
|
|
@@ -54,8 +88,9 @@ function buildWindowsLegacyUpdateCommand({ manager, pair, name, slot, targetVers
|
|
|
54
88
|
`$env:LIVEDESK_CLIENT_MANAGER = ${quotePowerShell(manager)}`,
|
|
55
89
|
`$env:LIVEDESK_CLIENT_PAIR_TOKEN = ${quotePowerShell(pair)}`,
|
|
56
90
|
`$env:LIVEDESK_CLIENT_NAME = ${quotePowerShell(name)}`,
|
|
57
|
-
`$env:LIVEDESK_CLIENT_SLOT = ${quotePowerShell(slot || '')}`,
|
|
58
|
-
`$env:LIVEDESK_CLIENT_UPDATE_TARGET_VERSION = ${quotePowerShell(targetVersion)}`,
|
|
91
|
+
`$env:LIVEDESK_CLIENT_SLOT = ${quotePowerShell(slot || '')}`,
|
|
92
|
+
`$env:LIVEDESK_CLIENT_UPDATE_TARGET_VERSION = ${quotePowerShell(targetVersion)}`,
|
|
93
|
+
`$env:LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION = ${quotePowerShell(targetProductVersion)}`,
|
|
59
94
|
'$node = [string]$env:LIVEDESK_NODE_EXECUTABLE',
|
|
60
95
|
'if ($node -and -not (Test-Path -LiteralPath $node -PathType Leaf)) { $node = "" }',
|
|
61
96
|
'if (-not $node) { $node = (Get-Command node.exe -ErrorAction SilentlyContinue).Source }',
|
|
@@ -88,7 +123,9 @@ function buildUnixClientUpdateBootstrapScript() {
|
|
|
88
123
|
'const npmExecPath = String(process.env.npm_execpath || process.env.NPM_EXECPATH || "").trim();',
|
|
89
124
|
'const npxCliCandidates = [process.env.LIVEDESK_NPX_CLI_PATH, npmExecPath ? path.join(path.dirname(npmExecPath), "npx-cli.js") : "", path.join(nodeDir, "node_modules", "npm", "bin", "npx-cli.js")].filter(Boolean);',
|
|
90
125
|
'const npxCli = npxCliCandidates.find(candidate => fs.existsSync(candidate));',
|
|
91
|
-
'const
|
|
126
|
+
'const targetProductVersion = String(process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION || "").trim();',
|
|
127
|
+
'const packageSpec = /^\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(targetProductVersion) ? "livedesk@" + targetProductVersion : "livedesk@latest";',
|
|
128
|
+
'const npxArgs = ["-y", "--prefer-online", packageSpec, "--force-role", "client", "--no-login", "--no-open"];',
|
|
92
129
|
'const command = npxCli ? process.execPath : (process.platform === "win32" ? (process.env.ComSpec || "cmd.exe") : npx);',
|
|
93
130
|
'const commandArgs = npxCli ? [npxCli, ...npxArgs] : (process.platform === "win32" ? ["/d", "/s", "/c", "call \\"" + npx + "\\" " + npxArgs.join(" ")] : npxArgs);',
|
|
94
131
|
'process.env.LIVEDESK_SKIP_BROWSER_OPEN = "1";',
|
|
@@ -97,7 +134,7 @@ function buildUnixClientUpdateBootstrapScript() {
|
|
|
97
134
|
].join('\n');
|
|
98
135
|
}
|
|
99
136
|
|
|
100
|
-
function buildUnixLegacyUpdateCommand({ manager, pair, name, slot, targetVersion }) {
|
|
137
|
+
function buildUnixLegacyUpdateCommand({ manager, pair, name, slot, targetVersion, targetProductVersion }) {
|
|
101
138
|
const script = [
|
|
102
139
|
'const { execFileSync, spawn } = require("node:child_process");',
|
|
103
140
|
'const fs = require("node:fs");',
|
|
@@ -115,8 +152,9 @@ function buildUnixLegacyUpdateCommand({ manager, pair, name, slot, targetVersion
|
|
|
115
152
|
`process.env.LIVEDESK_CLIENT_MANAGER = ${JSON.stringify(String(manager || ''))};`,
|
|
116
153
|
`process.env.LIVEDESK_CLIENT_PAIR_TOKEN = ${JSON.stringify(String(pair || ''))};`,
|
|
117
154
|
`process.env.LIVEDESK_CLIENT_NAME = ${JSON.stringify(String(name || ''))};`,
|
|
118
|
-
`process.env.LIVEDESK_CLIENT_SLOT = ${JSON.stringify(String(slot || ''))};`,
|
|
119
|
-
`process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION = ${JSON.stringify(String(targetVersion || ''))};`,
|
|
155
|
+
`process.env.LIVEDESK_CLIENT_SLOT = ${JSON.stringify(String(slot || ''))};`,
|
|
156
|
+
`process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION = ${JSON.stringify(String(targetVersion || ''))};`,
|
|
157
|
+
`process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION = ${JSON.stringify(String(targetProductVersion || ''))};`,
|
|
120
158
|
`const bootstrapScript = ${JSON.stringify(buildUnixClientUpdateBootstrapScript())};`,
|
|
121
159
|
'const bootstrapEnv = { ...process.env, LIVEDESK_UPDATE_WAIT_PID: String(waitPid), LIVEDESK_UPDATE_AGENT_PID: String(agentPid) };',
|
|
122
160
|
'const updater = spawn(process.execPath, ["-e", bootstrapScript], { detached: true, stdio: "ignore", env: bootstrapEnv });',
|
|
@@ -130,13 +168,14 @@ function buildUnixLegacyUpdateCommand({ manager, pair, name, slot, targetVersion
|
|
|
130
168
|
].join('\n');
|
|
131
169
|
}
|
|
132
170
|
|
|
133
|
-
function buildLegacyUpdateCommand(device, credentials, targetVersion) {
|
|
134
|
-
const payload = {
|
|
135
|
-
manager: credentials.manager,
|
|
171
|
+
function buildLegacyUpdateCommand(device, credentials, targetVersion, targetProductVersion) {
|
|
172
|
+
const payload = {
|
|
173
|
+
manager: credentials.agentEndpoint || credentials.manager,
|
|
136
174
|
pair: credentials.pairToken,
|
|
137
175
|
name: device.deviceName || device.hostname || '',
|
|
138
176
|
slot: device.slotNumber || '',
|
|
139
|
-
targetVersion
|
|
177
|
+
targetVersion,
|
|
178
|
+
targetProductVersion
|
|
140
179
|
};
|
|
141
180
|
return ['win32', 'windows'].includes(String(device.platform || '').toLowerCase())
|
|
142
181
|
? buildWindowsLegacyUpdateCommand(payload)
|
|
@@ -158,58 +197,103 @@ async function fetchLatestPackage(packageName, fetchImpl) {
|
|
|
158
197
|
return { version, package: payload };
|
|
159
198
|
}
|
|
160
199
|
|
|
161
|
-
export async function fetchLatestLiveDeskRelease(fetchImpl = globalThis.fetch) {
|
|
200
|
+
export async function fetchLatestLiveDeskRelease(fetchImpl = globalThis.fetch) {
|
|
162
201
|
if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable for LiveDesk update checks.');
|
|
163
202
|
const [manager, client] = await Promise.all([
|
|
164
203
|
fetchLatestPackage('livedesk', fetchImpl),
|
|
165
204
|
fetchLatestPackage('@livedesk/client', fetchImpl)
|
|
166
205
|
]);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
206
|
+
const bundledClientVersion = cleanVersion(manager.package?.livedeskClientVersion);
|
|
207
|
+
if (!bundledClientVersion) {
|
|
208
|
+
throw new Error(`livedesk@${manager.version} is missing required livedeskClientVersion release metadata.`);
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
latestManagerVersion: manager.version,
|
|
212
|
+
latestClientVersion: bundledClientVersion,
|
|
213
|
+
registryClientVersion: client.version,
|
|
214
|
+
releaseSkew: compareVersions(client.version, bundledClientVersion) === 0
|
|
215
|
+
? ''
|
|
216
|
+
: `livedesk@${manager.version} bundles Client ${bundledClientVersion}, while @livedesk/client latest is ${client.version}.`,
|
|
217
|
+
managerPackage: manager.package,
|
|
218
|
+
clientPackage: client.package,
|
|
219
|
+
checkedAt: new Date().toISOString()
|
|
220
|
+
};
|
|
221
|
+
}
|
|
175
222
|
|
|
176
|
-
function statusForRun(run) {
|
|
177
|
-
if (!run) return null;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
223
|
+
function statusForRun(run) {
|
|
224
|
+
if (!run) return null;
|
|
225
|
+
const completedCount = run.targets.filter(target => target.state === 'completed').length;
|
|
226
|
+
const failedCount = run.targets.filter(target => target.state === 'failed').length;
|
|
227
|
+
const queuedCount = run.targets.filter(target => target.state === 'queued').length;
|
|
228
|
+
const pendingOfflineCount = run.targets.filter(target => target.state === 'pending-offline').length;
|
|
229
|
+
const activeCount = run.targets.filter(target => target.state === 'waiting').length;
|
|
230
|
+
return {
|
|
231
|
+
operationId: run.operationId,
|
|
232
|
+
state: run.state,
|
|
233
|
+
startedAt: run.startedAt,
|
|
234
|
+
updatedAt: run.updatedAt,
|
|
235
|
+
targetCount: run.targets.length,
|
|
236
|
+
completedCount,
|
|
237
|
+
waitingCount: run.targets.length - completedCount - failedCount,
|
|
238
|
+
activeCount,
|
|
239
|
+
queuedCount,
|
|
240
|
+
pendingOfflineCount,
|
|
241
|
+
failedCount,
|
|
242
|
+
batchSize: run.batchSize,
|
|
243
|
+
latestManagerVersion: run.latestManagerVersion,
|
|
244
|
+
latestClientVersion: run.latestClientVersion,
|
|
245
|
+
error: run.error || '',
|
|
246
|
+
targets: run.targets.map(target => ({ ...target }))
|
|
247
|
+
};
|
|
248
|
+
}
|
|
191
249
|
|
|
192
|
-
function connectedDevices(remoteHub) {
|
|
250
|
+
function connectedDevices(remoteHub) {
|
|
193
251
|
return remoteHub.listDevices({ includeDataUrl: false })
|
|
194
252
|
.filter(device => device.connected === true && device.synthetic !== true)
|
|
195
253
|
.slice(0, 500);
|
|
196
|
-
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function knownOfflineDevices(remoteHub) {
|
|
257
|
+
return remoteHub.listDevices({ includeDataUrl: false })
|
|
258
|
+
.filter(device => device.connected !== true && device.synthetic !== true)
|
|
259
|
+
.slice(0, 500);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function deviceNeedsUpdate(device, latestManagerVersion, latestClientVersion) {
|
|
263
|
+
return !isVersionAtLeast(device?.productVersion, latestManagerVersion)
|
|
264
|
+
|| !isVersionAtLeast(device?.agentVersion, latestClientVersion);
|
|
265
|
+
}
|
|
197
266
|
|
|
198
267
|
export function createLiveDeskUpdateManager({
|
|
199
268
|
remoteHub,
|
|
200
269
|
currentManagerVersion,
|
|
201
|
-
currentClientVersion,
|
|
202
|
-
restartSupported = false,
|
|
203
|
-
requestHubRestart,
|
|
204
|
-
fetchImpl = globalThis.fetch,
|
|
205
|
-
now = () => Date.now()
|
|
206
|
-
|
|
270
|
+
currentClientVersion,
|
|
271
|
+
restartSupported = false,
|
|
272
|
+
requestHubRestart,
|
|
273
|
+
fetchImpl = globalThis.fetch,
|
|
274
|
+
now = () => Date.now(),
|
|
275
|
+
clientBatchSize = LIVE_DESK_UPDATE_CLIENT_BATCH_SIZE,
|
|
276
|
+
targetTimeoutMs = LIVE_DESK_UPDATE_TARGET_TIMEOUT_MS,
|
|
277
|
+
operationTimeoutMs = LIVE_DESK_UPDATE_TIMEOUT_MS
|
|
278
|
+
}) {
|
|
207
279
|
let latestRelease = null;
|
|
208
280
|
let checkError = '';
|
|
209
281
|
let checkPromise = null;
|
|
210
|
-
let run = null;
|
|
211
|
-
let checkTimer = null;
|
|
212
|
-
let runTimer = null;
|
|
282
|
+
let run = null;
|
|
283
|
+
let checkTimer = null;
|
|
284
|
+
let runTimer = null;
|
|
285
|
+
const effectiveClientBatchSize = Math.max(
|
|
286
|
+
1,
|
|
287
|
+
Math.min(50, Math.floor(Number(clientBatchSize) || LIVE_DESK_UPDATE_CLIENT_BATCH_SIZE))
|
|
288
|
+
);
|
|
289
|
+
const effectiveTargetTimeoutMs = Math.max(
|
|
290
|
+
10_000,
|
|
291
|
+
Number(targetTimeoutMs) || LIVE_DESK_UPDATE_TARGET_TIMEOUT_MS
|
|
292
|
+
);
|
|
293
|
+
const effectiveOperationTimeoutMs = Math.max(
|
|
294
|
+
effectiveTargetTimeoutMs,
|
|
295
|
+
Number(operationTimeoutMs) || LIVE_DESK_UPDATE_TIMEOUT_MS
|
|
296
|
+
);
|
|
213
297
|
|
|
214
298
|
const touch = () => {
|
|
215
299
|
if (run) run.updatedAt = new Date(now()).toISOString();
|
|
@@ -234,45 +318,93 @@ export function createLiveDeskUpdateManager({
|
|
|
234
318
|
};
|
|
235
319
|
|
|
236
320
|
const getStatus = () => {
|
|
237
|
-
const managerUpdateAvailable = !!latestRelease
|
|
238
|
-
&& compareVersions(latestRelease.latestManagerVersion, currentManagerVersion) > 0;
|
|
239
|
-
const clientDevices = connectedDevices(remoteHub);
|
|
240
|
-
const outdatedClientDevices = latestRelease
|
|
241
|
-
? clientDevices.filter(device =>
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
321
|
+
const managerUpdateAvailable = !!latestRelease
|
|
322
|
+
&& compareVersions(latestRelease.latestManagerVersion, currentManagerVersion) > 0;
|
|
323
|
+
const clientDevices = connectedDevices(remoteHub);
|
|
324
|
+
const outdatedClientDevices = latestRelease
|
|
325
|
+
? clientDevices.filter(device => deviceNeedsUpdate(
|
|
326
|
+
device,
|
|
327
|
+
latestRelease.latestManagerVersion,
|
|
328
|
+
latestRelease.latestClientVersion
|
|
329
|
+
))
|
|
330
|
+
: [];
|
|
331
|
+
const pendingOfflineClientCount = latestRelease
|
|
332
|
+
? knownOfflineDevices(remoteHub).filter(device => deviceNeedsUpdate(
|
|
333
|
+
device,
|
|
334
|
+
latestRelease.latestManagerVersion,
|
|
335
|
+
latestRelease.latestClientVersion
|
|
336
|
+
)).length
|
|
337
|
+
: 0;
|
|
338
|
+
const clientPackageUpdateAvailable = !!latestRelease
|
|
339
|
+
&& compareVersions(latestRelease.latestClientVersion, currentClientVersion) > 0;
|
|
245
340
|
const clientUpdateAvailable = clientPackageUpdateAvailable || outdatedClientDevices.length > 0;
|
|
246
341
|
const updateAvailable = managerUpdateAvailable || clientUpdateAvailable;
|
|
247
342
|
const activeRun = statusForRun(run);
|
|
248
343
|
return {
|
|
249
344
|
currentVersion: String(currentManagerVersion || ''),
|
|
250
345
|
currentClientVersion: String(currentClientVersion || ''),
|
|
251
|
-
latestVersion: latestRelease?.latestManagerVersion || '',
|
|
252
|
-
latestClientVersion: latestRelease?.latestClientVersion || '',
|
|
253
|
-
|
|
346
|
+
latestVersion: latestRelease?.latestManagerVersion || '',
|
|
347
|
+
latestClientVersion: latestRelease?.latestClientVersion || '',
|
|
348
|
+
registryClientVersion: latestRelease?.registryClientVersion || '',
|
|
349
|
+
releaseSkew: latestRelease?.releaseSkew || '',
|
|
350
|
+
updateAvailable,
|
|
254
351
|
managerUpdateAvailable,
|
|
255
352
|
clientUpdateAvailable,
|
|
256
|
-
clientPackageUpdateAvailable,
|
|
257
|
-
outdatedClientCount: outdatedClientDevices.length,
|
|
258
|
-
|
|
353
|
+
clientPackageUpdateAvailable,
|
|
354
|
+
outdatedClientCount: outdatedClientDevices.length,
|
|
355
|
+
pendingOfflineClientCount,
|
|
356
|
+
checkedAt: latestRelease?.checkedAt || '',
|
|
259
357
|
checkError,
|
|
260
358
|
restartSupported: !!restartSupported,
|
|
261
359
|
canApply: updateAvailable && (!managerUpdateAvailable && !clientPackageUpdateAvailable || !!restartSupported),
|
|
262
|
-
...(activeRun || {
|
|
360
|
+
...(activeRun || {
|
|
361
|
+
state: 'idle',
|
|
362
|
+
operationId: '',
|
|
363
|
+
targetCount: 0,
|
|
364
|
+
completedCount: 0,
|
|
365
|
+
waitingCount: 0,
|
|
366
|
+
activeCount: 0,
|
|
367
|
+
queuedCount: 0,
|
|
368
|
+
pendingOfflineCount: 0,
|
|
369
|
+
failedCount: 0,
|
|
370
|
+
batchSize: effectiveClientBatchSize,
|
|
371
|
+
targets: []
|
|
372
|
+
})
|
|
263
373
|
};
|
|
264
374
|
};
|
|
265
375
|
|
|
266
|
-
const failRun = (message) => {
|
|
267
|
-
if (!run) return;
|
|
268
|
-
run.state = 'failed';
|
|
269
|
-
run.error = String(message || 'LiveDesk update failed.');
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
376
|
+
const failRun = (message) => {
|
|
377
|
+
if (!run) return;
|
|
378
|
+
run.state = 'failed';
|
|
379
|
+
run.error = String(message || 'LiveDesk update failed.');
|
|
380
|
+
for (const target of run.targets) {
|
|
381
|
+
if (target.state === 'completed') continue;
|
|
382
|
+
target.state = 'failed';
|
|
383
|
+
if (!target.error) {
|
|
384
|
+
target.error = `Rollout stopped: ${run.error}`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
touch();
|
|
388
|
+
if (runTimer) {
|
|
389
|
+
clearInterval(runTimer);
|
|
390
|
+
runTimer = null;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const retrySettlingTargets = () => {
|
|
395
|
+
if (!run || run.state !== 'failed') return [];
|
|
396
|
+
const currentTime = now();
|
|
397
|
+
const devices = new Map(
|
|
398
|
+
remoteHub.listDevices({ includeDataUrl: false })
|
|
399
|
+
.map(device => [String(device.deviceId), device])
|
|
400
|
+
);
|
|
401
|
+
return run.targets.filter(target => {
|
|
402
|
+
if (target.state !== 'failed' || !target.dispatchedAt) return false;
|
|
403
|
+
const deadline = Date.parse(target.deadlineAt || '');
|
|
404
|
+
if (!(deadline > currentTime)) return false;
|
|
405
|
+
return devices.get(target.deviceId)?.connected !== true;
|
|
406
|
+
});
|
|
407
|
+
};
|
|
276
408
|
|
|
277
409
|
const requestRestart = () => {
|
|
278
410
|
if (!restartSupported) {
|
|
@@ -281,8 +413,8 @@ export function createLiveDeskUpdateManager({
|
|
|
281
413
|
}
|
|
282
414
|
const result = requestHubRestart?.({
|
|
283
415
|
operationId: run?.operationId || '',
|
|
284
|
-
latestVersion: latestRelease?.latestManagerVersion || '',
|
|
285
|
-
latestClientVersion: latestRelease?.latestClientVersion || '',
|
|
416
|
+
latestVersion: run?.latestManagerVersion || latestRelease?.latestManagerVersion || '',
|
|
417
|
+
latestClientVersion: run?.latestClientVersion || latestRelease?.latestClientVersion || '',
|
|
286
418
|
// Current versions update every connected Client before the Hub exits.
|
|
287
419
|
// Older Hub versions did the reverse and set this flag themselves; the
|
|
288
420
|
// new server still supports that one-time migration path.
|
|
@@ -299,75 +431,155 @@ export function createLiveDeskUpdateManager({
|
|
|
299
431
|
return true;
|
|
300
432
|
};
|
|
301
433
|
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const device = target.device;
|
|
337
|
-
const supportsDedicated = device.capabilities?.clientUpdate === true
|
|
338
|
-
&& isVersionAtLeast(device.agentVersion, LIVE_DESK_DEDICATED_CLIENT_UPDATE_MIN_VERSION);
|
|
339
|
-
const payload = {
|
|
340
|
-
targetVersion: run.latestClientVersion,
|
|
341
|
-
managerVersion: run.latestManagerVersion,
|
|
342
|
-
operationId: run.operationId
|
|
343
|
-
};
|
|
344
|
-
const result = supportsDedicated
|
|
345
|
-
? remoteHub.sendCommand(device.deviceId, { command: LIVE_DESK_UPDATE_COMMAND, payload })
|
|
346
|
-
: remoteHub.sendLegacyClientUpdate(device.deviceId, {
|
|
347
|
-
command: buildLegacyUpdateCommand(device, credentials, run.latestClientVersion),
|
|
348
|
-
timeoutMs: 30_000
|
|
349
|
-
});
|
|
350
|
-
if (result?.ok !== true) {
|
|
351
|
-
target.state = 'failed';
|
|
352
|
-
target.error = String(result?.error || 'client-update-dispatch-failed');
|
|
353
|
-
return false;
|
|
354
|
-
}
|
|
355
|
-
target.state = 'waiting';
|
|
356
|
-
target.method = supportsDedicated ? 'dedicated' : 'legacy-command-run';
|
|
357
|
-
target.commandId = String(result.commandId || result.taskId || '');
|
|
358
|
-
return true;
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
const startUpdate = async () => {
|
|
362
|
-
if (run && ['dispatching', 'waiting-for-clients', 'hub-restart-requested'].includes(run.state)) {
|
|
363
|
-
return { ok: true, ...getStatus() };
|
|
434
|
+
const dispatchTarget = (target, device, credentials) => {
|
|
435
|
+
const supportsDedicated = device.capabilities?.clientUpdate === true
|
|
436
|
+
&& isVersionAtLeast(device.agentVersion, LIVE_DESK_DEDICATED_CLIENT_UPDATE_MIN_VERSION);
|
|
437
|
+
const payload = {
|
|
438
|
+
targetVersion: run.latestClientVersion,
|
|
439
|
+
targetProductVersion: run.latestManagerVersion,
|
|
440
|
+
managerVersion: run.latestManagerVersion,
|
|
441
|
+
operationId: run.operationId
|
|
442
|
+
};
|
|
443
|
+
const result = supportsDedicated
|
|
444
|
+
? remoteHub.sendCommand(device.deviceId, { command: LIVE_DESK_UPDATE_COMMAND, payload })
|
|
445
|
+
: remoteHub.sendLegacyClientUpdate(device.deviceId, {
|
|
446
|
+
command: buildLegacyUpdateCommand(
|
|
447
|
+
device,
|
|
448
|
+
credentials,
|
|
449
|
+
run.latestClientVersion,
|
|
450
|
+
run.latestManagerVersion
|
|
451
|
+
),
|
|
452
|
+
timeoutMs: 30_000
|
|
453
|
+
});
|
|
454
|
+
if (result?.ok !== true) {
|
|
455
|
+
const dispatchError = String(result?.error || 'client-update-dispatch-failed');
|
|
456
|
+
if (['device-not-connected', 'device-not-found'].includes(dispatchError)) {
|
|
457
|
+
target.state = 'pending-offline';
|
|
458
|
+
target.method = '';
|
|
459
|
+
target.commandId = '';
|
|
460
|
+
target.dispatchedAt = '';
|
|
461
|
+
target.deadlineAt = '';
|
|
462
|
+
target.error = '';
|
|
463
|
+
return 'pending-offline';
|
|
464
|
+
}
|
|
465
|
+
target.state = 'failed';
|
|
466
|
+
target.error = dispatchError;
|
|
467
|
+
return false;
|
|
364
468
|
}
|
|
365
|
-
|
|
469
|
+
target.state = 'waiting';
|
|
470
|
+
target.method = supportsDedicated ? 'dedicated' : 'legacy-command-run';
|
|
471
|
+
target.commandId = String(result.commandId || result.taskId || '');
|
|
472
|
+
target.dispatchedAt = new Date(now()).toISOString();
|
|
473
|
+
target.deadlineAt = new Date(now() + effectiveTargetTimeoutMs).toISOString();
|
|
474
|
+
return true;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const dispatchQueuedTargets = () => {
|
|
478
|
+
if (!run || run.state !== 'waiting-for-clients') return true;
|
|
479
|
+
const activeCount = run.targets.filter(target => target.state === 'waiting').length;
|
|
480
|
+
let availableSlots = Math.max(0, run.batchSize - activeCount);
|
|
481
|
+
if (availableSlots <= 0) return true;
|
|
482
|
+
const devices = new Map(connectedDevices(remoteHub).map(device => [String(device.deviceId), device]));
|
|
483
|
+
const credentials = remoteHub.getStatus({ includeSecrets: true });
|
|
484
|
+
for (const target of run.targets) {
|
|
485
|
+
if (availableSlots <= 0) break;
|
|
486
|
+
if (!['queued', 'pending-offline'].includes(target.state)) continue;
|
|
487
|
+
const device = devices.get(target.deviceId);
|
|
488
|
+
if (!device?.connected) {
|
|
489
|
+
target.state = 'pending-offline';
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
target.state = 'queued';
|
|
493
|
+
const dispatchResult = dispatchTarget(target, device, credentials);
|
|
494
|
+
if (dispatchResult === false) {
|
|
495
|
+
failRun(`Client ${target.deviceName || target.deviceId} could not be scheduled: ${target.error}`);
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
if (dispatchResult === true) {
|
|
499
|
+
availableSlots -= 1;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
touch();
|
|
503
|
+
return true;
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const verifyTargets = () => {
|
|
507
|
+
if (!run || run.state !== 'waiting-for-clients') return;
|
|
508
|
+
const currentTime = now();
|
|
509
|
+
if (currentTime - Date.parse(run.startedAt) >= effectiveOperationTimeoutMs) {
|
|
510
|
+
const unfinishedCount = run.targets.filter(target => target.state !== 'completed').length;
|
|
511
|
+
failRun(`Timed out waiting for ${unfinishedCount} client(s) to finish the rollout.`);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const devices = new Map(connectedDevices(remoteHub).map(device => [String(device.deviceId), device]));
|
|
515
|
+
for (const target of run.targets) {
|
|
516
|
+
if (target.state === 'failed' || target.state === 'completed') continue;
|
|
517
|
+
const device = devices.get(target.deviceId);
|
|
518
|
+
if (target.state === 'waiting'
|
|
519
|
+
&& device?.connected === true
|
|
520
|
+
&& !deviceNeedsUpdate(device, run.latestManagerVersion, run.latestClientVersion)) {
|
|
521
|
+
target.state = 'completed';
|
|
522
|
+
target.currentVersion = String(device.agentVersion || '');
|
|
523
|
+
target.currentProductVersion = String(device.productVersion || '');
|
|
524
|
+
target.completedAt = new Date(currentTime).toISOString();
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (target.state === 'waiting'
|
|
528
|
+
&& Date.parse(target.deadlineAt || '') > 0
|
|
529
|
+
&& currentTime >= Date.parse(target.deadlineAt)) {
|
|
530
|
+
target.state = 'failed';
|
|
531
|
+
target.error = `Timed out waiting for product ${run.latestManagerVersion} / Client ${run.latestClientVersion}.`;
|
|
532
|
+
failRun(`Client ${target.deviceName || target.deviceId} did not restart on the requested versions within ${Math.round(effectiveTargetTimeoutMs / 1000)} seconds.`);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (target.state === 'pending-offline' && device?.connected === true) {
|
|
536
|
+
target.state = 'queued';
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (!dispatchQueuedTargets()) return;
|
|
540
|
+
touch();
|
|
541
|
+
if (run.targets.every(target => target.state === 'completed')) {
|
|
542
|
+
if (runTimer) {
|
|
543
|
+
clearInterval(runTimer);
|
|
544
|
+
runTimer = null;
|
|
545
|
+
}
|
|
546
|
+
if (run.needsHubRestart) {
|
|
547
|
+
requestRestart();
|
|
548
|
+
} else {
|
|
549
|
+
run.state = 'clients-updated';
|
|
550
|
+
touch();
|
|
551
|
+
}
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const startUpdate = async () => {
|
|
557
|
+
if (run && ['dispatching', 'waiting-for-clients', 'hub-restart-requested'].includes(run.state)) {
|
|
558
|
+
return { ok: true, ...getStatus() };
|
|
559
|
+
}
|
|
560
|
+
const settlingTargets = retrySettlingTargets();
|
|
561
|
+
if (settlingTargets.length > 0) {
|
|
562
|
+
const names = settlingTargets
|
|
563
|
+
.slice(0, 3)
|
|
564
|
+
.map(target => target.deviceName || target.deviceId)
|
|
565
|
+
.join(', ');
|
|
566
|
+
const remaining = settlingTargets.length > 3 ? ` and ${settlingTargets.length - 3} more` : '';
|
|
567
|
+
return {
|
|
568
|
+
...getStatus(),
|
|
569
|
+
ok: false,
|
|
570
|
+
error: `The previous rollout is still settling for offline Client(s): ${names}${remaining}. Retry after they reconnect or their restart deadline passes.`
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
const release = latestRelease || await checkLatest();
|
|
366
574
|
if (!release) return { ok: false, error: checkError || 'LiveDesk update check failed.', ...getStatus() };
|
|
367
575
|
const managerNeedsUpdate = compareVersions(release.latestManagerVersion, currentManagerVersion) > 0;
|
|
368
576
|
const clientPackageNeedsUpdate = compareVersions(release.latestClientVersion, currentClientVersion) > 0;
|
|
369
577
|
const connected = connectedDevices(remoteHub);
|
|
370
|
-
const outdatedClients = connected.filter(device =>
|
|
578
|
+
const outdatedClients = connected.filter(device => deviceNeedsUpdate(
|
|
579
|
+
device,
|
|
580
|
+
release.latestManagerVersion,
|
|
581
|
+
release.latestClientVersion
|
|
582
|
+
));
|
|
371
583
|
const needsHubRestart = managerNeedsUpdate || clientPackageNeedsUpdate;
|
|
372
584
|
if (!managerNeedsUpdate && !clientPackageNeedsUpdate && outdatedClients.length === 0) {
|
|
373
585
|
return { ok: true, state: 'clients-updated', ...getStatus() };
|
|
@@ -376,46 +588,49 @@ export function createLiveDeskUpdateManager({
|
|
|
376
588
|
return { ok: false, error: 'Hub launcher restart is unavailable. Start LiveDesk through npx livedesk@latest.', ...getStatus() };
|
|
377
589
|
}
|
|
378
590
|
const targets = outdatedClients;
|
|
379
|
-
|
|
380
|
-
run = {
|
|
591
|
+
run = {
|
|
381
592
|
operationId: randomUUID(),
|
|
382
593
|
state: 'dispatching',
|
|
383
594
|
startedAt: new Date(now()).toISOString(),
|
|
384
595
|
updatedAt: new Date(now()).toISOString(),
|
|
385
596
|
latestManagerVersion: release.latestManagerVersion,
|
|
386
|
-
latestClientVersion: release.latestClientVersion,
|
|
597
|
+
latestClientVersion: release.latestClientVersion,
|
|
387
598
|
needsHubRestart,
|
|
388
|
-
|
|
389
|
-
|
|
599
|
+
batchSize: effectiveClientBatchSize,
|
|
600
|
+
error: '',
|
|
601
|
+
targets: targets.map(device => ({
|
|
390
602
|
deviceId: String(device.deviceId || ''),
|
|
391
|
-
deviceName: String(device.deviceName || device.hostname || device.deviceId || ''),
|
|
392
|
-
oldVersion: String(device.agentVersion || ''),
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
} else {
|
|
603
|
+
deviceName: String(device.deviceName || device.hostname || device.deviceId || ''),
|
|
604
|
+
oldVersion: String(device.agentVersion || ''),
|
|
605
|
+
oldProductVersion: String(device.productVersion || ''),
|
|
606
|
+
currentVersion: '',
|
|
607
|
+
currentProductVersion: '',
|
|
608
|
+
state: 'queued',
|
|
609
|
+
method: '',
|
|
610
|
+
commandId: '',
|
|
611
|
+
dispatchedAt: '',
|
|
612
|
+
deadlineAt: '',
|
|
613
|
+
completedAt: '',
|
|
614
|
+
error: ''
|
|
615
|
+
}))
|
|
616
|
+
};
|
|
617
|
+
touch();
|
|
618
|
+
if (run.targets.length === 0) {
|
|
619
|
+
if (run.needsHubRestart) {
|
|
620
|
+
if (!requestRestart()) {
|
|
621
|
+
return { ok: false, error: run.error, ...getStatus() };
|
|
622
|
+
}
|
|
623
|
+
} else {
|
|
412
624
|
run.state = 'clients-updated';
|
|
413
625
|
touch();
|
|
414
626
|
}
|
|
415
627
|
return { ok: true, ...getStatus() };
|
|
416
|
-
}
|
|
417
|
-
run.state = 'waiting-for-clients';
|
|
418
|
-
|
|
628
|
+
}
|
|
629
|
+
run.state = 'waiting-for-clients';
|
|
630
|
+
if (!dispatchQueuedTargets()) {
|
|
631
|
+
return { ok: false, error: run.error, ...getStatus() };
|
|
632
|
+
}
|
|
633
|
+
runTimer = setInterval(verifyTargets, 1000);
|
|
419
634
|
runTimer.unref?.();
|
|
420
635
|
verifyTargets();
|
|
421
636
|
return { ok: true, ...getStatus() };
|
|
@@ -426,7 +641,9 @@ export function createLiveDeskUpdateManager({
|
|
|
426
641
|
const deviceId = String(event?.device?.deviceId || '');
|
|
427
642
|
const commandId = String(event?.commandId || '');
|
|
428
643
|
const target = run.targets.find(item => item.deviceId === deviceId && item.commandId === commandId);
|
|
429
|
-
if (!target
|
|
644
|
+
if (!target
|
|
645
|
+
|| target.state !== 'waiting'
|
|
646
|
+
|| !['dedicated', 'legacy-command-run'].includes(target.method)) return;
|
|
430
647
|
const result = event?.result;
|
|
431
648
|
if (event?.error || result?.ok === false || result?.status === 'failed' || result?.status === 'rejected') {
|
|
432
649
|
target.state = 'failed';
|