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.
- package/bin/livedesk.js +8 -8
- package/bootstrap/role-bootstrap-server.js +1 -1
- package/client/bin/livedesk-client-fast.js +1 -1
- package/client/bin/livedesk-client-node.js +1 -1
- package/client/bin/livedesk-client.js +30 -30
- package/client/package.json +6 -6
- package/client/src/runtime/client-runtime-server.js +4 -4
- package/diagnostics/livedesk-mac-physical-isolation.mjs +354 -354
- package/diagnostics/livedesk-transport-physical-core.mjs +702 -702
- package/electron/auth-config.mjs +10 -10
- package/electron/bounded-desktop-log.mjs +609 -609
- package/electron/electron-builder.win-dev.yml +1 -1
- package/electron/icon.svg +11 -11
- package/electron/main.mjs +5 -5
- package/electron/runtime-process-owner.mjs +287 -287
- package/hub/package.json +2 -2
- package/hub/src/server.js +2 -2
- package/package.json +6 -6
- package/runtime-core/package.json +1 -1
- package/runtime-core/src/auth-config.js +1 -1
- package/web/dist/app.webmanifest +1 -1
- package/web/dist/assets/LiveDeskApp-CvQUgQ08.js +178 -0
- package/web/dist/assets/{icons-DwJmccVC.js → icons-tFaWqIIe.js} +1 -1
- package/web/dist/assets/{index-DjIbOQxl.js → index-BDVKTkV3.js} +3 -3
- package/web/dist/assets/{index-DRI7OJI6.css → index-CpHNEjHy.css} +1 -1
- package/web/dist/assets/{react-DOZUPHF8.js → react-BQos8uGS.js} +1 -1
- package/web/dist/favicon.svg +11 -11
- package/web/dist/index.html +6 -6
- package/web/dist/livedesk-build-evidence.json +25 -25
- package/web/dist/sw.js +1 -1
- 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
|
+
}
|