browser_control/cli/
wait.rs1use 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}