@livedesk/client 0.1.170 → 0.1.172
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/livedesk-client-node.js +234 -77
- package/bin/livedesk-client.js +22 -10
- package/package.json +5 -5
|
@@ -43,13 +43,93 @@ function normalizeClientUpdateTargetVersion(value) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function getClientUpdateStatePath() {
|
|
46
|
-
return
|
|
46
|
+
return path.resolve(
|
|
47
|
+
String(process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(os.homedir(), '.livedesk', 'client-update.json'))
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getClientUpdateNeutralCwd(operationId) {
|
|
52
|
+
const safeOperationId = String(operationId || '')
|
|
53
|
+
.replace(/[^A-Za-z0-9._-]/g, '_')
|
|
54
|
+
.slice(0, 120) || 'unknown';
|
|
55
|
+
return path.join(os.tmpdir(), 'livedesk-update-cwd', `client-${safeOperationId}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function prepareClientUpdateNeutralCwd(operationId) {
|
|
59
|
+
const neutralCwd = path.resolve(getClientUpdateNeutralCwd(operationId));
|
|
60
|
+
await fs.mkdir(neutralCwd, { recursive: true });
|
|
61
|
+
for (let ancestor = neutralCwd; ; ancestor = path.dirname(ancestor)) {
|
|
62
|
+
const shadowPath = [
|
|
63
|
+
path.join(ancestor, 'package.json'),
|
|
64
|
+
path.join(ancestor, 'node_modules', 'livedesk', 'package.json')
|
|
65
|
+
].find(candidate => existsSync(candidate));
|
|
66
|
+
if (shadowPath) {
|
|
67
|
+
throw new Error(`LiveDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`);
|
|
68
|
+
}
|
|
69
|
+
const parent = path.dirname(ancestor);
|
|
70
|
+
if (parent === ancestor) break;
|
|
71
|
+
}
|
|
72
|
+
return neutralCwd;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildClientUpdateNpxEnvironment(baseEnv, neutralCwd) {
|
|
76
|
+
const env = { ...baseEnv };
|
|
77
|
+
const isolatedNames = new Set([
|
|
78
|
+
'init_cwd',
|
|
79
|
+
'npm_config_local_prefix',
|
|
80
|
+
'npm_config_workspace',
|
|
81
|
+
'npm_config_workspaces',
|
|
82
|
+
'npm_config_include_workspace_root',
|
|
83
|
+
'npm_package_json',
|
|
84
|
+
'npm_lifecycle_event',
|
|
85
|
+
'npm_lifecycle_script'
|
|
86
|
+
]);
|
|
87
|
+
for (const key of Object.keys(env)) {
|
|
88
|
+
if (isolatedNames.has(key.toLowerCase())) delete env[key];
|
|
89
|
+
}
|
|
90
|
+
env.INIT_CWD = neutralCwd;
|
|
91
|
+
env.npm_config_local_prefix = neutralCwd;
|
|
92
|
+
env.npm_config_include_workspace_root = 'false';
|
|
93
|
+
return env;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isClientUpdatePidAlive(value) {
|
|
97
|
+
const pid = Number(value);
|
|
98
|
+
if (!Number.isInteger(pid) || pid <= 1) return false;
|
|
99
|
+
try {
|
|
100
|
+
process.kill(pid, 0);
|
|
101
|
+
return true;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return error?.code === 'EPERM';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function canReclaimClientUpdateOperation(previous) {
|
|
108
|
+
const handoffOwners = [
|
|
109
|
+
previous?.supervisorPid,
|
|
110
|
+
previous?.starterPid
|
|
111
|
+
].map(Number).filter(pid => Number.isInteger(pid) && pid > 1);
|
|
112
|
+
if (handoffOwners.length > 0) {
|
|
113
|
+
return handoffOwners.every(pid => !isClientUpdatePidAlive(pid));
|
|
114
|
+
}
|
|
115
|
+
if (String(previous?.stage || '') === 'scheduled') {
|
|
116
|
+
const schedulingOwners = [
|
|
117
|
+
previous?.agentPid,
|
|
118
|
+
previous?.launcherPid
|
|
119
|
+
].map(Number).filter(pid => Number.isInteger(pid) && pid > 1);
|
|
120
|
+
return schedulingOwners.length > 0
|
|
121
|
+
&& schedulingOwners.every(pid => !isClientUpdatePidAlive(pid));
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
47
124
|
}
|
|
48
125
|
|
|
49
126
|
async function writeClientUpdateState(stage, details = {}) {
|
|
50
127
|
const statePath = getClientUpdateStatePath();
|
|
51
128
|
const operationId = String(details.operationId || process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
|
|
52
129
|
if (!operationId) return;
|
|
130
|
+
const attemptId = stage === 'scheduled'
|
|
131
|
+
? ''
|
|
132
|
+
: String(details.attemptId || process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim();
|
|
53
133
|
let previous = null;
|
|
54
134
|
try {
|
|
55
135
|
previous = JSON.parse(await fs.readFile(statePath, 'utf8'));
|
|
@@ -60,11 +140,28 @@ async function writeClientUpdateState(stage, details = {}) {
|
|
|
60
140
|
&& previous.operationId !== operationId
|
|
61
141
|
&& stage === 'scheduled'
|
|
62
142
|
&& ['connected', 'failed', 'restored'].includes(String(previous.stage || ''));
|
|
63
|
-
|
|
143
|
+
const reclaimsDeadOperation = previous?.operationId
|
|
144
|
+
&& previous.operationId !== operationId
|
|
145
|
+
&& stage === 'scheduled'
|
|
146
|
+
&& canReclaimClientUpdateOperation(previous);
|
|
147
|
+
const replacesPreviousOperation = replacesTerminalOperation || reclaimsDeadOperation;
|
|
148
|
+
if (previous?.operationId && previous.operationId !== operationId && !replacesPreviousOperation) return;
|
|
149
|
+
if (!replacesPreviousOperation
|
|
150
|
+
&& previous?.attemptId
|
|
151
|
+
&& attemptId
|
|
152
|
+
&& previous.attemptId !== attemptId) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!replacesPreviousOperation
|
|
156
|
+
&& previous?.stage === 'connected'
|
|
157
|
+
&& previous?.restartVerified === true
|
|
158
|
+
&& stage !== 'connected') {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
64
161
|
const targetProductVersion = String(
|
|
65
162
|
details.targetProductVersion
|
|
66
163
|
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
|
|
67
|
-
|| (!
|
|
164
|
+
|| (!replacesPreviousOperation ? previous?.targetProductVersion : '')
|
|
68
165
|
|| ''
|
|
69
166
|
).trim();
|
|
70
167
|
const targetAgentVersion = String(
|
|
@@ -72,12 +169,12 @@ async function writeClientUpdateState(stage, details = {}) {
|
|
|
72
169
|
|| details.targetVersion
|
|
73
170
|
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION
|
|
74
171
|
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
|
|
75
|
-
|| (!
|
|
76
|
-
|| (!
|
|
172
|
+
|| (!replacesPreviousOperation ? previous?.targetAgentVersion : '')
|
|
173
|
+
|| (!replacesPreviousOperation ? previous?.targetVersion : '')
|
|
77
174
|
|| ''
|
|
78
175
|
).trim();
|
|
79
176
|
const state = {
|
|
80
|
-
...(!
|
|
177
|
+
...(!replacesPreviousOperation && previous && typeof previous === 'object' ? previous : {}),
|
|
81
178
|
operationId,
|
|
82
179
|
stage,
|
|
83
180
|
targetVersion: targetAgentVersion,
|
|
@@ -87,7 +184,7 @@ async function writeClientUpdateState(stage, details = {}) {
|
|
|
87
184
|
agentVersion: AGENT_VERSION,
|
|
88
185
|
agentPid: process.pid,
|
|
89
186
|
launcherPid: Number(process.env.LIVEDESK_CLIENT_PARENT_PID || 0) || 0,
|
|
90
|
-
attemptId
|
|
187
|
+
attemptId,
|
|
91
188
|
updatedAt: new Date().toISOString(),
|
|
92
189
|
...details
|
|
93
190
|
};
|
|
@@ -95,15 +192,33 @@ async function writeClientUpdateState(stage, details = {}) {
|
|
|
95
192
|
const temporaryPath = `${statePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
96
193
|
try {
|
|
97
194
|
await fs.writeFile(temporaryPath, JSON.stringify(state, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
195
|
+
let current = null;
|
|
196
|
+
try {
|
|
197
|
+
current = JSON.parse(await fs.readFile(statePath, 'utf8'));
|
|
198
|
+
} catch {
|
|
199
|
+
// A missing first-write state is still owned by this transition.
|
|
200
|
+
}
|
|
201
|
+
if (String(current?.operationId || '') !== String(previous?.operationId || '')
|
|
202
|
+
|| String(current?.stage || '') !== String(previous?.stage || '')
|
|
203
|
+
|| String(current?.updatedAt || '') !== String(previous?.updatedAt || '')) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
98
206
|
await fs.rename(temporaryPath, statePath);
|
|
99
207
|
} finally {
|
|
100
208
|
await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
101
209
|
}
|
|
102
210
|
}
|
|
103
211
|
|
|
104
|
-
async function waitForClientUpdateShutdownSignal(
|
|
212
|
+
async function waitForClientUpdateShutdownSignal(
|
|
213
|
+
operationId,
|
|
214
|
+
updateDeadlineEpochMs,
|
|
215
|
+
timeoutMs = 180_000
|
|
216
|
+
) {
|
|
105
217
|
const statePath = getClientUpdateStatePath();
|
|
106
|
-
const deadline =
|
|
218
|
+
const deadline = Math.min(
|
|
219
|
+
Date.now() + timeoutMs,
|
|
220
|
+
Number(updateDeadlineEpochMs) || 0
|
|
221
|
+
);
|
|
107
222
|
while (Date.now() < deadline) {
|
|
108
223
|
try {
|
|
109
224
|
const state = JSON.parse(await fs.readFile(statePath, 'utf8'));
|
|
@@ -111,22 +226,32 @@ async function waitForClientUpdateShutdownSignal(operationId, timeoutMs = 150_00
|
|
|
111
226
|
await wait(200);
|
|
112
227
|
continue;
|
|
113
228
|
}
|
|
114
|
-
if (state.stage === 'waiting-for-shutdown')
|
|
229
|
+
if (state.stage === 'preflight-ready' || state.stage === 'waiting-for-shutdown') {
|
|
230
|
+
return { ready: true, error: '' };
|
|
231
|
+
}
|
|
115
232
|
if (state.stage === 'failed') {
|
|
116
|
-
|
|
117
|
-
|
|
233
|
+
const error = String(state.error || 'LiveDesk client update preparation failed.');
|
|
234
|
+
console.error(`LiveDesk client update preparation failed: ${error}`);
|
|
235
|
+
return { ready: false, error };
|
|
118
236
|
}
|
|
119
237
|
} catch {
|
|
120
238
|
// The detached supervisor may still be writing its first stage.
|
|
121
239
|
}
|
|
122
240
|
await wait(200);
|
|
123
241
|
}
|
|
242
|
+
const deadlineExpired = Date.now() >= Number(updateDeadlineEpochMs);
|
|
243
|
+
const error = deadlineExpired
|
|
244
|
+
? 'The absolute LiveDesk Client update deadline expired before shutdown.'
|
|
245
|
+
: 'Timed out waiting for the LiveDesk update supervisor to prepare the exact package.';
|
|
124
246
|
await writeClientUpdateState('failed', {
|
|
125
247
|
operationId,
|
|
126
|
-
error
|
|
248
|
+
error,
|
|
249
|
+
cancelRequested: true,
|
|
250
|
+
deadlineExpired,
|
|
251
|
+
failedAt: new Date().toISOString()
|
|
127
252
|
}).catch(() => undefined);
|
|
128
253
|
console.error('LiveDesk client update preparation timed out; the current Agent will remain running.');
|
|
129
|
-
return false;
|
|
254
|
+
return { ready: false, error };
|
|
130
255
|
}
|
|
131
256
|
|
|
132
257
|
function markClientUpdateConnected() {
|
|
@@ -1432,55 +1557,43 @@ function remotePolicyAllows(options, command) {
|
|
|
1432
1557
|
|
|
1433
1558
|
function buildClientUpdateBootstrapScript() {
|
|
1434
1559
|
return [
|
|
1435
|
-
'const { spawn
|
|
1560
|
+
'const { spawn } = require("node:child_process");',
|
|
1436
1561
|
'const fs = require("node:fs");',
|
|
1437
1562
|
'const path = require("node:path");',
|
|
1438
|
-
'const crypto = require("node:crypto");',
|
|
1439
|
-
'const waitPid = Number(process.env.LIVEDESK_UPDATE_WAIT_PID || 0);',
|
|
1440
|
-
'const agentPid = Number(process.env.LIVEDESK_UPDATE_AGENT_PID || 0);',
|
|
1441
1563
|
'const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || "").trim();',
|
|
1442
|
-
'const
|
|
1443
|
-
'const
|
|
1564
|
+
'const originalCwd = path.resolve(String(process.env.LIVEDESK_UPDATE_ORIGINAL_CWD || process.cwd()));',
|
|
1565
|
+
'const statePath = path.resolve(originalCwd, String(process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(require("node:os").homedir(), ".livedesk", "client-update.json")));',
|
|
1566
|
+
'const neutralCwdValue = String(process.env.LIVEDESK_UPDATE_NEUTRAL_CWD || "").trim();',
|
|
1567
|
+
'const neutralCwd = neutralCwdValue ? path.resolve(neutralCwdValue) : "";',
|
|
1444
1568
|
'const cleanVersion = value => String(value || "").trim().replace(/^v/i, "");',
|
|
1445
|
-
'const versionPattern = /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/;',
|
|
1446
1569
|
'const targetProductVersion = cleanVersion(process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION);',
|
|
1447
|
-
'const
|
|
1448
|
-
'const
|
|
1449
|
-
'const
|
|
1450
|
-
'for (const [label, version] of [["target product", targetProductVersion], ["target Agent", targetAgentVersion], ["current product", currentProductVersion], ["current Agent", currentAgentVersion]]) { if (!versionPattern.test(version)) throw new Error("Invalid LiveDesk " + label + " update version: " + version); }',
|
|
1451
|
-
'const packageSpec = "livedesk@" + targetProductVersion;',
|
|
1452
|
-
'const localLauncher = String(process.env.LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER || process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY || "").trim();',
|
|
1453
|
-
'const preservedArgs = (() => { try { const decoded = JSON.parse(Buffer.from(String(process.env.LIVEDESK_CLIENT_UPDATE_ARGS_BASE64 || ""), "base64").toString("utf8")); return Array.isArray(decoded) ? decoded.map(value => String(value)) : []; } catch { return []; } })();',
|
|
1454
|
-
'const proofTimeoutMs = Math.max(500, Number(process.env.LIVEDESK_CLIENT_UPDATE_PROOF_TIMEOUT_MS || 30000));',
|
|
1455
|
-
'const proofPollMs = Math.max(25, Number(process.env.LIVEDESK_CLIENT_UPDATE_PROOF_POLL_MS || 200));',
|
|
1456
|
-
'const killGraceMs = Math.max(50, Number(process.env.LIVEDESK_CLIENT_UPDATE_KILL_GRACE_MS || 1500));',
|
|
1457
|
-
'const targetLaunchLimit = 3;',
|
|
1458
|
-
'const restoreLaunchLimit = 2;',
|
|
1570
|
+
'const updateDeadlineEpochMs = Number(process.env.LIVEDESK_CLIENT_UPDATE_DEADLINE_EPOCH_MS || 0);',
|
|
1571
|
+
'const deadlineExpired = () => !Number.isSafeInteger(updateDeadlineEpochMs) || Date.now() >= updateDeadlineEpochMs;',
|
|
1572
|
+
'const versionPattern = /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/;',
|
|
1459
1573
|
'const readState = () => { try { return JSON.parse(fs.readFileSync(statePath, "utf8")); } catch { return {}; } };',
|
|
1460
|
-
'const writeState = (stage, details = {}) => { fs.mkdirSync(path.dirname(statePath), { recursive: true }); const previous = readState(); const replacesTerminalOperation = previous.operationId && previous.operationId !== operationId && stage === "preparing-package" && ["connected", "failed", "restored"].includes(String(previous.stage || "")); if (previous.operationId && previous.operationId !== operationId && !replacesTerminalOperation) return false; const base = replacesTerminalOperation ? {} : previous; const next = { ...base, operationId, stage, targetVersion: targetAgentVersion, targetProductVersion, targetAgentVersion, currentProductVersion, currentAgentVersion, supervisorPid: process.pid, updatedAt: new Date().toISOString(), ...details }; const temporary = statePath + "." + process.pid + "." + Math.random().toString(16).slice(2) + ".tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); return true; };',
|
|
1461
|
-
'const log = message => { try { fs.mkdirSync(path.dirname(logPath), { recursive: true }); fs.appendFileSync(logPath, new Date().toISOString() + " " + message + "\\n", "utf8"); } catch {} };',
|
|
1462
|
-
'const isAlive = pid => { try { process.kill(pid, 0); return true; } catch (error) { return error?.code !== "ESRCH"; } };',
|
|
1463
1574
|
'const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));',
|
|
1464
|
-
'const
|
|
1465
|
-
'const
|
|
1466
|
-
'const
|
|
1575
|
+
'const isAlive = pid => { try { process.kill(pid, 0); return true; } catch (error) { return error?.code === "EPERM"; } };',
|
|
1576
|
+
'const isGroupAlive = pid => { try { process.kill(-pid, 0); return true; } catch (error) { return error?.code === "EPERM"; } };',
|
|
1577
|
+
'const stateLockPath = statePath + ".lock"; const lockWaitBuffer = new Int32Array(new SharedArrayBuffer(4));',
|
|
1578
|
+
'const waitForStateLock = ms => Atomics.wait(lockWaitBuffer, 0, 0, Math.max(1, ms));',
|
|
1579
|
+
'const removeAbandonedStateLock = () => { let owner = null; try { owner = JSON.parse(fs.readFileSync(stateLockPath, "utf8")); } catch {} const ownerPid = Number(owner?.pid || 0); if (Number.isInteger(ownerPid) && ownerPid > 1) { if (isAlive(ownerPid)) return false; } else { try { if (Date.now() - fs.statSync(stateLockPath).mtimeMs < 5000) return false; } catch { return true; } } try { fs.rmSync(stateLockPath); return true; } catch { return false; } };',
|
|
1580
|
+
'const withStateLock = callback => { fs.mkdirSync(path.dirname(stateLockPath), { recursive: true }); const deadline = Date.now() + 10000; let descriptor = null; let token = ""; while (descriptor === null) { try { descriptor = fs.openSync(stateLockPath, "wx", 0o600); token = process.pid + "-" + Date.now() + "-" + Math.random().toString(16).slice(2); fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, token, acquiredAt: new Date().toISOString() }), "utf8"); } catch (error) { if (descriptor !== null) { try { fs.closeSync(descriptor); } catch {} descriptor = null; try { fs.rmSync(stateLockPath, { force: true }); } catch {} } if (error?.code !== "EEXIST") throw error; if (removeAbandonedStateLock()) continue; if (Date.now() >= deadline) throw new Error("Timed out waiting for the LiveDesk update state lock: " + stateLockPath); waitForStateLock(Math.min(25, Math.max(1, deadline - Date.now()))); } } try { return callback(); } finally { try { fs.closeSync(descriptor); } catch {} try { const owner = JSON.parse(fs.readFileSync(stateLockPath, "utf8")); if (owner?.token === token && Number(owner?.pid) === process.pid) fs.rmSync(stateLockPath, { force: true }); } catch {} } };',
|
|
1581
|
+
'const writeFailure = (error, cancelRequested = false) => { try { withStateLock(() => { const previous = readState(); if (previous.operationId && previous.operationId !== operationId) return; if (previous.stage === "preflight-ready" || previous.stage === "waiting-for-shutdown" || previous.stage === "connected" || previous.stage === "restored") return; const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "failed", targetProductVersion, restartVerified: false, cancelRequested: cancelRequested || previous.cancelRequested === true, error: String(error?.message || error).slice(0, 4000), failedAt: now, updatedAt: now }; const temporary = statePath + "." + process.pid + ".handoff.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); }); } catch {} process.stderr.write("LiveDesk update handoff failed: " + (error?.message || error) + "\\n"); process.exitCode = 1; };',
|
|
1582
|
+
'const writeCancellation = error => withStateLock(() => { const previous = readState(); if (previous.operationId && previous.operationId !== operationId) return "superseded"; if (previous.operationId !== operationId) return false; if (previous.stage === "preflight-ready" || previous.stage === "waiting-for-shutdown") return "ready"; if (previous.stage === "failed") return previous.cancelRequested === true ? "cancelled" : "failed"; const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "failed", targetProductVersion, restartVerified: false, cancelRequested: true, error: String(error?.message || error).slice(0, 4000), cancelledAt: now, failedAt: now, updatedAt: now }; const temporary = statePath + "." + process.pid + ".cancel.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); return "cancelled"; });',
|
|
1583
|
+
'const terminateTree = async child => { const pid = Number(child?.pid || 0); if (pid <= 1) return; if (process.platform === "win32") { await new Promise(resolve => { const killer = spawn("taskkill.exe", ["/PID", String(pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" }); let done = false; const finish = () => { if (done) return; done = true; clearTimeout(timer); resolve(); }; const timer = setTimeout(() => { try { killer.kill("SIGKILL"); } catch {} finish(); }, 10000); killer.once("error", finish); killer.once("exit", finish); }); if (isAlive(pid)) { try { child.kill("SIGKILL"); } catch {} } return; } try { process.kill(-pid, "SIGTERM"); } catch { try { child.kill("SIGTERM"); } catch {} } const gracefulDeadline = Date.now() + 2000; while (Date.now() < gracefulDeadline && (isGroupAlive(pid) || isAlive(pid))) await sleep(100); if (isGroupAlive(pid) || isAlive(pid)) { try { process.kill(-pid, "SIGKILL"); } catch { try { child.kill("SIGKILL"); } catch {} } } };',
|
|
1584
|
+
'const writeHandoffStarted = () => withStateLock(() => { if (deadlineExpired()) throw new Error("The absolute LiveDesk Client update deadline expired before handoff."); const previous = readState(); if (previous.operationId !== operationId) throw new Error("LiveDesk update handoff was superseded before exact-package launch."); const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "handoff-started", starterPid: process.pid, updateDeadlineEpochMs, restartVerified: false, error: "", updatedAt: now }; const temporary = statePath + "." + process.pid + ".handoff-started.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); });',
|
|
1585
|
+
'const monitorPreflight = child => { let settled = false; const finish = (error, preserveFailure = false) => { if (settled) return; settled = true; clearInterval(poll); clearTimeout(timeout); child.unref(); if (preserveFailure) process.exitCode = 1; else if (error) writeFailure(error); }; const inspect = () => { const state = readState(); if (state.operationId !== operationId) return; if (state.stage === "preflight-ready" || state.stage === "waiting-for-shutdown") finish(); else if (state.stage === "failed") finish(null, true); }; const cancelTimedOutHandoff = async () => { const error = new Error(deadlineExpired() ? "The absolute LiveDesk Client update deadline expired before exact-package preflight." : "Timed out waiting for exact-package update preflight."); while (!settled) { inspect(); if (settled) return; let outcome; try { outcome = writeCancellation(error); } catch (lockError) { finish(lockError); return; } if (outcome === "ready" || outcome === "failed") { inspect(); if (settled) return; } else if (outcome === "cancelled" || outcome === "superseded") { await terminateTree(child); finish(null, true); return; } await sleep(25); } }; const poll = setInterval(inspect, 100); const timeoutMs = Math.max(1, Math.min(Math.max(1000, Number(process.env.LIVEDESK_CLIENT_UPDATE_HANDOFF_TIMEOUT_MS || 140000)), updateDeadlineEpochMs - Date.now())); const timeout = setTimeout(() => { void cancelTimedOutHandoff(); }, timeoutMs); child.once("exit", (code, signal) => { inspect(); if (!settled) finish(new Error("Exact-package update supervisor exited before preflight (code=" + (code ?? "none") + ", signal=" + (signal || "none") + ").")); }); inspect(); };',
|
|
1586
|
+
'const prepareNeutralCwd = () => { if (!neutralCwd) throw new Error("LiveDesk update neutral working directory is unavailable"); fs.mkdirSync(neutralCwd, { recursive: true }); for (let ancestor = neutralCwd; ; ancestor = path.dirname(ancestor)) { const shadow = [path.join(ancestor, "package.json"), path.join(ancestor, "node_modules", "livedesk", "package.json")].find(fs.existsSync); if (shadow) throw new Error("LiveDesk update neutral working directory is shadowed by " + shadow + ": " + neutralCwd); const parent = path.dirname(ancestor); if (parent === ancestor) break; } };',
|
|
1587
|
+
'if (!operationId || !versionPattern.test(targetProductVersion) || deadlineExpired()) { writeFailure(new Error("Invalid or expired LiveDesk exact-package update handoff."), true); } else { try { prepareNeutralCwd(); writeHandoffStarted();',
|
|
1588
|
+
'const env = { ...process.env, LIVEDESK_UPDATE_STARTER_PID: String(process.pid) }; const isolated = new Set(["init_cwd", "npm_config_local_prefix", "npm_config_workspace", "npm_config_workspaces", "npm_config_include_workspace_root", "npm_package_json", "npm_lifecycle_event", "npm_lifecycle_script"]); for (const key of Object.keys(env)) if (isolated.has(key.toLowerCase())) delete env[key]; env.INIT_CWD = neutralCwd; env.npm_config_local_prefix = neutralCwd; env.npm_config_include_workspace_root = "false";',
|
|
1467
1589
|
'const nodeDir = path.dirname(process.execPath);',
|
|
1468
|
-
'const
|
|
1469
|
-
'const npx =
|
|
1470
|
-
'
|
|
1471
|
-
'const npmExecPath = String(process.env.npm_execpath || process.env.NPM_EXECPATH || "").trim();',
|
|
1472
|
-
'const npxCliCandidates = [process.env.LIVEDESK_NPX_CLI_PATH, npmExecPath ? path.join(path.dirname(npmExecPath), "npx-cli.js") : "", path.join(nodeDir, "node_modules", "npm", "bin", "npx-cli.js")].filter(Boolean);',
|
|
1473
|
-
'const npxCli = npxCliCandidates.find(candidate => fs.existsSync(candidate));',
|
|
1590
|
+
'const npxCli = [env.LIVEDESK_NPX_CLI_PATH, env.npm_execpath ? path.join(path.dirname(env.npm_execpath), "npx-cli.js") : "", env.LIVEDESK_NPX_EXECUTABLE ? path.join(path.dirname(env.LIVEDESK_NPX_EXECUTABLE), "node_modules", "npm", "bin", "npx-cli.js") : "", path.join(nodeDir, "node_modules", "npm", "bin", "npx-cli.js")].find(value => value && fs.existsSync(value));',
|
|
1591
|
+
'const npx = env.LIVEDESK_NPX_EXECUTABLE || (process.platform === "win32" ? "npx.cmd" : "npx");',
|
|
1592
|
+
'const args = ["-y", "--prefer-online", "livedesk@" + targetProductVersion, "--internal-legacy-client-update"];',
|
|
1474
1593
|
'const quoteCmd = value => "\\"" + String(value).replaceAll("%", "%%").replaceAll("\\"", "\\"\\"") + "\\"";',
|
|
1475
|
-
'const invocation =
|
|
1476
|
-
'
|
|
1477
|
-
'
|
|
1478
|
-
'const attemptIdFor = (kind, number) => operationId + "-" + kind + "-" + number + "-" + crypto.randomBytes(4).toString("hex");',
|
|
1479
|
-
'const updateAttempt = (attempt, patch) => { Object.assign(attempt, patch, { updatedAt: new Date().toISOString() }); return attempts.map(item => ({ ...item })); };',
|
|
1480
|
-
'const killAttempt = async (restartPid, attemptId) => { const state = readState(); const pids = new Set([Number(restartPid || 0)]); if (state.operationId === operationId && state.attemptId === attemptId) { pids.add(Number(state.launcherPid || 0)); pids.add(Number(state.agentPid || 0)); } for (const pid of pids) await killProcessTree(pid); };',
|
|
1481
|
-
'const launchAndVerify = async ({ kind, number, expectedProductVersion, expectedAgentVersion, call }) => { const attemptId = attemptIdFor(kind, number); const attempt = { attemptId, kind, number, status: "launching", expectedProductVersion, expectedAgentVersion, startedAt: new Date().toISOString() }; attempts.push(attempt); const attemptEnv = { ...process.env, LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID: attemptId, LIVEDESK_CLIENT_UPDATE_ATTEMPT_KIND: kind, LIVEDESK_CLIENT_UPDATE_ATTEMPT_NUMBER: String(number), LIVEDESK_CLIENT_UPDATE_OPERATION_ID: operationId, LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION: targetProductVersion, LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION: targetAgentVersion, LIVEDESK_CLIENT_UPDATE_TARGET_VERSION: targetAgentVersion }; const logFd = fs.openSync(logPath, "a"); let child; try { child = spawn(call.command, call.args, { cwd: process.env.LIVEDESK_CLIENT_UPDATE_CWD || process.cwd(), env: attemptEnv, detached: true, windowsHide: true, stdio: ["ignore", logFd, logFd] }); await new Promise((resolve, reject) => { child.once("error", reject); child.once("spawn", resolve); }); } finally { fs.closeSync(logFd); } const restartPid = Number(child?.pid || 0); updateAttempt(attempt, { status: "waiting-for-connected-proof", restartPid }); writeState(kind + "-launched", { attemptId, attemptKind: kind, attemptNumber: number, restartPid, attempts: attempts.map(item => ({ ...item })), restartVerified: false, error: "" }); let exitResult = null; child.once("exit", (code, signal) => { exitResult = { code, signal }; }); const deadline = Date.now() + proofTimeoutMs; let proof = null; let failure = ""; while (Date.now() < deadline) { const state = readState(); if (state.operationId === operationId && state.attemptId === attemptId && state.stage === "connected") { const observedProduct = cleanVersion(state.productVersion); const observedAgent = cleanVersion(state.agentVersion); if (observedProduct === expectedProductVersion && observedAgent === expectedAgentVersion) { proof = state; break; } failure = "Connected version mismatch expected product " + expectedProductVersion + " / Agent " + expectedAgentVersion + ", observed product " + (observedProduct || "unknown") + " / Agent " + (observedAgent || "unknown") + "."; break; } if (exitResult) { failure = "Launcher exited before exact connected proof (code=" + (exitResult.code ?? "none") + ", signal=" + (exitResult.signal || "none") + ")."; break; } await sleep(proofPollMs); } if (proof) { updateAttempt(attempt, { status: "connected", connectedAt: proof.connectedAt || new Date().toISOString(), productVersion: cleanVersion(proof.productVersion), agentVersion: cleanVersion(proof.agentVersion) }); if (child && !exitResult) child.unref(); writeState(kind === "target" ? "connected" : "restored", { attemptId, attemptKind: kind, attemptNumber: number, restartPid, launcherPid: Number(proof.launcherPid || 0), agentPid: Number(proof.agentPid || 0), productVersion: cleanVersion(proof.productVersion), agentVersion: cleanVersion(proof.agentVersion), attempts: attempts.map(item => ({ ...item })), restartVerified: kind === "target", restored: kind === "restore", connectedAt: proof.connectedAt || new Date().toISOString(), error: kind === "restore" ? "Target update failed; previous local Client was restored." : "" }); return true; } failure = failure || "Timed out waiting for exact connected proof for " + attemptId + "."; await killAttempt(restartPid, attemptId); updateAttempt(attempt, { status: "failed", failedAt: new Date().toISOString(), killed: true, error: failure }); writeState(kind + "-attempt-failed", { attemptId, attemptKind: kind, attemptNumber: number, restartPid, attempts: attempts.map(item => ({ ...item })), restartVerified: false, error: failure }); log(attemptId + " failed: " + failure); return false; };',
|
|
1482
|
-
'process.env.LIVEDESK_SKIP_BROWSER_OPEN = "1";',
|
|
1483
|
-
'(async () => { writeState("preparing-package", { packageSpec, attempts: [], error: "" }); log("preparing " + packageSpec); const prepared = await runToExit(["-y", "--prefer-online", packageSpec, "--version"], 120000); if (prepared.code !== 0) throw new Error("LiveDesk package preparation failed (exit=" + prepared.code + "): " + (prepared.stderr || prepared.stdout).trim().slice(-2000)); const stdoutVersions = prepared.stdout.split(/\\r?\\n/).map(line => line.trim()).filter(line => versionPattern.test(line)); if (!stdoutVersions.includes(targetProductVersion)) throw new Error("Prepared LiveDesk stdout did not report requested product version " + targetProductVersion + ". stdout=" + prepared.stdout.trim().slice(-1000)); writeState("waiting-for-shutdown", { observedVersion: targetProductVersion, observedProductVersion: targetProductVersion, preparedAt: new Date().toISOString(), attempts: [], error: "" }); log("package ready product=" + targetProductVersion + "; waiting for current runtime shutdown"); await ensureExit(agentPid, "Agent"); await ensureExit(waitPid, "Launcher"); const roleArgs = ["--force-role", "client", "--no-login", "--no-open", ...preservedArgs]; const targetCall = invocation(["-y", "--prefer-online", packageSpec, ...roleArgs]); for (let number = 1; number <= targetLaunchLimit; number += 1) { if (await launchAndVerify({ kind: "target", number, expectedProductVersion: targetProductVersion, expectedAgentVersion: targetAgentVersion, call: targetCall })) { log("target connected with exact product " + targetProductVersion + " / Agent " + targetAgentVersion); return; } } writeState("restoring", { attempts: attempts.map(item => ({ ...item })), error: "All target launch attempts failed; restoring previous local Client." }); const restoreCall = localLauncher && fs.existsSync(localLauncher) ? { command: process.execPath, args: [localLauncher, ...roleArgs] } : invocation(["-y", "--prefer-offline", "livedesk@" + currentProductVersion, ...roleArgs]); for (let number = 1; number <= restoreLaunchLimit; number += 1) { if (await launchAndVerify({ kind: "restore", number, expectedProductVersion: currentProductVersion, expectedAgentVersion: currentAgentVersion, call: restoreCall })) { log("previous local Client restored at product " + currentProductVersion + " / Agent " + currentAgentVersion); process.exitCode = 1; return; } } throw new Error("Target update failed after 3 attempts and previous local Client restoration failed after 2 attempts."); })().catch(error => { const message = error?.stack || error?.message || String(error); log("failed: " + message); try { writeState("failed", { attempts: attempts.map(item => ({ ...item })), error: String(error?.message || error).slice(0, 4000), failedAt: new Date().toISOString(), restartVerified: false }); } catch {} process.exitCode = 1; });',
|
|
1594
|
+
'const invocation = npxCli ? { command: process.execPath, args: [npxCli, ...args] } : process.platform === "win32" ? { command: env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", "call " + quoteCmd(npx) + " " + args.map(quoteCmd).join(" ")] } : { command: npx, args };',
|
|
1595
|
+
'if (!npxCli && path.isAbsolute(npx) && !fs.existsSync(npx)) { writeFailure(new Error("LiveDesk npx executable was not found: " + npx)); } else { try { const child = spawn(invocation.command, invocation.args, { cwd: neutralCwd, env, detached: true, stdio: "ignore", windowsHide: true }); child.once("error", writeFailure); child.once("spawn", () => monitorPreflight(child)); } catch (error) { writeFailure(error); } }',
|
|
1596
|
+
'} catch (error) { writeFailure(error, deadlineExpired()); } }',
|
|
1484
1597
|
''
|
|
1485
1598
|
].join('\n');
|
|
1486
1599
|
}
|
|
@@ -1490,6 +1603,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1490
1603
|
const statePath = getClientUpdateStatePath();
|
|
1491
1604
|
let targetProductVersion = '';
|
|
1492
1605
|
let targetAgentVersion = '';
|
|
1606
|
+
let updateDeadlineEpochMs = 0;
|
|
1493
1607
|
try {
|
|
1494
1608
|
targetProductVersion = normalizeClientUpdateTargetVersion(
|
|
1495
1609
|
payload.targetProductVersion
|
|
@@ -1509,11 +1623,22 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1509
1623
|
|| targetProductVersion
|
|
1510
1624
|
|| ''
|
|
1511
1625
|
);
|
|
1626
|
+
const updateTimeoutMs = Number(payload.updateTimeoutMs);
|
|
1627
|
+
if (!Number.isSafeInteger(updateTimeoutMs)
|
|
1628
|
+
|| updateTimeoutMs < 1_000
|
|
1629
|
+
|| updateTimeoutMs > 90 * 60_000) {
|
|
1630
|
+
throw new Error('LiveDesk Client update requires a bounded deadline budget.');
|
|
1631
|
+
}
|
|
1632
|
+
updateDeadlineEpochMs = Date.now() + updateTimeoutMs;
|
|
1633
|
+
const originalCwd = path.resolve(process.cwd());
|
|
1634
|
+
const neutralCwd = await prepareClientUpdateNeutralCwd(operationId);
|
|
1512
1635
|
await writeClientUpdateState('scheduled', {
|
|
1513
1636
|
operationId,
|
|
1514
1637
|
targetVersion: targetAgentVersion,
|
|
1515
1638
|
targetProductVersion,
|
|
1516
1639
|
targetAgentVersion,
|
|
1640
|
+
updateTimeoutMs,
|
|
1641
|
+
updateDeadlineEpochMs,
|
|
1517
1642
|
requestedAt: new Date().toISOString(),
|
|
1518
1643
|
restartVerified: false,
|
|
1519
1644
|
error: ''
|
|
@@ -1533,29 +1658,41 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1533
1658
|
if (!command) {
|
|
1534
1659
|
throw new Error('LiveDesk npx executable was not found.');
|
|
1535
1660
|
}
|
|
1661
|
+
const frozenCommand = path.isAbsolute(command) || !/[\\/]/.test(command)
|
|
1662
|
+
? command
|
|
1663
|
+
: path.resolve(originalCwd, command);
|
|
1664
|
+
const configuredLocalLauncher = String(
|
|
1665
|
+
process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY
|
|
1666
|
+
|| process.env.LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER
|
|
1667
|
+
|| ''
|
|
1668
|
+
).trim();
|
|
1669
|
+
const localLauncher = configuredLocalLauncher
|
|
1670
|
+
? path.resolve(originalCwd, configuredLocalLauncher)
|
|
1671
|
+
: '';
|
|
1672
|
+
const updateEnvironment = buildClientUpdateNpxEnvironment({
|
|
1673
|
+
...process.env,
|
|
1674
|
+
LIVEDESK_NODE_EXECUTABLE: process.execPath,
|
|
1675
|
+
LIVEDESK_NPX_EXECUTABLE: frozenCommand,
|
|
1676
|
+
LIVEDESK_UPDATE_WAIT_PID: String(parentPid),
|
|
1677
|
+
LIVEDESK_UPDATE_AGENT_PID: String(process.pid),
|
|
1678
|
+
LIVEDESK_SKIP_BROWSER_OPEN: '1',
|
|
1679
|
+
LIVEDESK_CLIENT_UPDATE_TARGET_VERSION: targetAgentVersion,
|
|
1680
|
+
LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION: targetProductVersion,
|
|
1681
|
+
LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION: targetAgentVersion,
|
|
1682
|
+
LIVEDESK_CLIENT_UPDATE_CURRENT_PRODUCT_VERSION: PRODUCT_VERSION,
|
|
1683
|
+
LIVEDESK_CLIENT_UPDATE_CURRENT_AGENT_VERSION: AGENT_VERSION,
|
|
1684
|
+
LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER: localLauncher,
|
|
1685
|
+
LIVEDESK_CLIENT_UPDATE_OPERATION_ID: operationId,
|
|
1686
|
+
LIVEDESK_CLIENT_UPDATE_DEADLINE_EPOCH_MS: String(updateDeadlineEpochMs),
|
|
1687
|
+
LIVEDESK_CLIENT_UPDATE_STATE_PATH: statePath,
|
|
1688
|
+
LIVEDESK_UPDATE_NEUTRAL_CWD: neutralCwd,
|
|
1689
|
+
LIVEDESK_UPDATE_ORIGINAL_CWD: originalCwd,
|
|
1690
|
+
LIVEDESK_CLIENT_UPDATE_CWD: originalCwd
|
|
1691
|
+
}, neutralCwd);
|
|
1536
1692
|
return await new Promise((resolve, reject) => {
|
|
1537
1693
|
const child = spawn(process.execPath, ['-e', buildClientUpdateBootstrapScript()], {
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
LIVEDESK_NODE_EXECUTABLE: process.execPath,
|
|
1541
|
-
LIVEDESK_NPX_EXECUTABLE: command,
|
|
1542
|
-
LIVEDESK_UPDATE_WAIT_PID: String(parentPid),
|
|
1543
|
-
LIVEDESK_UPDATE_AGENT_PID: String(process.pid),
|
|
1544
|
-
LIVEDESK_SKIP_BROWSER_OPEN: '1',
|
|
1545
|
-
LIVEDESK_CLIENT_UPDATE_TARGET_VERSION: targetAgentVersion,
|
|
1546
|
-
LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION: targetProductVersion,
|
|
1547
|
-
LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION: targetAgentVersion,
|
|
1548
|
-
LIVEDESK_CLIENT_UPDATE_CURRENT_PRODUCT_VERSION: PRODUCT_VERSION,
|
|
1549
|
-
LIVEDESK_CLIENT_UPDATE_CURRENT_AGENT_VERSION: AGENT_VERSION,
|
|
1550
|
-
LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER: String(
|
|
1551
|
-
process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY
|
|
1552
|
-
|| process.env.LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER
|
|
1553
|
-
|| ''
|
|
1554
|
-
),
|
|
1555
|
-
LIVEDESK_CLIENT_UPDATE_OPERATION_ID: operationId,
|
|
1556
|
-
LIVEDESK_CLIENT_UPDATE_STATE_PATH: statePath,
|
|
1557
|
-
LIVEDESK_CLIENT_UPDATE_CWD: process.cwd()
|
|
1558
|
-
},
|
|
1694
|
+
cwd: neutralCwd,
|
|
1695
|
+
env: updateEnvironment,
|
|
1559
1696
|
detached: true,
|
|
1560
1697
|
stdio: 'ignore',
|
|
1561
1698
|
windowsHide: true
|
|
@@ -1570,6 +1707,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1570
1707
|
targetProductVersion,
|
|
1571
1708
|
targetAgentVersion,
|
|
1572
1709
|
operationId,
|
|
1710
|
+
updateDeadlineEpochMs,
|
|
1573
1711
|
statePath,
|
|
1574
1712
|
parentPid,
|
|
1575
1713
|
restartAfterMs: 0
|
|
@@ -1623,8 +1761,27 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
1623
1761
|
commandId: message.commandId,
|
|
1624
1762
|
result
|
|
1625
1763
|
});
|
|
1626
|
-
|
|
1627
|
-
|
|
1764
|
+
const shutdownSignal = await waitForClientUpdateShutdownSignal(
|
|
1765
|
+
result.operationId,
|
|
1766
|
+
result.updateDeadlineEpochMs
|
|
1767
|
+
);
|
|
1768
|
+
if (shutdownSignal.ready) {
|
|
1769
|
+
// The exact package supervisor is the sole owner of old-runtime
|
|
1770
|
+
// termination, so a deadline can never race a voluntary exit.
|
|
1771
|
+
return;
|
|
1772
|
+
} else {
|
|
1773
|
+
writeJsonLine(socket, {
|
|
1774
|
+
type: 'command.result',
|
|
1775
|
+
commandId: message.commandId,
|
|
1776
|
+
error: shutdownSignal.error,
|
|
1777
|
+
result: {
|
|
1778
|
+
ok: false,
|
|
1779
|
+
status: 'failed',
|
|
1780
|
+
error: shutdownSignal.error,
|
|
1781
|
+
operationId: result.operationId,
|
|
1782
|
+
completedAt: new Date().toISOString()
|
|
1783
|
+
}
|
|
1784
|
+
});
|
|
1628
1785
|
}
|
|
1629
1786
|
} catch (error) {
|
|
1630
1787
|
writeJsonLine(socket, {
|
package/bin/livedesk-client.js
CHANGED
|
@@ -603,8 +603,15 @@ function readClientUpdateState() {
|
|
|
603
603
|
function writeClientUpdateRuntimeStage(stage, details = {}) {
|
|
604
604
|
const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
|
|
605
605
|
if (!operationId) return;
|
|
606
|
+
const attemptId = String(process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim();
|
|
606
607
|
const previous = readClientUpdateState();
|
|
607
608
|
if (previous?.operationId && previous.operationId !== operationId) return;
|
|
609
|
+
if (previous?.attemptId && attemptId && previous.attemptId !== attemptId) return;
|
|
610
|
+
if (previous?.stage === 'connected'
|
|
611
|
+
&& previous?.restartVerified === true
|
|
612
|
+
&& stage !== 'connected') {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
608
615
|
const targetVersion = String(process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION || '').trim().replace(/^v/i, '');
|
|
609
616
|
const productVersion = String(process.env.LIVEDESK_NPM_LAUNCHER_VERSION || readPackageVersion()).trim();
|
|
610
617
|
const state = {
|
|
@@ -615,6 +622,7 @@ function writeClientUpdateRuntimeStage(stage, details = {}) {
|
|
|
615
622
|
productVersion,
|
|
616
623
|
agentVersion: readPackageVersion(),
|
|
617
624
|
launcherPid: process.pid,
|
|
625
|
+
attemptId,
|
|
618
626
|
updatedAt: new Date().toISOString(),
|
|
619
627
|
...details
|
|
620
628
|
};
|
|
@@ -3592,9 +3600,10 @@ function buildClientAgentEnvironment(prepared) {
|
|
|
3592
3600
|
env.LIVEDESK_NPX_CLI_PATH = npxCliPath;
|
|
3593
3601
|
}
|
|
3594
3602
|
env.LIVEDESK_CLIENT_MANAGER = String(prepared?.manager || env.LIVEDESK_CLIENT_MANAGER || '');
|
|
3595
|
-
env.LIVEDESK_CLIENT_PAIR_TOKEN = String(prepared?.pair || env.LIVEDESK_CLIENT_PAIR_TOKEN || '');
|
|
3596
|
-
env.LIVEDESK_CLIENT_NAME = String(prepared?.name || env.LIVEDESK_CLIENT_NAME || '');
|
|
3603
|
+
env.LIVEDESK_CLIENT_PAIR_TOKEN = String(prepared?.pair || env.LIVEDESK_CLIENT_PAIR_TOKEN || '');
|
|
3604
|
+
env.LIVEDESK_CLIENT_NAME = String(prepared?.name || env.LIVEDESK_CLIENT_NAME || '');
|
|
3597
3605
|
env.LIVEDESK_CLIENT_SLOT = String(prepared?.slot || env.LIVEDESK_CLIENT_SLOT || '');
|
|
3606
|
+
env.LIVEDESK_DEVICE_ID = String(prepared?.deviceId || env.LIVEDESK_DEVICE_ID || '');
|
|
3598
3607
|
env.LIVEDESK_CLIENT_ENGINE = String(prepared?.engine || env.LIVEDESK_CLIENT_ENGINE || 'auto');
|
|
3599
3608
|
env.LIVEDESK_CLIENT_UPDATE_STATE_PATH = CLIENT_UPDATE_STATE_PATH;
|
|
3600
3609
|
env.LIVEDESK_CLIENT_UPDATE_ARGS_BASE64 = Buffer
|
|
@@ -3805,16 +3814,19 @@ function resolveFastLaunch(runtime) {
|
|
|
3805
3814
|
}
|
|
3806
3815
|
|
|
3807
3816
|
function spawnAgent(command, args, env = process.env, onStart = null) {
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
activeAgentProcess = child;
|
|
3814
|
-
writeClientUpdateRuntimeStage('agent-started', {
|
|
3815
|
-
agentPid: Number(child.pid || 0),
|
|
3817
|
+
// Publish only a pre-spawn stage. A very fast child can reach Hub welcome
|
|
3818
|
+
// before spawn() returns; a post-spawn write would then erase its one-shot
|
|
3819
|
+
// connected proof.
|
|
3820
|
+
writeClientUpdateRuntimeStage('agent-launching', {
|
|
3821
|
+
agentPid: 0,
|
|
3816
3822
|
restartVerified: false
|
|
3817
3823
|
});
|
|
3824
|
+
const child = spawn(command, args, {
|
|
3825
|
+
env,
|
|
3826
|
+
stdio: 'inherit',
|
|
3827
|
+
windowsHide: true
|
|
3828
|
+
});
|
|
3829
|
+
activeAgentProcess = child;
|
|
3818
3830
|
if (typeof onStart === 'function') {
|
|
3819
3831
|
onStart(child);
|
|
3820
3832
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.172",
|
|
4
4
|
"description": "LiveDesk local remote client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"ws": "^8.18.3"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"@livedesk/fast-linux-x64": "0.1.
|
|
43
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
44
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
45
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
42
|
+
"@livedesk/fast-linux-x64": "0.1.378",
|
|
43
|
+
"@livedesk/fast-osx-arm64": "0.1.378",
|
|
44
|
+
"@livedesk/fast-osx-x64": "0.1.378",
|
|
45
|
+
"@livedesk/fast-win-x64": "0.1.378"
|
|
46
46
|
},
|
|
47
47
|
"publishConfig": {
|
|
48
48
|
"access": "public"
|