livedesk 0.1.593 → 0.1.595

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.
Files changed (31) hide show
  1. package/bin/livedesk.js +8 -8
  2. package/bootstrap/role-bootstrap-server.js +1 -1
  3. package/client/bin/livedesk-client-fast.js +1 -1
  4. package/client/bin/livedesk-client-node.js +1 -1
  5. package/client/bin/livedesk-client.js +30 -30
  6. package/client/package.json +6 -6
  7. package/client/src/runtime/client-runtime-server.js +4 -4
  8. package/diagnostics/livedesk-mac-physical-isolation.mjs +354 -354
  9. package/diagnostics/livedesk-transport-physical-core.mjs +702 -702
  10. package/electron/auth-config.mjs +10 -10
  11. package/electron/bounded-desktop-log.mjs +609 -609
  12. package/electron/electron-builder.win-dev.yml +1 -1
  13. package/electron/icon.svg +11 -11
  14. package/electron/main.mjs +5 -5
  15. package/electron/runtime-process-owner.mjs +287 -287
  16. package/hub/package.json +2 -2
  17. package/hub/src/server.js +2 -2
  18. package/package.json +6 -6
  19. package/runtime-core/package.json +1 -1
  20. package/runtime-core/src/auth-config.js +1 -1
  21. package/web/dist/app.webmanifest +1 -1
  22. package/web/dist/assets/LiveDeskApp-CvQUgQ08.js +178 -0
  23. package/web/dist/assets/{icons-DwJmccVC.js → icons-tFaWqIIe.js} +1 -1
  24. package/web/dist/assets/{index-DjIbOQxl.js → index-BDVKTkV3.js} +3 -3
  25. package/web/dist/assets/{index-DRI7OJI6.css → index-CpHNEjHy.css} +1 -1
  26. package/web/dist/assets/{react-DOZUPHF8.js → react-BQos8uGS.js} +1 -1
  27. package/web/dist/favicon.svg +11 -11
  28. package/web/dist/index.html +6 -6
  29. package/web/dist/livedesk-build-evidence.json +25 -25
  30. package/web/dist/sw.js +1 -1
  31. package/web/dist/assets/LiveDeskApp-Djx1CsIp.js +0 -178
@@ -1,609 +1,609 @@
1
- import {
2
- createWriteStream,
3
- existsSync,
4
- mkdirSync,
5
- statSync,
6
- truncateSync
7
- } from 'node:fs';
8
- import {
9
- rename,
10
- rm
11
- } from 'node:fs/promises';
12
- import { dirname } from 'node:path';
13
-
14
- export const DEFAULT_DESKTOP_LOG_MAX_FILE_BYTES = 512 * 1024;
15
- export const DEFAULT_DESKTOP_LOG_FILE_COUNT = 3;
16
- export const DEFAULT_DESKTOP_LOG_MAX_PENDING_ENTRIES = 24;
17
- export const DEFAULT_DESKTOP_LOG_MAX_PENDING_BYTES = 64 * 1024;
18
- export const DEFAULT_DESKTOP_LOG_STREAM_HIGH_WATER_BYTES = 32 * 1024;
19
- export const DEFAULT_DESKTOP_LOG_CLOSE_TIMEOUT_MS = 1_000;
20
- export const DEFAULT_DESKTOP_LOG_LOW_PRIORITY_RECORDS_PER_SECOND = 40;
21
- export const DEFAULT_DESKTOP_LOG_LOW_PRIORITY_BYTES_PER_SECOND = 128 * 1024;
22
- export const DEFAULT_DESKTOP_LOG_MAX_MESSAGE_BYTES = 16 * 1024;
23
-
24
- function boundedInteger(value, fallback, minimum, maximum) {
25
- const numeric = Number(value);
26
- return Number.isFinite(numeric)
27
- ? Math.max(minimum, Math.min(maximum, Math.trunc(numeric)))
28
- : fallback;
29
- }
30
-
31
- function byteLength(value) {
32
- return Buffer.byteLength(String(value || ''), 'utf8');
33
- }
34
-
35
- function rawByteLength(value) {
36
- if (Buffer.isBuffer(value)) return value.byteLength;
37
- if (typeof ArrayBuffer !== 'undefined') {
38
- if (value instanceof ArrayBuffer) return value.byteLength;
39
- if (ArrayBuffer.isView(value)) return value.byteLength;
40
- }
41
- return byteLength(value);
42
- }
43
-
44
- function truncateUtf8(value, maximumBytes) {
45
- const source = String(value ?? '');
46
- if (byteLength(source) <= maximumBytes) return source;
47
- const suffix = '…[truncated]';
48
- const suffixBytes = byteLength(suffix);
49
- const sourceBuffer = Buffer.from(source, 'utf8');
50
- const retainedBytes = Math.max(0, maximumBytes - suffixBytes);
51
- return `${sourceBuffer.subarray(0, retainedBytes).toString('utf8').replace(/\uFFFD+$/g, '')}${suffix}`;
52
- }
53
-
54
- function encodeRecordWithin(record, maximumBytes) {
55
- let encoded = `${JSON.stringify(record)}\n`;
56
- if (byteLength(encoded) <= maximumBytes) return encoded;
57
- const sourceBytes = byteLength(record.message);
58
- let low = 0;
59
- let high = sourceBytes;
60
- let best = `${JSON.stringify({ ...record, message: '' })}\n`;
61
- while (low <= high) {
62
- const middle = Math.trunc((low + high) / 2);
63
- const candidate = `${JSON.stringify({
64
- ...record,
65
- message: truncateUtf8(record.message, middle)
66
- })}\n`;
67
- if (byteLength(candidate) <= maximumBytes) {
68
- best = candidate;
69
- low = middle + 1;
70
- } else {
71
- high = middle - 1;
72
- }
73
- }
74
- return best;
75
- }
76
-
77
- function finishStreamWithin(
78
- stream,
79
- timeoutMs,
80
- onForcedClose,
81
- onTimerStarted = () => {},
82
- onTimerStopped = () => {}
83
- ) {
84
- if (!stream || stream.destroyed || stream.closed) return Promise.resolve();
85
- return new Promise(resolveFinish => {
86
- let settled = false;
87
- const finish = () => {
88
- if (settled) return;
89
- settled = true;
90
- clearTimeout(timer);
91
- onTimerStopped();
92
- stream.off?.('error', finish);
93
- stream.off?.('close', finish);
94
- resolveFinish();
95
- };
96
- onTimerStarted();
97
- const timer = setTimeout(() => {
98
- onForcedClose();
99
- try { stream.destroy(); } catch { /* exact stream already closed */ }
100
- finish();
101
- }, Math.max(1, timeoutMs));
102
- stream.once?.('error', finish);
103
- stream.once?.('close', finish);
104
- try {
105
- stream.end(finish);
106
- } catch {
107
- finish();
108
- }
109
- });
110
- }
111
-
112
- export function createBoundedDesktopLogger(options = {}) {
113
- const logPath = String(options.logPath || '').trim();
114
- if (!logPath) throw new Error('desktop-log-path-required');
115
-
116
- const maxFileBytes = boundedInteger(
117
- options.maxFileBytes,
118
- DEFAULT_DESKTOP_LOG_MAX_FILE_BYTES,
119
- 1024,
120
- 16 * 1024 * 1024
121
- );
122
- const fileCount = boundedInteger(
123
- options.fileCount,
124
- DEFAULT_DESKTOP_LOG_FILE_COUNT,
125
- 2,
126
- 8
127
- );
128
- const maxPendingEntries = boundedInteger(
129
- options.maxPendingEntries,
130
- DEFAULT_DESKTOP_LOG_MAX_PENDING_ENTRIES,
131
- 1,
132
- 128
133
- );
134
- const maxPendingBytes = boundedInteger(
135
- options.maxPendingBytes,
136
- DEFAULT_DESKTOP_LOG_MAX_PENDING_BYTES,
137
- 1024,
138
- 512 * 1024
139
- );
140
- const streamHighWaterBytes = boundedInteger(
141
- options.streamHighWaterBytes,
142
- DEFAULT_DESKTOP_LOG_STREAM_HIGH_WATER_BYTES,
143
- 1024,
144
- 256 * 1024
145
- );
146
- const closeTimeoutMs = boundedInteger(
147
- options.closeTimeoutMs,
148
- DEFAULT_DESKTOP_LOG_CLOSE_TIMEOUT_MS,
149
- 25,
150
- 5_000
151
- );
152
- const lowPriorityRecordsPerSecond = boundedInteger(
153
- options.lowPriorityRecordsPerSecond,
154
- DEFAULT_DESKTOP_LOG_LOW_PRIORITY_RECORDS_PER_SECOND,
155
- 1,
156
- 1_000
157
- );
158
- const lowPriorityBytesPerSecond = boundedInteger(
159
- options.lowPriorityBytesPerSecond,
160
- DEFAULT_DESKTOP_LOG_LOW_PRIORITY_BYTES_PER_SECOND,
161
- 1024,
162
- 4 * 1024 * 1024
163
- );
164
- const maxMessageBytes = Math.min(
165
- maxFileBytes - 256,
166
- boundedInteger(
167
- options.maxMessageBytes,
168
- DEFAULT_DESKTOP_LOG_MAX_MESSAGE_BYTES,
169
- 256,
170
- 256 * 1024
171
- )
172
- );
173
- const now = typeof options.now === 'function' ? options.now : Date.now;
174
- const streamFactory = typeof options.streamFactory === 'function'
175
- ? options.streamFactory
176
- : (targetPath, streamOptions) => createWriteStream(targetPath, streamOptions);
177
- const rotateFile = typeof options.rename === 'function' ? options.rename : rename;
178
- const removeFile = typeof options.rm === 'function' ? options.rm : rm;
179
-
180
- let stream = null;
181
- let streamDrainHandler = null;
182
- let currentBytes = 0;
183
- let blocked = false;
184
- let rotatingPromise = null;
185
- let rotationEpoch = 0;
186
- let closePromise = null;
187
- let closing = false;
188
- let closed = false;
189
- let lowWindowStartedAt = now();
190
- let lowWindowRecords = 0;
191
- let lowWindowBytes = 0;
192
- let activeCloseTimers = 0;
193
- let activeIdleWaitTimers = 0;
194
- const pending = [];
195
- const idleWaiters = new Set();
196
- const counters = {
197
- acceptedEntries: 0,
198
- writtenEntries: 0,
199
- writtenBytes: 0,
200
- rotationCount: 0,
201
- streamOpenCount: 0,
202
- backpressureSignalCount: 0,
203
- droppedBackpressureEntries: 0,
204
- droppedBackpressureBytes: 0,
205
- droppedRateLimitedEntries: 0,
206
- droppedRateLimitedBytes: 0,
207
- droppedPendingLimitEntries: 0,
208
- droppedPendingLimitBytes: 0,
209
- droppedClosingEntries: 0,
210
- droppedClosingBytes: 0,
211
- lowPriorityPreAdmissionAcceptedEntries: 0,
212
- lowPriorityMaterializedEntries: 0,
213
- streamErrorCount: 0,
214
- forcedCloseCount: 0,
215
- rotationDeadlineCount: 0,
216
- startupOversizeResetCount: 0,
217
- maximumPendingEntries: 0,
218
- maximumPendingBytes: 0,
219
- maximumWritableBytes: 0
220
- };
221
- let pendingBytes = 0;
222
-
223
- try {
224
- mkdirSync(dirname(logPath), { recursive: true });
225
- } catch {
226
- // The desktop shell must still start when its diagnostic directory is not writable.
227
- }
228
- for (let index = 0; index < fileCount; index += 1) {
229
- const candidatePath = index === 0 ? logPath : `${logPath}.${index}`;
230
- try {
231
- const candidateBytes = existsSync(candidatePath)
232
- ? Math.max(0, Number(statSync(candidatePath).size || 0))
233
- : 0;
234
- if (candidateBytes > maxFileBytes) {
235
- truncateSync(candidatePath, 0);
236
- counters.startupOversizeResetCount += 1;
237
- } else if (index === 0) {
238
- currentBytes = candidateBytes;
239
- }
240
- } catch {
241
- if (index === 0) currentBytes = 0;
242
- }
243
- }
244
-
245
- const rotationPath = index => index === 0 ? logPath : `${logPath}.${index}`;
246
-
247
- function snapshot() {
248
- const writableBytes = Math.max(0, Number(stream?.writableLength || 0));
249
- return {
250
- ...counters,
251
- logPath,
252
- maxFileBytes,
253
- fileCount,
254
- maxPendingEntries,
255
- maxPendingBytes,
256
- streamHighWaterBytes,
257
- closeTimeoutMs,
258
- lowPriorityRecordsPerSecond,
259
- lowPriorityBytesPerSecond,
260
- currentBytes,
261
- pendingEntries: pending.length,
262
- pendingBytes,
263
- writableBytes,
264
- blocked,
265
- rotating: Boolean(rotatingPromise),
266
- closing,
267
- closed,
268
- activeCloseTimers,
269
- activeIdleWaitTimers
270
- };
271
- }
272
-
273
- function notifyIdle() {
274
- if (rotatingPromise || blocked || pending.length > 0) return;
275
- for (const resolveIdle of idleWaiters) resolveIdle(snapshot());
276
- idleWaiters.clear();
277
- }
278
-
279
- function recordDrop(reason, lineBytes) {
280
- const bytes = Math.max(0, Number(lineBytes || 0));
281
- if (reason === 'rate') {
282
- counters.droppedRateLimitedEntries += 1;
283
- counters.droppedRateLimitedBytes += bytes;
284
- } else if (reason === 'pending') {
285
- counters.droppedPendingLimitEntries += 1;
286
- counters.droppedPendingLimitBytes += bytes;
287
- } else if (reason === 'closing') {
288
- counters.droppedClosingEntries += 1;
289
- counters.droppedClosingBytes += bytes;
290
- } else {
291
- counters.droppedBackpressureEntries += 1;
292
- counters.droppedBackpressureBytes += bytes;
293
- }
294
- }
295
-
296
- function enqueue(line, priority) {
297
- const lineBytes = byteLength(line);
298
- if (pending.length >= maxPendingEntries || pendingBytes + lineBytes > maxPendingBytes) {
299
- recordDrop('pending', lineBytes);
300
- return false;
301
- }
302
- pending.push({ line, priority, bytes: lineBytes });
303
- pendingBytes += lineBytes;
304
- counters.maximumPendingEntries = Math.max(counters.maximumPendingEntries, pending.length);
305
- counters.maximumPendingBytes = Math.max(counters.maximumPendingBytes, pendingBytes);
306
- return true;
307
- }
308
-
309
- function handleStreamError() {
310
- counters.streamErrorCount += 1;
311
- blocked = false;
312
- if (streamDrainHandler && stream) stream.off?.('drain', streamDrainHandler);
313
- streamDrainHandler = null;
314
- try { stream?.destroy(); } catch { /* already failed */ }
315
- stream = null;
316
- notifyIdle();
317
- }
318
-
319
- function ensureStream() {
320
- if (stream || closing || closed) return stream;
321
- try {
322
- mkdirSync(dirname(logPath), { recursive: true });
323
- stream = streamFactory(logPath, {
324
- flags: 'a',
325
- highWaterMark: streamHighWaterBytes
326
- });
327
- counters.streamOpenCount += 1;
328
- stream.on?.('error', handleStreamError);
329
- return stream;
330
- } catch {
331
- counters.streamErrorCount += 1;
332
- stream = null;
333
- return null;
334
- }
335
- }
336
-
337
- async function rotateCurrentFile() {
338
- if (rotatingPromise || closing || closed) return rotatingPromise;
339
- const ownerEpoch = ++rotationEpoch;
340
- const pending = (async () => {
341
- const ownedStream = stream;
342
- stream = null;
343
- blocked = false;
344
- if (streamDrainHandler && ownedStream) ownedStream.off?.('drain', streamDrainHandler);
345
- streamDrainHandler = null;
346
- await finishStreamWithin(ownedStream, closeTimeoutMs, () => {
347
- counters.forcedCloseCount += 1;
348
- }, () => {
349
- activeCloseTimers += 1;
350
- }, () => {
351
- activeCloseTimers -= 1;
352
- });
353
- if (ownerEpoch !== rotationEpoch || closing || closed) return;
354
- for (let index = fileCount - 1; index >= 1; index -= 1) {
355
- if (ownerEpoch !== rotationEpoch || closing || closed) return;
356
- const sourcePath = rotationPath(index - 1);
357
- const destinationPath = rotationPath(index);
358
- try { await removeFile(destinationPath, { force: true }); } catch { /* bounded best effort */ }
359
- if (ownerEpoch !== rotationEpoch || closing || closed) return;
360
- try {
361
- await rotateFile(sourcePath, destinationPath);
362
- } catch (error) {
363
- if (error?.code !== 'ENOENT') counters.streamErrorCount += 1;
364
- }
365
- if (ownerEpoch !== rotationEpoch || closing || closed) return;
366
- }
367
- if (ownerEpoch !== rotationEpoch || closing || closed) return;
368
- currentBytes = 0;
369
- counters.rotationCount += 1;
370
- })();
371
- const tracked = pending.finally(() => {
372
- if (rotatingPromise === tracked) rotatingPromise = null;
373
- if (ownerEpoch === rotationEpoch && !closing && !closed) pump();
374
- notifyIdle();
375
- });
376
- rotatingPromise = tracked;
377
- return tracked;
378
- }
379
-
380
- function handleDrain() {
381
- blocked = false;
382
- streamDrainHandler = null;
383
- pump();
384
- notifyIdle();
385
- }
386
-
387
- function writeNow(line, priority) {
388
- const lineBytes = byteLength(line);
389
- if (currentBytes > 0 && currentBytes + lineBytes > maxFileBytes) {
390
- enqueue(line, priority);
391
- void rotateCurrentFile();
392
- return true;
393
- }
394
- const target = ensureStream();
395
- if (!target) {
396
- recordDrop('backpressure', lineBytes);
397
- return false;
398
- }
399
- let accepted;
400
- try {
401
- accepted = target.write(line);
402
- } catch {
403
- handleStreamError();
404
- recordDrop('backpressure', lineBytes);
405
- return false;
406
- }
407
- currentBytes += lineBytes;
408
- counters.writtenEntries += 1;
409
- counters.writtenBytes += lineBytes;
410
- counters.maximumWritableBytes = Math.max(
411
- counters.maximumWritableBytes,
412
- Math.max(0, Number(target.writableLength || 0))
413
- );
414
- if (!accepted) {
415
- counters.backpressureSignalCount += 1;
416
- blocked = true;
417
- streamDrainHandler = handleDrain;
418
- target.once?.('drain', streamDrainHandler);
419
- }
420
- if (currentBytes >= maxFileBytes && !rotatingPromise) void rotateCurrentFile();
421
- return true;
422
- }
423
-
424
- function pump() {
425
- if (closing || closed || rotatingPromise || blocked) return;
426
- while (pending.length > 0 && !blocked && !rotatingPromise && !closing && !closed) {
427
- const entry = pending.shift();
428
- pendingBytes -= entry.bytes;
429
- if (pendingBytes < 0) pendingBytes = 0;
430
- writeNow(entry.line, entry.priority);
431
- }
432
- notifyIdle();
433
- }
434
-
435
- function withinLowPriorityBudget(lineBytes) {
436
- const timestamp = now();
437
- if (timestamp - lowWindowStartedAt >= 1_000 || timestamp < lowWindowStartedAt) {
438
- lowWindowStartedAt = timestamp;
439
- lowWindowRecords = 0;
440
- lowWindowBytes = 0;
441
- }
442
- if (lowWindowRecords >= lowPriorityRecordsPerSecond
443
- || lowWindowBytes + lineBytes > lowPriorityBytesPerSecond) {
444
- return false;
445
- }
446
- lowWindowRecords += 1;
447
- lowWindowBytes += lineBytes;
448
- return true;
449
- }
450
-
451
- function write(message, metadata = {}) {
452
- const priority = metadata.priority === 'low' ? 'low' : 'normal';
453
- const preAdmitted = metadata.lowPriorityAdmissionToken === lowPriorityAdmissionToken;
454
- const record = {
455
- timestamp: new Date(now()).toISOString(),
456
- level: String(metadata.level || 'info').slice(0, 16),
457
- source: String(metadata.source || 'desktop').slice(0, 64),
458
- message: truncateUtf8(message, maxMessageBytes)
459
- };
460
- const line = encodeRecordWithin(record, Math.min(maxFileBytes, streamHighWaterBytes));
461
- const lineBytes = byteLength(line);
462
- if (closing || closed) {
463
- recordDrop('closing', lineBytes);
464
- return false;
465
- }
466
- if (priority === 'low' && !preAdmitted && !withinLowPriorityBudget(lineBytes)) {
467
- recordDrop('rate', lineBytes);
468
- return false;
469
- }
470
- counters.acceptedEntries += 1;
471
- if (rotatingPromise || blocked) {
472
- if (priority === 'low') {
473
- recordDrop('backpressure', lineBytes);
474
- return false;
475
- }
476
- return enqueue(line, priority);
477
- }
478
- return writeNow(line, priority);
479
- }
480
-
481
- const lowPriorityAdmissionToken = Object.freeze({});
482
-
483
- function writeLowPriority(message, metadata = {}, materialize = value => value) {
484
- const messageBytes = rawByteLength(message);
485
- if (closing || closed) {
486
- recordDrop('closing', messageBytes);
487
- return false;
488
- }
489
- if (rotatingPromise || blocked) {
490
- recordDrop('backpressure', messageBytes);
491
- return false;
492
- }
493
- if (!withinLowPriorityBudget(messageBytes)) {
494
- recordDrop('rate', messageBytes);
495
- return false;
496
- }
497
- counters.lowPriorityPreAdmissionAcceptedEntries += 1;
498
- let materialized;
499
- try {
500
- materialized = materialize(message);
501
- } catch {
502
- counters.streamErrorCount += 1;
503
- return false;
504
- }
505
- counters.lowPriorityMaterializedEntries += 1;
506
- return write(materialized, {
507
- ...metadata,
508
- priority: 'low',
509
- lowPriorityAdmissionToken
510
- });
511
- }
512
-
513
- function settleWithin(promise, timeoutMs) {
514
- if (!promise) return Promise.resolve(true);
515
- return new Promise(resolve => {
516
- let settled = false;
517
- const finish = value => {
518
- if (settled) return;
519
- settled = true;
520
- clearTimeout(timer);
521
- activeCloseTimers -= 1;
522
- resolve(value);
523
- };
524
- activeCloseTimers += 1;
525
- const timer = setTimeout(() => finish(false), Math.max(1, timeoutMs));
526
- promise.then(
527
- () => finish(true),
528
- () => finish(true)
529
- );
530
- });
531
- }
532
-
533
- function whenIdle(timeoutMs = closeTimeoutMs) {
534
- if (!rotatingPromise && !blocked && pending.length === 0) {
535
- return Promise.resolve(snapshot());
536
- }
537
- return new Promise(resolveIdle => {
538
- let settled = false;
539
- const finish = value => {
540
- if (settled) return;
541
- settled = true;
542
- clearTimeout(timer);
543
- activeIdleWaitTimers -= 1;
544
- idleWaiters.delete(onIdle);
545
- resolveIdle(value);
546
- };
547
- const onIdle = value => finish(value);
548
- idleWaiters.add(onIdle);
549
- activeIdleWaitTimers += 1;
550
- const timer = setTimeout(() => finish(snapshot()), Math.max(1, timeoutMs));
551
- });
552
- }
553
-
554
- async function close(request = {}) {
555
- if (closePromise) return closePromise;
556
- const requestedTimeoutMs = boundedInteger(
557
- typeof request === 'number' ? request : request.timeoutMs,
558
- closeTimeoutMs,
559
- 25,
560
- 5_000
561
- );
562
- closePromise = (async () => {
563
- const deadline = now() + requestedTimeoutMs;
564
- closing = true;
565
- while (pending.length > 0) {
566
- const entry = pending.shift();
567
- pendingBytes -= entry.bytes;
568
- recordDrop('closing', entry.bytes);
569
- }
570
- pendingBytes = 0;
571
- if (rotatingPromise) {
572
- const rotationFinished = await settleWithin(
573
- rotatingPromise,
574
- Math.max(1, deadline - now())
575
- );
576
- if (!rotationFinished) {
577
- counters.rotationDeadlineCount += 1;
578
- rotationEpoch += 1;
579
- rotatingPromise = null;
580
- }
581
- }
582
- const ownedStream = stream;
583
- stream = null;
584
- blocked = false;
585
- if (streamDrainHandler && ownedStream) ownedStream.off?.('drain', streamDrainHandler);
586
- streamDrainHandler = null;
587
- await finishStreamWithin(
588
- ownedStream,
589
- Math.max(1, deadline - now()),
590
- () => { counters.forcedCloseCount += 1; },
591
- () => { activeCloseTimers += 1; },
592
- () => { activeCloseTimers -= 1; }
593
- );
594
- closed = true;
595
- closing = false;
596
- notifyIdle();
597
- return snapshot();
598
- })();
599
- return closePromise;
600
- }
601
-
602
- return Object.freeze({
603
- write,
604
- writeLowPriority,
605
- whenIdle,
606
- close,
607
- getStats: snapshot
608
- });
609
- }
1
+ import {
2
+ createWriteStream,
3
+ existsSync,
4
+ mkdirSync,
5
+ statSync,
6
+ truncateSync
7
+ } from 'node:fs';
8
+ import {
9
+ rename,
10
+ rm
11
+ } from 'node:fs/promises';
12
+ import { dirname } from 'node:path';
13
+
14
+ export const DEFAULT_DESKTOP_LOG_MAX_FILE_BYTES = 512 * 1024;
15
+ export const DEFAULT_DESKTOP_LOG_FILE_COUNT = 3;
16
+ export const DEFAULT_DESKTOP_LOG_MAX_PENDING_ENTRIES = 24;
17
+ export const DEFAULT_DESKTOP_LOG_MAX_PENDING_BYTES = 64 * 1024;
18
+ export const DEFAULT_DESKTOP_LOG_STREAM_HIGH_WATER_BYTES = 32 * 1024;
19
+ export const DEFAULT_DESKTOP_LOG_CLOSE_TIMEOUT_MS = 1_000;
20
+ export const DEFAULT_DESKTOP_LOG_LOW_PRIORITY_RECORDS_PER_SECOND = 40;
21
+ export const DEFAULT_DESKTOP_LOG_LOW_PRIORITY_BYTES_PER_SECOND = 128 * 1024;
22
+ export const DEFAULT_DESKTOP_LOG_MAX_MESSAGE_BYTES = 16 * 1024;
23
+
24
+ function boundedInteger(value, fallback, minimum, maximum) {
25
+ const numeric = Number(value);
26
+ return Number.isFinite(numeric)
27
+ ? Math.max(minimum, Math.min(maximum, Math.trunc(numeric)))
28
+ : fallback;
29
+ }
30
+
31
+ function byteLength(value) {
32
+ return Buffer.byteLength(String(value || ''), 'utf8');
33
+ }
34
+
35
+ function rawByteLength(value) {
36
+ if (Buffer.isBuffer(value)) return value.byteLength;
37
+ if (typeof ArrayBuffer !== 'undefined') {
38
+ if (value instanceof ArrayBuffer) return value.byteLength;
39
+ if (ArrayBuffer.isView(value)) return value.byteLength;
40
+ }
41
+ return byteLength(value);
42
+ }
43
+
44
+ function truncateUtf8(value, maximumBytes) {
45
+ const source = String(value ?? '');
46
+ if (byteLength(source) <= maximumBytes) return source;
47
+ const suffix = '…[truncated]';
48
+ const suffixBytes = byteLength(suffix);
49
+ const sourceBuffer = Buffer.from(source, 'utf8');
50
+ const retainedBytes = Math.max(0, maximumBytes - suffixBytes);
51
+ return `${sourceBuffer.subarray(0, retainedBytes).toString('utf8').replace(/\uFFFD+$/g, '')}${suffix}`;
52
+ }
53
+
54
+ function encodeRecordWithin(record, maximumBytes) {
55
+ let encoded = `${JSON.stringify(record)}\n`;
56
+ if (byteLength(encoded) <= maximumBytes) return encoded;
57
+ const sourceBytes = byteLength(record.message);
58
+ let low = 0;
59
+ let high = sourceBytes;
60
+ let best = `${JSON.stringify({ ...record, message: '' })}\n`;
61
+ while (low <= high) {
62
+ const middle = Math.trunc((low + high) / 2);
63
+ const candidate = `${JSON.stringify({
64
+ ...record,
65
+ message: truncateUtf8(record.message, middle)
66
+ })}\n`;
67
+ if (byteLength(candidate) <= maximumBytes) {
68
+ best = candidate;
69
+ low = middle + 1;
70
+ } else {
71
+ high = middle - 1;
72
+ }
73
+ }
74
+ return best;
75
+ }
76
+
77
+ function finishStreamWithin(
78
+ stream,
79
+ timeoutMs,
80
+ onForcedClose,
81
+ onTimerStarted = () => {},
82
+ onTimerStopped = () => {}
83
+ ) {
84
+ if (!stream || stream.destroyed || stream.closed) return Promise.resolve();
85
+ return new Promise(resolveFinish => {
86
+ let settled = false;
87
+ const finish = () => {
88
+ if (settled) return;
89
+ settled = true;
90
+ clearTimeout(timer);
91
+ onTimerStopped();
92
+ stream.off?.('error', finish);
93
+ stream.off?.('close', finish);
94
+ resolveFinish();
95
+ };
96
+ onTimerStarted();
97
+ const timer = setTimeout(() => {
98
+ onForcedClose();
99
+ try { stream.destroy(); } catch { /* exact stream already closed */ }
100
+ finish();
101
+ }, Math.max(1, timeoutMs));
102
+ stream.once?.('error', finish);
103
+ stream.once?.('close', finish);
104
+ try {
105
+ stream.end(finish);
106
+ } catch {
107
+ finish();
108
+ }
109
+ });
110
+ }
111
+
112
+ export function createBoundedDesktopLogger(options = {}) {
113
+ const logPath = String(options.logPath || '').trim();
114
+ if (!logPath) throw new Error('desktop-log-path-required');
115
+
116
+ const maxFileBytes = boundedInteger(
117
+ options.maxFileBytes,
118
+ DEFAULT_DESKTOP_LOG_MAX_FILE_BYTES,
119
+ 1024,
120
+ 16 * 1024 * 1024
121
+ );
122
+ const fileCount = boundedInteger(
123
+ options.fileCount,
124
+ DEFAULT_DESKTOP_LOG_FILE_COUNT,
125
+ 2,
126
+ 8
127
+ );
128
+ const maxPendingEntries = boundedInteger(
129
+ options.maxPendingEntries,
130
+ DEFAULT_DESKTOP_LOG_MAX_PENDING_ENTRIES,
131
+ 1,
132
+ 128
133
+ );
134
+ const maxPendingBytes = boundedInteger(
135
+ options.maxPendingBytes,
136
+ DEFAULT_DESKTOP_LOG_MAX_PENDING_BYTES,
137
+ 1024,
138
+ 512 * 1024
139
+ );
140
+ const streamHighWaterBytes = boundedInteger(
141
+ options.streamHighWaterBytes,
142
+ DEFAULT_DESKTOP_LOG_STREAM_HIGH_WATER_BYTES,
143
+ 1024,
144
+ 256 * 1024
145
+ );
146
+ const closeTimeoutMs = boundedInteger(
147
+ options.closeTimeoutMs,
148
+ DEFAULT_DESKTOP_LOG_CLOSE_TIMEOUT_MS,
149
+ 25,
150
+ 5_000
151
+ );
152
+ const lowPriorityRecordsPerSecond = boundedInteger(
153
+ options.lowPriorityRecordsPerSecond,
154
+ DEFAULT_DESKTOP_LOG_LOW_PRIORITY_RECORDS_PER_SECOND,
155
+ 1,
156
+ 1_000
157
+ );
158
+ const lowPriorityBytesPerSecond = boundedInteger(
159
+ options.lowPriorityBytesPerSecond,
160
+ DEFAULT_DESKTOP_LOG_LOW_PRIORITY_BYTES_PER_SECOND,
161
+ 1024,
162
+ 4 * 1024 * 1024
163
+ );
164
+ const maxMessageBytes = Math.min(
165
+ maxFileBytes - 256,
166
+ boundedInteger(
167
+ options.maxMessageBytes,
168
+ DEFAULT_DESKTOP_LOG_MAX_MESSAGE_BYTES,
169
+ 256,
170
+ 256 * 1024
171
+ )
172
+ );
173
+ const now = typeof options.now === 'function' ? options.now : Date.now;
174
+ const streamFactory = typeof options.streamFactory === 'function'
175
+ ? options.streamFactory
176
+ : (targetPath, streamOptions) => createWriteStream(targetPath, streamOptions);
177
+ const rotateFile = typeof options.rename === 'function' ? options.rename : rename;
178
+ const removeFile = typeof options.rm === 'function' ? options.rm : rm;
179
+
180
+ let stream = null;
181
+ let streamDrainHandler = null;
182
+ let currentBytes = 0;
183
+ let blocked = false;
184
+ let rotatingPromise = null;
185
+ let rotationEpoch = 0;
186
+ let closePromise = null;
187
+ let closing = false;
188
+ let closed = false;
189
+ let lowWindowStartedAt = now();
190
+ let lowWindowRecords = 0;
191
+ let lowWindowBytes = 0;
192
+ let activeCloseTimers = 0;
193
+ let activeIdleWaitTimers = 0;
194
+ const pending = [];
195
+ const idleWaiters = new Set();
196
+ const counters = {
197
+ acceptedEntries: 0,
198
+ writtenEntries: 0,
199
+ writtenBytes: 0,
200
+ rotationCount: 0,
201
+ streamOpenCount: 0,
202
+ backpressureSignalCount: 0,
203
+ droppedBackpressureEntries: 0,
204
+ droppedBackpressureBytes: 0,
205
+ droppedRateLimitedEntries: 0,
206
+ droppedRateLimitedBytes: 0,
207
+ droppedPendingLimitEntries: 0,
208
+ droppedPendingLimitBytes: 0,
209
+ droppedClosingEntries: 0,
210
+ droppedClosingBytes: 0,
211
+ lowPriorityPreAdmissionAcceptedEntries: 0,
212
+ lowPriorityMaterializedEntries: 0,
213
+ streamErrorCount: 0,
214
+ forcedCloseCount: 0,
215
+ rotationDeadlineCount: 0,
216
+ startupOversizeResetCount: 0,
217
+ maximumPendingEntries: 0,
218
+ maximumPendingBytes: 0,
219
+ maximumWritableBytes: 0
220
+ };
221
+ let pendingBytes = 0;
222
+
223
+ try {
224
+ mkdirSync(dirname(logPath), { recursive: true });
225
+ } catch {
226
+ // The desktop shell must still start when its diagnostic directory is not writable.
227
+ }
228
+ for (let index = 0; index < fileCount; index += 1) {
229
+ const candidatePath = index === 0 ? logPath : `${logPath}.${index}`;
230
+ try {
231
+ const candidateBytes = existsSync(candidatePath)
232
+ ? Math.max(0, Number(statSync(candidatePath).size || 0))
233
+ : 0;
234
+ if (candidateBytes > maxFileBytes) {
235
+ truncateSync(candidatePath, 0);
236
+ counters.startupOversizeResetCount += 1;
237
+ } else if (index === 0) {
238
+ currentBytes = candidateBytes;
239
+ }
240
+ } catch {
241
+ if (index === 0) currentBytes = 0;
242
+ }
243
+ }
244
+
245
+ const rotationPath = index => index === 0 ? logPath : `${logPath}.${index}`;
246
+
247
+ function snapshot() {
248
+ const writableBytes = Math.max(0, Number(stream?.writableLength || 0));
249
+ return {
250
+ ...counters,
251
+ logPath,
252
+ maxFileBytes,
253
+ fileCount,
254
+ maxPendingEntries,
255
+ maxPendingBytes,
256
+ streamHighWaterBytes,
257
+ closeTimeoutMs,
258
+ lowPriorityRecordsPerSecond,
259
+ lowPriorityBytesPerSecond,
260
+ currentBytes,
261
+ pendingEntries: pending.length,
262
+ pendingBytes,
263
+ writableBytes,
264
+ blocked,
265
+ rotating: Boolean(rotatingPromise),
266
+ closing,
267
+ closed,
268
+ activeCloseTimers,
269
+ activeIdleWaitTimers
270
+ };
271
+ }
272
+
273
+ function notifyIdle() {
274
+ if (rotatingPromise || blocked || pending.length > 0) return;
275
+ for (const resolveIdle of idleWaiters) resolveIdle(snapshot());
276
+ idleWaiters.clear();
277
+ }
278
+
279
+ function recordDrop(reason, lineBytes) {
280
+ const bytes = Math.max(0, Number(lineBytes || 0));
281
+ if (reason === 'rate') {
282
+ counters.droppedRateLimitedEntries += 1;
283
+ counters.droppedRateLimitedBytes += bytes;
284
+ } else if (reason === 'pending') {
285
+ counters.droppedPendingLimitEntries += 1;
286
+ counters.droppedPendingLimitBytes += bytes;
287
+ } else if (reason === 'closing') {
288
+ counters.droppedClosingEntries += 1;
289
+ counters.droppedClosingBytes += bytes;
290
+ } else {
291
+ counters.droppedBackpressureEntries += 1;
292
+ counters.droppedBackpressureBytes += bytes;
293
+ }
294
+ }
295
+
296
+ function enqueue(line, priority) {
297
+ const lineBytes = byteLength(line);
298
+ if (pending.length >= maxPendingEntries || pendingBytes + lineBytes > maxPendingBytes) {
299
+ recordDrop('pending', lineBytes);
300
+ return false;
301
+ }
302
+ pending.push({ line, priority, bytes: lineBytes });
303
+ pendingBytes += lineBytes;
304
+ counters.maximumPendingEntries = Math.max(counters.maximumPendingEntries, pending.length);
305
+ counters.maximumPendingBytes = Math.max(counters.maximumPendingBytes, pendingBytes);
306
+ return true;
307
+ }
308
+
309
+ function handleStreamError() {
310
+ counters.streamErrorCount += 1;
311
+ blocked = false;
312
+ if (streamDrainHandler && stream) stream.off?.('drain', streamDrainHandler);
313
+ streamDrainHandler = null;
314
+ try { stream?.destroy(); } catch { /* already failed */ }
315
+ stream = null;
316
+ notifyIdle();
317
+ }
318
+
319
+ function ensureStream() {
320
+ if (stream || closing || closed) return stream;
321
+ try {
322
+ mkdirSync(dirname(logPath), { recursive: true });
323
+ stream = streamFactory(logPath, {
324
+ flags: 'a',
325
+ highWaterMark: streamHighWaterBytes
326
+ });
327
+ counters.streamOpenCount += 1;
328
+ stream.on?.('error', handleStreamError);
329
+ return stream;
330
+ } catch {
331
+ counters.streamErrorCount += 1;
332
+ stream = null;
333
+ return null;
334
+ }
335
+ }
336
+
337
+ async function rotateCurrentFile() {
338
+ if (rotatingPromise || closing || closed) return rotatingPromise;
339
+ const ownerEpoch = ++rotationEpoch;
340
+ const pending = (async () => {
341
+ const ownedStream = stream;
342
+ stream = null;
343
+ blocked = false;
344
+ if (streamDrainHandler && ownedStream) ownedStream.off?.('drain', streamDrainHandler);
345
+ streamDrainHandler = null;
346
+ await finishStreamWithin(ownedStream, closeTimeoutMs, () => {
347
+ counters.forcedCloseCount += 1;
348
+ }, () => {
349
+ activeCloseTimers += 1;
350
+ }, () => {
351
+ activeCloseTimers -= 1;
352
+ });
353
+ if (ownerEpoch !== rotationEpoch || closing || closed) return;
354
+ for (let index = fileCount - 1; index >= 1; index -= 1) {
355
+ if (ownerEpoch !== rotationEpoch || closing || closed) return;
356
+ const sourcePath = rotationPath(index - 1);
357
+ const destinationPath = rotationPath(index);
358
+ try { await removeFile(destinationPath, { force: true }); } catch { /* bounded best effort */ }
359
+ if (ownerEpoch !== rotationEpoch || closing || closed) return;
360
+ try {
361
+ await rotateFile(sourcePath, destinationPath);
362
+ } catch (error) {
363
+ if (error?.code !== 'ENOENT') counters.streamErrorCount += 1;
364
+ }
365
+ if (ownerEpoch !== rotationEpoch || closing || closed) return;
366
+ }
367
+ if (ownerEpoch !== rotationEpoch || closing || closed) return;
368
+ currentBytes = 0;
369
+ counters.rotationCount += 1;
370
+ })();
371
+ const tracked = pending.finally(() => {
372
+ if (rotatingPromise === tracked) rotatingPromise = null;
373
+ if (ownerEpoch === rotationEpoch && !closing && !closed) pump();
374
+ notifyIdle();
375
+ });
376
+ rotatingPromise = tracked;
377
+ return tracked;
378
+ }
379
+
380
+ function handleDrain() {
381
+ blocked = false;
382
+ streamDrainHandler = null;
383
+ pump();
384
+ notifyIdle();
385
+ }
386
+
387
+ function writeNow(line, priority) {
388
+ const lineBytes = byteLength(line);
389
+ if (currentBytes > 0 && currentBytes + lineBytes > maxFileBytes) {
390
+ enqueue(line, priority);
391
+ void rotateCurrentFile();
392
+ return true;
393
+ }
394
+ const target = ensureStream();
395
+ if (!target) {
396
+ recordDrop('backpressure', lineBytes);
397
+ return false;
398
+ }
399
+ let accepted;
400
+ try {
401
+ accepted = target.write(line);
402
+ } catch {
403
+ handleStreamError();
404
+ recordDrop('backpressure', lineBytes);
405
+ return false;
406
+ }
407
+ currentBytes += lineBytes;
408
+ counters.writtenEntries += 1;
409
+ counters.writtenBytes += lineBytes;
410
+ counters.maximumWritableBytes = Math.max(
411
+ counters.maximumWritableBytes,
412
+ Math.max(0, Number(target.writableLength || 0))
413
+ );
414
+ if (!accepted) {
415
+ counters.backpressureSignalCount += 1;
416
+ blocked = true;
417
+ streamDrainHandler = handleDrain;
418
+ target.once?.('drain', streamDrainHandler);
419
+ }
420
+ if (currentBytes >= maxFileBytes && !rotatingPromise) void rotateCurrentFile();
421
+ return true;
422
+ }
423
+
424
+ function pump() {
425
+ if (closing || closed || rotatingPromise || blocked) return;
426
+ while (pending.length > 0 && !blocked && !rotatingPromise && !closing && !closed) {
427
+ const entry = pending.shift();
428
+ pendingBytes -= entry.bytes;
429
+ if (pendingBytes < 0) pendingBytes = 0;
430
+ writeNow(entry.line, entry.priority);
431
+ }
432
+ notifyIdle();
433
+ }
434
+
435
+ function withinLowPriorityBudget(lineBytes) {
436
+ const timestamp = now();
437
+ if (timestamp - lowWindowStartedAt >= 1_000 || timestamp < lowWindowStartedAt) {
438
+ lowWindowStartedAt = timestamp;
439
+ lowWindowRecords = 0;
440
+ lowWindowBytes = 0;
441
+ }
442
+ if (lowWindowRecords >= lowPriorityRecordsPerSecond
443
+ || lowWindowBytes + lineBytes > lowPriorityBytesPerSecond) {
444
+ return false;
445
+ }
446
+ lowWindowRecords += 1;
447
+ lowWindowBytes += lineBytes;
448
+ return true;
449
+ }
450
+
451
+ function write(message, metadata = {}) {
452
+ const priority = metadata.priority === 'low' ? 'low' : 'normal';
453
+ const preAdmitted = metadata.lowPriorityAdmissionToken === lowPriorityAdmissionToken;
454
+ const record = {
455
+ timestamp: new Date(now()).toISOString(),
456
+ level: String(metadata.level || 'info').slice(0, 16),
457
+ source: String(metadata.source || 'desktop').slice(0, 64),
458
+ message: truncateUtf8(message, maxMessageBytes)
459
+ };
460
+ const line = encodeRecordWithin(record, Math.min(maxFileBytes, streamHighWaterBytes));
461
+ const lineBytes = byteLength(line);
462
+ if (closing || closed) {
463
+ recordDrop('closing', lineBytes);
464
+ return false;
465
+ }
466
+ if (priority === 'low' && !preAdmitted && !withinLowPriorityBudget(lineBytes)) {
467
+ recordDrop('rate', lineBytes);
468
+ return false;
469
+ }
470
+ counters.acceptedEntries += 1;
471
+ if (rotatingPromise || blocked) {
472
+ if (priority === 'low') {
473
+ recordDrop('backpressure', lineBytes);
474
+ return false;
475
+ }
476
+ return enqueue(line, priority);
477
+ }
478
+ return writeNow(line, priority);
479
+ }
480
+
481
+ const lowPriorityAdmissionToken = Object.freeze({});
482
+
483
+ function writeLowPriority(message, metadata = {}, materialize = value => value) {
484
+ const messageBytes = rawByteLength(message);
485
+ if (closing || closed) {
486
+ recordDrop('closing', messageBytes);
487
+ return false;
488
+ }
489
+ if (rotatingPromise || blocked) {
490
+ recordDrop('backpressure', messageBytes);
491
+ return false;
492
+ }
493
+ if (!withinLowPriorityBudget(messageBytes)) {
494
+ recordDrop('rate', messageBytes);
495
+ return false;
496
+ }
497
+ counters.lowPriorityPreAdmissionAcceptedEntries += 1;
498
+ let materialized;
499
+ try {
500
+ materialized = materialize(message);
501
+ } catch {
502
+ counters.streamErrorCount += 1;
503
+ return false;
504
+ }
505
+ counters.lowPriorityMaterializedEntries += 1;
506
+ return write(materialized, {
507
+ ...metadata,
508
+ priority: 'low',
509
+ lowPriorityAdmissionToken
510
+ });
511
+ }
512
+
513
+ function settleWithin(promise, timeoutMs) {
514
+ if (!promise) return Promise.resolve(true);
515
+ return new Promise(resolve => {
516
+ let settled = false;
517
+ const finish = value => {
518
+ if (settled) return;
519
+ settled = true;
520
+ clearTimeout(timer);
521
+ activeCloseTimers -= 1;
522
+ resolve(value);
523
+ };
524
+ activeCloseTimers += 1;
525
+ const timer = setTimeout(() => finish(false), Math.max(1, timeoutMs));
526
+ promise.then(
527
+ () => finish(true),
528
+ () => finish(true)
529
+ );
530
+ });
531
+ }
532
+
533
+ function whenIdle(timeoutMs = closeTimeoutMs) {
534
+ if (!rotatingPromise && !blocked && pending.length === 0) {
535
+ return Promise.resolve(snapshot());
536
+ }
537
+ return new Promise(resolveIdle => {
538
+ let settled = false;
539
+ const finish = value => {
540
+ if (settled) return;
541
+ settled = true;
542
+ clearTimeout(timer);
543
+ activeIdleWaitTimers -= 1;
544
+ idleWaiters.delete(onIdle);
545
+ resolveIdle(value);
546
+ };
547
+ const onIdle = value => finish(value);
548
+ idleWaiters.add(onIdle);
549
+ activeIdleWaitTimers += 1;
550
+ const timer = setTimeout(() => finish(snapshot()), Math.max(1, timeoutMs));
551
+ });
552
+ }
553
+
554
+ async function close(request = {}) {
555
+ if (closePromise) return closePromise;
556
+ const requestedTimeoutMs = boundedInteger(
557
+ typeof request === 'number' ? request : request.timeoutMs,
558
+ closeTimeoutMs,
559
+ 25,
560
+ 5_000
561
+ );
562
+ closePromise = (async () => {
563
+ const deadline = now() + requestedTimeoutMs;
564
+ closing = true;
565
+ while (pending.length > 0) {
566
+ const entry = pending.shift();
567
+ pendingBytes -= entry.bytes;
568
+ recordDrop('closing', entry.bytes);
569
+ }
570
+ pendingBytes = 0;
571
+ if (rotatingPromise) {
572
+ const rotationFinished = await settleWithin(
573
+ rotatingPromise,
574
+ Math.max(1, deadline - now())
575
+ );
576
+ if (!rotationFinished) {
577
+ counters.rotationDeadlineCount += 1;
578
+ rotationEpoch += 1;
579
+ rotatingPromise = null;
580
+ }
581
+ }
582
+ const ownedStream = stream;
583
+ stream = null;
584
+ blocked = false;
585
+ if (streamDrainHandler && ownedStream) ownedStream.off?.('drain', streamDrainHandler);
586
+ streamDrainHandler = null;
587
+ await finishStreamWithin(
588
+ ownedStream,
589
+ Math.max(1, deadline - now()),
590
+ () => { counters.forcedCloseCount += 1; },
591
+ () => { activeCloseTimers += 1; },
592
+ () => { activeCloseTimers -= 1; }
593
+ );
594
+ closed = true;
595
+ closing = false;
596
+ notifyIdle();
597
+ return snapshot();
598
+ })();
599
+ return closePromise;
600
+ }
601
+
602
+ return Object.freeze({
603
+ write,
604
+ writeLowPriority,
605
+ whenIdle,
606
+ close,
607
+ getStats: snapshot
608
+ });
609
+ }