full native (#754)
* full native * fix: apply cargo fmt formatting * fix: prevent zip path traversal in Chromium installer Use enclosed_name() to sanitize zip entry paths, preventing malicious archives from writing outside the extraction directory. * improvements * fix: apply cargo fmt formatting * benchmarks * bench * updates * fixes
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
use serde_json::{json, Value};
|
||||
use std::env;
|
||||
use tokio::sync::broadcast;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{broadcast, RwLock};
|
||||
|
||||
use super::auth;
|
||||
use super::browser::{BrowserManager, WaitUntil};
|
||||
use super::cdp::chrome::LaunchOptions;
|
||||
use super::cdp::client::CdpClient;
|
||||
use super::cdp::types::{
|
||||
AttachToTargetParams, AttachToTargetResult, CdpEvent, ConsoleApiCalledEvent,
|
||||
CreateTargetResult, ExceptionThrownEvent, TargetCreatedEvent, TargetDestroyedEvent,
|
||||
@@ -102,6 +104,8 @@ pub struct DaemonState {
|
||||
pub tracked_requests: Vec<TrackedRequest>,
|
||||
pub request_tracking: bool,
|
||||
pub active_frame_id: Option<String>,
|
||||
/// Shared slot for stream server to receive CDP client when browser launches.
|
||||
pub stream_client: Option<Arc<RwLock<Option<Arc<CdpClient>>>>>,
|
||||
}
|
||||
|
||||
impl DaemonState {
|
||||
@@ -134,15 +138,33 @@ impl DaemonState {
|
||||
tracked_requests: Vec::new(),
|
||||
request_tracking: false,
|
||||
active_frame_id: None,
|
||||
stream_client: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create state with an optional stream client slot (for daemon startup with stream server).
|
||||
pub fn new_with_stream_client(
|
||||
stream_client: Option<Arc<RwLock<Option<Arc<CdpClient>>>>>,
|
||||
) -> Self {
|
||||
let mut s = Self::new();
|
||||
s.stream_client = stream_client;
|
||||
s
|
||||
}
|
||||
|
||||
fn subscribe_to_browser_events(&mut self) {
|
||||
if let Some(ref browser) = self.browser {
|
||||
self.event_rx = Some(browser.client.subscribe());
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the stream server's CDP client slot when browser is set or cleared.
|
||||
pub async fn update_stream_client(&self) {
|
||||
if let Some(ref slot) = self.stream_client {
|
||||
let mut guard = slot.write().await;
|
||||
*guard = self.browser.as_ref().map(|m| Arc::clone(&m.client));
|
||||
}
|
||||
}
|
||||
|
||||
fn drain_cdp_events(
|
||||
&mut self,
|
||||
) -> (
|
||||
@@ -540,6 +562,7 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
|
||||
let _ = mgr.close().await;
|
||||
}
|
||||
state.browser = None;
|
||||
state.update_stream_client().await;
|
||||
}
|
||||
if let Err(e) = auto_launch(state).await {
|
||||
return error_response(&id, &format!("Auto-launch failed: {}", e));
|
||||
@@ -739,6 +762,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
|
||||
let mgr = BrowserManager::connect_cdp(&cdp).await?;
|
||||
state.browser = Some(mgr);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
try_auto_restore_state(state).await;
|
||||
return Ok(());
|
||||
}
|
||||
@@ -747,6 +771,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
|
||||
let mgr = BrowserManager::connect_auto().await?;
|
||||
state.browser = Some(mgr);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
try_auto_restore_state(state).await;
|
||||
return Ok(());
|
||||
}
|
||||
@@ -754,6 +779,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
|
||||
let mgr = BrowserManager::launch(options, engine.as_deref()).await?;
|
||||
state.browser = Some(mgr);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
try_auto_restore_state(state).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -857,6 +883,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
|
||||
if let Some(ref mut b) = state.browser {
|
||||
b.close().await?;
|
||||
state.browser = None;
|
||||
state.update_stream_client().await;
|
||||
}
|
||||
} else {
|
||||
return Ok(json!({ "launched": true, "reused": true }));
|
||||
@@ -894,18 +921,21 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
|
||||
if let Some(url) = cdp_url {
|
||||
state.browser = Some(BrowserManager::connect_cdp(url).await?);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
return Ok(json!({ "launched": true }));
|
||||
}
|
||||
|
||||
if let Some(port) = cdp_port {
|
||||
state.browser = Some(BrowserManager::connect_cdp(&port.to_string()).await?);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
return Ok(json!({ "launched": true }));
|
||||
}
|
||||
|
||||
if auto_connect {
|
||||
state.browser = Some(BrowserManager::connect_auto().await?);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
return Ok(json!({ "launched": true }));
|
||||
}
|
||||
|
||||
@@ -923,6 +953,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
|
||||
Ok(mgr) => {
|
||||
state.browser = Some(mgr);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
return Ok(json!({ "launched": true, "provider": provider }));
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -1008,6 +1039,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
|
||||
|
||||
state.browser = Some(BrowserManager::launch(options, engine.as_deref()).await?);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
|
||||
if let Some(ref filter) = state.domain_filter {
|
||||
if let Some(ref mgr) = state.browser {
|
||||
@@ -1287,6 +1319,7 @@ async fn handle_close(state: &mut DaemonState) -> Result<Value, String> {
|
||||
mgr.close().await?;
|
||||
}
|
||||
state.browser = None;
|
||||
state.update_stream_client().await;
|
||||
|
||||
// Close WebDriver sessions
|
||||
if let Some(ref mut wb) = state.webdriver_backend {
|
||||
@@ -1463,6 +1496,44 @@ async fn handle_click(cmd: &Value, state: &mut DaemonState) -> Result<Value, Str
|
||||
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
|
||||
let session_id = mgr.active_session_id()?.to_string();
|
||||
|
||||
let new_tab = cmd.get("newTab").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
|
||||
if new_tab {
|
||||
use super::element::resolve_element_object_id;
|
||||
let object_id =
|
||||
resolve_element_object_id(&mgr.client, &session_id, &state.ref_map, selector).await?;
|
||||
let call_params = json!({
|
||||
"objectId": object_id,
|
||||
"functionDeclaration": "function() { var h = this.getAttribute('href'); if (!h) return null; try { return new URL(h, document.baseURI).toString(); } catch(e) { return null; } }",
|
||||
"returnByValue": true
|
||||
});
|
||||
let call_result = mgr
|
||||
.client
|
||||
.send_command(
|
||||
"Runtime.callFunctionOn",
|
||||
Some(call_params),
|
||||
Some(&session_id),
|
||||
)
|
||||
.await?;
|
||||
let href = call_result
|
||||
.get("result")
|
||||
.and_then(|r| r.get("value"))
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"Element '{}' does not have an href attribute. --new-tab only works on links.",
|
||||
selector
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
|
||||
state.ref_map.clear();
|
||||
mgr.tab_new(Some(&href)).await?;
|
||||
|
||||
return Ok(json!({ "clicked": selector, "newTab": true, "url": href }));
|
||||
}
|
||||
|
||||
let button = cmd.get("button").and_then(|v| v.as_str()).unwrap_or("left");
|
||||
let click_count = cmd.get("clickCount").and_then(|v| v.as_i64()).unwrap_or(1) as i32;
|
||||
|
||||
@@ -5334,6 +5405,12 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_credentials_roundtrip_via_actions() {
|
||||
let _lock = crate::native::auth::AUTH_TEST_MUTEX.lock().unwrap();
|
||||
let key_var = "AGENT_BROWSER_ENCRYPTION_KEY";
|
||||
let original = std::env::var(key_var).ok();
|
||||
// SAFETY: AUTH_TEST_MUTEX serializes all test access so no concurrent mutation.
|
||||
unsafe { std::env::set_var(key_var, "a".repeat(64)) };
|
||||
|
||||
let mut state = DaemonState::new();
|
||||
|
||||
let set_cmd = json!({
|
||||
@@ -5366,6 +5443,12 @@ mod tests {
|
||||
});
|
||||
let result = execute_command(&del_cmd, &mut state).await;
|
||||
assert_eq!(result["success"], true);
|
||||
|
||||
// SAFETY: AUTH_TEST_MUTEX serializes all test access so no concurrent mutation.
|
||||
match original {
|
||||
Some(val) => unsafe { std::env::set_var(key_var, val) },
|
||||
None => unsafe { std::env::remove_var(key_var) },
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -160,7 +160,7 @@ impl BrowserProcess {
|
||||
}
|
||||
|
||||
pub struct BrowserManager {
|
||||
pub client: CdpClient,
|
||||
pub client: Arc<CdpClient>,
|
||||
browser_process: Option<BrowserProcess>,
|
||||
ws_url: String,
|
||||
pages: Vec<PageInfo>,
|
||||
@@ -226,7 +226,7 @@ impl BrowserManager {
|
||||
let manager = if engine == "lightpanda" {
|
||||
initialize_lightpanda_manager(ws_url, process).await?
|
||||
} else {
|
||||
let client = CdpClient::connect(&ws_url).await?;
|
||||
let client = Arc::new(CdpClient::connect(&ws_url).await?);
|
||||
let mut manager = Self {
|
||||
client,
|
||||
browser_process: Some(process),
|
||||
@@ -290,7 +290,7 @@ impl BrowserManager {
|
||||
|
||||
pub async fn connect_cdp(url: &str) -> Result<Self, String> {
|
||||
let ws_url = resolve_cdp_url(url).await?;
|
||||
let client = CdpClient::connect(&ws_url).await?;
|
||||
let client = Arc::new(CdpClient::connect(&ws_url).await?);
|
||||
let mut manager = Self {
|
||||
client,
|
||||
browser_process: None,
|
||||
@@ -1173,7 +1173,7 @@ async fn initialize_lightpanda_manager(
|
||||
};
|
||||
|
||||
let mut manager = BrowserManager {
|
||||
client,
|
||||
client: Arc::new(client),
|
||||
browser_process: None,
|
||||
ws_url: ws_url.clone(),
|
||||
pages: Vec::new(),
|
||||
|
||||
@@ -190,7 +190,7 @@ pub fn launch_chrome(options: &LaunchOptions) -> Result<ChromeProcess, String> {
|
||||
let chrome_path = match &options.executable_path {
|
||||
Some(p) => PathBuf::from(p),
|
||||
None => {
|
||||
find_chrome().ok_or("Chrome not found. Install Chrome or use --executable-path.")?
|
||||
find_chrome().ok_or("Chrome not found. Run `agent-browser install` to download Chrome, or use --executable-path.")?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -320,6 +320,12 @@ fn chrome_launch_error(message: &str, stderr_lines: &[String]) -> String {
|
||||
}
|
||||
|
||||
pub fn find_chrome() -> Option<PathBuf> {
|
||||
// 1. Check Chrome downloaded by `agent-browser install`
|
||||
if let Some(p) = crate::install::find_installed_chrome() {
|
||||
return Some(p);
|
||||
}
|
||||
|
||||
// 2. Check system-installed Chrome
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let candidates = [
|
||||
@@ -333,10 +339,6 @@ pub fn find_chrome() -> Option<PathBuf> {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(p) = find_playwright_chromium() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -357,10 +359,6 @@ pub fn find_chrome() -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(p) = find_playwright_chromium() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -383,6 +381,11 @@ pub fn find_chrome() -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback: check Playwright's browser cache (for existing installs)
|
||||
if let Some(p) = find_playwright_chromium() {
|
||||
return Some(p);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
@@ -500,7 +503,7 @@ fn should_disable_sandbox(existing_args: &[String]) -> bool {
|
||||
}
|
||||
|
||||
/// Search Playwright's browser cache for a Chromium binary.
|
||||
/// This is where `agent-browser install` (via `npx playwright install chromium`) puts it.
|
||||
/// Legacy fallback for users who previously installed Chromium via Playwright.
|
||||
fn find_playwright_chromium() -> Option<PathBuf> {
|
||||
let mut search_dirs = Vec::new();
|
||||
|
||||
|
||||
@@ -342,6 +342,7 @@ mod tests {
|
||||
socket.write_all(response.as_bytes()).await.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn waits_for_ready_without_logs() {
|
||||
let port = unused_port();
|
||||
@@ -369,6 +370,7 @@ mod tests {
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn child_exit_surfaces_logs() {
|
||||
let port = unused_port();
|
||||
@@ -389,6 +391,7 @@ mod tests {
|
||||
assert!(err.contains("boom"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn timeout_reports_last_probe_error() {
|
||||
let port = unused_port();
|
||||
|
||||
+109
-11
@@ -3,12 +3,17 @@ use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::signal;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
|
||||
use super::actions::{execute_command, DaemonState};
|
||||
use super::cdp::client::CdpClient;
|
||||
use super::state;
|
||||
use super::stream::StreamServer;
|
||||
|
||||
pub async fn run_daemon(session: &str) {
|
||||
let socket_dir = get_daemon_socket_dir();
|
||||
@@ -33,7 +38,34 @@ pub async fn run_daemon(session: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
let result = run_socket_server(&socket_path, session).await;
|
||||
let mut stream_client: Option<Arc<RwLock<Option<Arc<CdpClient>>>>> = None;
|
||||
if let Ok(port_str) = env::var("AGENT_BROWSER_STREAM_PORT") {
|
||||
if let Ok(port) = port_str.parse::<u16>() {
|
||||
if port > 0 {
|
||||
match StreamServer::start_without_client(port, session.to_string()).await {
|
||||
Ok((stream_server, client_slot)) => {
|
||||
stream_client = Some(client_slot.clone());
|
||||
let stream_path = socket_dir.join(format!("{}.stream", session));
|
||||
if let Err(e) = fs::write(&stream_path, stream_server.port().to_string()) {
|
||||
eprintln!("Failed to write .stream file: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Stream server failed to start: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-shutdown the daemon after this many ms of inactivity (no commands received).
|
||||
// Disabled when unset or 0.
|
||||
let idle_timeout_ms = env::var("AGENT_BROWSER_IDLE_TIMEOUT_MS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.filter(|&ms| ms > 0);
|
||||
|
||||
let result = run_socket_server(&socket_path, session, stream_client, idle_timeout_ms).await;
|
||||
|
||||
let _ = fs::remove_file(&socket_path);
|
||||
let _ = fs::remove_file(&pid_path);
|
||||
@@ -47,23 +79,36 @@ pub async fn run_daemon(session: &str) {
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn run_socket_server(socket_path: &PathBuf, _session: &str) -> Result<(), String> {
|
||||
async fn run_socket_server(
|
||||
socket_path: &PathBuf,
|
||||
_session: &str,
|
||||
stream_client: Option<Arc<RwLock<Option<Arc<CdpClient>>>>>,
|
||||
idle_timeout_ms: Option<u64>,
|
||||
) -> Result<(), String> {
|
||||
use tokio::net::UnixListener;
|
||||
|
||||
let listener =
|
||||
UnixListener::bind(socket_path).map_err(|e| format!("Failed to bind socket: {}", e))?;
|
||||
|
||||
let state: std::sync::Arc<tokio::sync::Mutex<DaemonState>> =
|
||||
std::sync::Arc::new(tokio::sync::Mutex::new(DaemonState::new()));
|
||||
let state: std::sync::Arc<tokio::sync::Mutex<DaemonState>> = std::sync::Arc::new(
|
||||
tokio::sync::Mutex::new(DaemonState::new_with_stream_client(stream_client)),
|
||||
);
|
||||
|
||||
let (reset_tx, mut reset_rx) = mpsc::channel::<()>(64);
|
||||
let reset_tx = idle_timeout_ms.map(|_| Arc::new(reset_tx));
|
||||
|
||||
loop {
|
||||
let sleep_future = idle_timeout_ms.map(|ms| tokio::time::sleep(Duration::from_millis(ms)));
|
||||
let mut sleep_pin = sleep_future.map(Box::pin);
|
||||
|
||||
tokio::select! {
|
||||
accept_result = listener.accept() => {
|
||||
match accept_result {
|
||||
Ok((stream, _)) => {
|
||||
let state = state.clone();
|
||||
let reset_tx = reset_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
handle_connection(stream, state).await;
|
||||
handle_connection(stream, state, reset_tx).await;
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -71,6 +116,22 @@ async fn run_socket_server(socket_path: &PathBuf, _session: &str) -> Result<(),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = async {
|
||||
if let Some(ref mut s) = sleep_pin {
|
||||
s.as_mut().await
|
||||
} else {
|
||||
std::future::pending::<()>().await
|
||||
}
|
||||
}, if idle_timeout_ms.is_some() => {
|
||||
let mut s = state.lock().await;
|
||||
if let Some(ref mut mgr) = s.browser {
|
||||
let _ = mgr.close().await;
|
||||
}
|
||||
break;
|
||||
}
|
||||
_ = reset_rx.recv(), if idle_timeout_ms.is_some() => {
|
||||
continue;
|
||||
}
|
||||
_ = shutdown_signal() => {
|
||||
let mut s = state.lock().await;
|
||||
if let Some(ref mut mgr) = s.browser {
|
||||
@@ -85,7 +146,12 @@ async fn run_socket_server(socket_path: &PathBuf, _session: &str) -> Result<(),
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn run_socket_server(socket_path: &PathBuf, session: &str) -> Result<(), String> {
|
||||
async fn run_socket_server(
|
||||
socket_path: &PathBuf,
|
||||
session: &str,
|
||||
stream_client: Option<Arc<RwLock<Option<Arc<CdpClient>>>>>,
|
||||
idle_timeout_ms: Option<u64>,
|
||||
) -> Result<(), String> {
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
let port = get_port_for_session(session);
|
||||
@@ -97,17 +163,25 @@ async fn run_socket_server(socket_path: &PathBuf, session: &str) -> Result<(), S
|
||||
let port_path = socket_dir.join(format!("{}.port", session));
|
||||
let _ = fs::write(&port_path, port.to_string());
|
||||
|
||||
let state: std::sync::Arc<tokio::sync::Mutex<DaemonState>> =
|
||||
std::sync::Arc::new(tokio::sync::Mutex::new(DaemonState::new()));
|
||||
let state: std::sync::Arc<tokio::sync::Mutex<DaemonState>> = std::sync::Arc::new(
|
||||
tokio::sync::Mutex::new(DaemonState::new_with_stream_client(stream_client)),
|
||||
);
|
||||
|
||||
let (reset_tx, mut reset_rx) = mpsc::channel::<()>(64);
|
||||
let reset_tx = idle_timeout_ms.map(|_| Arc::new(reset_tx));
|
||||
|
||||
loop {
|
||||
let sleep_future = idle_timeout_ms.map(|ms| tokio::time::sleep(Duration::from_millis(ms)));
|
||||
let mut sleep_pin = sleep_future.map(Box::pin);
|
||||
|
||||
tokio::select! {
|
||||
accept_result = listener.accept() => {
|
||||
match accept_result {
|
||||
Ok((stream, _)) => {
|
||||
let state = state.clone();
|
||||
let reset_tx = reset_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
handle_connection(stream, state).await;
|
||||
handle_connection(stream, state, reset_tx).await;
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -115,6 +189,23 @@ async fn run_socket_server(socket_path: &PathBuf, session: &str) -> Result<(), S
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = async {
|
||||
if let Some(ref mut s) = sleep_pin {
|
||||
s.as_mut().await
|
||||
} else {
|
||||
std::future::pending::<()>().await
|
||||
}
|
||||
}, if idle_timeout_ms.is_some() => {
|
||||
let mut s = state.lock().await;
|
||||
if let Some(ref mut mgr) = s.browser {
|
||||
let _ = mgr.close().await;
|
||||
}
|
||||
let _ = fs::remove_file(&port_path);
|
||||
break;
|
||||
}
|
||||
_ = reset_rx.recv(), if idle_timeout_ms.is_some() => {
|
||||
continue;
|
||||
}
|
||||
_ = shutdown_signal() => {
|
||||
let mut s = state.lock().await;
|
||||
if let Some(ref mut mgr) = s.browser {
|
||||
@@ -129,8 +220,11 @@ async fn run_socket_server(socket_path: &PathBuf, session: &str) -> Result<(), S
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_connection<S>(stream: S, state: std::sync::Arc<tokio::sync::Mutex<DaemonState>>)
|
||||
where
|
||||
async fn handle_connection<S>(
|
||||
stream: S,
|
||||
state: std::sync::Arc<tokio::sync::Mutex<DaemonState>>,
|
||||
idle_reset_tx: Option<Arc<mpsc::Sender<()>>>,
|
||||
) where
|
||||
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
||||
{
|
||||
let (reader, mut writer) = tokio::io::split(stream);
|
||||
@@ -165,6 +259,10 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ref tx) = idle_reset_tx {
|
||||
let _ = tx.try_send(());
|
||||
}
|
||||
|
||||
let is_close = cmd.get("action").and_then(|v| v.as_str()) == Some("close");
|
||||
|
||||
let response = {
|
||||
|
||||
@@ -533,6 +533,7 @@ async fn test_daemon_state_new_defaults() {
|
||||
assert!(state.tracked_requests.is_empty());
|
||||
assert!(state.active_frame_id.is_none());
|
||||
assert!(state.webdriver_backend.is_none());
|
||||
assert!(state.stream_client.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
+40
-13
@@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use tokio::sync::{broadcast, Mutex, RwLock};
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
use super::cdp::client::CdpClient;
|
||||
@@ -47,6 +47,27 @@ impl StreamServer {
|
||||
client: Arc<CdpClient>,
|
||||
session_id: String,
|
||||
) -> Result<Self, String> {
|
||||
let client_slot = Arc::new(RwLock::new(Some(client)));
|
||||
let (server, _) = Self::start_inner(preferred_port, client_slot, session_id).await?;
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
/// Start the stream server without a CDP client (e.g. at daemon startup before browser launch).
|
||||
/// Returns the server and a shared slot to set the client when the browser launches.
|
||||
/// Input messages are ignored until the client is set.
|
||||
pub async fn start_without_client(
|
||||
preferred_port: u16,
|
||||
session_id: String,
|
||||
) -> Result<(Self, Arc<RwLock<Option<Arc<CdpClient>>>>), String> {
|
||||
let client_slot = Arc::new(RwLock::new(None::<Arc<CdpClient>>));
|
||||
Self::start_inner(preferred_port, client_slot, session_id).await
|
||||
}
|
||||
|
||||
async fn start_inner(
|
||||
preferred_port: u16,
|
||||
client_slot: Arc<RwLock<Option<Arc<CdpClient>>>>,
|
||||
session_id: String,
|
||||
) -> Result<(Self, Arc<RwLock<Option<Arc<CdpClient>>>>), String> {
|
||||
let addr = format!("127.0.0.1:{}", preferred_port);
|
||||
let listener = TcpListener::bind(&addr)
|
||||
.await
|
||||
@@ -62,23 +83,27 @@ impl StreamServer {
|
||||
|
||||
let frame_tx_clone = frame_tx.clone();
|
||||
let client_count_clone = client_count.clone();
|
||||
let client_slot_clone = client_slot.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
accept_loop(
|
||||
listener,
|
||||
frame_tx_clone,
|
||||
client_count_clone,
|
||||
client,
|
||||
client_slot_clone,
|
||||
session_id,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
port,
|
||||
frame_tx,
|
||||
client_count,
|
||||
})
|
||||
Ok((
|
||||
Self {
|
||||
port,
|
||||
frame_tx,
|
||||
client_count,
|
||||
},
|
||||
client_slot,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn port(&self) -> u16 {
|
||||
@@ -140,17 +165,17 @@ async fn accept_loop(
|
||||
listener: TcpListener,
|
||||
frame_tx: broadcast::Sender<String>,
|
||||
client_count: Arc<Mutex<usize>>,
|
||||
cdp_client: Arc<CdpClient>,
|
||||
client_slot: Arc<RwLock<Option<Arc<CdpClient>>>>,
|
||||
session_id: String,
|
||||
) {
|
||||
while let Ok((stream, addr)) = listener.accept().await {
|
||||
let frame_rx = frame_tx.subscribe();
|
||||
let client_count = client_count.clone();
|
||||
let cdp = cdp_client.clone();
|
||||
let client_slot = client_slot.clone();
|
||||
let sid = session_id.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
handle_ws_client(stream, addr, frame_rx, client_count, cdp, sid).await;
|
||||
handle_ws_client(stream, addr, frame_rx, client_count, client_slot, sid).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -161,10 +186,9 @@ async fn handle_ws_client(
|
||||
_addr: SocketAddr,
|
||||
mut frame_rx: broadcast::Receiver<String>,
|
||||
client_count: Arc<Mutex<usize>>,
|
||||
cdp_client: Arc<CdpClient>,
|
||||
client_slot: Arc<RwLock<Option<Arc<CdpClient>>>>,
|
||||
session_id: String,
|
||||
) {
|
||||
// Origin checking on WebSocket handshake
|
||||
let callback =
|
||||
|req: &tokio_tungstenite::tungstenite::handshake::server::Request,
|
||||
resp: tokio_tungstenite::tungstenite::handshake::server::Response| {
|
||||
@@ -211,7 +235,10 @@ async fn handle_ws_client(
|
||||
msg = ws_rx.next() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
handle_client_message(&text, &cdp_client, &session_id).await;
|
||||
let guard = client_slot.read().await;
|
||||
if let Some(ref client) = *guard {
|
||||
handle_client_message(&text, client.as_ref(), &session_id).await;
|
||||
}
|
||||
}
|
||||
Some(Ok(Message::Close(_))) | None => break,
|
||||
_ => {}
|
||||
|
||||
Reference in New Issue
Block a user