@livedesk/client 0.1.236 → 0.1.238
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.
|
@@ -1,59 +1,319 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const net = require('node:net');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const UPDATE_HOST_PROTOCOL_VERSION = 1;
|
|
7
|
+
const UPDATE_HOST_MAX_MESSAGE_BYTES = 2 * 1024 * 1024;
|
|
8
|
+
const bootstrapPath = String(process.env.LIVEDESK_CLIENT_UPDATE_BOOTSTRAP_PATH || '').trim();
|
|
9
|
+
if (bootstrapPath) {
|
|
10
|
+
try { fs.rmSync(bootstrapPath, { force: true }); } catch { /* operation-owned temporary file */ }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
|
|
6
14
|
const originalCwd = path.resolve(String(process.env.LIVEDESK_UPDATE_ORIGINAL_CWD || process.cwd()));
|
|
7
|
-
const statePath = path.resolve(originalCwd, String(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const
|
|
15
|
+
const statePath = path.resolve(originalCwd, String(
|
|
16
|
+
process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(os.homedir(), '.livedesk', 'client-update.json')
|
|
17
|
+
));
|
|
18
|
+
const neutralCwdValue = String(process.env.LIVEDESK_UPDATE_NEUTRAL_CWD || '').trim();
|
|
19
|
+
const neutralCwd = neutralCwdValue ? path.resolve(neutralCwdValue) : '';
|
|
20
|
+
const cleanVersion = value => String(value || '').trim().replace(/^v/i, '');
|
|
21
|
+
const targetProductVersion = cleanVersion(
|
|
22
|
+
process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
|
|
23
|
+
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
|
|
24
|
+
);
|
|
12
25
|
const updateDeadlineEpochMs = Number(process.env.LIVEDESK_CLIENT_UPDATE_DEADLINE_EPOCH_MS || 0);
|
|
13
|
-
const deadlineExpired = () => !Number.isSafeInteger(updateDeadlineEpochMs) || Date.now() >= updateDeadlineEpochMs;
|
|
14
26
|
const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
27
|
+
const stateLockPath = `${statePath}.lock`;
|
|
28
|
+
const lockWaitBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
29
|
+
|
|
30
|
+
function readJson(filePath) {
|
|
31
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isAlive(pid) {
|
|
35
|
+
try {
|
|
36
|
+
process.kill(Number(pid), 0);
|
|
37
|
+
return true;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return error?.code === 'EPERM';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function removeAbandonedStateLock() {
|
|
44
|
+
let owner = null;
|
|
45
|
+
try { owner = JSON.parse(fs.readFileSync(stateLockPath, 'utf8')); } catch { /* checked below */ }
|
|
46
|
+
const ownerPid = Number(owner?.pid || 0);
|
|
47
|
+
if (Number.isInteger(ownerPid) && ownerPid > 1) {
|
|
48
|
+
if (isAlive(ownerPid)) return false;
|
|
49
|
+
} else {
|
|
50
|
+
try {
|
|
51
|
+
if (Date.now() - fs.statSync(stateLockPath).mtimeMs < 5_000) return false;
|
|
52
|
+
} catch {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
fs.rmSync(stateLockPath);
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function withStateLock(callback) {
|
|
65
|
+
fs.mkdirSync(path.dirname(stateLockPath), { recursive: true });
|
|
66
|
+
const deadline = Date.now() + 10_000;
|
|
67
|
+
let descriptor = null;
|
|
68
|
+
let token = '';
|
|
69
|
+
while (descriptor === null) {
|
|
70
|
+
try {
|
|
71
|
+
descriptor = fs.openSync(stateLockPath, 'wx', 0o600);
|
|
72
|
+
token = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
73
|
+
fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, token }), 'utf8');
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (descriptor !== null) {
|
|
76
|
+
try { fs.closeSync(descriptor); } catch { /* best effort */ }
|
|
77
|
+
descriptor = null;
|
|
78
|
+
try { fs.rmSync(stateLockPath, { force: true }); } catch { /* best effort */ }
|
|
79
|
+
}
|
|
80
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
81
|
+
if (removeAbandonedStateLock()) continue;
|
|
82
|
+
if (Date.now() >= deadline) throw new Error(`Timed out waiting for the LiveDesk update state lock: ${stateLockPath}`);
|
|
83
|
+
Atomics.wait(lockWaitBuffer, 0, 0, Math.min(25, Math.max(1, deadline - Date.now())));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return callback();
|
|
88
|
+
} finally {
|
|
89
|
+
try { fs.closeSync(descriptor); } catch { /* best effort */ }
|
|
90
|
+
try {
|
|
91
|
+
const owner = JSON.parse(fs.readFileSync(stateLockPath, 'utf8'));
|
|
92
|
+
if (owner?.token === token && Number(owner?.pid) === process.pid) fs.rmSync(stateLockPath, { force: true });
|
|
93
|
+
} catch { /* another owner already replaced a stale lock */ }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function writeState(mutator, suffix) {
|
|
98
|
+
return withStateLock(() => {
|
|
99
|
+
const previous = readJson(statePath) || {};
|
|
100
|
+
const next = mutator(previous);
|
|
101
|
+
if (!next) return previous;
|
|
102
|
+
const temporary = `${statePath}.${process.pid}.${suffix}.tmp`;
|
|
103
|
+
fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
104
|
+
fs.renameSync(temporary, statePath);
|
|
105
|
+
return next;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeFailure(error, cancelRequested = false) {
|
|
110
|
+
try {
|
|
111
|
+
writeState(previous => {
|
|
112
|
+
if (previous.operationId && previous.operationId !== operationId) return null;
|
|
113
|
+
if (['preflight-ready', 'waiting-for-shutdown', 'connected', 'restored'].includes(previous.stage)) return null;
|
|
114
|
+
const now = new Date().toISOString();
|
|
115
|
+
return {
|
|
116
|
+
...previous,
|
|
117
|
+
operationId,
|
|
118
|
+
stage: 'failed',
|
|
119
|
+
targetProductVersion,
|
|
120
|
+
restartVerified: false,
|
|
121
|
+
cancelRequested: cancelRequested || previous.cancelRequested === true,
|
|
122
|
+
error: String(error?.message || error).slice(0, 4000),
|
|
123
|
+
failedAt: now,
|
|
124
|
+
updatedAt: now
|
|
125
|
+
};
|
|
126
|
+
}, 'handoff-failed');
|
|
127
|
+
} catch { /* preserve the original handoff failure */ }
|
|
128
|
+
process.stderr.write(`LiveDesk update handoff failed: ${error?.message || error}\n`);
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function prepareNeutralCwd() {
|
|
133
|
+
if (!neutralCwd) throw new Error('LiveDesk update neutral working directory is unavailable.');
|
|
134
|
+
fs.mkdirSync(neutralCwd, { recursive: true });
|
|
135
|
+
const unexpectedEntry = fs.readdirSync(neutralCwd)[0];
|
|
136
|
+
if (unexpectedEntry) {
|
|
137
|
+
throw new Error(`LiveDesk update neutral working directory is shadowed by ${path.join(neutralCwd, unexpectedEntry)}.`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function writeHandoffStarted() {
|
|
142
|
+
return writeState(previous => {
|
|
143
|
+
if (Date.now() >= updateDeadlineEpochMs) {
|
|
144
|
+
throw new Error('The absolute LiveDesk Client update deadline expired before handoff.');
|
|
145
|
+
}
|
|
146
|
+
if (previous.operationId !== operationId) {
|
|
147
|
+
throw new Error('LiveDesk update handoff was superseded before exact-package launch.');
|
|
148
|
+
}
|
|
149
|
+
const now = new Date().toISOString();
|
|
150
|
+
return {
|
|
151
|
+
...previous,
|
|
152
|
+
operationId,
|
|
153
|
+
stage: 'handoff-started',
|
|
154
|
+
starterPid: process.pid,
|
|
155
|
+
updateDeadlineEpochMs,
|
|
156
|
+
restartVerified: false,
|
|
157
|
+
error: '',
|
|
158
|
+
updatedAt: now
|
|
159
|
+
};
|
|
160
|
+
}, 'handoff-started');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readUpdateHostStatus() {
|
|
164
|
+
const statusPath = String(process.env.LIVEDESK_UPDATE_HOST_STATUS_PATH || '').trim();
|
|
165
|
+
return statusPath ? readJson(statusPath) : null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function submitUpdateHostJob(job) {
|
|
169
|
+
return new Promise((resolveRequest, rejectRequest) => {
|
|
170
|
+
const status = readUpdateHostStatus();
|
|
171
|
+
const endpoint = String(process.env.LIVEDESK_UPDATE_HOST_ENDPOINT || status?.endpoint || '').trim();
|
|
172
|
+
const token = String(status?.token || '');
|
|
173
|
+
if (process.env.LIVEDESK_UPDATE_HOST_AVAILABLE !== '1'
|
|
174
|
+
|| Number(process.env.LIVEDESK_UPDATE_HOST_PROTOCOL_VERSION || 0) !== UPDATE_HOST_PROTOCOL_VERSION
|
|
175
|
+
|| status?.protocolVersion !== UPDATE_HOST_PROTOCOL_VERSION
|
|
176
|
+
|| !endpoint
|
|
177
|
+
|| !/^[a-f0-9]{64}$/i.test(token)) {
|
|
178
|
+
rejectRequest(new Error('The independent LiveDesk Update Host is unavailable; the existing Client was left running.'));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const request = JSON.stringify({
|
|
182
|
+
protocolVersion: UPDATE_HOST_PROTOCOL_VERSION,
|
|
183
|
+
token,
|
|
184
|
+
type: 'run-worker',
|
|
185
|
+
job
|
|
186
|
+
});
|
|
187
|
+
if (Buffer.byteLength(request, 'utf8') > UPDATE_HOST_MAX_MESSAGE_BYTES) {
|
|
188
|
+
rejectRequest(new Error('The LiveDesk Client update request exceeded the Update Host message limit.'));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const socket = net.createConnection(endpoint);
|
|
192
|
+
socket.setEncoding('utf8');
|
|
193
|
+
let responseText = '';
|
|
194
|
+
let settled = false;
|
|
195
|
+
const finish = (error, value) => {
|
|
196
|
+
if (settled) return;
|
|
197
|
+
settled = true;
|
|
198
|
+
clearTimeout(timeout);
|
|
199
|
+
socket.destroy();
|
|
200
|
+
if (error) rejectRequest(error);
|
|
201
|
+
else resolveRequest(value);
|
|
202
|
+
};
|
|
203
|
+
const timeout = setTimeout(() => {
|
|
204
|
+
finish(new Error('Timed out registering the Client update with the independent Update Host.'));
|
|
205
|
+
}, 10_000);
|
|
206
|
+
socket.once('connect', () => socket.write(`${request}\n`));
|
|
207
|
+
socket.on('data', chunk => {
|
|
208
|
+
responseText += chunk;
|
|
209
|
+
if (Buffer.byteLength(responseText, 'utf8') > UPDATE_HOST_MAX_MESSAGE_BYTES) {
|
|
210
|
+
finish(new Error('The LiveDesk Update Host response exceeded its message limit.'));
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
socket.once('error', error => finish(error));
|
|
214
|
+
socket.once('end', () => {
|
|
215
|
+
try {
|
|
216
|
+
const response = JSON.parse(responseText || '{}');
|
|
217
|
+
if (response?.ok !== true || response?.accepted !== true || Number(response?.job?.workerPid || 0) <= 1) {
|
|
218
|
+
throw new Error(String(response?.error || 'The LiveDesk Update Host did not accept the Client update.'));
|
|
219
|
+
}
|
|
220
|
+
finish(null, response);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
finish(error);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function resolveExecutable(candidate) {
|
|
229
|
+
const value = String(candidate || '').trim();
|
|
230
|
+
if (!value) return '';
|
|
231
|
+
if (path.isAbsolute(value)) return fs.existsSync(value) ? value : '';
|
|
232
|
+
const extensions = process.platform === 'win32'
|
|
233
|
+
? String(process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';')
|
|
234
|
+
: [''];
|
|
235
|
+
for (const directory of String(process.env.PATH || process.env.Path || '').split(path.delimiter)) {
|
|
236
|
+
if (!directory) continue;
|
|
237
|
+
for (const extension of extensions) {
|
|
238
|
+
const filePath = path.join(directory, `${value}${extension}`);
|
|
239
|
+
if (fs.existsSync(filePath)) return path.resolve(filePath);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return '';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function buildWorkerInvocation(env) {
|
|
246
|
+
const nodeDirectory = path.dirname(process.execPath);
|
|
247
|
+
const npxCli = [
|
|
248
|
+
env.LIVEDESK_NPX_CLI_PATH,
|
|
249
|
+
env.npm_execpath ? path.join(path.dirname(env.npm_execpath), 'npx-cli.js') : '',
|
|
250
|
+
env.LIVEDESK_NPX_EXECUTABLE
|
|
251
|
+
? path.join(path.dirname(env.LIVEDESK_NPX_EXECUTABLE), 'node_modules', 'npm', 'bin', 'npx-cli.js')
|
|
252
|
+
: '',
|
|
253
|
+
path.join(nodeDirectory, 'node_modules', 'npm', 'bin', 'npx-cli.js')
|
|
254
|
+
].find(value => value && fs.existsSync(value));
|
|
255
|
+
const args = [
|
|
256
|
+
'-y',
|
|
257
|
+
'--prefer-online',
|
|
258
|
+
'--prefix',
|
|
259
|
+
neutralCwd,
|
|
260
|
+
'--workspaces=false',
|
|
261
|
+
`livedesk@${targetProductVersion}`,
|
|
262
|
+
'--internal-legacy-client-update'
|
|
263
|
+
];
|
|
264
|
+
if (npxCli) return { command: process.execPath, args: [npxCli, ...args] };
|
|
265
|
+
const npxExecutable = resolveExecutable(env.LIVEDESK_NPX_EXECUTABLE || (process.platform === 'win32' ? 'npx.cmd' : 'npx'));
|
|
266
|
+
return { command: npxExecutable, args };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function main() {
|
|
270
|
+
if (!operationId || !versionPattern.test(targetProductVersion)
|
|
271
|
+
|| !Number.isSafeInteger(updateDeadlineEpochMs) || Date.now() >= updateDeadlineEpochMs) {
|
|
272
|
+
throw new Error('Invalid or expired LiveDesk exact-package update handoff.');
|
|
273
|
+
}
|
|
274
|
+
prepareNeutralCwd();
|
|
275
|
+
writeHandoffStarted();
|
|
276
|
+
|
|
277
|
+
const env = { ...process.env, LIVEDESK_UPDATE_STARTER_PID: String(process.pid) };
|
|
278
|
+
const isolated = new Set([
|
|
279
|
+
'init_cwd', 'npm_config_local_prefix', 'npm_config_workspace', 'npm_config_workspaces',
|
|
280
|
+
'npm_config_include_workspace_root', 'npm_package_json', 'npm_lifecycle_event', 'npm_lifecycle_script'
|
|
281
|
+
]);
|
|
282
|
+
for (const key of Object.keys(env)) {
|
|
283
|
+
if (isolated.has(key.toLowerCase())) delete env[key];
|
|
284
|
+
}
|
|
285
|
+
env.INIT_CWD = neutralCwd;
|
|
286
|
+
env.npm_config_local_prefix = neutralCwd;
|
|
287
|
+
env.npm_config_workspaces = 'false';
|
|
288
|
+
env.npm_config_include_workspace_root = 'false';
|
|
289
|
+
|
|
290
|
+
const invocation = buildWorkerInvocation(env);
|
|
291
|
+
if (!path.isAbsolute(invocation.command) || !fs.existsSync(invocation.command)) {
|
|
292
|
+
throw new Error('LiveDesk could not prepare an absolute Client update worker command for the independent Update Host.');
|
|
293
|
+
}
|
|
294
|
+
const result = await submitUpdateHostJob({
|
|
295
|
+
jobId: `client-${operationId.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 160)}`,
|
|
296
|
+
kind: 'client-update',
|
|
297
|
+
operationId,
|
|
298
|
+
command: invocation.command,
|
|
299
|
+
args: invocation.args,
|
|
300
|
+
cwd: neutralCwd,
|
|
301
|
+
env,
|
|
302
|
+
resultPath: statePath,
|
|
303
|
+
requestedAt: new Date().toISOString(),
|
|
304
|
+
expiresAt: new Date(updateDeadlineEpochMs).toISOString()
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
writeState(previous => {
|
|
308
|
+
if (previous.operationId !== operationId || previous.stage !== 'handoff-started') return null;
|
|
309
|
+
return {
|
|
310
|
+
...previous,
|
|
311
|
+
updateHostPid: Number(result?.host?.pid || 0),
|
|
312
|
+
updateHostJobId: String(result?.job?.jobId || ''),
|
|
313
|
+
updateWorkerPid: Number(result?.job?.workerPid || 0),
|
|
314
|
+
updatedAt: new Date().toISOString()
|
|
315
|
+
};
|
|
316
|
+
}, 'host-accepted');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
main().catch(error => writeFailure(error, Date.now() >= updateDeadlineEpochMs));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.238",
|
|
4
4
|
"description": "LiveDesk local remote client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"ws": "^8.18.3"
|
|
43
43
|
},
|
|
44
44
|
"optionalDependencies": {
|
|
45
|
-
"@livedesk/fast-linux-x64": "0.1.
|
|
46
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
47
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
48
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
45
|
+
"@livedesk/fast-linux-x64": "0.1.436",
|
|
46
|
+
"@livedesk/fast-osx-arm64": "0.1.436",
|
|
47
|
+
"@livedesk/fast-osx-x64": "0.1.436",
|
|
48
|
+
"@livedesk/fast-win-x64": "0.1.436"
|
|
49
49
|
},
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|