@livedesk/client 0.1.215 → 0.1.216
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 +506 -555
- package/bin/livedesk-client-update-bootstrap.cjs +59 -0
- package/bin/livedesk-client.js +195 -58
- package/package.json +11 -11
- package/src/runtime/agent-process-lifecycle.js +85 -23
- package/src/runtime/client-runtime-server.js +719 -131
- package/src/runtime/hub-wake-listener.js +42 -11
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import net from 'net';
|
|
4
4
|
import os from 'os';
|
|
@@ -7,259 +7,263 @@ import crypto from 'crypto';
|
|
|
7
7
|
import { existsSync, promises as fs, statfsSync } from 'fs';
|
|
8
8
|
import { spawn } from 'child_process';
|
|
9
9
|
import { createRequire } from 'node:module';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
10
11
|
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
12
|
-
const
|
|
13
|
+
const CLIENT_UPDATE_BOOTSTRAP_PATH = fileURLToPath(
|
|
14
|
+
new URL('./livedesk-client-update-bootstrap.cjs', import.meta.url)
|
|
15
|
+
);
|
|
16
|
+
const AGENT_VERSION = (() => {
|
|
13
17
|
try {
|
|
14
18
|
return String(require('../package.json').version || '0.0.0');
|
|
15
19
|
} catch {
|
|
16
20
|
return '0.0.0';
|
|
17
|
-
}
|
|
18
|
-
})();
|
|
19
|
-
const PRODUCT_VERSION = String(process.env.LIVEDESK_NPM_LAUNCHER_VERSION || AGENT_VERSION).trim() || AGENT_VERSION;
|
|
20
|
-
const DEFAULT_MANAGER = '127.0.0.1:5197';
|
|
21
|
+
}
|
|
22
|
+
})();
|
|
23
|
+
const PRODUCT_VERSION = String(process.env.LIVEDESK_NPM_LAUNCHER_VERSION || AGENT_VERSION).trim() || AGENT_VERSION;
|
|
24
|
+
const DEFAULT_MANAGER = '127.0.0.1:5197';
|
|
21
25
|
const DEFAULT_HEARTBEAT_MS = 5000;
|
|
22
|
-
const DEFAULT_RECONNECT_MS = 5000;
|
|
23
|
-
const EXIT_INVALID_PAIR_TOKEN = 23;
|
|
24
|
-
const EXIT_CLIENT_UPDATE = 42;
|
|
25
|
-
const DEFAULT_LIVE_FPS = 30;
|
|
26
|
-
const MAX_LIVE_FPS = 30;
|
|
27
|
-
const MAX_FRAME_BASE64_CHARS = 3 * 1024 * 1024;
|
|
28
|
-
const MAX_FILE_TRANSFER_FILES = 24;
|
|
26
|
+
const DEFAULT_RECONNECT_MS = 5000;
|
|
27
|
+
const EXIT_INVALID_PAIR_TOKEN = 23;
|
|
28
|
+
const EXIT_CLIENT_UPDATE = 42;
|
|
29
|
+
const DEFAULT_LIVE_FPS = 30;
|
|
30
|
+
const MAX_LIVE_FPS = 30;
|
|
31
|
+
const MAX_FRAME_BASE64_CHARS = 3 * 1024 * 1024;
|
|
32
|
+
const MAX_FILE_TRANSFER_FILES = 24;
|
|
29
33
|
const MAX_FILE_TRANSFER_BYTES = 24 * 1024 * 1024;
|
|
30
34
|
const MAX_AGENT_OUTPUT_CHARS = 32000;
|
|
31
|
-
const launchedProcessRegistry = new Map();
|
|
32
|
-
const CLIENT_UPDATE_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
33
|
-
|
|
34
|
-
function normalizeClientUpdateTargetVersion(value) {
|
|
35
|
-
const version = String(value || '').trim().replace(/^v/i, '');
|
|
36
|
-
if (!version) return '';
|
|
37
|
-
if (!CLIENT_UPDATE_VERSION_PATTERN.test(version)) {
|
|
38
|
-
throw new Error(`Invalid LiveDesk product update version: ${version}`);
|
|
39
|
-
}
|
|
40
|
-
return version;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function getClientUpdateStatePath() {
|
|
44
|
-
return path.resolve(
|
|
45
|
-
String(process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(os.homedir(), '.livedesk', 'client-update.json'))
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function getClientUpdateNeutralCwd(operationId) {
|
|
50
|
-
const safeOperationId = String(operationId || '')
|
|
51
|
-
.replace(/[^A-Za-z0-9._-]/g, '_')
|
|
52
|
-
.slice(0, 120) || 'unknown';
|
|
53
|
-
return path.join(os.tmpdir(), 'livedesk-update-cwd', `client-${safeOperationId}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function prepareClientUpdateNeutralCwd(operationId) {
|
|
57
|
-
const neutralCwd = path.resolve(getClientUpdateNeutralCwd(operationId));
|
|
58
|
-
await fs.mkdir(neutralCwd, { recursive: true });
|
|
59
|
-
const unexpectedEntry = (await fs.readdir(neutralCwd))[0];
|
|
60
|
-
if (unexpectedEntry) {
|
|
61
|
-
const shadowPath = path.join(neutralCwd, unexpectedEntry);
|
|
62
|
-
throw new Error(`LiveDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`);
|
|
63
|
-
}
|
|
64
|
-
return neutralCwd;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function buildClientUpdateNpxEnvironment(baseEnv, neutralCwd) {
|
|
68
|
-
const env = { ...baseEnv };
|
|
69
|
-
const isolatedNames = new Set([
|
|
70
|
-
'init_cwd',
|
|
71
|
-
'npm_config_local_prefix',
|
|
72
|
-
'npm_config_workspace',
|
|
73
|
-
'npm_config_workspaces',
|
|
74
|
-
'npm_config_include_workspace_root',
|
|
75
|
-
'npm_package_json',
|
|
76
|
-
'npm_lifecycle_event',
|
|
77
|
-
'npm_lifecycle_script'
|
|
78
|
-
]);
|
|
79
|
-
for (const key of Object.keys(env)) {
|
|
80
|
-
if (isolatedNames.has(key.toLowerCase())) delete env[key];
|
|
81
|
-
}
|
|
82
|
-
env.INIT_CWD = neutralCwd;
|
|
83
|
-
env.npm_config_local_prefix = neutralCwd;
|
|
84
|
-
env.npm_config_workspaces = 'false';
|
|
85
|
-
env.npm_config_include_workspace_root = 'false';
|
|
86
|
-
return env;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function isClientUpdatePidAlive(value) {
|
|
90
|
-
const pid = Number(value);
|
|
91
|
-
if (!Number.isInteger(pid) || pid <= 1) return false;
|
|
92
|
-
try {
|
|
93
|
-
process.kill(pid, 0);
|
|
94
|
-
return true;
|
|
95
|
-
} catch (error) {
|
|
96
|
-
return error?.code === 'EPERM';
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function canReclaimClientUpdateOperation(previous) {
|
|
101
|
-
const handoffOwners = [
|
|
102
|
-
previous?.supervisorPid,
|
|
103
|
-
previous?.starterPid
|
|
104
|
-
].map(Number).filter(pid => Number.isInteger(pid) && pid > 1);
|
|
105
|
-
if (handoffOwners.length > 0) {
|
|
106
|
-
return handoffOwners.every(pid => !isClientUpdatePidAlive(pid));
|
|
107
|
-
}
|
|
108
|
-
if (String(previous?.stage || '') === 'scheduled') {
|
|
109
|
-
const schedulingOwners = [
|
|
110
|
-
previous?.agentPid,
|
|
111
|
-
previous?.launcherPid
|
|
112
|
-
].map(Number).filter(pid => Number.isInteger(pid) && pid > 1);
|
|
113
|
-
return schedulingOwners.length > 0
|
|
114
|
-
&& schedulingOwners.every(pid => !isClientUpdatePidAlive(pid));
|
|
115
|
-
}
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function writeClientUpdateState(stage, details = {}) {
|
|
120
|
-
const statePath = getClientUpdateStatePath();
|
|
121
|
-
const operationId = String(details.operationId || process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
|
|
122
|
-
if (!operationId) return;
|
|
123
|
-
const attemptId = stage === 'scheduled'
|
|
124
|
-
? ''
|
|
125
|
-
: String(details.attemptId || process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim();
|
|
126
|
-
let previous = null;
|
|
127
|
-
try {
|
|
128
|
-
previous = JSON.parse(await fs.readFile(statePath, 'utf8'));
|
|
129
|
-
} catch {
|
|
130
|
-
// The first update stage creates the file.
|
|
131
|
-
}
|
|
132
|
-
const replacesTerminalOperation = previous?.operationId
|
|
133
|
-
&& previous.operationId !== operationId
|
|
134
|
-
&& stage === 'scheduled'
|
|
135
|
-
&& ['connected', 'failed', 'restored'].includes(String(previous.stage || ''));
|
|
136
|
-
const reclaimsDeadOperation = previous?.operationId
|
|
137
|
-
&& previous.operationId !== operationId
|
|
138
|
-
&& stage === 'scheduled'
|
|
139
|
-
&& canReclaimClientUpdateOperation(previous);
|
|
140
|
-
const replacesPreviousOperation = replacesTerminalOperation || reclaimsDeadOperation;
|
|
141
|
-
if (previous?.operationId && previous.operationId !== operationId && !replacesPreviousOperation) return;
|
|
142
|
-
if (!replacesPreviousOperation
|
|
143
|
-
&& previous?.attemptId
|
|
144
|
-
&& attemptId
|
|
145
|
-
&& previous.attemptId !== attemptId) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (!replacesPreviousOperation
|
|
149
|
-
&& previous?.stage === 'connected'
|
|
150
|
-
&& previous?.restartVerified === true
|
|
151
|
-
&& stage !== 'connected') {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
const targetProductVersion = String(
|
|
155
|
-
details.targetProductVersion
|
|
156
|
-
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
|
|
157
|
-
|| (!replacesPreviousOperation ? previous?.targetProductVersion : '')
|
|
158
|
-
|| ''
|
|
159
|
-
).trim();
|
|
160
|
-
const targetAgentVersion = String(
|
|
161
|
-
details.targetAgentVersion
|
|
162
|
-
|| details.targetVersion
|
|
163
|
-
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION
|
|
164
|
-
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
|
|
165
|
-
|| (!replacesPreviousOperation ? previous?.targetAgentVersion : '')
|
|
166
|
-
|| (!replacesPreviousOperation ? previous?.targetVersion : '')
|
|
167
|
-
|| ''
|
|
168
|
-
).trim();
|
|
169
|
-
const state = {
|
|
170
|
-
...(!replacesPreviousOperation && previous && typeof previous === 'object' ? previous : {}),
|
|
171
|
-
operationId,
|
|
172
|
-
stage,
|
|
173
|
-
targetVersion: targetAgentVersion,
|
|
174
|
-
targetProductVersion,
|
|
175
|
-
targetAgentVersion,
|
|
176
|
-
productVersion: PRODUCT_VERSION,
|
|
177
|
-
agentVersion: AGENT_VERSION,
|
|
178
|
-
agentPid: process.pid,
|
|
179
|
-
launcherPid: Number(process.env.LIVEDESK_CLIENT_PARENT_PID || 0) || 0,
|
|
180
|
-
attemptId,
|
|
181
|
-
updatedAt: new Date().toISOString(),
|
|
182
|
-
...details
|
|
183
|
-
};
|
|
184
|
-
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
185
|
-
const temporaryPath = `${statePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
186
|
-
try {
|
|
187
|
-
await fs.writeFile(temporaryPath, JSON.stringify(state, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
188
|
-
let current = null;
|
|
189
|
-
try {
|
|
190
|
-
current = JSON.parse(await fs.readFile(statePath, 'utf8'));
|
|
191
|
-
} catch {
|
|
192
|
-
// A missing first-write state is still owned by this transition.
|
|
193
|
-
}
|
|
194
|
-
if (String(current?.operationId || '') !== String(previous?.operationId || '')
|
|
195
|
-
|| String(current?.stage || '') !== String(previous?.stage || '')
|
|
196
|
-
|| String(current?.updatedAt || '') !== String(previous?.updatedAt || '')) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
await fs.rename(temporaryPath, statePath);
|
|
200
|
-
} finally {
|
|
201
|
-
await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async function waitForClientUpdateShutdownSignal(
|
|
206
|
-
operationId,
|
|
207
|
-
updateDeadlineEpochMs,
|
|
208
|
-
timeoutMs = 180_000
|
|
209
|
-
) {
|
|
210
|
-
const statePath = getClientUpdateStatePath();
|
|
211
|
-
const deadline = Math.min(
|
|
212
|
-
Date.now() + timeoutMs,
|
|
213
|
-
Number(updateDeadlineEpochMs) || 0
|
|
214
|
-
);
|
|
215
|
-
while (Date.now() < deadline) {
|
|
216
|
-
try {
|
|
217
|
-
const state = JSON.parse(await fs.readFile(statePath, 'utf8'));
|
|
218
|
-
if (state?.operationId !== operationId) {
|
|
219
|
-
await wait(200);
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
if (state.stage === 'preflight-ready' || state.stage === 'waiting-for-shutdown') {
|
|
223
|
-
return { ready: true, error: '' };
|
|
224
|
-
}
|
|
225
|
-
if (state.stage === 'failed') {
|
|
226
|
-
const error = String(state.error || 'LiveDesk client update preparation failed.');
|
|
227
|
-
console.error(`LiveDesk client update preparation failed: ${error}`);
|
|
228
|
-
return { ready: false, error };
|
|
229
|
-
}
|
|
230
|
-
} catch {
|
|
231
|
-
// The detached supervisor may still be writing its first stage.
|
|
232
|
-
}
|
|
233
|
-
await wait(200);
|
|
234
|
-
}
|
|
235
|
-
const deadlineExpired = Date.now() >= Number(updateDeadlineEpochMs);
|
|
236
|
-
const error = deadlineExpired
|
|
237
|
-
? 'The absolute LiveDesk Client update deadline expired before shutdown.'
|
|
238
|
-
: 'Timed out waiting for the LiveDesk update supervisor to prepare the exact package.';
|
|
239
|
-
await writeClientUpdateState('failed', {
|
|
240
|
-
operationId,
|
|
241
|
-
error,
|
|
242
|
-
cancelRequested: true,
|
|
243
|
-
deadlineExpired,
|
|
244
|
-
failedAt: new Date().toISOString()
|
|
245
|
-
}).catch(() => undefined);
|
|
246
|
-
console.error('LiveDesk client update preparation timed out; the current Agent will remain running.');
|
|
247
|
-
return { ready: false, error };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function markClientUpdateConnected() {
|
|
251
|
-
const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
|
|
252
|
-
if (!operationId) return;
|
|
253
|
-
void writeClientUpdateState('connected', {
|
|
254
|
-
operationId,
|
|
255
|
-
attemptId: String(process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim(),
|
|
256
|
-
connectedAt: new Date().toISOString(),
|
|
257
|
-
restartVerified: true,
|
|
258
|
-
error: ''
|
|
259
|
-
}).catch(error => {
|
|
260
|
-
console.error(`LiveDesk client update diagnostics could not be persisted: ${error?.message || error}`);
|
|
261
|
-
});
|
|
262
|
-
}
|
|
35
|
+
const launchedProcessRegistry = new Map();
|
|
36
|
+
const CLIENT_UPDATE_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
37
|
+
|
|
38
|
+
function normalizeClientUpdateTargetVersion(value) {
|
|
39
|
+
const version = String(value || '').trim().replace(/^v/i, '');
|
|
40
|
+
if (!version) return '';
|
|
41
|
+
if (!CLIENT_UPDATE_VERSION_PATTERN.test(version)) {
|
|
42
|
+
throw new Error(`Invalid LiveDesk product update version: ${version}`);
|
|
43
|
+
}
|
|
44
|
+
return version;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getClientUpdateStatePath() {
|
|
48
|
+
return path.resolve(
|
|
49
|
+
String(process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(os.homedir(), '.livedesk', 'client-update.json'))
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getClientUpdateNeutralCwd(operationId) {
|
|
54
|
+
const safeOperationId = String(operationId || '')
|
|
55
|
+
.replace(/[^A-Za-z0-9._-]/g, '_')
|
|
56
|
+
.slice(0, 120) || 'unknown';
|
|
57
|
+
return path.join(os.tmpdir(), 'livedesk-update-cwd', `client-${safeOperationId}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function prepareClientUpdateNeutralCwd(operationId) {
|
|
61
|
+
const neutralCwd = path.resolve(getClientUpdateNeutralCwd(operationId));
|
|
62
|
+
await fs.mkdir(neutralCwd, { recursive: true });
|
|
63
|
+
const unexpectedEntry = (await fs.readdir(neutralCwd))[0];
|
|
64
|
+
if (unexpectedEntry) {
|
|
65
|
+
const shadowPath = path.join(neutralCwd, unexpectedEntry);
|
|
66
|
+
throw new Error(`LiveDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`);
|
|
67
|
+
}
|
|
68
|
+
return neutralCwd;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildClientUpdateNpxEnvironment(baseEnv, neutralCwd) {
|
|
72
|
+
const env = { ...baseEnv };
|
|
73
|
+
const isolatedNames = new Set([
|
|
74
|
+
'init_cwd',
|
|
75
|
+
'npm_config_local_prefix',
|
|
76
|
+
'npm_config_workspace',
|
|
77
|
+
'npm_config_workspaces',
|
|
78
|
+
'npm_config_include_workspace_root',
|
|
79
|
+
'npm_package_json',
|
|
80
|
+
'npm_lifecycle_event',
|
|
81
|
+
'npm_lifecycle_script'
|
|
82
|
+
]);
|
|
83
|
+
for (const key of Object.keys(env)) {
|
|
84
|
+
if (isolatedNames.has(key.toLowerCase())) delete env[key];
|
|
85
|
+
}
|
|
86
|
+
env.INIT_CWD = neutralCwd;
|
|
87
|
+
env.npm_config_local_prefix = neutralCwd;
|
|
88
|
+
env.npm_config_workspaces = 'false';
|
|
89
|
+
env.npm_config_include_workspace_root = 'false';
|
|
90
|
+
return env;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isClientUpdatePidAlive(value) {
|
|
94
|
+
const pid = Number(value);
|
|
95
|
+
if (!Number.isInteger(pid) || pid <= 1) return false;
|
|
96
|
+
try {
|
|
97
|
+
process.kill(pid, 0);
|
|
98
|
+
return true;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return error?.code === 'EPERM';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function canReclaimClientUpdateOperation(previous) {
|
|
105
|
+
const handoffOwners = [
|
|
106
|
+
previous?.supervisorPid,
|
|
107
|
+
previous?.starterPid
|
|
108
|
+
].map(Number).filter(pid => Number.isInteger(pid) && pid > 1);
|
|
109
|
+
if (handoffOwners.length > 0) {
|
|
110
|
+
return handoffOwners.every(pid => !isClientUpdatePidAlive(pid));
|
|
111
|
+
}
|
|
112
|
+
if (String(previous?.stage || '') === 'scheduled') {
|
|
113
|
+
const schedulingOwners = [
|
|
114
|
+
previous?.agentPid,
|
|
115
|
+
previous?.launcherPid
|
|
116
|
+
].map(Number).filter(pid => Number.isInteger(pid) && pid > 1);
|
|
117
|
+
return schedulingOwners.length > 0
|
|
118
|
+
&& schedulingOwners.every(pid => !isClientUpdatePidAlive(pid));
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function writeClientUpdateState(stage, details = {}) {
|
|
124
|
+
const statePath = getClientUpdateStatePath();
|
|
125
|
+
const operationId = String(details.operationId || process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
|
|
126
|
+
if (!operationId) return;
|
|
127
|
+
const attemptId = stage === 'scheduled'
|
|
128
|
+
? ''
|
|
129
|
+
: String(details.attemptId || process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim();
|
|
130
|
+
let previous = null;
|
|
131
|
+
try {
|
|
132
|
+
previous = JSON.parse(await fs.readFile(statePath, 'utf8'));
|
|
133
|
+
} catch {
|
|
134
|
+
// The first update stage creates the file.
|
|
135
|
+
}
|
|
136
|
+
const replacesTerminalOperation = previous?.operationId
|
|
137
|
+
&& previous.operationId !== operationId
|
|
138
|
+
&& stage === 'scheduled'
|
|
139
|
+
&& ['connected', 'failed', 'restored'].includes(String(previous.stage || ''));
|
|
140
|
+
const reclaimsDeadOperation = previous?.operationId
|
|
141
|
+
&& previous.operationId !== operationId
|
|
142
|
+
&& stage === 'scheduled'
|
|
143
|
+
&& canReclaimClientUpdateOperation(previous);
|
|
144
|
+
const replacesPreviousOperation = replacesTerminalOperation || reclaimsDeadOperation;
|
|
145
|
+
if (previous?.operationId && previous.operationId !== operationId && !replacesPreviousOperation) return;
|
|
146
|
+
if (!replacesPreviousOperation
|
|
147
|
+
&& previous?.attemptId
|
|
148
|
+
&& attemptId
|
|
149
|
+
&& previous.attemptId !== attemptId) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!replacesPreviousOperation
|
|
153
|
+
&& previous?.stage === 'connected'
|
|
154
|
+
&& previous?.restartVerified === true
|
|
155
|
+
&& stage !== 'connected') {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const targetProductVersion = String(
|
|
159
|
+
details.targetProductVersion
|
|
160
|
+
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
|
|
161
|
+
|| (!replacesPreviousOperation ? previous?.targetProductVersion : '')
|
|
162
|
+
|| ''
|
|
163
|
+
).trim();
|
|
164
|
+
const targetAgentVersion = String(
|
|
165
|
+
details.targetAgentVersion
|
|
166
|
+
|| details.targetVersion
|
|
167
|
+
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION
|
|
168
|
+
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
|
|
169
|
+
|| (!replacesPreviousOperation ? previous?.targetAgentVersion : '')
|
|
170
|
+
|| (!replacesPreviousOperation ? previous?.targetVersion : '')
|
|
171
|
+
|| ''
|
|
172
|
+
).trim();
|
|
173
|
+
const state = {
|
|
174
|
+
...(!replacesPreviousOperation && previous && typeof previous === 'object' ? previous : {}),
|
|
175
|
+
operationId,
|
|
176
|
+
stage,
|
|
177
|
+
targetVersion: targetAgentVersion,
|
|
178
|
+
targetProductVersion,
|
|
179
|
+
targetAgentVersion,
|
|
180
|
+
productVersion: PRODUCT_VERSION,
|
|
181
|
+
agentVersion: AGENT_VERSION,
|
|
182
|
+
agentPid: process.pid,
|
|
183
|
+
launcherPid: Number(process.env.LIVEDESK_CLIENT_PARENT_PID || 0) || 0,
|
|
184
|
+
attemptId,
|
|
185
|
+
updatedAt: new Date().toISOString(),
|
|
186
|
+
...details
|
|
187
|
+
};
|
|
188
|
+
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
189
|
+
const temporaryPath = `${statePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
190
|
+
try {
|
|
191
|
+
await fs.writeFile(temporaryPath, JSON.stringify(state, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
192
|
+
let current = null;
|
|
193
|
+
try {
|
|
194
|
+
current = JSON.parse(await fs.readFile(statePath, 'utf8'));
|
|
195
|
+
} catch {
|
|
196
|
+
// A missing first-write state is still owned by this transition.
|
|
197
|
+
}
|
|
198
|
+
if (String(current?.operationId || '') !== String(previous?.operationId || '')
|
|
199
|
+
|| String(current?.stage || '') !== String(previous?.stage || '')
|
|
200
|
+
|| String(current?.updatedAt || '') !== String(previous?.updatedAt || '')) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
await fs.rename(temporaryPath, statePath);
|
|
204
|
+
} finally {
|
|
205
|
+
await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function waitForClientUpdateShutdownSignal(
|
|
210
|
+
operationId,
|
|
211
|
+
updateDeadlineEpochMs,
|
|
212
|
+
timeoutMs = 180_000
|
|
213
|
+
) {
|
|
214
|
+
const statePath = getClientUpdateStatePath();
|
|
215
|
+
const deadline = Math.min(
|
|
216
|
+
Date.now() + timeoutMs,
|
|
217
|
+
Number(updateDeadlineEpochMs) || 0
|
|
218
|
+
);
|
|
219
|
+
while (Date.now() < deadline) {
|
|
220
|
+
try {
|
|
221
|
+
const state = JSON.parse(await fs.readFile(statePath, 'utf8'));
|
|
222
|
+
if (state?.operationId !== operationId) {
|
|
223
|
+
await wait(200);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (state.stage === 'preflight-ready' || state.stage === 'waiting-for-shutdown') {
|
|
227
|
+
return { ready: true, error: '' };
|
|
228
|
+
}
|
|
229
|
+
if (state.stage === 'failed') {
|
|
230
|
+
const error = String(state.error || 'LiveDesk client update preparation failed.');
|
|
231
|
+
console.error(`LiveDesk client update preparation failed: ${error}`);
|
|
232
|
+
return { ready: false, error };
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// The detached supervisor may still be writing its first stage.
|
|
236
|
+
}
|
|
237
|
+
await wait(200);
|
|
238
|
+
}
|
|
239
|
+
const deadlineExpired = Date.now() >= Number(updateDeadlineEpochMs);
|
|
240
|
+
const error = deadlineExpired
|
|
241
|
+
? 'The absolute LiveDesk Client update deadline expired before shutdown.'
|
|
242
|
+
: 'Timed out waiting for the LiveDesk update supervisor to prepare the exact package.';
|
|
243
|
+
await writeClientUpdateState('failed', {
|
|
244
|
+
operationId,
|
|
245
|
+
error,
|
|
246
|
+
cancelRequested: true,
|
|
247
|
+
deadlineExpired,
|
|
248
|
+
failedAt: new Date().toISOString()
|
|
249
|
+
}).catch(() => undefined);
|
|
250
|
+
console.error('LiveDesk client update preparation timed out; the current Agent will remain running.');
|
|
251
|
+
return { ready: false, error };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function markClientUpdateConnected() {
|
|
255
|
+
const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
|
|
256
|
+
if (!operationId) return;
|
|
257
|
+
void writeClientUpdateState('connected', {
|
|
258
|
+
operationId,
|
|
259
|
+
attemptId: String(process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim(),
|
|
260
|
+
connectedAt: new Date().toISOString(),
|
|
261
|
+
restartVerified: true,
|
|
262
|
+
error: ''
|
|
263
|
+
}).catch(error => {
|
|
264
|
+
console.error(`LiveDesk client update diagnostics could not be persisted: ${error?.message || error}`);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
263
267
|
|
|
264
268
|
function printHelp() {
|
|
265
269
|
console.log(`
|
|
@@ -279,10 +283,10 @@ Options:
|
|
|
279
283
|
--no-thumbnail Disable thumbnail capture capability.
|
|
280
284
|
--live Enable focused live screen streaming. Default on.
|
|
281
285
|
--no-live Disable focused live screen streaming.
|
|
282
|
-
--tasks Enable safe remote task inbox. Default on.
|
|
283
|
-
--no-tasks Disable remote task dispatch capability.
|
|
284
|
-
--files-dir <path> Default folder for received files. Default: ~/Desktop/LiveDeskFiles.
|
|
285
|
-
--fake-thumbnail Use generated thumbnail frames for smoke tests.
|
|
286
|
+
--tasks Enable safe remote task inbox. Default on.
|
|
287
|
+
--no-tasks Disable remote task dispatch capability.
|
|
288
|
+
--files-dir <path> Default folder for received files. Default: ~/Desktop/LiveDeskFiles.
|
|
289
|
+
--fake-thumbnail Use generated thumbnail frames for smoke tests.
|
|
286
290
|
--exit-on-disconnect Exit after Hub disconnect. Useful for tests.
|
|
287
291
|
--exit-on-invalid-pair Exit when the Hub rejects the pair token.
|
|
288
292
|
--once Connect, send one status packet, and exit after welcome.
|
|
@@ -322,10 +326,10 @@ function parseArgs(argv) {
|
|
|
322
326
|
heartbeatMs: DEFAULT_HEARTBEAT_MS,
|
|
323
327
|
deviceId: '',
|
|
324
328
|
thumbnailEnabled: defaultDesktopCaptureEnabled && !isFalsy(process.env.LIVEDESK_CLIENT_THUMBNAIL ?? process.env.MINDEXEC_REMOTE_THUMBNAIL),
|
|
325
|
-
liveEnabled: defaultDesktopCaptureEnabled && !isFalsy(process.env.LIVEDESK_CLIENT_LIVE ?? process.env.MINDEXEC_REMOTE_LIVE),
|
|
326
|
-
taskEnabled: !isFalsy(process.env.LIVEDESK_CLIENT_TASKS ?? process.env.MINDEXEC_REMOTE_TASKS),
|
|
327
|
-
filesDir: process.env.LIVEDESK_CLIENT_FILES_DIR || process.env.MINDEXEC_REMOTE_FILES_DIR || '',
|
|
328
|
-
fakeThumbnail: isTruthy(process.env.LIVEDESK_CLIENT_FAKE_THUMBNAIL || process.env.MINDEXEC_REMOTE_FAKE_THUMBNAIL),
|
|
329
|
+
liveEnabled: defaultDesktopCaptureEnabled && !isFalsy(process.env.LIVEDESK_CLIENT_LIVE ?? process.env.MINDEXEC_REMOTE_LIVE),
|
|
330
|
+
taskEnabled: !isFalsy(process.env.LIVEDESK_CLIENT_TASKS ?? process.env.MINDEXEC_REMOTE_TASKS),
|
|
331
|
+
filesDir: process.env.LIVEDESK_CLIENT_FILES_DIR || process.env.MINDEXEC_REMOTE_FILES_DIR || '',
|
|
332
|
+
fakeThumbnail: isTruthy(process.env.LIVEDESK_CLIENT_FAKE_THUMBNAIL || process.env.MINDEXEC_REMOTE_FAKE_THUMBNAIL),
|
|
329
333
|
exitOnDisconnect: false,
|
|
330
334
|
exitOnInvalidPair: false,
|
|
331
335
|
once: false,
|
|
@@ -377,11 +381,11 @@ function parseArgs(argv) {
|
|
|
377
381
|
case '--no-tasks':
|
|
378
382
|
result.taskEnabled = false;
|
|
379
383
|
break;
|
|
380
|
-
case '--files-dir':
|
|
381
|
-
result.filesDir = args[++index] || result.filesDir;
|
|
382
|
-
break;
|
|
383
|
-
case '--fake-thumbnail':
|
|
384
|
-
result.fakeThumbnail = true;
|
|
384
|
+
case '--files-dir':
|
|
385
|
+
result.filesDir = args[++index] || result.filesDir;
|
|
386
|
+
break;
|
|
387
|
+
case '--fake-thumbnail':
|
|
388
|
+
result.fakeThumbnail = true;
|
|
385
389
|
result.thumbnailEnabled = true;
|
|
386
390
|
break;
|
|
387
391
|
case '--exit-on-disconnect':
|
|
@@ -561,7 +565,7 @@ function getStatus(options = {}) {
|
|
|
561
565
|
usedMemRatio,
|
|
562
566
|
platform: os.platform(),
|
|
563
567
|
release: os.release(),
|
|
564
|
-
role: options.taskEnabled ? 'Command-ready Client' : 'Local machine',
|
|
568
|
+
role: options.taskEnabled ? 'Command-ready Client' : 'Local machine',
|
|
565
569
|
cpu: {
|
|
566
570
|
cores: cpuCores,
|
|
567
571
|
model: cpus[0]?.model || '',
|
|
@@ -584,9 +588,9 @@ function getStatus(options = {}) {
|
|
|
584
588
|
gpu: acceleratorStatus.gpu,
|
|
585
589
|
npu: acceleratorStatus.npu,
|
|
586
590
|
network: getNetworkStatus(),
|
|
587
|
-
workload: {
|
|
588
|
-
role: options.taskEnabled ? 'Command-ready Client' : 'Local machine',
|
|
589
|
-
status: 'idle',
|
|
591
|
+
workload: {
|
|
592
|
+
role: options.taskEnabled ? 'Command-ready Client' : 'Local machine',
|
|
593
|
+
status: 'idle',
|
|
590
594
|
title: 'idle'
|
|
591
595
|
},
|
|
592
596
|
timestamp: new Date().toISOString()
|
|
@@ -916,17 +920,17 @@ async function captureThumbnailFrame(options, payload, frameSeq) {
|
|
|
916
920
|
return await captureScreenFrame(options, payload, frameSeq, 'thumbnail');
|
|
917
921
|
}
|
|
918
922
|
|
|
919
|
-
const RETIRED_CLIENT_AGENT_TASK_ERROR = 'agent-ai-assist-retired';
|
|
920
|
-
|
|
921
|
-
function getRetiredClientAgentTaskError(payload = {}) {
|
|
922
|
-
const source = payload && typeof payload === 'object' ? payload : {};
|
|
923
|
-
const approvalLevel = String(source.approvalLevel || '').trim().toLowerCase();
|
|
924
|
-
const hasModelSelection = ['model', 'aiModel', 'aiProvider', 'provider']
|
|
925
|
-
.some(key => Object.prototype.hasOwnProperty.call(source, key));
|
|
926
|
-
return approvalLevel === 'ai-assist' || hasModelSelection
|
|
927
|
-
? RETIRED_CLIENT_AGENT_TASK_ERROR
|
|
928
|
-
: '';
|
|
929
|
-
}
|
|
923
|
+
const RETIRED_CLIENT_AGENT_TASK_ERROR = 'agent-ai-assist-retired';
|
|
924
|
+
|
|
925
|
+
function getRetiredClientAgentTaskError(payload = {}) {
|
|
926
|
+
const source = payload && typeof payload === 'object' ? payload : {};
|
|
927
|
+
const approvalLevel = String(source.approvalLevel || '').trim().toLowerCase();
|
|
928
|
+
const hasModelSelection = ['model', 'aiModel', 'aiProvider', 'provider']
|
|
929
|
+
.some(key => Object.prototype.hasOwnProperty.call(source, key));
|
|
930
|
+
return approvalLevel === 'ai-assist' || hasModelSelection
|
|
931
|
+
? RETIRED_CLIENT_AGENT_TASK_ERROR
|
|
932
|
+
: '';
|
|
933
|
+
}
|
|
930
934
|
|
|
931
935
|
function clampInteger(value, min, max, fallback) {
|
|
932
936
|
const number = Number(value);
|
|
@@ -1372,15 +1376,15 @@ async function executeNodeAgentOperation(options, operation, payload = {}) {
|
|
|
1372
1376
|
restart: { executable: 'shutdown.exe', args: ['/r', '/t', delaySec] },
|
|
1373
1377
|
shutdown: { executable: 'shutdown.exe', args: ['/s', '/t', delaySec] }
|
|
1374
1378
|
}[action] || {});
|
|
1375
|
-
} else if (process.platform === 'darwin') {
|
|
1376
|
-
({ executable, args: powerArgs } = {
|
|
1377
|
-
lock: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to keystroke "q" using {control down, command down}'] },
|
|
1378
|
-
sleep: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to sleep'] },
|
|
1379
|
-
logoff: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to log out'] },
|
|
1380
|
-
restart: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to restart'] },
|
|
1381
|
-
shutdown: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to shut down'] }
|
|
1382
|
-
}[action] || {});
|
|
1383
|
-
} else {
|
|
1379
|
+
} else if (process.platform === 'darwin') {
|
|
1380
|
+
({ executable, args: powerArgs } = {
|
|
1381
|
+
lock: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to keystroke "q" using {control down, command down}'] },
|
|
1382
|
+
sleep: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to sleep'] },
|
|
1383
|
+
logoff: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to log out'] },
|
|
1384
|
+
restart: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to restart'] },
|
|
1385
|
+
shutdown: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to shut down'] }
|
|
1386
|
+
}[action] || {});
|
|
1387
|
+
} else {
|
|
1384
1388
|
({ executable, args: powerArgs } = {
|
|
1385
1389
|
lock: { executable: 'loginctl', args: ['lock-session'] },
|
|
1386
1390
|
sleep: { executable: 'systemctl', args: ['suspend'] },
|
|
@@ -1432,11 +1436,11 @@ function remotePolicyAllows(options, command) {
|
|
|
1432
1436
|
const policy = options.effectivePolicy;
|
|
1433
1437
|
if (!policy || typeof policy !== 'object') return { ok: true };
|
|
1434
1438
|
if (policy.accessMode === 'block-remote-access') return { ok: false, error: 'remote-access-blocked-by-settings' };
|
|
1435
|
-
const required = command === 'input.control'
|
|
1436
|
-
? ['allowControl']
|
|
1437
|
-
: command === 'system.power'
|
|
1438
|
-
? ['allowPowerActions']
|
|
1439
|
-
: command.startsWith('file.transfer')
|
|
1439
|
+
const required = command === 'input.control'
|
|
1440
|
+
? ['allowControl']
|
|
1441
|
+
: command === 'system.power'
|
|
1442
|
+
? ['allowPowerActions']
|
|
1443
|
+
: command.startsWith('file.transfer')
|
|
1440
1444
|
? ['allowFileTransfer']
|
|
1441
1445
|
: command === 'audio.start'
|
|
1442
1446
|
? ['allowRemoteAudio']
|
|
@@ -1449,192 +1453,139 @@ function remotePolicyAllows(options, command) {
|
|
|
1449
1453
|
return denied ? { ok: false, error: `${denied}-blocked-by-settings` } : { ok: true };
|
|
1450
1454
|
}
|
|
1451
1455
|
|
|
1452
|
-
function
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
''
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
||
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
await
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
:
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
LIVEDESK_UPDATE_AGENT_PID: String(process.pid),
|
|
1586
|
-
LIVEDESK_SKIP_BROWSER_OPEN: '1',
|
|
1587
|
-
LIVEDESK_CLIENT_UPDATE_TARGET_VERSION: targetAgentVersion,
|
|
1588
|
-
LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION: targetProductVersion,
|
|
1589
|
-
LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION: targetAgentVersion,
|
|
1590
|
-
LIVEDESK_CLIENT_UPDATE_CURRENT_PRODUCT_VERSION: PRODUCT_VERSION,
|
|
1591
|
-
LIVEDESK_CLIENT_UPDATE_CURRENT_AGENT_VERSION: AGENT_VERSION,
|
|
1592
|
-
LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER: localLauncher,
|
|
1593
|
-
LIVEDESK_CLIENT_UPDATE_OPERATION_ID: operationId,
|
|
1594
|
-
LIVEDESK_CLIENT_UPDATE_DEADLINE_EPOCH_MS: String(updateDeadlineEpochMs),
|
|
1595
|
-
LIVEDESK_CLIENT_UPDATE_STATE_PATH: statePath,
|
|
1596
|
-
LIVEDESK_UPDATE_NEUTRAL_CWD: neutralCwd,
|
|
1597
|
-
LIVEDESK_UPDATE_ORIGINAL_CWD: originalCwd,
|
|
1598
|
-
LIVEDESK_CLIENT_UPDATE_CWD: originalCwd
|
|
1599
|
-
}, neutralCwd);
|
|
1600
|
-
return await new Promise((resolve, reject) => {
|
|
1601
|
-
const child = spawn(process.execPath, ['-e', buildClientUpdateBootstrapScript()], {
|
|
1602
|
-
cwd: neutralCwd,
|
|
1603
|
-
env: updateEnvironment,
|
|
1604
|
-
detached: true,
|
|
1605
|
-
stdio: 'ignore',
|
|
1606
|
-
windowsHide: true
|
|
1607
|
-
});
|
|
1608
|
-
child.once('error', reject);
|
|
1609
|
-
child.once('spawn', () => {
|
|
1610
|
-
child.unref();
|
|
1611
|
-
resolve({
|
|
1612
|
-
ok: true,
|
|
1613
|
-
status: 'update-scheduled',
|
|
1614
|
-
targetVersion: targetAgentVersion,
|
|
1615
|
-
targetProductVersion,
|
|
1616
|
-
targetAgentVersion,
|
|
1617
|
-
operationId,
|
|
1618
|
-
updateDeadlineEpochMs,
|
|
1619
|
-
statePath,
|
|
1620
|
-
parentPid,
|
|
1621
|
-
restartAfterMs: 0
|
|
1622
|
-
});
|
|
1623
|
-
});
|
|
1624
|
-
});
|
|
1625
|
-
} catch (error) {
|
|
1626
|
-
await writeClientUpdateState('failed', {
|
|
1627
|
-
operationId,
|
|
1628
|
-
targetVersion: targetAgentVersion,
|
|
1629
|
-
targetProductVersion,
|
|
1630
|
-
targetAgentVersion,
|
|
1631
|
-
error: error?.message || String(error),
|
|
1632
|
-
failedAt: new Date().toISOString(),
|
|
1633
|
-
restartVerified: false
|
|
1634
|
-
}).catch(() => undefined);
|
|
1635
|
-
throw error;
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1456
|
+
async function scheduleClientUpdate(options, payload = {}) {
|
|
1457
|
+
const operationId = String(payload.operationId || crypto.randomUUID()).trim();
|
|
1458
|
+
const statePath = getClientUpdateStatePath();
|
|
1459
|
+
let targetProductVersion = '';
|
|
1460
|
+
let targetAgentVersion = '';
|
|
1461
|
+
let updateDeadlineEpochMs = 0;
|
|
1462
|
+
try {
|
|
1463
|
+
targetProductVersion = normalizeClientUpdateTargetVersion(
|
|
1464
|
+
payload.targetProductVersion
|
|
1465
|
+
|| payload.productVersion
|
|
1466
|
+
|| payload.managerVersion
|
|
1467
|
+
|| payload.targetVersion
|
|
1468
|
+
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
|
|
1469
|
+
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
|
|
1470
|
+
|| ''
|
|
1471
|
+
);
|
|
1472
|
+
targetAgentVersion = normalizeClientUpdateTargetVersion(
|
|
1473
|
+
payload.targetAgentVersion
|
|
1474
|
+
|| payload.clientVersion
|
|
1475
|
+
|| payload.targetVersion
|
|
1476
|
+
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION
|
|
1477
|
+
|| process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
|
|
1478
|
+
|| targetProductVersion
|
|
1479
|
+
|| ''
|
|
1480
|
+
);
|
|
1481
|
+
const updateTimeoutMs = Number(payload.updateTimeoutMs);
|
|
1482
|
+
if (!Number.isSafeInteger(updateTimeoutMs)
|
|
1483
|
+
|| updateTimeoutMs < 1_000
|
|
1484
|
+
|| updateTimeoutMs > 90 * 60_000) {
|
|
1485
|
+
throw new Error('LiveDesk Client update requires a bounded deadline budget.');
|
|
1486
|
+
}
|
|
1487
|
+
updateDeadlineEpochMs = Date.now() + updateTimeoutMs;
|
|
1488
|
+
const originalCwd = path.resolve(process.cwd());
|
|
1489
|
+
const neutralCwd = await prepareClientUpdateNeutralCwd(operationId);
|
|
1490
|
+
await writeClientUpdateState('scheduled', {
|
|
1491
|
+
operationId,
|
|
1492
|
+
targetVersion: targetAgentVersion,
|
|
1493
|
+
targetProductVersion,
|
|
1494
|
+
targetAgentVersion,
|
|
1495
|
+
updateTimeoutMs,
|
|
1496
|
+
updateDeadlineEpochMs,
|
|
1497
|
+
requestedAt: new Date().toISOString(),
|
|
1498
|
+
restartVerified: false,
|
|
1499
|
+
error: ''
|
|
1500
|
+
});
|
|
1501
|
+
const configuredParentPid = Number(process.env.LIVEDESK_CLIENT_PARENT_PID || process.ppid);
|
|
1502
|
+
const parentPid = Number.isInteger(configuredParentPid) && configuredParentPid > 1
|
|
1503
|
+
? configuredParentPid
|
|
1504
|
+
: process.pid;
|
|
1505
|
+
if (!Number.isInteger(parentPid) || parentPid <= 1) {
|
|
1506
|
+
throw new Error('LiveDesk client launcher process id is unavailable.');
|
|
1507
|
+
}
|
|
1508
|
+
const nodeBinDir = path.dirname(process.execPath);
|
|
1509
|
+
const npxCandidates = process.platform === 'win32'
|
|
1510
|
+
? [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeBinDir, 'npx.cmd'), path.join(nodeBinDir, 'npx.exe'), 'npx.cmd']
|
|
1511
|
+
: [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeBinDir, 'npx'), 'npx'];
|
|
1512
|
+
const command = npxCandidates.find(candidate => candidate && (!path.isAbsolute(candidate) || existsSync(candidate)));
|
|
1513
|
+
if (!command) {
|
|
1514
|
+
throw new Error('LiveDesk npx executable was not found.');
|
|
1515
|
+
}
|
|
1516
|
+
const frozenCommand = path.isAbsolute(command) || !/[\\/]/.test(command)
|
|
1517
|
+
? command
|
|
1518
|
+
: path.resolve(originalCwd, command);
|
|
1519
|
+
const configuredLocalLauncher = String(
|
|
1520
|
+
process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY
|
|
1521
|
+
|| process.env.LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER
|
|
1522
|
+
|| ''
|
|
1523
|
+
).trim();
|
|
1524
|
+
const localLauncher = configuredLocalLauncher
|
|
1525
|
+
? path.resolve(originalCwd, configuredLocalLauncher)
|
|
1526
|
+
: '';
|
|
1527
|
+
const updateEnvironment = buildClientUpdateNpxEnvironment({
|
|
1528
|
+
...process.env,
|
|
1529
|
+
LIVEDESK_NODE_EXECUTABLE: process.execPath,
|
|
1530
|
+
LIVEDESK_NPX_EXECUTABLE: frozenCommand,
|
|
1531
|
+
LIVEDESK_UPDATE_WAIT_PID: String(parentPid),
|
|
1532
|
+
LIVEDESK_UPDATE_AGENT_PID: String(process.pid),
|
|
1533
|
+
LIVEDESK_SKIP_BROWSER_OPEN: '1',
|
|
1534
|
+
LIVEDESK_CLIENT_UPDATE_TARGET_VERSION: targetAgentVersion,
|
|
1535
|
+
LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION: targetProductVersion,
|
|
1536
|
+
LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION: targetAgentVersion,
|
|
1537
|
+
LIVEDESK_CLIENT_UPDATE_CURRENT_PRODUCT_VERSION: PRODUCT_VERSION,
|
|
1538
|
+
LIVEDESK_CLIENT_UPDATE_CURRENT_AGENT_VERSION: AGENT_VERSION,
|
|
1539
|
+
LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER: localLauncher,
|
|
1540
|
+
LIVEDESK_CLIENT_UPDATE_OPERATION_ID: operationId,
|
|
1541
|
+
LIVEDESK_CLIENT_UPDATE_DEADLINE_EPOCH_MS: String(updateDeadlineEpochMs),
|
|
1542
|
+
LIVEDESK_CLIENT_UPDATE_STATE_PATH: statePath,
|
|
1543
|
+
LIVEDESK_UPDATE_NEUTRAL_CWD: neutralCwd,
|
|
1544
|
+
LIVEDESK_UPDATE_ORIGINAL_CWD: originalCwd,
|
|
1545
|
+
LIVEDESK_CLIENT_UPDATE_CWD: originalCwd
|
|
1546
|
+
}, neutralCwd);
|
|
1547
|
+
const bootstrapStat = await fs.stat(CLIENT_UPDATE_BOOTSTRAP_PATH);
|
|
1548
|
+
if (!bootstrapStat.isFile() || bootstrapStat.size <= 0) {
|
|
1549
|
+
throw new Error(`LiveDesk Client update bootstrap is unavailable: ${CLIENT_UPDATE_BOOTSTRAP_PATH}`);
|
|
1550
|
+
}
|
|
1551
|
+
return await new Promise((resolve, reject) => {
|
|
1552
|
+
const child = spawn(process.execPath, [CLIENT_UPDATE_BOOTSTRAP_PATH], {
|
|
1553
|
+
cwd: neutralCwd,
|
|
1554
|
+
env: updateEnvironment,
|
|
1555
|
+
detached: true,
|
|
1556
|
+
stdio: 'ignore',
|
|
1557
|
+
windowsHide: true
|
|
1558
|
+
});
|
|
1559
|
+
child.once('error', reject);
|
|
1560
|
+
child.once('spawn', () => {
|
|
1561
|
+
child.unref();
|
|
1562
|
+
resolve({
|
|
1563
|
+
ok: true,
|
|
1564
|
+
status: 'update-scheduled',
|
|
1565
|
+
targetVersion: targetAgentVersion,
|
|
1566
|
+
targetProductVersion,
|
|
1567
|
+
targetAgentVersion,
|
|
1568
|
+
operationId,
|
|
1569
|
+
updateDeadlineEpochMs,
|
|
1570
|
+
statePath,
|
|
1571
|
+
parentPid,
|
|
1572
|
+
restartAfterMs: 0
|
|
1573
|
+
});
|
|
1574
|
+
});
|
|
1575
|
+
});
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
await writeClientUpdateState('failed', {
|
|
1578
|
+
operationId,
|
|
1579
|
+
targetVersion: targetAgentVersion,
|
|
1580
|
+
targetProductVersion,
|
|
1581
|
+
targetAgentVersion,
|
|
1582
|
+
error: error?.message || String(error),
|
|
1583
|
+
failedAt: new Date().toISOString(),
|
|
1584
|
+
restartVerified: false
|
|
1585
|
+
}).catch(() => undefined);
|
|
1586
|
+
throw error;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1638
1589
|
|
|
1639
1590
|
async function handleRemoteCommand(socket, options, message, nextFrameSeq, activeStreams) {
|
|
1640
1591
|
const command = String(message.command || '');
|
|
@@ -1661,37 +1612,37 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
1661
1612
|
return;
|
|
1662
1613
|
}
|
|
1663
1614
|
|
|
1664
|
-
if (command === 'livedesk.client-update') {
|
|
1665
|
-
try {
|
|
1666
|
-
const result = await scheduleClientUpdate(options, message.payload || {});
|
|
1615
|
+
if (command === 'livedesk.client-update') {
|
|
1616
|
+
try {
|
|
1617
|
+
const result = await scheduleClientUpdate(options, message.payload || {});
|
|
1667
1618
|
writeJsonLine(socket, {
|
|
1668
1619
|
type: 'command.result',
|
|
1669
|
-
commandId: message.commandId,
|
|
1670
|
-
result
|
|
1671
|
-
});
|
|
1672
|
-
const shutdownSignal = await waitForClientUpdateShutdownSignal(
|
|
1673
|
-
result.operationId,
|
|
1674
|
-
result.updateDeadlineEpochMs
|
|
1675
|
-
);
|
|
1676
|
-
if (shutdownSignal.ready) {
|
|
1677
|
-
// The exact package supervisor is the sole owner of old-runtime
|
|
1678
|
-
// termination, so a deadline can never race a voluntary exit.
|
|
1679
|
-
return;
|
|
1680
|
-
} else {
|
|
1681
|
-
writeJsonLine(socket, {
|
|
1682
|
-
type: 'command.result',
|
|
1683
|
-
commandId: message.commandId,
|
|
1684
|
-
error: shutdownSignal.error,
|
|
1685
|
-
result: {
|
|
1686
|
-
ok: false,
|
|
1687
|
-
status: 'failed',
|
|
1688
|
-
error: shutdownSignal.error,
|
|
1689
|
-
operationId: result.operationId,
|
|
1690
|
-
completedAt: new Date().toISOString()
|
|
1691
|
-
}
|
|
1692
|
-
});
|
|
1693
|
-
}
|
|
1694
|
-
} catch (error) {
|
|
1620
|
+
commandId: message.commandId,
|
|
1621
|
+
result
|
|
1622
|
+
});
|
|
1623
|
+
const shutdownSignal = await waitForClientUpdateShutdownSignal(
|
|
1624
|
+
result.operationId,
|
|
1625
|
+
result.updateDeadlineEpochMs
|
|
1626
|
+
);
|
|
1627
|
+
if (shutdownSignal.ready) {
|
|
1628
|
+
// The exact package supervisor is the sole owner of old-runtime
|
|
1629
|
+
// termination, so a deadline can never race a voluntary exit.
|
|
1630
|
+
return;
|
|
1631
|
+
} else {
|
|
1632
|
+
writeJsonLine(socket, {
|
|
1633
|
+
type: 'command.result',
|
|
1634
|
+
commandId: message.commandId,
|
|
1635
|
+
error: shutdownSignal.error,
|
|
1636
|
+
result: {
|
|
1637
|
+
ok: false,
|
|
1638
|
+
status: 'failed',
|
|
1639
|
+
error: shutdownSignal.error,
|
|
1640
|
+
operationId: result.operationId,
|
|
1641
|
+
completedAt: new Date().toISOString()
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
} catch (error) {
|
|
1695
1646
|
writeJsonLine(socket, {
|
|
1696
1647
|
type: 'command.result',
|
|
1697
1648
|
commandId: message.commandId,
|
|
@@ -1745,25 +1696,25 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
1745
1696
|
error: 'remote task capability is disabled'
|
|
1746
1697
|
});
|
|
1747
1698
|
return;
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
const payload = message.payload || {};
|
|
1751
|
-
const retiredTaskError = getRetiredClientAgentTaskError(payload);
|
|
1752
|
-
if (retiredTaskError) {
|
|
1753
|
-
writeJsonLine(socket, {
|
|
1754
|
-
type: 'command.result',
|
|
1755
|
-
commandId: message.commandId,
|
|
1756
|
-
error: retiredTaskError,
|
|
1757
|
-
result: {
|
|
1758
|
-
ok: false,
|
|
1759
|
-
status: 'failed',
|
|
1760
|
-
error: retiredTaskError,
|
|
1761
|
-
completedAt: new Date().toISOString()
|
|
1762
|
-
}
|
|
1763
|
-
});
|
|
1764
|
-
return;
|
|
1765
|
-
}
|
|
1766
|
-
const instruction = String(payload.instruction || '').replace(/\0/g, '').trim().slice(0, 4000);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
const payload = message.payload || {};
|
|
1702
|
+
const retiredTaskError = getRetiredClientAgentTaskError(payload);
|
|
1703
|
+
if (retiredTaskError) {
|
|
1704
|
+
writeJsonLine(socket, {
|
|
1705
|
+
type: 'command.result',
|
|
1706
|
+
commandId: message.commandId,
|
|
1707
|
+
error: retiredTaskError,
|
|
1708
|
+
result: {
|
|
1709
|
+
ok: false,
|
|
1710
|
+
status: 'failed',
|
|
1711
|
+
error: retiredTaskError,
|
|
1712
|
+
completedAt: new Date().toISOString()
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
const instruction = String(payload.instruction || '').replace(/\0/g, '').trim().slice(0, 4000);
|
|
1767
1718
|
if (!instruction) {
|
|
1768
1719
|
writeJsonLine(socket, {
|
|
1769
1720
|
type: 'command.result',
|
|
@@ -1773,13 +1724,13 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
1773
1724
|
return;
|
|
1774
1725
|
}
|
|
1775
1726
|
|
|
1776
|
-
const completedAt = new Date().toISOString();
|
|
1777
|
-
const title = String(payload.title || instruction.split(/\r?\n/)[0] || 'Remote task')
|
|
1778
|
-
.replace(/[\r\n\t]/g, ' ')
|
|
1779
|
-
.trim()
|
|
1780
|
-
.slice(0, 120);
|
|
1781
|
-
|
|
1782
|
-
writeJsonLine(socket, {
|
|
1727
|
+
const completedAt = new Date().toISOString();
|
|
1728
|
+
const title = String(payload.title || instruction.split(/\r?\n/)[0] || 'Remote task')
|
|
1729
|
+
.replace(/[\r\n\t]/g, ' ')
|
|
1730
|
+
.trim()
|
|
1731
|
+
.slice(0, 120);
|
|
1732
|
+
|
|
1733
|
+
writeJsonLine(socket, {
|
|
1783
1734
|
type: 'command.result',
|
|
1784
1735
|
commandId: message.commandId,
|
|
1785
1736
|
result: {
|
|
@@ -2004,10 +1955,10 @@ function connectOnce(options, deviceId) {
|
|
|
2004
1955
|
hostname: os.hostname(),
|
|
2005
1956
|
platform: os.platform(),
|
|
2006
1957
|
arch: os.arch(),
|
|
2007
|
-
pid: process.pid,
|
|
2008
|
-
agentVersion: AGENT_VERSION,
|
|
2009
|
-
productVersion: PRODUCT_VERSION,
|
|
2010
|
-
capabilities: {
|
|
1958
|
+
pid: process.pid,
|
|
1959
|
+
agentVersion: AGENT_VERSION,
|
|
1960
|
+
productVersion: PRODUCT_VERSION,
|
|
1961
|
+
capabilities: {
|
|
2011
1962
|
status: true,
|
|
2012
1963
|
thumbnail: options.thumbnailEnabled,
|
|
2013
1964
|
liveStream: options.liveEnabled,
|
|
@@ -2022,13 +1973,13 @@ function connectOnce(options, deviceId) {
|
|
|
2022
1973
|
fileTransferMaxBytes: MAX_FILE_TRANSFER_BYTES,
|
|
2023
1974
|
computerAgent: options.taskEnabled,
|
|
2024
1975
|
taskDispatch: options.taskEnabled,
|
|
2025
|
-
clientUpdate: true,
|
|
2026
|
-
productVersion: PRODUCT_VERSION,
|
|
2027
|
-
agentApproval: options.taskEnabled,
|
|
2028
|
-
agentAudit: options.taskEnabled,
|
|
2029
|
-
agentTools: [...NODE_AGENT_OPERATIONS],
|
|
2030
|
-
elevation: typeof process.getuid === 'function' ? process.getuid() === 0 : false,
|
|
2031
|
-
externalEffects: options.taskEnabled
|
|
1976
|
+
clientUpdate: true,
|
|
1977
|
+
productVersion: PRODUCT_VERSION,
|
|
1978
|
+
agentApproval: options.taskEnabled,
|
|
1979
|
+
agentAudit: options.taskEnabled,
|
|
1980
|
+
agentTools: [...NODE_AGENT_OPERATIONS],
|
|
1981
|
+
elevation: typeof process.getuid === 'function' ? process.getuid() === 0 : false,
|
|
1982
|
+
externalEffects: options.taskEnabled
|
|
2032
1983
|
}
|
|
2033
1984
|
});
|
|
2034
1985
|
});
|
|
@@ -2045,12 +1996,12 @@ function connectOnce(options, deviceId) {
|
|
|
2045
1996
|
}
|
|
2046
1997
|
|
|
2047
1998
|
const message = JSON.parse(line);
|
|
2048
|
-
if (message.type === 'welcome') {
|
|
2049
|
-
options.effectivePolicy = message.effectivePolicy && typeof message.effectivePolicy === 'object'
|
|
2050
|
-
? message.effectivePolicy
|
|
1999
|
+
if (message.type === 'welcome') {
|
|
2000
|
+
options.effectivePolicy = message.effectivePolicy && typeof message.effectivePolicy === 'object'
|
|
2001
|
+
? message.effectivePolicy
|
|
2051
2002
|
: null;
|
|
2052
|
-
console.log(`Connected to LiveDesk Hub as ${options.name} (${deviceId})`);
|
|
2053
|
-
markClientUpdateConnected();
|
|
2003
|
+
console.log(`Connected to LiveDesk Hub as ${options.name} (${deviceId})`);
|
|
2004
|
+
markClientUpdateConnected();
|
|
2054
2005
|
writeJsonLine(socket, {
|
|
2055
2006
|
type: 'status',
|
|
2056
2007
|
status: getStatus(options)
|
|
@@ -2060,17 +2011,17 @@ function connectOnce(options, deviceId) {
|
|
|
2060
2011
|
return;
|
|
2061
2012
|
}
|
|
2062
2013
|
|
|
2063
|
-
heartbeatTimer = setInterval(() => {
|
|
2064
|
-
writeJsonLine(socket, {
|
|
2065
|
-
type: 'status',
|
|
2066
|
-
status: getStatus(options)
|
|
2067
|
-
});
|
|
2068
|
-
}, options.heartbeatMs);
|
|
2069
|
-
} else if (message.type === 'policy.update') {
|
|
2070
|
-
options.effectivePolicy = message.effectivePolicy && typeof message.effectivePolicy === 'object'
|
|
2071
|
-
? message.effectivePolicy
|
|
2072
|
-
: options.effectivePolicy;
|
|
2073
|
-
} else if (message.type === 'command') {
|
|
2014
|
+
heartbeatTimer = setInterval(() => {
|
|
2015
|
+
writeJsonLine(socket, {
|
|
2016
|
+
type: 'status',
|
|
2017
|
+
status: getStatus(options)
|
|
2018
|
+
});
|
|
2019
|
+
}, options.heartbeatMs);
|
|
2020
|
+
} else if (message.type === 'policy.update') {
|
|
2021
|
+
options.effectivePolicy = message.effectivePolicy && typeof message.effectivePolicy === 'object'
|
|
2022
|
+
? message.effectivePolicy
|
|
2023
|
+
: options.effectivePolicy;
|
|
2024
|
+
} else if (message.type === 'command') {
|
|
2074
2025
|
handleRemoteCommand(socket, options, message, () => {
|
|
2075
2026
|
frameSeq += 1;
|
|
2076
2027
|
return frameSeq;
|