@livedesk/hub 0.1.36 → 0.1.37
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 +32 -32
- package/src/agents/agent-audit-store.js +6 -16
- package/src/agents/agent-permissions.js +27 -33
- package/src/agents/agent-tool-registry.js +322 -340
- package/src/captures/capture-store.js +252 -299
- package/src/filesystem/shared-folders.js +181 -189
- package/src/filesystem/transfer-jobs.js +314 -345
- package/src/live-desk-update.js +615 -683
- package/src/remote-hub.js +59 -643
- package/src/server.js +111 -957
- package/src/settings/settings-schema.js +40 -32
- package/src/settings/settings-store.js +2 -7
- package/src/transport/relay-hub-control.js +1376 -1703
- package/src/transport/udp-hub-transport.js +520 -543
- package/src/transport/udp-rendezvous.js +408 -574
- package/src/security/device-credential-authority.js +0 -406
- package/src/security/security-audit-store.js +0 -260
- package/src/transport/secure-direct-acceptor.js +0 -543
package/src/live-desk-update.js
CHANGED
|
@@ -1,317 +1,273 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// 0.1.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
export const
|
|
11
|
-
export const
|
|
12
|
-
export const
|
|
13
|
-
export const
|
|
14
|
-
export const
|
|
15
|
-
export const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (left.length === 0
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return comparePrerelease(a.prerelease, b.prerelease);
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
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;
|
|
16
|
+
|
|
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);
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
export function isVersionAtLeast(candidate, required) {
|
|
66
65
|
return !!cleanVersion(candidate) && !!cleanVersion(required) && compareVersions(candidate, required) >= 0;
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
function encodePowerShell(value) {
|
|
70
|
-
return Buffer.from(String(value || ''), 'utf16le').toString('base64');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function buildLegacyPackageSupervisorStarterSource() {
|
|
74
|
-
const source = [
|
|
75
|
-
"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','--prefix',w,'--workspaces=false','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);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_workspaces:'false',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});",
|
|
76
|
-
"e.r.on('error',()=>process.exit(1));e.r.unref();"
|
|
77
|
-
].join('');
|
|
78
|
-
if (source.includes(' ')) throw new Error('LiveDesk legacy starter source must not contain spaces.');
|
|
79
|
-
return source;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function assertLegacyCommandLength(command, platform) {
|
|
83
|
-
if (command.length > LIVE_DESK_LEGACY_COMMAND_MAX_LENGTH) {
|
|
84
|
-
throw new Error(
|
|
85
|
-
`LiveDesk legacy ${platform} update command is ${command.length} characters; `
|
|
86
|
-
+ `the compatibility limit is ${LIVE_DESK_LEGACY_COMMAND_MAX_LENGTH}.`
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
return command;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function buildLegacyPlatformCommand(platform, payload) {
|
|
93
|
-
const environment = [
|
|
94
|
-
String(payload.operationId || ''),
|
|
95
|
-
String(payload.deviceId || ''),
|
|
96
|
-
String(Number(payload.agentPid) || 0),
|
|
97
|
-
cleanVersion(payload.currentProductVersion),
|
|
98
|
-
cleanVersion(payload.currentAgentVersion),
|
|
99
|
-
cleanVersion(payload.targetVersion),
|
|
100
|
-
cleanVersion(payload.targetProductVersion),
|
|
101
|
-
String(Math.floor(Number(payload.updateTimeoutMs) || 0))
|
|
102
|
-
];
|
|
103
|
-
const configBase64 = Buffer.from(JSON.stringify(environment), 'utf8').toString('base64');
|
|
104
|
-
const starter = buildLegacyPackageSupervisorStarterSource();
|
|
105
|
-
const starterBase64 = Buffer.from(starter, 'utf8').toString('base64');
|
|
106
|
-
const windows = ['win32', 'windows'].includes(String(platform || '').toLowerCase());
|
|
107
|
-
const powerShellStarter = [
|
|
108
|
-
"$ErrorActionPreference='Stop'",
|
|
109
|
-
'$n=[string]$env:LIVEDESK_NODE_EXECUTABLE',
|
|
110
|
-
'if(-not $n){$n=(Get-Command node.exe -ErrorAction SilentlyContinue).Source}',
|
|
111
|
-
"if(-not $n){throw 'LiveDesk node executable was not found.'}",
|
|
112
|
-
"$j=\"eval(Buffer.from(process.env.S,'base64').toString('utf8'))\"",
|
|
113
|
-
"$p=Start-Process -FilePath $n -ArgumentList @('-e',$j) -WindowStyle Hidden -PassThru",
|
|
114
|
-
"if(-not $p){throw 'LiveDesk update starter failed.'}"
|
|
115
|
-
].join(';');
|
|
116
|
-
const command = windows
|
|
117
|
-
? `set C=${configBase64}&set S=${starterBase64}&powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodePowerShell(powerShellStarter)}`
|
|
118
|
-
: `C='${configBase64}' S='${starterBase64}' "${'${LIVEDESK_NODE_EXECUTABLE:-node}'}" -e 'eval(Buffer.from(process.env.S,"base64").toString("utf8"))'`;
|
|
119
|
-
return assertLegacyCommandLength(command, windows ? 'Windows' : 'Unix');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function buildLegacyUpdateCommand(
|
|
123
|
-
device,
|
|
124
|
-
operationId,
|
|
125
|
-
targetVersion,
|
|
126
|
-
targetProductVersion,
|
|
127
|
-
updateTimeoutMs
|
|
128
|
-
) {
|
|
129
|
-
const payload = {
|
|
130
|
-
operationId,
|
|
131
|
-
deviceId: device.deviceId || '',
|
|
132
|
-
agentPid: device.pid || 0,
|
|
133
|
-
currentProductVersion: device.productVersion || '',
|
|
134
|
-
currentAgentVersion: device.agentVersion || '',
|
|
135
|
-
targetVersion,
|
|
136
|
-
targetProductVersion,
|
|
137
|
-
updateTimeoutMs
|
|
138
|
-
};
|
|
139
|
-
return buildLegacyPlatformCommand(device.platform, payload);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function
|
|
143
|
-
const encodedName = packageName.startsWith('@') ? packageName.replace('/', '%2F') : packageName;
|
|
144
|
-
const
|
|
145
|
-
const response = await fetchImpl(`https://registry.npmjs.org/${encodedName}/${encodedVersion}`, {
|
|
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','--prefix',w,'--workspaces=false','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);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_workspaces:'false',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
|
+
}
|
|
140
|
+
|
|
141
|
+
async function fetchLatestPackage(packageName, fetchImpl) {
|
|
142
|
+
const encodedName = packageName.startsWith('@') ? packageName.replace('/', '%2F') : packageName;
|
|
143
|
+
const response = await fetchImpl(`https://registry.npmjs.org/${encodedName}/latest`, {
|
|
146
144
|
headers: { Accept: 'application/json' },
|
|
147
145
|
signal: AbortSignal.timeout(12_000)
|
|
148
146
|
});
|
|
149
147
|
if (!response.ok) {
|
|
150
|
-
throw new Error(`npm registry returned HTTP ${response.status} for ${packageName}
|
|
148
|
+
throw new Error(`npm registry returned HTTP ${response.status} for ${packageName}.`);
|
|
151
149
|
}
|
|
152
150
|
const payload = await response.json();
|
|
153
|
-
const
|
|
154
|
-
if (!
|
|
155
|
-
return { version
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
managerPackage: manager.package,
|
|
211
|
-
clientPackage: client.package,
|
|
212
|
-
checkedAt: new Date(now()).toISOString(),
|
|
213
|
-
manifestVerified: true,
|
|
214
|
-
manifestGeneratedAt: manifest.generatedAt,
|
|
215
|
-
manifestExpiresAt: manifest.expiresAt,
|
|
216
|
-
blockedVersions: manifest.blockedVersions
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function statusForRun(run) {
|
|
221
|
-
if (!run) return null;
|
|
222
|
-
const completedCount = run.targets.filter(target => target.state === 'completed').length;
|
|
223
|
-
const failedCount = run.targets.filter(target => target.state === 'failed').length;
|
|
224
|
-
const queuedCount = run.targets.filter(target => target.state === 'queued').length;
|
|
225
|
-
const pendingOfflineCount = run.targets.filter(target => target.state === 'pending-offline').length;
|
|
226
|
-
const activeCount = run.targets.filter(target => target.state === 'waiting').length;
|
|
227
|
-
return {
|
|
228
|
-
operationId: run.operationId,
|
|
229
|
-
state: run.state,
|
|
230
|
-
startedAt: run.startedAt,
|
|
231
|
-
updatedAt: run.updatedAt,
|
|
232
|
-
targetCount: run.targets.length,
|
|
233
|
-
completedCount,
|
|
234
|
-
waitingCount: run.targets.length - completedCount - failedCount,
|
|
235
|
-
activeCount,
|
|
236
|
-
queuedCount,
|
|
237
|
-
pendingOfflineCount,
|
|
238
|
-
failedCount,
|
|
239
|
-
batchSize: run.batchSize,
|
|
240
|
-
clientRestartStabilityMs: run.clientRestartStabilityMs,
|
|
241
|
-
latestManagerVersion: run.latestManagerVersion,
|
|
242
|
-
latestClientVersion: run.latestClientVersion,
|
|
243
|
-
error: run.error || '',
|
|
244
|
-
targets: run.targets.map(target => ({ ...target }))
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function connectedDevices(remoteHub) {
|
|
151
|
+
const version = cleanVersion(payload?.version);
|
|
152
|
+
if (!version) throw new Error(`npm registry returned no version for ${packageName}.`);
|
|
153
|
+
return { version, package: payload };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function fetchLatestLiveDeskRelease(fetchImpl = globalThis.fetch) {
|
|
157
|
+
if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable for LiveDesk update checks.');
|
|
158
|
+
const [manager, client] = await Promise.all([
|
|
159
|
+
fetchLatestPackage('livedesk', fetchImpl),
|
|
160
|
+
fetchLatestPackage('@livedesk/client', fetchImpl)
|
|
161
|
+
]);
|
|
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
|
+
}
|
|
178
|
+
|
|
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
|
+
}
|
|
206
|
+
|
|
207
|
+
function connectedDevices(remoteHub) {
|
|
249
208
|
return remoteHub.listDevices({ includeDataUrl: false })
|
|
250
209
|
.filter(device => device.connected === true && device.synthetic !== true)
|
|
251
210
|
.slice(0, 500);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function knownOfflineDevices(remoteHub) {
|
|
255
|
-
return remoteHub.listDevices({ includeDataUrl: false })
|
|
256
|
-
.filter(device => device.connected !== true && device.synthetic !== true)
|
|
257
|
-
.slice(0, 500);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function deviceNeedsUpdate(device, latestManagerVersion, latestClientVersion) {
|
|
261
|
-
return !isVersionAtLeast(device?.productVersion, latestManagerVersion)
|
|
262
|
-
|| !isVersionAtLeast(device?.agentVersion, latestClientVersion);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function resetReconnectCandidate(target) {
|
|
266
|
-
target.candidateSessionId = '';
|
|
267
|
-
target.candidatePid = 0;
|
|
268
|
-
target.candidateConnectedAt = '';
|
|
269
|
-
target.candidateProductVersion = '';
|
|
270
|
-
target.candidateAgentVersion = '';
|
|
271
|
-
target.candidateSince = '';
|
|
272
|
-
target.stabilityDeadlineAt = '';
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export function createLiveDeskUpdateManager({
|
|
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({
|
|
276
235
|
remoteHub,
|
|
277
236
|
currentManagerVersion,
|
|
278
|
-
currentClientVersion,
|
|
279
|
-
restartSupported = false,
|
|
280
|
-
requestHubRestart,
|
|
281
|
-
fetchImpl = globalThis.fetch,
|
|
282
|
-
now = () => Date.now(),
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
operationTimeoutMs = LIVE_DESK_UPDATE_TIMEOUT_MS,
|
|
289
|
-
clientRestartStabilityMs = LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS
|
|
290
|
-
}) {
|
|
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
|
+
}) {
|
|
291
247
|
let latestRelease = null;
|
|
292
248
|
let checkError = '';
|
|
293
249
|
let checkPromise = null;
|
|
294
|
-
let run = null;
|
|
295
|
-
let checkTimer = null;
|
|
296
|
-
let runTimer = null;
|
|
297
|
-
const effectiveClientBatchSize = Math.max(
|
|
298
|
-
1,
|
|
299
|
-
Math.min(50, Math.floor(Number(clientBatchSize) || LIVE_DESK_UPDATE_CLIENT_BATCH_SIZE))
|
|
300
|
-
);
|
|
301
|
-
const effectiveTargetTimeoutMs = Math.max(
|
|
302
|
-
10_000,
|
|
303
|
-
Number(targetTimeoutMs) || LIVE_DESK_UPDATE_TARGET_TIMEOUT_MS
|
|
304
|
-
);
|
|
305
|
-
const effectiveOperationTimeoutMs = Math.max(
|
|
306
|
-
effectiveTargetTimeoutMs,
|
|
307
|
-
Number(operationTimeoutMs) || LIVE_DESK_UPDATE_TIMEOUT_MS
|
|
308
|
-
);
|
|
309
|
-
const effectiveClientRestartStabilityMs = Math.max(
|
|
310
|
-
0,
|
|
311
|
-
Number.isFinite(Number(clientRestartStabilityMs))
|
|
312
|
-
? Number(clientRestartStabilityMs)
|
|
313
|
-
: LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS
|
|
314
|
-
);
|
|
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
|
+
);
|
|
315
271
|
|
|
316
272
|
const touch = () => {
|
|
317
273
|
if (run) run.updatedAt = new Date(now()).toISOString();
|
|
@@ -319,30 +275,9 @@ export function createLiveDeskUpdateManager({
|
|
|
319
275
|
|
|
320
276
|
const checkLatest = async () => {
|
|
321
277
|
if (checkPromise) return checkPromise;
|
|
322
|
-
checkPromise = fetchLatestLiveDeskRelease(fetchImpl
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
now,
|
|
326
|
-
allowUnsignedRegistryForTests
|
|
327
|
-
})
|
|
328
|
-
.then(release => {
|
|
329
|
-
if (cleanVersion(currentManagerVersion)
|
|
330
|
-
&& compareVersions(release.latestManagerVersion, currentManagerVersion) < 0) {
|
|
331
|
-
throw new Error('signed-update-manager-downgrade-rejected');
|
|
332
|
-
}
|
|
333
|
-
if (cleanVersion(currentClientVersion)
|
|
334
|
-
&& compareVersions(release.latestClientVersion, currentClientVersion) < 0) {
|
|
335
|
-
throw new Error('signed-update-client-downgrade-rejected');
|
|
336
|
-
}
|
|
337
|
-
if (release.blockedVersions?.manager?.includes(cleanVersion(currentManagerVersion))
|
|
338
|
-
&& compareVersions(release.latestManagerVersion, currentManagerVersion) <= 0) {
|
|
339
|
-
throw new Error('signed-update-blocked-manager-without-safe-upgrade');
|
|
340
|
-
}
|
|
341
|
-
if (release.blockedVersions?.client?.includes(cleanVersion(currentClientVersion))
|
|
342
|
-
&& compareVersions(release.latestClientVersion, currentClientVersion) <= 0) {
|
|
343
|
-
throw new Error('signed-update-blocked-client-without-safe-upgrade');
|
|
344
|
-
}
|
|
345
|
-
latestRelease = release;
|
|
278
|
+
checkPromise = fetchLatestLiveDeskRelease(fetchImpl)
|
|
279
|
+
.then(release => {
|
|
280
|
+
latestRelease = release;
|
|
346
281
|
checkError = '';
|
|
347
282
|
return release;
|
|
348
283
|
})
|
|
@@ -357,127 +292,124 @@ export function createLiveDeskUpdateManager({
|
|
|
357
292
|
};
|
|
358
293
|
|
|
359
294
|
const getStatus = () => {
|
|
360
|
-
const managerUpdateAvailable = !!latestRelease
|
|
361
|
-
&& compareVersions(latestRelease.latestManagerVersion, currentManagerVersion) > 0;
|
|
362
|
-
const clientDevices = connectedDevices(remoteHub);
|
|
363
|
-
const outdatedClientDevices = latestRelease
|
|
364
|
-
? clientDevices.filter(device => deviceNeedsUpdate(
|
|
365
|
-
device,
|
|
366
|
-
latestRelease.latestManagerVersion,
|
|
367
|
-
latestRelease.latestClientVersion
|
|
368
|
-
))
|
|
369
|
-
: [];
|
|
370
|
-
const pendingOfflineClientCount = latestRelease
|
|
371
|
-
? knownOfflineDevices(remoteHub).filter(device => deviceNeedsUpdate(
|
|
372
|
-
device,
|
|
373
|
-
latestRelease.latestManagerVersion,
|
|
374
|
-
latestRelease.latestClientVersion
|
|
375
|
-
)).length
|
|
376
|
-
: 0;
|
|
377
|
-
const clientPackageUpdateAvailable = !!latestRelease
|
|
378
|
-
&& compareVersions(latestRelease.latestClientVersion, currentClientVersion) > 0;
|
|
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;
|
|
379
314
|
const clientUpdateAvailable = clientPackageUpdateAvailable || outdatedClientDevices.length > 0;
|
|
380
315
|
const updateAvailable = managerUpdateAvailable || clientUpdateAvailable;
|
|
381
316
|
const activeRun = statusForRun(run);
|
|
382
317
|
return {
|
|
383
318
|
currentVersion: String(currentManagerVersion || ''),
|
|
384
319
|
currentClientVersion: String(currentClientVersion || ''),
|
|
385
|
-
latestVersion: latestRelease?.latestManagerVersion || '',
|
|
386
|
-
latestClientVersion: latestRelease?.latestClientVersion || '',
|
|
387
|
-
registryClientVersion: latestRelease?.registryClientVersion || '',
|
|
388
|
-
releaseSkew: latestRelease?.releaseSkew || '',
|
|
389
|
-
|
|
390
|
-
manifestGeneratedAt: latestRelease?.manifestGeneratedAt || '',
|
|
391
|
-
manifestExpiresAt: latestRelease?.manifestExpiresAt || '',
|
|
392
|
-
updateAvailable,
|
|
320
|
+
latestVersion: latestRelease?.latestManagerVersion || '',
|
|
321
|
+
latestClientVersion: latestRelease?.latestClientVersion || '',
|
|
322
|
+
registryClientVersion: latestRelease?.registryClientVersion || '',
|
|
323
|
+
releaseSkew: latestRelease?.releaseSkew || '',
|
|
324
|
+
updateAvailable,
|
|
393
325
|
managerUpdateAvailable,
|
|
394
326
|
clientUpdateAvailable,
|
|
395
|
-
clientPackageUpdateAvailable,
|
|
396
|
-
outdatedClientCount: outdatedClientDevices.length,
|
|
397
|
-
pendingOfflineClientCount,
|
|
398
|
-
checkedAt: latestRelease?.checkedAt || '',
|
|
327
|
+
clientPackageUpdateAvailable,
|
|
328
|
+
outdatedClientCount: outdatedClientDevices.length,
|
|
329
|
+
pendingOfflineClientCount,
|
|
330
|
+
checkedAt: latestRelease?.checkedAt || '',
|
|
399
331
|
checkError,
|
|
400
332
|
restartSupported: !!restartSupported,
|
|
401
333
|
canApply: updateAvailable && (!managerUpdateAvailable && !clientPackageUpdateAvailable || !!restartSupported),
|
|
402
|
-
...(activeRun || {
|
|
403
|
-
state: 'idle',
|
|
404
|
-
operationId: '',
|
|
405
|
-
targetCount: 0,
|
|
406
|
-
completedCount: 0,
|
|
407
|
-
waitingCount: 0,
|
|
408
|
-
activeCount: 0,
|
|
409
|
-
queuedCount: 0,
|
|
410
|
-
pendingOfflineCount: 0,
|
|
411
|
-
failedCount: 0,
|
|
412
|
-
batchSize: effectiveClientBatchSize,
|
|
413
|
-
clientRestartStabilityMs: effectiveClientRestartStabilityMs,
|
|
414
|
-
targets: []
|
|
415
|
-
})
|
|
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
|
+
})
|
|
416
348
|
};
|
|
417
349
|
};
|
|
418
350
|
|
|
419
|
-
const failRun = (message) => {
|
|
420
|
-
if (!run) return;
|
|
421
|
-
run.state = 'failed';
|
|
422
|
-
run.error = String(message || 'LiveDesk update failed.');
|
|
423
|
-
for (const target of run.targets) {
|
|
424
|
-
if (target.state === 'completed') continue;
|
|
425
|
-
target.state = 'failed';
|
|
426
|
-
if (!target.error) {
|
|
427
|
-
target.error = `Rollout stopped: ${run.error}`;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
touch();
|
|
431
|
-
if (runTimer) {
|
|
432
|
-
clearInterval(runTimer);
|
|
433
|
-
runTimer = null;
|
|
434
|
-
}
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
const reconcileHubRestartResult = (restartResult) => {
|
|
438
|
-
if (!run || run.state !== 'hub-restart-requested') return false;
|
|
439
|
-
const operationId = String(restartResult?.operationId || '').trim();
|
|
440
|
-
if (!operationId || operationId !== run.operationId) return false;
|
|
441
|
-
const stage = String(restartResult?.stage || '').trim();
|
|
442
|
-
const outcome = String(restartResult?.outcome || '').trim();
|
|
443
|
-
if (outcome !== 'failed' || !['failed', 'preflight-failed'].includes(stage)) return false;
|
|
444
|
-
failRun(
|
|
445
|
-
restartResult?.error
|
|
446
|
-
|| (stage === 'preflight-failed'
|
|
447
|
-
? 'Hub update restart preflight failed before shutdown.'
|
|
448
|
-
: 'Hub update restart failed.')
|
|
449
|
-
);
|
|
450
|
-
return true;
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
const retrySettlingTargets = () => {
|
|
454
|
-
if (!run || run.state !== 'failed') return [];
|
|
455
|
-
const currentTime = now();
|
|
456
|
-
const devices = new Map(
|
|
457
|
-
remoteHub.listDevices({ includeDataUrl: false })
|
|
458
|
-
.map(device => [String(device.deviceId), device])
|
|
459
|
-
);
|
|
460
|
-
return run.targets.filter(target => {
|
|
461
|
-
if (target.state !== 'failed' || !target.dispatchedAt) return false;
|
|
462
|
-
const deadline = Date.parse(target.deadlineAt || '');
|
|
463
|
-
if (!(deadline > currentTime)) return false;
|
|
464
|
-
return devices.get(target.deviceId)?.connected !== true;
|
|
465
|
-
});
|
|
466
|
-
};
|
|
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
|
+
};
|
|
467
399
|
|
|
468
400
|
const requestRestart = () => {
|
|
469
401
|
if (!restartSupported) {
|
|
470
402
|
failRun('Hub launcher restart is unavailable. Start LiveDesk through npx livedesk@latest.');
|
|
471
403
|
return false;
|
|
472
404
|
}
|
|
473
|
-
const result = requestHubRestart?.({
|
|
474
|
-
operationId: run?.operationId || '',
|
|
475
|
-
latestVersion: run?.latestManagerVersion || latestRelease?.latestManagerVersion || '',
|
|
476
|
-
latestClientVersion: run?.latestClientVersion || latestRelease?.latestClientVersion || '',
|
|
477
|
-
// Current versions update every connected Client before the Hub exits.
|
|
478
|
-
// Older Hub versions did the reverse and set this flag themselves; the
|
|
479
|
-
// new server still supports that one-time migration path.
|
|
480
|
-
continueClientUpdate: false
|
|
405
|
+
const result = requestHubRestart?.({
|
|
406
|
+
operationId: run?.operationId || '',
|
|
407
|
+
latestVersion: run?.latestManagerVersion || latestRelease?.latestManagerVersion || '',
|
|
408
|
+
latestClientVersion: run?.latestClientVersion || latestRelease?.latestClientVersion || '',
|
|
409
|
+
// Current versions update every connected Client before the Hub exits.
|
|
410
|
+
// Older Hub versions did the reverse and set this flag themselves; the
|
|
411
|
+
// new server still supports that one-time migration path.
|
|
412
|
+
continueClientUpdate: false
|
|
481
413
|
});
|
|
482
414
|
if (result?.ok !== true) {
|
|
483
415
|
failRun(result?.error || 'Hub launcher restart request failed.');
|
|
@@ -490,214 +422,214 @@ export function createLiveDeskUpdateManager({
|
|
|
490
422
|
return true;
|
|
491
423
|
};
|
|
492
424
|
|
|
493
|
-
const dispatchTarget = (target, device) => {
|
|
494
|
-
const dispatchedAtEpochMs = now();
|
|
495
|
-
const updateDeadlineEpochMs = dispatchedAtEpochMs + effectiveTargetTimeoutMs;
|
|
496
|
-
const dispatchedSessionId = String(device?.sessionId || '').trim();
|
|
497
|
-
if (!dispatchedSessionId) {
|
|
498
|
-
target.state = 'failed';
|
|
499
|
-
target.error = 'client-update-session-unavailable';
|
|
500
|
-
return false;
|
|
501
|
-
}
|
|
502
|
-
target.dispatchedSessionId = dispatchedSessionId;
|
|
503
|
-
target.dispatchedPid = Math.max(0, Math.floor(Number(device?.pid) || 0));
|
|
504
|
-
resetReconnectCandidate(target);
|
|
505
|
-
const supportsDedicated = device.capabilities?.clientUpdate === true
|
|
506
|
-
&& isVersionAtLeast(device.agentVersion, LIVE_DESK_DEDICATED_CLIENT_UPDATE_MIN_VERSION);
|
|
507
|
-
const payload = {
|
|
508
|
-
targetVersion: run.latestClientVersion,
|
|
509
|
-
targetProductVersion: run.latestManagerVersion,
|
|
510
|
-
managerVersion: run.latestManagerVersion,
|
|
511
|
-
operationId: run.operationId,
|
|
512
|
-
updateTimeoutMs: effectiveTargetTimeoutMs
|
|
513
|
-
};
|
|
514
|
-
const result = supportsDedicated
|
|
515
|
-
? remoteHub.sendCommand(device.deviceId, { command: LIVE_DESK_UPDATE_COMMAND, payload })
|
|
516
|
-
: remoteHub.sendLegacyClientUpdate(device.deviceId, {
|
|
517
|
-
command: buildLegacyUpdateCommand(
|
|
518
|
-
device,
|
|
519
|
-
run.operationId,
|
|
520
|
-
run.latestClientVersion,
|
|
521
|
-
run.latestManagerVersion,
|
|
522
|
-
effectiveTargetTimeoutMs
|
|
523
|
-
),
|
|
524
|
-
timeoutMs: 30_000
|
|
525
|
-
});
|
|
526
|
-
if (result?.ok !== true) {
|
|
527
|
-
const dispatchError = String(result?.error || 'client-update-dispatch-failed');
|
|
528
|
-
if (['device-not-connected', 'device-not-found'].includes(dispatchError)) {
|
|
529
|
-
target.state = 'pending-offline';
|
|
530
|
-
target.method = '';
|
|
531
|
-
target.commandId = '';
|
|
532
|
-
target.dispatchedAt = '';
|
|
533
|
-
target.deadlineAt = '';
|
|
534
|
-
target.dispatchedSessionId = '';
|
|
535
|
-
target.dispatchedPid = 0;
|
|
536
|
-
resetReconnectCandidate(target);
|
|
537
|
-
target.error = '';
|
|
538
|
-
return 'pending-offline';
|
|
539
|
-
}
|
|
540
|
-
target.state = 'failed';
|
|
541
|
-
target.error = dispatchError;
|
|
542
|
-
return false;
|
|
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;
|
|
475
|
+
}
|
|
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
|
+
}
|
|
543
515
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
return
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
if (target.state === 'pending-offline' && device?.connected === true) {
|
|
654
|
-
target.state = 'queued';
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
if (!dispatchQueuedTargets()) return;
|
|
658
|
-
touch();
|
|
659
|
-
if (run.targets.every(target => target.state === 'completed')) {
|
|
660
|
-
if (runTimer) {
|
|
661
|
-
clearInterval(runTimer);
|
|
662
|
-
runTimer = null;
|
|
663
|
-
}
|
|
664
|
-
if (run.needsHubRestart) {
|
|
665
|
-
requestRestart();
|
|
666
|
-
} else {
|
|
667
|
-
run.state = 'clients-updated';
|
|
668
|
-
touch();
|
|
669
|
-
}
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
};
|
|
673
|
-
|
|
674
|
-
const startUpdate = async () => {
|
|
675
|
-
if (run && ['dispatching', 'waiting-for-clients', 'hub-restart-requested'].includes(run.state)) {
|
|
676
|
-
return { ok: true, ...getStatus() };
|
|
677
|
-
}
|
|
678
|
-
const settlingTargets = retrySettlingTargets();
|
|
679
|
-
if (settlingTargets.length > 0) {
|
|
680
|
-
const names = settlingTargets
|
|
681
|
-
.slice(0, 3)
|
|
682
|
-
.map(target => target.deviceName || target.deviceId)
|
|
683
|
-
.join(', ');
|
|
684
|
-
const remaining = settlingTargets.length > 3 ? ` and ${settlingTargets.length - 3} more` : '';
|
|
685
|
-
return {
|
|
686
|
-
...getStatus(),
|
|
687
|
-
ok: false,
|
|
688
|
-
error: `The previous rollout is still settling for offline Client(s): ${names}${remaining}. Retry after they reconnect or their restart deadline passes.`
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
const release = latestRelease || await checkLatest();
|
|
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();
|
|
692
624
|
if (!release) return { ok: false, error: checkError || 'LiveDesk update check failed.', ...getStatus() };
|
|
693
625
|
const managerNeedsUpdate = compareVersions(release.latestManagerVersion, currentManagerVersion) > 0;
|
|
694
626
|
const clientPackageNeedsUpdate = compareVersions(release.latestClientVersion, currentClientVersion) > 0;
|
|
695
627
|
const connected = connectedDevices(remoteHub);
|
|
696
|
-
const outdatedClients = connected.filter(device => deviceNeedsUpdate(
|
|
697
|
-
device,
|
|
698
|
-
release.latestManagerVersion,
|
|
699
|
-
release.latestClientVersion
|
|
700
|
-
));
|
|
628
|
+
const outdatedClients = connected.filter(device => deviceNeedsUpdate(
|
|
629
|
+
device,
|
|
630
|
+
release.latestManagerVersion,
|
|
631
|
+
release.latestClientVersion
|
|
632
|
+
));
|
|
701
633
|
const needsHubRestart = managerNeedsUpdate || clientPackageNeedsUpdate;
|
|
702
634
|
if (!managerNeedsUpdate && !clientPackageNeedsUpdate && outdatedClients.length === 0) {
|
|
703
635
|
return { ok: true, state: 'clients-updated', ...getStatus() };
|
|
@@ -705,78 +637,78 @@ export function createLiveDeskUpdateManager({
|
|
|
705
637
|
if (needsHubRestart && !restartSupported) {
|
|
706
638
|
return { ok: false, error: 'Hub launcher restart is unavailable. Start LiveDesk through npx livedesk@latest.', ...getStatus() };
|
|
707
639
|
}
|
|
708
|
-
const targets = outdatedClients;
|
|
709
|
-
run = {
|
|
640
|
+
const targets = outdatedClients;
|
|
641
|
+
run = {
|
|
710
642
|
operationId: randomUUID(),
|
|
711
643
|
state: 'dispatching',
|
|
712
644
|
startedAt: new Date(now()).toISOString(),
|
|
713
645
|
updatedAt: new Date(now()).toISOString(),
|
|
714
646
|
latestManagerVersion: release.latestManagerVersion,
|
|
715
|
-
latestClientVersion: release.latestClientVersion,
|
|
716
|
-
needsHubRestart,
|
|
717
|
-
batchSize: effectiveClientBatchSize,
|
|
718
|
-
clientRestartStabilityMs: effectiveClientRestartStabilityMs,
|
|
719
|
-
error: '',
|
|
720
|
-
targets: targets.map(device => ({
|
|
647
|
+
latestClientVersion: release.latestClientVersion,
|
|
648
|
+
needsHubRestart,
|
|
649
|
+
batchSize: effectiveClientBatchSize,
|
|
650
|
+
clientRestartStabilityMs: effectiveClientRestartStabilityMs,
|
|
651
|
+
error: '',
|
|
652
|
+
targets: targets.map(device => ({
|
|
721
653
|
deviceId: String(device.deviceId || ''),
|
|
722
|
-
deviceName: String(device.deviceName || device.hostname || device.deviceId || ''),
|
|
723
|
-
oldVersion: String(device.agentVersion || ''),
|
|
724
|
-
oldProductVersion: String(device.productVersion || ''),
|
|
725
|
-
currentVersion: '',
|
|
726
|
-
currentProductVersion: '',
|
|
727
|
-
state: 'queued',
|
|
728
|
-
method: '',
|
|
729
|
-
commandId: '',
|
|
730
|
-
dispatchedAt: '',
|
|
731
|
-
deadlineAt: '',
|
|
732
|
-
dispatchedSessionId: '',
|
|
733
|
-
dispatchedPid: 0,
|
|
734
|
-
candidateSessionId: '',
|
|
735
|
-
candidatePid: 0,
|
|
736
|
-
candidateConnectedAt: '',
|
|
737
|
-
candidateProductVersion: '',
|
|
738
|
-
candidateAgentVersion: '',
|
|
739
|
-
candidateSince: '',
|
|
740
|
-
stabilityDeadlineAt: '',
|
|
741
|
-
completedAt: '',
|
|
742
|
-
error: ''
|
|
743
|
-
}))
|
|
744
|
-
};
|
|
745
|
-
touch();
|
|
746
|
-
if (run.targets.length === 0) {
|
|
747
|
-
if (run.needsHubRestart) {
|
|
748
|
-
if (!requestRestart()) {
|
|
749
|
-
return { ok: false, error: run.error, ...getStatus() };
|
|
750
|
-
}
|
|
751
|
-
} else {
|
|
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 {
|
|
752
684
|
run.state = 'clients-updated';
|
|
753
685
|
touch();
|
|
754
686
|
}
|
|
755
687
|
return { ok: true, ...getStatus() };
|
|
756
|
-
}
|
|
757
|
-
run.state = 'waiting-for-clients';
|
|
758
|
-
if (!dispatchQueuedTargets()) {
|
|
759
|
-
return { ok: false, error: run.error, ...getStatus() };
|
|
760
|
-
}
|
|
761
|
-
runTimer = setInterval(verifyTargets, 1000);
|
|
688
|
+
}
|
|
689
|
+
run.state = 'waiting-for-clients';
|
|
690
|
+
if (!dispatchQueuedTargets()) {
|
|
691
|
+
return { ok: false, error: run.error, ...getStatus() };
|
|
692
|
+
}
|
|
693
|
+
runTimer = setInterval(verifyTargets, 1000);
|
|
762
694
|
runTimer.unref?.();
|
|
763
695
|
verifyTargets();
|
|
764
696
|
return { ok: true, ...getStatus() };
|
|
765
|
-
};
|
|
766
|
-
|
|
767
|
-
const handleRemoteEvent = (type, event) => {
|
|
768
|
-
if (!run || run.state !== 'waiting-for-clients') return;
|
|
769
|
-
if (type === 'RemoteDeviceConnected' || type === 'RemoteDeviceDisconnected') {
|
|
770
|
-
verifyTargets();
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
if (type !== 'RemoteCommandResult') return;
|
|
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;
|
|
774
706
|
const deviceId = String(event?.device?.deviceId || '');
|
|
775
707
|
const commandId = String(event?.commandId || '');
|
|
776
708
|
const target = run.targets.find(item => item.deviceId === deviceId && item.commandId === commandId);
|
|
777
|
-
if (!target
|
|
778
|
-
|| target.state !== 'waiting'
|
|
779
|
-
|| !['dedicated', 'legacy-command-run'].includes(target.method)) return;
|
|
709
|
+
if (!target
|
|
710
|
+
|| target.state !== 'waiting'
|
|
711
|
+
|| !['dedicated', 'legacy-command-run'].includes(target.method)) return;
|
|
780
712
|
const result = event?.result;
|
|
781
713
|
if (event?.error || result?.ok === false || result?.status === 'failed' || result?.status === 'rejected') {
|
|
782
714
|
target.state = 'failed';
|
|
@@ -794,11 +726,11 @@ export function createLiveDeskUpdateManager({
|
|
|
794
726
|
|
|
795
727
|
return {
|
|
796
728
|
checkLatest,
|
|
797
|
-
getStatus,
|
|
798
|
-
startUpdate,
|
|
799
|
-
handleRemoteEvent,
|
|
800
|
-
reconcileHubRestartResult,
|
|
801
|
-
close() {
|
|
729
|
+
getStatus,
|
|
730
|
+
startUpdate,
|
|
731
|
+
handleRemoteEvent,
|
|
732
|
+
reconcileHubRestartResult,
|
|
733
|
+
close() {
|
|
802
734
|
if (checkTimer) clearInterval(checkTimer);
|
|
803
735
|
if (runTimer) clearInterval(runTimer);
|
|
804
736
|
checkTimer = null;
|