Add persistent ChromeDriver support for faster WebDriver startup

When webdriver_start is called, now checks if chromedriver is already
running on the configured port and reuses it instead of spawning a new
process. This significantly reduces startup time for subsequent sessions.

New config option:
  [webdriver]
  persistent_chrome = true  # Keep chromedriver running between sessions

When enabled, webdriver_quit closes the browser session but leaves
chromedriver running for reuse by the next session.
This commit is contained in:
Dhanji R. Prasanna
2026-01-17 09:26:25 +05:30
parent eb6268641f
commit 8ed360024f
3 changed files with 66 additions and 5 deletions

View File

@@ -80,3 +80,4 @@ model = "claude-sonnet-4-5"
# browser = "chrome-headless" # Default. Alternative: "safari"
# chrome_binary = "/path/to/chrome" # Optional: custom Chrome path
# chromedriver_binary = "/path/to/driver" # Optional: custom ChromeDriver path
# persistent_chrome = true # Keep chromedriver running between sessions for faster startup

View File

@@ -179,6 +179,10 @@ pub struct WebDriverConfig {
pub chromedriver_binary: Option<String>,
#[serde(default)]
pub browser: WebDriverBrowser,
#[serde(default)]
/// Keep chromedriver running after session ends for faster subsequent startups
/// When true, chromedriver process is not killed on webdriver_quit
pub persistent_chrome: bool,
}
impl Default for AgentConfig {

View File

@@ -11,6 +11,25 @@ use crate::ToolCall;
use super::executor::ToolContext;
// ─────────────────────────────────────────────────────────────────────────────
// Port checking helpers
// ─────────────────────────────────────────────────────────────────────────────
/// Check if chromedriver is already running on the given port.
async fn check_chromedriver_running(port: u16) -> bool {
// Try to connect to the chromedriver status endpoint
let url = format!("http://localhost:{}/status", port);
match reqwest::Client::new()
.get(&url)
.timeout(std::time::Duration::from_millis(500))
.send()
.await
{
Ok(response) => response.status().is_success(),
Err(_) => false,
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Session helpers
// ─────────────────────────────────────────────────────────────────────────────
@@ -119,6 +138,32 @@ async fn start_safari_driver<W: UiWriter>(ctx: &ToolContext<'_, W>) -> Result<St
async fn start_chrome_driver<W: UiWriter>(ctx: &ToolContext<'_, W>) -> Result<String> {
let port = ctx.config.webdriver.chrome_port;
// Check if chromedriver is already running on this port
let already_running = check_chromedriver_running(port).await;
if already_running {
// Try to connect to existing chromedriver
let driver_result = match &ctx.config.webdriver.chrome_binary {
Some(binary) => {
g3_computer_control::ChromeDriver::with_port_headless_and_binary(port, Some(binary))
.await
}
None => g3_computer_control::ChromeDriver::with_port_headless(port).await,
};
if let Ok(driver) = driver_result {
let session =
std::sync::Arc::new(tokio::sync::Mutex::new(WebDriverSession::Chrome(driver)));
*ctx.webdriver_session.write().await = Some(session);
// Don't store process - we didn't start it
return Ok(
"✅ WebDriver session started (reusing existing chromedriver)."
.to_string(),
);
}
// If connection failed, fall through to start a new one
}
// Use configured chromedriver binary or fall back to 'chromedriver' in PATH
let chromedriver_cmd = ctx
.config
@@ -599,22 +644,33 @@ pub async fn execute_webdriver_quit<W: UiWriter>(
Ok(_) => {
debug!("WebDriver session closed successfully");
// Kill the driver process
if let Some(mut process) = ctx.webdriver_process.write().await.take() {
// Kill the driver process (unless persistent_chrome is enabled for Chrome)
use g3_config::WebDriverBrowser;
let is_chrome = matches!(&ctx.config.webdriver.browser, WebDriverBrowser::ChromeHeadless);
let keep_running = is_chrome && ctx.config.webdriver.persistent_chrome;
if keep_running {
debug!("Keeping chromedriver running (persistent_chrome enabled)");
// Still take the process handle but don't kill it
let _ = ctx.webdriver_process.write().await.take();
} else if let Some(mut process) = ctx.webdriver_process.write().await.take() {
if let Err(e) = process.kill().await {
warn!("Failed to kill driver process: {}", e);
} else {
debug!("Driver process terminated");
}
}
}
// Return appropriate message based on browser type
use g3_config::WebDriverBrowser;
let driver_name = match &ctx.config.webdriver.browser {
WebDriverBrowser::Safari => "safaridriver",
WebDriverBrowser::ChromeHeadless => "chromedriver",
};
Ok(format!("✅ WebDriver session closed and {} stopped", driver_name))
if keep_running {
Ok(format!("✅ WebDriver session closed ({} still running for reuse)", driver_name))
} else {
Ok(format!("✅ WebDriver session closed and {} stopped", driver_name))
}
}
Err(e) => Ok(format!("❌ Failed to quit WebDriver: {}", e)),
}