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