1use anyhow::{anyhow, Context, Result};
4use serde::Serialize;
5use std::path::PathBuf;
6
7use crate::cli::output::print_json;
8use crate::detect::{self, Engine, Installed, Kind};
9use crate::launch::{self, LaunchOpts};
10use crate::paths;
11use crate::registry::{self, BrowserRow, Registry};
12
13#[derive(Debug, Serialize)]
14pub struct StartResult {
15 pub name: String,
16 pub kind: Kind,
17 pub pid: u32,
18 pub engine: Engine,
19 pub endpoint: String,
20 pub profile: PathBuf,
21 pub headless: bool,
22 pub started_at: String,
23 pub reused: bool,
24}
25
26pub async fn run(
27 browser: Option<String>,
28 headless: bool,
29 no_wait: bool,
30 wait_timeout: u64,
31 json: bool,
32) -> Result<()> {
33 let res = ensure_started(browser, headless, no_wait, wait_timeout).await?;
34 emit(&res, json)?;
35 Ok(())
36}
37
38pub async fn ensure_started(
50 browser: Option<String>,
51 headless: bool,
52 no_wait: bool,
53 wait_timeout: u64,
54) -> Result<StartResult> {
55 let installed = detect::list_installed();
56 if installed.is_empty() {
57 anyhow::bail!("no supported browsers installed; run `browser-control list-installed`");
58 }
59
60 let resolved_kind: Kind = match browser.as_deref() {
61 None => resolve_kind_when_unspecified(&installed)
62 .ok_or_else(|| anyhow!("no chromium-based browser installed"))?,
63 Some(s) => Kind::parse(s).ok_or_else(|| {
64 anyhow!("unknown browser kind `{s}`; valid: chrome, edge, chromium, brave, firefox")
65 })?,
66 };
67
68 let installed_match = installed
69 .iter()
70 .find(|i| i.kind == resolved_kind)
71 .cloned()
72 .ok_or_else(|| {
73 anyhow!(
74 "browser `{}` is not installed on this machine",
75 resolved_kind.as_str()
76 )
77 })?;
78
79 let registry = Registry::open()?;
80 if let Some(row) = registry.first_alive_by_kind(resolved_kind)? {
81 if !no_wait {
82 crate::cli::wait::wait_until_ready(
83 &row.endpoint,
84 row.engine,
85 std::time::Duration::from_secs(wait_timeout),
86 )
87 .await?;
88 }
89 return Ok(to_result(&row, true));
90 }
91
92 let name = registry::naming::generate_default(resolved_kind, ®istry)?;
93 let profile_dir = paths::default_profile_dir(resolved_kind)?;
94 std::fs::create_dir_all(&profile_dir).context("creating profile directory")?;
95 let opts = LaunchOpts {
96 headless,
97 profile_dir: profile_dir.clone(),
98 };
99 let handle = launch::launch(&installed_match, opts)
100 .await
101 .with_context(|| format!("launching {}", installed_match.executable.display()))?;
102
103 let row = BrowserRow {
104 name: name.clone(),
105 kind: resolved_kind,
106 engine: handle.engine,
107 pid: handle.pid,
108 endpoint: handle.endpoint.clone(),
109 port: handle.port,
110 profile_dir: handle.profile_dir.clone(),
111 executable: installed_match.executable.clone(),
112 headless,
113 started_at: registry::now_iso8601(),
114 };
115 registry.insert(&row)?;
116 let _pid = handle.forget();
117
118 if !no_wait {
119 crate::cli::wait::wait_until_ready(
120 &row.endpoint,
121 row.engine,
122 std::time::Duration::from_secs(wait_timeout),
123 )
124 .await?;
125 }
126
127 Ok(to_result(&row, false))
128}
129
130pub(crate) fn first_chromium_or_first(installed: &[Installed]) -> Option<Kind> {
131 installed
132 .iter()
133 .find(|i| i.kind.is_chromium())
134 .map(|i| i.kind)
135 .or_else(|| installed.first().map(|i| i.kind))
136}
137
138pub(crate) fn resolve_kind_when_unspecified(installed: &[Installed]) -> Option<Kind> {
142 let running = detect::list_running_kinds(installed);
143 installed
144 .iter()
145 .find(|i| running.contains(&i.kind))
146 .map(|i| i.kind)
147 .or_else(|| first_chromium_or_first(installed))
148}
149
150fn to_result(row: &BrowserRow, reused: bool) -> StartResult {
151 StartResult {
152 name: row.name.clone(),
153 kind: row.kind,
154 pid: row.pid,
155 engine: row.engine,
156 endpoint: row.endpoint.clone(),
157 profile: row.profile_dir.clone(),
158 headless: row.headless,
159 started_at: row.started_at.clone(),
160 reused,
161 }
162}
163
164fn emit(res: &StartResult, json: bool) -> Result<()> {
165 if json {
166 print_json(&mut std::io::stdout(), res)?;
167 } else {
168 let reused = if res.reused { " (reused)" } else { "" };
169 println!("Started {}{}", res.name, reused);
170 println!(" kind: {}", res.kind.as_str());
171 println!(" pid: {}", res.pid);
172 println!(" engine: {:?}", res.engine);
173 println!(" endpoint: {}", res.endpoint);
174 println!(" profile: {}", res.profile.display());
175 }
176 Ok(())
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 fn spawn_stand_in_process() -> (std::process::Child, PathBuf) {
187 let exe = PathBuf::from(if cfg!(windows) {
188 r"C:\Windows\System32\timeout.exe"
189 } else {
190 "/bin/sleep"
191 });
192 assert!(exe.exists(), "test requires {exe:?} to exist");
193 let child = std::process::Command::new(&exe)
194 .arg("5")
195 .spawn()
196 .expect("spawn stand-in process");
197 (child, exe)
198 }
199
200 fn installed(kind: Kind, executable: PathBuf) -> Installed {
201 Installed {
202 kind,
203 executable,
204 version: "unknown".to_string(),
205 engine: kind.engine(),
206 }
207 }
208
209 #[test]
210 fn resolve_kind_when_unspecified_prefers_running_kind_over_hardcoded_order() {
211 let (mut child, exe) = spawn_stand_in_process();
212
213 let candidates = vec![
217 installed(Kind::Chrome, PathBuf::from("/definitely/not/a/real/chrome")),
218 installed(Kind::Firefox, exe),
219 ];
220
221 let resolved = resolve_kind_when_unspecified(&candidates);
222
223 let _ = child.kill();
224 let _ = child.wait();
225
226 assert_eq!(resolved, Some(Kind::Firefox));
227 }
228
229 #[test]
230 fn resolve_kind_when_unspecified_falls_back_when_nothing_running() {
231 let candidates = vec![
232 installed(
233 Kind::Firefox,
234 PathBuf::from("/definitely/not/a/real/firefox"),
235 ),
236 installed(Kind::Chrome, PathBuf::from("/definitely/not/a/real/chrome")),
237 ];
238 assert_eq!(
239 resolve_kind_when_unspecified(&candidates),
240 first_chromium_or_first(&candidates)
241 );
242 assert_eq!(
243 resolve_kind_when_unspecified(&candidates),
244 Some(Kind::Chrome)
245 );
246 }
247
248 #[test]
249 fn resolve_kind_when_unspecified_none_when_nothing_installed() {
250 assert_eq!(resolve_kind_when_unspecified(&[]), None);
251 }
252}