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(
73 "/__rdesktop__/agent/elements",
74 get(agent_api::query_elements),
75 )
76 .route(
77 "/__rdesktop__/agent/action",
78 post(agent_api::execute_action),
79 )
80 .route("/__rdesktop__/agent/state", get(agent_api::get_state))
81 .route("/__rdesktop__/agent/ipc", post(agent_api::send_ipc))
82 .route(
83 "/__rdesktop__/agent/screenshot",
84 get(agent_api::take_screenshot),
85 )
86 .route("/__rdesktop__/health", get(|| async { "ok" }))
88 .route("/__rdesktop__/info", get(dev_info))
90 .route("/__rdesktop__/state", post(update_state))
92 .route("/__rdesktop__/dom", post(update_dom))
93 .layer(CorsLayer::permissive())
95 .with_state(state.clone())
96 .fallback_service(tower_http::services::ServeDir::new(&self.frontend_dir));
98
99 tracing::info!("rdesktop dev server starting at {}", url);
100 if self.config.agent_mode {
101 tracing::info!("Agent API available at {}/__rdesktop__/agent/", url);
102 }
103
104 let listener = tokio::net::TcpListener::bind(&addr).await?;
105 tracing::info!("Listening on {}", addr);
106
107 let server_url = url.clone();
109 tokio::spawn(async move {
110 if let Err(e) = axum::serve(listener, app).await {
111 tracing::error!("Server error: {}", e);
112 }
113 });
114
115 if self.config.open_browser {
117 if let Err(e) = open::that(&url) {
118 tracing::warn!("Failed to open browser: {}", e);
119 }
120 }
121
122 Ok(server_url)
123 }
124}
125
126async fn dev_info() -> Json<serde_json::Value> {
128 Json(serde_json::json!({
129 "framework": "rdesktop",
130 "mode": "development",
131 "version": env!("CARGO_PKG_VERSION"),
132 "agent_api": true,
133 "endpoints": {
134 "dom": "/__rdesktop__/agent/dom",
135 "elements": "/__rdesktop__/agent/elements?selector=<css>",
136 "action": "/__rdesktop__/agent/action",
137 "state": "/__rdesktop__/agent/state",
138 "ipc": "/__rdesktop__/agent/ipc",
139 "screenshot": "/__rdesktop__/agent/screenshot",
140 }
141 }))
142}
143
144async fn update_dom(
146 AxumState(state): AxumState<DevServerState>,
147 Json(body): Json<serde_json::Value>,
148) -> Json<serde_json::Value> {
149 let html = body["html"].as_str().unwrap_or("").to_string();
150 let mut snapshot = state.last_dom_snapshot.write().await;
151 *snapshot = Some(html);
152 Json(serde_json::json!({ "ok": true }))
153}
154
155async fn update_state(
157 AxumState(state): AxumState<DevServerState>,
158 Json(body): Json<serde_json::Value>,
159) -> Json<serde_json::Value> {
160 let mut app_state = state.last_app_state.write().await;
161 *app_state = Some(body);
162 Json(serde_json::json!({ "ok": true }))
163}