@aliou/pi-processes 0.9.4 → 0.10.0
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/CONTRIBUTING.md +40 -42
- package/README.md +54 -84
- package/extensions/process-tabs.ts +26 -0
- package/extensions/processes/client.ts +194 -0
- package/extensions/processes/commands/clear.ts +29 -0
- package/extensions/processes/commands/kill.ts +152 -0
- package/extensions/processes/commands/overview.ts +102 -0
- package/extensions/processes/components/overview-component.ts +729 -0
- package/extensions/processes/components/overview-panel.ts +177 -0
- package/extensions/processes/config/defaults.ts +28 -0
- package/extensions/processes/config/index.ts +13 -0
- package/extensions/processes/config/loader.ts +62 -0
- package/extensions/processes/config/migrations/000-import-legacy-process-config.ts +113 -0
- package/extensions/processes/config/migrations/001-v0-9-4-to-v0-10-0-config.ts +184 -0
- package/extensions/processes/config/migrations/002-stamp-v0-10-0-config-version.ts +21 -0
- package/extensions/processes/config/migrations/index.ts +25 -0
- package/extensions/processes/config/schema.ts +9 -0
- package/extensions/processes/config/types.ts +72 -0
- package/extensions/processes/constants.ts +1 -0
- package/extensions/processes/handlers/commands.ts +113 -0
- package/extensions/processes/handlers/kill-process.ts +31 -0
- package/extensions/processes/handlers/notifications.ts +147 -0
- package/extensions/processes/handlers/requests.ts +114 -0
- package/extensions/processes/handlers/subscriptions.ts +144 -0
- package/extensions/processes/hooks/background-blocker.ts +201 -0
- package/extensions/processes/hooks/cleanup.ts +43 -0
- package/extensions/processes/hooks/event-bridge.ts +32 -0
- package/extensions/processes/i18n/index.ts +3 -0
- package/extensions/processes/i18n/messages.ts +27 -0
- package/extensions/processes/i18n/translator.ts +49 -0
- package/extensions/processes/index.ts +109 -0
- package/extensions/processes/message-renderer.ts +117 -0
- package/extensions/processes/notification-sender.ts +43 -0
- package/extensions/processes/notifications/classify.ts +27 -0
- package/extensions/processes/notifications/log-matchers.ts +146 -0
- package/extensions/processes/notifications/registry.ts +225 -0
- package/extensions/processes/notifications/render-content.ts +122 -0
- package/extensions/processes/notifications/service.ts +336 -0
- package/extensions/processes/notifications/types.ts +11 -0
- package/extensions/processes/settings/apply-setting-change.ts +30 -0
- package/extensions/processes/settings/build-sections.ts +420 -0
- package/extensions/processes/settings/index.ts +42 -0
- package/extensions/processes/tools/clear/index.ts +21 -0
- package/extensions/processes/tools/clear/render.ts +40 -0
- package/extensions/processes/tools/components/index.ts +4 -0
- package/extensions/processes/tools/components/process-action-header.ts +31 -0
- package/extensions/processes/tools/components/process-action-title.ts +17 -0
- package/extensions/processes/tools/components/tool-layout.ts +70 -0
- package/extensions/processes/tools/components/watch.ts +53 -0
- package/extensions/processes/tools/index.ts +265 -0
- package/extensions/processes/tools/list/index.ts +157 -0
- package/extensions/processes/tools/list/render.ts +211 -0
- package/extensions/processes/tools/notify.ts +191 -0
- package/extensions/processes/tools/output/index.ts +307 -0
- package/extensions/processes/tools/output/render.ts +229 -0
- package/extensions/processes/tools/schema.ts +263 -0
- package/extensions/processes/tools/start/index.ts +64 -0
- package/extensions/processes/tools/start/render.ts +67 -0
- package/extensions/processes/tools/stop/index.ts +35 -0
- package/extensions/processes/tools/stop/render.ts +43 -0
- package/extensions/processes/tools/update/index.ts +320 -0
- package/extensions/processes/tools/update/render.ts +251 -0
- package/extensions/processes/tools/utils.ts +112 -0
- package/extensions/processes/tools/watch-format.ts +14 -0
- package/extensions/processes/tools/write/index.ts +75 -0
- package/extensions/processes/tools/write/render.ts +68 -0
- package/extensions/processes/utils/truncate.ts +285 -0
- package/extensions/processes-dock/client.ts +60 -0
- package/extensions/processes-dock/commands/dock.ts +70 -0
- package/extensions/processes-dock/commands/pin.ts +166 -0
- package/extensions/processes-dock/components/log-dock-component.ts +375 -0
- package/extensions/processes-dock/dock-state.ts +33 -0
- package/extensions/processes-dock/index.ts +28 -0
- package/extensions/processes-dock/logs-client.ts +5 -0
- package/extensions/processes-dock/widget/setup.ts +520 -0
- package/extensions/processes-dock/widget/status.ts +149 -0
- package/extensions/processes-dock/widget/types.ts +16 -0
- package/extensions/processes-logs/client.ts +51 -0
- package/extensions/processes-logs/commands/logs.ts +108 -0
- package/extensions/processes-logs/completions.ts +31 -0
- package/extensions/processes-logs/components/log-file-viewer.ts +274 -0
- package/extensions/processes-logs/components/log-overlay-component.ts +685 -0
- package/extensions/processes-logs/index.ts +27 -0
- package/extensions/processes-logs/logs-client.ts +101 -0
- package/extensions/shared/line-buffer.ts +35 -0
- package/extensions/shared/output-payload.ts +28 -0
- package/extensions/shared/ui.ts +146 -0
- package/package.json +47 -20
- package/schema.json +134 -0
- package/skills/pi-processes/SKILL.md +496 -25
- package/src/get-manager.ts +15 -0
- package/src/manager/index.ts +182 -0
- package/src/manager/internal-types.ts +130 -0
- package/src/manager/limits.ts +15 -0
- package/src/manager/process-log-store.ts +422 -0
- package/src/manager/process-output.ts +345 -0
- package/src/manager/process-registry.ts +78 -0
- package/src/manager/process-runtime-controller.ts +499 -0
- package/src/protocol/broadcasts.ts +14 -0
- package/src/protocol/channels.ts +30 -0
- package/src/protocol/commands.ts +24 -0
- package/src/protocol/index.ts +34 -0
- package/src/protocol/logs.ts +23 -0
- package/src/protocol/notifications.ts +48 -0
- package/src/protocol/requests.ts +76 -0
- package/src/types.ts +66 -0
- package/src/utils/ansi.ts +2 -4
- package/src/utils/format.ts +24 -32
- package/src/utils/index.ts +5 -1
- package/src/utils/is-record.ts +3 -0
- package/src/utils/match-line.ts +32 -0
- package/src/utils/platform.ts +13 -0
- package/src/utils/shorten-path.ts +11 -0
- package/src/utils/signals.ts +22 -0
- package/src/commands/clear/command.ts +0 -14
- package/src/commands/clear/index.ts +0 -1
- package/src/commands/completions.ts +0 -38
- package/src/commands/dock/command.ts +0 -29
- package/src/commands/dock/index.ts +0 -1
- package/src/commands/index.ts +0 -33
- package/src/commands/kill/command.ts +0 -69
- package/src/commands/kill/index.ts +0 -1
- package/src/commands/logs/command.ts +0 -47
- package/src/commands/logs/index.ts +0 -1
- package/src/commands/pick-process.ts +0 -34
- package/src/commands/pin/command.ts +0 -34
- package/src/commands/pin/index.ts +0 -1
- package/src/commands/processes/command.ts +0 -39
- package/src/commands/processes/index.ts +0 -1
- package/src/commands/settings/apply-setting-change.ts +0 -72
- package/src/commands/settings/build-sections.ts +0 -160
- package/src/commands/settings/command.ts +0 -20
- package/src/commands/settings/index.ts +0 -1
- package/src/components/log-dock-component.test.ts +0 -29
- package/src/components/log-dock-component.ts +0 -236
- package/src/components/log-file-viewer.ts +0 -317
- package/src/components/log-overlay-component.ts +0 -546
- package/src/components/panel-helpers.ts +0 -37
- package/src/components/process-picker-component.ts +0 -166
- package/src/components/processes-component.ts +0 -485
- package/src/components/status-format.ts +0 -38
- package/src/config.ts +0 -107
- package/src/constants/index.ts +0 -16
- package/src/constants/types.ts +0 -106
- package/src/hooks/background-blocker.ts +0 -66
- package/src/hooks/cleanup.ts +0 -10
- package/src/hooks/index.ts +0 -32
- package/src/hooks/message-renderer.ts +0 -115
- package/src/hooks/process-end.ts +0 -69
- package/src/hooks/process-watch.ts +0 -85
- package/src/hooks/utils.ts +0 -23
- package/src/hooks/widget/index.ts +0 -2
- package/src/hooks/widget/setup.ts +0 -167
- package/src/hooks/widget/status-widget.ts +0 -105
- package/src/hooks/widget/types.ts +0 -20
- package/src/index.ts +0 -35
- package/src/manager.test.ts +0 -331
- package/src/manager.ts +0 -758
- package/src/tools/actions/clear.ts +0 -20
- package/src/tools/actions/debug.ts +0 -148
- package/src/tools/actions/index.ts +0 -171
- package/src/tools/actions/kill.ts +0 -89
- package/src/tools/actions/list.ts +0 -188
- package/src/tools/actions/logs.ts +0 -117
- package/src/tools/actions/output.ts +0 -269
- package/src/tools/actions/start.ts +0 -245
- package/src/tools/actions/write.ts +0 -113
- package/src/tools/index.ts +0 -207
- package/src/utils/ansi.test.ts +0 -22
- package/src/utils/command-executor.test.ts +0 -48
- package/src/utils/keybindings.ts +0 -71
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
closeSync,
|
|
4
|
+
fstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
readSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
statSync,
|
|
12
|
+
truncateSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
import type { ProcessLogPaths } from "./internal-types";
|
|
18
|
+
import { MAX_LOG_FILE_BYTES, MAX_TAIL_READ_BYTES } from "./limits";
|
|
19
|
+
|
|
20
|
+
const TAIL_CHUNK_BYTES = 64 * 1024;
|
|
21
|
+
const MAX_FULL_FILE_BYTES = MAX_TAIL_READ_BYTES * 8;
|
|
22
|
+
const TAIL_TRUNCATION_PREFIX = "[… truncated] ";
|
|
23
|
+
const STDOUT_TRUNCATION_MARKER =
|
|
24
|
+
"[log truncated at 64 MB — earlier output discarded]\n";
|
|
25
|
+
const STDERR_TRUNCATION_MARKER =
|
|
26
|
+
"[log truncated at 64 MB — earlier output discarded]\n";
|
|
27
|
+
const COMBINED_TRUNCATION_MARKER =
|
|
28
|
+
"2:[log truncated at 64 MB — earlier output discarded]\n";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Stores process log files on disk and enforces a per-file size cap.
|
|
32
|
+
*
|
|
33
|
+
* The log directory is created lazily: constructing a store without an
|
|
34
|
+
* explicit `logDir` does not touch the filesystem, so a session that never
|
|
35
|
+
* starts a process leaves no temp directory behind. An explicit `logDir` is
|
|
36
|
+
* also no longer created eagerly at construction; it is created on first use.
|
|
37
|
+
*
|
|
38
|
+
* Each file holds a single window of history. When a file exceeds
|
|
39
|
+
* {@link MAX_LOG_FILE_BYTES} it is truncated to zero and writing restarts
|
|
40
|
+
* from a marker line, so `stdout.log`, `stderr.log`, and `combined.log` can
|
|
41
|
+
* hold different windows of history after a breach. `combined.log` is what
|
|
42
|
+
* the UI reads; the per-stream files are what the agent greps.
|
|
43
|
+
*/
|
|
44
|
+
export class ProcessLogStore {
|
|
45
|
+
private logDir: string | null = null;
|
|
46
|
+
private readonly explicitLogDir: string | undefined;
|
|
47
|
+
private readonly maxFileBytes: number;
|
|
48
|
+
private readonly fileBytes = new Map<string, number>();
|
|
49
|
+
|
|
50
|
+
constructor(logDir?: string, options?: { maxFileBytes?: number }) {
|
|
51
|
+
this.explicitLogDir = logDir;
|
|
52
|
+
this.maxFileBytes = options?.maxFileBytes ?? MAX_LOG_FILE_BYTES;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private ensureLogDir(): string {
|
|
56
|
+
if (this.logDir) return this.logDir;
|
|
57
|
+
if (this.explicitLogDir) {
|
|
58
|
+
mkdirSync(this.explicitLogDir, { recursive: true });
|
|
59
|
+
this.logDir = this.explicitLogDir;
|
|
60
|
+
return this.logDir;
|
|
61
|
+
}
|
|
62
|
+
const tempParent = tmpdir();
|
|
63
|
+
mkdirSync(tempParent, { recursive: true });
|
|
64
|
+
this.logDir = mkdtempSync(join(tempParent, "pi-processes-"));
|
|
65
|
+
return this.logDir;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getLogDir(): string {
|
|
69
|
+
return this.ensureLogDir();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
createLogs(processId: string): ProcessLogPaths {
|
|
73
|
+
const dir = this.ensureLogDir();
|
|
74
|
+
const stdoutFile = join(dir, `${processId}-stdout.log`);
|
|
75
|
+
const stderrFile = join(dir, `${processId}-stderr.log`);
|
|
76
|
+
const combinedFile = join(dir, `${processId}-combined.log`);
|
|
77
|
+
|
|
78
|
+
// `appendFileSync("", ...)` does not truncate, so a reused explicit logDir
|
|
79
|
+
// can hold pre-existing content. Seed the counters from the actual file
|
|
80
|
+
// sizes so the cap accounts for those bytes.
|
|
81
|
+
appendFileSync(stdoutFile, "");
|
|
82
|
+
appendFileSync(stderrFile, "");
|
|
83
|
+
appendFileSync(combinedFile, "");
|
|
84
|
+
this.fileBytes.set(stdoutFile, this.sizeOf(stdoutFile));
|
|
85
|
+
this.fileBytes.set(stderrFile, this.sizeOf(stderrFile));
|
|
86
|
+
this.fileBytes.set(combinedFile, this.sizeOf(combinedFile));
|
|
87
|
+
|
|
88
|
+
return { stdoutFile, stderrFile, combinedFile };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
appendStdout(file: string, data: Buffer): void {
|
|
92
|
+
this.appendCapped(file, data, STDOUT_TRUNCATION_MARKER);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
appendStderr(file: string, data: Buffer): void {
|
|
96
|
+
this.appendCapped(file, data, STDERR_TRUNCATION_MARKER);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
appendCombinedLine(
|
|
100
|
+
file: string,
|
|
101
|
+
source: "stdout" | "stderr",
|
|
102
|
+
line: string,
|
|
103
|
+
): void {
|
|
104
|
+
const tag = source === "stdout" ? "1" : "2";
|
|
105
|
+
const data = `${tag}:${line}\n`;
|
|
106
|
+
// The marker must carry a stream tag, or getCombinedOutput will parse it
|
|
107
|
+
// as untagged stdout. Use the stderr tag so it renders with the warning
|
|
108
|
+
// tone in the dock and overlay.
|
|
109
|
+
this.appendCapped(file, data, COMBINED_TRUNCATION_MARKER);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
appendErrorLine(file: string, message: string): void {
|
|
113
|
+
this.appendCapped(file, `${message}\n`, STDERR_TRUNCATION_MARKER);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
readTailLines(filePath: string, lines: number): string[] {
|
|
117
|
+
if (lines <= 0) return [];
|
|
118
|
+
|
|
119
|
+
let fd: number | undefined;
|
|
120
|
+
try {
|
|
121
|
+
fd = openSync(filePath, "r");
|
|
122
|
+
const size = fstatSync(fd).size;
|
|
123
|
+
if (size === 0) return [];
|
|
124
|
+
|
|
125
|
+
const buffer = Buffer.allocUnsafe(Math.min(size, MAX_TAIL_READ_BYTES));
|
|
126
|
+
let bufferStart = buffer.length;
|
|
127
|
+
let position = size;
|
|
128
|
+
let bytesRead = 0;
|
|
129
|
+
let newlineCount = 0;
|
|
130
|
+
|
|
131
|
+
while (
|
|
132
|
+
position > 0 &&
|
|
133
|
+
bytesRead < MAX_TAIL_READ_BYTES &&
|
|
134
|
+
newlineCount <= lines
|
|
135
|
+
) {
|
|
136
|
+
const length = Math.min(
|
|
137
|
+
TAIL_CHUNK_BYTES,
|
|
138
|
+
position,
|
|
139
|
+
MAX_TAIL_READ_BYTES - bytesRead,
|
|
140
|
+
);
|
|
141
|
+
position -= length;
|
|
142
|
+
bufferStart -= length;
|
|
143
|
+
readExact(fd, buffer, bufferStart, length, position);
|
|
144
|
+
bytesRead += length;
|
|
145
|
+
newlineCount += countNewlines(
|
|
146
|
+
buffer.subarray(bufferStart, bufferStart + length),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let populated: Buffer<ArrayBufferLike> = buffer.subarray(bufferStart);
|
|
151
|
+
const startedMidFile = position > 0;
|
|
152
|
+
let partialFirstLine: Buffer | undefined;
|
|
153
|
+
if (startedMidFile) {
|
|
154
|
+
const firstNewline = populated.indexOf(0x0a);
|
|
155
|
+
if (firstNewline === -1) {
|
|
156
|
+
populated = alignUtf8Start(populated);
|
|
157
|
+
const suffix = decodeUtf8Bounded(populated, MAX_TAIL_READ_BYTES);
|
|
158
|
+
return [`${TAIL_TRUNCATION_PREFIX}${suffix}`];
|
|
159
|
+
}
|
|
160
|
+
// The read began in the middle of a logical line. Dropping through
|
|
161
|
+
// its newline also removes any partial UTF-8 code point at the start.
|
|
162
|
+
const partial = alignUtf8Start(populated.subarray(0, firstNewline));
|
|
163
|
+
if (partial.length > 0) {
|
|
164
|
+
partialFirstLine = partial;
|
|
165
|
+
}
|
|
166
|
+
populated = populated.subarray(firstNewline + 1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const rawLines = tailLineBuffersNewestFirst(populated, lines);
|
|
170
|
+
if (partialFirstLine && rawLines.length < lines) {
|
|
171
|
+
rawLines.push(partialFirstLine);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const decodedNewestFirst: string[] = [];
|
|
175
|
+
let remainingBytes = MAX_TAIL_READ_BYTES;
|
|
176
|
+
for (
|
|
177
|
+
let index = 0;
|
|
178
|
+
index < rawLines.length && remainingBytes > 0;
|
|
179
|
+
index++
|
|
180
|
+
) {
|
|
181
|
+
const rawLine = rawLines[index];
|
|
182
|
+
const decoded = decodeUtf8Bounded(rawLine, remainingBytes);
|
|
183
|
+
const marked =
|
|
184
|
+
partialFirstLine && rawLine === partialFirstLine
|
|
185
|
+
? `${TAIL_TRUNCATION_PREFIX}${decoded}`
|
|
186
|
+
: decoded;
|
|
187
|
+
decodedNewestFirst.push(marked);
|
|
188
|
+
remainingBytes -= Buffer.byteLength(decoded);
|
|
189
|
+
}
|
|
190
|
+
return decodedNewestFirst.reverse();
|
|
191
|
+
} catch (_error) {
|
|
192
|
+
return [];
|
|
193
|
+
} finally {
|
|
194
|
+
if (fd !== undefined) closeSync(fd);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
readFullFile(filePath: string): string {
|
|
199
|
+
try {
|
|
200
|
+
const size = statSync(filePath).size;
|
|
201
|
+
if (size > MAX_FULL_FILE_BYTES) {
|
|
202
|
+
const marker = `[… truncated, see ${filePath}]\n`;
|
|
203
|
+
const markerBytes = Buffer.byteLength(marker);
|
|
204
|
+
const suffix = this.readFileSuffix(
|
|
205
|
+
filePath,
|
|
206
|
+
Math.max(0, MAX_FULL_FILE_BYTES - markerBytes),
|
|
207
|
+
);
|
|
208
|
+
return marker + suffix;
|
|
209
|
+
}
|
|
210
|
+
return readFileSync(filePath, "utf-8");
|
|
211
|
+
} catch (_error) {
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
getCombinedOutput(
|
|
217
|
+
combinedFile: string,
|
|
218
|
+
tailLines: number,
|
|
219
|
+
): Array<{ type: "stdout" | "stderr"; text: string }> {
|
|
220
|
+
const rawLines = this.readTailLines(combinedFile, tailLines);
|
|
221
|
+
return rawLines.map((line) => {
|
|
222
|
+
if (line.startsWith("2:")) {
|
|
223
|
+
return { type: "stderr" as const, text: line.slice(2) };
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
type: "stdout" as const,
|
|
227
|
+
text: line.startsWith("1:") ? line.slice(2) : line,
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
getFileSize(paths: ProcessLogPaths): { stdout: number; stderr: number } {
|
|
233
|
+
try {
|
|
234
|
+
return {
|
|
235
|
+
stdout: statSync(paths.stdoutFile).size,
|
|
236
|
+
stderr: statSync(paths.stderrFile).size,
|
|
237
|
+
};
|
|
238
|
+
} catch (_error) {
|
|
239
|
+
return { stdout: 0, stderr: 0 };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
removeLogs(paths: ProcessLogPaths): void {
|
|
244
|
+
try {
|
|
245
|
+
rmSync(paths.stdoutFile, { force: true });
|
|
246
|
+
rmSync(paths.stderrFile, { force: true });
|
|
247
|
+
rmSync(paths.combinedFile, { force: true });
|
|
248
|
+
} catch (_error) {
|
|
249
|
+
void _error; // Intentionally ignored
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
cleanup(): void {
|
|
254
|
+
if (!this.logDir) return;
|
|
255
|
+
try {
|
|
256
|
+
rmSync(this.logDir, { recursive: true, force: true });
|
|
257
|
+
} catch (_error) {
|
|
258
|
+
void _error; // Intentionally ignored
|
|
259
|
+
}
|
|
260
|
+
this.logDir = null;
|
|
261
|
+
this.fileBytes.clear();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
[Symbol.dispose](): void {
|
|
265
|
+
this.cleanup();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Append `data` to `file`, enforcing the per-file size cap. When the cap
|
|
270
|
+
* would be exceeded, the file is truncated to zero and restarted with
|
|
271
|
+
* `marker`, then `data` is written. Returns silently on any fs error,
|
|
272
|
+
* matching the existing best-effort append behavior. Each file is capped
|
|
273
|
+
* independently, so the per-stream and combined files can hold different
|
|
274
|
+
* windows of history after a breach.
|
|
275
|
+
*/
|
|
276
|
+
private appendCapped(
|
|
277
|
+
file: string,
|
|
278
|
+
data: string | Buffer,
|
|
279
|
+
marker: string,
|
|
280
|
+
): void {
|
|
281
|
+
try {
|
|
282
|
+
const size = Buffer.byteLength(data as never);
|
|
283
|
+
const current = this.fileBytes.get(file) ?? 0;
|
|
284
|
+
if (current + size > this.maxFileBytes) {
|
|
285
|
+
truncateSync(file, 0);
|
|
286
|
+
appendFileSync(file, marker);
|
|
287
|
+
this.fileBytes.set(file, Buffer.byteLength(marker));
|
|
288
|
+
}
|
|
289
|
+
appendFileSync(file, data);
|
|
290
|
+
this.fileBytes.set(file, (this.fileBytes.get(file) ?? 0) + size);
|
|
291
|
+
} catch (_error) {
|
|
292
|
+
void _error; // Intentionally ignored
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private sizeOf(file: string): number {
|
|
297
|
+
try {
|
|
298
|
+
return statSync(file).size;
|
|
299
|
+
} catch (_error) {
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private readFileSuffix(filePath: string, maxBytes: number): string {
|
|
305
|
+
let fd: number | undefined;
|
|
306
|
+
try {
|
|
307
|
+
fd = openSync(filePath, "r");
|
|
308
|
+
const size = fstatSync(fd).size;
|
|
309
|
+
const length = Math.min(size, maxBytes);
|
|
310
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
311
|
+
readExact(fd, buffer, 0, length, size - length);
|
|
312
|
+
return decodeUtf8Bounded(alignUtf8Start(buffer), maxBytes);
|
|
313
|
+
} catch (_error) {
|
|
314
|
+
return "";
|
|
315
|
+
} finally {
|
|
316
|
+
if (fd !== undefined) closeSync(fd);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function countNewlines(buffer: Buffer): number {
|
|
322
|
+
let count = 0;
|
|
323
|
+
for (const byte of buffer) {
|
|
324
|
+
if (byte === 0x0a) count++;
|
|
325
|
+
}
|
|
326
|
+
return count;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function alignUtf8Start(buffer: Buffer): Buffer {
|
|
330
|
+
let start = 0;
|
|
331
|
+
while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) start++;
|
|
332
|
+
return buffer.subarray(start);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function readExact(
|
|
336
|
+
fd: number,
|
|
337
|
+
buffer: Buffer,
|
|
338
|
+
offset: number,
|
|
339
|
+
length: number,
|
|
340
|
+
position: number,
|
|
341
|
+
): void {
|
|
342
|
+
let total = 0;
|
|
343
|
+
while (total < length) {
|
|
344
|
+
const count = readSync(
|
|
345
|
+
fd,
|
|
346
|
+
buffer,
|
|
347
|
+
offset + total,
|
|
348
|
+
length - total,
|
|
349
|
+
position + total,
|
|
350
|
+
);
|
|
351
|
+
if (count === 0) throw new Error("Unexpected end of log file");
|
|
352
|
+
total += count;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function tailLineBuffersNewestFirst(buffer: Buffer, count: number): Buffer[] {
|
|
357
|
+
if (buffer.length === 0 || count <= 0) return [];
|
|
358
|
+
|
|
359
|
+
const result: Buffer[] = [];
|
|
360
|
+
let end = buffer.length;
|
|
361
|
+
if (buffer[end - 1] === 0x0a) end--;
|
|
362
|
+
|
|
363
|
+
while (result.length < count && end >= 0) {
|
|
364
|
+
const newline = end > 0 ? buffer.lastIndexOf(0x0a, end - 1) : -1;
|
|
365
|
+
const start = newline + 1;
|
|
366
|
+
let lineEnd = end;
|
|
367
|
+
if (lineEnd > start && buffer[lineEnd - 1] === 0x0d) lineEnd--;
|
|
368
|
+
result.push(buffer.subarray(start, lineEnd));
|
|
369
|
+
if (newline === -1) break;
|
|
370
|
+
end = newline;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function decodeUtf8Bounded(buffer: Buffer, maxOutputBytes: number): string {
|
|
377
|
+
if (buffer.length === 0 || maxOutputBytes <= 0) return "";
|
|
378
|
+
|
|
379
|
+
const decoder = new TextDecoder("utf-8");
|
|
380
|
+
const parts: string[] = [];
|
|
381
|
+
let remaining = maxOutputBytes;
|
|
382
|
+
|
|
383
|
+
for (let offset = 0; offset < buffer.length && remaining > 0; ) {
|
|
384
|
+
const end = Math.min(buffer.length, offset + TAIL_CHUNK_BYTES);
|
|
385
|
+
const text = decoder.decode(buffer.subarray(offset, end), {
|
|
386
|
+
stream: end < buffer.length,
|
|
387
|
+
});
|
|
388
|
+
const textBytes = Buffer.byteLength(text);
|
|
389
|
+
if (textBytes <= remaining) {
|
|
390
|
+
parts.push(text);
|
|
391
|
+
remaining -= textBytes;
|
|
392
|
+
} else {
|
|
393
|
+
const prefix = trimIncompleteUtf8Suffix(
|
|
394
|
+
Buffer.from(text).subarray(0, remaining),
|
|
395
|
+
);
|
|
396
|
+
parts.push(prefix.toString("utf-8"));
|
|
397
|
+
remaining = 0;
|
|
398
|
+
}
|
|
399
|
+
offset = end;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return parts.join("");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function trimIncompleteUtf8Suffix(buffer: Buffer): Buffer {
|
|
406
|
+
if (buffer.length === 0) return buffer;
|
|
407
|
+
let lead = buffer.length - 1;
|
|
408
|
+
while (lead >= 0 && (buffer[lead] & 0xc0) === 0x80) lead--;
|
|
409
|
+
if (lead < 0) return Buffer.alloc(0);
|
|
410
|
+
const byte = buffer[lead];
|
|
411
|
+
const expected =
|
|
412
|
+
byte < 0x80
|
|
413
|
+
? 1
|
|
414
|
+
: (byte & 0xe0) === 0xc0
|
|
415
|
+
? 2
|
|
416
|
+
: (byte & 0xf0) === 0xe0
|
|
417
|
+
? 3
|
|
418
|
+
: (byte & 0xf8) === 0xf0
|
|
419
|
+
? 4
|
|
420
|
+
: 1;
|
|
421
|
+
return buffer.length - lead < expected ? buffer.subarray(0, lead) : buffer;
|
|
422
|
+
}
|