fix: suppress terminal query response leaks
This commit is contained in:
@@ -24,6 +24,7 @@ interface MountedTerminal {
|
||||
host: HTMLDivElement;
|
||||
root: HTMLDivElement;
|
||||
runtime: TerminalEmulatorRuntime;
|
||||
inputs: string[];
|
||||
sizes: TerminalSize[];
|
||||
}
|
||||
|
||||
@@ -65,9 +66,13 @@ function createTerminalHost(input: { width: number; height: number }): MountedTe
|
||||
document.body.appendChild(root);
|
||||
|
||||
const sizes: TerminalSize[] = [];
|
||||
const inputs: string[] = [];
|
||||
const runtime = new TerminalEmulatorRuntime();
|
||||
runtime.setCallbacks({
|
||||
callbacks: {
|
||||
onInput: (data) => {
|
||||
inputs.push(data);
|
||||
},
|
||||
onResize: (size) => {
|
||||
sizes.push(size);
|
||||
},
|
||||
@@ -84,7 +89,7 @@ function createTerminalHost(input: { width: number; height: number }): MountedTe
|
||||
},
|
||||
});
|
||||
|
||||
const mounted = { host, root, runtime, sizes };
|
||||
const mounted = { host, root, runtime, inputs, sizes };
|
||||
mountedTerminals.push(mounted);
|
||||
return mounted;
|
||||
}
|
||||
@@ -157,6 +162,22 @@ describe("terminal emulator runtime in a real browser", () => {
|
||||
expect(refreshCalls.at(-1)).toEqual([0, terminal.rows - 1]);
|
||||
});
|
||||
|
||||
it("suppresses non-identity terminal query responses as PTY input", async () => {
|
||||
await page.viewport(900, 600);
|
||||
const mounted = createTerminalHost({ width: 720, height: 360 });
|
||||
|
||||
await waitFor({ predicate: () => mounted.sizes.length > 0 });
|
||||
|
||||
const querySequences = ["\x1b[5n", "\x1b[6n", "\x1b[?6n", "\x1b[1$p", "\x1b[?1$p"];
|
||||
|
||||
for (const sequence of querySequences) {
|
||||
mounted.runtime.write({ text: sequence });
|
||||
}
|
||||
await nextFrame();
|
||||
|
||||
expect(mounted.inputs).toEqual([]);
|
||||
});
|
||||
|
||||
it("replays snapshots without synchronously resetting the visible terminal", async () => {
|
||||
await page.viewport(900, 600);
|
||||
const mounted = createTerminalHost({ width: 720, height: 360 });
|
||||
|
||||
@@ -237,11 +237,14 @@ export class TerminalEmulatorRuntime {
|
||||
}
|
||||
});
|
||||
|
||||
// Suppress terminal query responses — the server-side headless xterm handles these.
|
||||
// Without this, xterm.js generates DA/CPR responses via onData that feed back
|
||||
// to the PTY as visible text.
|
||||
terminal.parser.registerCsiHandler({ final: "c" }, () => true);
|
||||
// Suppress cursor/status query responses whose async browser replies can race
|
||||
// with foreground app exit and feed back to the shell as visible text. DA is
|
||||
// intentionally left to xterm so attached TUIs can identify the terminal.
|
||||
terminal.parser.registerCsiHandler({ final: "n" }, () => true);
|
||||
terminal.parser.registerCsiHandler({ prefix: "?", final: "n" }, () => true);
|
||||
terminal.parser.registerCsiHandler({ final: "R" }, () => true);
|
||||
terminal.parser.registerCsiHandler({ intermediates: "$", final: "p" }, () => true);
|
||||
terminal.parser.registerCsiHandler({ prefix: "?", intermediates: "$", final: "p" }, () => true);
|
||||
|
||||
const restoreDocumentStyles = this.applyDocumentBoundsStyles({
|
||||
root: input.root,
|
||||
|
||||
@@ -978,6 +978,23 @@ describe("subscribe", () => {
|
||||
|
||||
expect(messages.length).toBe(0);
|
||||
});
|
||||
|
||||
it("does not answer terminal identity queries from the headless snapshot parser", async () => {
|
||||
const session = trackSession(
|
||||
await createTerminal({
|
||||
cwd: "/tmp",
|
||||
shell: "/bin/sh",
|
||||
env: { PS1: "$ " },
|
||||
}),
|
||||
);
|
||||
|
||||
await waitForLines(session, ["$"]);
|
||||
|
||||
session.send({ type: "input", data: "printf '\\033[c'; echo after-da\r" });
|
||||
await waitForState(session, (state) => getLines(state).join("\n").includes("after-da"));
|
||||
|
||||
expect(getLines(session.getState()).join("\n")).not.toContain("62;4;22c");
|
||||
});
|
||||
});
|
||||
|
||||
describe("stream snapshots", () => {
|
||||
|
||||
@@ -657,15 +657,6 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
|
||||
}
|
||||
emitTitleChange(initialTitle);
|
||||
|
||||
// Respond to DA1 queries (CSI c or CSI 0 c) — apps like nvim query terminal capabilities
|
||||
terminal.parser.registerCsiHandler({ final: "c" }, (params) => {
|
||||
if (params.length === 0 || (params.length === 1 && params[0] === 0)) {
|
||||
ptyProcess.write("\x1b[?62;4;22c");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
let disposeTitleChangeSubscription: { dispose(): void } | null = null;
|
||||
if (!lockedTitle) {
|
||||
disposeTitleChangeSubscription = terminal.onTitleChange((nextTitle) => {
|
||||
|
||||
Reference in New Issue
Block a user