diff --git a/server/packages/sandbox-agent/src/browser_install.rs b/server/packages/sandbox-agent/src/browser_install.rs new file mode 100644 index 0000000..a67902c --- /dev/null +++ b/server/packages/sandbox-agent/src/browser_install.rs @@ -0,0 +1,153 @@ +use crate::desktop_install::{ + detect_package_manager, find_binary, prompt_yes_no, render_install_command, + run_install_commands, running_as_root, DesktopPackageManager, +}; + +const AUTOMATIC_INSTALL_SUPPORTED_DISTROS: &str = + "Automatic browser dependency installation is supported on Debian/Ubuntu (apt), Fedora/RHEL (dnf), and Alpine (apk)."; +const AUTOMATIC_INSTALL_UNSUPPORTED_ENVS: &str = + "Automatic installation is not supported on macOS, Windows, or Linux distributions without apt, dnf, or apk."; + +#[derive(Debug, Clone)] +pub struct BrowserInstallRequest { + pub yes: bool, + pub print_only: bool, + pub package_manager: Option, +} + +pub(crate) fn browser_platform_support_message() -> String { + format!("Browser APIs are only supported on Linux. {AUTOMATIC_INSTALL_SUPPORTED_DISTROS}") +} + +fn linux_install_support_message() -> String { + format!("{AUTOMATIC_INSTALL_SUPPORTED_DISTROS} {AUTOMATIC_INSTALL_UNSUPPORTED_ENVS}") +} + +pub fn install_browser(request: BrowserInstallRequest) -> Result<(), String> { + if std::env::consts::OS != "linux" { + return Err(format!( + "browser installation is only supported on Linux. {}", + linux_install_support_message() + )); + } + + let package_manager = match request.package_manager { + Some(value) => value, + None => detect_package_manager().ok_or_else(|| { + format!( + "could not detect a supported package manager. {} Install the browser dependencies manually on this distribution.", + linux_install_support_message() + ) + })?, + }; + + let packages = browser_packages(package_manager); + let used_sudo = !running_as_root() && find_binary("sudo").is_some(); + if !running_as_root() && !used_sudo { + return Err( + "browser installation requires root or sudo access; rerun as root or install dependencies manually" + .to_string(), + ); + } + + println!("Browser package manager: {}", package_manager); + println!("Browser packages:"); + for package in &packages { + println!(" - {package}"); + } + println!("Install command:"); + println!( + " {}", + render_install_command(package_manager, used_sudo, &packages) + ); + + if request.print_only { + return Ok(()); + } + + if !request.yes && !prompt_yes_no("Proceed with browser dependency installation? [y/N] ")? { + return Err("installation cancelled".to_string()); + } + + run_install_commands(package_manager, used_sudo, &packages)?; + + println!("Browser dependencies installed."); + Ok(()) +} + +fn browser_packages(package_manager: DesktopPackageManager) -> Vec { + match package_manager { + DesktopPackageManager::Apt => vec![ + "chromium", + "chromium-sandbox", + "libnss3", + "libatk-bridge2.0-0", + "libdrm2", + "libxcomposite1", + "libxdamage1", + "libxrandr2", + "libgbm1", + "libasound2", + "libpangocairo-1.0-0", + "libgtk-3-0", + ], + DesktopPackageManager::Dnf => vec!["chromium"], + DesktopPackageManager::Apk => vec!["chromium", "nss"], + } + .into_iter() + .map(str::to_string) + .collect() +} + +/// Checks for missing browser dependencies (Chromium binary and desktop libs). +pub(crate) fn detect_missing_browser_dependencies() -> Vec { + let mut missing = Vec::new(); + + // Check for chromium binary (may be named chromium or chromium-browser) + if find_binary("chromium").is_none() && find_binary("chromium-browser").is_none() { + missing.push("chromium".to_string()); + } + + // Check for key desktop dependency libraries + for (name, binary) in [("Xvfb", "Xvfb"), ("xrandr", "xrandr")] { + if find_binary(binary).is_none() { + missing.push(name.to_string()); + } + } + + missing +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn browser_platform_support_message_mentions_linux_and_supported_distros() { + let message = browser_platform_support_message(); + assert!(message.contains("only supported on Linux")); + assert!(message.contains("Debian/Ubuntu (apt)")); + assert!(message.contains("Fedora/RHEL (dnf)")); + assert!(message.contains("Alpine (apk)")); + } + + #[test] + fn browser_packages_apt_includes_chromium_and_libs() { + let packages = browser_packages(DesktopPackageManager::Apt); + assert!(packages.iter().any(|p| p == "chromium")); + assert!(packages.iter().any(|p| p == "libnss3")); + assert!(packages.iter().any(|p| p == "libgbm1")); + } + + #[test] + fn browser_packages_dnf_includes_chromium() { + let packages = browser_packages(DesktopPackageManager::Dnf); + assert_eq!(packages, vec!["chromium"]); + } + + #[test] + fn browser_packages_apk_includes_chromium_and_nss() { + let packages = browser_packages(DesktopPackageManager::Apk); + assert_eq!(packages, vec!["chromium", "nss"]); + } +} diff --git a/server/packages/sandbox-agent/src/desktop_install.rs b/server/packages/sandbox-agent/src/desktop_install.rs index 480da7d..d4745ad 100644 --- a/server/packages/sandbox-agent/src/desktop_install.rs +++ b/server/packages/sandbox-agent/src/desktop_install.rs @@ -85,7 +85,7 @@ pub fn install_desktop(request: DesktopInstallRequest) -> Result<(), String> { Ok(()) } -fn detect_package_manager() -> Option { +pub(crate) fn detect_package_manager() -> Option { if find_binary("apt-get").is_some() { return Some(DesktopPackageManager::Apt); } @@ -149,7 +149,7 @@ fn desktop_packages(package_manager: DesktopPackageManager, no_fonts: bool) -> V packages } -fn render_install_command( +pub(crate) fn render_install_command( package_manager: DesktopPackageManager, used_sudo: bool, packages: &[String], @@ -169,7 +169,7 @@ fn render_install_command( } } -fn run_install_commands( +pub(crate) fn run_install_commands( package_manager: DesktopPackageManager, used_sudo: bool, packages: &[String], @@ -233,7 +233,7 @@ fn run_command((program, args): (String, Vec)) -> Result<(), String> { Ok(()) } -fn prompt_yes_no(prompt: &str) -> Result { +pub(crate) fn prompt_yes_no(prompt: &str) -> Result { print!("{prompt}"); io::stdout() .flush() @@ -246,7 +246,7 @@ fn prompt_yes_no(prompt: &str) -> Result { Ok(matches!(normalized.as_str(), "y" | "yes")) } -fn running_as_root() -> bool { +pub(crate) fn running_as_root() -> bool { #[cfg(unix)] unsafe { return libc::geteuid() == 0; @@ -257,7 +257,7 @@ fn running_as_root() -> bool { } } -fn find_binary(name: &str) -> Option { +pub(crate) fn find_binary(name: &str) -> Option { let path_env = std::env::var_os("PATH")?; for path in std::env::split_paths(&path_env) { let candidate = path.join(name); diff --git a/server/packages/sandbox-agent/src/lib.rs b/server/packages/sandbox-agent/src/lib.rs index d7b92d6..faa1d52 100644 --- a/server/packages/sandbox-agent/src/lib.rs +++ b/server/packages/sandbox-agent/src/lib.rs @@ -1,6 +1,7 @@ //! Sandbox agent core utilities. mod acp_proxy_runtime; +mod browser_install; pub mod cli; pub mod daemon; mod desktop_errors;