Fix terminal stream stalls after resize
This commit is contained in:
@@ -40,10 +40,6 @@ export type TerminalStreamControllerOptions = {
|
|||||||
|
|
||||||
const TERMINAL_EXITED_ERROR = "Terminal exited";
|
const TERMINAL_EXITED_ERROR = "Terminal exited";
|
||||||
|
|
||||||
function logTerminalStreamController(event: string, details: Record<string, unknown>): void {
|
|
||||||
console.log("[terminal-stream-controller]", event, details);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TerminalStreamController {
|
export class TerminalStreamController {
|
||||||
private readonly decoder = new TextDecoder();
|
private readonly decoder = new TextDecoder();
|
||||||
private readonly unsubscribeStreamEvents: () => void;
|
private readonly unsubscribeStreamEvents: () => void;
|
||||||
@@ -69,19 +65,10 @@ export class TerminalStreamController {
|
|||||||
|
|
||||||
setTerminal(input: { terminalId: string | null }): void {
|
setTerminal(input: { terminalId: string | null }): void {
|
||||||
if (this.disposed || input.terminalId === this.terminalId) {
|
if (this.disposed || input.terminalId === this.terminalId) {
|
||||||
logTerminalStreamController("set_terminal_noop", {
|
|
||||||
disposed: this.disposed,
|
|
||||||
currentTerminalId: this.terminalId,
|
|
||||||
requestedTerminalId: input.terminalId,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nextTerminalId = input.terminalId;
|
const nextTerminalId = input.terminalId;
|
||||||
const previousTerminalId = this.terminalId;
|
const previousTerminalId = this.terminalId;
|
||||||
logTerminalStreamController("set_terminal", {
|
|
||||||
previousTerminalId,
|
|
||||||
nextTerminalId,
|
|
||||||
});
|
|
||||||
this.terminalId = nextTerminalId;
|
this.terminalId = nextTerminalId;
|
||||||
this.decoder.decode();
|
this.decoder.decode();
|
||||||
if (previousTerminalId) {
|
if (previousTerminalId) {
|
||||||
@@ -95,11 +82,6 @@ export class TerminalStreamController {
|
|||||||
void this.options.client
|
void this.options.client
|
||||||
.subscribeTerminal(nextTerminalId)
|
.subscribeTerminal(nextTerminalId)
|
||||||
.then((payload) => {
|
.then((payload) => {
|
||||||
logTerminalStreamController("subscribe_resolved", {
|
|
||||||
nextTerminalId,
|
|
||||||
payloadTerminalId: payload.terminalId,
|
|
||||||
error: payload.error ?? null,
|
|
||||||
});
|
|
||||||
if (this.disposed || this.terminalId !== nextTerminalId) {
|
if (this.disposed || this.terminalId !== nextTerminalId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -113,10 +95,6 @@ export class TerminalStreamController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const preferredSize = this.options.getPreferredSize();
|
const preferredSize = this.options.getPreferredSize();
|
||||||
logTerminalStreamController("preferred_size_after_subscribe", {
|
|
||||||
nextTerminalId,
|
|
||||||
preferredSize,
|
|
||||||
});
|
|
||||||
if (preferredSize) {
|
if (preferredSize) {
|
||||||
this.options.client.sendTerminalInput(nextTerminalId, {
|
this.options.client.sendTerminalInput(nextTerminalId, {
|
||||||
type: "resize",
|
type: "resize",
|
||||||
@@ -131,10 +109,6 @@ export class TerminalStreamController {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
logTerminalStreamController("subscribe_rejected", {
|
|
||||||
nextTerminalId,
|
|
||||||
error: error instanceof Error ? error.message : "Unable to subscribe to terminal",
|
|
||||||
});
|
|
||||||
if (this.disposed || this.terminalId !== nextTerminalId) {
|
if (this.disposed || this.terminalId !== nextTerminalId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -148,10 +122,6 @@ export class TerminalStreamController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleTerminalExit(input: { terminalId: string }): void {
|
handleTerminalExit(input: { terminalId: string }): void {
|
||||||
logTerminalStreamController("terminal_exit", {
|
|
||||||
currentTerminalId: this.terminalId,
|
|
||||||
exitedTerminalId: input.terminalId,
|
|
||||||
});
|
|
||||||
if (this.disposed || input.terminalId !== this.terminalId) {
|
if (this.disposed || input.terminalId !== this.terminalId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -168,9 +138,6 @@ export class TerminalStreamController {
|
|||||||
if (this.disposed) {
|
if (this.disposed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logTerminalStreamController("dispose", {
|
|
||||||
terminalId: this.terminalId,
|
|
||||||
});
|
|
||||||
this.disposed = true;
|
this.disposed = true;
|
||||||
this.decoder.decode();
|
this.decoder.decode();
|
||||||
const terminalId = this.terminalId;
|
const terminalId = this.terminalId;
|
||||||
|
|||||||
@@ -626,6 +626,45 @@ describe("daemon E2E terminal", () => {
|
|||||||
rmSync(cwd, { recursive: true, force: true });
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
|
test("resize does not stall streamed output for an attached client", async () => {
|
||||||
|
const cwd = tmpCwd();
|
||||||
|
const created = await ctx.client.createTerminal(cwd);
|
||||||
|
const terminalId = created.terminal!.id;
|
||||||
|
const secondClient = await connectClient(
|
||||||
|
ctx.daemon.port,
|
||||||
|
`terminal-resize-stream-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ctx.client.subscribeTerminal(terminalId);
|
||||||
|
await secondClient.subscribeTerminal(terminalId);
|
||||||
|
|
||||||
|
secondClient.sendTerminalInput(terminalId, {
|
||||||
|
type: "resize",
|
||||||
|
rows: 12,
|
||||||
|
cols: 40,
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstOutput = waitForTerminalOutput(ctx.client, terminalId, (text) =>
|
||||||
|
text.includes("after-resize-stream"),
|
||||||
|
);
|
||||||
|
const secondOutput = waitForTerminalOutput(secondClient, terminalId, (text) =>
|
||||||
|
text.includes("after-resize-stream"),
|
||||||
|
);
|
||||||
|
|
||||||
|
secondClient.sendTerminalInput(terminalId, {
|
||||||
|
type: "input",
|
||||||
|
data: "printf 'after-resize-stream\\n'\r",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await firstOutput).toContain("after-resize-stream");
|
||||||
|
expect(await secondOutput).toContain("after-resize-stream");
|
||||||
|
} finally {
|
||||||
|
await secondClient.close();
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
test("terminal exits notify the client", async () => {
|
test("terminal exits notify the client", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
const created = await ctx.client.createTerminal(cwd);
|
const created = await ctx.client.createTerminal(cwd);
|
||||||
|
|||||||
@@ -1817,7 +1817,6 @@ export class Session {
|
|||||||
if (!resize) {
|
if (!resize) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activeStream.needsSnapshot = true;
|
|
||||||
terminal.send({ type: "resize", rows: resize.rows, cols: resize.cols });
|
terminal.send({ type: "resize", rows: resize.rows, cols: resize.cols });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -7859,6 +7858,7 @@ export class Session {
|
|||||||
this.markAllActiveTerminalStreamsForSnapshot();
|
this.markAllActiveTerminalStreamsForSnapshot();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.trySendTerminalSnapshot(activeStream);
|
||||||
return slot;
|
return slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user