1use anyhow::{Context, Result};
4
5use crate::cli::env_resolver::{self, BrowserSelector, ResolvedBrowser, Source};
6use crate::detect::Kind;
7use crate::mcp::server::{run, ServerState, ToolRegistry};
8use crate::registry::{BrowserRow, Registry};
9
10pub async fn run_cli(
12 browser_arg: Option<String>,
13 playwright_version: Option<String>,
14) -> Result<()> {
15 let resolved = resolve_browser(browser_arg).await?;
16 let sidecar_config = crate::sidecar::SidecarConfig {
17 version: playwright_version,
18 };
19 let state = ServerState::with_sidecar_config(resolved, sidecar_config);
20 let tools = ToolRegistry::new();
21 crate::mcp::tools::register_all(&tools);
22 run(state, tools).await
23}
24
25pub async fn resolve_browser(browser_arg: Option<String>) -> Result<ResolvedBrowser> {
35 let registry = Registry::open()?;
36 if let Some(arg) = browser_arg.as_deref().filter(|s| !s.is_empty()) {
37 let sel = env_resolver::parse(arg)?;
38 return resolve_selector_or_start(sel, ®istry).await;
39 }
40 if let Some(value) = crate::config::load()?.default {
41 match resolve_default_browser(&value, ®istry).await {
42 Ok(resolved) => return Ok(resolved),
43 Err(default_err) => {
44 if let Some(row) = registry.most_recent_alive()? {
45 return Ok(resolved_from_row(row));
46 }
47 if let Ok(sel) = env_resolver::parse(&value) {
48 if let Some(kind) = startable_kind_from_selector(&sel) {
49 return start_and_resolve(Some(kind.as_str().to_string()), false, 30)
50 .await
51 .with_context(|| {
52 format!(
53 "default browser `{value}` failed to resolve ({default_err:#}); also failed to start {}",
54 kind.as_str()
55 )
56 });
57 }
58 }
59 return start_and_resolve(None, false, 30).await.with_context(|| {
60 format!(
61 "default browser `{value}` failed to resolve ({default_err:#}); also failed to start a browser"
62 )
63 });
64 }
65 }
66 }
67 if let Some(row) = registry.most_recent_alive()? {
68 return Ok(resolved_from_row(row));
69 }
70 start_and_resolve(None, false, 30).await
71}
72
73async fn resolve_default_browser(value: &str, registry: &Registry) -> Result<ResolvedBrowser> {
74 let sel = env_resolver::parse(value)?;
75 if let BrowserSelector::Name(name) = &sel {
76 if let Some(kind) = stale_default_kind(name, registry)? {
77 rewrite_default_if_unchanged(value, kind)?;
78 return env_resolver::resolve(BrowserSelector::Kind(kind), registry)
79 .await
80 .with_context(|| {
81 format!(
82 "default browser `{name}` is stale; rewrote default to `{}` but failed to resolve a live {} browser",
83 kind.as_str(),
84 kind.as_str()
85 )
86 });
87 }
88 }
89 env_resolver::resolve(sel, registry).await
90}
91
92async fn resolve_selector_or_start(
93 selector: BrowserSelector,
94 registry: &Registry,
95) -> Result<ResolvedBrowser> {
96 match env_resolver::resolve(selector.clone(), registry).await {
97 Ok(resolved) => Ok(resolved),
98 Err(resolve_err) => {
99 if let Some(kind) = startable_kind_from_selector(&selector) {
100 return start_and_resolve(Some(kind.as_str().to_string()), false, 30)
101 .await
102 .with_context(|| {
103 format!(
104 "browser selector failed to resolve ({resolve_err:#}); also failed to start {}",
105 kind.as_str()
106 )
107 });
108 }
109 Err(resolve_err)
110 }
111 }
112}
113
114pub(crate) fn startable_kind_from_selector(selector: &BrowserSelector) -> Option<Kind> {
115 match selector {
116 BrowserSelector::Kind(kind) => Some(*kind),
117 BrowserSelector::Name(name) => kind_from_generated_name(name),
118 BrowserSelector::Url(_) | BrowserSelector::ExecutablePath(_) => None,
119 }
120}
121
122pub async fn start_and_resolve(
123 browser: Option<String>,
124 headless: bool,
125 wait_timeout: u64,
126) -> Result<ResolvedBrowser> {
127 let started = crate::cli::start::ensure_started(browser, headless, false, wait_timeout).await?;
128 Ok(ResolvedBrowser {
129 endpoint: started.endpoint,
130 engine: started.engine,
131 source: Source::Registered { name: started.name },
132 })
133}
134
135fn resolved_from_row(row: BrowserRow) -> ResolvedBrowser {
136 ResolvedBrowser {
137 endpoint: row.endpoint,
138 engine: row.engine,
139 source: Source::Registered { name: row.name },
140 }
141}
142
143fn stale_default_kind(name: &str, registry: &Registry) -> Result<Option<Kind>> {
144 if let Some(row) = registry
145 .get_by_name(name)
146 .with_context(|| format!("looking up default browser {name}"))?
147 {
148 return match crate::registry::liveness(&row) {
149 crate::registry::BrowserLiveness::DeadPid => {
150 registry
151 .delete(&row.name)
152 .with_context(|| format!("pruning stale default browser {}", row.name))?;
153 Ok(Some(row.kind))
154 }
155 crate::registry::BrowserLiveness::Alive
156 | crate::registry::BrowserLiveness::EndpointUnreachable => Ok(None),
157 };
158 }
159
160 Ok(kind_from_generated_name(name))
161}
162
163fn kind_from_generated_name(name: &str) -> Option<Kind> {
164 let (prefix, _) = name.split_once('-')?;
165 Kind::parse(prefix)
166}
167
168fn rewrite_default_if_unchanged(previous: &str, kind: Kind) -> Result<()> {
169 let mut cfg = crate::config::load()?;
170 if cfg.default.as_deref() == Some(previous) {
171 cfg.default = Some(kind.as_str().to_string());
172 crate::config::save(&cfg)
173 .with_context(|| format!("rewriting stale default browser `{previous}`"))?;
174 }
175 Ok(())
176}
177
178pub fn acquire_bidi_lock_if_needed(
183 registry: &crate::registry::Registry,
184 resolved: &ResolvedBrowser,
185) -> Result<Option<crate::registry::BidiLockGuard>> {
186 use crate::detect::Engine;
187 if resolved.engine != Engine::Bidi {
188 return Ok(None);
189 }
190 let name = match &resolved.source {
191 crate::cli::env_resolver::Source::Registered { name } => name.clone(),
192 crate::cli::env_resolver::Source::External => return Ok(None),
193 };
194 let guard = registry.bidi_lock_acquire(&name, std::time::Duration::from_secs(30))?;
195 Ok(Some(guard))
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::config::{self, Config};
202 use crate::detect::{Engine, Kind};
203 use crate::registry::{BrowserRow, Registry};
204 use std::path::PathBuf;
205
206 struct EnvGuard;
207
208 impl Drop for EnvGuard {
209 fn drop(&mut self) {
210 std::env::remove_var("BROWSER_CONTROL_DATA_DIR");
211 std::env::remove_var("BROWSER_CONTROL_CONFIG_DIR");
212 }
213 }
214
215 struct AliveListener {
216 _listener: std::net::TcpListener,
217 port: u16,
218 }
219
220 fn alive_listener() -> AliveListener {
221 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
222 let port = listener.local_addr().unwrap().port();
223 AliveListener {
224 _listener: listener,
225 port,
226 }
227 }
228
229 fn row(name: &str, kind: Kind, port: u16, pid: u32, started_at: &str) -> BrowserRow {
230 BrowserRow {
231 name: name.to_string(),
232 kind,
233 engine: kind.engine(),
234 pid,
235 endpoint: format!("ws://127.0.0.1:{port}/devtools/browser/{name}"),
236 port,
237 profile_dir: PathBuf::from(format!("/tmp/profiles/{name}")),
238 executable: PathBuf::from("/usr/bin/example"),
239 headless: false,
240 started_at: started_at.to_string(),
241 }
242 }
243
244 fn with_tmp_env<R>(f: impl FnOnce() -> R) -> R {
245 let _lock = crate::test_support::ENV_LOCK
246 .lock()
247 .unwrap_or_else(|e| e.into_inner());
248 let data = tempfile::TempDir::new().unwrap();
249 let cfg = tempfile::TempDir::new().unwrap();
250 std::env::set_var("BROWSER_CONTROL_DATA_DIR", data.path());
251 std::env::set_var("BROWSER_CONTROL_CONFIG_DIR", cfg.path());
252 let _guard = EnvGuard;
253 f()
254 }
255
256 #[test]
257 fn stale_named_default_rewrites_to_kind_and_resolves_live_same_kind() {
258 with_tmp_env(|| {
259 let live = alive_listener();
260 let reg = Registry::open().unwrap();
261 reg.insert(&row(
262 "brave-cosmos",
263 Kind::Brave,
264 9,
265 99_999_999,
266 "2024-01-01T00:00:00Z",
267 ))
268 .unwrap();
269 let live_row = row(
270 "brave-spruce",
271 Kind::Brave,
272 live.port,
273 std::process::id(),
274 "2024-01-02T00:00:00Z",
275 );
276 reg.insert(&live_row).unwrap();
277 config::save(&Config {
278 default: Some("brave-cosmos".into()),
279 })
280 .unwrap();
281 drop(reg);
282
283 let rt = tokio::runtime::Runtime::new().unwrap();
284 let got = rt.block_on(resolve_browser(None)).unwrap();
285 assert_eq!(got.endpoint, live_row.endpoint);
286 assert_eq!(got.engine, Engine::Cdp);
287 assert_eq!(
288 got.source,
289 env_resolver::Source::Registered {
290 name: "brave-spruce".into()
291 }
292 );
293 assert_eq!(config::load().unwrap().default.as_deref(), Some("brave"));
294 let reg = Registry::open().unwrap();
295 assert!(reg.get_by_name("brave-cosmos").unwrap().is_none());
296 });
297 }
298
299 #[test]
300 fn missing_generated_default_rewrites_to_kind_and_resolves_live_same_kind() {
301 with_tmp_env(|| {
302 let live = alive_listener();
303 let reg = Registry::open().unwrap();
304 let live_row = row(
305 "brave-spruce",
306 Kind::Brave,
307 live.port,
308 std::process::id(),
309 "2024-01-02T00:00:00Z",
310 );
311 reg.insert(&live_row).unwrap();
312 config::save(&Config {
313 default: Some("brave-cosmos".into()),
314 })
315 .unwrap();
316 drop(reg);
317
318 let rt = tokio::runtime::Runtime::new().unwrap();
319 let got = rt.block_on(resolve_browser(None)).unwrap();
320 assert_eq!(got.endpoint, live_row.endpoint);
321 assert_eq!(
322 got.source,
323 env_resolver::Source::Registered {
324 name: "brave-spruce".into()
325 }
326 );
327 assert_eq!(config::load().unwrap().default.as_deref(), Some("brave"));
328 });
329 }
330}