@livedesk/client 0.1.171 → 0.1.173
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 +215 -45
- package/bin/livedesk-client.js +52 -14
- package/package.json +5 -5
- package/src/runtime/client-runtime-server.js +29 -16
|
@@ -43,7 +43,84 @@ 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 = {}) {
|
|
@@ -63,14 +140,19 @@ async function writeClientUpdateState(stage, details = {}) {
|
|
|
63
140
|
&& previous.operationId !== operationId
|
|
64
141
|
&& stage === 'scheduled'
|
|
65
142
|
&& ['connected', 'failed', 'restored'].includes(String(previous.stage || ''));
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
68
150
|
&& previous?.attemptId
|
|
69
151
|
&& attemptId
|
|
70
152
|
&& previous.attemptId !== attemptId) {
|
|
71
153
|
return;
|
|
72
154
|
}
|
|
73
|
-
if (!
|
|
155
|
+
if (!replacesPreviousOperation
|
|
74
156
|
&& previous?.stage === 'connected'
|
|
75
157
|
&& previous?.restartVerified === true
|
|
76
158
|
&& stage !== 'connected') {
|
|
@@ -79,7 +161,7 @@ async function writeClientUpdateState(stage, details = {}) {
|
|
|
79
161
|
const targetProductVersion = String(
|
|
80
162
|
details.targetProductVersion
|
|
81
163
|
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
|
|
82
|
-
|| (!
|
|
164
|
+
|| (!replacesPreviousOperation ? previous?.targetProductVersion : '')
|
|
83
165
|
|| ''
|
|
84
166
|
).trim();
|
|
85
167
|
const targetAgentVersion = String(
|
|
@@ -87,12 +169,12 @@ async function writeClientUpdateState(stage, details = {}) {
|
|
|
87
169
|
|| details.targetVersion
|
|
88
170
|
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION
|
|
89
171
|
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
|
|
90
|
-
|| (!
|
|
91
|
-
|| (!
|
|
172
|
+
|| (!replacesPreviousOperation ? previous?.targetAgentVersion : '')
|
|
173
|
+
|| (!replacesPreviousOperation ? previous?.targetVersion : '')
|
|
92
174
|
|| ''
|
|
93
175
|
).trim();
|
|
94
176
|
const state = {
|
|
95
|
-
...(!
|
|
177
|
+
...(!replacesPreviousOperation && previous && typeof previous === 'object' ? previous : {}),
|
|
96
178
|
operationId,
|
|
97
179
|
stage,
|
|
98
180
|
targetVersion: targetAgentVersion,
|
|
@@ -110,15 +192,33 @@ async function writeClientUpdateState(stage, details = {}) {
|
|
|
110
192
|
const temporaryPath = `${statePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
111
193
|
try {
|
|
112
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
|
+
}
|
|
113
206
|
await fs.rename(temporaryPath, statePath);
|
|
114
207
|
} finally {
|
|
115
208
|
await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
116
209
|
}
|
|
117
210
|
}
|
|
118
211
|
|
|
119
|
-
async function waitForClientUpdateShutdownSignal(
|
|
212
|
+
async function waitForClientUpdateShutdownSignal(
|
|
213
|
+
operationId,
|
|
214
|
+
updateDeadlineEpochMs,
|
|
215
|
+
timeoutMs = 180_000
|
|
216
|
+
) {
|
|
120
217
|
const statePath = getClientUpdateStatePath();
|
|
121
|
-
const deadline =
|
|
218
|
+
const deadline = Math.min(
|
|
219
|
+
Date.now() + timeoutMs,
|
|
220
|
+
Number(updateDeadlineEpochMs) || 0
|
|
221
|
+
);
|
|
122
222
|
while (Date.now() < deadline) {
|
|
123
223
|
try {
|
|
124
224
|
const state = JSON.parse(await fs.readFile(statePath, 'utf8'));
|
|
@@ -126,22 +226,32 @@ async function waitForClientUpdateShutdownSignal(operationId, timeoutMs = 150_00
|
|
|
126
226
|
await wait(200);
|
|
127
227
|
continue;
|
|
128
228
|
}
|
|
129
|
-
if (state.stage === 'preflight-ready' || state.stage === 'waiting-for-shutdown')
|
|
229
|
+
if (state.stage === 'preflight-ready' || state.stage === 'waiting-for-shutdown') {
|
|
230
|
+
return { ready: true, error: '' };
|
|
231
|
+
}
|
|
130
232
|
if (state.stage === 'failed') {
|
|
131
|
-
|
|
132
|
-
|
|
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 };
|
|
133
236
|
}
|
|
134
237
|
} catch {
|
|
135
238
|
// The detached supervisor may still be writing its first stage.
|
|
136
239
|
}
|
|
137
240
|
await wait(200);
|
|
138
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.';
|
|
139
246
|
await writeClientUpdateState('failed', {
|
|
140
247
|
operationId,
|
|
141
|
-
error
|
|
248
|
+
error,
|
|
249
|
+
cancelRequested: true,
|
|
250
|
+
deadlineExpired,
|
|
251
|
+
failedAt: new Date().toISOString()
|
|
142
252
|
}).catch(() => undefined);
|
|
143
253
|
console.error('LiveDesk client update preparation timed out; the current Agent will remain running.');
|
|
144
|
-
return false;
|
|
254
|
+
return { ready: false, error };
|
|
145
255
|
}
|
|
146
256
|
|
|
147
257
|
function markClientUpdateConnected() {
|
|
@@ -1451,23 +1561,39 @@ function buildClientUpdateBootstrapScript() {
|
|
|
1451
1561
|
'const fs = require("node:fs");',
|
|
1452
1562
|
'const path = require("node:path");',
|
|
1453
1563
|
'const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || "").trim();',
|
|
1454
|
-
'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) : "";',
|
|
1455
1568
|
'const cleanVersion = value => String(value || "").trim().replace(/^v/i, "");',
|
|
1456
1569
|
'const targetProductVersion = cleanVersion(process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION);',
|
|
1570
|
+
'const updateDeadlineEpochMs = Number(process.env.LIVEDESK_CLIENT_UPDATE_DEADLINE_EPOCH_MS || 0);',
|
|
1571
|
+
'const deadlineExpired = () => !Number.isSafeInteger(updateDeadlineEpochMs) || Date.now() >= updateDeadlineEpochMs;',
|
|
1457
1572
|
'const versionPattern = /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/;',
|
|
1458
1573
|
'const readState = () => { try { return JSON.parse(fs.readFileSync(statePath, "utf8")); } catch { return {}; } };',
|
|
1459
|
-
'const
|
|
1460
|
-
'const
|
|
1461
|
-
'
|
|
1462
|
-
'const
|
|
1574
|
+
'const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));',
|
|
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";',
|
|
1463
1589
|
'const nodeDir = path.dirname(process.execPath);',
|
|
1464
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));',
|
|
1465
1591
|
'const npx = env.LIVEDESK_NPX_EXECUTABLE || (process.platform === "win32" ? "npx.cmd" : "npx");',
|
|
1466
1592
|
'const args = ["-y", "--prefer-online", "livedesk@" + targetProductVersion, "--internal-legacy-client-update"];',
|
|
1467
1593
|
'const quoteCmd = value => "\\"" + String(value).replaceAll("%", "%%").replaceAll("\\"", "\\"\\"") + "\\"";',
|
|
1468
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 };',
|
|
1469
|
-
'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, { env, detached: true, stdio: "ignore", windowsHide: true }); child.once("error", writeFailure); child.once("spawn", () => monitorPreflight(child)); } catch (error) { writeFailure(error); } }',
|
|
1470
|
-
'}',
|
|
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()); } }',
|
|
1471
1597
|
''
|
|
1472
1598
|
].join('\n');
|
|
1473
1599
|
}
|
|
@@ -1477,6 +1603,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1477
1603
|
const statePath = getClientUpdateStatePath();
|
|
1478
1604
|
let targetProductVersion = '';
|
|
1479
1605
|
let targetAgentVersion = '';
|
|
1606
|
+
let updateDeadlineEpochMs = 0;
|
|
1480
1607
|
try {
|
|
1481
1608
|
targetProductVersion = normalizeClientUpdateTargetVersion(
|
|
1482
1609
|
payload.targetProductVersion
|
|
@@ -1496,11 +1623,22 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1496
1623
|
|| targetProductVersion
|
|
1497
1624
|
|| ''
|
|
1498
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);
|
|
1499
1635
|
await writeClientUpdateState('scheduled', {
|
|
1500
1636
|
operationId,
|
|
1501
1637
|
targetVersion: targetAgentVersion,
|
|
1502
1638
|
targetProductVersion,
|
|
1503
1639
|
targetAgentVersion,
|
|
1640
|
+
updateTimeoutMs,
|
|
1641
|
+
updateDeadlineEpochMs,
|
|
1504
1642
|
requestedAt: new Date().toISOString(),
|
|
1505
1643
|
restartVerified: false,
|
|
1506
1644
|
error: ''
|
|
@@ -1520,29 +1658,41 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1520
1658
|
if (!command) {
|
|
1521
1659
|
throw new Error('LiveDesk npx executable was not found.');
|
|
1522
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);
|
|
1523
1692
|
return await new Promise((resolve, reject) => {
|
|
1524
1693
|
const child = spawn(process.execPath, ['-e', buildClientUpdateBootstrapScript()], {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
LIVEDESK_NODE_EXECUTABLE: process.execPath,
|
|
1528
|
-
LIVEDESK_NPX_EXECUTABLE: command,
|
|
1529
|
-
LIVEDESK_UPDATE_WAIT_PID: String(parentPid),
|
|
1530
|
-
LIVEDESK_UPDATE_AGENT_PID: String(process.pid),
|
|
1531
|
-
LIVEDESK_SKIP_BROWSER_OPEN: '1',
|
|
1532
|
-
LIVEDESK_CLIENT_UPDATE_TARGET_VERSION: targetAgentVersion,
|
|
1533
|
-
LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION: targetProductVersion,
|
|
1534
|
-
LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION: targetAgentVersion,
|
|
1535
|
-
LIVEDESK_CLIENT_UPDATE_CURRENT_PRODUCT_VERSION: PRODUCT_VERSION,
|
|
1536
|
-
LIVEDESK_CLIENT_UPDATE_CURRENT_AGENT_VERSION: AGENT_VERSION,
|
|
1537
|
-
LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER: String(
|
|
1538
|
-
process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY
|
|
1539
|
-
|| process.env.LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER
|
|
1540
|
-
|| ''
|
|
1541
|
-
),
|
|
1542
|
-
LIVEDESK_CLIENT_UPDATE_OPERATION_ID: operationId,
|
|
1543
|
-
LIVEDESK_CLIENT_UPDATE_STATE_PATH: statePath,
|
|
1544
|
-
LIVEDESK_CLIENT_UPDATE_CWD: process.cwd()
|
|
1545
|
-
},
|
|
1694
|
+
cwd: neutralCwd,
|
|
1695
|
+
env: updateEnvironment,
|
|
1546
1696
|
detached: true,
|
|
1547
1697
|
stdio: 'ignore',
|
|
1548
1698
|
windowsHide: true
|
|
@@ -1557,6 +1707,7 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1557
1707
|
targetProductVersion,
|
|
1558
1708
|
targetAgentVersion,
|
|
1559
1709
|
operationId,
|
|
1710
|
+
updateDeadlineEpochMs,
|
|
1560
1711
|
statePath,
|
|
1561
1712
|
parentPid,
|
|
1562
1713
|
restartAfterMs: 0
|
|
@@ -1610,8 +1761,27 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
1610
1761
|
commandId: message.commandId,
|
|
1611
1762
|
result
|
|
1612
1763
|
});
|
|
1613
|
-
|
|
1614
|
-
|
|
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
|
+
});
|
|
1615
1785
|
}
|
|
1616
1786
|
} catch (error) {
|
|
1617
1787
|
writeJsonLine(socket, {
|
package/bin/livedesk-client.js
CHANGED
|
@@ -2888,15 +2888,18 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
2888
2888
|
|
|
2889
2889
|
async function chooseClientConnection(supabase, options = {}) {
|
|
2890
2890
|
if (isTruthy(process.env.LIVEDESK_UNIFIED_RUNTIME)) {
|
|
2891
|
-
const connectionPage = createClientRuntimeServer({
|
|
2892
|
-
host: process.env.LIVEDESK_CLIENT_RUNTIME_HOST || '127.0.0.1',
|
|
2893
|
-
port: options.authPort || DEFAULT_AUTH_CALLBACK_PORT,
|
|
2894
|
-
webDist: process.env.LIVEDESK_WEB_DIST || '',
|
|
2891
|
+
const connectionPage = createClientRuntimeServer({
|
|
2892
|
+
host: process.env.LIVEDESK_CLIENT_RUNTIME_HOST || '127.0.0.1',
|
|
2893
|
+
port: options.authPort || DEFAULT_AUTH_CALLBACK_PORT,
|
|
2894
|
+
webDist: process.env.LIVEDESK_WEB_DIST || '',
|
|
2895
2895
|
appVersion: process.env.LIVEDESK_NPM_LAUNCHER_VERSION || readPackageVersion(),
|
|
2896
|
-
deviceId: options.deviceId,
|
|
2897
|
-
engine: options.engine,
|
|
2896
|
+
deviceId: options.deviceId,
|
|
2897
|
+
engine: options.engine,
|
|
2898
2898
|
savedSession: options.savedSession,
|
|
2899
|
+
initialChoice: options.initialChoice,
|
|
2900
|
+
initialChoiceMessage: options.initialChoiceMessage,
|
|
2899
2901
|
beginGoogleSignIn: async redirectTo => {
|
|
2902
|
+
if (!supabase?.auth) throw new Error('supabase-session-required');
|
|
2900
2903
|
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
2901
2904
|
provider: 'google',
|
|
2902
2905
|
options: {
|
|
@@ -2910,15 +2913,22 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
2910
2913
|
return { url: data.url };
|
|
2911
2914
|
},
|
|
2912
2915
|
exchangeGoogleCode: async code => {
|
|
2916
|
+
if (!supabase?.auth) throw new Error('supabase-session-required');
|
|
2913
2917
|
const { data, error } = await supabase.auth.exchangeCodeForSession(code);
|
|
2914
2918
|
if (error) throw error;
|
|
2915
2919
|
if (!data?.session) throw new Error('google-session-missing');
|
|
2916
2920
|
if (!writeSavedSessionToFile(data.session)) throw new Error('refresh-token-required');
|
|
2917
2921
|
return data.session;
|
|
2918
2922
|
},
|
|
2919
|
-
resolvePin: pin =>
|
|
2920
|
-
|
|
2921
|
-
|
|
2923
|
+
resolvePin: pin => {
|
|
2924
|
+
if (!supabase) throw new Error('supabase-session-required');
|
|
2925
|
+
return resolveManagerFromPin(supabase, pin);
|
|
2926
|
+
},
|
|
2927
|
+
changeRole: async (role, snapshot) => {
|
|
2928
|
+
if (!supabase) {
|
|
2929
|
+
return { ok: false, error: 'supabase-session-required' };
|
|
2930
|
+
}
|
|
2931
|
+
const session = await refreshSessionIfNeeded(supabase);
|
|
2922
2932
|
if (!session?.access_token) {
|
|
2923
2933
|
return { ok: false, error: 'supabase-session-required' };
|
|
2924
2934
|
}
|
|
@@ -3255,7 +3265,7 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
3255
3265
|
let connectionPage = null;
|
|
3256
3266
|
let discoverySource = '';
|
|
3257
3267
|
|
|
3258
|
-
if (shouldLogin) {
|
|
3268
|
+
if (shouldLogin) {
|
|
3259
3269
|
const supabase = await createSupabaseClient();
|
|
3260
3270
|
const startupArgs = buildStartupClientArgs(parsed);
|
|
3261
3271
|
let savedSession = null;
|
|
@@ -3362,10 +3372,38 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
3362
3372
|
message: `Connected to LiveDesk Hub at ${manager}.`
|
|
3363
3373
|
});
|
|
3364
3374
|
}
|
|
3365
|
-
console.log(`Found LiveDesk Hub at ${manager}.`);
|
|
3366
|
-
}
|
|
3367
|
-
|
|
3368
|
-
|
|
3375
|
+
console.log(`Found LiveDesk Hub at ${manager}.`);
|
|
3376
|
+
}
|
|
3377
|
+
// Exact-package updates preserve the already paired Hub endpoint and pass
|
|
3378
|
+
// --no-login so the replacement cannot block on browser auth. The unified
|
|
3379
|
+
// launcher must still own its local runtime/status API; the package
|
|
3380
|
+
// supervisor uses that API to prove the new launcher and Agent are stable
|
|
3381
|
+
// before it commits the update.
|
|
3382
|
+
if (!shouldLogin
|
|
3383
|
+
&& !existingConnectionPage
|
|
3384
|
+
&& isTruthy(process.env.LIVEDESK_UNIFIED_RUNTIME)
|
|
3385
|
+
&& manager
|
|
3386
|
+
&& pair) {
|
|
3387
|
+
const preservedSession = readSavedSessionFromFile();
|
|
3388
|
+
const explicitConnection = await chooseClientConnection(null, {
|
|
3389
|
+
authPort: parsed.authPort,
|
|
3390
|
+
deviceId: parsed.deviceId,
|
|
3391
|
+
engine: parsed.engine,
|
|
3392
|
+
savedSession: preservedSession,
|
|
3393
|
+
openBrowser: false,
|
|
3394
|
+
initialChoice: {
|
|
3395
|
+
type: 'explicit',
|
|
3396
|
+
manager,
|
|
3397
|
+
pair,
|
|
3398
|
+
endpointCandidates: [manager],
|
|
3399
|
+
...(preservedSession?.access_token ? { session: preservedSession } : {})
|
|
3400
|
+
},
|
|
3401
|
+
initialChoiceMessage: `Using the existing LiveDesk Hub pairing at ${manager}.`
|
|
3402
|
+
});
|
|
3403
|
+
connectionPage = explicitConnection.connectionPage || null;
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
const slot = normalizeSlotNumber(parsed.slot);
|
|
3369
3407
|
if (manager) {
|
|
3370
3408
|
forwarded = upsertForwardedOption(forwarded, '--manager', manager);
|
|
3371
3409
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.173",
|
|
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.379",
|
|
43
|
+
"@livedesk/fast-osx-arm64": "0.1.379",
|
|
44
|
+
"@livedesk/fast-osx-x64": "0.1.379",
|
|
45
|
+
"@livedesk/fast-win-x64": "0.1.379"
|
|
46
46
|
},
|
|
47
47
|
"publishConfig": {
|
|
48
48
|
"access": "public"
|
|
@@ -514,9 +514,16 @@ function sendText(res, status, body, type = 'text/plain; charset=utf-8') {
|
|
|
514
514
|
res.end(body);
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
-
export function createClientRuntimeServer(options = {}) {
|
|
518
|
-
const host = normalizeString(options.host, 80) || DEFAULT_HOST;
|
|
517
|
+
export function createClientRuntimeServer(options = {}) {
|
|
518
|
+
const host = normalizeString(options.host, 80) || DEFAULT_HOST;
|
|
519
519
|
const port = normalizePort(options.port, DEFAULT_PORT);
|
|
520
|
+
const initialChoice = options.initialChoice && typeof options.initialChoice === 'object'
|
|
521
|
+
? options.initialChoice
|
|
522
|
+
: null;
|
|
523
|
+
const initialChoiceMessage = normalizeString(
|
|
524
|
+
options.initialChoiceMessage,
|
|
525
|
+
1000
|
|
526
|
+
) || 'Existing LiveDesk credentials accepted. Starting the Client.';
|
|
520
527
|
const trustedWebOrigins = createTrustedWebOrigins(port);
|
|
521
528
|
const webDist = resolve(String(options.webDist || process.env.LIVEDESK_WEB_DIST || '').trim() || join(process.cwd(), 'apps', 'web', 'dist'));
|
|
522
529
|
const deviceId = normalizeString(options.deviceId || process.env.LIVEDESK_DEVICE_ID, 160);
|
|
@@ -1030,23 +1037,29 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1030
1037
|
else res.end();
|
|
1031
1038
|
});
|
|
1032
1039
|
});
|
|
1033
|
-
server.once('error', rejectStart);
|
|
1034
|
-
server.listen(port, host, () => {
|
|
1035
|
-
runtime.emit('runtime.http.started', { host, port });
|
|
1036
|
-
|
|
1037
|
-
if (saved.ok) {
|
|
1040
|
+
server.once('error', rejectStart);
|
|
1041
|
+
server.listen(port, host, () => {
|
|
1042
|
+
runtime.emit('runtime.http.started', { host, port });
|
|
1043
|
+
if (initialChoice) {
|
|
1038
1044
|
setImmediate(() => {
|
|
1039
|
-
|
|
1040
|
-
if (!writeSavedSession(saved.session)) throw new Error('refresh-token-required');
|
|
1041
|
-
state.auth.persisted = true;
|
|
1042
|
-
complete({ type: 'google', session: saved.session }, 'Saved sign-in found. Finding the LiveDesk Hub.');
|
|
1043
|
-
} catch (error) {
|
|
1044
|
-
recordAuthAttempt('rejected', `session-persistence-failed:${normalizeString(error?.message || error, 160)}`);
|
|
1045
|
-
}
|
|
1045
|
+
complete(initialChoice, initialChoiceMessage);
|
|
1046
1046
|
});
|
|
1047
|
+
} else {
|
|
1048
|
+
const saved = normalizeRuntimeAuthSession(options.savedSession, { requireRefreshToken: true });
|
|
1049
|
+
if (saved.ok) {
|
|
1050
|
+
setImmediate(() => {
|
|
1051
|
+
try {
|
|
1052
|
+
if (!writeSavedSession(saved.session)) throw new Error('refresh-token-required');
|
|
1053
|
+
state.auth.persisted = true;
|
|
1054
|
+
complete({ type: 'google', session: saved.session }, 'Saved sign-in found. Finding the LiveDesk Hub.');
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
recordAuthAttempt('rejected', `session-persistence-failed:${normalizeString(error?.message || error, 160)}`);
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1047
1060
|
}
|
|
1048
|
-
resolveStart();
|
|
1049
|
-
});
|
|
1061
|
+
resolveStart();
|
|
1062
|
+
});
|
|
1050
1063
|
});
|
|
1051
1064
|
|
|
1052
1065
|
return {
|