Skip to content

Commit ce6876e

Browse files
committed
fix(cli): keep UTF-8 log tails valid
1 parent 077e35f commit ce6876e

2 files changed

Lines changed: 36 additions & 2 deletions

File tree

src/cli/logs-cli.runtime.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,26 @@ describe("execFileUtf8Tail", () => {
5454
await expect(resultPromise).resolves.toMatchObject({ code: 1, stderr: "spawn failed" });
5555
expect(kill).not.toHaveBeenCalled();
5656
});
57+
58+
it("does not return a replacement character when stdout tail starts mid UTF-8 sequence", async () => {
59+
const stdout = new EventEmitter();
60+
const stderr = new EventEmitter();
61+
const child = Object.assign(new EventEmitter(), {
62+
kill: vi.fn(() => true),
63+
stderr,
64+
stdout,
65+
});
66+
spawnMock.mockReturnValue(child as unknown as ChildProcess);
67+
68+
const resultPromise = execFileUtf8Tail("journalctl", ["--no-pager"], { maxBytes: 1 });
69+
stdout.emit("data", Buffer.from("é", "utf8"));
70+
child.emit("close", 0);
71+
72+
await expect(resultPromise).resolves.toEqual({
73+
code: 0,
74+
stderr: "",
75+
stdout: "",
76+
truncated: true,
77+
});
78+
});
5779
});

src/cli/logs-cli.runtime.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ export { readSystemdServiceRuntime } from "../daemon/systemd.js";
77

88
type ExecFileTailResult = { stdout: string; stderr: string; code: number; truncated: boolean };
99

10+
function decodeUtf8Tail(chunks: Buffer[], truncated: boolean): string {
11+
const buffer = Buffer.concat(chunks);
12+
if (!truncated || buffer.length === 0) {
13+
return buffer.toString("utf8");
14+
}
15+
let offset = 0;
16+
while (offset < buffer.length && (buffer[offset] & 0xc0) === 0x80) {
17+
offset += 1;
18+
}
19+
return buffer.subarray(offset).toString("utf8");
20+
}
21+
1022
export async function execFileUtf8Tail(
1123
command: string,
1224
args: string[],
@@ -68,7 +80,7 @@ export async function execFileUtf8Tail(
6880
child.kill();
6981
}
7082
resolve({
71-
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
83+
stdout: decodeUtf8Tail(stdoutChunks, truncated),
7284
stderr: error instanceof Error ? error.message : String(error),
7385
code: 1,
7486
truncated,
@@ -84,7 +96,7 @@ export async function execFileUtf8Tail(
8496
}
8597
settled = true;
8698
resolve({
87-
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
99+
stdout: decodeUtf8Tail(stdoutChunks, truncated),
88100
stderr: Buffer.concat(stderrChunks).toString("utf8"),
89101
code: typeof code === "number" ? code : 1,
90102
truncated,

0 commit comments

Comments
 (0)