livedesk 0.1.455 → 0.1.456
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/README.md +31 -4
- package/bin/livedesk.js +98 -18
- package/client/bin/livedesk-client.js +2 -2
- package/client/package.json +22 -22
- package/diagnostics/livedesk-mac-physical-gate-core.mjs +365 -0
- package/diagnostics/livedesk-mac-physical-isolation.mjs +311 -0
- package/diagnostics/livedesk-mode3-capture-smoke.mjs +1393 -0
- package/hub/package.json +1 -1
- package/hub/src/server.js +6 -5
- package/package.json +12 -11
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
+
|
|
7
|
+
export function createMacDiagnosticAbortState() {
|
|
8
|
+
let requestedSignal = '';
|
|
9
|
+
let abortError;
|
|
10
|
+
const listeners = new Set();
|
|
11
|
+
return Object.freeze({
|
|
12
|
+
request(signalName) {
|
|
13
|
+
if (requestedSignal) return false;
|
|
14
|
+
requestedSignal = String(signalName || 'abort').trim().toUpperCase() || 'ABORT';
|
|
15
|
+
abortError = new Error(`Mac physical diagnostic interrupted by ${requestedSignal}.`);
|
|
16
|
+
abortError.code = 'LIVEDESK_MAC_DIAGNOSTIC_ABORTED';
|
|
17
|
+
abortError.signal = requestedSignal;
|
|
18
|
+
for (const listener of [...listeners]) {
|
|
19
|
+
try {
|
|
20
|
+
listener(abortError);
|
|
21
|
+
} catch {
|
|
22
|
+
// Abort notification must remain idempotent even if a listener fails.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
listeners.clear();
|
|
26
|
+
return true;
|
|
27
|
+
},
|
|
28
|
+
get requested() {
|
|
29
|
+
return Boolean(requestedSignal);
|
|
30
|
+
},
|
|
31
|
+
get signal() {
|
|
32
|
+
return requestedSignal;
|
|
33
|
+
},
|
|
34
|
+
get error() {
|
|
35
|
+
return abortError;
|
|
36
|
+
},
|
|
37
|
+
throwIfRequested() {
|
|
38
|
+
if (abortError) throw abortError;
|
|
39
|
+
},
|
|
40
|
+
subscribe(listener) {
|
|
41
|
+
if (typeof listener !== 'function') {
|
|
42
|
+
throw new TypeError('Mac diagnostic abort listener must be a function.');
|
|
43
|
+
}
|
|
44
|
+
if (abortError) {
|
|
45
|
+
listener(abortError);
|
|
46
|
+
return () => {};
|
|
47
|
+
}
|
|
48
|
+
listeners.add(listener);
|
|
49
|
+
return () => listeners.delete(listener);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createIdempotentAsyncCleanup(cleanup) {
|
|
55
|
+
if (typeof cleanup !== 'function') {
|
|
56
|
+
throw new TypeError('Mac diagnostic cleanup must be a function.');
|
|
57
|
+
}
|
|
58
|
+
let cleanupPromise;
|
|
59
|
+
return () => {
|
|
60
|
+
if (!cleanupPromise) {
|
|
61
|
+
cleanupPromise = Promise.resolve().then(cleanup);
|
|
62
|
+
}
|
|
63
|
+
return cleanupPromise;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildMacDiagnosticTeardownPlan({
|
|
68
|
+
agentStopped = false,
|
|
69
|
+
hubStopped = false,
|
|
70
|
+
decodersStopped = false,
|
|
71
|
+
helperFinalCount = -1
|
|
72
|
+
} = {}) {
|
|
73
|
+
const reasons = [];
|
|
74
|
+
if (agentStopped !== true) reasons.push('owned RemoteFast did not confirm exit');
|
|
75
|
+
if (hubStopped !== true) reasons.push('owned Hub did not confirm exit');
|
|
76
|
+
if (decodersStopped !== true) reasons.push('owned decoder did not confirm exit');
|
|
77
|
+
if (Number(helperFinalCount) !== 0) {
|
|
78
|
+
reasons.push(`capture helper drain ended at ${Number(helperFinalCount)}`);
|
|
79
|
+
}
|
|
80
|
+
return Object.freeze({
|
|
81
|
+
canRemoveState: reasons.length === 0,
|
|
82
|
+
reasons: Object.freeze(reasons)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function runMacDiagnosticStateTeardown({
|
|
87
|
+
plan,
|
|
88
|
+
removeWorkspace,
|
|
89
|
+
releaseLease
|
|
90
|
+
} = {}) {
|
|
91
|
+
if (plan?.canRemoveState !== true) {
|
|
92
|
+
return {
|
|
93
|
+
removed: false,
|
|
94
|
+
reasons: Array.isArray(plan?.reasons) ? [...plan.reasons] : ['unsafe teardown plan']
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (typeof removeWorkspace === 'function') {
|
|
98
|
+
await removeWorkspace();
|
|
99
|
+
}
|
|
100
|
+
if (typeof releaseLease === 'function') {
|
|
101
|
+
await releaseLease();
|
|
102
|
+
}
|
|
103
|
+
return { removed: true, reasons: [] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function settleAllOrThrowFirst(promises) {
|
|
107
|
+
const results = await Promise.allSettled(Array.from(promises || []));
|
|
108
|
+
const firstFailure = results.find(result => result.status === 'rejected');
|
|
109
|
+
if (firstFailure) throw firstFailure.reason;
|
|
110
|
+
return results.map(result => result.value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function runMacDiagnosticAbortableAttempt(
|
|
114
|
+
getValue,
|
|
115
|
+
{
|
|
116
|
+
abortState,
|
|
117
|
+
timeoutMs = 2_000,
|
|
118
|
+
label = 'diagnostic attempt'
|
|
119
|
+
} = {}
|
|
120
|
+
) {
|
|
121
|
+
if (typeof getValue !== 'function') {
|
|
122
|
+
return Promise.reject(new TypeError('Mac diagnostic attempt must be a function.'));
|
|
123
|
+
}
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
let settled = false;
|
|
127
|
+
let unsubscribeAbort = () => {};
|
|
128
|
+
const finish = (callback, value) => {
|
|
129
|
+
if (settled) return;
|
|
130
|
+
settled = true;
|
|
131
|
+
clearTimeout(timeout);
|
|
132
|
+
unsubscribeAbort();
|
|
133
|
+
callback(value);
|
|
134
|
+
};
|
|
135
|
+
const timeout = setTimeout(() => {
|
|
136
|
+
const error = new Error(`${label} exceeded ${Math.max(1, timeoutMs)}ms.`);
|
|
137
|
+
error.code = 'LIVEDESK_MAC_DIAGNOSTIC_ATTEMPT_TIMEOUT';
|
|
138
|
+
controller.abort(error);
|
|
139
|
+
finish(reject, error);
|
|
140
|
+
}, Math.max(1, timeoutMs));
|
|
141
|
+
timeout.unref?.();
|
|
142
|
+
if (abortState?.subscribe) {
|
|
143
|
+
unsubscribeAbort = abortState.subscribe(error => {
|
|
144
|
+
controller.abort(error);
|
|
145
|
+
finish(reject, error);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
Promise.resolve()
|
|
149
|
+
.then(() => {
|
|
150
|
+
if (settled) return undefined;
|
|
151
|
+
return getValue(controller.signal);
|
|
152
|
+
})
|
|
153
|
+
.then(
|
|
154
|
+
value => finish(resolve, value),
|
|
155
|
+
error => finish(reject, error)
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function waitForOwnedProcessExit(child, timeoutMs = 10_000) {
|
|
161
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) {
|
|
162
|
+
return Promise.resolve(true);
|
|
163
|
+
}
|
|
164
|
+
return Promise.race([
|
|
165
|
+
new Promise(resolve => child.once('exit', () => resolve(true))),
|
|
166
|
+
delay(timeoutMs).then(() => false)
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function stopOwnedProcess(child, label, timeoutMs = 10_000) {
|
|
171
|
+
if (
|
|
172
|
+
!child
|
|
173
|
+
|| Number(child.pid || 0) <= 1
|
|
174
|
+
|| child.exitCode !== null
|
|
175
|
+
|| child.signalCode !== null
|
|
176
|
+
) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
child.kill('SIGTERM');
|
|
180
|
+
if (await waitForOwnedProcessExit(child, timeoutMs)) return true;
|
|
181
|
+
child.kill('SIGKILL');
|
|
182
|
+
if (await waitForOwnedProcessExit(child, 2_000)) return true;
|
|
183
|
+
throw new Error(`${label} pid ${Number(child.pid || 0)} did not exit after owned-process cleanup.`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function isProcessAlive(pid) {
|
|
187
|
+
if (!Number.isInteger(pid) || pid <= 1) return false;
|
|
188
|
+
try {
|
|
189
|
+
process.kill(pid, 0);
|
|
190
|
+
return true;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return error?.code === 'EPERM';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function inspectMacDiagnosticLease({
|
|
197
|
+
directory = path.join(homedir(), '.livedesk', 'diagnostics'),
|
|
198
|
+
pidAlive = isProcessAlive
|
|
199
|
+
} = {}) {
|
|
200
|
+
const lockPath = path.join(path.resolve(directory), 'mac-physical-gate.lock');
|
|
201
|
+
let owner;
|
|
202
|
+
try {
|
|
203
|
+
owner = JSON.parse(await readFile(lockPath, 'utf8'));
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error?.code === 'ENOENT') {
|
|
206
|
+
return Object.freeze({ status: 'missing', path: lockPath, pid: 0 });
|
|
207
|
+
}
|
|
208
|
+
return Object.freeze({
|
|
209
|
+
status: 'unreadable',
|
|
210
|
+
path: lockPath,
|
|
211
|
+
pid: 0,
|
|
212
|
+
error: error instanceof Error ? error.message : String(error)
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const ownerPid = Number(owner?.pid || 0);
|
|
216
|
+
const ownerToken = String(owner?.token || '').trim();
|
|
217
|
+
if (!Number.isInteger(ownerPid) || ownerPid <= 1 || ownerToken.length < 16) {
|
|
218
|
+
return Object.freeze({
|
|
219
|
+
status: 'unreadable',
|
|
220
|
+
path: lockPath,
|
|
221
|
+
pid: ownerPid,
|
|
222
|
+
error: 'invalid diagnostic lease owner'
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
let active;
|
|
226
|
+
try {
|
|
227
|
+
active = pidAlive(ownerPid) === true;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
return Object.freeze({
|
|
230
|
+
status: 'unreadable',
|
|
231
|
+
path: lockPath,
|
|
232
|
+
pid: ownerPid,
|
|
233
|
+
error: error instanceof Error ? error.message : String(error)
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return Object.freeze({
|
|
237
|
+
status: active ? 'active' : 'stale',
|
|
238
|
+
path: lockPath,
|
|
239
|
+
pid: ownerPid
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function acquireMacDiagnosticLease({
|
|
244
|
+
directory = path.join(homedir(), '.livedesk', 'diagnostics'),
|
|
245
|
+
ownerPid = process.pid,
|
|
246
|
+
token = randomBytes(16).toString('hex'),
|
|
247
|
+
pidAlive = isProcessAlive
|
|
248
|
+
} = {}) {
|
|
249
|
+
if (!Number.isInteger(ownerPid) || ownerPid <= 1) {
|
|
250
|
+
throw new Error(`Invalid Mac diagnostic lease owner pid: ${ownerPid}`);
|
|
251
|
+
}
|
|
252
|
+
const lockPath = path.join(path.resolve(directory), 'mac-physical-gate.lock');
|
|
253
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
254
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
255
|
+
let handle;
|
|
256
|
+
let created = false;
|
|
257
|
+
try {
|
|
258
|
+
handle = await open(lockPath, 'wx', 0o600);
|
|
259
|
+
created = true;
|
|
260
|
+
await handle.writeFile(`${JSON.stringify({
|
|
261
|
+
schemaVersion: 1,
|
|
262
|
+
pid: ownerPid,
|
|
263
|
+
token,
|
|
264
|
+
startedAt: new Date().toISOString()
|
|
265
|
+
})}\n`, 'utf8');
|
|
266
|
+
await handle.close();
|
|
267
|
+
return {
|
|
268
|
+
path: lockPath,
|
|
269
|
+
token,
|
|
270
|
+
ownerPid,
|
|
271
|
+
async release() {
|
|
272
|
+
let current;
|
|
273
|
+
try {
|
|
274
|
+
current = JSON.parse(await readFile(lockPath, 'utf8'));
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
if (String(current?.token || '') !== token || Number(current?.pid || 0) !== ownerPid) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
await rm(lockPath, { force: true });
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
} catch (error) {
|
|
286
|
+
try { await handle?.close(); } catch {}
|
|
287
|
+
if (error?.code !== 'EEXIST') {
|
|
288
|
+
if (created) await rm(lockPath, { force: true }).catch(() => undefined);
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
let owner;
|
|
292
|
+
try {
|
|
293
|
+
owner = JSON.parse(await readFile(lockPath, 'utf8'));
|
|
294
|
+
} catch {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`Mac physical diagnostic lease ownership is unreadable at ${lockPath}; `
|
|
297
|
+
+ 'no process was stopped and the diagnostic did not start.'
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const existingPid = Number(owner?.pid || 0);
|
|
301
|
+
if (pidAlive(existingPid)) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`Mac physical diagnostic is already running (pid ${existingPid}); `
|
|
304
|
+
+ 'no second capture helper was started.'
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
await rm(lockPath, { force: true });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
throw new Error('Unable to acquire the Mac physical diagnostic lease.');
|
|
311
|
+
}
|