browser_automation_cli/doctor/
mod.rs1use serde_json::json;
4
5use crate::envelope::print_success_json;
6use crate::install;
7use crate::native::cdp::chrome;
8
9#[derive(Default, Clone, Copy)]
11pub struct DoctorOptions {
12 pub offline: bool,
14 pub quick: bool,
16 pub fix: bool,
18 pub json: bool,
20}
21
22pub fn run_doctor(opts: DoctorOptions) -> i32 {
24 let mut checks = Vec::new();
25 let mut failed = false;
26
27 let chrome = chrome::find_chrome();
28 match &chrome {
29 Some(p) => checks.push(
30 json!({"id":"chrome","status":"pass","message": format!("found {}", p.display())}),
31 ),
32 None => {
33 failed = true;
34 checks.push(json!({
35 "id":"chrome",
36 "status":"fail",
37 "message":"Chrome/Chromium not found on PATH or cache",
38 "fix":"install Chromium or set executable path"
39 }));
40 }
41 }
42
43 let cache = install::get_browsers_dir();
44 checks.push(json!({
45 "id":"browsers_dir",
46 "status":"info",
47 "message": format!("cache dir {}", cache.display())
48 }));
49
50 if !opts.quick && chrome.is_some() {
51 match crate::browser::block_on_browser(async {
52 let mut s = crate::browser::OneShotSession::launch_headless().await?;
53 let _ = s
54 .goto("about:blank", crate::robots::RobotsPolicy::Ignore)
55 .await?;
56 let _ = s.shutdown().await;
57 Ok::<_, crate::error::CliError>(())
58 }) {
59 Ok(()) => checks
60 .push(json!({"id":"launch","status":"pass","message":"headless about:blank ok"})),
61 Err(e) => {
62 failed = true;
63 checks.push(json!({"id":"launch","status":"fail","message": e.message()}));
64 }
65 }
66 }
67
68 let lh_resolved = resolve_lighthouse_for_doctor();
70 let lighthouse_present = lh_resolved.is_some();
71 match &lh_resolved {
72 Some((path, source)) => checks.push(json!({
73 "id": "lighthouse",
74 "status": "pass",
75 "message": format!("found {path} (source={source})"),
76 "lighthouse_present": true,
77 "lighthouse_resolved": path,
78 "lighthouse_source": source,
79 })),
80 None => {
81 let mut fix_msg = None;
82 if opts.fix {
83 fix_msg = Some(
84 "browser-automation-cli config set lighthouse_path /absolute/path/to/lighthouse",
85 );
86 }
87 let xdg_path = crate::xdg::lighthouse_path_from_config().filter(|s| !s.is_empty());
88 let mut entry = json!({
89 "id": "lighthouse",
90 "status": "info",
91 "message": "lighthouse not on PATH or XDG (optional external binary; e2e may use mock-lighthouse.sh)",
92 "lighthouse_present": false,
93 "lighthouse_resolved": null,
94 "lighthouse_source": "missing",
95 "lighthouse_path_xdg": xdg_path,
96 "suggestion": "browser-automation-cli config set lighthouse_path /absolute/path/to/lighthouse",
97 });
98 if let Some(fix) = fix_msg {
99 entry
100 .as_object_mut()
101 .unwrap()
102 .insert("fix".into(), json!(fix));
103 }
104 checks.push(entry);
105 }
106 }
107
108 checks.push(cache_redis_check());
110
111 let ffmpeg_present = which_bin("ffmpeg");
112 checks.push(json!({
113 "id": "ffmpeg",
114 "status": if ffmpeg_present.is_some() { "pass" } else { "info" },
115 "message": ffmpeg_present
116 .as_deref()
117 .map(|p| format!("found {p}"))
118 .unwrap_or_else(|| "ffmpeg not on PATH (optional for media pipelines)".into()),
119 "ffmpeg_present": ffmpeg_present.is_some(),
120 }));
121
122 checks.push(json!({
124 "id": "windows_job_object",
125 "status": "info",
126 "message": crate::win_job::capability_summary(),
127 "supported": crate::win_job::platform_supports_job_objects(),
128 }));
129
130 if opts.offline {
132 checks.push(json!({
133 "id": "offline",
134 "status": "pass",
135 "message": "offline mode: network probes skipped",
136 "offline": true,
137 }));
138 }
139
140 if opts.fix && chrome.is_none() {
143 if let Some(c) = checks
144 .iter_mut()
145 .find(|c| c.get("id").and_then(|v| v.as_str()) == Some("chrome"))
146 {
147 if let Some(obj) = c.as_object_mut() {
148 obj.insert(
149 "fix".into(),
150 json!("install system Chrome/Chromium or: browser-automation-cli config set chrome_path /path/to/chrome"),
151 );
152 }
153 }
154 }
155
156 let data = json!({
157 "schema_version": 1,
158 "checks": checks,
159 "lighthouse_present": lighthouse_present,
160 "ffmpeg_present": ffmpeg_present.is_some(),
161 "offline": opts.offline,
162 "fix_requested": opts.fix,
163 "ok": !failed,
164 });
165
166 if opts.json {
167 let _ = print_success_json(data);
168 } else {
169 for c in checks {
170 println!(
171 "[{}] {} — {}",
172 c.get("status").and_then(|s| s.as_str()).unwrap_or("?"),
173 c.get("id").and_then(|s| s.as_str()).unwrap_or("?"),
174 c.get("message").and_then(|s| s.as_str()).unwrap_or("")
175 );
176 }
177 }
178
179 if failed {
180 1
181 } else {
182 0
183 }
184}
185
186fn resolve_lighthouse_for_doctor() -> Option<(String, &'static str)> {
188 if let Some(xdg) = crate::xdg::lighthouse_path_from_config().filter(|s| !s.is_empty()) {
189 let p = std::path::Path::new(&xdg);
190 if p.is_file() {
191 let source = if xdg.contains("mock-lighthouse") {
192 "mock"
193 } else {
194 "xdg"
195 };
196 return Some((xdg, source));
197 }
198 }
199 which_bin("lighthouse").map(|p| (p, "path"))
200}
201
202fn cache_redis_check() -> serde_json::Value {
204 let cfg = crate::xdg::load_config().unwrap_or_default();
205 let backend = cfg
206 .cache_backend
207 .as_deref()
208 .unwrap_or("sqlite")
209 .to_ascii_lowercase();
210 if backend != "redis" {
211 return json!({
212 "id": "cache_redis",
213 "status": "info",
214 "backend": backend,
215 "message": format!("redis not active (cache_backend={backend})"),
216 });
217 }
218 let url = cfg.cache_redis_url.as_deref().unwrap_or("");
219 match crate::cache::RedisCache::connect(url) {
220 Ok(_) => json!({
221 "id": "cache_redis",
222 "status": "pass",
223 "backend": "redis",
224 "message": "redis PING ok (XDG cache_redis_url)",
225 }),
226 Err(e) => json!({
227 "id": "cache_redis",
228 "status": "fail",
229 "backend": "redis",
230 "message": e.message(),
231 "suggestion": "Start redis-server or: browser-automation-cli config set cache_backend sqlite",
232 }),
233 }
234}
235
236fn which_bin(name: &str) -> Option<String> {
237 std::env::var_os("PATH").and_then(|paths| {
238 for dir in std::env::split_paths(&paths) {
239 let candidate = dir.join(name);
240 if candidate.is_file() {
241 return Some(candidate.display().to_string());
242 }
243 #[cfg(windows)]
244 {
245 let with_exe = dir.join(format!("{name}.exe"));
246 if with_exe.is_file() {
247 return Some(with_exe.display().to_string());
248 }
249 }
250 }
251 None
252 })
253}