Skip to content

Commit 9f439a2

Browse files
authored
Merge pull request #618 from debugmcp/codex/569-bounded-log-tail
fix: bound proxy diagnostic log reads (#569)
2 parents b4511f8 + b9b6b44 commit 9f439a2

25 files changed

Lines changed: 96 additions & 41 deletions

changelog.d/569.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Bound proxy-failure log reads to the final 64 KiB before redacting and trimming them to 80 lines, and remove redundant raw-error serialization from diagnostic log metadata.

packages/adapter-rust/tests/rust-adapter.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { deriveSourceMapFromBinary } from '@debugmcp/codelldb-common';
4343
const mockDependencies: AdapterDependencies = {
4444
fileSystem: {
4545
readFile: vi.fn(),
46+
readTail: vi.fn(),
4647
writeFile: vi.fn(),
4748
outputFile: vi.fn(),
4849
exists: vi.fn(),

packages/adapter-rust/tests/rust-debug-adapter.toolchain.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { resolveCodeLLDBExecutable } from '../src/utils/codelldb-resolver.js';
5050
const createDependencies = (): AdapterDependencies => ({
5151
fileSystem: {
5252
readFile: vi.fn(),
53+
readTail: vi.fn(),
5354
writeFile: vi.fn(),
5455
outputFile: vi.fn(),
5556
exists: vi.fn(),

packages/shared/src/interfaces/external-dependencies.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export interface IProxyManager {
2121
export interface IFileSystem {
2222
// Basic fs operations
2323
readFile(path: string, encoding?: BufferEncoding): Promise<string>;
24+
/** Read at most `maxBytes` from the end of a file as UTF-8 text. */
25+
readTail(path: string, maxBytes: number): Promise<string>;
2426
writeFile(path: string, data: string | Buffer): Promise<void>;
2527
exists(path: string): Promise<boolean>;
2628
mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;

src/implementations/file-system-impl.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44
import fs from 'fs-extra';
55
import { Stats } from 'fs';
6+
import { open } from 'node:fs/promises';
67
import { IFileSystem } from '@debugmcp/shared';
78

89
export class FileSystemImpl implements IFileSystem {
@@ -11,6 +12,26 @@ export class FileSystemImpl implements IFileSystem {
1112
return fs.readFile(path, encoding || 'utf-8');
1213
}
1314

15+
async readTail(path: string, maxBytes: number): Promise<string> {
16+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
17+
throw new RangeError('maxBytes must be a non-negative safe integer');
18+
}
19+
if (maxBytes === 0) {
20+
return '';
21+
}
22+
23+
const file = await open(path, 'r');
24+
try {
25+
const { size } = await file.stat();
26+
const length = Math.min(size, maxBytes);
27+
const buffer = Buffer.alloc(length);
28+
const { bytesRead } = await file.read(buffer, 0, length, size - length);
29+
return buffer.subarray(0, bytesRead).toString('utf8');
30+
} finally {
31+
await file.close();
32+
}
33+
}
34+
1435
async writeFile(path: string, data: string | Buffer): Promise<void> {
1536
return fs.writeFile(path, data);
1637
}

src/interfaces/external-dependencies.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import type { IDebugAdapter } from '@debugmcp/shared';
1717
export interface IFileSystem {
1818
// Basic fs operations
1919
readFile(path: string, encoding?: BufferEncoding): Promise<string>;
20+
/** Read at most `maxBytes` from the end of a file as UTF-8 text. */
21+
readTail(path: string, maxBytes: number): Promise<string>;
2022
writeFile(path: string, data: string | Buffer): Promise<void>;
2123
exists(path: string): Promise<boolean>;
2224
mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;

src/session/launch/proxy-failure-diagnostics.ts

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,11 @@ import type { ProxyInitProgress } from '../../utils/error-messages.js';
2323
import { getErrorMessage, SessionNotFoundError } from '../../errors/debug-errors.js';
2424
import { proxyLogPathFor } from '../../proxy/session-log-layout.js';
2525

26-
/** How many trailing proxy-log lines are worth reading after a failure. */
26+
/** How many trailing proxy-log lines are worth reporting after a failure. */
2727
const PROXY_LOG_TAIL_LINES = 80;
2828

29-
/**
30-
* Character cap on the tail. Generous on purpose: it exists so a proxy log with
31-
* one pathological multi-megabyte line cannot blow up the record, not to trim a
32-
* normal 80-line tail, which is a few kilobytes.
33-
*/
34-
const PROXY_LOG_TAIL_CHARS = 64 * 1024;
29+
/** Hard I/O and allocation cap applied before the log is sanitized. */
30+
export const PROXY_LOG_TAIL_MAX_BYTES = 64 * 1024;
3531

3632
/** The pointers a failed launch/attach returns to the caller (issue #493 / #551). */
3733
export interface ProxyFailureDiagnostics {
@@ -45,7 +41,7 @@ const PROXY_LOG_TAIL_CHARS = 64 * 1024;
4541
*/
4642
export interface ProxyFailureLogDeps {
4743
logger: ILogger;
48-
fileSystem: Pick<IFileSystem, 'readFile'>;
44+
fileSystem: Pick<IFileSystem, 'readTail'>;
4945
}
5046

5147
/** The two operations that can fail this way, named as they appear in the log. */
@@ -94,7 +90,7 @@ export function collectProxyFailureDiagnostics(
9490
/**
9591
* Read the last `tailLineCount` lines of the proxy log, if there is one.
9692
*
97-
* Reads straight through rather than asking `pathExists` first: the proxy is
93+
* Reads the bounded tail rather than asking `pathExists` first: the proxy is
9894
* still writing (and may rotate) this file, so an exists-then-read pair can
9995
* report "no log" for a file that appeared a millisecond later, and spends a
10096
* second syscall to do it. `ENOENT` — the answer that check was buying — is
@@ -111,18 +107,18 @@ export function collectProxyFailureDiagnostics(
111107
* error that sent us here still reaches the log intact.
112108
*/
113109
export async function readProxyLogTail(
114-
fileSystem: Pick<IFileSystem, 'readFile'>,
110+
fileSystem: Pick<IFileSystem, 'readTail'>,
115111
proxyLogPath: string | undefined,
116112
tailLineCount: number = PROXY_LOG_TAIL_LINES
117113
): Promise<string | undefined> {
118114
if (!proxyLogPath) {
119115
return undefined;
120116
}
121117
try {
122-
const logContent = await fileSystem.readFile(proxyLogPath, 'utf-8');
118+
const logContent = await fileSystem.readTail(proxyLogPath, PROXY_LOG_TAIL_MAX_BYTES);
123119
return sanitizeStderrTail(logContent, {
124120
maxLines: tailLineCount,
125-
maxChars: PROXY_LOG_TAIL_CHARS
121+
maxChars: PROXY_LOG_TAIL_MAX_BYTES
126122
});
127123
} catch (logReadError) {
128124
if ((logReadError as NodeJS.ErrnoException)?.code === 'ENOENT') {
@@ -155,13 +151,6 @@ export function buildProxyFailureErrorDetails(
155151
proxyLogTail
156152
};
157153

158-
// Try to capture raw error object
159-
try {
160-
errorDetails.raw = JSON.stringify(error);
161-
} catch {
162-
errorDetails.raw = 'Error not JSON serializable';
163-
}
164-
165154
return errorDetails;
166155
}
167156

tests/adapters/go/integration/go-session-smoke.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { GoAdapterFactory } from '@debugmcp/adapter-go';
88
const createDependencies = (): AdapterDependencies => ({
99
fileSystem: {
1010
readFile: async () => '',
11+
readTail: async () => '',
1112
writeFile: async () => {},
1213
exists: async () => false,
1314
mkdir: async () => {},

tests/adapters/go/unit/go-adapter-factory.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const mockSpawn = vi.mocked(spawn);
1919
const createMockDependencies = (): AdapterDependencies => ({
2020
fileSystem: {
2121
readFile: async () => '',
22+
readTail: async () => '',
2223
writeFile: async () => {},
2324
exists: async () => false,
2425
mkdir: async () => {},

tests/adapters/go/unit/go-debug-adapter.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const mockSpawn = vi.mocked(spawn);
1919
const createMockDependencies = (): AdapterDependencies => ({
2020
fileSystem: {
2121
readFile: async () => '',
22+
readTail: async () => '',
2223
writeFile: async () => {},
2324
exists: async () => false,
2425
mkdir: async () => {},

0 commit comments

Comments
 (0)