Skip to content

Commit

Permalink
Ensure vm is finished booting before connecting to it via terminal (#…
Browse files Browse the repository at this point in the history
…1064)

## Done

- Ensure vm is finished booting before connecting to it via terminal

Fixes #1063

## QA

1. Run the LXD-UI:
- On the demo server via the link posted by @webteam-app below. This is
only available for PRs created by collaborators of the repo. Ask
@mas-who or @edlerd for access.
- With a local copy of this branch, [build and run as described in the
docs](../CONTRIBUTING.md#setting-up-for-development).
2. Perform the following QA steps:
    - start / stop vms and from the instance terminal page.
  • Loading branch information
edlerd authored Jan 21, 2025
2 parents 979d3da + de7ad0c commit 4350da7
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 16 deletions.
6 changes: 5 additions & 1 deletion src/pages/instances/InstanceDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const InstanceDetail: FC = () => {
const {
data: instance,
error,
refetch: refreshInstance,
isLoading,
} = useQuery({
queryKey: [queryKeys.instances, name, project],
Expand Down Expand Up @@ -96,7 +97,10 @@ const InstanceDetail: FC = () => {

{activeTab === "terminal" && (
<div role="tabpanel" aria-labelledby="terminal">
<InstanceTerminal instance={instance} />
<InstanceTerminal
instance={instance}
refreshInstance={refreshInstance}
/>
</div>
)}

Expand Down
56 changes: 41 additions & 15 deletions src/pages/instances/InstanceTerminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ export const defaultPayload: TerminalConnectPayload = {

interface Props {
instance: LxdInstance;
refreshInstance: () => Promise<unknown>;
}

const InstanceTerminal: FC<Props> = ({ instance }) => {
const InstanceTerminal: FC<Props> = ({ instance, refreshInstance }) => {
const { name, project } = useParams<{
name: string;
project: string;
Expand All @@ -60,6 +61,8 @@ const InstanceTerminal: FC<Props> = ({ instance }) => {
const [fitAddon] = useState<FitAddon>(new FitAddon());
const [userInteracted, setUserInteracted] = useState(false);
const xtermRef = useRef<Terminal>(null);
const refreshTimerRef = useRef<NodeJS.Timeout | null>(null);
const [version, setVersion] = useState(0);

usePrompt({
when: userInteracted,
Expand All @@ -73,8 +76,6 @@ const InstanceTerminal: FC<Props> = ({ instance }) => {
};
useEventListener("beforeunload", handleCloseTab);

const isRunning = instance.status === "Running";

const openWebsockets = async (payload: TerminalConnectPayload) => {
if (!name) {
notify.failure("Missing name", new Error());
Expand Down Expand Up @@ -143,16 +144,37 @@ const InstanceTerminal: FC<Props> = ({ instance }) => {
return [data, control];
};

const isRunning = instance.status === "Running";
const isBooting = isRunning && (instance.state?.processes ?? 0) < 1;
const canConnect = isRunning && !isBooting;

useEffect(() => {
if (isBooting && refreshTimerRef.current === null) {
const delay = 1000;
const triggerRefresh = () => {
void refreshInstance();
refreshTimerRef.current = null;
setVersion((old) => old + 1);
};
const timeout = setTimeout(triggerRefresh, delay);
refreshTimerRef.current = timeout;

return () => clearTimeout(timeout);
}
}, [isBooting, version]);

useEffect(() => {
xtermRef.current?.clear();
notify.clear();
const websocketPromise = openWebsockets(payload);
return () => {
void websocketPromise.then((websockets) => {
websockets?.map((websocket) => websocket.close());
});
};
}, [payload, instance.status]);
if (canConnect) {
const websocketPromise = openWebsockets(payload);
return () => {
void websocketPromise.then((websockets) => {
websockets?.map((websocket) => websocket.close());
});
};
}
}, [payload, instance.status, canConnect]);

const handleResize = () => {
if (controlWs?.readyState === WebSocket.CLOSED) {
Expand Down Expand Up @@ -194,7 +216,7 @@ const InstanceTerminal: FC<Props> = ({ instance }) => {

return (
<div className="instance-terminal-tab">
{isRunning && (
{canConnect && (
<>
<div className="p-panel__controls">
<ReconnectTerminalBtn reconnect={setPayload} payload={payload} />
Expand All @@ -216,16 +238,20 @@ const InstanceTerminal: FC<Props> = ({ instance }) => {
)}
</>
)}
{!isRunning && (
{!canConnect && (
<EmptyState
className="empty-state"
image={<Icon name="pods" className="empty-state-icon" />}
title="Instance stopped"
title={isBooting ? "Instance starting" : "Instance stopped"}
>
<p>Start the instance to access the terminal.</p>
<p>
{isBooting
? "Terminal will be ready once the instance is finished booting."
: "Start the instance to access the terminal."}
</p>
<ActionButton
appearance="positive"
loading={isStartLoading}
loading={isStartLoading || isBooting}
onClick={handleStart}
>
Start instance
Expand Down

0 comments on commit 4350da7

Please sign in to comment.