Cherry-picks upstream agent-browser #1396. Adds a configurable --hide-scrollbars flag (AGENT_BROWSER_HIDE_SCROLLBARS env, hideScrollbars config key, default true) that appends Chrome's --hide-scrollbars launch arg for headless (non-extension) launches so native scrollbars aren't painted into screenshots. Plumbed through flags.rs, connection.rs, main.rs, native/actions.rs and native/cdp/chrome.rs; help text in output.rs + skill-data. Fork adaptation: - the arg lands in the headless && !has_extensions block, separate from the stealth base args — no interaction with anti-detection. - dropped upstream docs/, agent-browser.schema.json and README hunks (removed or rewritten in this fork). Verified: cargo check --tests passes.
189 lines
5.3 KiB
Rust
189 lines
5.3 KiB
Rust
//! Live launch test: spawn a scratch daemon session, launch headless
|
|
//! Chrome, navigate to `about:blank`, then close. Skipped under `--quick`.
|
|
//!
|
|
//! A `LaunchGuard` Drop impl ensures the scratch session is closed and its
|
|
//! sidecar files cleaned even on panic or early return.
|
|
|
|
use std::env;
|
|
use std::time::{Duration, Instant, SystemTime};
|
|
|
|
use serde_json::{json, Value};
|
|
|
|
use super::helpers::new_id;
|
|
use super::{Check, Status};
|
|
use crate::connection::{cleanup_stale_files, ensure_daemon, send_command, DaemonOptions};
|
|
|
|
pub(super) fn check(checks: &mut Vec<Check>) {
|
|
let category = "Launch test";
|
|
|
|
if env::var("AGENT_BROWSER_PROVIDER").is_ok() {
|
|
checks.push(Check::new(
|
|
"launch.skipped.provider",
|
|
category,
|
|
Status::Info,
|
|
"Skipped (AGENT_BROWSER_PROVIDER is set; would consume cloud quota)",
|
|
));
|
|
return;
|
|
}
|
|
if env::var("AGENT_BROWSER_CDP").is_ok() {
|
|
checks.push(Check::new(
|
|
"launch.skipped.cdp",
|
|
category,
|
|
Status::Info,
|
|
"Skipped (AGENT_BROWSER_CDP is set; would attach to a real browser)",
|
|
));
|
|
return;
|
|
}
|
|
|
|
let session = format!(
|
|
"doctor-{}-{}",
|
|
std::process::id(),
|
|
SystemTime::now()
|
|
.duration_since(SystemTime::UNIX_EPOCH)
|
|
.map(|d| d.as_millis())
|
|
.unwrap_or(0)
|
|
);
|
|
|
|
// Armed after `ensure_daemon` succeeds so we don't send a stray `close`
|
|
// or delete sidecar files for a daemon that never started. On every early
|
|
// return past the `Some(...)` assignment below, Drop runs one close and
|
|
// one `cleanup_stale_files`.
|
|
let mut _guard: Option<LaunchGuard> = None;
|
|
|
|
let opts = DaemonOptions {
|
|
headed: false,
|
|
debug: false,
|
|
executable_path: None,
|
|
extensions: &[],
|
|
init_scripts: &[],
|
|
enable: &[],
|
|
args: None,
|
|
user_agent: None,
|
|
proxy: None,
|
|
proxy_bypass: None,
|
|
proxy_username: None,
|
|
proxy_password: None,
|
|
ignore_https_errors: false,
|
|
allow_file_access: false,
|
|
hide_scrollbars: true,
|
|
profile: None,
|
|
state: None,
|
|
provider: None,
|
|
device: None,
|
|
session_name: None,
|
|
download_path: None,
|
|
allowed_domains: None,
|
|
action_policy: None,
|
|
confirm_actions: None,
|
|
engine: None,
|
|
auto_connect: false,
|
|
force_launch: false,
|
|
idle_timeout: None,
|
|
default_timeout: None,
|
|
cdp: None,
|
|
no_auto_dialog: false,
|
|
};
|
|
|
|
let started = Instant::now();
|
|
if let Err(e) = ensure_daemon(&session, &opts) {
|
|
checks.push(
|
|
Check::new(
|
|
"launch.daemon",
|
|
category,
|
|
Status::Fail,
|
|
format!("Could not start daemon: {}", e),
|
|
)
|
|
.with_fix("check Chrome install and re-run with --debug"),
|
|
);
|
|
return;
|
|
}
|
|
_guard = Some(LaunchGuard {
|
|
session: session.clone(),
|
|
});
|
|
|
|
let launch_cmd = json!({
|
|
"id": new_id(),
|
|
"action": "launch",
|
|
"headless": true,
|
|
});
|
|
if let Err(e) = send_json(launch_cmd, &session) {
|
|
checks.push(
|
|
Check::new(
|
|
"launch.launch",
|
|
category,
|
|
Status::Fail,
|
|
format!("Browser launch failed: {}", e),
|
|
)
|
|
.with_fix("agent-browser install # or check --debug output"),
|
|
);
|
|
return;
|
|
}
|
|
|
|
let open_cmd = json!({
|
|
"id": new_id(),
|
|
"action": "navigate",
|
|
"url": "about:blank",
|
|
});
|
|
if let Err(e) = send_json(open_cmd, &session) {
|
|
checks.push(
|
|
Check::new(
|
|
"launch.navigate",
|
|
category,
|
|
Status::Fail,
|
|
format!("Navigation to about:blank failed: {}", e),
|
|
)
|
|
.with_fix("re-run with --debug for full launch logs"),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Close + stale-file cleanup happen exactly once via LaunchGuard::drop at
|
|
// end of scope; no explicit close here.
|
|
let elapsed = started.elapsed();
|
|
let secs = elapsed.as_secs_f64();
|
|
if elapsed > Duration::from_secs(5) {
|
|
checks.push(Check::new(
|
|
"launch.elapsed",
|
|
category,
|
|
Status::Warn,
|
|
format!(
|
|
"Headless launch + about:blank in {:.2}s (slow; expected < 5s)",
|
|
secs
|
|
),
|
|
));
|
|
} else {
|
|
checks.push(Check::new(
|
|
"launch.elapsed",
|
|
category,
|
|
Status::Pass,
|
|
format!("Headless launch + about:blank in {:.2}s", secs),
|
|
));
|
|
}
|
|
}
|
|
|
|
fn send_json(cmd: Value, session: &str) -> Result<(), String> {
|
|
match send_command(cmd, session) {
|
|
Ok(resp) => {
|
|
if resp.success {
|
|
Ok(())
|
|
} else {
|
|
Err(resp.error.unwrap_or_else(|| "unknown error".to_string()))
|
|
}
|
|
}
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
/// Best-effort cleanup when the launch test panics or returns early.
|
|
struct LaunchGuard {
|
|
session: String,
|
|
}
|
|
|
|
impl Drop for LaunchGuard {
|
|
fn drop(&mut self) {
|
|
let close_cmd = json!({ "id": new_id(), "action": "close" });
|
|
let _ = send_command(close_cmd, &self.session);
|
|
cleanup_stale_files(&self.session);
|
|
}
|
|
}
|