@livedesk/client 0.1.239 → 0.1.241
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 +20 -20
- package/THIRD_PARTY_NOTICES.md +26 -26
- package/bin/client-version.js +23 -23
- package/bin/livedesk-client-fast.js +22 -22
- package/bin/livedesk-client-node.js +66 -66
- package/bin/livedesk-client-update-bootstrap.cjs +319 -319
- package/bin/livedesk-client.js +1 -1
- package/package.json +6 -6
- package/src/runtime/agent-process-lifecycle.js +860 -860
- package/src/runtime/client-runtime-server.js +10 -10
- package/src/runtime/hub-wake-listener.js +159 -159
- package/src/runtime/linux-video-acceleration.js +390 -390
- package/src/runtime/windows-owned-process-manifest.js +402 -402
- package/tests/client-version.test.mjs +27 -27
|
@@ -1,402 +1,402 @@
|
|
|
1
|
-
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
-
import {
|
|
3
|
-
chmodSync,
|
|
4
|
-
linkSync,
|
|
5
|
-
mkdirSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
renameSync,
|
|
8
|
-
unlinkSync,
|
|
9
|
-
writeFileSync
|
|
10
|
-
} from 'node:fs';
|
|
11
|
-
import { dirname, join } from 'node:path';
|
|
12
|
-
import os from 'node:os';
|
|
13
|
-
|
|
14
|
-
export const WINDOWS_OWNED_PROCESS_MANIFEST_VERSION = 1;
|
|
15
|
-
export const DEFAULT_WINDOWS_OWNED_PROCESS_MANIFEST_MAX_AGE_MS = 30_000;
|
|
16
|
-
|
|
17
|
-
const WINDOWS_OWNED_PROCESS_MANIFEST_FILE_NAME = 'client-owned-windows-processes.json';
|
|
18
|
-
const MAX_FUTURE_CLOCK_SKEW_MS = 5_000;
|
|
19
|
-
|
|
20
|
-
function resultError(message) {
|
|
21
|
-
return new Error(message);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function resolveNow(now) {
|
|
25
|
-
const value = typeof now === 'function' ? now() : now;
|
|
26
|
-
const milliseconds = value === undefined ? Date.now() : Number(value);
|
|
27
|
-
return Number.isFinite(milliseconds) ? milliseconds : NaN;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function normalizeOwner(value) {
|
|
31
|
-
const owner = {
|
|
32
|
-
pid: Number(value?.pid || 0),
|
|
33
|
-
ownerToken: String(value?.ownerToken || '').trim(),
|
|
34
|
-
ownerInstanceMarker: String(value?.ownerInstanceMarker || '').trim(),
|
|
35
|
-
ownerStartOrder: String(value?.ownerStartOrder || '').trim()
|
|
36
|
-
};
|
|
37
|
-
if (!Number.isInteger(owner.pid)
|
|
38
|
-
|| owner.pid <= 1
|
|
39
|
-
|| !/^[a-f0-9]{32}$/i.test(owner.ownerToken)
|
|
40
|
-
|| !owner.ownerInstanceMarker.startsWith(`${owner.pid}:`)
|
|
41
|
-
|| !/^\d+$/.test(owner.ownerStartOrder)) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
return owner;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function ownersMatch(leftValue, rightValue) {
|
|
48
|
-
const left = normalizeOwner(leftValue);
|
|
49
|
-
const right = normalizeOwner(rightValue);
|
|
50
|
-
return !!left
|
|
51
|
-
&& !!right
|
|
52
|
-
&& left.pid === right.pid
|
|
53
|
-
&& left.ownerToken === right.ownerToken
|
|
54
|
-
&& left.ownerInstanceMarker === right.ownerInstanceMarker
|
|
55
|
-
&& left.ownerStartOrder === right.ownerStartOrder;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function normalizeRecord(value) {
|
|
59
|
-
const record = {
|
|
60
|
-
pid: Number(value?.pid ?? value?.ProcessId ?? 0),
|
|
61
|
-
parentPid: Number(value?.parentPid ?? value?.ParentProcessId ?? 0),
|
|
62
|
-
startMarker: String(value?.startMarker ?? value?.CreationDate ?? '').trim(),
|
|
63
|
-
startOrder: String(value?.startOrder ?? value?.CreationUtcTicks ?? '').trim(),
|
|
64
|
-
depth: Math.max(0, Math.trunc(Number(value?.depth || 0)))
|
|
65
|
-
};
|
|
66
|
-
if (!Number.isInteger(record.pid)
|
|
67
|
-
|| record.pid <= 1
|
|
68
|
-
|| !Number.isInteger(record.parentPid)
|
|
69
|
-
|| record.parentPid < 0
|
|
70
|
-
|| !record.startMarker
|
|
71
|
-
|| !/^\d+$/.test(record.startOrder)
|
|
72
|
-
|| !Number.isFinite(record.depth)) {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
return record;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function normalizeRecords(values) {
|
|
79
|
-
if (!Array.isArray(values)) return null;
|
|
80
|
-
const records = values.map(normalizeRecord);
|
|
81
|
-
if (records.some(record => !record)) return null;
|
|
82
|
-
const unique = new Map();
|
|
83
|
-
for (const record of records) {
|
|
84
|
-
unique.set(`${record.pid}:${record.startOrder}`, record);
|
|
85
|
-
}
|
|
86
|
-
return [...unique.values()];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function sourceRevision(sourceText) {
|
|
90
|
-
return createHash('sha256').update(String(sourceText || ''), 'utf8').digest('hex');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function writeJsonAtomic(path, value) {
|
|
94
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
95
|
-
const temporaryPath = `${path}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
96
|
-
let published = false;
|
|
97
|
-
try {
|
|
98
|
-
writeFileSync(temporaryPath, JSON.stringify(value, null, 2), {
|
|
99
|
-
encoding: 'utf8',
|
|
100
|
-
mode: 0o600,
|
|
101
|
-
flag: 'wx'
|
|
102
|
-
});
|
|
103
|
-
renameSync(temporaryPath, path);
|
|
104
|
-
// Existing state directories can have been created by older versions;
|
|
105
|
-
// enforce the owner-only contract after every atomic replacement too.
|
|
106
|
-
try { chmodSync(path, 0o600); } catch { /* Windows ACLs remain authoritative */ }
|
|
107
|
-
published = true;
|
|
108
|
-
} finally {
|
|
109
|
-
if (!published) {
|
|
110
|
-
try { unlinkSync(temporaryPath); } catch { /* temporary file was never published */ }
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function getWindowsOwnedProcessManifestPath(
|
|
116
|
-
stateDir = join(os.homedir(), '.livedesk')
|
|
117
|
-
) {
|
|
118
|
-
return join(stateDir, WINDOWS_OWNED_PROCESS_MANIFEST_FILE_NAME);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Publishes one immutable launcher generation and all exact Agent records.
|
|
123
|
-
* Every record retains PID + full-precision CreationDate ticks so a later
|
|
124
|
-
* launcher can harmlessly ignore a PID that has since been reused.
|
|
125
|
-
*/
|
|
126
|
-
export function writeWindowsOwnedProcessManifest({
|
|
127
|
-
stateDir,
|
|
128
|
-
owner,
|
|
129
|
-
records,
|
|
130
|
-
now
|
|
131
|
-
} = {}) {
|
|
132
|
-
const exactOwner = normalizeOwner(owner);
|
|
133
|
-
const exactRecords = normalizeRecords(records);
|
|
134
|
-
const nowMs = resolveNow(now);
|
|
135
|
-
if (!exactOwner) {
|
|
136
|
-
return {
|
|
137
|
-
ok: false,
|
|
138
|
-
error: resultError('An exact Windows LiveDesk launcher owner is required.')
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
if (!exactRecords) {
|
|
142
|
-
return {
|
|
143
|
-
ok: false,
|
|
144
|
-
error: resultError('Every Windows LiveDesk process record requires exact PID and CreationDate identity.')
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
if (!Number.isFinite(nowMs)) {
|
|
148
|
-
return { ok: false, error: resultError('A valid manifest publication time is required.') };
|
|
149
|
-
}
|
|
150
|
-
const path = getWindowsOwnedProcessManifestPath(stateDir);
|
|
151
|
-
const updatedAt = new Date(nowMs).toISOString();
|
|
152
|
-
try {
|
|
153
|
-
writeJsonAtomic(path, {
|
|
154
|
-
protocolVersion: WINDOWS_OWNED_PROCESS_MANIFEST_VERSION,
|
|
155
|
-
owner: exactOwner,
|
|
156
|
-
updatedAt,
|
|
157
|
-
records: exactRecords
|
|
158
|
-
});
|
|
159
|
-
return { ok: true, path, records: exactRecords, updatedAt };
|
|
160
|
-
} catch (error) {
|
|
161
|
-
return { ok: false, path, records: [], error };
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function readExactWindowsOwnedProcessManifest({
|
|
166
|
-
stateDir,
|
|
167
|
-
owner,
|
|
168
|
-
maxAgeMs = null,
|
|
169
|
-
includeRevision = false,
|
|
170
|
-
now
|
|
171
|
-
} = {}) {
|
|
172
|
-
const exactOwner = normalizeOwner(owner);
|
|
173
|
-
const nowMs = resolveNow(now);
|
|
174
|
-
const maximumAge = maxAgeMs === null ? null : Number(maxAgeMs);
|
|
175
|
-
const path = getWindowsOwnedProcessManifestPath(stateDir);
|
|
176
|
-
if (!exactOwner) {
|
|
177
|
-
return {
|
|
178
|
-
ok: false,
|
|
179
|
-
records: [],
|
|
180
|
-
error: resultError('An exact Windows LiveDesk launcher owner is required.')
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
if (!Number.isFinite(nowMs)
|
|
184
|
-
|| (maximumAge !== null
|
|
185
|
-
&& (!Number.isFinite(maximumAge) || maximumAge < 0))) {
|
|
186
|
-
return {
|
|
187
|
-
ok: false,
|
|
188
|
-
records: [],
|
|
189
|
-
error: resultError('A valid manifest time and maximum age are required.')
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
let parsed;
|
|
194
|
-
let sourceText;
|
|
195
|
-
try {
|
|
196
|
-
sourceText = readFileSync(path, 'utf8');
|
|
197
|
-
parsed = JSON.parse(sourceText);
|
|
198
|
-
} catch (error) {
|
|
199
|
-
return {
|
|
200
|
-
ok: false,
|
|
201
|
-
records: [],
|
|
202
|
-
missing: error?.code === 'ENOENT',
|
|
203
|
-
error
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
const updatedAt = String(parsed?.updatedAt || '');
|
|
207
|
-
const updatedAtMs = Date.parse(updatedAt);
|
|
208
|
-
if (parsed?.protocolVersion !== WINDOWS_OWNED_PROCESS_MANIFEST_VERSION) {
|
|
209
|
-
return {
|
|
210
|
-
ok: false,
|
|
211
|
-
records: [],
|
|
212
|
-
updatedAt,
|
|
213
|
-
error: resultError('Unsupported Windows LiveDesk process-manifest version.')
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
if (!ownersMatch(parsed?.owner, exactOwner)) {
|
|
217
|
-
return {
|
|
218
|
-
ok: false,
|
|
219
|
-
records: [],
|
|
220
|
-
updatedAt,
|
|
221
|
-
error: resultError('Windows LiveDesk process manifest belongs to another launcher generation.')
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
if (!Number.isFinite(updatedAtMs)
|
|
225
|
-
|| updatedAtMs > nowMs + MAX_FUTURE_CLOCK_SKEW_MS
|
|
226
|
-
|| (maximumAge !== null && nowMs - updatedAtMs > maximumAge)) {
|
|
227
|
-
return {
|
|
228
|
-
ok: false,
|
|
229
|
-
records: [],
|
|
230
|
-
updatedAt,
|
|
231
|
-
error: resultError('Windows LiveDesk process manifest is stale or has an invalid timestamp.')
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
const exactRecords = normalizeRecords(parsed?.records);
|
|
235
|
-
if (!exactRecords) {
|
|
236
|
-
return {
|
|
237
|
-
ok: false,
|
|
238
|
-
records: [],
|
|
239
|
-
updatedAt,
|
|
240
|
-
error: resultError('Windows LiveDesk process manifest contains an invalid process identity.')
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
return {
|
|
244
|
-
ok: true,
|
|
245
|
-
records: exactRecords,
|
|
246
|
-
updatedAt,
|
|
247
|
-
...(includeRevision ? { revision: sourceRevision(sourceText) } : {})
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Reads only a fresh manifest belonging to the exact runtime-lock generation.
|
|
253
|
-
* Owner mismatch, malformed identity, expiry, and PID reuse all fail closed.
|
|
254
|
-
*/
|
|
255
|
-
export function readWindowsOwnedProcessManifest({
|
|
256
|
-
stateDir,
|
|
257
|
-
owner,
|
|
258
|
-
maxAgeMs = DEFAULT_WINDOWS_OWNED_PROCESS_MANIFEST_MAX_AGE_MS,
|
|
259
|
-
now
|
|
260
|
-
} = {}) {
|
|
261
|
-
return readExactWindowsOwnedProcessManifest({
|
|
262
|
-
stateDir,
|
|
263
|
-
owner,
|
|
264
|
-
maxAgeMs,
|
|
265
|
-
now
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Recovers only the exact manifest generation returned by a successful
|
|
271
|
-
* stale-lock compare-and-swap. Its age is intentionally unbounded because a
|
|
272
|
-
* machine can be restarted long after the launcher and Agent died. Malformed,
|
|
273
|
-
* mismatched, or future-dated manifests still fail closed.
|
|
274
|
-
*/
|
|
275
|
-
export function readDeadWindowsOwnedProcessManifest({
|
|
276
|
-
stateDir,
|
|
277
|
-
owner,
|
|
278
|
-
now
|
|
279
|
-
} = {}) {
|
|
280
|
-
return readExactWindowsOwnedProcessManifest({
|
|
281
|
-
stateDir,
|
|
282
|
-
owner,
|
|
283
|
-
maxAgeMs: null,
|
|
284
|
-
includeRevision: true,
|
|
285
|
-
now
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Atomically quarantines and consumes only the exact dead-owner manifest
|
|
291
|
-
* revision that was drained. If another generation replaces the canonical
|
|
292
|
-
* path between validation and rename, the moved file is restored or preserved
|
|
293
|
-
* instead of ever being deleted.
|
|
294
|
-
*/
|
|
295
|
-
export function consumeDeadWindowsOwnedProcessManifest({
|
|
296
|
-
stateDir,
|
|
297
|
-
owner,
|
|
298
|
-
revision,
|
|
299
|
-
now
|
|
300
|
-
} = {}) {
|
|
301
|
-
const exactOwner = normalizeOwner(owner);
|
|
302
|
-
const expectedRevision = String(revision || '').trim().toLowerCase();
|
|
303
|
-
const nowMs = resolveNow(now);
|
|
304
|
-
const path = getWindowsOwnedProcessManifestPath(stateDir);
|
|
305
|
-
if (!exactOwner || !/^[a-f0-9]{64}$/.test(expectedRevision)) {
|
|
306
|
-
return {
|
|
307
|
-
ok: false,
|
|
308
|
-
error: resultError('An exact Windows manifest owner and revision are required for consumption.')
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
if (!Number.isFinite(nowMs)) {
|
|
312
|
-
return { ok: false, error: resultError('A valid manifest consumption time is required.') };
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const current = readDeadWindowsOwnedProcessManifest({
|
|
316
|
-
stateDir,
|
|
317
|
-
owner: exactOwner,
|
|
318
|
-
now: nowMs
|
|
319
|
-
});
|
|
320
|
-
if (!current.ok) {
|
|
321
|
-
return current.missing === true
|
|
322
|
-
? { ok: true, path, consumed: false, missing: true }
|
|
323
|
-
: { ...current, path, consumed: false };
|
|
324
|
-
}
|
|
325
|
-
if (current.revision !== expectedRevision) {
|
|
326
|
-
return {
|
|
327
|
-
ok: false,
|
|
328
|
-
path,
|
|
329
|
-
consumed: false,
|
|
330
|
-
error: resultError('Windows LiveDesk process manifest changed after its exact process set was drained.')
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const quarantinePath = `${path}.consume.${process.pid}.${randomBytes(8).toString('hex')}`;
|
|
335
|
-
try {
|
|
336
|
-
renameSync(path, quarantinePath);
|
|
337
|
-
} catch (error) {
|
|
338
|
-
return { ok: false, path, quarantinePath, consumed: false, error };
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
let movedSourceText = '';
|
|
342
|
-
let movedParsed = null;
|
|
343
|
-
try {
|
|
344
|
-
movedSourceText = readFileSync(quarantinePath, 'utf8');
|
|
345
|
-
movedParsed = JSON.parse(movedSourceText);
|
|
346
|
-
} catch {
|
|
347
|
-
// The exact post-rename validation below restores or preserves this
|
|
348
|
-
// file; unreadable content is never evidence that deletion is safe.
|
|
349
|
-
}
|
|
350
|
-
const movedUpdatedAtMs = Date.parse(String(movedParsed?.updatedAt || ''));
|
|
351
|
-
const movedRecords = normalizeRecords(movedParsed?.records);
|
|
352
|
-
const movedIsExact = movedParsed?.protocolVersion === WINDOWS_OWNED_PROCESS_MANIFEST_VERSION
|
|
353
|
-
&& ownersMatch(movedParsed?.owner, exactOwner)
|
|
354
|
-
&& Number.isFinite(movedUpdatedAtMs)
|
|
355
|
-
&& movedUpdatedAtMs <= nowMs + MAX_FUTURE_CLOCK_SKEW_MS
|
|
356
|
-
&& movedRecords !== null
|
|
357
|
-
&& sourceRevision(movedSourceText) === expectedRevision;
|
|
358
|
-
if (!movedIsExact) {
|
|
359
|
-
let restored = false;
|
|
360
|
-
let restoreError = null;
|
|
361
|
-
try {
|
|
362
|
-
// Hard-link publication is create-if-absent. Unlike rename, it
|
|
363
|
-
// can never overwrite a newer canonical generation that appears
|
|
364
|
-
// during restoration.
|
|
365
|
-
linkSync(quarantinePath, path);
|
|
366
|
-
restored = true;
|
|
367
|
-
try { unlinkSync(quarantinePath); } catch { /* both exact links safely preserve the file */ }
|
|
368
|
-
} catch (error) {
|
|
369
|
-
restoreError = error;
|
|
370
|
-
}
|
|
371
|
-
return {
|
|
372
|
-
ok: false,
|
|
373
|
-
path,
|
|
374
|
-
quarantinePath: restored ? undefined : quarantinePath,
|
|
375
|
-
consumed: false,
|
|
376
|
-
error: resultError(
|
|
377
|
-
'Windows LiveDesk process manifest changed during consumption; '
|
|
378
|
-
+ `${restored ? 'the moved generation was restored.' : 'the moved generation was preserved.'}`
|
|
379
|
-
+ `${restoreError?.message ? ` ${restoreError.message}` : ''}`
|
|
380
|
-
)
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
try {
|
|
385
|
-
unlinkSync(quarantinePath);
|
|
386
|
-
return {
|
|
387
|
-
ok: true,
|
|
388
|
-
path,
|
|
389
|
-
revision: expectedRevision,
|
|
390
|
-
records: movedRecords,
|
|
391
|
-
consumed: true
|
|
392
|
-
};
|
|
393
|
-
} catch (error) {
|
|
394
|
-
return {
|
|
395
|
-
ok: false,
|
|
396
|
-
path,
|
|
397
|
-
quarantinePath,
|
|
398
|
-
consumed: false,
|
|
399
|
-
error
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
}
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
linkSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
writeFileSync
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
|
|
14
|
+
export const WINDOWS_OWNED_PROCESS_MANIFEST_VERSION = 1;
|
|
15
|
+
export const DEFAULT_WINDOWS_OWNED_PROCESS_MANIFEST_MAX_AGE_MS = 30_000;
|
|
16
|
+
|
|
17
|
+
const WINDOWS_OWNED_PROCESS_MANIFEST_FILE_NAME = 'client-owned-windows-processes.json';
|
|
18
|
+
const MAX_FUTURE_CLOCK_SKEW_MS = 5_000;
|
|
19
|
+
|
|
20
|
+
function resultError(message) {
|
|
21
|
+
return new Error(message);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveNow(now) {
|
|
25
|
+
const value = typeof now === 'function' ? now() : now;
|
|
26
|
+
const milliseconds = value === undefined ? Date.now() : Number(value);
|
|
27
|
+
return Number.isFinite(milliseconds) ? milliseconds : NaN;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeOwner(value) {
|
|
31
|
+
const owner = {
|
|
32
|
+
pid: Number(value?.pid || 0),
|
|
33
|
+
ownerToken: String(value?.ownerToken || '').trim(),
|
|
34
|
+
ownerInstanceMarker: String(value?.ownerInstanceMarker || '').trim(),
|
|
35
|
+
ownerStartOrder: String(value?.ownerStartOrder || '').trim()
|
|
36
|
+
};
|
|
37
|
+
if (!Number.isInteger(owner.pid)
|
|
38
|
+
|| owner.pid <= 1
|
|
39
|
+
|| !/^[a-f0-9]{32}$/i.test(owner.ownerToken)
|
|
40
|
+
|| !owner.ownerInstanceMarker.startsWith(`${owner.pid}:`)
|
|
41
|
+
|| !/^\d+$/.test(owner.ownerStartOrder)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return owner;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ownersMatch(leftValue, rightValue) {
|
|
48
|
+
const left = normalizeOwner(leftValue);
|
|
49
|
+
const right = normalizeOwner(rightValue);
|
|
50
|
+
return !!left
|
|
51
|
+
&& !!right
|
|
52
|
+
&& left.pid === right.pid
|
|
53
|
+
&& left.ownerToken === right.ownerToken
|
|
54
|
+
&& left.ownerInstanceMarker === right.ownerInstanceMarker
|
|
55
|
+
&& left.ownerStartOrder === right.ownerStartOrder;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeRecord(value) {
|
|
59
|
+
const record = {
|
|
60
|
+
pid: Number(value?.pid ?? value?.ProcessId ?? 0),
|
|
61
|
+
parentPid: Number(value?.parentPid ?? value?.ParentProcessId ?? 0),
|
|
62
|
+
startMarker: String(value?.startMarker ?? value?.CreationDate ?? '').trim(),
|
|
63
|
+
startOrder: String(value?.startOrder ?? value?.CreationUtcTicks ?? '').trim(),
|
|
64
|
+
depth: Math.max(0, Math.trunc(Number(value?.depth || 0)))
|
|
65
|
+
};
|
|
66
|
+
if (!Number.isInteger(record.pid)
|
|
67
|
+
|| record.pid <= 1
|
|
68
|
+
|| !Number.isInteger(record.parentPid)
|
|
69
|
+
|| record.parentPid < 0
|
|
70
|
+
|| !record.startMarker
|
|
71
|
+
|| !/^\d+$/.test(record.startOrder)
|
|
72
|
+
|| !Number.isFinite(record.depth)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return record;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeRecords(values) {
|
|
79
|
+
if (!Array.isArray(values)) return null;
|
|
80
|
+
const records = values.map(normalizeRecord);
|
|
81
|
+
if (records.some(record => !record)) return null;
|
|
82
|
+
const unique = new Map();
|
|
83
|
+
for (const record of records) {
|
|
84
|
+
unique.set(`${record.pid}:${record.startOrder}`, record);
|
|
85
|
+
}
|
|
86
|
+
return [...unique.values()];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sourceRevision(sourceText) {
|
|
90
|
+
return createHash('sha256').update(String(sourceText || ''), 'utf8').digest('hex');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function writeJsonAtomic(path, value) {
|
|
94
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
95
|
+
const temporaryPath = `${path}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
96
|
+
let published = false;
|
|
97
|
+
try {
|
|
98
|
+
writeFileSync(temporaryPath, JSON.stringify(value, null, 2), {
|
|
99
|
+
encoding: 'utf8',
|
|
100
|
+
mode: 0o600,
|
|
101
|
+
flag: 'wx'
|
|
102
|
+
});
|
|
103
|
+
renameSync(temporaryPath, path);
|
|
104
|
+
// Existing state directories can have been created by older versions;
|
|
105
|
+
// enforce the owner-only contract after every atomic replacement too.
|
|
106
|
+
try { chmodSync(path, 0o600); } catch { /* Windows ACLs remain authoritative */ }
|
|
107
|
+
published = true;
|
|
108
|
+
} finally {
|
|
109
|
+
if (!published) {
|
|
110
|
+
try { unlinkSync(temporaryPath); } catch { /* temporary file was never published */ }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getWindowsOwnedProcessManifestPath(
|
|
116
|
+
stateDir = join(os.homedir(), '.livedesk')
|
|
117
|
+
) {
|
|
118
|
+
return join(stateDir, WINDOWS_OWNED_PROCESS_MANIFEST_FILE_NAME);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Publishes one immutable launcher generation and all exact Agent records.
|
|
123
|
+
* Every record retains PID + full-precision CreationDate ticks so a later
|
|
124
|
+
* launcher can harmlessly ignore a PID that has since been reused.
|
|
125
|
+
*/
|
|
126
|
+
export function writeWindowsOwnedProcessManifest({
|
|
127
|
+
stateDir,
|
|
128
|
+
owner,
|
|
129
|
+
records,
|
|
130
|
+
now
|
|
131
|
+
} = {}) {
|
|
132
|
+
const exactOwner = normalizeOwner(owner);
|
|
133
|
+
const exactRecords = normalizeRecords(records);
|
|
134
|
+
const nowMs = resolveNow(now);
|
|
135
|
+
if (!exactOwner) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
error: resultError('An exact Windows LiveDesk launcher owner is required.')
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (!exactRecords) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
error: resultError('Every Windows LiveDesk process record requires exact PID and CreationDate identity.')
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (!Number.isFinite(nowMs)) {
|
|
148
|
+
return { ok: false, error: resultError('A valid manifest publication time is required.') };
|
|
149
|
+
}
|
|
150
|
+
const path = getWindowsOwnedProcessManifestPath(stateDir);
|
|
151
|
+
const updatedAt = new Date(nowMs).toISOString();
|
|
152
|
+
try {
|
|
153
|
+
writeJsonAtomic(path, {
|
|
154
|
+
protocolVersion: WINDOWS_OWNED_PROCESS_MANIFEST_VERSION,
|
|
155
|
+
owner: exactOwner,
|
|
156
|
+
updatedAt,
|
|
157
|
+
records: exactRecords
|
|
158
|
+
});
|
|
159
|
+
return { ok: true, path, records: exactRecords, updatedAt };
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return { ok: false, path, records: [], error };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readExactWindowsOwnedProcessManifest({
|
|
166
|
+
stateDir,
|
|
167
|
+
owner,
|
|
168
|
+
maxAgeMs = null,
|
|
169
|
+
includeRevision = false,
|
|
170
|
+
now
|
|
171
|
+
} = {}) {
|
|
172
|
+
const exactOwner = normalizeOwner(owner);
|
|
173
|
+
const nowMs = resolveNow(now);
|
|
174
|
+
const maximumAge = maxAgeMs === null ? null : Number(maxAgeMs);
|
|
175
|
+
const path = getWindowsOwnedProcessManifestPath(stateDir);
|
|
176
|
+
if (!exactOwner) {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
records: [],
|
|
180
|
+
error: resultError('An exact Windows LiveDesk launcher owner is required.')
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (!Number.isFinite(nowMs)
|
|
184
|
+
|| (maximumAge !== null
|
|
185
|
+
&& (!Number.isFinite(maximumAge) || maximumAge < 0))) {
|
|
186
|
+
return {
|
|
187
|
+
ok: false,
|
|
188
|
+
records: [],
|
|
189
|
+
error: resultError('A valid manifest time and maximum age are required.')
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let parsed;
|
|
194
|
+
let sourceText;
|
|
195
|
+
try {
|
|
196
|
+
sourceText = readFileSync(path, 'utf8');
|
|
197
|
+
parsed = JSON.parse(sourceText);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
records: [],
|
|
202
|
+
missing: error?.code === 'ENOENT',
|
|
203
|
+
error
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const updatedAt = String(parsed?.updatedAt || '');
|
|
207
|
+
const updatedAtMs = Date.parse(updatedAt);
|
|
208
|
+
if (parsed?.protocolVersion !== WINDOWS_OWNED_PROCESS_MANIFEST_VERSION) {
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
records: [],
|
|
212
|
+
updatedAt,
|
|
213
|
+
error: resultError('Unsupported Windows LiveDesk process-manifest version.')
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (!ownersMatch(parsed?.owner, exactOwner)) {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
records: [],
|
|
220
|
+
updatedAt,
|
|
221
|
+
error: resultError('Windows LiveDesk process manifest belongs to another launcher generation.')
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (!Number.isFinite(updatedAtMs)
|
|
225
|
+
|| updatedAtMs > nowMs + MAX_FUTURE_CLOCK_SKEW_MS
|
|
226
|
+
|| (maximumAge !== null && nowMs - updatedAtMs > maximumAge)) {
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
records: [],
|
|
230
|
+
updatedAt,
|
|
231
|
+
error: resultError('Windows LiveDesk process manifest is stale or has an invalid timestamp.')
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const exactRecords = normalizeRecords(parsed?.records);
|
|
235
|
+
if (!exactRecords) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
records: [],
|
|
239
|
+
updatedAt,
|
|
240
|
+
error: resultError('Windows LiveDesk process manifest contains an invalid process identity.')
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
ok: true,
|
|
245
|
+
records: exactRecords,
|
|
246
|
+
updatedAt,
|
|
247
|
+
...(includeRevision ? { revision: sourceRevision(sourceText) } : {})
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Reads only a fresh manifest belonging to the exact runtime-lock generation.
|
|
253
|
+
* Owner mismatch, malformed identity, expiry, and PID reuse all fail closed.
|
|
254
|
+
*/
|
|
255
|
+
export function readWindowsOwnedProcessManifest({
|
|
256
|
+
stateDir,
|
|
257
|
+
owner,
|
|
258
|
+
maxAgeMs = DEFAULT_WINDOWS_OWNED_PROCESS_MANIFEST_MAX_AGE_MS,
|
|
259
|
+
now
|
|
260
|
+
} = {}) {
|
|
261
|
+
return readExactWindowsOwnedProcessManifest({
|
|
262
|
+
stateDir,
|
|
263
|
+
owner,
|
|
264
|
+
maxAgeMs,
|
|
265
|
+
now
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Recovers only the exact manifest generation returned by a successful
|
|
271
|
+
* stale-lock compare-and-swap. Its age is intentionally unbounded because a
|
|
272
|
+
* machine can be restarted long after the launcher and Agent died. Malformed,
|
|
273
|
+
* mismatched, or future-dated manifests still fail closed.
|
|
274
|
+
*/
|
|
275
|
+
export function readDeadWindowsOwnedProcessManifest({
|
|
276
|
+
stateDir,
|
|
277
|
+
owner,
|
|
278
|
+
now
|
|
279
|
+
} = {}) {
|
|
280
|
+
return readExactWindowsOwnedProcessManifest({
|
|
281
|
+
stateDir,
|
|
282
|
+
owner,
|
|
283
|
+
maxAgeMs: null,
|
|
284
|
+
includeRevision: true,
|
|
285
|
+
now
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Atomically quarantines and consumes only the exact dead-owner manifest
|
|
291
|
+
* revision that was drained. If another generation replaces the canonical
|
|
292
|
+
* path between validation and rename, the moved file is restored or preserved
|
|
293
|
+
* instead of ever being deleted.
|
|
294
|
+
*/
|
|
295
|
+
export function consumeDeadWindowsOwnedProcessManifest({
|
|
296
|
+
stateDir,
|
|
297
|
+
owner,
|
|
298
|
+
revision,
|
|
299
|
+
now
|
|
300
|
+
} = {}) {
|
|
301
|
+
const exactOwner = normalizeOwner(owner);
|
|
302
|
+
const expectedRevision = String(revision || '').trim().toLowerCase();
|
|
303
|
+
const nowMs = resolveNow(now);
|
|
304
|
+
const path = getWindowsOwnedProcessManifestPath(stateDir);
|
|
305
|
+
if (!exactOwner || !/^[a-f0-9]{64}$/.test(expectedRevision)) {
|
|
306
|
+
return {
|
|
307
|
+
ok: false,
|
|
308
|
+
error: resultError('An exact Windows manifest owner and revision are required for consumption.')
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (!Number.isFinite(nowMs)) {
|
|
312
|
+
return { ok: false, error: resultError('A valid manifest consumption time is required.') };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const current = readDeadWindowsOwnedProcessManifest({
|
|
316
|
+
stateDir,
|
|
317
|
+
owner: exactOwner,
|
|
318
|
+
now: nowMs
|
|
319
|
+
});
|
|
320
|
+
if (!current.ok) {
|
|
321
|
+
return current.missing === true
|
|
322
|
+
? { ok: true, path, consumed: false, missing: true }
|
|
323
|
+
: { ...current, path, consumed: false };
|
|
324
|
+
}
|
|
325
|
+
if (current.revision !== expectedRevision) {
|
|
326
|
+
return {
|
|
327
|
+
ok: false,
|
|
328
|
+
path,
|
|
329
|
+
consumed: false,
|
|
330
|
+
error: resultError('Windows LiveDesk process manifest changed after its exact process set was drained.')
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const quarantinePath = `${path}.consume.${process.pid}.${randomBytes(8).toString('hex')}`;
|
|
335
|
+
try {
|
|
336
|
+
renameSync(path, quarantinePath);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
return { ok: false, path, quarantinePath, consumed: false, error };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let movedSourceText = '';
|
|
342
|
+
let movedParsed = null;
|
|
343
|
+
try {
|
|
344
|
+
movedSourceText = readFileSync(quarantinePath, 'utf8');
|
|
345
|
+
movedParsed = JSON.parse(movedSourceText);
|
|
346
|
+
} catch {
|
|
347
|
+
// The exact post-rename validation below restores or preserves this
|
|
348
|
+
// file; unreadable content is never evidence that deletion is safe.
|
|
349
|
+
}
|
|
350
|
+
const movedUpdatedAtMs = Date.parse(String(movedParsed?.updatedAt || ''));
|
|
351
|
+
const movedRecords = normalizeRecords(movedParsed?.records);
|
|
352
|
+
const movedIsExact = movedParsed?.protocolVersion === WINDOWS_OWNED_PROCESS_MANIFEST_VERSION
|
|
353
|
+
&& ownersMatch(movedParsed?.owner, exactOwner)
|
|
354
|
+
&& Number.isFinite(movedUpdatedAtMs)
|
|
355
|
+
&& movedUpdatedAtMs <= nowMs + MAX_FUTURE_CLOCK_SKEW_MS
|
|
356
|
+
&& movedRecords !== null
|
|
357
|
+
&& sourceRevision(movedSourceText) === expectedRevision;
|
|
358
|
+
if (!movedIsExact) {
|
|
359
|
+
let restored = false;
|
|
360
|
+
let restoreError = null;
|
|
361
|
+
try {
|
|
362
|
+
// Hard-link publication is create-if-absent. Unlike rename, it
|
|
363
|
+
// can never overwrite a newer canonical generation that appears
|
|
364
|
+
// during restoration.
|
|
365
|
+
linkSync(quarantinePath, path);
|
|
366
|
+
restored = true;
|
|
367
|
+
try { unlinkSync(quarantinePath); } catch { /* both exact links safely preserve the file */ }
|
|
368
|
+
} catch (error) {
|
|
369
|
+
restoreError = error;
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
ok: false,
|
|
373
|
+
path,
|
|
374
|
+
quarantinePath: restored ? undefined : quarantinePath,
|
|
375
|
+
consumed: false,
|
|
376
|
+
error: resultError(
|
|
377
|
+
'Windows LiveDesk process manifest changed during consumption; '
|
|
378
|
+
+ `${restored ? 'the moved generation was restored.' : 'the moved generation was preserved.'}`
|
|
379
|
+
+ `${restoreError?.message ? ` ${restoreError.message}` : ''}`
|
|
380
|
+
)
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
unlinkSync(quarantinePath);
|
|
386
|
+
return {
|
|
387
|
+
ok: true,
|
|
388
|
+
path,
|
|
389
|
+
revision: expectedRevision,
|
|
390
|
+
records: movedRecords,
|
|
391
|
+
consumed: true
|
|
392
|
+
};
|
|
393
|
+
} catch (error) {
|
|
394
|
+
return {
|
|
395
|
+
ok: false,
|
|
396
|
+
path,
|
|
397
|
+
quarantinePath,
|
|
398
|
+
consumed: false,
|
|
399
|
+
error
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|