use std::path::Path;
use std::time::Duration;
use tokio::process::Command;
#[derive(Debug, Clone, Copy)]
pub(crate) enum CliTimeout {
Unbounded,
Bounded(Duration),
}
pub(crate) struct CliSpawn<'a> {
pub(crate) path: &'a Path,
pub(crate) args: &'a [&'a str],
pub(crate) session: Option<&'a str>,
pub(crate) json: bool,
pub(crate) capture_stderr: bool,
pub(crate) cancel_kills: bool,
pub(crate) input: Option<Vec<u8>>,
pub(crate) timeout: CliTimeout,
pub(crate) chrome_deadline: Option<Duration>,
}
pub(crate) enum CliRun {
Output(std::process::Output),
SpawnFailure,
TimedOut,
}
pub(crate) fn ensure_chrome_env(cmd: &mut Command) {
if std::env::var_os("HOME").is_none() {
cmd.env("HOME", "/tmp");
}
if std::env::var_os("CHROMIUM_FLAGS").is_none() {
cmd.env(
"CHROMIUM_FLAGS",
"--no-first-run --no-default-browser-check --disable-gpu",
);
}
cmd.env("AGENT_BROWSER_DEFAULT_TIMEOUT", "15000");
cmd.env("AGENT_BROWSER_IDLE_TIMEOUT_MS", "300000");
cmd.env("AGENT_BROWSER_HUMANIZE", "human");
cmd.env("CHROME_USE_NO_UPDATE_CHECK", "1");
cmd.env("AGENT_BROWSER_NO_UPDATE_CHECK", "1");
cmd.env("AGENT_BROWSER_NO_AUTO_RECONNECT", "1");
cmd.env("AGENT_BROWSER_RELAY_REVIVE_SECS", "0");
}
pub(crate) fn apply_chrome_deadline(cmd: &mut Command, deadline: Option<Duration>) {
if let Some(d) = deadline {
cmd.env("AGENT_BROWSER_DEFAULT_TIMEOUT", d.as_millis().to_string());
}
}
fn build_argv(args: &[&str], json: bool, session: Option<&str>) -> Vec<String> {
let mut global: Vec<String> = Vec::new();
if json {
global.push("--json".to_string());
}
if let Some(s) = session {
global.extend(["--session".to_string(), s.to_string()]);
}
if global.is_empty() {
return args.iter().map(|a| (*a).to_string()).collect();
}
let insert_at = args.iter().position(|a| *a == "--").unwrap_or(args.len());
let mut folded: Vec<String> = args[..insert_at].iter().map(|a| (*a).to_string()).collect();
folded.extend(global);
folded.extend(args[insert_at..].iter().map(|a| (*a).to_string()));
folded
}
pub(crate) async fn spawn_cli(spec: CliSpawn<'_>) -> CliRun {
let mut cmd = Command::new(spec.path);
ensure_chrome_env(&mut cmd);
apply_chrome_deadline(&mut cmd, spec.chrome_deadline);
cmd.args(build_argv(spec.args, spec.json, spec.session));
if spec.json {
cmd.stdout(std::process::Stdio::piped());
} else {
cmd.stdout(std::process::Stdio::null());
}
if spec.capture_stderr {
cmd.stderr(std::process::Stdio::piped());
} else {
cmd.stderr(std::process::Stdio::null());
}
cmd.kill_on_drop(spec.cancel_kills);
if spec.input.is_some() {
cmd.stdin(std::process::Stdio::piped());
}
let run = async {
let Some(data) = spec.input else {
return cmd.output().await;
};
let mut child = cmd.spawn()?;
let mut stdin = child.stdin.take().expect("stdin piped when input is set");
let writer = tokio::task::spawn(async move {
use tokio::io::AsyncWriteExt;
let _ = stdin.write_all(&data).await;
let _ = stdin.shutdown().await;
});
let out = child.wait_with_output().await;
let _ = writer.await; out
};
match spec.timeout {
CliTimeout::Unbounded => match run.await {
Ok(out) => CliRun::Output(out),
Err(_) => CliRun::SpawnFailure,
},
CliTimeout::Bounded(d) => match tokio::time::timeout(d, run).await {
Err(_) => CliRun::TimedOut,
Ok(Err(_)) => CliRun::SpawnFailure,
Ok(Ok(out)) => CliRun::Output(out),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::test::set_env_var;
use tokio::process::Command;
fn cmd_env(cmd: &Command, key: &str) -> Option<String> {
cmd.as_std()
.get_envs()
.find(|(k, _)| *k == std::ffi::OsStr::new(key))
.and_then(|(_, v)| v.map(|v| v.to_string_lossy().into_owned()))
}
#[test]
fn build_argv_folds_global_flags_before_dash_shield() {
assert_eq!(
build_argv(
&["fill", "#q", "--", "-tail"],
true,
Some("mahbot-chrome-abc")
),
[
"fill",
"#q",
"--json",
"--session",
"mahbot-chrome-abc",
"--",
"-tail"
]
);
assert_eq!(
build_argv(&["open", "https://x"], true, Some("s")),
["open", "https://x", "--json", "--session", "s"]
);
assert_eq!(
build_argv(&["session", "list"], false, None),
["session", "list"]
);
assert_eq!(
build_argv(&["session", "list"], false, Some("s")),
["session", "list", "--session", "s"]
);
}
#[test]
fn ensure_chrome_env_defaults_home_only_when_missing() {
{
let _guard = set_env_var("HOME", None);
let mut cmd = Command::new("true");
ensure_chrome_env(&mut cmd);
assert_eq!(cmd_env(&cmd, "HOME").as_deref(), Some("/tmp"));
}
{
let _guard = set_env_var("HOME", Some("/home/user"));
let mut cmd = Command::new("true");
ensure_chrome_env(&mut cmd);
assert_eq!(cmd_env(&cmd, "HOME"), None);
}
}
#[test]
fn ensure_chrome_env_defaults_chromium_flags_only_when_missing() {
{
let _guard = set_env_var("CHROMIUM_FLAGS", None);
let mut cmd = Command::new("true");
ensure_chrome_env(&mut cmd);
assert_eq!(
cmd_env(&cmd, "CHROMIUM_FLAGS").as_deref(),
Some("--no-first-run --no-default-browser-check --disable-gpu")
);
}
{
let _guard = set_env_var("CHROMIUM_FLAGS", Some("--headless"));
let mut cmd = Command::new("true");
ensure_chrome_env(&mut cmd);
assert_eq!(cmd_env(&cmd, "CHROMIUM_FLAGS"), None);
}
}
#[test]
fn ensure_chrome_env_sets_fixed_env_vars() {
let mut cmd = Command::new("true");
ensure_chrome_env(&mut cmd);
assert_eq!(
cmd_env(&cmd, "AGENT_BROWSER_DEFAULT_TIMEOUT").as_deref(),
Some("15000")
);
assert_eq!(
cmd_env(&cmd, "AGENT_BROWSER_IDLE_TIMEOUT_MS").as_deref(),
Some("300000")
);
assert_eq!(
cmd_env(&cmd, "AGENT_BROWSER_NO_AUTO_RECONNECT").as_deref(),
Some("1")
);
}
#[test]
fn chrome_deadline_override_replaces_env_default() {
let mut cmd = Command::new("true");
ensure_chrome_env(&mut cmd);
apply_chrome_deadline(&mut cmd, Some(Duration::from_secs(18)));
assert_eq!(
cmd_env(&cmd, "AGENT_BROWSER_DEFAULT_TIMEOUT").as_deref(),
Some("18000")
);
let mut cmd = Command::new("true");
ensure_chrome_env(&mut cmd);
apply_chrome_deadline(&mut cmd, None);
assert_eq!(
cmd_env(&cmd, "AGENT_BROWSER_DEFAULT_TIMEOUT").as_deref(),
Some("15000")
);
}
}