rexy 0.1.0

Launch a browser with a local MITM proxy for transparent dev redirects
use std::path::{Path, PathBuf};
use std::process::Stdio;

use tokio::process::{Child, Command};

#[derive(Debug, Clone)]
pub struct Browser {
    executable: PathBuf,
}

pub fn resolve_browser(value: &str) -> Result<Browser, Box<dyn std::error::Error>> {
    let executable = match value {
        "chrome" => find_chrome()?,
        "chromium" => find_chromium()?,
        path => {
            let path = PathBuf::from(path);

            if !path.exists() {
                return Err(
                    format!("browser executable does not exist: {}", path.display()).into(),
                );
            }

            path
        }
    };

    Ok(Browser { executable })
}

impl Browser {
    pub fn spawn(
        &self,
        pac_url: &str,
        browser_args: &[String],
    ) -> Result<Child, Box<dyn std::error::Error>> {
        let mut command = Command::new(&self.executable);

        command
            .arg(format!("--proxy-pac-url={pac_url}"))
            .arg("--disable-quic")
            .args(browser_args)
            .stdin(Stdio::null())
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit());

        tracing::info!(
            executable = %self.executable.display(),
            pac_url = %pac_url,
            "starting browser"
        );

        let child = command.spawn()?;

        Ok(child)
    }
}

fn find_chrome() -> Result<PathBuf, Box<dyn std::error::Error>> {
    #[cfg(target_os = "macos")]
    {
        let candidates = [
            "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
            "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
        ];

        for candidate in candidates {
            if Path::new(candidate).exists() {
                return Ok(PathBuf::from(candidate));
            }
        }
    }

    #[cfg(target_os = "windows")]
    {
        let candidates = [
            std::env::var("PROGRAMFILES")
                .ok()
                .map(|v| PathBuf::from(v).join("Google/Chrome/Application/chrome.exe")),
            std::env::var("PROGRAMFILES(X86)")
                .ok()
                .map(|v| PathBuf::from(v).join("Google/Chrome/Application/chrome.exe")),
            std::env::var("LOCALAPPDATA")
                .ok()
                .map(|v| PathBuf::from(v).join("Google/Chrome/Application/chrome.exe")),
        ];

        for candidate in candidates.into_iter().flatten() {
            if candidate.exists() {
                return Ok(candidate);
            }
        }
    }

    #[cfg(target_os = "linux")]
    {
        for candidate in [
            "/usr/bin/google-chrome",
            "/usr/bin/google-chrome-stable",
            "/usr/bin/chromium",
            "/usr/bin/chromium-browser",
        ] {
            if Path::new(candidate).exists() {
                return Ok(PathBuf::from(candidate));
            }
        }
    }

    Err("Google Chrome was not found; pass --browser /path/to/chrome".into())
}

fn find_chromium() -> Result<PathBuf, Box<dyn std::error::Error>> {
    #[cfg(target_os = "macos")]
    {
        let candidates = ["/Applications/Chromium.app/Contents/MacOS/Chromium"];

        for candidate in candidates {
            if Path::new(candidate).exists() {
                return Ok(PathBuf::from(candidate));
            }
        }
    }

    #[cfg(target_os = "windows")]
    {
        let candidates = [std::env::var("LOCALAPPDATA")
            .ok()
            .map(|v| PathBuf::from(v).join("Chromium/Application/chrome.exe"))];

        for candidate in candidates.into_iter().flatten() {
            if candidate.exists() {
                return Ok(candidate);
            }
        }
    }

    #[cfg(target_os = "linux")]
    {
        for candidate in ["/usr/bin/chromium", "/usr/bin/chromium-browser"] {
            if Path::new(candidate).exists() {
                return Ok(PathBuf::from(candidate));
            }
        }
    }

    Err("Chromium was not found; pass --browser /path/to/chromium".into())
}