Skip to main content

browser_control/cli/
wait.rs

1//! `browser-control wait` — block until the browser endpoint is ready.
2
3use anyhow::{anyhow, bail, Result};
4use std::time::{Duration, Instant};
5
6use crate::detect::Engine;
7
8pub async fn run(browser: Option<String>, ready: bool, timeout: u64) -> Result<()> {
9    if !ready {
10        bail!("`wait` requires --ready in this version");
11    }
12    let resolved = crate::cli::mcp::resolve_browser(browser).await?;
13    wait_until_ready(&resolved.endpoint, resolved.engine, Duration::from_secs(timeout)).await?;
14    println!("ready");
15    Ok(())
16}
17
18async fn probe_once(client: &reqwest::Client, endpoint: &str, engine: Engine) -> bool {
19    let base = endpoint.trim_end_matches('/');
20    let url = format!("{base}/json/version");
21    let resp = match client.get(&url).send().await {
22        Ok(r) => r,
23        Err(_) => return false,
24    };
25    if !resp.status().is_success() {
26        return false;
27    }
28    match engine {
29        Engine::Cdp => {
30            let v: serde_json::Value = match resp.json().await {
31                Ok(v) => v,
32                Err(_) => return false,
33            };
34            v.get("webSocketDebuggerUrl")
35                .and_then(|x| x.as_str())
36                .is_some()
37        }
38        Engine::Bidi => true,
39    }
40}
41
42async fn wait_until_ready(endpoint: &str, engine: Engine, timeout: Duration) -> Result<()> {
43    let client = reqwest::Client::builder()
44        .timeout(Duration::from_secs(2))
45        .build()
46        .map_err(|e| anyhow!("failed to build http client: {e}"))?;
47    let deadline = Instant::now() + timeout;
48    loop {
49        if probe_once(&client, endpoint, engine).await {
50            return Ok(());
51        }
52        if Instant::now() >= deadline {
53            return Err(anyhow!(
54                "timeout after {}s waiting for browser ready",
55                timeout.as_secs()
56            ));
57        }
58        tokio::time::sleep(Duration::from_millis(250)).await;
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[tokio::test]
67    async fn wait_ready_succeeds_when_server_immediately_responds() {
68        let mut server = mockito::Server::new_async().await;
69        let _m = server
70            .mock("GET", "/json/version")
71            .with_status(200)
72            .with_header("content-type", "application/json")
73            .with_body(r#"{"webSocketDebuggerUrl":"ws://x"}"#)
74            .expect_at_least(1)
75            .create_async()
76            .await;
77
78        let url = server.url();
79        let res = tokio::time::timeout(
80            Duration::from_secs(1),
81            wait_until_ready(&url, Engine::Cdp, Duration::from_secs(5)),
82        )
83        .await
84        .expect("did not complete within 1s");
85        assert!(res.is_ok(), "expected Ok, got {res:?}");
86    }
87
88    #[tokio::test]
89    async fn wait_ready_times_out_when_server_500s() {
90        let mut server = mockito::Server::new_async().await;
91        let _m = server
92            .mock("GET", "/json/version")
93            .with_status(500)
94            .expect_at_least(1)
95            .create_async()
96            .await;
97
98        let url = server.url();
99        let res = wait_until_ready(&url, Engine::Cdp, Duration::from_secs(1)).await;
100        let err = res.expect_err("expected timeout error");
101        assert!(
102            err.to_string().contains("timeout"),
103            "unexpected error: {err}"
104        );
105    }
106
107    #[tokio::test]
108    async fn wait_ready_succeeds_after_initial_failure() {
109        let mut server = mockito::Server::new_async().await;
110        let m_fail = server
111            .mock("GET", "/json/version")
112            .with_status(500)
113            .expect(2)
114            .create_async()
115            .await;
116        let m_ok = server
117            .mock("GET", "/json/version")
118            .with_status(200)
119            .with_header("content-type", "application/json")
120            .with_body(r#"{"webSocketDebuggerUrl":"ws://x"}"#)
121            .expect_at_least(1)
122            .create_async()
123            .await;
124
125        let url = server.url();
126        let res = wait_until_ready(&url, Engine::Cdp, Duration::from_secs(5)).await;
127        assert!(res.is_ok(), "expected Ok, got {res:?}");
128        m_fail.assert_async().await;
129        m_ok.assert_async().await;
130    }
131}