use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::Duration;
use serde_json::{Value, json};
use tokio::time::sleep;
use crate::cdp::core::CdpCore;
use crate::cdp::tab::ChromiumTab;
use crate::protocol::Connection;
use crate::{Error, Result};
pub struct ChromiumBrowser {
conn: Connection,
child: Option<tokio::process::Child>,
user_data_dir: Option<PathBuf>,
}
impl ChromiumBrowser {
pub async fn launch(headless: bool) -> Result<Self> {
let exe = chrome_path()?;
let dir =
std::env::temp_dir().join(format!("drission-cdp-{}-{}", std::process::id(), now_ms()));
std::fs::create_dir_all(&dir)
.map_err(|e| Error::msg(format!("CDP: 建 user-data-dir 失败: {e}")))?;
let mut cmd = tokio::process::Command::new(&exe);
cmd.arg("--remote-debugging-port=0")
.arg(format!("--user-data-dir={}", dir.display()))
.arg("--no-first-run")
.arg("--no-default-browser-check")
.arg("--disable-background-networking")
.arg("--disable-features=Translate,OptimizationHints")
.arg("about:blank");
if headless {
cmd.arg("--headless=new").arg("--disable-gpu");
}
cmd.stdout(Stdio::null())
.stderr(Stdio::null())
.kill_on_drop(true);
let child = cmd
.spawn()
.map_err(|e| Error::msg(format!("CDP: 启动浏览器失败({}): {e}", exe.display())))?;
let port = wait_for_devtools_port(&dir.join("DevToolsActivePort")).await?;
let ws_url = browser_ws_url(&format!("http://127.0.0.1:{port}")).await?;
let ws = crate::transport::ws_connect(&ws_url).await?;
Ok(Self {
conn: Connection::from_ws(ws),
child: Some(child),
user_data_dir: Some(dir),
})
}
pub async fn connect(debug_http_url: &str) -> Result<Self> {
let ws_url = browser_ws_url(debug_http_url.trim_end_matches('/')).await?;
let ws = crate::transport::ws_connect(&ws_url).await?;
Ok(Self {
conn: Connection::from_ws(ws),
child: None,
user_data_dir: None,
})
}
pub async fn new_tab(&self, url: &str) -> Result<ChromiumTab> {
let r = self
.conn
.send("Target.createTarget", json!({ "url": url }), None)
.await?;
let target_id = r["targetId"]
.as_str()
.ok_or_else(|| Error::msg("CDP: 创建标签无 targetId"))?
.to_string();
self.attach(target_id).await
}
pub async fn latest_tab(&self) -> Result<ChromiumTab> {
let r = self.conn.send("Target.getTargets", json!({}), None).await?;
let targets = r["targetInfos"].as_array().cloned().unwrap_or_default();
let page = targets
.iter()
.rev()
.find(|t| t["type"].as_str() == Some("page"))
.and_then(|t| t["targetId"].as_str())
.ok_or_else(|| Error::msg("CDP: 没有可附着的 page 标签"))?
.to_string();
self.attach(page).await
}
async fn attach(&self, target_id: String) -> Result<ChromiumTab> {
let a = self
.conn
.send(
"Target.attachToTarget",
json!({ "targetId": target_id, "flatten": true }),
None,
)
.await?;
let session_id = a["sessionId"]
.as_str()
.ok_or_else(|| Error::msg("CDP: 附着无 sessionId"))?
.to_string();
let core = CdpCore::new(self.conn.clone(), session_id, target_id);
let _ = core.send("Page.enable", json!({})).await;
let _ = core.send("Runtime.enable", json!({})).await;
Ok(ChromiumTab::new(core))
}
pub async fn quit(mut self) -> Result<()> {
let _ = self.conn.send("Browser.close", json!({}), None).await;
if let Some(mut c) = self.child.take() {
let _ = c.kill().await;
}
if let Some(d) = self.user_data_dir.take() {
let _ = std::fs::remove_dir_all(&d);
}
Ok(())
}
}
fn now_ms() -> u128 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
}
fn chrome_path() -> Result<PathBuf> {
if let Ok(p) = std::env::var("CHROME_BIN") {
let pb = PathBuf::from(p);
if pb.exists() {
return Ok(pb);
}
}
let candidates: &[&str] = if cfg!(target_os = "macos") {
&[
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
]
} else if cfg!(target_os = "windows") {
&[
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
]
} else {
&[
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/usr/bin/microsoft-edge",
]
};
for c in candidates {
let p = Path::new(c);
if p.exists() {
return Ok(p.to_path_buf());
}
}
Err(Error::msg(
"CDP: 未找到 Chrome/Edge,可设 CHROME_BIN 指定可执行文件路径",
))
}
async fn wait_for_devtools_port(file: &Path) -> Result<u16> {
for _ in 0..100 {
if let Ok(s) = std::fs::read_to_string(file) {
if let Some(line) = s.lines().next() {
if let Ok(port) = line.trim().parse::<u16>() {
return Ok(port);
}
}
}
sleep(Duration::from_millis(100)).await;
}
Err(Error::msg(
"CDP: 等待 DevToolsActivePort 超时(浏览器未就绪)",
))
}
async fn browser_ws_url(http: &str) -> Result<String> {
let body: Value = reqwest::get(format!("{http}/json/version"))
.await
.map_err(|e| Error::msg(format!("CDP: 访问 {http}/json/version 失败: {e}")))?
.json()
.await
.map_err(|e| Error::msg(format!("CDP: 解析 /json/version 失败: {e}")))?;
body["webSocketDebuggerUrl"]
.as_str()
.map(|s| s.to_string())
.ok_or_else(|| Error::msg("CDP: /json/version 无 webSocketDebuggerUrl"))
}