feat(v2): refactor reconnection probe to TCP-only and add frontend listeners

Replace attempt_ws_connect() with attempt_tcp_probe() in RemoteManager to
avoid allocating per-connection resources (PtyManager, SidecarManager) on
the relay during reconnection probes. Add onRemoteMachineReconnecting and
onRemoteMachineReconnectReady event listeners in remote-bridge.ts. Wire
machines store to auto-reconnect when relay becomes reachable.
This commit is contained in:
Hibryda 2026-03-06 21:50:45 +01:00
parent 218570ac35
commit 71100da125
3 changed files with 59 additions and 23 deletions

View file

@ -269,14 +269,14 @@ impl RemoteManager {
"backoffSecs": delay.as_secs(), "backoffSecs": delay.as_secs(),
})); }));
// Try to get config for reconnection // Try to get URL for TCP probe
let config = { let url = {
let machines = reconnect_machines.lock().await; let machines = reconnect_machines.lock().await;
machines.get(&reconnect_mid).map(|m| (m.config.url.clone(), m.config.token.clone())) machines.get(&reconnect_mid).map(|m| m.config.url.clone())
}; };
if let Some((url, token)) = config { if let Some(url) = url {
if attempt_ws_connect(&url, &token).await.is_ok() { if attempt_tcp_probe(&url).await.is_ok() {
log::info!("Reconnection probe succeeded for {reconnect_mid}"); log::info!("Reconnection probe succeeded for {reconnect_mid}");
// Mark as ready for reconnection — frontend should call connect() // Mark as ready for reconnection — frontend should call connect()
let _ = reconnect_app.emit("remote-machine-reconnect-ready", &serde_json::json!({ let _ = reconnect_app.emit("remote-machine-reconnect-ready", &serde_json::json!({
@ -412,30 +412,25 @@ impl RemoteManager {
} }
} }
/// Probe whether a relay is reachable (connect + immediate close). /// Probe whether a relay is reachable via TCP connect only (no WS upgrade).
async fn attempt_ws_connect(url: &str, token: &str) -> Result<(), String> { /// This avoids allocating per-connection resources (PtyManager, SidecarManager) on the relay.
let request = tokio_tungstenite::tungstenite::http::Request::builder() async fn attempt_tcp_probe(url: &str) -> Result<(), String> {
.uri(url) let host = extract_host(url).ok_or_else(|| "Invalid URL".to_string())?;
.header("Authorization", format!("Bearer {token}")) // Parse host:port, default to 9750 if no port
.header("Sec-WebSocket-Key", tokio_tungstenite::tungstenite::handshake::client::generate_key()) let addr = if host.contains(':') {
.header("Sec-WebSocket-Version", "13") host.clone()
.header("Connection", "Upgrade") } else {
.header("Upgrade", "websocket") format!("{host}:9750")
.header("Host", extract_host(url).unwrap_or_default()) };
.body(())
.map_err(|e| format!("Request build failed: {e}"))?;
let (ws, _) = tokio::time::timeout( tokio::time::timeout(
std::time::Duration::from_secs(5), std::time::Duration::from_secs(5),
tokio_tungstenite::connect_async(request), tokio::net::TcpStream::connect(&addr),
) )
.await .await
.map_err(|_| "Connection timeout".to_string())? .map_err(|_| "Connection timeout".to_string())?
.map_err(|e| format!("Connection failed: {e}"))?; .map_err(|e| format!("TCP connect failed: {e}"))?;
// Close immediately — this was just a probe
let (mut tx, _) = ws.split();
let _ = tx.close().await;
Ok(()) Ok(())
} }

View file

@ -120,3 +120,24 @@ export async function onRemoteError(
callback(event.payload); callback(event.payload);
}); });
} }
export interface RemoteReconnectingEvent {
machineId: string;
backoffSecs: number;
}
export async function onRemoteMachineReconnecting(
callback: (msg: RemoteReconnectingEvent) => void,
): Promise<UnlistenFn> {
return listen<RemoteReconnectingEvent>('remote-machine-reconnecting', (event) => {
callback(event.payload);
});
}
export async function onRemoteMachineReconnectReady(
callback: (msg: RemoteMachineEvent) => void,
): Promise<UnlistenFn> {
return listen<RemoteMachineEvent>('remote-machine-reconnect-ready', (event) => {
callback(event.payload);
});
}

View file

@ -9,6 +9,8 @@ import {
onRemoteMachineReady, onRemoteMachineReady,
onRemoteMachineDisconnected, onRemoteMachineDisconnected,
onRemoteError, onRemoteError,
onRemoteMachineReconnecting,
onRemoteMachineReconnectReady,
type RemoteMachineConfig, type RemoteMachineConfig,
type RemoteMachineInfo, type RemoteMachineInfo,
} from '../adapters/remote-bridge'; } from '../adapters/remote-bridge';
@ -94,4 +96,22 @@ export async function initMachineListeners(): Promise<void> {
notify('error', `Error from ${machine.label}: ${msg.error}`); notify('error', `Error from ${machine.label}: ${msg.error}`);
} }
}); });
await onRemoteMachineReconnecting((msg) => {
const machine = machines.find(m => m.id === msg.machineId);
if (machine) {
machine.status = 'reconnecting';
notify('info', `Reconnecting to ${machine.label} in ${msg.backoffSecs}s…`);
}
});
await onRemoteMachineReconnectReady((msg) => {
const machine = machines.find(m => m.id === msg.machineId);
if (machine) {
notify('info', `${machine.label} reachable — reconnecting…`);
connectMachine(msg.machineId).catch((e) => {
notify('error', `Auto-reconnect failed for ${machine.label}: ${e}`);
});
}
});
} }