mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat: Add support for connecting to local Chrome instance via CDP
- Extend BrowserConfig with options for connecting to existing Chrome - Add connect_ws and connect_port for CDP connection - Add user_data_dir and persist_profile for custom Chrome profiles - Add anti-automation options (locale, timezone, accept_language, user_agent) - Implement discovery function to find Chrome debug port - Update BrowserManager to support both connecting and launching Chrome - Add apply_page_overrides to set human-like browser environment - Fix browser preview layout and URL tracking issues - Add /browser local command support for local Chrome connections This allows the browser to appear less automated and supports connecting to an existing Chrome instance for better debugging and testing. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
This commit is contained in:
Generated
+133
-3
@@ -517,6 +517,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chromiumoxide"
|
||||
version = "0.7.0"
|
||||
@@ -711,6 +717,7 @@ dependencies = [
|
||||
"image",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
@@ -718,6 +725,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -1920,8 +1928,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1931,9 +1941,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2141,6 +2153,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2177,7 +2190,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"socket2 0.6.0",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
@@ -2764,6 +2777,12 @@ dependencies = [
|
||||
"hashbrown 0.15.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.94.1"
|
||||
@@ -2936,7 +2955,7 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"cfg_aliases 0.1.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -3474,6 +3493,61 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases 0.2.1",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.3",
|
||||
"lru-slab",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.12",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
|
||||
dependencies = [
|
||||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.5.10",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
@@ -3796,6 +3870,8 @@ dependencies = [
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3803,6 +3879,7 @@ dependencies = [
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -3812,6 +3889,7 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3870,6 +3948,12 @@ version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -3912,6 +3996,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
@@ -3924,6 +4009,7 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -4357,6 +4443,16 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.0"
|
||||
@@ -4830,6 +4926,21 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.47.1"
|
||||
@@ -4845,7 +4956,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"socket2",
|
||||
"socket2 0.6.0",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@@ -5519,6 +5630,25 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.10"
|
||||
|
||||
@@ -21,9 +21,11 @@ futures = "0.3"
|
||||
image = "0.25"
|
||||
once_cell = "1.20"
|
||||
regex = "1"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tempfile = "3"
|
||||
url = "2"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["fs", "io-util", "macros", "rt-multi-thread", "sync", "time"] }
|
||||
tracing = "0.1"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BrowserConfig {
|
||||
@@ -22,6 +23,40 @@ pub struct BrowserConfig {
|
||||
|
||||
#[serde(default = "default_format")]
|
||||
pub format: ImageFormat,
|
||||
|
||||
/// Launch Chrome in headless mode. Prefer headed for fewer false positives.
|
||||
#[serde(default)]
|
||||
pub headless: bool,
|
||||
|
||||
/// Connect to an already-running Chrome DevTools WS endpoint
|
||||
/// e.g. ws://127.0.0.1:9222/devtools/browser/XXXXXXXX
|
||||
#[serde(default)]
|
||||
pub connect_ws: Option<String>,
|
||||
|
||||
/// Or discover the WS endpoint from a --remote-debugging-port (e.g. 9222).
|
||||
#[serde(default)]
|
||||
pub connect_port: Option<u16>,
|
||||
|
||||
/// Use a persistent profile instead of temp. If set, we won't delete it.
|
||||
#[serde(default)]
|
||||
pub user_data_dir: Option<PathBuf>,
|
||||
|
||||
/// If true and `user_data_dir` is Some, never delete on drop.
|
||||
#[serde(default = "default_persist_profile")]
|
||||
pub persist_profile: bool,
|
||||
|
||||
/// "Human" env hints applied via CDP immediately after page creation.
|
||||
#[serde(default)]
|
||||
pub locale: Option<String>, // e.g. Some("en-AU".into())
|
||||
|
||||
#[serde(default)]
|
||||
pub timezone: Option<String>, // e.g. Some("Australia/Brisbane".into())
|
||||
|
||||
#[serde(default)]
|
||||
pub accept_language: Option<String>, // e.g. Some("en-AU,en;q=0.9".into())
|
||||
|
||||
#[serde(default)]
|
||||
pub user_agent: Option<String>, // leave None to let Chrome decide
|
||||
}
|
||||
|
||||
impl Default for BrowserConfig {
|
||||
@@ -34,6 +69,15 @@ impl Default for BrowserConfig {
|
||||
segments_max: default_segments_max(),
|
||||
idle_timeout_ms: default_idle_timeout_ms(),
|
||||
format: default_format(),
|
||||
headless: false, // Prefer headed for fewer false positives
|
||||
connect_ws: None,
|
||||
connect_port: None,
|
||||
user_data_dir: None,
|
||||
persist_profile: default_persist_profile(),
|
||||
locale: Some("en-AU".into()),
|
||||
timezone: Some("Australia/Brisbane".into()),
|
||||
accept_language: Some("en-AU,en;q=0.9".into()),
|
||||
user_agent: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,4 +135,8 @@ fn default_device_scale_factor() -> f64 {
|
||||
|
||||
fn default_format() -> ImageFormat {
|
||||
ImageFormat::Png
|
||||
}
|
||||
|
||||
fn default_persist_profile() -> bool {
|
||||
true
|
||||
}
|
||||
+191
-57
@@ -1,11 +1,35 @@
|
||||
use crate::{config::BrowserConfig, page::Page, BrowserError, Result};
|
||||
use chromiumoxide::{Browser, BrowserConfig as CdpConfig};
|
||||
use chromiumoxide::cdp::browser_protocol::{emulation, network};
|
||||
use futures::StreamExt;
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tokio::time::{sleep, Duration, Instant};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JsonVersion {
|
||||
#[serde(rename = "webSocketDebuggerUrl")]
|
||||
web_socket_debugger_url: String,
|
||||
}
|
||||
|
||||
async fn discover_ws_via_port(port: u16) -> Result<String> {
|
||||
let url = format!("http://127.0.0.1:{}/json/version", port);
|
||||
let resp = Client::new().get(&url).send().await
|
||||
.map_err(|e| BrowserError::CdpError(format!("Failed to connect to Chrome debug port: {}", e)))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(BrowserError::CdpError(format!("Chrome /json/version returned {}", resp.status())));
|
||||
}
|
||||
|
||||
let body: JsonVersion = resp.json().await
|
||||
.map_err(|e| BrowserError::CdpError(format!("Failed to parse Chrome debug response: {}", e)))?;
|
||||
|
||||
Ok(body.web_socket_debugger_url)
|
||||
}
|
||||
|
||||
pub struct BrowserManager {
|
||||
pub config: Arc<RwLock<BrowserConfig>>,
|
||||
browser: Arc<Mutex<Option<Browser>>>,
|
||||
@@ -14,6 +38,7 @@ pub struct BrowserManager {
|
||||
idle_monitor_handle: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
|
||||
assets: Arc<Mutex<Option<Arc<crate::assets::AssetManager>>>>,
|
||||
user_data_dir: Arc<Mutex<Option<String>>>,
|
||||
cleanup_profile_on_drop: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
impl BrowserManager {
|
||||
@@ -26,6 +51,7 @@ impl BrowserManager {
|
||||
idle_monitor_handle: Arc::new(Mutex::new(None)),
|
||||
assets: Arc::new(Mutex::new(None)),
|
||||
user_data_dir: Arc::new(Mutex::new(None)),
|
||||
cleanup_profile_on_drop: Arc::new(Mutex::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,37 +61,94 @@ impl BrowserManager {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Starting browser");
|
||||
let config = self.config.read().await.clone();
|
||||
|
||||
// Use timestamp to ensure unique directory for each browser instance
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis();
|
||||
let user_data_path = format!("/tmp/coder-browser-{}-{}", std::process::id(), timestamp);
|
||||
|
||||
// Ensure the directory doesn't exist before starting
|
||||
if tokio::fs::metadata(&user_data_path).await.is_ok() {
|
||||
if let Err(e) = tokio::fs::remove_dir_all(&user_data_path).await {
|
||||
warn!("Failed to cleanup existing browser directory {}: {}", user_data_path, e);
|
||||
// 1) Attach to a live Chrome, if requested
|
||||
if let Some(ws) = config.connect_ws.clone() {
|
||||
info!("Connecting to Chrome via WebSocket: {}", ws);
|
||||
let (browser, mut handler) = Browser::connect(ws).await?;
|
||||
tokio::spawn(async move {
|
||||
while let Some(_evt) = handler.next().await {}
|
||||
});
|
||||
*browser_guard = Some(browser);
|
||||
*self.cleanup_profile_on_drop.lock().await = false;
|
||||
|
||||
self.start_idle_monitor().await;
|
||||
self.update_activity().await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(port) = config.connect_port {
|
||||
info!("Discovering Chrome via debug port: {}", port);
|
||||
match discover_ws_via_port(port).await {
|
||||
Ok(ws) => {
|
||||
info!("Connecting to Chrome via discovered WebSocket: {}", ws);
|
||||
let (browser, mut handler) = Browser::connect(ws).await?;
|
||||
tokio::spawn(async move {
|
||||
while let Some(_evt) = handler.next().await {}
|
||||
});
|
||||
*browser_guard = Some(browser);
|
||||
*self.cleanup_profile_on_drop.lock().await = false;
|
||||
|
||||
self.start_idle_monitor().await;
|
||||
self.update_activity().await;
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to connect to Chrome on port {}: {}. Will launch new instance.", port, e);
|
||||
// Fall through to launch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (browser, mut handler) = Browser::launch(
|
||||
CdpConfig::builder()
|
||||
.window_size(1024, 768)
|
||||
.user_data_dir(&user_data_path)
|
||||
.arg("--disable-web-security")
|
||||
.arg("--disable-features=VizDisplayCompositor")
|
||||
.arg("--disable-dev-shm-usage")
|
||||
.arg("--no-sandbox")
|
||||
.arg("--disable-gpu")
|
||||
.build()
|
||||
.map_err(|e| BrowserError::CdpError(e.to_string()))?
|
||||
)
|
||||
.await?;
|
||||
|
||||
let _handle = tokio::spawn(async move {
|
||||
// 2) Otherwise: launch a browser
|
||||
info!("Launching new browser instance");
|
||||
|
||||
let mut builder = CdpConfig::builder();
|
||||
|
||||
// Use persistent profile if specified, otherwise temp
|
||||
let user_data_path = if let Some(dir) = &config.user_data_dir {
|
||||
builder = builder.user_data_dir(dir.clone());
|
||||
dir.to_string_lossy().to_string()
|
||||
} else {
|
||||
// Create temp profile
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis();
|
||||
let temp_path = format!("/tmp/coder-browser-{}-{}", std::process::id(), timestamp);
|
||||
|
||||
// Ensure the directory doesn't exist before starting
|
||||
if tokio::fs::metadata(&temp_path).await.is_ok() {
|
||||
if let Err(e) = tokio::fs::remove_dir_all(&temp_path).await {
|
||||
warn!("Failed to cleanup existing browser directory {}: {}", temp_path, e);
|
||||
}
|
||||
}
|
||||
|
||||
builder = builder.user_data_dir(&temp_path);
|
||||
temp_path
|
||||
};
|
||||
|
||||
// Configure viewport
|
||||
builder = builder
|
||||
.window_size(config.viewport.width, config.viewport.height);
|
||||
|
||||
// Set headless mode based on config
|
||||
if config.headless {
|
||||
builder = builder.headless_mode(chromiumoxide::browser::HeadlessMode::New);
|
||||
}
|
||||
|
||||
// Add less automation-screamy flags
|
||||
builder = builder
|
||||
.arg("--disable-blink-features=AutomationControlled")
|
||||
.arg("--disable-features=VizDisplayCompositor");
|
||||
|
||||
let browser_config = builder.build()
|
||||
.map_err(|e| BrowserError::CdpError(e.to_string()))?;
|
||||
|
||||
let (browser, mut handler) = Browser::launch(browser_config).await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = handler.next().await {
|
||||
debug!("Browser event: {:?}", event);
|
||||
}
|
||||
@@ -76,9 +159,13 @@ impl BrowserManager {
|
||||
// Store the user data directory path for cleanup
|
||||
{
|
||||
let mut user_data_guard = self.user_data_dir.lock().await;
|
||||
*user_data_guard = Some(user_data_path);
|
||||
*user_data_guard = Some(user_data_path.clone());
|
||||
}
|
||||
|
||||
// Determine if we should cleanup on drop
|
||||
let should_cleanup = config.user_data_dir.is_none() || !config.persist_profile;
|
||||
*self.cleanup_profile_on_drop.lock().await = should_cleanup;
|
||||
|
||||
self.start_idle_monitor().await;
|
||||
self.update_activity().await;
|
||||
|
||||
@@ -97,22 +184,25 @@ impl BrowserManager {
|
||||
browser.close().await?;
|
||||
}
|
||||
|
||||
// Cleanup user data directory synchronously to ensure it completes
|
||||
let mut user_data_guard = self.user_data_dir.lock().await;
|
||||
if let Some(user_data_path) = user_data_guard.take() {
|
||||
// Give Chrome a moment to fully release the profile
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
|
||||
if let Err(e) = tokio::fs::remove_dir_all(&user_data_path).await {
|
||||
warn!("Failed to cleanup browser user data directory {}: {}", user_data_path, e);
|
||||
// Try a more aggressive cleanup on macOS
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _ = tokio::process::Command::new("rm")
|
||||
.arg("-rf")
|
||||
.arg(&user_data_path)
|
||||
.output()
|
||||
.await;
|
||||
// Only cleanup user data directory if we should
|
||||
let should_cleanup = *self.cleanup_profile_on_drop.lock().await;
|
||||
if should_cleanup {
|
||||
let mut user_data_guard = self.user_data_dir.lock().await;
|
||||
if let Some(user_data_path) = user_data_guard.take() {
|
||||
// Give Chrome a moment to fully release the profile
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
|
||||
if let Err(e) = tokio::fs::remove_dir_all(&user_data_path).await {
|
||||
warn!("Failed to cleanup browser user data directory {}: {}", user_data_path, e);
|
||||
// Try a more aggressive cleanup on macOS
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _ = tokio::process::Command::new("rm")
|
||||
.arg("-rf")
|
||||
.arg(&user_data_path)
|
||||
.output()
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,20 +225,11 @@ impl BrowserManager {
|
||||
.ok_or(BrowserError::NotInitialized)?;
|
||||
|
||||
let cdp_page = browser.new_page("about:blank").await?;
|
||||
|
||||
// Apply page overrides (UA, locale, timezone, viewport, etc.)
|
||||
self.apply_page_overrides(&cdp_page).await?;
|
||||
|
||||
let config = self.config.read().await;
|
||||
|
||||
// Set viewport dimensions using CDP protocol
|
||||
use chromiumoxide::cdp::browser_protocol::emulation::SetDeviceMetricsOverrideParams;
|
||||
let params = SetDeviceMetricsOverrideParams::builder()
|
||||
.width(config.viewport.width as i64)
|
||||
.height(config.viewport.height as i64)
|
||||
.device_scale_factor(config.viewport.device_scale_factor)
|
||||
.mobile(config.viewport.mobile)
|
||||
.build()
|
||||
.map_err(|e| BrowserError::CdpError(e))?;
|
||||
|
||||
cdp_page.execute(params).await?;
|
||||
|
||||
let page = Arc::new(Page::new(cdp_page, config.clone()));
|
||||
*page_guard = Some(Arc::clone(&page));
|
||||
|
||||
@@ -222,6 +303,59 @@ impl BrowserManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply "human" environment: UA / Accept-Language / Timezone / Locale / DPR+Viewport
|
||||
pub async fn apply_page_overrides(
|
||||
&self,
|
||||
page: &chromiumoxide::Page,
|
||||
) -> Result<()> {
|
||||
let config = self.config.read().await;
|
||||
|
||||
// Enable Network domain before setting headers
|
||||
page.execute(network::EnableParams::default()).await?;
|
||||
|
||||
// SetUserAgentOverrideParams can handle both user agent and accept-language
|
||||
if config.user_agent.is_some() || config.accept_language.is_some() {
|
||||
let mut params_builder = network::SetUserAgentOverrideParams::builder();
|
||||
|
||||
if let Some(ua) = &config.user_agent {
|
||||
params_builder = params_builder.user_agent(ua);
|
||||
}
|
||||
|
||||
if let Some(al) = &config.accept_language {
|
||||
params_builder = params_builder.accept_language(al);
|
||||
}
|
||||
|
||||
let params = params_builder
|
||||
.build()
|
||||
.map_err(|e| BrowserError::CdpError(e))?;
|
||||
page.execute(params).await?;
|
||||
}
|
||||
|
||||
if let Some(tz) = &config.timezone {
|
||||
page.execute(emulation::SetTimezoneOverrideParams {
|
||||
timezone_id: tz.clone(),
|
||||
}).await?;
|
||||
}
|
||||
|
||||
if let Some(locale) = &config.locale {
|
||||
let params = emulation::SetLocaleOverrideParams::builder()
|
||||
.locale(locale)
|
||||
.build();
|
||||
page.execute(params).await?;
|
||||
}
|
||||
|
||||
let params = emulation::SetDeviceMetricsOverrideParams::builder()
|
||||
.width(config.viewport.width as i64)
|
||||
.height(config.viewport.height as i64)
|
||||
.device_scale_factor(config.viewport.device_scale_factor)
|
||||
.mobile(config.viewport.mobile)
|
||||
.build()
|
||||
.map_err(|e| BrowserError::CdpError(e))?;
|
||||
page.execute(params).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_browser(&self) -> Result<()> {
|
||||
let browser_guard = self.browser.lock().await;
|
||||
if browser_guard.is_none() {
|
||||
|
||||
@@ -1330,6 +1330,59 @@ impl ChatWidget<'_> {
|
||||
} else {
|
||||
// It's a subcommand
|
||||
match first_arg {
|
||||
"local" => {
|
||||
// Connect to local Chrome instance
|
||||
browser_manager.set_enabled_sync(true);
|
||||
|
||||
// Update the config to use local Chrome connection
|
||||
let browser_manager_local = browser_manager.clone();
|
||||
tokio::spawn(async move {
|
||||
// First, update config to try connecting to default port
|
||||
{
|
||||
let mut config = browser_manager_local.config.write().await;
|
||||
config.connect_port = Some(9222); // Default Chrome debug port
|
||||
config.headless = false;
|
||||
config.persist_profile = true;
|
||||
}
|
||||
|
||||
// Try to connect
|
||||
match browser_manager_local.start().await {
|
||||
Ok(_) => {
|
||||
tracing::info!("Connected to local Chrome instance");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to connect to local Chrome on port 9222: {}. Trying to launch Chrome with debug port...", e);
|
||||
|
||||
// Try launching Chrome with debug port
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _ = tokio::process::Command::new("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
|
||||
.arg("--remote-debugging-port=9222")
|
||||
.arg("--user-data-dir=/tmp/coder-chrome-profile")
|
||||
.spawn();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let _ = tokio::process::Command::new("google-chrome")
|
||||
.arg("--remote-debugging-port=9222")
|
||||
.arg("--user-data-dir=/tmp/coder-chrome-profile")
|
||||
.spawn();
|
||||
}
|
||||
|
||||
// Wait a bit for Chrome to start
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
// Try connecting again
|
||||
if let Err(e) = browser_manager_local.start().await {
|
||||
tracing::error!("Failed to connect to Chrome after launching: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
"Connecting to local Chrome instance (port 9222)...\nIf Chrome is not running with --remote-debugging-port=9222, it will be launched.".to_string()
|
||||
}
|
||||
"off" => {
|
||||
// Disable browser mode
|
||||
browser_manager.set_enabled_sync(false);
|
||||
|
||||
Reference in New Issue
Block a user