use std::net::TcpListener;
use std::sync::Arc;
use futures_util::{SinkExt, StreamExt};
use tokio::net::TcpListener as TokioTcpListener;
use tokio::sync::Mutex;
use tokio_tungstenite::tungstenite::Message;
use crate::bridge::Bridge;
use crate::handlers;
use crate::protocol::{Command, Request, Response};
use crate::state::PluginState;
pub struct Server {
port: u16,
bridge: Bridge,
app_handle: Arc<Mutex<Option<tauri::AppHandle>>>,
state: PluginState,
}
impl Server {
pub fn new(
bind_address: &str,
port_range: (u16, u16),
bridge: Bridge,
state: PluginState,
) -> Result<Self, String> {
let port = find_available_port(bind_address, port_range.0, port_range.1)
.ok_or_else(|| {
format!("No available port in range {}-{}", port_range.0, port_range.1)
})?;
Ok(Self {
port,
bridge,
app_handle: Arc::new(Mutex::new(None)),
state,
})
}
pub fn port(&self) -> u16 {
self.port
}
pub fn set_app_handle(&self, handle: tauri::AppHandle) {
let app_handle = self.app_handle.clone();
tokio::spawn(async move {
*app_handle.lock().await = Some(handle);
});
}
pub async fn run(&self, bind_address: String) -> Result<(), String> {
let addr = format!("{bind_address}:{}", self.port);
let listener = TokioTcpListener::bind(&addr)
.await
.map_err(|e| e.to_string())?;
println!("[connector][server] Listening on {addr}");
loop {
match listener.accept().await {
Ok((stream, peer)) => {
println!("[connector][server] Client connected: {peer}");
let bridge = self.bridge.clone();
let app_handle = self.app_handle.clone();
let state = self.state.clone();
tokio::spawn(async move {
let ws = match tokio_tungstenite::accept_async(stream).await {
Ok(ws) => ws,
Err(e) => {
eprintln!("[connector][server] WebSocket handshake error: {e}");
return;
}
};
let (ws_tx, mut ws_rx) = ws.split();
let ws_tx = Arc::new(Mutex::new(ws_tx));
while let Some(Ok(msg)) = ws_rx.next().await {
let Message::Text(text) = msg else { continue };
let request: Request = match serde_json::from_str(&text) {
Ok(r) => r,
Err(e) => {
let resp = Response::error("unknown".to_string(), format!("Invalid request: {e}"));
let _ = send_response(&ws_tx, &resp).await;
continue;
}
};
let id = request.id.clone();
let bridge = bridge.clone();
let app_handle = app_handle.clone();
let ws_tx = ws_tx.clone();
let state = state.clone();
tokio::spawn(async move {
let app = app_handle.lock().await;
let response = handle_command(id, request.command, &bridge, app.as_ref(), &state).await;
let _ = send_response(&ws_tx, &response).await;
});
}
println!("[connector][server] Client disconnected");
});
}
Err(e) => {
eprintln!("[connector][server] Accept error: {e}");
}
}
}
}
}
async fn handle_command(
id: String,
command: Command,
bridge: &Bridge,
app: Option<&tauri::AppHandle>,
state: &PluginState,
) -> Response {
match command {
Command::Ping => Response::success(id, serde_json::json!("pong")),
Command::ExecuteJs { script, window_id } => {
handlers::execute_js(&id, &script, &window_id, bridge).await
}
Command::Screenshot { format, quality, max_width, window_id } => {
handlers::screenshot(&id, &format, quality, max_width, &window_id, bridge, app).await
}
Command::DomSnapshot { snapshot_type, selector, window_id } => {
handlers::dom_snapshot(&id, &snapshot_type, selector.as_deref(), &window_id, bridge).await
}
Command::GetCachedDom { window_id } => {
handlers::get_cached_dom(&id, &window_id, state).await
}
Command::FindElement { selector, strategy, window_id } => {
handlers::find_element(&id, &selector, &strategy, &window_id, bridge).await
}
Command::GetStyles { selector, properties, window_id } => {
handlers::get_styles(&id, &selector, properties.as_deref(), &window_id, bridge).await
}
Command::SelectElement { .. } => {
Response::error(id, "Select element (visual picker) not yet implemented")
}
Command::GetPointedElement { .. } => {
handlers::get_pointed_element(&id, state).await
}
Command::Interact { action, selector, strategy, x, y, direction, distance, window_id } => {
handlers::interact(&id, &action, selector.as_deref(), &strategy, x, y, direction.as_deref(), distance, &window_id, bridge).await
}
Command::Keyboard { action, text, key, modifiers, window_id } => {
handlers::keyboard(&id, &action, text.as_deref(), key.as_deref(), modifiers.as_deref(), &window_id, bridge).await
}
Command::WaitFor { selector, strategy, text, timeout, window_id } => {
handlers::wait_for(&id, selector.as_deref(), &strategy, text.as_deref(), timeout, &window_id, bridge).await
}
Command::WindowList => handlers::window_list(&id, app).await,
Command::WindowInfo { window_id } => handlers::window_info(&id, &window_id, app).await,
Command::WindowResize { window_id, width, height } => {
handlers::window_resize(&id, &window_id, width, height, app).await
}
Command::BackendState => handlers::backend_state(&id, app).await,
Command::IpcExecuteCommand { command, args } => {
handlers::ipc_execute_command(&id, &command, args.as_ref(), "main", bridge).await
}
Command::IpcMonitor { action } => handlers::ipc_monitor(&id, &action, state).await,
Command::IpcGetCaptured { filter, limit } => {
handlers::ipc_get_captured(&id, filter.as_deref(), limit, state).await
}
Command::IpcEmitEvent { event_name, payload } => {
handlers::ipc_emit_event(&id, &event_name, payload.as_ref(), app).await
}
Command::ConsoleLogs { lines, filter, window_id } => {
handlers::console_logs(&id, lines, filter.as_deref(), &window_id, state).await
}
}
}
async fn send_response<S>(
ws_tx: &Arc<Mutex<S>>,
response: &Response,
) -> Result<(), String>
where
S: futures_util::Sink<Message, Error = tokio_tungstenite::tungstenite::Error> + Unpin,
{
let json = serde_json::to_string(response).map_err(|e| e.to_string())?;
let mut tx = ws_tx.lock().await;
tx.send(Message::Text(json.into()))
.await
.map_err(|e| e.to_string())
}
fn find_available_port(addr: &str, start: u16, end: u16) -> Option<u16> {
(start..end).find(|&port| TcpListener::bind((addr, port)).is_ok())
}