1use std::io::{Read, Write};
7use std::net::{TcpListener, TcpStream};
8use std::sync::{Arc, Mutex};
9use std::thread;
10
11use serde::Serialize;
12
13use crate::session::Session;
14
15pub type RerunHandler = Arc<dyn Fn() -> std::io::Result<Session> + Send + Sync>;
17
18struct ServerState {
20 session: Session,
21 generation: u64,
22 running: bool,
23 last_error: Option<String>,
24}
25
26#[derive(Serialize)]
28struct StatusResponse<'a> {
29 running: bool,
30 can_rerun: bool,
31 generation: u64,
32 session_title: &'a str,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 last_error: Option<&'a str>,
35}
36
37fn content_type_for_path(path: &str) -> &'static str {
43 match path {
44 "/" => "text/html; charset=utf-8",
45 "/app.js" => "application/javascript; charset=utf-8",
46 "/app.css" => "text/css; charset=utf-8",
47 "/globals.css" => "text/css; charset=utf-8",
48 "/session.json" => "application/json; charset=utf-8",
49 "/api/status" => "application/json; charset=utf-8",
50 "/api/rerun" => "application/json; charset=utf-8",
51 _ => "text/plain; charset=utf-8",
52 }
53}
54
55fn write_response(
57 stream: &mut TcpStream,
58 method: &str,
59 status: &str,
60 content_type: &str,
61 body: &str,
62) -> std::io::Result<()> {
63 let body_len = body.len();
64 let response = if method == "HEAD" {
65 format!(
66 "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {body_len}\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n"
67 )
68 } else {
69 format!(
70 "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {body_len}\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n{body}"
71 )
72 };
73 stream.write_all(response.as_bytes())
74}
75
76fn parse_request(stream: &mut TcpStream) -> std::io::Result<(String, String)> {
78 let mut buffer = [0_u8; 4096];
79 let bytes_read = stream.read(&mut buffer)?;
80 let request = String::from_utf8_lossy(&buffer[..bytes_read]);
81 let line = request.lines().next().unwrap_or_default();
82 let mut parts = line.split_whitespace();
83 let method = parts.next().unwrap_or("GET");
84 let raw_path = parts.next().unwrap_or("/");
85 let path = raw_path.split(['?', '#']).next().unwrap_or("/");
86 Ok((method.to_owned(), path.to_owned()))
87}
88
89fn json_body<T: Serialize>(value: &T) -> std::io::Result<String> {
91 serde_json::to_string(value).map_err(std::io::Error::other)
92}
93
94fn serve_session_inner(
100 session: Session,
101 host: &str,
102 port: u16,
103 rerun: Option<RerunHandler>,
104) -> std::io::Result<()> {
105 let listener = TcpListener::bind((host, port))?;
106 let shared = Arc::new(Mutex::new(ServerState {
107 session,
108 generation: 0,
109 running: false,
110 last_error: None,
111 }));
112
113 let html = super::ui::index_html();
114 let app_js = super::ui::app_js();
115 let app_css = super::ui::app_css();
116 let globals_css = super::ui::globals_css();
117
118 println!("Debugger UI: http://{host}:{port}");
119
120 for stream in listener.incoming() {
121 let mut stream = match stream {
122 Ok(stream) => stream,
123 Err(_) => continue,
124 };
125
126 let (method, path) = match parse_request(&mut stream) {
127 Ok(request) => request,
128 Err(_) => continue,
129 };
130
131 let result = handle_request(
132 &mut stream,
133 &method,
134 &path,
135 &shared,
136 &rerun,
137 &html,
138 &app_js,
139 &app_css,
140 &globals_css,
141 );
142
143 if let Err(error) = result {
144 if !matches!(
146 error.kind(),
147 std::io::ErrorKind::BrokenPipe | std::io::ErrorKind::ConnectionReset
148 ) {
149 return Err(error);
150 }
151 }
152 }
153
154 Ok(())
155}
156
157#[allow(clippy::too_many_arguments)]
159fn handle_request(
160 stream: &mut TcpStream,
161 method: &str,
162 path: &str,
163 shared: &Arc<Mutex<ServerState>>,
164 rerun: &Option<RerunHandler>,
165 html: &str,
166 app_js: &str,
167 app_css: &str,
168 globals_css: &str,
169) -> std::io::Result<()> {
170 match (method, path) {
171 (_, "/") => write_response(stream, method, "200 OK", content_type_for_path("/"), html),
172 (_, "/app.js") => write_response(
173 stream,
174 method,
175 "200 OK",
176 content_type_for_path("/app.js"),
177 app_js,
178 ),
179 (_, "/app.css") => write_response(
180 stream,
181 method,
182 "200 OK",
183 content_type_for_path("/app.css"),
184 app_css,
185 ),
186 (_, "/globals.css") => write_response(
187 stream,
188 method,
189 "200 OK",
190 content_type_for_path("/globals.css"),
191 globals_css,
192 ),
193 (_, "/session.json") => {
194 let body = {
195 let state = shared
196 .lock()
197 .expect("dbgflow-core serve session mutex poisoned");
198 json_body(&state.session)?
199 };
200 write_response(
201 stream,
202 method,
203 "200 OK",
204 content_type_for_path("/session.json"),
205 &body,
206 )
207 }
208 (_, "/api/status") => {
209 let body = {
210 let state = shared
211 .lock()
212 .expect("dbgflow-core serve session mutex poisoned");
213 let status = StatusResponse {
214 running: state.running,
215 can_rerun: rerun.is_some(),
216 generation: state.generation,
217 session_title: &state.session.title,
218 last_error: state.last_error.as_deref(),
219 };
220 json_body(&status)?
221 };
222 write_response(
223 stream,
224 method,
225 "200 OK",
226 content_type_for_path("/api/status"),
227 &body,
228 )
229 }
230 ("POST", "/api/rerun") => handle_rerun_request(stream, method, shared, rerun),
231 _ => write_response(
232 stream,
233 method,
234 "404 Not Found",
235 content_type_for_path(""),
236 "not found",
237 ),
238 }
239}
240
241fn handle_rerun_request(
243 stream: &mut TcpStream,
244 method: &str,
245 shared: &Arc<Mutex<ServerState>>,
246 rerun: &Option<RerunHandler>,
247) -> std::io::Result<()> {
248 let body = if let Some(rerun_handler) = rerun.clone() {
249 let should_spawn = {
250 let mut state = shared
251 .lock()
252 .expect("dbgflow-core serve session mutex poisoned");
253 if state.running {
254 false
255 } else {
256 state.running = true;
257 state.last_error = None;
258 true
259 }
260 };
261
262 if should_spawn {
263 let shared = Arc::clone(shared);
264 thread::spawn(move || {
265 let rerun_result = rerun_handler();
266 let mut state = shared
267 .lock()
268 .expect("dbgflow-core serve session mutex poisoned");
269 state.running = false;
270 match rerun_result {
271 Ok(session) => {
272 state.session = session;
273 state.generation += 1;
274 }
275 Err(error) => {
276 state.last_error = Some(error.to_string());
277 }
278 }
279 });
280 }
281
282 let state = shared
283 .lock()
284 .expect("dbgflow-core serve session mutex poisoned");
285 let status = StatusResponse {
286 running: state.running,
287 can_rerun: true,
288 generation: state.generation,
289 session_title: &state.session.title,
290 last_error: state.last_error.as_deref(),
291 };
292 json_body(&status)?
293 } else {
294 json_body(&serde_json::json!({
295 "running": false,
296 "can_rerun": false,
297 "generation": 0_u64,
298 "last_error": "rerun is not available for this session"
299 }))?
300 };
301
302 let status = if rerun.is_some() {
303 "202 Accepted"
304 } else {
305 "405 Method Not Allowed"
306 };
307
308 write_response(
309 stream,
310 method,
311 status,
312 content_type_for_path("/api/rerun"),
313 &body,
314 )
315}
316
317pub fn serve_session(session: Session, host: &str, port: u16) -> std::io::Result<()> {
323 serve_session_inner(session, host, port, None)
324}
325
326pub fn serve_session_with_rerun(
332 session: Session,
333 host: &str,
334 port: u16,
335 rerun: impl Fn() -> std::io::Result<Session> + Send + Sync + 'static,
336) -> std::io::Result<()> {
337 serve_session_inner(session, host, port, Some(Arc::new(rerun)))
338}