Skip to content

Commit 2d4b8f9

Browse files
committed
Remove terminalXService
This is a prelude to the change I actually want to make, which is to send the size of the terminal on the web socket URL after we do a fit. I have found xstate so confusing that it was easier to just rewrite it.
1 parent 1b99882 commit 2d4b8f9

File tree

2 files changed

+135
-423
lines changed

2 files changed

+135
-423
lines changed

site/src/pages/TerminalPage/TerminalPage.tsx

Lines changed: 135 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { makeStyles, useTheme } from "@mui/styles";
2-
import { useMachine } from "@xstate/react";
32
import { FC, useCallback, useEffect, useRef, useState } from "react";
43
import { Helmet } from "react-helmet-async";
54
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
@@ -14,14 +13,15 @@ import { Unicode11Addon } from "xterm-addon-unicode11";
1413
import "xterm/css/xterm.css";
1514
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
1615
import { pageTitle } from "utils/page";
17-
import { terminalMachine } from "xServices/terminal/terminalXService";
1816
import { useProxy } from "contexts/ProxyContext";
1917
import Box from "@mui/material/Box";
2018
import { useDashboard } from "components/Dashboard/DashboardProvider";
2119
import { Region } from "api/typesGenerated";
2220
import { getLatencyColor } from "utils/latency";
2321
import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency";
2422
import { portForwardURL } from "utils/portForward";
23+
import { terminalWebsocketUrl } from "utils/terminal";
24+
import { getMatchingAgentOrFirst } from "utils/workspace";
2525
import {
2626
DisconnectedAlert,
2727
ErrorScriptAlert,
@@ -30,6 +30,7 @@ import {
3030
} from "./TerminalAlerts";
3131
import { useQuery } from "react-query";
3232
import { deploymentConfig } from "api/queries/deployment";
33+
import { workspaceByOwnerAndName } from "api/queries/workspaces";
3334
import {
3435
Popover,
3536
PopoverContent,
@@ -48,9 +49,11 @@ const TerminalPage: FC = () => {
4849
const { proxy } = useProxy();
4950
const params = useParams() as { username: string; workspace: string };
5051
const username = params.username.replace("@", "");
51-
const workspaceName = params.workspace;
5252
const xtermRef = useRef<HTMLDivElement>(null);
5353
const [terminal, setTerminal] = useState<XTerm.Terminal | null>(null);
54+
const [terminalState, setTerminalState] = useState<
55+
"connected" | "disconnected" | "initializing"
56+
>("initializing");
5457
const [fitAddon, setFitAddon] = useState<FitAddon | null>(null);
5558
const [searchParams] = useSearchParams();
5659
// The reconnection token is a unique token that identifies
@@ -60,37 +63,13 @@ const TerminalPage: FC = () => {
6063
const command = searchParams.get("command") || undefined;
6164
// The workspace name is in the format:
6265
// <workspace name>[.<agent name>]
63-
const workspaceNameParts = workspaceName?.split(".");
64-
const [terminalState, sendEvent] = useMachine(terminalMachine, {
65-
context: {
66-
agentName: workspaceNameParts?.[1],
67-
reconnection: reconnectionToken,
68-
workspaceName: workspaceNameParts?.[0],
69-
username: username,
70-
command: command,
71-
baseURL: proxy.preferredPathAppURL,
72-
},
73-
actions: {
74-
readMessage: (_, event) => {
75-
if (typeof event.data === "string") {
76-
// This exclusively occurs when testing.
77-
// "jest-websocket-mock" doesn't support ArrayBuffer.
78-
terminal?.write(event.data);
79-
} else {
80-
terminal?.write(new Uint8Array(event.data));
81-
}
82-
},
83-
},
84-
});
85-
const isConnected = terminalState.matches("connected");
86-
const isDisconnected = terminalState.matches("disconnected");
87-
const {
88-
workspaceError,
89-
workspace,
90-
workspaceAgentError,
91-
workspaceAgent,
92-
websocketError,
93-
} = terminalState.context;
66+
const workspaceNameParts = params.workspace?.split(".");
67+
const workspace = useQuery(
68+
workspaceByOwnerAndName(username, workspaceNameParts?.[0]),
69+
);
70+
const workspaceAgent = workspace.data
71+
? getMatchingAgentOrFirst(workspace.data, workspaceNameParts?.[1])
72+
: undefined;
9473
const dashboard = useDashboard();
9574
const proxyContext = useProxy();
9675
const selectedProxy = proxyContext.proxy.proxy;
@@ -111,7 +90,7 @@ const TerminalPage: FC = () => {
11190
(uri: string) => {
11291
if (
11392
!workspaceAgent ||
114-
!workspace ||
93+
!workspace.data ||
11594
!username ||
11695
!proxy.preferredWildcardHostname
11796
) {
@@ -145,15 +124,15 @@ const TerminalPage: FC = () => {
145124
proxy.preferredWildcardHostname,
146125
parseInt(url.port),
147126
workspaceAgent.name,
148-
workspace.name,
127+
workspace.data.name,
149128
username,
150129
) + url.pathname,
151130
);
152131
} catch (ex) {
153132
open(uri);
154133
}
155134
},
156-
[workspaceAgent, workspace, username, proxy.preferredWildcardHostname],
135+
[workspaceAgent, workspace.data, username, proxy.preferredWildcardHostname],
157136
);
158137

159138
// Create the terminal!
@@ -186,23 +165,6 @@ const TerminalPage: FC = () => {
186165
handleWebLink(uri);
187166
}),
188167
);
189-
terminal.onData((data) => {
190-
sendEvent({
191-
type: "WRITE",
192-
request: {
193-
data: data,
194-
},
195-
});
196-
});
197-
terminal.onResize((event) => {
198-
sendEvent({
199-
type: "WRITE",
200-
request: {
201-
height: event.rows,
202-
width: event.cols,
203-
},
204-
});
205-
});
206168
setTerminal(terminal);
207169
terminal.open(xtermRef.current);
208170
const listener = () => {
@@ -214,11 +176,9 @@ const TerminalPage: FC = () => {
214176
window.removeEventListener("resize", listener);
215177
terminal.dispose();
216178
};
217-
}, [config.data, config.isLoading, sendEvent, xtermRef, handleWebLink]);
179+
}, [config.data, config.isLoading, xtermRef, handleWebLink]);
218180

219-
// Triggers the initial terminal connection using
220-
// the reconnection token and workspace name found
221-
// from the router.
181+
// Updates the reconnection token into the URL if necessary.
222182
useEffect(() => {
223183
if (searchParams.get("reconnect") === reconnectionToken) {
224184
return;
@@ -234,7 +194,7 @@ const TerminalPage: FC = () => {
234194
);
235195
}, [searchParams, navigate, reconnectionToken]);
236196

237-
// Apply terminal options based on connection state.
197+
// Hook up the terminal through a web socket.
238198
useEffect(() => {
239199
if (!terminal || !fitAddon) {
240200
return;
@@ -246,68 +206,136 @@ const TerminalPage: FC = () => {
246206
fitAddon.fit();
247207
fitAddon.fit();
248208

249-
if (!isConnected) {
250-
// Disable user input when not connected.
251-
terminal.options = {
252-
disableStdin: true,
253-
};
254-
if (workspaceError instanceof Error) {
255-
terminal.writeln(
256-
Language.workspaceErrorMessagePrefix + workspaceError.message,
257-
);
258-
}
259-
if (workspaceAgentError instanceof Error) {
260-
terminal.writeln(
261-
Language.workspaceAgentErrorMessagePrefix +
262-
workspaceAgentError.message,
263-
);
264-
}
265-
if (websocketError instanceof Error) {
266-
terminal.writeln(
267-
Language.websocketErrorMessagePrefix + websocketError.message,
268-
);
269-
}
270-
return;
271-
}
272-
273209
// The terminal should be cleared on each reconnect
274210
// because all data is re-rendered from the backend.
275211
terminal.clear();
276212

277-
// Focusing on connection allows users to reload the
278-
// page and start typing immediately.
213+
// Focusing on connection allows users to reload the page and start
214+
// typing immediately.
279215
terminal.focus();
280-
terminal.options = {
281-
disableStdin: false,
282-
windowsMode: workspaceAgent?.operating_system === "windows",
283-
};
284216

285-
// Update the terminal size post-fit.
286-
sendEvent({
287-
type: "WRITE",
288-
request: {
289-
height: terminal.rows,
290-
width: terminal.cols,
291-
},
292-
});
217+
// Disable input while we connect.
218+
terminal.options.disableStdin = true;
219+
220+
// Show a message if we failed to find the workspace or agent.
221+
if (workspace.isLoading) {
222+
return;
223+
} else if (workspace.error instanceof Error) {
224+
terminal.writeln(
225+
Language.workspaceErrorMessagePrefix + workspace.error.message,
226+
);
227+
return;
228+
} else if (!workspaceAgent) {
229+
terminal.writeln(
230+
Language.workspaceAgentErrorMessagePrefix + "no agent found with ID",
231+
);
232+
return;
233+
}
234+
235+
// Hook up terminal events to the websocket.
236+
let websocket: WebSocket | null;
237+
const disposers = [
238+
terminal.onData((data) => {
239+
websocket?.send(
240+
new TextEncoder().encode(JSON.stringify({ data: data })),
241+
);
242+
}),
243+
terminal.onResize((event) => {
244+
websocket?.send(
245+
new TextEncoder().encode(
246+
JSON.stringify({
247+
height: event.rows,
248+
width: event.cols,
249+
}),
250+
),
251+
);
252+
}),
253+
];
254+
255+
let disposed = false;
256+
257+
// Open the web socket and hook it up to the terminal.
258+
terminalWebsocketUrl(
259+
proxy.preferredPathAppURL,
260+
reconnectionToken,
261+
workspaceAgent.id,
262+
command,
263+
)
264+
.then((url) => {
265+
if (disposed) {
266+
return; // Unmounted while we waited for the async call.
267+
}
268+
websocket = new WebSocket(url);
269+
websocket.binaryType = "arraybuffer";
270+
websocket.addEventListener("open", () => {
271+
// Now that we are connected, allow user input.
272+
terminal.options = {
273+
disableStdin: false,
274+
windowsMode: workspaceAgent?.operating_system === "windows",
275+
};
276+
// Send the initial size.
277+
websocket?.send(
278+
new TextEncoder().encode(
279+
JSON.stringify({
280+
height: terminal.rows,
281+
width: terminal.cols,
282+
}),
283+
),
284+
);
285+
setTerminalState("connected");
286+
});
287+
websocket.addEventListener("error", () => {
288+
terminal.options.disableStdin = true;
289+
terminal.writeln(
290+
Language.websocketErrorMessagePrefix + "socket errored",
291+
);
292+
setTerminalState("disconnected");
293+
});
294+
websocket.addEventListener("close", () => {
295+
terminal.options.disableStdin = true;
296+
setTerminalState("disconnected");
297+
});
298+
websocket.addEventListener("message", (event) => {
299+
if (typeof event.data === "string") {
300+
// This exclusively occurs when testing.
301+
// "jest-websocket-mock" doesn't support ArrayBuffer.
302+
terminal.write(event.data);
303+
} else {
304+
terminal.write(new Uint8Array(event.data));
305+
}
306+
});
307+
})
308+
.catch((error) => {
309+
if (disposed) {
310+
return; // Unmounted while we waited for the async call.
311+
}
312+
terminal.writeln(Language.websocketErrorMessagePrefix + error.message);
313+
setTerminalState("disconnected");
314+
});
315+
316+
return () => {
317+
disposed = true; // Could use AbortController instead?
318+
disposers.forEach((d) => d.dispose());
319+
websocket?.close(1000);
320+
};
293321
}, [
294-
workspaceError,
295-
workspaceAgentError,
296-
websocketError,
297-
workspaceAgent,
298-
terminal,
322+
command,
299323
fitAddon,
300-
isConnected,
301-
sendEvent,
324+
proxy.preferredPathAppURL,
325+
reconnectionToken,
326+
terminal,
327+
workspace.isLoading,
328+
workspace.error,
329+
workspaceAgent,
302330
]);
303331

304332
return (
305333
<>
306334
<Helmet>
307335
<title>
308-
{terminalState.context.workspace
336+
{workspace.data
309337
? pageTitle(
310-
`Terminal · ${terminalState.context.workspace.owner_name}/${terminalState.context.workspace.name}`,
338+
`Terminal · ${workspace.data.owner_name}/${workspace.data.name}`,
311339
)
312340
: ""}
313341
</title>
@@ -317,7 +345,7 @@ const TerminalPage: FC = () => {
317345
{lifecycleState === "starting" && <LoadingScriptsAlert />}
318346
{lifecycleState === "ready" &&
319347
prevLifecycleState.current === "starting" && <LoadedScriptsAlert />}
320-
{isDisconnected && <DisconnectedAlert />}
348+
{terminalState === "disconnected" && <DisconnectedAlert />}
321349
<div
322350
className={styles.terminal}
323351
ref={xtermRef}

0 commit comments

Comments
 (0)