use crate::BoxError;
use crate::browser::{self, BrowserOptions};
use crate::cdp::client::CdpClient;
use crate::cli::{Cli, Command};
use crate::run_helpers::{connect_page, resolve_page_target};
use crate::session::{self, SessionStore};
const fn want_headless(headed_flag: bool, stored_headless: Option<bool>) -> bool {
if headed_flag {
return false;
}
match stored_headless {
Some(headless) => headless,
None => true,
}
}
pub async fn resolve_cli_connection(
cli: &Cli,
) -> Result<(SessionStore, CdpClient, CdpClient, String), BoxError> {
let mut store = session::load_session()?;
let requested_proxy =
browser::normalized_proxy_option(cli.connect.as_deref(), cli.proxy_server.as_deref())?;
let requested_chrome_args =
browser::normalized_chrome_args_option(cli.connect.as_deref(), &cli.chrome_args)?;
let existing_mode = store.browsers.get(&cli.browser).map(|b| b.headless);
let want_headless = want_headless(cli.headed, existing_mode);
let stored_proxy = store
.browsers
.get(&cli.browser)
.and_then(|b| b.proxy_server.clone());
let effective_proxy = requested_proxy.clone().or(stored_proxy);
let effective_chrome_args =
crate::chrome_args::effective_chrome_args(&store, &cli.browser, &requested_chrome_args);
let (conn, browser_client) = if let Some(existing) = store.browsers.get(&cli.browser) {
let mode_matches = existing.headless == want_headless;
let ws = &existing.ws_endpoint;
let http = browser::extract_http_from_ws(ws);
if mode_matches {
if let Ok(client) = CdpClient::connect(ws).await {
session::ensure_proxy_compatible(existing, requested_proxy.as_deref())?;
session::ensure_chrome_args_compatible(existing, &requested_chrome_args)?;
let conn = browser::BrowserConnection {
ws_endpoint: ws.clone(),
http_endpoint: Some(http),
pid: existing.pid,
};
(conn, client)
} else {
if let Some(pid) = existing.pid {
browser::kill_and_await_exit(&cli.browser, pid)?;
}
store.browsers.remove(&cli.browser);
let opts = BrowserOptions {
name: cli.browser.clone(),
headless: want_headless,
ignore_https_errors: cli.ignore_https_errors,
stealth: cli.stealth,
connect: cli.connect.clone(),
proxy_server: effective_proxy.clone(),
copy_cookies: cli.copy_cookies,
chrome_args: effective_chrome_args.clone(),
};
let conn = Box::pin(browser::resolve_browser(&opts)).await?;
let client = CdpClient::connect(&conn.ws_endpoint).await?;
(conn, client)
}
} else {
if let Some(pid) = existing.pid {
browser::kill_and_await_exit(&cli.browser, pid)?;
}
store.browsers.remove(&cli.browser);
let opts = BrowserOptions {
name: cli.browser.clone(),
headless: want_headless,
ignore_https_errors: cli.ignore_https_errors,
stealth: cli.stealth,
connect: cli.connect.clone(),
proxy_server: effective_proxy.clone(),
copy_cookies: cli.copy_cookies,
chrome_args: effective_chrome_args.clone(),
};
let conn = Box::pin(browser::resolve_browser(&opts)).await?;
let client = CdpClient::connect(&conn.ws_endpoint).await?;
(conn, client)
}
} else {
let needs_existing = !matches!(cli.command, Command::Goto { .. } | Command::Pipe);
if needs_existing {
return Err(format!(
"No browser session '{}'. Run `chrome-agent --browser {} goto <url>` first.",
cli.browser, cli.browser
)
.into());
}
let opts = BrowserOptions {
name: cli.browser.clone(),
headless: want_headless,
ignore_https_errors: cli.ignore_https_errors,
stealth: cli.stealth,
connect: cli.connect.clone(),
proxy_server: effective_proxy.clone(),
copy_cookies: cli.copy_cookies,
chrome_args: effective_chrome_args.clone(),
};
let conn = Box::pin(browser::resolve_browser(&opts)).await?;
let client = CdpClient::connect(&conn.ws_endpoint).await?;
(conn, client)
};
browser_client.set_call_timeout(std::time::Duration::from_secs(cli.timeout));
let http_endpoint = conn
.http_endpoint
.as_deref()
.ok_or("No HTTP endpoint available. Cannot resolve page WebSocket URL.")?;
let target_id = {
let browser_session = session::ensure_browser(
&mut store,
&cli.browser,
&conn.ws_endpoint,
conn.pid,
want_headless,
effective_proxy,
effective_chrome_args,
);
resolve_page_target(&browser_client, browser_session, &cli.page).await?
};
session::save_session(&mut store)?;
let client = Box::pin(connect_page(http_endpoint, &target_id, cli.stealth)).await?;
Ok((store, browser_client, client, target_id))
}
#[cfg(test)]
mod tests {
use super::want_headless;
use clap::Parser as _;
#[test]
fn an_explicit_headed_overrules_a_stored_mode_and_its_absence_does_not() {
assert!(
!want_headless(true, Some(true)),
"--headed against a headless browser must ask for headed, i.e. relaunch"
);
assert!(
!want_headless(true, None),
"--headed on a first launch is headed"
);
assert!(
!want_headless(true, Some(false)),
"--headed on a headed browser: no change"
);
assert!(
!want_headless(false, Some(false)),
"a plain command against a HEADED browser must not ask for headless (it would kill it)"
);
assert!(
want_headless(false, Some(true)),
"a headless browser stays headless"
);
assert!(
want_headless(false, None),
"headless is the default for a first launch"
);
}
#[cfg(unix)]
#[test]
fn a_pid_that_is_not_a_browser_is_neither_killed_nor_waited_for() {
let mut bystander = std::process::Command::new("sleep")
.arg("30")
.spawn()
.expect("stand-in for a recycled pid");
let started = std::time::Instant::now();
let outcome = crate::browser::kill_and_await_exit("test-not-a-browser", bystander.id());
let elapsed = started.elapsed();
assert!(outcome.is_ok(), "a refused kill is not a failed relaunch");
assert!(
elapsed < std::time::Duration::from_secs(1),
"nothing was signalled, so nothing may be waited out: took {elapsed:?}"
);
assert!(
bystander.try_wait().expect("poll the stand-in").is_none(),
"a pid the guard refused must still be running"
);
let _ = bystander.kill();
let _ = bystander.wait();
}
#[cfg(unix)]
#[test]
fn an_already_dead_browser_does_not_block_the_relaunch() {
let mut child = std::process::Command::new("sleep")
.arg("30")
.spawn()
.expect("spawn a pid we can then free");
let pid = child.id();
child.kill().expect("signal it");
child.wait().expect("reap it");
let started = std::time::Instant::now();
assert!(crate::browser::kill_and_await_exit("test-already-gone", pid).is_ok());
assert!(
started.elapsed() < std::time::Duration::from_secs(1),
"a dead pid is answered without a wait"
);
}
#[test]
fn the_headed_flag_is_true_only_when_it_was_written() {
let plain = crate::cli::Cli::try_parse_from(["chrome-agent", "goto", "https://e.com"])
.expect("a plain goto parses");
assert!(!plain.headed, "omitted --headed must not read as passed");
for argv in [
vec!["chrome-agent", "--headed", "goto", "https://e.com"],
vec!["chrome-agent", "goto", "--headed", "https://e.com"],
] {
let cli = crate::cli::Cli::try_parse_from(argv.clone()).expect("global flag parses");
assert!(cli.headed, "--headed must read as passed in {argv:?}");
}
assert!(
crate::cli::Cli::try_parse_from(["chrome-agent", "--headed=false", "goto", "u"])
.is_err(),
"--headed must take no value for `headed == true` to mean `explicitly passed`"
);
}
}