//! Startup warmup and periodic heartbeat — mimics real webview lifecycle. //! //! The real Electron webview calls these methods on startup and then sends //! Heartbeat every ~30 seconds. Without this, the LS sees a "user" that //! never initializes and never heartbeats — an obvious bot fingerprint. use crate::backend::Backend; use rand::Rng; use std::sync::Arc; use std::time::Duration; use tokio::task::JoinHandle; use tracing::{debug, info, warn}; /// Run the exact startup sequence the real webview performs on load. /// /// Called BEFORE accepting any API requests. Each call is fire-and-forget /// (we don't care if some fail — the LS might not support all methods). pub async fn warmup_sequence(backend: &Backend) { info!("Running webview warmup sequence..."); let calls: &[(&str, serde_json::Value)] = &[ ("GetStatus", serde_json::json!({})), ("Heartbeat", serde_json::json!({})), ("GetUserStatus", serde_json::json!({})), ("GetCascadeModelConfigs", serde_json::json!({})), ("GetCascadeModelConfigData", serde_json::json!({})), ("GetWorkspaceInfos", serde_json::json!({})), ("GetWorkingDirectories", serde_json::json!({})), ("GetAllCascadeTrajectories", serde_json::json!({})), ("GetMcpServerStates", serde_json::json!({})), ("GetWebDocsOptions", serde_json::json!({})), ("GetRepoInfos", serde_json::json!({})), ("GetAllSkills", serde_json::json!({})), ("InitializeCascadePanelState", serde_json::json!({})), ]; for (method, body) in calls { // Timeout per call — in headless mode, the LS can't reach Google's API // so these would hang forever without a timeout. Warmup is best-effort. match tokio::time::timeout( Duration::from_secs(5), backend.call_json(method, body), ) .await { Ok(Ok((status, _))) => debug!("Warmup {method}: {status}"), Ok(Err(e)) => warn!("Warmup {method} failed: {e}"), Err(_) => warn!("Warmup {method} timed out"), } // Small delay between calls — real webview doesn't blast them instantly let delay = rand::thread_rng().gen_range(50..200); tokio::time::sleep(Duration::from_millis(delay)).await; } info!("Warmup complete"); } /// Spawn a background task that sends Heartbeat every ~30s ± jitter. /// /// Returns a JoinHandle that runs until the task is aborted (on shutdown). pub fn start_heartbeat(backend: Arc) -> JoinHandle<()> { tokio::spawn(async move { loop { // ~30s interval (± 500ms) — matches real setInterval(30000) precision let interval_ms = rand::thread_rng().gen_range(29_500..30_500); tokio::time::sleep(Duration::from_millis(interval_ms)).await; match backend .call_json("Heartbeat", &serde_json::json!({})) .await { Ok((status, _)) => debug!("Heartbeat: {status}"), Err(e) => warn!("Heartbeat failed: {e}"), } } }) }