use std::path::PathBuf;
use std::sync::Arc;
use axum::extract::State as AxumState;
use axum::routing::{get, post};
use axum::{Json, Router};
use tokio::sync::RwLock;
use tower_http::cors::CorsLayer;
use rdesktop_core::config::DevConfig;
use crate::agent_api;
#[derive(Clone)]
pub struct DevServerState {
pub last_dom_snapshot: Arc<RwLock<Option<String>>>,
pub last_app_state: Arc<RwLock<Option<serde_json::Value>>>,
pub frontend_dir: PathBuf,
}
pub struct DevServer {
config: DevConfig,
frontend_dir: PathBuf,
}
impl DevServer {
pub fn new(config: DevConfig, frontend_dir: PathBuf) -> Self {
Self {
config,
frontend_dir,
}
}
pub async fn start(&self) -> anyhow::Result<String> {
let addr = format!("{}:{}", self.config.host, self.config.port);
let url = format!("http://{}", addr);
let state = DevServerState {
last_dom_snapshot: Arc::new(RwLock::new(None)),
last_app_state: Arc::new(RwLock::new(None)),
frontend_dir: self.frontend_dir.clone(),
};
let app = Router::new()
.route("/__rdesktop__/agent/dom", get(agent_api::get_dom))
.route("/__rdesktop__/agent/elements", get(agent_api::query_elements))
.route("/__rdesktop__/agent/action", post(agent_api::execute_action))
.route("/__rdesktop__/agent/state", get(agent_api::get_state))
.route("/__rdesktop__/agent/ipc", post(agent_api::send_ipc))
.route("/__rdesktop__/agent/screenshot", get(agent_api::take_screenshot))
.route("/__rdesktop__/health", get(|| async { "ok" }))
.route("/__rdesktop__/info", get(dev_info))
.route("/__rdesktop__/state", post(update_state))
.route("/__rdesktop__/dom", post(update_dom))
.layer(CorsLayer::permissive())
.with_state(state.clone())
.fallback_service(tower_http::services::ServeDir::new(&self.frontend_dir));
tracing::info!("rdesktop dev server starting at {}", url);
if self.config.agent_mode {
tracing::info!("Agent API available at {}/__rdesktop__/agent/", url);
}
let listener = tokio::net::TcpListener::bind(&addr).await?;
tracing::info!("Listening on {}", addr);
let server_url = url.clone();
tokio::spawn(async move {
if let Err(e) = axum::serve(listener, app).await {
tracing::error!("Server error: {}", e);
}
});
if self.config.open_browser {
if let Err(e) = open::that(&url) {
tracing::warn!("Failed to open browser: {}", e);
}
}
Ok(server_url)
}
}
async fn dev_info() -> Json<serde_json::Value> {
Json(serde_json::json!({
"framework": "rdesktop",
"mode": "development",
"version": env!("CARGO_PKG_VERSION"),
"agent_api": true,
"endpoints": {
"dom": "/__rdesktop__/agent/dom",
"elements": "/__rdesktop__/agent/elements?selector=<css>",
"action": "/__rdesktop__/agent/action",
"state": "/__rdesktop__/agent/state",
"ipc": "/__rdesktop__/agent/ipc",
"screenshot": "/__rdesktop__/agent/screenshot",
}
}))
}
async fn update_dom(
AxumState(state): AxumState<DevServerState>,
Json(body): Json<serde_json::Value>,
) -> Json<serde_json::Value> {
let html = body["html"].as_str().unwrap_or("").to_string();
let mut snapshot = state.last_dom_snapshot.write().await;
*snapshot = Some(html);
Json(serde_json::json!({ "ok": true }))
}
async fn update_state(
AxumState(state): AxumState<DevServerState>,
Json(body): Json<serde_json::Value>,
) -> Json<serde_json::Value> {
let mut app_state = state.last_app_state.write().await;
*app_state = Some(body);
Json(serde_json::json!({ "ok": true }))
}