fix(app): terminal issues (#14435)
This commit is contained in:
@@ -6,6 +6,7 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes
|
|||||||
await gotoSession()
|
await gotoSession()
|
||||||
|
|
||||||
const terminals = page.locator(terminalSelector)
|
const terminals = page.locator(terminalSelector)
|
||||||
|
const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]')
|
||||||
const opened = await terminals.first().isVisible()
|
const opened = await terminals.first().isVisible()
|
||||||
|
|
||||||
if (!opened) {
|
if (!opened) {
|
||||||
@@ -21,6 +22,7 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes
|
|||||||
await page.locator(promptSelector).click()
|
await page.locator(promptSelector).click()
|
||||||
await page.keyboard.press("Control+Alt+T")
|
await page.keyboard.press("Control+Alt+T")
|
||||||
|
|
||||||
await expect(terminals).toHaveCount(2)
|
await expect(tabs).toHaveCount(2)
|
||||||
await expect(terminals.nth(1).locator("textarea")).toHaveCount(1)
|
await expect(terminals).toHaveCount(1)
|
||||||
|
await expect(terminals.first().locator("textarea")).toHaveCount(1)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -540,7 +540,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
disposed = true
|
disposed = true
|
||||||
if (fitFrame !== undefined) cancelAnimationFrame(fitFrame)
|
if (fitFrame !== undefined) cancelAnimationFrame(fitFrame)
|
||||||
if (sizeTimer !== undefined) clearTimeout(sizeTimer)
|
if (sizeTimer !== undefined) clearTimeout(sizeTimer)
|
||||||
if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close()
|
if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close(1000)
|
||||||
|
|
||||||
const finalize = () => {
|
const finalize = () => {
|
||||||
persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup })
|
persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup })
|
||||||
|
|||||||
@@ -67,11 +67,11 @@ export function TerminalPanel() {
|
|||||||
on(
|
on(
|
||||||
() => terminal.active(),
|
() => terminal.active(),
|
||||||
(activeId) => {
|
(activeId) => {
|
||||||
if (!activeId || !opened()) return
|
if (!activeId || !open()) return
|
||||||
if (document.activeElement instanceof HTMLElement) {
|
if (document.activeElement instanceof HTMLElement) {
|
||||||
document.activeElement.blur()
|
document.activeElement.blur()
|
||||||
}
|
}
|
||||||
focusTerminalById(activeId)
|
setTimeout(() => focusTerminalById(activeId), 0)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -209,21 +209,17 @@ export function TerminalPanel() {
|
|||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<div class="flex-1 min-h-0 relative">
|
<div class="flex-1 min-h-0 relative">
|
||||||
<For each={all()}>
|
<Show when={terminal.active()} keyed>
|
||||||
{(pty) => (
|
{(id) => (
|
||||||
<div
|
<Show when={byId().get(id)}>
|
||||||
id={`terminal-wrapper-${pty.id}`}
|
{(pty) => (
|
||||||
class="absolute inset-0"
|
<div id={`terminal-wrapper-${id}`} class="absolute inset-0">
|
||||||
style={{
|
<Terminal pty={pty()} onCleanup={terminal.update} onConnectError={() => terminal.clone(id)} />
|
||||||
display: terminal.active() === pty.id ? "block" : "none",
|
</div>
|
||||||
}}
|
)}
|
||||||
>
|
</Show>
|
||||||
<Show when={pty.id} keyed>
|
|
||||||
<Terminal pty={pty} onCleanup={terminal.update} onConnectError={() => terminal.clone(pty.id)} />
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</For>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DragOverlay>
|
<DragOverlay>
|
||||||
|
|||||||
@@ -41,13 +41,38 @@ export namespace Pty {
|
|||||||
|
|
||||||
const token = (ws: Socket) => {
|
const token = (ws: Socket) => {
|
||||||
const data = ws.data
|
const data = ws.data
|
||||||
if (!data || typeof data !== "object") return
|
if (data === undefined) return
|
||||||
|
if (data === null) return
|
||||||
|
if (typeof data !== "object") return data
|
||||||
|
|
||||||
const events = (data as { events?: unknown }).events
|
const id = (data as { connId?: unknown }).connId
|
||||||
if (events && typeof events === "object") return events
|
if (typeof id === "number" || typeof id === "string") return id
|
||||||
|
|
||||||
|
const href = (data as { href?: unknown }).href
|
||||||
|
if (typeof href === "string") return href
|
||||||
|
|
||||||
const url = (data as { url?: unknown }).url
|
const url = (data as { url?: unknown }).url
|
||||||
if (url && typeof url === "object") return url
|
if (typeof url === "string") return url
|
||||||
|
if (url && typeof url === "object") {
|
||||||
|
const href = (url as { href?: unknown }).href
|
||||||
|
if (typeof href === "string") return href
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = (data as { events?: unknown }).events
|
||||||
|
if (typeof events === "number" || typeof events === "string") return events
|
||||||
|
if (events && typeof events === "object") {
|
||||||
|
const id = (events as { connId?: unknown }).connId
|
||||||
|
if (typeof id === "number" || typeof id === "string") return id
|
||||||
|
|
||||||
|
const id2 = (events as { connection?: unknown }).connection
|
||||||
|
if (typeof id2 === "number" || typeof id2 === "string") return id2
|
||||||
|
|
||||||
|
const id3 = (events as { id?: unknown }).id
|
||||||
|
if (typeof id3 === "number" || typeof id3 === "string") return id3
|
||||||
|
|
||||||
|
return events
|
||||||
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
@@ -210,7 +235,7 @@ export namespace Pty {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub.token !== undefined && token(ws) !== sub.token) {
|
if (token(ws) !== sub.token) {
|
||||||
session.subscribers.delete(ws)
|
session.subscribers.delete(ws)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,4 +97,48 @@ describe("pty", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("does not leak output when socket data mutates in-place", async () => {
|
||||||
|
await using dir = await tmpdir({ git: true })
|
||||||
|
|
||||||
|
await Instance.provide({
|
||||||
|
directory: dir.path,
|
||||||
|
fn: async () => {
|
||||||
|
const a = await Pty.create({ command: "cat", title: "a" })
|
||||||
|
try {
|
||||||
|
const outA: string[] = []
|
||||||
|
const outB: string[] = []
|
||||||
|
|
||||||
|
const ctx = { connId: 1 }
|
||||||
|
const ws = {
|
||||||
|
readyState: 1,
|
||||||
|
data: ctx,
|
||||||
|
send: (data: unknown) => {
|
||||||
|
outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
|
||||||
|
},
|
||||||
|
close: () => {
|
||||||
|
// no-op
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Pty.connect(a.id, ws as any)
|
||||||
|
outA.length = 0
|
||||||
|
|
||||||
|
// Simulate the runtime mutating per-connection data without
|
||||||
|
// swapping the reference (ws.data stays the same object).
|
||||||
|
ctx.connId = 2
|
||||||
|
ws.send = (data: unknown) => {
|
||||||
|
outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8"))
|
||||||
|
}
|
||||||
|
|
||||||
|
Pty.write(a.id, "AAA\n")
|
||||||
|
await Bun.sleep(100)
|
||||||
|
|
||||||
|
expect(outB.join("")).not.toContain("AAA")
|
||||||
|
} finally {
|
||||||
|
await Pty.remove(a.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user