mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-16 06:45:48 -07:00
Adjusted Remote Shell Behavior to be more Deterministic
This commit is contained in:
@@ -118,6 +118,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
const [copyFlash, setCopyFlash] = useState(false);
|
const [copyFlash, setCopyFlash] = useState(false);
|
||||||
const [, setPolling] = useState(false);
|
const [, setPolling] = useState(false);
|
||||||
const [psStatus, setPsStatus] = useState({});
|
const [psStatus, setPsStatus] = useState({});
|
||||||
|
const [statusLine, setStatusLine] = useState("Idle");
|
||||||
const [milestones, setMilestones] = useState({
|
const [milestones, setMilestones] = useState({
|
||||||
requested: false,
|
requested: false,
|
||||||
leaseIssued: false,
|
leaseIssued: false,
|
||||||
@@ -132,6 +133,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
const terminalRef = useRef(null);
|
const terminalRef = useRef(null);
|
||||||
const joinRetryRef = useRef(null);
|
const joinRetryRef = useRef(null);
|
||||||
const joinAttemptsRef = useRef(0);
|
const joinAttemptsRef = useRef(0);
|
||||||
|
const tunnelRef = useRef(null);
|
||||||
const DOMAIN_REMOTE_SHELL = "remote-interactive-shell";
|
const DOMAIN_REMOTE_SHELL = "remote-interactive-shell";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -185,8 +187,13 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
ack: false,
|
ack: false,
|
||||||
active: false,
|
active: false,
|
||||||
});
|
});
|
||||||
|
setStatusLine("Idle");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
tunnelRef.current = tunnel?.tunnel_id || null;
|
||||||
|
}, [tunnel?.tunnel_id]);
|
||||||
|
|
||||||
const disconnectSocket = useCallback(() => {
|
const disconnectSocket = useCallback(() => {
|
||||||
const socket = socketRef.current;
|
const socket = socketRef.current;
|
||||||
if (socket) {
|
if (socket) {
|
||||||
@@ -197,6 +204,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const stopPolling = useCallback(() => {
|
const stopPolling = useCallback(() => {
|
||||||
|
setStatusLine("Stopping poll loop…");
|
||||||
if (pollTimerRef.current) {
|
if (pollTimerRef.current) {
|
||||||
clearTimeout(pollTimerRef.current);
|
clearTimeout(pollTimerRef.current);
|
||||||
pollTimerRef.current = null;
|
pollTimerRef.current = null;
|
||||||
@@ -204,10 +212,10 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setPolling(false);
|
setPolling(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const stopTunnel = useCallback(
|
const stopTunnel = useCallback(async (reason = "operator_disconnect", tunnelIdOverride = null) => {
|
||||||
async (reason = "operator_disconnect") => {
|
const tunnelId = tunnelIdOverride || tunnelRef.current;
|
||||||
const tunnelId = tunnel?.tunnel_id;
|
|
||||||
if (!tunnelId) return;
|
if (!tunnelId) return;
|
||||||
|
setStatusLine(`Stopping tunnel ${tunnelId} reason=${reason}`);
|
||||||
try {
|
try {
|
||||||
await fetch(`/api/tunnel/${tunnelId}`, {
|
await fetch(`/api/tunnel/${tunnelId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
@@ -217,22 +225,21 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// best-effort; socket close frame acts as fallback
|
// best-effort; socket close frame acts as fallback
|
||||||
}
|
}
|
||||||
},
|
}, []);
|
||||||
[tunnel?.tunnel_id]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
debugLog("cleanup on unmount", { tunnelId: tunnel?.tunnel_id });
|
debugLog("cleanup on unmount", { tunnelId: tunnelRef.current });
|
||||||
stopPolling();
|
stopPolling();
|
||||||
disconnectSocket();
|
disconnectSocket();
|
||||||
if (joinRetryRef.current) {
|
if (joinRetryRef.current) {
|
||||||
clearTimeout(joinRetryRef.current);
|
clearTimeout(joinRetryRef.current);
|
||||||
joinRetryRef.current = null;
|
joinRetryRef.current = null;
|
||||||
}
|
}
|
||||||
stopTunnel("component_unmount");
|
stopTunnel("component_unmount", tunnelRef.current);
|
||||||
};
|
};
|
||||||
}, [disconnectSocket, stopPolling, stopTunnel]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const appendOutput = useCallback((text) => {
|
const appendOutput = useCallback((text) => {
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
@@ -297,6 +304,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
const pollLoop = useCallback(
|
const pollLoop = useCallback(
|
||||||
(socket, tunnelId) => {
|
(socket, tunnelId) => {
|
||||||
if (!socket || !tunnelId) return;
|
if (!socket || !tunnelId) return;
|
||||||
|
setStatusLine(`Polling tunnel ${tunnelId}`);
|
||||||
debugLog("pollLoop tick", { tunnelId });
|
debugLog("pollLoop tick", { tunnelId });
|
||||||
setPolling(true);
|
setPolling(true);
|
||||||
pollTimerRef.current = setTimeout(async () => {
|
pollTimerRef.current = setTimeout(async () => {
|
||||||
@@ -317,6 +325,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setPsStatus(resp.status);
|
setPsStatus(resp.status);
|
||||||
debugLog("pollLoop status", resp.status);
|
debugLog("pollLoop status", resp.status);
|
||||||
if (resp.status.closed) {
|
if (resp.status.closed) {
|
||||||
|
setStatusLine(`Tunnel ${tunnelId} reported closed`);
|
||||||
setSessionState("closed");
|
setSessionState("closed");
|
||||||
setTunnel(null);
|
setTunnel(null);
|
||||||
stopPolling();
|
stopPolling();
|
||||||
@@ -324,10 +333,12 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
}
|
}
|
||||||
if (resp.status.open_sent) {
|
if (resp.status.open_sent) {
|
||||||
setMilestones((prev) => ({ ...prev, channelOpened: true }));
|
setMilestones((prev) => ({ ...prev, channelOpened: true }));
|
||||||
|
setStatusLine("Remote shell opening…");
|
||||||
}
|
}
|
||||||
if (resp.status.ack) {
|
if (resp.status.ack) {
|
||||||
setSessionState("connected");
|
setSessionState("connected");
|
||||||
setMilestones((prev) => ({ ...prev, ack: true, active: true }));
|
setMilestones((prev) => ({ ...prev, ack: true, active: true }));
|
||||||
|
setStatusLine("Remote shell active");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pollLoop(socket, tunnelId);
|
pollLoop(socket, tunnelId);
|
||||||
@@ -339,6 +350,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
const handleDisconnect = useCallback(
|
const handleDisconnect = useCallback(
|
||||||
async (reason = "operator_disconnect") => {
|
async (reason = "operator_disconnect") => {
|
||||||
debugLog("handleDisconnect begin", { reason, tunnelId: tunnel?.tunnel_id, psStatus, sessionState });
|
debugLog("handleDisconnect begin", { reason, tunnelId: tunnel?.tunnel_id, psStatus, sessionState });
|
||||||
|
setStatusLine(`Disconnect requested (${reason})`);
|
||||||
const socket = socketRef.current;
|
const socket = socketRef.current;
|
||||||
const tunnelId = tunnel?.tunnel_id;
|
const tunnelId = tunnel?.tunnel_id;
|
||||||
if (joinRetryRef.current) {
|
if (joinRetryRef.current) {
|
||||||
@@ -358,6 +370,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setTunnel(null);
|
setTunnel(null);
|
||||||
setSessionState("closed");
|
setSessionState("closed");
|
||||||
setMilestones((prev) => ({ ...prev, active: false }));
|
setMilestones((prev) => ({ ...prev, active: false }));
|
||||||
|
setStatusLine("Disconnected");
|
||||||
debugLog("handleDisconnect finished", { tunnelId });
|
debugLog("handleDisconnect finished", { tunnelId });
|
||||||
},
|
},
|
||||||
[disconnectSocket, stopPolling, stopTunnel, tunnel?.tunnel_id]
|
[disconnectSocket, stopPolling, stopTunnel, tunnel?.tunnel_id]
|
||||||
@@ -400,10 +413,12 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
disconnectSocket();
|
disconnectSocket();
|
||||||
stopPolling();
|
stopPolling();
|
||||||
setSessionState("waiting");
|
setSessionState("waiting");
|
||||||
const socket = io(`${window.location.origin}/tunnel`, { transports: ["websocket"] });
|
const socket = io(`${window.location.origin}/tunnel`, { transports: ["websocket", "polling"] });
|
||||||
socketRef.current = socket;
|
socketRef.current = socket;
|
||||||
|
setStatusLine("Connecting to tunnel namespace…");
|
||||||
|
|
||||||
socket.on("connect_error", () => {
|
socket.on("connect_error", () => {
|
||||||
|
setStatusLine("Socket connect error");
|
||||||
debugLog("socket connect_error");
|
debugLog("socket connect_error");
|
||||||
setStatusSeverity("warning");
|
setStatusSeverity("warning");
|
||||||
setStatusMessage("Tunnel namespace unavailable.");
|
setStatusMessage("Tunnel namespace unavailable.");
|
||||||
@@ -412,6 +427,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
|
setStatusLine("Socket disconnected");
|
||||||
debugLog("socket disconnect", { tunnelId: tunnel?.tunnel_id });
|
debugLog("socket disconnect", { tunnelId: tunnel?.tunnel_id });
|
||||||
stopPolling();
|
stopPolling();
|
||||||
if (sessionState !== "closed") {
|
if (sessionState !== "closed") {
|
||||||
@@ -425,6 +441,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
socket.on("connect", async () => {
|
socket.on("connect", async () => {
|
||||||
debugLog("socket connect", { tunnelId: lease.tunnel_id });
|
debugLog("socket connect", { tunnelId: lease.tunnel_id });
|
||||||
setMilestones((prev) => ({ ...prev, operatorJoined: true }));
|
setMilestones((prev) => ({ ...prev, operatorJoined: true }));
|
||||||
|
setStatusLine("Operator attached; joining tunnel…");
|
||||||
setStatusSeverity("info");
|
setStatusSeverity("info");
|
||||||
setStatusMessage("Joining tunnel...");
|
setStatusMessage("Joining tunnel...");
|
||||||
const joinResp = await emitAsync(socket, "join", { tunnel_id: lease.tunnel_id }, 5000);
|
const joinResp = await emitAsync(socket, "join", { tunnel_id: lease.tunnel_id }, 5000);
|
||||||
@@ -435,15 +452,18 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setSessionState("waiting_agent");
|
setSessionState("waiting_agent");
|
||||||
setStatusSeverity("info");
|
setStatusSeverity("info");
|
||||||
setStatusMessage("Waiting for agent to establish tunnel...");
|
setStatusMessage("Waiting for agent to establish tunnel...");
|
||||||
|
setStatusLine("Waiting for agent to attach to tunnel…");
|
||||||
} else if (isTimeout || joinResp.error === "attach_failed") {
|
} else if (isTimeout || joinResp.error === "attach_failed") {
|
||||||
setSessionState("waiting_agent");
|
setSessionState("waiting_agent");
|
||||||
setStatusSeverity("warning");
|
setStatusSeverity("warning");
|
||||||
setStatusMessage("Tunnel join timed out. Retrying...");
|
setStatusMessage("Tunnel join timed out. Retrying...");
|
||||||
|
setStatusLine(`Join timed out (attempt ${attempt}/5); retrying…`);
|
||||||
} else {
|
} else {
|
||||||
debugLog("join error", joinResp);
|
debugLog("join error", joinResp);
|
||||||
setSessionState("error");
|
setSessionState("error");
|
||||||
setStatusSeverity("error");
|
setStatusSeverity("error");
|
||||||
setStatusMessage(joinResp.error);
|
setStatusMessage(joinResp.error);
|
||||||
|
setStatusLine(`Join failed: ${joinResp.error}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (attempt <= 5) {
|
if (attempt <= 5) {
|
||||||
@@ -453,12 +473,14 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setTunnel(null);
|
setTunnel(null);
|
||||||
setStatusSeverity("warning");
|
setStatusSeverity("warning");
|
||||||
setStatusMessage("Operator could not attach to tunnel. Try Connect again.");
|
setStatusMessage("Operator could not attach to tunnel. Try Connect again.");
|
||||||
|
setStatusLine("Operator attach failed after retries");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dims = measureTerminal();
|
const dims = measureTerminal();
|
||||||
debugLog("ps_open emit", { tunnelId: lease.tunnel_id, dims });
|
debugLog("ps_open emit", { tunnelId: lease.tunnel_id, dims });
|
||||||
setMilestones((prev) => ({ ...prev, channelOpened: true }));
|
setMilestones((prev) => ({ ...prev, channelOpened: true }));
|
||||||
|
setStatusLine("Opening remote shell…");
|
||||||
const openResp = await emitAsync(socket, "ps_open", dims, 5000);
|
const openResp = await emitAsync(socket, "ps_open", dims, 5000);
|
||||||
if (openResp?.error && openResp.error === "ps_unsupported") {
|
if (openResp?.error && openResp.error === "ps_unsupported") {
|
||||||
// Suppress warming message; channel will settle once agent attaches.
|
// Suppress warming message; channel will settle once agent attaches.
|
||||||
@@ -480,6 +502,7 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
debugLog("requestTunnel", { agentId, connectionType });
|
debugLog("requestTunnel", { agentId, connectionType });
|
||||||
|
setStatusLine("Requesting tunnel…");
|
||||||
if (!agentId) {
|
if (!agentId) {
|
||||||
setStatusSeverity("warning");
|
setStatusSeverity("warning");
|
||||||
setStatusMessage("Agent ID is required to request a tunnel.");
|
setStatusMessage("Agent ID is required to request a tunnel.");
|
||||||
@@ -512,11 +535,13 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
setTunnel(data);
|
setTunnel(data);
|
||||||
setStatusMessage("");
|
setStatusMessage("");
|
||||||
setSessionState("lease_issued");
|
setSessionState("lease_issued");
|
||||||
|
setStatusLine(`Lease issued for tunnel ${data.tunnel_id || "-"}`);
|
||||||
connectSocket(data);
|
connectSocket(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setSessionState("error");
|
setSessionState("error");
|
||||||
setStatusSeverity("error");
|
setStatusSeverity("error");
|
||||||
setStatusMessage("");
|
setStatusMessage("");
|
||||||
|
setStatusLine("Tunnel request failed");
|
||||||
}
|
}
|
||||||
}, [DOMAIN_REMOTE_SHELL, agentId, connectSocket, connectionType, resetState]);
|
}, [DOMAIN_REMOTE_SHELL, agentId, connectSocket, connectionType, resetState]);
|
||||||
|
|
||||||
@@ -674,6 +699,19 @@ export default function ReverseTunnelPowershell({ device }) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
mt: 0.5,
|
||||||
|
color: MAGIC_UI.textMuted,
|
||||||
|
fontWeight: 600,
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
overflow: "hidden",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{statusLine}
|
||||||
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
Reference in New Issue
Block a user