1use std::path::PathBuf;
12use std::sync::Arc;
13
14use axum::extract::State as AxumState;
15use axum::routing::{get, post};
16use axum::{Json, Router};
17use tokio::sync::RwLock;
18use tower_http::cors::CorsLayer;
19
20use rdesktop_core::config::DevConfig;
21
22use crate::agent_api;
23
24#[derive(Clone)]
26pub struct DevServerState {
27 pub last_dom_snapshot: Arc<RwLock<Option<String>>>,
29
30 pub last_app_state: Arc<RwLock<Option<serde_json::Value>>>,
32
33 pub frontend_dir: PathBuf,
35}
36
37pub struct DevServer {
42 config: DevConfig,
43 frontend_dir: PathBuf,
44}
45
46impl DevServer {
47 pub fn new(config: DevConfig, frontend_dir: PathBuf) -> Self {
49 Self {
50 config,
51 frontend_dir,
52 }
53 }
54
55 pub async fn start(&self) -> anyhow::Result<String> {
59 let addr = format!("{}:{}", self.config.host, self.config.port);
60 let url = format!("http://{}", addr);
61
62 let state = DevServerState {
63 last_dom_snapshot: Arc::new(RwLock::new(None)),
64 last_app_state: Arc::new(RwLock::new(None)),
65 frontend_dir: self.frontend_dir.clone(),
66 };
67
68 let app = Router::new()
70 .route("/__rdesktop__/agent/dom", get(agent_api::get_dom))
72 .route("/__rdesktop__/agent/elements", get(agent_api::query_elements))
73 .route("/__rdesktop__/agent/action", post(agent_api::execute_action))
74 .route("/__rdesktop__/agent/state", get(agent_api::get_state))
75 .route("/__rdesktop__/agent/ipc", post(agent_api::send_ipc))
76 .route("/__rdesktop__/agent/screenshot", get(agent_api::take_screenshot))
77 .route("/__rdesktop__/health", get(|| async { "ok" }))
79 .route("/__rdesktop__/info", get(dev_info))
81 .route("/__rdesktop__/state", post(update_state))
83 .route("/__rdesktop__/dom", post(update_dom))
84 .layer(CorsLayer::permissive())
86 .with_state(state.clone())
87 .fallback_service(tower_http::services::ServeDir::new(&self.frontend_dir));
89
90 tracing::info!("rdesktop dev server starting at {}", url);
91 if self.config.agent_mode {
92 tracing::info!("Agent API available at {}/__rdesktop__/agent/", url);
93 }
94
95 let listener = tokio::net::TcpListener::bind(&addr).await?;
96 tracing::info!("Listening on {}", addr);
97
98 let server_url = url.clone();
100 tokio::spawn(async move {
101 if let Err(e) = axum::serve(listener, app).await {
102 tracing::error!("Server error: {}", e);
103 }
104 });
105
106 if self.config.open_browser {
108 if let Err(e) = open::that(&url) {
109 tracing::warn!("Failed to open browser: {}", e);
110 }
111 }
112
113 Ok(server_url)
114 }
115}
116
117async fn dev_info() -> Json<serde_json::Value> {
119 Json(serde_json::json!({
120 "framework": "rdesktop",
121 "mode": "development",
122 "version": env!("CARGO_PKG_VERSION"),
123 "agent_api": true,
124 "endpoints": {
125 "dom": "/__rdesktop__/agent/dom",
126 "elements": "/__rdesktop__/agent/elements?selector=<css>",
127 "action": "/__rdesktop__/agent/action",
128 "state": "/__rdesktop__/agent/state",
129 "ipc": "/__rdesktop__/agent/ipc",
130 "screenshot": "/__rdesktop__/agent/screenshot",
131 }
132 }))
133}
134
135async fn update_dom(
137 AxumState(state): AxumState<DevServerState>,
138 Json(body): Json<serde_json::Value>,
139) -> Json<serde_json::Value> {
140 let html = body["html"].as_str().unwrap_or("").to_string();
141 let mut snapshot = state.last_dom_snapshot.write().await;
142 *snapshot = Some(html);
143 Json(serde_json::json!({ "ok": true }))
144}
145
146async fn update_state(
148 AxumState(state): AxumState<DevServerState>,
149 Json(body): Json<serde_json::Value>,
150) -> Json<serde_json::Value> {
151 let mut app_state = state.last_app_state.write().await;
152 *app_state = Some(body);
153 Json(serde_json::json!({ "ok": true }))
154}