fix(voice-call): bound tailscale status output

This commit is contained in:
Vincent Koc
2026-05-28 16:07:16 +02:00
parent 910354b07f
commit c7891ec67e
2 changed files with 50 additions and 5 deletions

View File

@@ -18,12 +18,14 @@ vi.mock("node:child_process", async () => {
});
import {
appendTailscaleCommandStdout,
cleanupTailscaleExposure,
cleanupTailscaleExposureRoute,
getTailscaleDnsName,
getTailscaleSelfInfo,
setupTailscaleExposure,
setupTailscaleExposureRoute,
TAILSCALE_COMMAND_STDOUT_MAX_BYTES,
} from "./tailscale.js";
function createProc(params?: { code?: number; stdout?: string }) {
@@ -104,6 +106,21 @@ describe("voice-call tailscale helpers", () => {
await expect(getTailscaleSelfInfo()).resolves.toBeNull();
});
it("tracks tailscale stdout without retaining over-limit output", () => {
let stdout = appendTailscaleCommandStdout({ bytes: 0, exceeded: false, text: "" }, "ok", 4);
stdout = appendTailscaleCommandStdout(stdout, "boom", 4);
expect(stdout).toEqual({ bytes: 6, exceeded: true, text: "" });
});
it("kills tailscale status when stdout exceeds the capture limit", async () => {
const proc = createProc({ stdout: "x".repeat(TAILSCALE_COMMAND_STDOUT_MAX_BYTES + 1) });
spawnMock.mockReturnValueOnce(proc);
await expect(getTailscaleSelfInfo()).resolves.toBeNull();
expect(proc.kill).toHaveBeenCalledWith("SIGKILL");
});
it("sets up and cleans up exposure routes with the selected mode", async () => {
spawnMock
.mockReturnValueOnce(
@@ -127,7 +144,7 @@ describe("voice-call tailscale helpers", () => {
expect(spawnMock).toHaveBeenNthCalledWith(
1,
"tailscale",
["status", "--json"],
["status", "--json", "--peers=false"],
tailscaleSpawnOptions,
);
expect(spawnMock).toHaveBeenNthCalledWith(

View File

@@ -6,6 +6,30 @@ type TailscaleSelfInfo = {
nodeId: string | null;
};
export const TAILSCALE_COMMAND_STDOUT_MAX_BYTES = 4 * 1024 * 1024;
type TailscaleCommandStdout = {
bytes: number;
exceeded: boolean;
text: string;
};
export function appendTailscaleCommandStdout(
current: TailscaleCommandStdout,
data: Buffer | string,
maxBytes = TAILSCALE_COMMAND_STDOUT_MAX_BYTES,
): TailscaleCommandStdout {
if (current.exceeded) {
return current;
}
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
const bytes = current.bytes + buffer.byteLength;
if (bytes > maxBytes) {
return { bytes, exceeded: true, text: "" };
}
return { bytes, exceeded: false, text: `${current.text}${buffer.toString("utf8")}` };
}
function runTailscaleCommand(
args: string[],
timeoutMs = 2500,
@@ -15,7 +39,7 @@ function runTailscaleCommand(
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stdout: TailscaleCommandStdout = { bytes: 0, exceeded: false, text: "" };
let settled = false;
let timer: ReturnType<typeof setTimeout>;
const finish = (result: { code: number; stdout: string }) => {
@@ -28,7 +52,11 @@ function runTailscaleCommand(
};
proc.stdout.on("data", (data) => {
stdout += data;
stdout = appendTailscaleCommandStdout(stdout, data);
if (stdout.exceeded) {
proc.kill("SIGKILL");
finish({ code: -1, stdout: "" });
}
});
timer = setTimeout(() => {
@@ -41,13 +69,13 @@ function runTailscaleCommand(
});
proc.on("close", (code) => {
finish({ code: code ?? -1, stdout });
finish({ code: code ?? -1, stdout: stdout.text });
});
});
}
export async function getTailscaleSelfInfo(): Promise<TailscaleSelfInfo | null> {
const { code, stdout } = await runTailscaleCommand(["status", "--json"]);
const { code, stdout } = await runTailscaleCommand(["status", "--json", "--peers=false"]);
if (code !== 0) {
return null;
}