@livedesk/hub 0.1.4 → 0.1.6
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 +7 -3
- 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/live-desk-update.js
CHANGED
|
@@ -1,147 +1,142 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
|
-
export const LIVE_DESK_UPDATE_COMMAND = 'livedesk.client-update';
|
|
4
|
-
// 0.1.
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
export const LIVE_DESK_UPDATE_COMMAND = 'livedesk.client-update';
|
|
4
|
+
// 0.1.172 delegates to the exact-version, identity-preserving package
|
|
5
|
+
// supervisor. 0.1.171 still lacks bounded handoff cancellation, so it must
|
|
6
|
+
// use the Hub-owned compatibility bridge too.
|
|
7
|
+
// Older dedicated handlers can spawn the new package but cannot prove that the
|
|
8
|
+
// same device returned, so the Hub-owned compatibility bridge upgrades them.
|
|
9
|
+
export const LIVE_DESK_DEDICATED_CLIENT_UPDATE_MIN_VERSION = '0.1.172';
|
|
10
|
+
export const LIVE_DESK_UPDATE_CLIENT_BATCH_SIZE = 5;
|
|
11
|
+
export const LIVE_DESK_UPDATE_TARGET_TIMEOUT_MS = 480_000;
|
|
12
|
+
export const LIVE_DESK_UPDATE_TIMEOUT_MS = 90 * 60_000;
|
|
13
|
+
export const LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS = 60_000;
|
|
14
|
+
export const LIVE_DESK_UPDATE_CHECK_INTERVAL_MS = 60_000;
|
|
15
|
+
export const LIVE_DESK_LEGACY_COMMAND_MAX_LENGTH = 3900;
|
|
9
16
|
|
|
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
|
-
|
|
17
|
+
function cleanVersion(value) {
|
|
18
|
+
return String(value || '').trim().replace(/^v/i, '');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseVersion(value) {
|
|
22
|
+
const cleaned = cleanVersion(value);
|
|
23
|
+
const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
24
|
+
if (!match) return null;
|
|
25
|
+
return {
|
|
26
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
27
|
+
prerelease: match[4] ? match[4].split('.') : []
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function comparePrerelease(left, right) {
|
|
32
|
+
if (left.length === 0 && right.length === 0) return 0;
|
|
33
|
+
if (left.length === 0) return 1;
|
|
34
|
+
if (right.length === 0) return -1;
|
|
35
|
+
const length = Math.max(left.length, right.length);
|
|
36
|
+
for (let index = 0; index < length; index += 1) {
|
|
37
|
+
if (left[index] === undefined) return -1;
|
|
38
|
+
if (right[index] === undefined) return 1;
|
|
39
|
+
const leftNumeric = /^\d+$/.test(left[index]);
|
|
40
|
+
const rightNumeric = /^\d+$/.test(right[index]);
|
|
41
|
+
if (leftNumeric && rightNumeric) {
|
|
42
|
+
const difference = Number(left[index]) - Number(right[index]);
|
|
43
|
+
if (difference !== 0) return difference;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
|
|
47
|
+
const difference = left[index].localeCompare(right[index], 'en');
|
|
48
|
+
if (difference !== 0) return difference;
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function compareVersions(left, right) {
|
|
54
|
+
const a = parseVersion(left);
|
|
55
|
+
const b = parseVersion(right);
|
|
56
|
+
if (!a || !b) return cleanVersion(left).localeCompare(cleanVersion(right), 'en');
|
|
57
|
+
for (let index = 0; index < 3; index += 1) {
|
|
58
|
+
const difference = a.core[index] - b.core[index];
|
|
59
|
+
if (difference !== 0) return difference;
|
|
60
|
+
}
|
|
61
|
+
return comparePrerelease(a.prerelease, b.prerelease);
|
|
23
62
|
}
|
|
24
63
|
|
|
25
64
|
export function isVersionAtLeast(candidate, required) {
|
|
26
65
|
return !!cleanVersion(candidate) && !!cleanVersion(required) && compareVersions(candidate, required) >= 0;
|
|
27
66
|
}
|
|
28
67
|
|
|
29
|
-
function encodePowerShell(value) {
|
|
30
|
-
return Buffer.from(String(value || ''), 'utf16le').toString('base64');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
''
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
'
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const script = [
|
|
102
|
-
'const { execFileSync, spawn } = require("node:child_process");',
|
|
103
|
-
'const fs = require("node:fs");',
|
|
104
|
-
'const path = require("node:path");',
|
|
105
|
-
'const parentOf = pid => { try { return Number(execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8" }).trim()) || 0; } catch { return 0; } };',
|
|
106
|
-
'const commandOf = pid => { try { return execFileSync("ps", ["-o", "command=", "-p", String(pid)], { encoding: "utf8" }); } catch { return ""; } };',
|
|
107
|
-
'const isAlive = pid => { try { process.kill(pid, 0); return true; } catch { return false; } };',
|
|
108
|
-
'const configuredLauncherPid = Number(process.env.LIVEDESK_CLIENT_PARENT_PID || 0);',
|
|
109
|
-
'let launcherPid = Number.isInteger(configuredLauncherPid) && configuredLauncherPid > 1 && isAlive(configuredLauncherPid) ? configuredLauncherPid : 0;',
|
|
110
|
-
'let agentPid = Number(process.env.LIVEDESK_CLIENT_AGENT_PID || 0);',
|
|
111
|
-
'let cursor = parentOf(process.pid);',
|
|
112
|
-
'for (let depth = 0; depth < 12 && cursor; depth++) { const command = commandOf(cursor); if (!agentPid && /livedesk-client-node\\.js|livedesk-client-fast|RemoteAgent\\.Fast/i.test(command)) agentPid = cursor; if (!launcherPid && /livedesk-client\\.js|livedesk\\.js/i.test(command)) launcherPid = cursor; cursor = parentOf(cursor); }',
|
|
113
|
-
'if (launcherPid <= 1) throw new Error("LiveDesk client launcher process was not found.");',
|
|
114
|
-
'const waitPid = launcherPid;',
|
|
115
|
-
`process.env.LIVEDESK_CLIENT_MANAGER = ${JSON.stringify(String(manager || ''))};`,
|
|
116
|
-
`process.env.LIVEDESK_CLIENT_PAIR_TOKEN = ${JSON.stringify(String(pair || ''))};`,
|
|
117
|
-
`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 || ''))};`,
|
|
120
|
-
`const bootstrapScript = ${JSON.stringify(buildUnixClientUpdateBootstrapScript())};`,
|
|
121
|
-
'const bootstrapEnv = { ...process.env, LIVEDESK_UPDATE_WAIT_PID: String(waitPid), LIVEDESK_UPDATE_AGENT_PID: String(agentPid) };',
|
|
122
|
-
'const updater = spawn(process.execPath, ["-e", bootstrapScript], { detached: true, stdio: "ignore", env: bootstrapEnv });',
|
|
123
|
-
'updater.once("error", error => { console.error("LiveDesk updater bootstrap failed to start: " + error.message); process.exitCode = 1; });',
|
|
124
|
-
'updater.once("spawn", () => { updater.unref(); setTimeout(() => { try { process.kill(waitPid, "SIGTERM"); } catch {} if (agentPid > 1 && agentPid !== process.pid) setTimeout(() => { try { process.kill(agentPid, "SIGTERM"); } catch {} }, 500); }, 750); });',
|
|
125
|
-
''
|
|
126
|
-
].join('\n');
|
|
127
|
-
return [
|
|
128
|
-
'if [ -n "$LIVEDESK_NODE_EXECUTABLE" ]; then node_bin="$LIVEDESK_NODE_EXECUTABLE"; else node_bin="$(ps -p "$LIVEDESK_CLIENT_PARENT_PID" -o comm= 2>/dev/null | sed "s/^ *//")"; if [ -z "$node_bin" ]; then node_bin="node"; fi; fi',
|
|
129
|
-
`"$node_bin" -e ${JSON.stringify(script)}`
|
|
130
|
-
].join('\n');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function buildLegacyUpdateCommand(device, credentials, targetVersion) {
|
|
134
|
-
const payload = {
|
|
135
|
-
manager: credentials.manager,
|
|
136
|
-
pair: credentials.pairToken,
|
|
137
|
-
name: device.deviceName || device.hostname || '',
|
|
138
|
-
slot: device.slotNumber || '',
|
|
139
|
-
targetVersion
|
|
140
|
-
};
|
|
141
|
-
return ['win32', 'windows'].includes(String(device.platform || '').toLowerCase())
|
|
142
|
-
? buildWindowsLegacyUpdateCommand(payload)
|
|
143
|
-
: buildUnixLegacyUpdateCommand(payload);
|
|
144
|
-
}
|
|
68
|
+
function encodePowerShell(value) {
|
|
69
|
+
return Buffer.from(String(value || ''), 'utf16le').toString('base64');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildLegacyPackageSupervisorStarterSource() {
|
|
73
|
+
const source = [
|
|
74
|
+
"const{spawn}=require('node:child_process'),f=require('node:fs'),p=require('node:path'),c=JSON.parse(Buffer.from(process.env.C,'base64')),e={...process.env,LIVEDESK_UPDATE_STARTER_PID:String(process.pid),LIVEDESK_UPDATE_ORIGINAL_CWD:process.cwd()},w=p.join(require('node:os').tmpdir(),'livedesk-update-cwd','client-'+c[0]),u=e.LIVEDESK_NPX_EXECUTABLE,s=process.platform,j=process.execPath,n=u||(s==='win32'?'npx.cmd':'npx'),x=[e.LIVEDESK_NPX_CLI_PATH,e.npm_execpath&&p.join(p.dirname(e.npm_execpath),'npx-cli.js'),u&&p.join(p.dirname(u),'node_modules/npm/bin/npx-cli.js'),p.join(p.dirname(j),'node_modules/npm/bin/npx-cli.js')].find(v=>v&&f.existsSync(v)),a=['-y','--prefer-online','livedesk@'+c[6],'--internal-legacy-client-update'],z=String.fromCharCode(32),q=v=>'\"'+String(v).replaceAll('\"','\"\"')+'\"';if(!(+c[7]>0))process.exit(1);c[7]=String(Date.now()+Number(c[7]));e.C=Buffer.from(JSON.stringify(c)).toString('base64');f.mkdirSync(w,{recursive:true});if(f.readdirSync(w)[0])process.exit(1);let/**/t=w;for(;;t=p.dirname(t)){if(['package.json','node_modules/livedesk/package.json'].some(v=>f.existsSync(p.join(t,v))))process.exit(1);if(p.dirname(t)===t)break}Object.keys(e).forEach(k=>/^(INIT_CWD|npm_config_(local_prefix|workspaces?|include_workspace_root))$/i.test(k)&&Reflect.deleteProperty(e,k));Object.assign(e,{INIT_CWD:w,npm_config_local_prefix:w,npm_config_include_workspace_root:'false',LIVEDESK_UPDATE_NEUTRAL_CWD:w});e.i=x?{c:j,a:[x,...a]}:s==='win32'?{c:e.ComSpec||'cmd.exe',a:['/d','/s','/c','call'+z+q(n)+z+a.map(q).join(z)]}:{c:n,a};e.r=spawn(e.i.c,e.i.a,{cwd:w,env:e,detached:true,stdio:'ignore',windowsHide:true});",
|
|
75
|
+
"e.r.on('error',()=>process.exit(1));e.r.unref();"
|
|
76
|
+
].join('');
|
|
77
|
+
if (source.includes(' ')) throw new Error('LiveDesk legacy starter source must not contain spaces.');
|
|
78
|
+
return source;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assertLegacyCommandLength(command, platform) {
|
|
82
|
+
if (command.length > LIVE_DESK_LEGACY_COMMAND_MAX_LENGTH) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`LiveDesk legacy ${platform} update command is ${command.length} characters; `
|
|
85
|
+
+ `the compatibility limit is ${LIVE_DESK_LEGACY_COMMAND_MAX_LENGTH}.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return command;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildLegacyPlatformCommand(platform, payload) {
|
|
92
|
+
const environment = [
|
|
93
|
+
String(payload.operationId || ''),
|
|
94
|
+
String(payload.deviceId || ''),
|
|
95
|
+
String(Number(payload.agentPid) || 0),
|
|
96
|
+
cleanVersion(payload.currentProductVersion),
|
|
97
|
+
cleanVersion(payload.currentAgentVersion),
|
|
98
|
+
cleanVersion(payload.targetVersion),
|
|
99
|
+
cleanVersion(payload.targetProductVersion),
|
|
100
|
+
String(Math.floor(Number(payload.updateTimeoutMs) || 0))
|
|
101
|
+
];
|
|
102
|
+
const configBase64 = Buffer.from(JSON.stringify(environment), 'utf8').toString('base64');
|
|
103
|
+
const starter = buildLegacyPackageSupervisorStarterSource();
|
|
104
|
+
const starterBase64 = Buffer.from(starter, 'utf8').toString('base64');
|
|
105
|
+
const windows = ['win32', 'windows'].includes(String(platform || '').toLowerCase());
|
|
106
|
+
const powerShellStarter = [
|
|
107
|
+
"$ErrorActionPreference='Stop'",
|
|
108
|
+
'$n=[string]$env:LIVEDESK_NODE_EXECUTABLE',
|
|
109
|
+
'if(-not $n){$n=(Get-Command node.exe -ErrorAction SilentlyContinue).Source}',
|
|
110
|
+
"if(-not $n){throw 'LiveDesk node executable was not found.'}",
|
|
111
|
+
"$j=\"eval(Buffer.from(process.env.S,'base64').toString('utf8'))\"",
|
|
112
|
+
"$p=Start-Process -FilePath $n -ArgumentList @('-e',$j) -WindowStyle Hidden -PassThru",
|
|
113
|
+
"if(-not $p){throw 'LiveDesk update starter failed.'}"
|
|
114
|
+
].join(';');
|
|
115
|
+
const command = windows
|
|
116
|
+
? `set C=${configBase64}&set S=${starterBase64}&powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodePowerShell(powerShellStarter)}`
|
|
117
|
+
: `C='${configBase64}' S='${starterBase64}' "${'${LIVEDESK_NODE_EXECUTABLE:-node}'}" -e 'eval(Buffer.from(process.env.S,"base64").toString("utf8"))'`;
|
|
118
|
+
return assertLegacyCommandLength(command, windows ? 'Windows' : 'Unix');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildLegacyUpdateCommand(
|
|
122
|
+
device,
|
|
123
|
+
operationId,
|
|
124
|
+
targetVersion,
|
|
125
|
+
targetProductVersion,
|
|
126
|
+
updateTimeoutMs
|
|
127
|
+
) {
|
|
128
|
+
const payload = {
|
|
129
|
+
operationId,
|
|
130
|
+
deviceId: device.deviceId || '',
|
|
131
|
+
agentPid: device.pid || 0,
|
|
132
|
+
currentProductVersion: device.productVersion || '',
|
|
133
|
+
currentAgentVersion: device.agentVersion || '',
|
|
134
|
+
targetVersion,
|
|
135
|
+
targetProductVersion,
|
|
136
|
+
updateTimeoutMs
|
|
137
|
+
};
|
|
138
|
+
return buildLegacyPlatformCommand(device.platform, payload);
|
|
139
|
+
}
|
|
145
140
|
|
|
146
141
|
async function fetchLatestPackage(packageName, fetchImpl) {
|
|
147
142
|
const encodedName = packageName.startsWith('@') ? packageName.replace('/', '%2F') : packageName;
|
|
@@ -158,58 +153,121 @@ async function fetchLatestPackage(packageName, fetchImpl) {
|
|
|
158
153
|
return { version, package: payload };
|
|
159
154
|
}
|
|
160
155
|
|
|
161
|
-
export async function fetchLatestLiveDeskRelease(fetchImpl = globalThis.fetch) {
|
|
156
|
+
export async function fetchLatestLiveDeskRelease(fetchImpl = globalThis.fetch) {
|
|
162
157
|
if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable for LiveDesk update checks.');
|
|
163
158
|
const [manager, client] = await Promise.all([
|
|
164
159
|
fetchLatestPackage('livedesk', fetchImpl),
|
|
165
160
|
fetchLatestPackage('@livedesk/client', fetchImpl)
|
|
166
161
|
]);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
162
|
+
const bundledClientVersion = cleanVersion(manager.package?.livedeskClientVersion);
|
|
163
|
+
if (!bundledClientVersion) {
|
|
164
|
+
throw new Error(`livedesk@${manager.version} is missing required livedeskClientVersion release metadata.`);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
latestManagerVersion: manager.version,
|
|
168
|
+
latestClientVersion: bundledClientVersion,
|
|
169
|
+
registryClientVersion: client.version,
|
|
170
|
+
releaseSkew: compareVersions(client.version, bundledClientVersion) === 0
|
|
171
|
+
? ''
|
|
172
|
+
: `livedesk@${manager.version} bundles Client ${bundledClientVersion}, while @livedesk/client latest is ${client.version}.`,
|
|
173
|
+
managerPackage: manager.package,
|
|
174
|
+
clientPackage: client.package,
|
|
175
|
+
checkedAt: new Date().toISOString()
|
|
176
|
+
};
|
|
177
|
+
}
|
|
175
178
|
|
|
176
|
-
function statusForRun(run) {
|
|
177
|
-
if (!run) return null;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
179
|
+
function statusForRun(run) {
|
|
180
|
+
if (!run) return null;
|
|
181
|
+
const completedCount = run.targets.filter(target => target.state === 'completed').length;
|
|
182
|
+
const failedCount = run.targets.filter(target => target.state === 'failed').length;
|
|
183
|
+
const queuedCount = run.targets.filter(target => target.state === 'queued').length;
|
|
184
|
+
const pendingOfflineCount = run.targets.filter(target => target.state === 'pending-offline').length;
|
|
185
|
+
const activeCount = run.targets.filter(target => target.state === 'waiting').length;
|
|
186
|
+
return {
|
|
187
|
+
operationId: run.operationId,
|
|
188
|
+
state: run.state,
|
|
189
|
+
startedAt: run.startedAt,
|
|
190
|
+
updatedAt: run.updatedAt,
|
|
191
|
+
targetCount: run.targets.length,
|
|
192
|
+
completedCount,
|
|
193
|
+
waitingCount: run.targets.length - completedCount - failedCount,
|
|
194
|
+
activeCount,
|
|
195
|
+
queuedCount,
|
|
196
|
+
pendingOfflineCount,
|
|
197
|
+
failedCount,
|
|
198
|
+
batchSize: run.batchSize,
|
|
199
|
+
clientRestartStabilityMs: run.clientRestartStabilityMs,
|
|
200
|
+
latestManagerVersion: run.latestManagerVersion,
|
|
201
|
+
latestClientVersion: run.latestClientVersion,
|
|
202
|
+
error: run.error || '',
|
|
203
|
+
targets: run.targets.map(target => ({ ...target }))
|
|
204
|
+
};
|
|
205
|
+
}
|
|
191
206
|
|
|
192
|
-
function connectedDevices(remoteHub) {
|
|
207
|
+
function connectedDevices(remoteHub) {
|
|
193
208
|
return remoteHub.listDevices({ includeDataUrl: false })
|
|
194
209
|
.filter(device => device.connected === true && device.synthetic !== true)
|
|
195
210
|
.slice(0, 500);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function knownOfflineDevices(remoteHub) {
|
|
214
|
+
return remoteHub.listDevices({ includeDataUrl: false })
|
|
215
|
+
.filter(device => device.connected !== true && device.synthetic !== true)
|
|
216
|
+
.slice(0, 500);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function deviceNeedsUpdate(device, latestManagerVersion, latestClientVersion) {
|
|
220
|
+
return !isVersionAtLeast(device?.productVersion, latestManagerVersion)
|
|
221
|
+
|| !isVersionAtLeast(device?.agentVersion, latestClientVersion);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function resetReconnectCandidate(target) {
|
|
225
|
+
target.candidateSessionId = '';
|
|
226
|
+
target.candidatePid = 0;
|
|
227
|
+
target.candidateConnectedAt = '';
|
|
228
|
+
target.candidateProductVersion = '';
|
|
229
|
+
target.candidateAgentVersion = '';
|
|
230
|
+
target.candidateSince = '';
|
|
231
|
+
target.stabilityDeadlineAt = '';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function createLiveDeskUpdateManager({
|
|
199
235
|
remoteHub,
|
|
200
236
|
currentManagerVersion,
|
|
201
|
-
currentClientVersion,
|
|
202
|
-
restartSupported = false,
|
|
203
|
-
requestHubRestart,
|
|
204
|
-
fetchImpl = globalThis.fetch,
|
|
205
|
-
now = () => Date.now()
|
|
206
|
-
|
|
237
|
+
currentClientVersion,
|
|
238
|
+
restartSupported = false,
|
|
239
|
+
requestHubRestart,
|
|
240
|
+
fetchImpl = globalThis.fetch,
|
|
241
|
+
now = () => Date.now(),
|
|
242
|
+
clientBatchSize = LIVE_DESK_UPDATE_CLIENT_BATCH_SIZE,
|
|
243
|
+
targetTimeoutMs = LIVE_DESK_UPDATE_TARGET_TIMEOUT_MS,
|
|
244
|
+
operationTimeoutMs = LIVE_DESK_UPDATE_TIMEOUT_MS,
|
|
245
|
+
clientRestartStabilityMs = LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS
|
|
246
|
+
}) {
|
|
207
247
|
let latestRelease = null;
|
|
208
248
|
let checkError = '';
|
|
209
249
|
let checkPromise = null;
|
|
210
|
-
let run = null;
|
|
211
|
-
let checkTimer = null;
|
|
212
|
-
let runTimer = null;
|
|
250
|
+
let run = null;
|
|
251
|
+
let checkTimer = null;
|
|
252
|
+
let runTimer = null;
|
|
253
|
+
const effectiveClientBatchSize = Math.max(
|
|
254
|
+
1,
|
|
255
|
+
Math.min(50, Math.floor(Number(clientBatchSize) || LIVE_DESK_UPDATE_CLIENT_BATCH_SIZE))
|
|
256
|
+
);
|
|
257
|
+
const effectiveTargetTimeoutMs = Math.max(
|
|
258
|
+
10_000,
|
|
259
|
+
Number(targetTimeoutMs) || LIVE_DESK_UPDATE_TARGET_TIMEOUT_MS
|
|
260
|
+
);
|
|
261
|
+
const effectiveOperationTimeoutMs = Math.max(
|
|
262
|
+
effectiveTargetTimeoutMs,
|
|
263
|
+
Number(operationTimeoutMs) || LIVE_DESK_UPDATE_TIMEOUT_MS
|
|
264
|
+
);
|
|
265
|
+
const effectiveClientRestartStabilityMs = Math.max(
|
|
266
|
+
0,
|
|
267
|
+
Number.isFinite(Number(clientRestartStabilityMs))
|
|
268
|
+
? Number(clientRestartStabilityMs)
|
|
269
|
+
: LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS
|
|
270
|
+
);
|
|
213
271
|
|
|
214
272
|
const touch = () => {
|
|
215
273
|
if (run) run.updatedAt = new Date(now()).toISOString();
|
|
@@ -234,45 +292,110 @@ export function createLiveDeskUpdateManager({
|
|
|
234
292
|
};
|
|
235
293
|
|
|
236
294
|
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
|
-
|
|
295
|
+
const managerUpdateAvailable = !!latestRelease
|
|
296
|
+
&& compareVersions(latestRelease.latestManagerVersion, currentManagerVersion) > 0;
|
|
297
|
+
const clientDevices = connectedDevices(remoteHub);
|
|
298
|
+
const outdatedClientDevices = latestRelease
|
|
299
|
+
? clientDevices.filter(device => deviceNeedsUpdate(
|
|
300
|
+
device,
|
|
301
|
+
latestRelease.latestManagerVersion,
|
|
302
|
+
latestRelease.latestClientVersion
|
|
303
|
+
))
|
|
304
|
+
: [];
|
|
305
|
+
const pendingOfflineClientCount = latestRelease
|
|
306
|
+
? knownOfflineDevices(remoteHub).filter(device => deviceNeedsUpdate(
|
|
307
|
+
device,
|
|
308
|
+
latestRelease.latestManagerVersion,
|
|
309
|
+
latestRelease.latestClientVersion
|
|
310
|
+
)).length
|
|
311
|
+
: 0;
|
|
312
|
+
const clientPackageUpdateAvailable = !!latestRelease
|
|
313
|
+
&& compareVersions(latestRelease.latestClientVersion, currentClientVersion) > 0;
|
|
245
314
|
const clientUpdateAvailable = clientPackageUpdateAvailable || outdatedClientDevices.length > 0;
|
|
246
315
|
const updateAvailable = managerUpdateAvailable || clientUpdateAvailable;
|
|
247
316
|
const activeRun = statusForRun(run);
|
|
248
317
|
return {
|
|
249
318
|
currentVersion: String(currentManagerVersion || ''),
|
|
250
319
|
currentClientVersion: String(currentClientVersion || ''),
|
|
251
|
-
latestVersion: latestRelease?.latestManagerVersion || '',
|
|
252
|
-
latestClientVersion: latestRelease?.latestClientVersion || '',
|
|
253
|
-
|
|
320
|
+
latestVersion: latestRelease?.latestManagerVersion || '',
|
|
321
|
+
latestClientVersion: latestRelease?.latestClientVersion || '',
|
|
322
|
+
registryClientVersion: latestRelease?.registryClientVersion || '',
|
|
323
|
+
releaseSkew: latestRelease?.releaseSkew || '',
|
|
324
|
+
updateAvailable,
|
|
254
325
|
managerUpdateAvailable,
|
|
255
326
|
clientUpdateAvailable,
|
|
256
|
-
clientPackageUpdateAvailable,
|
|
257
|
-
outdatedClientCount: outdatedClientDevices.length,
|
|
258
|
-
|
|
327
|
+
clientPackageUpdateAvailable,
|
|
328
|
+
outdatedClientCount: outdatedClientDevices.length,
|
|
329
|
+
pendingOfflineClientCount,
|
|
330
|
+
checkedAt: latestRelease?.checkedAt || '',
|
|
259
331
|
checkError,
|
|
260
332
|
restartSupported: !!restartSupported,
|
|
261
333
|
canApply: updateAvailable && (!managerUpdateAvailable && !clientPackageUpdateAvailable || !!restartSupported),
|
|
262
|
-
...(activeRun || {
|
|
334
|
+
...(activeRun || {
|
|
335
|
+
state: 'idle',
|
|
336
|
+
operationId: '',
|
|
337
|
+
targetCount: 0,
|
|
338
|
+
completedCount: 0,
|
|
339
|
+
waitingCount: 0,
|
|
340
|
+
activeCount: 0,
|
|
341
|
+
queuedCount: 0,
|
|
342
|
+
pendingOfflineCount: 0,
|
|
343
|
+
failedCount: 0,
|
|
344
|
+
batchSize: effectiveClientBatchSize,
|
|
345
|
+
clientRestartStabilityMs: effectiveClientRestartStabilityMs,
|
|
346
|
+
targets: []
|
|
347
|
+
})
|
|
263
348
|
};
|
|
264
349
|
};
|
|
265
350
|
|
|
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
|
-
|
|
351
|
+
const failRun = (message) => {
|
|
352
|
+
if (!run) return;
|
|
353
|
+
run.state = 'failed';
|
|
354
|
+
run.error = String(message || 'LiveDesk update failed.');
|
|
355
|
+
for (const target of run.targets) {
|
|
356
|
+
if (target.state === 'completed') continue;
|
|
357
|
+
target.state = 'failed';
|
|
358
|
+
if (!target.error) {
|
|
359
|
+
target.error = `Rollout stopped: ${run.error}`;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
touch();
|
|
363
|
+
if (runTimer) {
|
|
364
|
+
clearInterval(runTimer);
|
|
365
|
+
runTimer = null;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const reconcileHubRestartResult = (restartResult) => {
|
|
370
|
+
if (!run || run.state !== 'hub-restart-requested') return false;
|
|
371
|
+
const operationId = String(restartResult?.operationId || '').trim();
|
|
372
|
+
if (!operationId || operationId !== run.operationId) return false;
|
|
373
|
+
const stage = String(restartResult?.stage || '').trim();
|
|
374
|
+
const outcome = String(restartResult?.outcome || '').trim();
|
|
375
|
+
if (outcome !== 'failed' || !['failed', 'preflight-failed'].includes(stage)) return false;
|
|
376
|
+
failRun(
|
|
377
|
+
restartResult?.error
|
|
378
|
+
|| (stage === 'preflight-failed'
|
|
379
|
+
? 'Hub update restart preflight failed before shutdown.'
|
|
380
|
+
: 'Hub update restart failed.')
|
|
381
|
+
);
|
|
382
|
+
return true;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const retrySettlingTargets = () => {
|
|
386
|
+
if (!run || run.state !== 'failed') return [];
|
|
387
|
+
const currentTime = now();
|
|
388
|
+
const devices = new Map(
|
|
389
|
+
remoteHub.listDevices({ includeDataUrl: false })
|
|
390
|
+
.map(device => [String(device.deviceId), device])
|
|
391
|
+
);
|
|
392
|
+
return run.targets.filter(target => {
|
|
393
|
+
if (target.state !== 'failed' || !target.dispatchedAt) return false;
|
|
394
|
+
const deadline = Date.parse(target.deadlineAt || '');
|
|
395
|
+
if (!(deadline > currentTime)) return false;
|
|
396
|
+
return devices.get(target.deviceId)?.connected !== true;
|
|
397
|
+
});
|
|
398
|
+
};
|
|
276
399
|
|
|
277
400
|
const requestRestart = () => {
|
|
278
401
|
if (!restartSupported) {
|
|
@@ -281,8 +404,8 @@ export function createLiveDeskUpdateManager({
|
|
|
281
404
|
}
|
|
282
405
|
const result = requestHubRestart?.({
|
|
283
406
|
operationId: run?.operationId || '',
|
|
284
|
-
latestVersion: latestRelease?.latestManagerVersion || '',
|
|
285
|
-
latestClientVersion: latestRelease?.latestClientVersion || '',
|
|
407
|
+
latestVersion: run?.latestManagerVersion || latestRelease?.latestManagerVersion || '',
|
|
408
|
+
latestClientVersion: run?.latestClientVersion || latestRelease?.latestClientVersion || '',
|
|
286
409
|
// Current versions update every connected Client before the Hub exits.
|
|
287
410
|
// Older Hub versions did the reverse and set this flag themselves; the
|
|
288
411
|
// new server still supports that one-time migration path.
|
|
@@ -299,75 +422,214 @@ export function createLiveDeskUpdateManager({
|
|
|
299
422
|
return true;
|
|
300
423
|
};
|
|
301
424
|
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
const
|
|
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
|
-
|
|
425
|
+
const dispatchTarget = (target, device) => {
|
|
426
|
+
const dispatchedAtEpochMs = now();
|
|
427
|
+
const updateDeadlineEpochMs = dispatchedAtEpochMs + effectiveTargetTimeoutMs;
|
|
428
|
+
const dispatchedSessionId = String(device?.sessionId || '').trim();
|
|
429
|
+
if (!dispatchedSessionId) {
|
|
430
|
+
target.state = 'failed';
|
|
431
|
+
target.error = 'client-update-session-unavailable';
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
target.dispatchedSessionId = dispatchedSessionId;
|
|
435
|
+
target.dispatchedPid = Math.max(0, Math.floor(Number(device?.pid) || 0));
|
|
436
|
+
resetReconnectCandidate(target);
|
|
437
|
+
const supportsDedicated = device.capabilities?.clientUpdate === true
|
|
438
|
+
&& isVersionAtLeast(device.agentVersion, LIVE_DESK_DEDICATED_CLIENT_UPDATE_MIN_VERSION);
|
|
439
|
+
const payload = {
|
|
440
|
+
targetVersion: run.latestClientVersion,
|
|
441
|
+
targetProductVersion: run.latestManagerVersion,
|
|
442
|
+
managerVersion: run.latestManagerVersion,
|
|
443
|
+
operationId: run.operationId,
|
|
444
|
+
updateTimeoutMs: effectiveTargetTimeoutMs
|
|
445
|
+
};
|
|
446
|
+
const result = supportsDedicated
|
|
447
|
+
? remoteHub.sendCommand(device.deviceId, { command: LIVE_DESK_UPDATE_COMMAND, payload })
|
|
448
|
+
: remoteHub.sendLegacyClientUpdate(device.deviceId, {
|
|
449
|
+
command: buildLegacyUpdateCommand(
|
|
450
|
+
device,
|
|
451
|
+
run.operationId,
|
|
452
|
+
run.latestClientVersion,
|
|
453
|
+
run.latestManagerVersion,
|
|
454
|
+
effectiveTargetTimeoutMs
|
|
455
|
+
),
|
|
456
|
+
timeoutMs: 30_000
|
|
457
|
+
});
|
|
458
|
+
if (result?.ok !== true) {
|
|
459
|
+
const dispatchError = String(result?.error || 'client-update-dispatch-failed');
|
|
460
|
+
if (['device-not-connected', 'device-not-found'].includes(dispatchError)) {
|
|
461
|
+
target.state = 'pending-offline';
|
|
462
|
+
target.method = '';
|
|
463
|
+
target.commandId = '';
|
|
464
|
+
target.dispatchedAt = '';
|
|
465
|
+
target.deadlineAt = '';
|
|
466
|
+
target.dispatchedSessionId = '';
|
|
467
|
+
target.dispatchedPid = 0;
|
|
468
|
+
resetReconnectCandidate(target);
|
|
469
|
+
target.error = '';
|
|
470
|
+
return 'pending-offline';
|
|
471
|
+
}
|
|
472
|
+
target.state = 'failed';
|
|
473
|
+
target.error = dispatchError;
|
|
474
|
+
return false;
|
|
332
475
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
476
|
+
target.state = 'waiting';
|
|
477
|
+
target.method = supportsDedicated ? 'dedicated' : 'legacy-command-run';
|
|
478
|
+
target.commandId = String(result.commandId || result.taskId || '');
|
|
479
|
+
target.dispatchedAt = new Date(dispatchedAtEpochMs).toISOString();
|
|
480
|
+
target.deadlineAt = new Date(updateDeadlineEpochMs).toISOString();
|
|
481
|
+
return true;
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const dispatchQueuedTargets = () => {
|
|
485
|
+
if (!run || run.state !== 'waiting-for-clients') return true;
|
|
486
|
+
const activeCount = run.targets.filter(target => target.state === 'waiting').length;
|
|
487
|
+
let availableSlots = Math.max(0, run.batchSize - activeCount);
|
|
488
|
+
if (availableSlots <= 0) return true;
|
|
489
|
+
const devices = new Map(connectedDevices(remoteHub).map(device => [String(device.deviceId), device]));
|
|
490
|
+
for (const target of run.targets) {
|
|
491
|
+
if (availableSlots <= 0) break;
|
|
492
|
+
if (!['queued', 'pending-offline'].includes(target.state)) continue;
|
|
493
|
+
const device = devices.get(target.deviceId);
|
|
494
|
+
if (!device?.connected) {
|
|
495
|
+
target.state = 'pending-offline';
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
target.state = 'queued';
|
|
499
|
+
let dispatchResult;
|
|
500
|
+
try {
|
|
501
|
+
dispatchResult = dispatchTarget(target, device);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
target.state = 'failed';
|
|
504
|
+
target.error = String(error?.message || error || 'client-update-command-build-failed').slice(0, 500);
|
|
505
|
+
failRun(`Client ${target.deviceName || target.deviceId} could not be scheduled: ${target.error}`);
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
if (dispatchResult === false) {
|
|
509
|
+
failRun(`Client ${target.deviceName || target.deviceId} could not be scheduled: ${target.error}`);
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
if (dispatchResult === true) {
|
|
513
|
+
availableSlots -= 1;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
touch();
|
|
517
|
+
return true;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const verifyTargets = () => {
|
|
521
|
+
if (!run || run.state !== 'waiting-for-clients') return;
|
|
522
|
+
const currentTime = now();
|
|
523
|
+
if (currentTime - Date.parse(run.startedAt) >= effectiveOperationTimeoutMs) {
|
|
524
|
+
const unfinishedCount = run.targets.filter(target => target.state !== 'completed').length;
|
|
525
|
+
failRun(`Timed out waiting for ${unfinishedCount} client(s) to finish the rollout.`);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const devices = new Map(connectedDevices(remoteHub).map(device => [String(device.deviceId), device]));
|
|
529
|
+
for (const target of run.targets) {
|
|
530
|
+
if (target.state === 'failed' || target.state === 'completed') continue;
|
|
531
|
+
const device = devices.get(target.deviceId);
|
|
532
|
+
if (target.state === 'waiting') {
|
|
533
|
+
const sessionId = String(device?.sessionId || '').trim();
|
|
534
|
+
const connectedAt = String(device?.connectedAt || '');
|
|
535
|
+
const connectedAtEpochMs = Date.parse(connectedAt);
|
|
536
|
+
const dispatchedAtEpochMs = Date.parse(target.dispatchedAt || '');
|
|
537
|
+
const candidatePid = Math.max(0, Math.floor(Number(device?.pid) || 0));
|
|
538
|
+
const candidateProductVersion = String(device?.productVersion || '');
|
|
539
|
+
const candidateAgentVersion = String(device?.agentVersion || '');
|
|
540
|
+
const qualifiesForStability = device?.connected === true
|
|
541
|
+
&& !!sessionId
|
|
542
|
+
&& candidatePid > 1
|
|
543
|
+
&& sessionId !== target.dispatchedSessionId
|
|
544
|
+
&& connectedAtEpochMs >= dispatchedAtEpochMs
|
|
545
|
+
&& !deviceNeedsUpdate(device, run.latestManagerVersion, run.latestClientVersion);
|
|
546
|
+
const sameCandidate = qualifiesForStability
|
|
547
|
+
&& target.candidateSessionId === sessionId
|
|
548
|
+
&& target.candidatePid === candidatePid
|
|
549
|
+
&& target.candidateConnectedAt === connectedAt
|
|
550
|
+
&& target.candidateProductVersion === candidateProductVersion
|
|
551
|
+
&& target.candidateAgentVersion === candidateAgentVersion;
|
|
552
|
+
|
|
553
|
+
if (!qualifiesForStability) {
|
|
554
|
+
resetReconnectCandidate(target);
|
|
555
|
+
} else if (!sameCandidate) {
|
|
556
|
+
target.candidateSessionId = sessionId;
|
|
557
|
+
target.candidatePid = candidatePid;
|
|
558
|
+
target.candidateConnectedAt = connectedAt;
|
|
559
|
+
target.candidateProductVersion = candidateProductVersion;
|
|
560
|
+
target.candidateAgentVersion = candidateAgentVersion;
|
|
561
|
+
target.candidateSince = new Date(currentTime).toISOString();
|
|
562
|
+
target.stabilityDeadlineAt = new Date(
|
|
563
|
+
currentTime + run.clientRestartStabilityMs
|
|
564
|
+
).toISOString();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (qualifiesForStability
|
|
568
|
+
&& target.candidateSessionId === sessionId
|
|
569
|
+
&& currentTime >= Date.parse(target.stabilityDeadlineAt || '')) {
|
|
570
|
+
target.state = 'completed';
|
|
571
|
+
target.currentVersion = String(device.agentVersion || '');
|
|
572
|
+
target.currentProductVersion = String(device.productVersion || '');
|
|
573
|
+
target.completedAt = new Date(currentTime).toISOString();
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (target.state === 'waiting'
|
|
578
|
+
&& Date.parse(target.deadlineAt || '') > 0
|
|
579
|
+
&& currentTime >= Date.parse(target.deadlineAt)) {
|
|
580
|
+
target.state = 'failed';
|
|
581
|
+
target.error = `Timed out waiting for product ${run.latestManagerVersion} / Client ${run.latestClientVersion}.`;
|
|
582
|
+
failRun(`Client ${target.deviceName || target.deviceId} did not restart on the requested versions within ${Math.round(effectiveTargetTimeoutMs / 1000)} seconds.`);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (target.state === 'pending-offline' && device?.connected === true) {
|
|
586
|
+
target.state = 'queued';
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (!dispatchQueuedTargets()) return;
|
|
590
|
+
touch();
|
|
591
|
+
if (run.targets.every(target => target.state === 'completed')) {
|
|
592
|
+
if (runTimer) {
|
|
593
|
+
clearInterval(runTimer);
|
|
594
|
+
runTimer = null;
|
|
595
|
+
}
|
|
596
|
+
if (run.needsHubRestart) {
|
|
597
|
+
requestRestart();
|
|
598
|
+
} else {
|
|
599
|
+
run.state = 'clients-updated';
|
|
600
|
+
touch();
|
|
601
|
+
}
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const startUpdate = async () => {
|
|
607
|
+
if (run && ['dispatching', 'waiting-for-clients', 'hub-restart-requested'].includes(run.state)) {
|
|
608
|
+
return { ok: true, ...getStatus() };
|
|
609
|
+
}
|
|
610
|
+
const settlingTargets = retrySettlingTargets();
|
|
611
|
+
if (settlingTargets.length > 0) {
|
|
612
|
+
const names = settlingTargets
|
|
613
|
+
.slice(0, 3)
|
|
614
|
+
.map(target => target.deviceName || target.deviceId)
|
|
615
|
+
.join(', ');
|
|
616
|
+
const remaining = settlingTargets.length > 3 ? ` and ${settlingTargets.length - 3} more` : '';
|
|
617
|
+
return {
|
|
618
|
+
...getStatus(),
|
|
619
|
+
ok: false,
|
|
620
|
+
error: `The previous rollout is still settling for offline Client(s): ${names}${remaining}. Retry after they reconnect or their restart deadline passes.`
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
const release = latestRelease || await checkLatest();
|
|
366
624
|
if (!release) return { ok: false, error: checkError || 'LiveDesk update check failed.', ...getStatus() };
|
|
367
625
|
const managerNeedsUpdate = compareVersions(release.latestManagerVersion, currentManagerVersion) > 0;
|
|
368
626
|
const clientPackageNeedsUpdate = compareVersions(release.latestClientVersion, currentClientVersion) > 0;
|
|
369
627
|
const connected = connectedDevices(remoteHub);
|
|
370
|
-
const outdatedClients = connected.filter(device =>
|
|
628
|
+
const outdatedClients = connected.filter(device => deviceNeedsUpdate(
|
|
629
|
+
device,
|
|
630
|
+
release.latestManagerVersion,
|
|
631
|
+
release.latestClientVersion
|
|
632
|
+
));
|
|
371
633
|
const needsHubRestart = managerNeedsUpdate || clientPackageNeedsUpdate;
|
|
372
634
|
if (!managerNeedsUpdate && !clientPackageNeedsUpdate && outdatedClients.length === 0) {
|
|
373
635
|
return { ok: true, state: 'clients-updated', ...getStatus() };
|
|
@@ -376,57 +638,77 @@ export function createLiveDeskUpdateManager({
|
|
|
376
638
|
return { ok: false, error: 'Hub launcher restart is unavailable. Start LiveDesk through npx livedesk@latest.', ...getStatus() };
|
|
377
639
|
}
|
|
378
640
|
const targets = outdatedClients;
|
|
379
|
-
|
|
380
|
-
run = {
|
|
641
|
+
run = {
|
|
381
642
|
operationId: randomUUID(),
|
|
382
643
|
state: 'dispatching',
|
|
383
644
|
startedAt: new Date(now()).toISOString(),
|
|
384
645
|
updatedAt: new Date(now()).toISOString(),
|
|
385
646
|
latestManagerVersion: release.latestManagerVersion,
|
|
386
|
-
latestClientVersion: release.latestClientVersion,
|
|
647
|
+
latestClientVersion: release.latestClientVersion,
|
|
387
648
|
needsHubRestart,
|
|
388
|
-
|
|
389
|
-
|
|
649
|
+
batchSize: effectiveClientBatchSize,
|
|
650
|
+
clientRestartStabilityMs: effectiveClientRestartStabilityMs,
|
|
651
|
+
error: '',
|
|
652
|
+
targets: targets.map(device => ({
|
|
390
653
|
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
|
-
|
|
654
|
+
deviceName: String(device.deviceName || device.hostname || device.deviceId || ''),
|
|
655
|
+
oldVersion: String(device.agentVersion || ''),
|
|
656
|
+
oldProductVersion: String(device.productVersion || ''),
|
|
657
|
+
currentVersion: '',
|
|
658
|
+
currentProductVersion: '',
|
|
659
|
+
state: 'queued',
|
|
660
|
+
method: '',
|
|
661
|
+
commandId: '',
|
|
662
|
+
dispatchedAt: '',
|
|
663
|
+
deadlineAt: '',
|
|
664
|
+
dispatchedSessionId: '',
|
|
665
|
+
dispatchedPid: 0,
|
|
666
|
+
candidateSessionId: '',
|
|
667
|
+
candidatePid: 0,
|
|
668
|
+
candidateConnectedAt: '',
|
|
669
|
+
candidateProductVersion: '',
|
|
670
|
+
candidateAgentVersion: '',
|
|
671
|
+
candidateSince: '',
|
|
672
|
+
stabilityDeadlineAt: '',
|
|
673
|
+
completedAt: '',
|
|
674
|
+
error: ''
|
|
675
|
+
}))
|
|
676
|
+
};
|
|
677
|
+
touch();
|
|
678
|
+
if (run.targets.length === 0) {
|
|
679
|
+
if (run.needsHubRestart) {
|
|
680
|
+
if (!requestRestart()) {
|
|
681
|
+
return { ok: false, error: run.error, ...getStatus() };
|
|
682
|
+
}
|
|
683
|
+
} else {
|
|
412
684
|
run.state = 'clients-updated';
|
|
413
685
|
touch();
|
|
414
686
|
}
|
|
415
687
|
return { ok: true, ...getStatus() };
|
|
416
|
-
}
|
|
417
|
-
run.state = 'waiting-for-clients';
|
|
418
|
-
|
|
688
|
+
}
|
|
689
|
+
run.state = 'waiting-for-clients';
|
|
690
|
+
if (!dispatchQueuedTargets()) {
|
|
691
|
+
return { ok: false, error: run.error, ...getStatus() };
|
|
692
|
+
}
|
|
693
|
+
runTimer = setInterval(verifyTargets, 1000);
|
|
419
694
|
runTimer.unref?.();
|
|
420
695
|
verifyTargets();
|
|
421
696
|
return { ok: true, ...getStatus() };
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
const handleRemoteEvent = (type, event) => {
|
|
425
|
-
if (!run || run.state !== 'waiting-for-clients'
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const handleRemoteEvent = (type, event) => {
|
|
700
|
+
if (!run || run.state !== 'waiting-for-clients') return;
|
|
701
|
+
if (type === 'RemoteDeviceConnected' || type === 'RemoteDeviceDisconnected') {
|
|
702
|
+
verifyTargets();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (type !== 'RemoteCommandResult') return;
|
|
426
706
|
const deviceId = String(event?.device?.deviceId || '');
|
|
427
707
|
const commandId = String(event?.commandId || '');
|
|
428
708
|
const target = run.targets.find(item => item.deviceId === deviceId && item.commandId === commandId);
|
|
429
|
-
if (!target
|
|
709
|
+
if (!target
|
|
710
|
+
|| target.state !== 'waiting'
|
|
711
|
+
|| !['dedicated', 'legacy-command-run'].includes(target.method)) return;
|
|
430
712
|
const result = event?.result;
|
|
431
713
|
if (event?.error || result?.ok === false || result?.status === 'failed' || result?.status === 'rejected') {
|
|
432
714
|
target.state = 'failed';
|
|
@@ -444,10 +726,11 @@ export function createLiveDeskUpdateManager({
|
|
|
444
726
|
|
|
445
727
|
return {
|
|
446
728
|
checkLatest,
|
|
447
|
-
getStatus,
|
|
448
|
-
startUpdate,
|
|
449
|
-
handleRemoteEvent,
|
|
450
|
-
|
|
729
|
+
getStatus,
|
|
730
|
+
startUpdate,
|
|
731
|
+
handleRemoteEvent,
|
|
732
|
+
reconcileHubRestartResult,
|
|
733
|
+
close() {
|
|
451
734
|
if (checkTimer) clearInterval(checkTimer);
|
|
452
735
|
if (runTimer) clearInterval(runTimer);
|
|
453
736
|
checkTimer = null;
|