fix: suppress terminal query response leaks
This commit is contained in:
@@ -24,6 +24,7 @@ interface MountedTerminal {
|
|||||||
host: HTMLDivElement;
|
host: HTMLDivElement;
|
||||||
root: HTMLDivElement;
|
root: HTMLDivElement;
|
||||||
runtime: TerminalEmulatorRuntime;
|
runtime: TerminalEmulatorRuntime;
|
||||||
|
inputs: string[];
|
||||||
sizes: TerminalSize[];
|
sizes: TerminalSize[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,9 +66,13 @@ function createTerminalHost(input: { width: number; height: number }): MountedTe
|
|||||||
document.body.appendChild(root);
|
document.body.appendChild(root);
|
||||||
|
|
||||||
const sizes: TerminalSize[] = [];
|
const sizes: TerminalSize[] = [];
|
||||||
|
const inputs: string[] = [];
|
||||||
const runtime = new TerminalEmulatorRuntime();
|
const runtime = new TerminalEmulatorRuntime();
|
||||||
runtime.setCallbacks({
|
runtime.setCallbacks({
|
||||||
callbacks: {
|
callbacks: {
|
||||||
|
onInput: (data) => {
|
||||||
|
inputs.push(data);
|
||||||
|
},
|
||||||
onResize: (size) => {
|
onResize: (size) => {
|
||||||
sizes.push(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);
|
mountedTerminals.push(mounted);
|
||||||
return mounted;
|
return mounted;
|
||||||
}
|
}
|
||||||
@@ -157,6 +162,22 @@ describe("terminal emulator runtime in a real browser", () => {
|
|||||||
expect(refreshCalls.at(-1)).toEqual([0, terminal.rows - 1]);
|
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 () => {
|
it("replays snapshots without synchronously resetting the visible terminal", async () => {
|
||||||
await page.viewport(900, 600);
|
await page.viewport(900, 600);
|
||||||
const mounted = createTerminalHost({ width: 720, height: 360 });
|
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.
|
// Suppress cursor/status query responses whose async browser replies can race
|
||||||
// Without this, xterm.js generates DA/CPR responses via onData that feed back
|
// with foreground app exit and feed back to the shell as visible text. DA is
|
||||||
// to the PTY as visible text.
|
// intentionally left to xterm so attached TUIs can identify the terminal.
|
||||||
terminal.parser.registerCsiHandler({ final: "c" }, () => true);
|
terminal.parser.registerCsiHandler({ final: "n" }, () => true);
|
||||||
|
terminal.parser.registerCsiHandler({ prefix: "?", final: "n" }, () => true);
|
||||||
terminal.parser.registerCsiHandler({ final: "R" }, () => 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({
|
const restoreDocumentStyles = this.applyDocumentBoundsStyles({
|
||||||
root: input.root,
|
root: input.root,
|
||||||
|
|||||||
@@ -978,6 +978,23 @@ describe("subscribe", () => {
|
|||||||
|
|
||||||
expect(messages.length).toBe(0);
|
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", () => {
|
describe("stream snapshots", () => {
|
||||||
|
|||||||
@@ -657,15 +657,6 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
|
|||||||
}
|
}
|
||||||
emitTitleChange(initialTitle);
|
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;
|
let disposeTitleChangeSubscription: { dispose(): void } | null = null;
|
||||||
if (!lockedTitle) {
|
if (!lockedTitle) {
|
||||||
disposeTitleChangeSubscription = terminal.onTitleChange((nextTitle) => {
|
disposeTitleChangeSubscription = terminal.onTitleChange((nextTitle) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user