1#![warn(missing_docs)]
6
7use std::any::type_name_of_val;
8use std::cell::RefCell;
9use std::collections::{BTreeMap, BTreeSet};
10use std::env;
11use std::fmt::Debug;
12use std::fs;
13use std::io::{Read, Write};
14use std::net::{TcpListener, TcpStream};
15use std::path::{Path, PathBuf};
16use std::process;
17use std::sync::atomic::{AtomicU64, Ordering};
18use std::sync::{Mutex, MutexGuard, OnceLock};
19
20use serde::{Deserialize, Serialize};
21
22#[derive(Clone, Debug, Serialize, Deserialize)]
24pub struct Node {
25 pub id: String,
27 pub label: String,
29 pub kind: NodeKind,
31 pub module_path: String,
33 pub file: String,
35 pub line: u32,
37}
38
39#[derive(Clone, Debug, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum NodeKind {
43 Function,
45 Type,
47 Test,
49}
50
51#[derive(Clone, Debug, Serialize, Deserialize)]
53pub struct Edge {
54 pub from: String,
56 pub to: String,
58}
59
60#[derive(Clone, Debug, Serialize, Deserialize)]
62pub struct ValueSlot {
63 pub name: String,
65 pub preview: String,
67}
68
69#[derive(Clone, Debug, Serialize, Deserialize)]
71pub struct Event {
72 pub seq: u64,
74 pub call_id: Option<u64>,
76 pub parent_call_id: Option<u64>,
78 pub node_id: String,
80 pub kind: EventKind,
82 pub title: String,
84 pub values: Vec<ValueSlot>,
86}
87
88#[derive(Clone, Debug, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum EventKind {
92 FunctionEnter,
94 FunctionExit,
96 ValueSnapshot,
98 TestStarted,
100 TestPassed,
102 TestFailed,
104}
105
106#[derive(Clone, Debug, Serialize, Deserialize)]
108pub struct Session {
109 pub title: String,
111 pub nodes: Vec<Node>,
113 pub edges: Vec<Edge>,
115 pub events: Vec<Event>,
117}
118
119impl Session {
120 pub fn new(title: impl Into<String>) -> Self {
122 Self {
123 title: title.into(),
124 nodes: Vec::new(),
125 edges: Vec::new(),
126 events: Vec::new(),
127 }
128 }
129}
130
131#[derive(Clone, Copy, Debug)]
133pub struct FunctionMeta {
134 pub id: &'static str,
136 pub label: &'static str,
138 pub module_path: &'static str,
140 pub file: &'static str,
142 pub line: u32,
144}
145
146#[derive(Clone, Copy, Debug)]
148pub struct TypeMeta {
149 pub id: &'static str,
151 pub label: &'static str,
153 pub module_path: &'static str,
155 pub file: &'static str,
157 pub line: u32,
159}
160
161#[derive(Clone, Debug)]
162struct CallFrame {
163 call_id: u64,
164 node_id: String,
165}
166
167#[derive(Default)]
168struct RuntimeState {
169 title: String,
170 nodes: BTreeMap<String, Node>,
171 edges: BTreeSet<(String, String)>,
172 events: Vec<Event>,
173 last_event_node_id: Option<String>,
174 next_seq: AtomicU64,
175 next_call_id: AtomicU64,
176}
177
178thread_local! {
179 static CALL_STACK: RefCell<Vec<CallFrame>> = const { RefCell::new(Vec::new()) };
180}
181
182static STATE: OnceLock<Mutex<RuntimeState>> = OnceLock::new();
183
184fn state() -> &'static Mutex<RuntimeState> {
185 STATE.get_or_init(|| Mutex::new(RuntimeState::default()))
186}
187
188fn lock_state() -> MutexGuard<'static, RuntimeState> {
189 state().lock().expect("dbgflow-core runtime mutex poisoned")
190}
191
192fn next_seq(state: &RuntimeState) -> u64 {
193 state.next_seq.fetch_add(1, Ordering::Relaxed) + 1
194}
195
196fn next_call_id(state: &RuntimeState) -> u64 {
197 state.next_call_id.fetch_add(1, Ordering::Relaxed) + 1
198}
199
200fn push_event(state: &mut RuntimeState, mut event: Event) {
201 event.seq = next_seq(state);
202 state.last_event_node_id = Some(event.node_id.clone());
203 state.events.push(event);
204}
205
206fn ensure_node(state: &mut RuntimeState, node: Node) {
207 state.nodes.entry(node.id.clone()).or_insert(node);
208}
209
210fn ensure_edge(state: &mut RuntimeState, from: &str, to: &str) {
211 if from == to {
212 return;
213 }
214
215 state.edges.insert((from.to_owned(), to.to_owned()));
216}
217
218pub fn reset_session(title: impl Into<String>) {
220 let mut state = lock_state();
221 state.title = title.into();
222 state.nodes.clear();
223 state.edges.clear();
224 state.events.clear();
225 state.last_event_node_id = None;
226 state.next_seq.store(0, Ordering::Relaxed);
227 state.next_call_id.store(0, Ordering::Relaxed);
228 CALL_STACK.with(|stack| stack.borrow_mut().clear());
229}
230
231pub fn current_session() -> Session {
233 let state = lock_state();
234 let nodes = state.nodes.values().cloned().collect();
235 let edges = state
236 .edges
237 .iter()
238 .map(|(from, to)| Edge {
239 from: from.clone(),
240 to: to.clone(),
241 })
242 .collect();
243
244 Session {
245 title: state.title.clone(),
246 nodes,
247 edges,
248 events: state.events.clone(),
249 }
250}
251
252pub fn write_session_json(path: impl AsRef<Path>) -> std::io::Result<()> {
254 let session = current_session();
255 let json = serde_json::to_string_pretty(&session).map_err(std::io::Error::other)?;
256
257 if let Some(parent) = path.as_ref().parent() {
258 fs::create_dir_all(parent)?;
259 }
260
261 fs::write(path, json)
262}
263
264pub fn read_session_json(path: impl AsRef<Path>) -> std::io::Result<Session> {
266 let content = fs::read_to_string(path)?;
267 let session = serde_json::from_str(&content).map_err(std::io::Error::other)?;
268 Ok(session)
269}
270
271fn sanitize_filename(label: &str) -> String {
272 let sanitized: String = label
273 .chars()
274 .map(|ch| match ch {
275 'a'..='z' | 'A'..='Z' | '0'..='9' => ch,
276 _ => '-',
277 })
278 .collect();
279
280 sanitized.trim_matches('-').to_owned()
281}
282
283pub fn write_session_snapshot_in_dir(
285 dir: impl AsRef<Path>,
286 label: impl AsRef<str>,
287) -> std::io::Result<PathBuf> {
288 fs::create_dir_all(&dir)?;
289
290 let file_name = format!(
291 "{}-{}.json",
292 sanitize_filename(label.as_ref()),
293 process::id()
294 );
295 let path = dir.as_ref().join(file_name);
296 write_session_json(&path)?;
297 Ok(path)
298}
299
300pub fn write_session_snapshot_from_env(label: impl AsRef<str>) -> std::io::Result<Option<PathBuf>> {
302 match env::var("DBG_SESSION_DIR") {
303 Ok(dir) => write_session_snapshot_in_dir(dir, label).map(Some),
304 Err(env::VarError::NotPresent) => Ok(None),
305 Err(error) => Err(std::io::Error::other(error)),
306 }
307}
308
309pub fn type_preview<T>(value: &T) -> String {
311 format!("type {}", type_name_of_val(value))
312}
313
314pub trait UiDebugValue: Debug + Sized {
316 fn ui_debug_type_meta() -> TypeMeta;
318
319 fn ui_debug_snapshot(&self) -> String {
321 format!("{self:#?}")
322 }
323
324 fn emit_snapshot(&self, label: impl Into<String>) {
326 runtime::record_type_snapshot(self, label.into());
327 }
328}
329
330pub mod runtime {
332 use super::{
333 CALL_STACK, CallFrame, Event, EventKind, FunctionMeta, Node, NodeKind, UiDebugValue,
334 ValueSlot, ensure_edge, ensure_node, lock_state, next_call_id, push_event,
335 };
336
337 #[derive(Debug)]
339 pub struct TraceFrame {
340 call_id: u64,
341 parent_call_id: Option<u64>,
342 node_id: &'static str,
343 finished: bool,
344 }
345
346 impl TraceFrame {
347 pub fn enter(meta: FunctionMeta, values: Vec<ValueSlot>) -> Self {
349 let (call_id, parent_call_id) = {
350 let mut state = lock_state();
351
352 ensure_node(
353 &mut state,
354 Node {
355 id: meta.id.to_owned(),
356 label: meta.label.to_owned(),
357 kind: NodeKind::Function,
358 module_path: meta.module_path.to_owned(),
359 file: meta.file.to_owned(),
360 line: meta.line,
361 },
362 );
363
364 let call_id = next_call_id(&state);
365 let parent_call_id = CALL_STACK.with(|stack| {
366 let stack = stack.borrow();
367 stack.last().map(|frame| frame.call_id)
368 });
369
370 let parent_node = CALL_STACK.with(|stack| {
371 let stack = stack.borrow();
372 stack.last().map(|frame| frame.node_id.clone())
373 });
374
375 if let Some(parent_node) = parent_node {
376 ensure_edge(&mut state, &parent_node, meta.id);
377 }
378
379 push_event(
380 &mut state,
381 Event {
382 seq: 0,
383 call_id: Some(call_id),
384 parent_call_id,
385 node_id: meta.id.to_owned(),
386 kind: EventKind::FunctionEnter,
387 title: format!("enter {}", meta.label),
388 values,
389 },
390 );
391
392 (call_id, parent_call_id)
393 };
394
395 CALL_STACK.with(|stack| {
396 stack.borrow_mut().push(CallFrame {
397 call_id,
398 node_id: meta.id.to_owned(),
399 });
400 });
401
402 Self {
403 call_id,
404 parent_call_id,
405 node_id: meta.id,
406 finished: false,
407 }
408 }
409
410 pub fn finish_return<T>(&mut self, result: &T) {
412 if self.finished {
413 return;
414 }
415
416 {
417 let mut state = lock_state();
418 push_event(
419 &mut state,
420 Event {
421 seq: 0,
422 call_id: Some(self.call_id),
423 parent_call_id: self.parent_call_id,
424 node_id: self.node_id.to_owned(),
425 kind: EventKind::FunctionExit,
426 title: format!(
427 "return {}",
428 self.node_id.rsplit("::").next().unwrap_or(self.node_id)
429 ),
430 values: vec![ValueSlot {
431 name: "result".to_owned(),
432 preview: super::type_preview(result),
433 }],
434 },
435 );
436 }
437
438 self.finished = true;
439 pop_stack(self.call_id);
440 }
441
442 pub fn finish_error(&mut self, message: impl Into<String>) {
444 if self.finished {
445 return;
446 }
447
448 {
449 let mut state = lock_state();
450 push_event(
451 &mut state,
452 Event {
453 seq: 0,
454 call_id: Some(self.call_id),
455 parent_call_id: self.parent_call_id,
456 node_id: self.node_id.to_owned(),
457 kind: EventKind::FunctionExit,
458 title: format!(
459 "panic {}",
460 self.node_id.rsplit("::").next().unwrap_or(self.node_id)
461 ),
462 values: vec![ValueSlot {
463 name: "status".to_owned(),
464 preview: message.into(),
465 }],
466 },
467 );
468 }
469
470 self.finished = true;
471 pop_stack(self.call_id);
472 }
473 }
474
475 impl Drop for TraceFrame {
476 fn drop(&mut self) {
477 if self.finished {
478 return;
479 }
480
481 self.finish_error("unwound before explicit return");
482 }
483 }
484
485 fn pop_stack(call_id: u64) {
486 CALL_STACK.with(|stack| {
487 let mut stack = stack.borrow_mut();
488 if stack.last().map(|frame| frame.call_id) == Some(call_id) {
489 stack.pop();
490 }
491 });
492 }
493
494 pub fn preview_argument<T>(name: impl Into<String>, value: &T) -> ValueSlot {
496 ValueSlot {
497 name: name.into(),
498 preview: super::type_preview(value),
499 }
500 }
501
502 pub fn record_type_snapshot<T: UiDebugValue>(value: &T, label: impl Into<String>) {
504 let meta = T::ui_debug_type_meta();
505 let mut state = lock_state();
506
507 ensure_node(
508 &mut state,
509 Node {
510 id: meta.id.to_owned(),
511 label: meta.label.to_owned(),
512 kind: NodeKind::Type,
513 module_path: meta.module_path.to_owned(),
514 file: meta.file.to_owned(),
515 line: meta.line,
516 },
517 );
518
519 if let Some(parent_node) = CALL_STACK.with(|stack| {
520 let stack = stack.borrow();
521 stack.last().map(|frame| frame.node_id.clone())
522 }) {
523 ensure_edge(&mut state, &parent_node, meta.id);
524 }
525
526 push_event(
527 &mut state,
528 Event {
529 seq: 0,
530 call_id: CALL_STACK.with(|stack| {
531 let stack = stack.borrow();
532 stack.last().map(|frame| frame.call_id)
533 }),
534 parent_call_id: None,
535 node_id: meta.id.to_owned(),
536 kind: EventKind::ValueSnapshot,
537 title: label.into(),
538 values: vec![ValueSlot {
539 name: meta.label.to_owned(),
540 preview: value.ui_debug_snapshot(),
541 }],
542 },
543 );
544 }
545
546 pub fn record_test_started(test_name: impl Into<String>, node_id: impl Into<String>) {
548 record_test_event(
549 EventKind::TestStarted,
550 test_name.into(),
551 node_id.into(),
552 Vec::new(),
553 );
554 }
555
556 pub fn record_test_passed(test_name: impl Into<String>, node_id: impl Into<String>) {
558 record_test_event(
559 EventKind::TestPassed,
560 test_name.into(),
561 node_id.into(),
562 Vec::new(),
563 );
564 }
565
566 pub fn record_test_failed(
568 test_name: impl Into<String>,
569 node_id: impl Into<String>,
570 failure: impl Into<String>,
571 ) {
572 record_test_event(
573 EventKind::TestFailed,
574 test_name.into(),
575 node_id.into(),
576 vec![ValueSlot {
577 name: "failure".to_owned(),
578 preview: failure.into(),
579 }],
580 );
581 }
582
583 pub fn latest_node_id() -> Option<String> {
585 let state = lock_state();
586 state.last_event_node_id.clone()
587 }
588
589 pub fn record_test_started_latest(test_name: impl Into<String>) {
591 let test_name = test_name.into();
592 let node_id = latest_node_id().unwrap_or_else(|| format!("test::{test_name}"));
593 record_test_event(EventKind::TestStarted, test_name, node_id, Vec::new());
594 }
595
596 pub fn record_test_passed_latest(test_name: impl Into<String>) {
598 let test_name = test_name.into();
599 let node_id = latest_node_id().unwrap_or_else(|| format!("test::{test_name}"));
600 record_test_event(EventKind::TestPassed, test_name, node_id, Vec::new());
601 }
602
603 pub fn record_test_failed_latest(test_name: impl Into<String>, failure: impl Into<String>) {
605 let test_name = test_name.into();
606 let node_id = latest_node_id().unwrap_or_else(|| format!("test::{test_name}"));
607 record_test_event(
608 EventKind::TestFailed,
609 test_name,
610 node_id,
611 vec![ValueSlot {
612 name: "failure".to_owned(),
613 preview: failure.into(),
614 }],
615 );
616 }
617
618 fn record_test_event(
619 kind: EventKind,
620 test_name: String,
621 node_id: String,
622 values: Vec<ValueSlot>,
623 ) {
624 let test_id = format!("test::{test_name}");
625 let mut state = lock_state();
626
627 ensure_node(
628 &mut state,
629 Node {
630 id: test_id.clone(),
631 label: test_name.clone(),
632 kind: NodeKind::Test,
633 module_path: "cargo::test".to_owned(),
634 file: "<runner>".to_owned(),
635 line: 0,
636 },
637 );
638 ensure_edge(&mut state, &test_id, &node_id);
639
640 push_event(
641 &mut state,
642 Event {
643 seq: 0,
644 call_id: None,
645 parent_call_id: None,
646 node_id: test_id,
647 kind,
648 title: test_name,
649 values,
650 },
651 );
652 }
653}
654
655fn content_type(path: &str) -> &'static str {
656 match path {
657 "/" => "text/html; charset=utf-8",
658 "/app.js" => "application/javascript; charset=utf-8",
659 "/app.css" => "text/css; charset=utf-8",
660 "/session.json" => "application/json; charset=utf-8",
661 _ => "text/plain; charset=utf-8",
662 }
663}
664
665fn write_response(
666 stream: &mut TcpStream,
667 method: &str,
668 status: &str,
669 content_type: &str,
670 body: &str,
671) -> std::io::Result<()> {
672 let response = if method == "HEAD" {
673 format!(
674 "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
675 body.len()
676 )
677 } else {
678 format!(
679 "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
680 body.len()
681 )
682 };
683 stream.write_all(response.as_bytes())
684}
685
686fn read_request(stream: &mut TcpStream) -> std::io::Result<(String, String)> {
687 let mut buffer = [0_u8; 4096];
688 let bytes_read = stream.read(&mut buffer)?;
689 let request = String::from_utf8_lossy(&buffer[..bytes_read]);
690 let line = request.lines().next().unwrap_or_default();
691 let mut parts = line.split_whitespace();
692 let method = parts.next().unwrap_or("GET");
693 let path = parts.next().unwrap_or("/");
694 Ok((method.to_owned(), path.to_owned()))
695}
696
697pub fn serve_session(session: Session, host: &str, port: u16) -> std::io::Result<()> {
699 let listener = TcpListener::bind((host, port))?;
700 let json = serde_json::to_string(&session).map_err(std::io::Error::other)?;
701 let html = ui::index_html();
702 let app_js = ui::app_js();
703 let app_css = ui::app_css();
704
705 println!("Debugger UI: http://{host}:{port}");
706
707 for stream in listener.incoming() {
708 let mut stream = match stream {
709 Ok(stream) => stream,
710 Err(_) => continue,
711 };
712
713 let (method, path) = match read_request(&mut stream) {
714 Ok(request) => request,
715 Err(_) => continue,
716 };
717
718 let result = match path.as_str() {
719 "/" => write_response(&mut stream, &method, "200 OK", content_type("/"), &html),
720 "/app.js" => write_response(
721 &mut stream,
722 &method,
723 "200 OK",
724 content_type("/app.js"),
725 &app_js,
726 ),
727 "/app.css" => write_response(
728 &mut stream,
729 &method,
730 "200 OK",
731 content_type("/app.css"),
732 &app_css,
733 ),
734 "/session.json" => write_response(
735 &mut stream,
736 &method,
737 "200 OK",
738 content_type("/session.json"),
739 &json,
740 ),
741 _ => write_response(
742 &mut stream,
743 &method,
744 "404 Not Found",
745 content_type(""),
746 "not found",
747 ),
748 };
749
750 if let Err(error) = result {
751 if !matches!(
752 error.kind(),
753 std::io::ErrorKind::BrokenPipe | std::io::ErrorKind::ConnectionReset
754 ) {
755 return Err(error);
756 }
757 }
758 }
759
760 Ok(())
761}
762
763mod ui {
764 pub fn index_html() -> String {
765 include_str!("../ui/index.html").to_owned()
766 }
767
768 pub fn app_js() -> String {
769 include_str!("../ui/app.js").to_owned()
770 }
771
772 pub fn app_css() -> String {
773 include_str!("../ui/app.css").to_owned()
774 }
775}