Skip to content

Commit b1a6d85

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 81acc6d commit b1a6d85

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,7 +13,6 @@ 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";
@@ -23,6 +21,8 @@ import { getLatencyColor } from "utils/latency";
2321
import Popover from "@mui/material/Popover";
2422
import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency";
2523
import { portForwardURL } from "utils/portForward";
24+
import { terminalWebsocketUrl } from "utils/terminal";
25+
import { getMatchingAgentOrFirst } from "utils/workspace";
2626
import {
2727
DisconnectedAlert,
2828
ErrorScriptAlert,
@@ -31,6 +31,7 @@ import {
3131
} from "./TerminalAlerts";
3232
import { useQuery } from "react-query";
3333
import { deploymentConfig } from "api/queries/deployment";
34+
import { workspaceByOwnerAndName } from "api/queries/workspaces";
3435

3536
export const Language = {
3637
workspaceErrorMessagePrefix: "Unable to fetch workspace: ",
@@ -44,9 +45,11 @@ const TerminalPage: FC = () => {
4445
const { proxy } = useProxy();
4546
const params = useParams() as { username: string; workspace: string };
4647
const username = params.username.replace("@", "");
47-
const workspaceName = params.workspace;
4848
const xtermRef = useRef<HTMLDivElement>(null);
4949
const [terminal, setTerminal] = useState<XTerm.Terminal | null>(null);
50+
const [terminalState, setTerminalState] = useState<
51+
"connected" | "disconnected" | "initializing"
52+
>("initializing");
5053
const [fitAddon, setFitAddon] = useState<FitAddon | null>(null);
5154
const [searchParams] = useSearchParams();
5255
// The reconnection token is a unique token that identifies
@@ -56,37 +59,13 @@ const TerminalPage: FC = () => {
5659
const command = searchParams.get("command") || undefined;
5760
// The workspace name is in the format:
5861
// <workspace name>[.<agent name>]
59-
const workspaceNameParts = workspaceName?.split(".");
60-
const [terminalState, sendEvent] = useMachine(terminalMachine, {
61-
context: {
62-
agentName: workspaceNameParts?.[1],
63-
reconnection: reconnectionToken,
64-
workspaceName: workspaceNameParts?.[0],
65-
username: username,
66-
command: command,
67-
baseURL: proxy.preferredPathAppURL,
68-
},
69-
actions: {
70-
readMessage: (_, event) => {
71-
if (typeof event.data === "string") {
72-
// This exclusively occurs when testing.
73-
// "jest-websocket-mock" doesn't support ArrayBuffer.
74-
terminal?.write(event.data);
75-
} else {
76-
terminal?.write(new Uint8Array(event.data));
77-
}
78-
},
79-
},
80-
});
81-
const isConnected = terminalState.matches("connected");
82-
const isDisconnected = terminalState.matches("disconnected");
83-
const {
84-
workspaceError,
85-
workspace,
86-
workspaceAgentError,
87-
workspaceAgent,
88-
websocketError,
89-
} = terminalState.context;
62+
const workspaceNameParts = params.workspace?.split(".");
63+
const workspace = useQuery(
64+
workspaceByOwnerAndName(username, workspaceNameParts?.[0]),
65+
);
66+
const workspaceAgent = workspace.data
67+
? getMatchingAgentOrFirst(workspace.data, workspaceNameParts?.[1])
68+
: undefined;
9069
const dashboard = useDashboard();
9170
const proxyContext = useProxy();
9271
const selectedProxy = proxyContext.proxy.proxy;
@@ -107,7 +86,7 @@ const TerminalPage: FC = () => {
10786
(uri: string) => {
10887
if (
10988
!workspaceAgent ||
110-
!workspace ||
89+
!workspace.data ||
11190
!username ||
11291
!proxy.preferredWildcardHostname
11392
) {
@@ -141,15 +120,15 @@ const TerminalPage: FC = () => {
141120
proxy.preferredWildcardHostname,
142121
parseInt(url.port),
143122
workspaceAgent.name,
144-
workspace.name,
123+
workspace.data.name,
145124
username,
146125
) + url.pathname,
147126
);
148127
} catch (ex) {
149128
open(uri);
150129
}
151130
},
152-
[workspaceAgent, workspace, username, proxy.preferredWildcardHostname],
131+
[workspaceAgent, workspace.data, username, proxy.preferredWildcardHostname],
153132
);
154133

155134
// Create the terminal!
@@ -182,23 +161,6 @@ const TerminalPage: FC = () => {
182161
handleWebLink(uri);
183162
}),
184163
);
185-
terminal.onData((data) => {
186-
sendEvent({
187-
type: "WRITE",
188-
request: {
189-
data: data,
190-
},
191-
});
192-
});
193-
terminal.onResize((event) => {
194-
sendEvent({
195-
type: "WRITE",
196-
request: {
197-
height: event.rows,
198-
width: event.cols,
199-
},
200-
});
201-
});
202164
setTerminal(terminal);
203165
terminal.open(xtermRef.current);
204166
const listener = () => {
@@ -210,11 +172,9 @@ const TerminalPage: FC = () => {
210172
window.removeEventListener("resize", listener);
211173
terminal.dispose();
212174
};
213-
}, [config.data, config.isLoading, sendEvent, xtermRef, handleWebLink]);
175+
}, [config.data, config.isLoading, xtermRef, handleWebLink]);
214176

215-
// Triggers the initial terminal connection using
216-
// the reconnection token and workspace name found
217-
// from the router.
177+
// Updates the reconnection token into the URL if necessary.
218178
useEffect(() => {
219179
if (searchParams.get("reconnect") === reconnectionToken) {
220180
return;
@@ -230,7 +190,7 @@ const TerminalPage: FC = () => {
230190
);
231191
}, [searchParams, navigate, reconnectionToken]);
232192

233-
// Apply terminal options based on connection state.
193+
// Hook up the terminal through a web socket.
234194
useEffect(() => {
235195
if (!terminal || !fitAddon) {
236196
return;
@@ -242,68 +202,136 @@ const TerminalPage: FC = () => {
242202
fitAddon.fit();
243203
fitAddon.fit();
244204

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

273-
// Focusing on connection allows users to reload the
274-
// page and start typing immediately.
209+
// Focusing on connection allows users to reload the page and start
210+
// typing immediately.
275211
terminal.focus();
276-
terminal.options = {
277-
disableStdin: false,
278-
windowsMode: workspaceAgent?.operating_system === "windows",
279-
};
280212

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

300328
return (
301329
<>
302330
<Helmet>
303331
<title>
304-
{terminalState.context.workspace
332+
{workspace.data
305333
? pageTitle(
306-
`Terminal · ${terminalState.context.workspace.owner_name}/${terminalState.context.workspace.name}`,
334+
`Terminal · ${workspace.data.owner_name}/${workspace.data.name}`,
307335
)
308336
: ""}
309337
</title>
@@ -313,7 +341,7 @@ const TerminalPage: FC = () => {
313341
{lifecycleState === "starting" && <LoadingScriptsAlert />}
314342
{lifecycleState === "ready" &&
315343
prevLifecycleState.current === "starting" && <LoadedScriptsAlert />}
316-
{isDisconnected && <DisconnectedAlert />}
344+
{terminalState === "disconnected" && <DisconnectedAlert />}
317345
<div
318346
className={styles.terminal}
319347
ref={xtermRef}

0 commit comments

Comments
 (0)