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(&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}