use anyhow::Result;
use chrono::Utc;
use earl_core::{ExecutionContext, ProtocolExecutor, RawExecutionResult};
use crate::PreparedBrowserCommand;
use crate::launcher::{configure_page, connect_chrome, launch_chrome};
use crate::session::{
SessionFile, acquire_session_lock, ensure_sessions_dir, is_pid_alive, session_file_path,
sessions_dir,
};
use crate::steps::execute_steps;
pub struct BrowserExecutor;
impl ProtocolExecutor for BrowserExecutor {
type PreparedData = PreparedBrowserCommand;
async fn execute(
&mut self,
data: &PreparedBrowserCommand,
_ctx: &ExecutionContext,
) -> Result<RawExecutionResult> {
let result = run_browser_command(data).await?;
let body = serde_json::to_vec(&result)?;
Ok(RawExecutionResult {
status: 0,
url: "browser://command".into(),
body,
content_type: Some("application/json".into()),
})
}
}
async fn run_browser_command(data: &PreparedBrowserCommand) -> Result<serde_json::Value> {
match data.session_id.as_deref() {
None => run_ephemeral(data).await,
Some(session_id) => run_with_session(data, session_id).await,
}
}
async fn run_ephemeral(data: &PreparedBrowserCommand) -> Result<serde_json::Value> {
let (mut browser, _ws_url) = launch_chrome(data.headless).await?;
let page = match browser.new_page("about:blank").await {
Ok(p) => p,
Err(e) => {
let _ = browser.close().await;
return Err(e.into());
}
};
if let Err(e) = configure_page(&page).await {
let _ = browser.close().await;
return Err(e);
}
let result = execute_steps(
&page,
&data.steps,
data.timeout_ms,
data.on_failure_screenshot,
)
.await;
let _ = browser.close().await;
result
}
async fn run_with_session(
data: &PreparedBrowserCommand,
session_id: &str,
) -> Result<serde_json::Value> {
let dir = sessions_dir()?;
ensure_sessions_dir(&dir)?;
let _lock = acquire_session_lock(session_id).await?;
let sf_path = session_file_path(session_id)?;
let existing = SessionFile::load_from(&sf_path)?;
let (browser, ws_url) = if let Some(ref sf) = existing {
if is_pid_alive(sf.pid, Some(sf.started_at)) {
match connect_chrome(&sf.websocket_url).await {
Ok(b) => (b, sf.websocket_url.clone()),
Err(_) => {
let (b, ws) = launch_chrome(data.headless).await?;
(b, ws)
}
}
} else {
let (b, ws) = launch_chrome(data.headless).await?;
(b, ws)
}
} else {
let (b, ws) = launch_chrome(data.headless).await?;
(b, ws)
};
let page = match browser.pages().await {
Ok(pages) if !pages.is_empty() => {
let p = pages.into_iter().next().unwrap();
configure_page(&p).await?;
p
}
_ => {
let p = browser.new_page("about:blank").await?;
configure_page(&p).await?;
p
}
};
let target_id = page.target_id().as_ref().to_string();
let now = Utc::now();
let started_at = existing.as_ref().map(|sf| sf.started_at).unwrap_or(now);
let sf_to_save = SessionFile {
pid: 0,
websocket_url: ws_url,
target_id,
started_at,
last_used_at: now,
interrupted: false,
};
let step_result = execute_steps(
&page,
&data.steps,
data.timeout_ms,
data.on_failure_screenshot,
)
.await;
let mut updated_sf = sf_to_save;
updated_sf.last_used_at = Utc::now();
updated_sf.interrupted = step_result.is_err();
if let Err(e) = updated_sf.save_to(&sf_path) {
tracing::warn!(error = %e, "failed to persist browser session file");
}
step_result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn browser_executor_implements_protocol_executor() {
fn assert_impl<T: earl_core::ProtocolExecutor>() {}
assert_impl::<BrowserExecutor>();
}
}