diff --git a/Data/Engine/services/WebSocket/Agent/reverse_tunnel_orchestrator.py b/Data/Engine/services/WebSocket/Agent/reverse_tunnel_orchestrator.py index 8c167d97..3e9df698 100644 --- a/Data/Engine/services/WebSocket/Agent/reverse_tunnel_orchestrator.py +++ b/Data/Engine/services/WebSocket/Agent/reverse_tunnel_orchestrator.py @@ -668,7 +668,23 @@ class ReverseTunnelService: server.close(code=code, reason=reason) except Exception: self.logger.debug("protocol server close failed tunnel_id=%s", tunnel_id, exc_info=True) + if tunnel_id in self._protocol_servers: + try: + self._protocol_servers.pop(tunnel_id, None) + except Exception: + pass self._push_stop_to_agent(lease, reason=reason) + websocket = self._agent_sockets.pop(tunnel_id, None) + if websocket is not None: + try: + self.lease_manager.mark_agent_disconnected(tunnel_id) + except Exception: + pass + try: + if self._loop: + self._loop.call_soon_threadsafe(asyncio.create_task, websocket.close()) + except Exception: + self.logger.debug("agent websocket close failed tunnel_id=%s", tunnel_id, exc_info=True) self.release_bridge(tunnel_id, reason=reason) return True diff --git a/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx b/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx index be33422b..3200f652 100644 --- a/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx +++ b/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx @@ -107,6 +107,15 @@ function highlightPs(code) { } } +const INITIAL_MILESTONES = { + requested: false, + leaseIssued: false, + operatorJoined: false, + channelOpened: false, + ack: false, + active: false, +}; + export default function ReverseTunnelPowershell({ device }) { const [connectionType, setConnectionType] = useState("ps"); const [tunnel, setTunnel] = useState(null); @@ -119,14 +128,7 @@ export default function ReverseTunnelPowershell({ device }) { const [, setPolling] = useState(false); const [psStatus, setPsStatus] = useState({}); const [statusLine, setStatusLine] = useState("Idle"); - const [milestones, setMilestones] = useState({ - requested: false, - leaseIssued: false, - operatorJoined: false, - channelOpened: false, - ack: false, - active: false, - }); + const [milestones, setMilestones] = useState(() => ({ ...INITIAL_MILESTONES })); const socketRef = useRef(null); const pollTimerRef = useRef(null); const resizeTimerRef = useRef(null); @@ -142,12 +144,6 @@ export default function ReverseTunnelPowershell({ device }) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { - debugLog("component mount", { hostname: device?.hostname, agentId }); - return () => debugLog("component unmount"); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const hostname = useMemo(() => { return ( normalizeText(device?.hostname) || @@ -179,14 +175,7 @@ export default function ReverseTunnelPowershell({ device }) { setOutput(""); setInput(""); setPsStatus({}); - setMilestones({ - requested: false, - leaseIssued: false, - operatorJoined: false, - channelOpened: false, - ack: false, - active: false, - }); + setMilestones({ ...INITIAL_MILESTONES }); setStatusLine("Idle"); }, []); @@ -204,7 +193,6 @@ export default function ReverseTunnelPowershell({ device }) { }, []); const stopPolling = useCallback(() => { - setStatusLine("Stopping poll loop…"); if (pollTimerRef.current) { clearTimeout(pollTimerRef.current); pollTimerRef.current = null; @@ -328,6 +316,7 @@ export default function ReverseTunnelPowershell({ device }) { setStatusLine(`Tunnel ${tunnelId} reported closed`); setSessionState("closed"); setTunnel(null); + setMilestones({ ...INITIAL_MILESTONES }); stopPolling(); return; } @@ -351,6 +340,7 @@ export default function ReverseTunnelPowershell({ device }) { async (reason = "operator_disconnect") => { debugLog("handleDisconnect begin", { reason, tunnelId: tunnel?.tunnel_id, psStatus, sessionState }); setStatusLine(`Disconnect requested (${reason})`); + setPsStatus({}); const socket = socketRef.current; const tunnelId = tunnel?.tunnel_id; if (joinRetryRef.current) { @@ -369,7 +359,7 @@ export default function ReverseTunnelPowershell({ device }) { disconnectSocket(); setTunnel(null); setSessionState("closed"); - setMilestones((prev) => ({ ...prev, active: false })); + setMilestones({ ...INITIAL_MILESTONES }); setStatusLine("Disconnected"); debugLog("handleDisconnect finished", { tunnelId }); }, @@ -561,7 +551,7 @@ export default function ReverseTunnelPowershell({ device }) { [appendOutput, emitAsync] ); - const isConnected = sessionState === "connected" || psStatus?.ack; + const isConnected = sessionState === "connected" || (psStatus?.ack && !psStatus?.closed); const isClosed = sessionState === "closed" || psStatus?.closed; const isBusy = sessionState === "requesting" ||