Skip to content

Commit 6678e32

Browse files
committed
fix(web): proxy terminal websocket through browser api
1 parent d944590 commit 6678e32

3 files changed

Lines changed: 79 additions & 50 deletions

File tree

packages/app/src/web/terminal-panel-runtime.ts

Lines changed: 76 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,9 @@ import { parseTerminalServerMessage, resolveTerminalWebSocketUrl } from "./termi
99

1010
export type TerminalStatus = "attached" | "connecting" | "error" | "exited"
1111

12-
export type TerminalConnectionState = {
13-
opened: boolean
14-
}
12+
export type TerminalConnectionState = { opened: boolean }
1513

16-
type TerminalRuntime = {
17-
readonly fitAddon: FitAddon
18-
readonly terminal: Terminal
19-
}
14+
type TerminalRuntime = { readonly fitAddon: FitAddon; readonly terminal: Terminal }
2015

2116
type TerminalMessageHandlers = {
2217
readonly notifyMessage: (message: string) => void
@@ -41,6 +36,33 @@ type TerminalLifecycleArgs = {
4136
readonly setStatus: (status: TerminalStatus) => void
4237
}
4338

39+
type TerminalSocketListenerArgs = {
40+
readonly connectionRef: { current: TerminalConnectionState }
41+
readonly onClose: () => void
42+
readonly onError: () => void
43+
readonly onMessage: (payload: string) => void
44+
readonly onOpen: () => void
45+
readonly socket: WebSocket
46+
}
47+
48+
type TerminalSocketFailureHandlerArgs = {
49+
readonly notifyMessage: (message: string) => void
50+
readonly setStatus: (status: TerminalStatus) => void
51+
readonly terminal: Terminal
52+
readonly terminalLine: string
53+
readonly uiMessage: string
54+
}
55+
56+
type TerminalSessionSocketArgs = {
57+
readonly connectionRef: { current: TerminalConnectionState }
58+
readonly handlers: TerminalMessageHandlers
59+
readonly notifyMessage: (message: string) => void
60+
readonly sendResize: () => void
61+
readonly setStatus: (status: TerminalStatus) => void
62+
readonly socket: WebSocket
63+
readonly terminal: Terminal
64+
}
65+
4466
const requestSessionClose = (closePath: string): void => {
4567
void Effect.runPromise(deleteTerminalSessionByPath(closePath).pipe(Effect.either, Effect.asVoid))
4668
}
@@ -67,14 +89,7 @@ const createTerminalRuntime = (host: HTMLDivElement): TerminalRuntime => {
6789
const createTerminalSocket = (
6890
session: ActiveTerminalSession,
6991
terminal: Terminal
70-
): WebSocket =>
71-
new WebSocket(
72-
resolveTerminalWebSocketUrl(
73-
session.websocketPath,
74-
terminal.cols,
75-
terminal.rows
76-
)
77-
)
92+
): WebSocket => new WebSocket(resolveTerminalWebSocketUrl(session.websocketPath, terminal.cols, terminal.rows))
7893

7994
const sendTerminalResize = (
8095
fitAddon: FitAddon,
@@ -99,9 +114,7 @@ const observeTerminalResize = (
99114
if (typeof ResizeObserver !== "function") {
100115
return null
101116
}
102-
const resizeObserver = new ResizeObserver(() => {
103-
onResize()
104-
})
117+
const resizeObserver = new ResizeObserver(onResize)
105118
resizeObserver.observe(host)
106119
return resizeObserver
107120
}
@@ -151,11 +164,7 @@ const handleTerminalServerMessage = (
151164
}
152165

153166
const attachTerminalSocketListeners = (
154-
connectionRef: { current: TerminalConnectionState },
155-
socket: WebSocket,
156-
onOpen: () => void,
157-
onMessage: (payload: string) => void,
158-
onError: () => void
167+
{ connectionRef, onClose, onError, onMessage, onOpen, socket }: TerminalSocketListenerArgs
159168
): void => {
160169
socket.addEventListener("open", () => {
161170
connectionRef.current.opened = true
@@ -164,6 +173,11 @@ const attachTerminalSocketListeners = (
164173
socket.addEventListener("message", (event) => {
165174
onMessage(typeof event.data === "string" ? event.data : "")
166175
})
176+
socket.addEventListener("close", () => {
177+
if (!connectionRef.current.opened) {
178+
onClose()
179+
}
180+
})
167181
socket.addEventListener("error", onError)
168182
}
169183

@@ -192,15 +206,13 @@ const createMessageHandlers = (
192206
terminal
193207
})
194208

195-
const createSocketErrorHandler = (
196-
notifyMessage: (message: string) => void,
197-
setStatus: (status: TerminalStatus) => void,
198-
terminal: Terminal
209+
const createSocketFailureHandler = (
210+
{ notifyMessage, setStatus, terminal, terminalLine, uiMessage }: TerminalSocketFailureHandlerArgs
199211
) =>
200-
() => {
201-
terminal.writeln("\r\n[websocket error]")
212+
(): void => {
213+
terminal.writeln(`\r\n${terminalLine}`)
202214
setStatus("error")
203-
notifyMessage("Terminal websocket error.")
215+
notifyMessage(uiMessage)
204216
}
205217

206218
const maybeDeletePendingSession = (
@@ -215,6 +227,33 @@ const maybeDeletePendingSession = (
215227
}
216228
}
217229

230+
const attachTerminalSessionSocket = (
231+
{ connectionRef, handlers, notifyMessage, sendResize, setStatus, socket, terminal }: TerminalSessionSocketArgs
232+
): void => {
233+
attachTerminalSocketListeners({
234+
connectionRef,
235+
onClose: createSocketFailureHandler({
236+
notifyMessage,
237+
setStatus,
238+
terminal,
239+
terminalLine: "[websocket closed before attach]",
240+
uiMessage: "Terminal websocket closed before attach."
241+
}),
242+
onError: createSocketFailureHandler({
243+
notifyMessage,
244+
setStatus,
245+
terminal,
246+
terminalLine: "[websocket error]",
247+
uiMessage: "Terminal websocket error."
248+
}),
249+
onMessage: (payload) => {
250+
handleTerminalServerMessage(handlers, payload)
251+
},
252+
onOpen: sendResize,
253+
socket
254+
})
255+
}
256+
218257
const mountTerminalSession = (
219258
{ connectionRef, hostRef, notifyMessage, session, setStatus }: TerminalLifecycleArgs
220259
): (() => void) | undefined => {
@@ -239,15 +278,15 @@ const mountTerminalSession = (
239278
)
240279

241280
globalThis.addEventListener("resize", sendResize)
242-
attachTerminalSocketListeners(
281+
attachTerminalSessionSocket({
243282
connectionRef,
244-
socket,
283+
handlers,
284+
notifyMessage,
245285
sendResize,
246-
(payload) => {
247-
handleTerminalServerMessage(handlers, payload)
248-
},
249-
createSocketErrorHandler(notifyMessage, setStatus, terminal)
250-
)
286+
setStatus,
287+
socket,
288+
terminal
289+
})
251290

252291
return () => {
253292
cleanupTerminalResources({

packages/app/src/web/terminal.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,7 @@ const resolveTerminalApiBaseUrl = (): string => {
2525
return trimTrailingSlash(configured.trim())
2626
}
2727

28-
const apiBaseUrl = resolveApiBaseUrl()
29-
if (apiBaseUrl.startsWith("http://") || apiBaseUrl.startsWith("https://")) {
30-
return apiBaseUrl
31-
}
32-
33-
if (globalThis.location.protocol === "http:") {
34-
const apiPort = import.meta.env.VITE_DOCKER_GIT_TERMINAL_API_PORT?.trim() || "3334"
35-
return `http://${globalThis.location.hostname}:${apiPort}`
36-
}
37-
38-
return apiBaseUrl
28+
return resolveApiBaseUrl()
3929
}
4030

4131
const resolveApiUrl = (): URL => {

packages/app/tests/docker-git/terminal.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe("browser terminal helpers", () => {
3838
)
3939
})
4040

41-
it("falls back to direct local api origin for relative browser api paths", () => {
41+
it("uses same-origin api proxy for relative browser api paths", () => {
4242
const host = "terminal.example.local"
4343
const httpProtocol = ["ht", "tp:"].join("")
4444
const wsProtocol = ["ws", "://"].join("")
@@ -53,7 +53,7 @@ describe("browser terminal helpers", () => {
5353
expect(resolveTerminalWebSocketUrl("/projects/proj/terminal-sessions/sess/ws", 80, 24)).toBe([
5454
wsProtocol,
5555
host,
56-
":3334/projects/proj/terminal-sessions/sess/ws?cols=80&rows=24"
56+
":4176/api/projects/proj/terminal-sessions/sess/ws?cols=80&rows=24"
5757
].join(""))
5858
})
5959

0 commit comments

Comments
 (0)