@livedesk/client 0.1.213 → 0.1.215
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.215",
|
|
4
4
|
"description": "LiveDesk local remote client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
"livedesk-client-fast": "bin/livedesk-client-fast.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"bin/",
|
|
14
|
-
"src/",
|
|
15
|
-
"
|
|
13
|
+
"bin/",
|
|
14
|
+
"src/",
|
|
15
|
+
"tests/",
|
|
16
|
+
"README.md",
|
|
16
17
|
"THIRD_PARTY_NOTICES.md"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
@@ -40,10 +41,10 @@
|
|
|
40
41
|
"ws": "^8.18.3"
|
|
41
42
|
},
|
|
42
43
|
"optionalDependencies": {
|
|
43
|
-
"@livedesk/fast-linux-x64": "0.1.
|
|
44
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
45
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
46
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
44
|
+
"@livedesk/fast-linux-x64": "0.1.419",
|
|
45
|
+
"@livedesk/fast-osx-arm64": "0.1.419",
|
|
46
|
+
"@livedesk/fast-osx-x64": "0.1.419",
|
|
47
|
+
"@livedesk/fast-win-x64": "0.1.419"
|
|
47
48
|
},
|
|
48
49
|
"publishConfig": {
|
|
49
50
|
"access": "public"
|
|
@@ -165,6 +165,12 @@ function readCpuTotals() {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
function finiteNumber(value) {
|
|
168
|
+
if (value === null
|
|
169
|
+
|| value === undefined
|
|
170
|
+
|| typeof value === 'boolean'
|
|
171
|
+
|| (typeof value === 'string' && value.trim() === '')) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
168
174
|
const number = Number(value);
|
|
169
175
|
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
170
176
|
}
|
|
@@ -300,6 +306,53 @@ function normalizeGpu(value) {
|
|
|
300
306
|
};
|
|
301
307
|
}
|
|
302
308
|
|
|
309
|
+
export function normalizeGpuIdentityName(value) {
|
|
310
|
+
return normalizeString(value, 160)
|
|
311
|
+
.normalize('NFKC')
|
|
312
|
+
.replace(/\((?:r|tm)\)/giu, '')
|
|
313
|
+
.replace(/[®™]/gu, '')
|
|
314
|
+
.toLocaleLowerCase('en-US')
|
|
315
|
+
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
|
316
|
+
.trim();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function mergeGpuSnapshots(platformGpus = [], telemetryGpus = []) {
|
|
320
|
+
const merged = [];
|
|
321
|
+
const indexesByIdentity = new Map();
|
|
322
|
+
for (const value of Array.isArray(platformGpus) ? platformGpus : []) {
|
|
323
|
+
const gpu = normalizeGpu(value);
|
|
324
|
+
const identity = normalizeGpuIdentityName(gpu?.name);
|
|
325
|
+
if (!gpu || !identity) continue;
|
|
326
|
+
const index = merged.length;
|
|
327
|
+
merged.push(gpu);
|
|
328
|
+
const indexes = indexesByIdentity.get(identity) || [];
|
|
329
|
+
indexes.push(index);
|
|
330
|
+
indexesByIdentity.set(identity, indexes);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const matchedIndexes = new Set();
|
|
334
|
+
for (const value of Array.isArray(telemetryGpus) ? telemetryGpus : []) {
|
|
335
|
+
const gpu = normalizeGpu(value);
|
|
336
|
+
const identity = normalizeGpuIdentityName(gpu?.name);
|
|
337
|
+
if (!gpu || !identity) continue;
|
|
338
|
+
const existingIndex = (indexesByIdentity.get(identity) || [])
|
|
339
|
+
.find(index => !matchedIndexes.has(index));
|
|
340
|
+
if (existingIndex === undefined) {
|
|
341
|
+
merged.push(gpu);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
matchedIndexes.add(existingIndex);
|
|
345
|
+
const existing = merged[existingIndex];
|
|
346
|
+
merged[existingIndex] = {
|
|
347
|
+
...existing,
|
|
348
|
+
usagePercent: gpu.usagePercent ?? existing.usagePercent,
|
|
349
|
+
memoryTotalBytes: gpu.memoryTotalBytes ?? existing.memoryTotalBytes,
|
|
350
|
+
memoryUsedBytes: gpu.memoryUsedBytes ?? existing.memoryUsedBytes
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return merged;
|
|
354
|
+
}
|
|
355
|
+
|
|
303
356
|
function normalizeDisk(value) {
|
|
304
357
|
if (!value || typeof value !== 'object') return null;
|
|
305
358
|
const mount = normalizeString(value.mount, 80);
|
|
@@ -430,7 +483,10 @@ async function collectHardwareSnapshot() {
|
|
|
430
483
|
|
|
431
484
|
const nvidiaGpus = await nvidiaPromise;
|
|
432
485
|
return {
|
|
433
|
-
|
|
486
|
+
// Preserve the platform adapter inventory on hybrid-GPU computers. The
|
|
487
|
+
// NVIDIA sampler enriches its matching adapter instead of replacing every
|
|
488
|
+
// non-NVIDIA display adapter with the telemetry subset.
|
|
489
|
+
gpus: mergeGpuSnapshots(platformSnapshot.gpus, nvidiaGpus),
|
|
434
490
|
disks: platformSnapshot.disks,
|
|
435
491
|
collectedAt: new Date().toISOString()
|
|
436
492
|
};
|
|
@@ -730,7 +786,11 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
730
786
|
try {
|
|
731
787
|
const gpus = await collectNvidiaGpuSnapshot();
|
|
732
788
|
if (gpus.length > 0) {
|
|
733
|
-
hardwareSnapshot = {
|
|
789
|
+
hardwareSnapshot = {
|
|
790
|
+
...hardwareSnapshot,
|
|
791
|
+
gpus: mergeGpuSnapshots(hardwareSnapshot.gpus, gpus),
|
|
792
|
+
collectedAt: new Date().toISOString()
|
|
793
|
+
};
|
|
734
794
|
}
|
|
735
795
|
} finally {
|
|
736
796
|
gpuRefreshInFlight = false;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import {
|
|
3
3
|
chmodSync,
|
|
4
|
+
linkSync,
|
|
4
5
|
mkdirSync,
|
|
5
6
|
readFileSync,
|
|
6
7
|
renameSync,
|
|
@@ -85,6 +86,10 @@ function normalizeRecords(values) {
|
|
|
85
86
|
return [...unique.values()];
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
function sourceRevision(sourceText) {
|
|
90
|
+
return createHash('sha256').update(String(sourceText || ''), 'utf8').digest('hex');
|
|
91
|
+
}
|
|
92
|
+
|
|
88
93
|
function writeJsonAtomic(path, value) {
|
|
89
94
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
90
95
|
const temporaryPath = `${path}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
@@ -157,19 +162,16 @@ export function writeWindowsOwnedProcessManifest({
|
|
|
157
162
|
}
|
|
158
163
|
}
|
|
159
164
|
|
|
160
|
-
|
|
161
|
-
* Reads only a fresh manifest belonging to the exact runtime-lock generation.
|
|
162
|
-
* Owner mismatch, malformed identity, expiry, and PID reuse all fail closed.
|
|
163
|
-
*/
|
|
164
|
-
export function readWindowsOwnedProcessManifest({
|
|
165
|
+
function readExactWindowsOwnedProcessManifest({
|
|
165
166
|
stateDir,
|
|
166
167
|
owner,
|
|
167
|
-
maxAgeMs =
|
|
168
|
+
maxAgeMs = null,
|
|
169
|
+
includeRevision = false,
|
|
168
170
|
now
|
|
169
171
|
} = {}) {
|
|
170
172
|
const exactOwner = normalizeOwner(owner);
|
|
171
173
|
const nowMs = resolveNow(now);
|
|
172
|
-
const maximumAge = Number(maxAgeMs);
|
|
174
|
+
const maximumAge = maxAgeMs === null ? null : Number(maxAgeMs);
|
|
173
175
|
const path = getWindowsOwnedProcessManifestPath(stateDir);
|
|
174
176
|
if (!exactOwner) {
|
|
175
177
|
return {
|
|
@@ -179,8 +181,8 @@ export function readWindowsOwnedProcessManifest({
|
|
|
179
181
|
};
|
|
180
182
|
}
|
|
181
183
|
if (!Number.isFinite(nowMs)
|
|
182
|
-
||
|
|
183
|
-
|
|
184
|
+
|| (maximumAge !== null
|
|
185
|
+
&& (!Number.isFinite(maximumAge) || maximumAge < 0))) {
|
|
184
186
|
return {
|
|
185
187
|
ok: false,
|
|
186
188
|
records: [],
|
|
@@ -189,10 +191,17 @@ export function readWindowsOwnedProcessManifest({
|
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
let parsed;
|
|
194
|
+
let sourceText;
|
|
192
195
|
try {
|
|
193
|
-
|
|
196
|
+
sourceText = readFileSync(path, 'utf8');
|
|
197
|
+
parsed = JSON.parse(sourceText);
|
|
194
198
|
} catch (error) {
|
|
195
|
-
return {
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
records: [],
|
|
202
|
+
missing: error?.code === 'ENOENT',
|
|
203
|
+
error
|
|
204
|
+
};
|
|
196
205
|
}
|
|
197
206
|
const updatedAt = String(parsed?.updatedAt || '');
|
|
198
207
|
const updatedAtMs = Date.parse(updatedAt);
|
|
@@ -214,7 +223,7 @@ export function readWindowsOwnedProcessManifest({
|
|
|
214
223
|
}
|
|
215
224
|
if (!Number.isFinite(updatedAtMs)
|
|
216
225
|
|| updatedAtMs > nowMs + MAX_FUTURE_CLOCK_SKEW_MS
|
|
217
|
-
|| nowMs - updatedAtMs > maximumAge) {
|
|
226
|
+
|| (maximumAge !== null && nowMs - updatedAtMs > maximumAge)) {
|
|
218
227
|
return {
|
|
219
228
|
ok: false,
|
|
220
229
|
records: [],
|
|
@@ -231,5 +240,163 @@ export function readWindowsOwnedProcessManifest({
|
|
|
231
240
|
error: resultError('Windows LiveDesk process manifest contains an invalid process identity.')
|
|
232
241
|
};
|
|
233
242
|
}
|
|
234
|
-
return {
|
|
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
|
+
}
|
|
235
402
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
import { formatClientVersionBanner, readClientPackageVersion } from '../bin/client-version.js';
|
|
5
|
+
|
|
6
|
+
const packageInfo = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
7
|
+
|
|
8
|
+
test('reads the version shipped in the @livedesk/client npm package', () => {
|
|
9
|
+
assert.equal(readClientPackageVersion(), packageInfo.version);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('prints the public launcher version in the user-facing banner', () => {
|
|
13
|
+
assert.equal(
|
|
14
|
+
formatClientVersionBanner({
|
|
15
|
+
LIVEDESK_NPM_LAUNCHER_NAME: 'livedesk',
|
|
16
|
+
LIVEDESK_NPM_LAUNCHER_VERSION: '9.8.7'
|
|
17
|
+
}),
|
|
18
|
+
'[LiveDesk Client] livedesk@9.8.7'
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('uses the product name for direct client launches', () => {
|
|
23
|
+
assert.equal(
|
|
24
|
+
formatClientVersionBanner({}),
|
|
25
|
+
`[LiveDesk Client] livedesk@${packageInfo.version}`
|
|
26
|
+
);
|
|
27
|
+
});
|