pi-model-profiles 0.3.0 → 0.3.2
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/CHANGELOG.md +53 -41
- package/package.json +66 -66
- package/src/constants.ts +1 -1
- package/src/debug-logger.ts +383 -351
- package/src/index.ts +1 -1
- package/src/pi-api-utils.ts +1 -1
- package/src/profile-modal.ts +1062 -1062
- package/src/types-shims.d.ts +2 -2
package/src/debug-logger.ts
CHANGED
|
@@ -1,351 +1,383 @@
|
|
|
1
|
-
import { appendFile, chmod } from "node:fs/promises";
|
|
2
|
-
import {
|
|
3
|
-
CONFIG_PATH,
|
|
4
|
-
DEBUG_DIR,
|
|
5
|
-
DEBUG_LOG_PATH,
|
|
6
|
-
EXTENSION_NAME,
|
|
7
|
-
ensureMultiProfilesDebugDirectory,
|
|
8
|
-
loadMultiProfilesConfig,
|
|
9
|
-
} from "./config.js";
|
|
10
|
-
|
|
11
|
-
const DEFAULT_FLUSH_INTERVAL_MS = 5_000;
|
|
12
|
-
const DEFAULT_FLUSH_ENTRY_LIMIT = 100;
|
|
13
|
-
const DEFAULT_FLUSH_BYTE_LIMIT = 50 * 1024;
|
|
14
|
-
const DEFAULT_MAX_BUFFERED_ENTRIES = 1_000;
|
|
15
|
-
const DEFAULT_MAX_BUFFERED_BYTES = 512 * 1024;
|
|
16
|
-
|
|
17
|
-
export interface MultiProfilesDebugLoggerOptions {
|
|
18
|
-
configPath?: string;
|
|
19
|
-
debugDir?: string;
|
|
20
|
-
logPath?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function safeJsonStringify(value: unknown): string {
|
|
24
|
-
const seen = new WeakSet<object>();
|
|
25
|
-
return JSON.stringify(value, (_key, currentValue) => {
|
|
26
|
-
if (currentValue instanceof Error) {
|
|
27
|
-
return {
|
|
28
|
-
name: currentValue.name,
|
|
29
|
-
message: currentValue.message,
|
|
30
|
-
stack: currentValue.stack,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (typeof currentValue === "bigint") {
|
|
35
|
-
return currentValue.toString();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (typeof currentValue === "object" && currentValue !== null) {
|
|
39
|
-
if (seen.has(currentValue)) {
|
|
40
|
-
return "[Circular]";
|
|
41
|
-
}
|
|
42
|
-
seen.add(currentValue);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return currentValue;
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function normalizePositiveInteger(value: number | undefined, fallback: number): number {
|
|
50
|
-
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
51
|
-
return Math.floor(value);
|
|
52
|
-
}
|
|
53
|
-
return fallback;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface AsyncBufferedLogWriterOptions {
|
|
57
|
-
enabled: boolean;
|
|
58
|
-
logPath: string;
|
|
59
|
-
ensureDirectory: () => string | undefined;
|
|
60
|
-
flushIntervalMs?: number;
|
|
61
|
-
flushEntryLimit?: number;
|
|
62
|
-
flushByteLimit?: number;
|
|
63
|
-
maxBufferedEntries?: number;
|
|
64
|
-
maxBufferedBytes?: number;
|
|
65
|
-
createDroppedEntriesLine?: (droppedEntries: number) => string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export class AsyncBufferedLogWriter {
|
|
69
|
-
private readonly flushIntervalMs: number;
|
|
70
|
-
private readonly flushEntryLimit: number;
|
|
71
|
-
private readonly flushByteLimit: number;
|
|
72
|
-
private readonly maxBufferedEntries: number;
|
|
73
|
-
private readonly maxBufferedBytes: number;
|
|
74
|
-
private readonly createDroppedEntriesLine?: (droppedEntries: number) => string;
|
|
75
|
-
private readonly lines: string[] = [];
|
|
76
|
-
private enabled: boolean;
|
|
77
|
-
private bufferedBytes = 0;
|
|
78
|
-
private droppedEntries = 0;
|
|
79
|
-
private directoryReady = false;
|
|
80
|
-
private initializationError: string | undefined;
|
|
81
|
-
private flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
82
|
-
private flushPromise: Promise<void> | null = null;
|
|
83
|
-
private flushRequestedWhileBusy = false;
|
|
84
|
-
private shutdownHooksRegistered = false;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
this.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
this.
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
this.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
this.
|
|
204
|
-
|
|
205
|
-
this.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
this.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
this.
|
|
325
|
-
|
|
326
|
-
this.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
1
|
+
import { appendFile, chmod } from "node:fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
CONFIG_PATH,
|
|
4
|
+
DEBUG_DIR,
|
|
5
|
+
DEBUG_LOG_PATH,
|
|
6
|
+
EXTENSION_NAME,
|
|
7
|
+
ensureMultiProfilesDebugDirectory,
|
|
8
|
+
loadMultiProfilesConfig,
|
|
9
|
+
} from "./config.js";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 5_000;
|
|
12
|
+
const DEFAULT_FLUSH_ENTRY_LIMIT = 100;
|
|
13
|
+
const DEFAULT_FLUSH_BYTE_LIMIT = 50 * 1024;
|
|
14
|
+
const DEFAULT_MAX_BUFFERED_ENTRIES = 1_000;
|
|
15
|
+
const DEFAULT_MAX_BUFFERED_BYTES = 512 * 1024;
|
|
16
|
+
|
|
17
|
+
export interface MultiProfilesDebugLoggerOptions {
|
|
18
|
+
configPath?: string;
|
|
19
|
+
debugDir?: string;
|
|
20
|
+
logPath?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function safeJsonStringify(value: unknown): string {
|
|
24
|
+
const seen = new WeakSet<object>();
|
|
25
|
+
return JSON.stringify(value, (_key, currentValue) => {
|
|
26
|
+
if (currentValue instanceof Error) {
|
|
27
|
+
return {
|
|
28
|
+
name: currentValue.name,
|
|
29
|
+
message: currentValue.message,
|
|
30
|
+
stack: currentValue.stack,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof currentValue === "bigint") {
|
|
35
|
+
return currentValue.toString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof currentValue === "object" && currentValue !== null) {
|
|
39
|
+
if (seen.has(currentValue)) {
|
|
40
|
+
return "[Circular]";
|
|
41
|
+
}
|
|
42
|
+
seen.add(currentValue);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return currentValue;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizePositiveInteger(value: number | undefined, fallback: number): number {
|
|
50
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
51
|
+
return Math.floor(value);
|
|
52
|
+
}
|
|
53
|
+
return fallback;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AsyncBufferedLogWriterOptions {
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
logPath: string;
|
|
59
|
+
ensureDirectory: () => string | undefined;
|
|
60
|
+
flushIntervalMs?: number;
|
|
61
|
+
flushEntryLimit?: number;
|
|
62
|
+
flushByteLimit?: number;
|
|
63
|
+
maxBufferedEntries?: number;
|
|
64
|
+
maxBufferedBytes?: number;
|
|
65
|
+
createDroppedEntriesLine?: (droppedEntries: number) => string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class AsyncBufferedLogWriter {
|
|
69
|
+
private readonly flushIntervalMs: number;
|
|
70
|
+
private readonly flushEntryLimit: number;
|
|
71
|
+
private readonly flushByteLimit: number;
|
|
72
|
+
private readonly maxBufferedEntries: number;
|
|
73
|
+
private readonly maxBufferedBytes: number;
|
|
74
|
+
private readonly createDroppedEntriesLine?: (droppedEntries: number) => string;
|
|
75
|
+
private readonly lines: string[] = [];
|
|
76
|
+
private enabled: boolean;
|
|
77
|
+
private bufferedBytes = 0;
|
|
78
|
+
private droppedEntries = 0;
|
|
79
|
+
private directoryReady = false;
|
|
80
|
+
private initializationError: string | undefined;
|
|
81
|
+
private flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
82
|
+
private flushPromise: Promise<void> | null = null;
|
|
83
|
+
private flushRequestedWhileBusy = false;
|
|
84
|
+
private shutdownHooksRegistered = false;
|
|
85
|
+
private shutdownFlushHandler: (() => void) | null = null;
|
|
86
|
+
|
|
87
|
+
constructor(private readonly options: AsyncBufferedLogWriterOptions) {
|
|
88
|
+
this.enabled = options.enabled;
|
|
89
|
+
this.flushIntervalMs = normalizePositiveInteger(
|
|
90
|
+
options.flushIntervalMs,
|
|
91
|
+
DEFAULT_FLUSH_INTERVAL_MS,
|
|
92
|
+
);
|
|
93
|
+
this.flushEntryLimit = normalizePositiveInteger(
|
|
94
|
+
options.flushEntryLimit,
|
|
95
|
+
DEFAULT_FLUSH_ENTRY_LIMIT,
|
|
96
|
+
);
|
|
97
|
+
this.flushByteLimit = normalizePositiveInteger(
|
|
98
|
+
options.flushByteLimit,
|
|
99
|
+
DEFAULT_FLUSH_BYTE_LIMIT,
|
|
100
|
+
);
|
|
101
|
+
this.maxBufferedEntries = normalizePositiveInteger(
|
|
102
|
+
options.maxBufferedEntries,
|
|
103
|
+
DEFAULT_MAX_BUFFERED_ENTRIES,
|
|
104
|
+
);
|
|
105
|
+
this.maxBufferedBytes = normalizePositiveInteger(
|
|
106
|
+
options.maxBufferedBytes,
|
|
107
|
+
DEFAULT_MAX_BUFFERED_BYTES,
|
|
108
|
+
);
|
|
109
|
+
this.createDroppedEntriesLine = options.createDroppedEntriesLine;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setEnabled(enabled: boolean): void {
|
|
113
|
+
if (this.enabled === enabled) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.enabled = enabled;
|
|
118
|
+
if (!enabled) {
|
|
119
|
+
this.unregisterShutdownHooks();
|
|
120
|
+
this.clearBuffer();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
writeLine(line: string): string | undefined {
|
|
125
|
+
if (!this.enabled) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const directoryError = this.ensureReady();
|
|
130
|
+
if (directoryError) {
|
|
131
|
+
return directoryError;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.registerShutdownHooks();
|
|
135
|
+
this.pushLine(line);
|
|
136
|
+
if (
|
|
137
|
+
this.lines.length >= this.flushEntryLimit ||
|
|
138
|
+
this.bufferedBytes >= this.flushByteLimit
|
|
139
|
+
) {
|
|
140
|
+
void this.flush();
|
|
141
|
+
} else {
|
|
142
|
+
this.scheduleFlush();
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async flush(): Promise<void> {
|
|
148
|
+
if (!this.enabled || this.lines.length === 0) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (this.flushPromise) {
|
|
153
|
+
this.flushRequestedWhileBusy = true;
|
|
154
|
+
await this.flushPromise;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.clearFlushTimer();
|
|
159
|
+
const payload = this.drainBuffer();
|
|
160
|
+
if (!payload) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const flushPromise = (async () => {
|
|
165
|
+
try {
|
|
166
|
+
await appendFile(this.options.logPath, payload, "utf-8");
|
|
167
|
+
} catch (error) {
|
|
168
|
+
this.initializationError = error instanceof Error ? error.message : "Failed to write debug log.";
|
|
169
|
+
this.directoryReady = false;
|
|
170
|
+
this.requeuePayload(payload);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await this.hardenLogPermissions();
|
|
175
|
+
})().finally(async () => {
|
|
176
|
+
this.flushPromise = null;
|
|
177
|
+
if (this.flushRequestedWhileBusy || this.lines.length > 0) {
|
|
178
|
+
this.flushRequestedWhileBusy = false;
|
|
179
|
+
await this.flush();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
this.flushPromise = flushPromise;
|
|
183
|
+
await flushPromise;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private ensureReady(): string | undefined {
|
|
187
|
+
if (this.initializationError) {
|
|
188
|
+
return this.initializationError;
|
|
189
|
+
}
|
|
190
|
+
if (this.directoryReady) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const result = this.options.ensureDirectory();
|
|
196
|
+
if (!result) {
|
|
197
|
+
this.initializationError = "Debug directory could not be created or accessed.";
|
|
198
|
+
return this.initializationError;
|
|
199
|
+
}
|
|
200
|
+
this.directoryReady = true;
|
|
201
|
+
return undefined;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
this.initializationError =
|
|
204
|
+
error instanceof Error ? error.message : "Failed to initialize debug directory.";
|
|
205
|
+
return this.initializationError;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private async hardenLogPermissions(): Promise<void> {
|
|
210
|
+
if (process.platform === "win32") {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
await chmod(this.options.logPath, 0o600);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
this.initializationError =
|
|
218
|
+
error instanceof Error ? error.message : "Failed to harden debug log permissions.";
|
|
219
|
+
this.setEnabled(false);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private registerShutdownHooks(): void {
|
|
224
|
+
if (this.shutdownHooksRegistered) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.shutdownHooksRegistered = true;
|
|
229
|
+
const flushSafely = (): void => {
|
|
230
|
+
void this.flush();
|
|
231
|
+
};
|
|
232
|
+
this.shutdownFlushHandler = flushSafely;
|
|
233
|
+
process.once("beforeExit", flushSafely);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private unregisterShutdownHooks(): void {
|
|
237
|
+
if (!this.shutdownHooksRegistered || !this.shutdownFlushHandler) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
process.off("beforeExit", this.shutdownFlushHandler);
|
|
242
|
+
this.shutdownHooksRegistered = false;
|
|
243
|
+
this.shutdownFlushHandler = null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async dispose(): Promise<void> {
|
|
247
|
+
this.unregisterShutdownHooks();
|
|
248
|
+
await this.flush();
|
|
249
|
+
this.clearBuffer();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private pushLine(line: string): void {
|
|
253
|
+
const normalizedLine = line.endsWith("\n") ? line : `${line}\n`;
|
|
254
|
+
this.lines.push(normalizedLine);
|
|
255
|
+
this.bufferedBytes += Buffer.byteLength(normalizedLine, "utf-8");
|
|
256
|
+
this.enforceBufferLimits();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private enforceBufferLimits(): void {
|
|
260
|
+
while (
|
|
261
|
+
this.lines.length > this.maxBufferedEntries ||
|
|
262
|
+
this.bufferedBytes > this.maxBufferedBytes
|
|
263
|
+
) {
|
|
264
|
+
const droppedLine = this.lines.shift();
|
|
265
|
+
if (!droppedLine) {
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
this.bufferedBytes = Math.max(
|
|
269
|
+
0,
|
|
270
|
+
this.bufferedBytes - Buffer.byteLength(droppedLine, "utf-8"),
|
|
271
|
+
);
|
|
272
|
+
this.droppedEntries += 1;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private scheduleFlush(): void {
|
|
277
|
+
if (this.flushTimer) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
this.flushTimer = setTimeout(() => {
|
|
282
|
+
this.flushTimer = null;
|
|
283
|
+
void this.flush();
|
|
284
|
+
}, this.flushIntervalMs);
|
|
285
|
+
const flushTimer = this.flushTimer as ReturnType<typeof setTimeout> & {
|
|
286
|
+
unref?: () => void;
|
|
287
|
+
};
|
|
288
|
+
flushTimer.unref?.();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private clearFlushTimer(): void {
|
|
292
|
+
if (!this.flushTimer) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
clearTimeout(this.flushTimer);
|
|
296
|
+
this.flushTimer = null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private drainBuffer(): string {
|
|
300
|
+
const pendingLines: string[] = [];
|
|
301
|
+
if (this.droppedEntries > 0 && this.createDroppedEntriesLine) {
|
|
302
|
+
pendingLines.push(this.createDroppedEntriesLine(this.droppedEntries));
|
|
303
|
+
this.droppedEntries = 0;
|
|
304
|
+
}
|
|
305
|
+
pendingLines.push(...this.lines);
|
|
306
|
+
this.lines.length = 0;
|
|
307
|
+
this.bufferedBytes = 0;
|
|
308
|
+
return pendingLines.join("");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private requeuePayload(payload: string): void {
|
|
312
|
+
this.clearBuffer();
|
|
313
|
+
for (const line of payload.split(/(?<=\n)/u)) {
|
|
314
|
+
if (!line) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
this.pushLine(line);
|
|
318
|
+
}
|
|
319
|
+
this.scheduleFlush();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private clearBuffer(): void {
|
|
323
|
+
this.clearFlushTimer();
|
|
324
|
+
this.lines.length = 0;
|
|
325
|
+
this.bufferedBytes = 0;
|
|
326
|
+
this.droppedEntries = 0;
|
|
327
|
+
this.flushRequestedWhileBusy = false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export class MultiProfilesDebugLogger {
|
|
332
|
+
private initialized = false;
|
|
333
|
+
private readonly writer: AsyncBufferedLogWriter;
|
|
334
|
+
|
|
335
|
+
constructor(private readonly options: MultiProfilesDebugLoggerOptions = {}) {
|
|
336
|
+
this.writer = new AsyncBufferedLogWriter({
|
|
337
|
+
enabled: false,
|
|
338
|
+
logPath: this.options.logPath ?? DEBUG_LOG_PATH,
|
|
339
|
+
ensureDirectory: () => ensureMultiProfilesDebugDirectory(this.options.debugDir ?? DEBUG_DIR),
|
|
340
|
+
createDroppedEntriesLine: (droppedEntries) =>
|
|
341
|
+
`${safeJsonStringify({
|
|
342
|
+
timestamp: new Date().toISOString(),
|
|
343
|
+
level: "warn",
|
|
344
|
+
extension: EXTENSION_NAME,
|
|
345
|
+
event: "debug_log_overflow",
|
|
346
|
+
droppedEntries,
|
|
347
|
+
})}\n`,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private initialize(): void {
|
|
352
|
+
if (this.initialized) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
this.initialized = true;
|
|
357
|
+
const configResult = loadMultiProfilesConfig(this.options.configPath ?? CONFIG_PATH);
|
|
358
|
+
this.writer.setEnabled(configResult.config.debug);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
log(event: string, payload: Record<string, unknown> = {}): void {
|
|
362
|
+
try {
|
|
363
|
+
this.initialize();
|
|
364
|
+
this.writer.writeLine(
|
|
365
|
+
`${safeJsonStringify({
|
|
366
|
+
timestamp: new Date().toISOString(),
|
|
367
|
+
level: "debug",
|
|
368
|
+
extension: EXTENSION_NAME,
|
|
369
|
+
event,
|
|
370
|
+
...payload,
|
|
371
|
+
})}\n`,
|
|
372
|
+
);
|
|
373
|
+
} catch {
|
|
374
|
+
// Debug log failures must never affect extension functionality.
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
flush(): Promise<void> {
|
|
379
|
+
return this.writer.flush();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export const multiProfilesDebugLogger = new MultiProfilesDebugLogger();
|