1use std::{
20 sync::Arc,
21 time::{
22 Duration,
23 Instant,
24 },
25};
26
27use arboard::Clipboard;
28use crossterm::event::{
29 KeyCode,
30 KeyModifiers,
31};
32use nix::{
33 errno::Errno,
34 sys::signal::Signal,
35 unistd::Pid,
36};
37use ratatui::{
38 layout::{
39 Position,
40 Size,
41 },
42 style::Stylize,
43 text::Line,
44 widgets::Widget,
45};
46use tracexec_backend_ptrace::ptrace::RunningTracer;
47use tracexec_core::{
48 cli::{
49 args::{
50 DebuggerArgs,
51 LogModeArgs,
52 ModifierArgs,
53 TuiModeArgs,
54 },
55 config::ExitHandling,
56 options::{
57 ActivePane,
58 AppLayout,
59 },
60 },
61 event::{
62 Event,
63 ProcessStateUpdate,
64 ProcessStateUpdateEvent,
65 TracerEventDetails,
66 TracerMessage,
67 },
68 primitives::local_chan,
69 printer::PrinterArgs,
70 proc::BaselineInfo,
71 pty::{
72 PtySize,
73 UnixMasterPty,
74 },
75};
76use tracing::{
77 debug,
78 trace,
79};
80
81use super::{
82 Tui,
83 breakpoint_manager::BreakPointManagerState,
84 copy_popup::CopyPopupState,
85 event_list::EventList,
86 hit_manager::HitManagerState,
87 pseudo_term::PseudoTerminalPane,
88 query::QueryBuilder,
89};
90use crate::{
91 action::{
92 Action,
93 ActivePopup,
94 },
95 error_popup::InfoPopupState,
96 event::TracerEventDetailsTuiExt,
97 query::QueryKind,
98};
99
100mod ui;
101
102pub const DEFAULT_MAX_EVENTS: u64 = 1_000_000;
103
104pub struct App {
105 pub event_list: EventList,
106 pub printer_args: PrinterArgs,
107 pub term: Option<PseudoTerminalPane>,
108 pub root_pid: Option<Pid>,
109 pub active_pane: ActivePane,
110 pub clipboard: Option<Clipboard>,
111 pub split_percentage: u16,
112 pub layout: AppLayout,
113 pub should_handle_internal_resize: bool,
114 pub popup: Vec<ActivePopup>,
115 pub active_experiments: Vec<&'static str>,
116 tracer: Option<RunningTracer>,
117 query_builder: Option<QueryBuilder>,
118 breakpoint_manager: Option<BreakPointManagerState>,
119 hit_manager_state: Option<HitManagerState>,
120 exit_handling: ExitHandling,
121}
122
123pub struct PTracer {
124 pub tracer: RunningTracer,
125 pub debugger_args: DebuggerArgs,
126}
127
128impl App {
129 #[allow(clippy::too_many_arguments)]
130 pub fn new(
131 mut tracer: Option<PTracer>,
132 tracing_args: &LogModeArgs,
133 modifier_args: &ModifierArgs,
134 tui_args: TuiModeArgs,
135 baseline: Arc<BaselineInfo>,
136 pty_master: Option<UnixMasterPty>,
137 ) -> color_eyre::Result<Self> {
138 let active_pane = if pty_master.is_some() {
139 tui_args.active_pane.unwrap_or_default()
140 } else {
141 ActivePane::Events
142 };
143 if let Some(tracer) = tracer.as_mut() {
144 for bp in tracer.debugger_args.breakpoints.drain(..) {
145 tracer.tracer.add_breakpoint(bp);
146 }
147 }
148 let clipboard = Clipboard::new().ok();
149 Ok(Self {
150 event_list: EventList::new(
151 baseline,
152 tui_args.follow,
153 modifier_args.to_owned(),
154 tui_args.max_events.unwrap_or(DEFAULT_MAX_EVENTS),
155 tracer.is_some(),
156 clipboard.is_some(),
157 true,
158 ),
159 printer_args: PrinterArgs::from_cli(tracing_args, modifier_args),
160 split_percentage: if pty_master.is_some() { 50 } else { 100 },
161 term: if let Some(pty_master) = pty_master {
162 let mut term = PseudoTerminalPane::new(
163 PtySize {
164 rows: 24,
165 cols: 80,
166 pixel_width: 0,
167 pixel_height: 0,
168 },
169 pty_master,
170 )?;
171 if active_pane == ActivePane::Terminal {
172 term.focus(true);
173 }
174 Some(term)
175 } else {
176 None
177 },
178 root_pid: None,
179 active_pane,
180 clipboard,
181 layout: tui_args.layout.unwrap_or_default(),
182 should_handle_internal_resize: true,
183 popup: vec![],
184 query_builder: None,
185 breakpoint_manager: None,
186 active_experiments: vec![],
187 tracer: tracer.as_ref().map(|t| t.tracer.clone()),
188 hit_manager_state: tracer
189 .map(|t| HitManagerState::new(t.tracer, t.debugger_args.default_external_command))
190 .transpose()?,
191 exit_handling: {
192 if tui_args.kill_on_exit {
193 ExitHandling::Kill
194 } else if tui_args.terminate_on_exit {
195 ExitHandling::Terminate
196 } else {
197 ExitHandling::Wait
198 }
199 },
200 })
201 }
202
203 pub fn activate_experiment(&mut self, experiment: &'static str) {
204 self.active_experiments.push(experiment);
205 }
206
207 pub fn shrink_pane(&mut self) {
208 if self.term.is_some() {
209 self.split_percentage = self.split_percentage.saturating_sub(1).max(10);
210 }
211 }
212
213 pub fn grow_pane(&mut self) {
214 if self.term.is_some() {
215 self.split_percentage = self.split_percentage.saturating_add(1).min(90);
216 }
217 }
218
219 pub async fn run(&mut self, tui: &mut Tui) -> color_eyre::Result<()> {
220 let (action_tx, action_rx) = local_chan::unbounded();
221
222 let mut last_refresh_timestamp = Instant::now();
223 loop {
224 if let Some(e) = tui.next().await {
226 if e != Event::Render {
227 trace!("Received event {e:?}");
228 }
229 match e {
230 Event::ShouldQuit => {
231 action_tx.send(Action::Quit);
232 }
233 Event::Key(ke) => {
234 if ke.code == KeyCode::Char('s') && ke.modifiers.contains(KeyModifiers::CONTROL) {
235 action_tx.send(Action::SwitchActivePane);
236 self.popup.clear();
238 if self.query_builder.as_ref().is_some_and(|b| b.editing()) {
240 self.query_builder = None;
241 self.event_list.set_query(None).await;
242 }
243 if self.breakpoint_manager.is_some() {
245 self.breakpoint_manager = None;
246 }
247 if let Some(h) = self.hit_manager_state.as_mut()
249 && h.visible
250 {
251 h.hide();
252 }
253 } else {
255 trace!("TUI: Active pane: {}", self.active_pane);
256 if self.active_pane == ActivePane::Events {
257 if let Some(popup) = &mut self.popup.last_mut() {
260 match popup {
261 ActivePopup::Backtrace(state) => state.handle_key_event(ke, &action_tx).await?,
262 ActivePopup::Help => {
263 self.popup.pop();
264 }
265 ActivePopup::ViewDetails(state) => {
266 state.handle_key_event(
267 ke,
268 self.clipboard.as_mut(),
269 &self.event_list,
270 &action_tx,
271 )?;
272 }
273 ActivePopup::CopyTargetSelection(state) => {
274 if let Some(action) = state.handle_key_event(ke)? {
275 action_tx.send(action);
276 }
277 }
278 ActivePopup::InfoPopup(state) => {
279 if let Some(action) = state.handle_key_event(ke) {
280 action_tx.send(action);
281 }
282 }
283 }
284 continue;
285 }
286
287 if let Some(h) = self.hit_manager_state.as_mut()
289 && h.visible
290 {
291 if let Some(action) = h.handle_key_event(ke) {
292 action_tx.send(action);
293 }
294 continue;
295 }
296
297 if let Some(breakpoint_manager) = self.breakpoint_manager.as_mut() {
299 if let Some(action) = breakpoint_manager.handle_key_event(ke) {
300 action_tx.send(action);
301 }
302 continue;
303 }
304
305 if let Some(query_builder) = self.query_builder.as_mut() {
307 if query_builder.editing() {
308 match query_builder.handle_key_events(ke) {
309 Ok(Some(action)) => {
310 action_tx.send(action);
311 }
312 Ok(None) => {}
313 Err(e) => {
314 self
316 .popup
317 .push(ActivePopup::InfoPopup(InfoPopupState::error(
318 "Regex Error".to_owned(),
319 e,
320 )));
321 }
322 }
323 continue;
324 } else {
325 match (ke.code, ke.modifiers) {
326 (KeyCode::Char('n'), KeyModifiers::NONE) => {
327 trace!("Query: Next match");
328 action_tx.send(Action::NextMatch);
329 continue;
330 }
331 (KeyCode::Char('p'), KeyModifiers::NONE) => {
332 trace!("Query: Prev match");
333 action_tx.send(Action::PrevMatch);
334 continue;
335 }
336 _ => {}
337 }
338 }
339 }
340
341 match ke.code {
342 KeyCode::Char('q') if ke.modifiers == KeyModifiers::NONE => {
343 if !self.popup.is_empty() {
344 self.popup.pop();
345 } else {
346 action_tx.send(Action::Quit);
347 }
348 }
349 KeyCode::Char('l') if ke.modifiers == KeyModifiers::ALT => {
350 action_tx.send(Action::SwitchLayout);
351 }
352 _ => self.event_list.handle_key_event(ke, &action_tx).await?,
353 }
354 } else {
355 action_tx.send(Action::HandleTerminalKeyPress(ke));
356 }
358 }
359 }
360 Event::Tracer(msg) => {
361 match msg {
362 TracerMessage::Event(e) => {
363 if let TracerEventDetails::TraceeSpawn { pid, .. } = &e.details {
364 debug!("Received tracee spawn event: {pid}");
366 self.root_pid = Some(*pid);
367 }
368 self.event_list.push(e.id, e.details);
369 }
370 TracerMessage::StateUpdate(update) => {
371 trace!("Received process state update: {update:?}");
372 let mut handled = false;
373 match &update {
374 ProcessStateUpdateEvent {
375 update: ProcessStateUpdate::BreakPointHit(hit),
376 ..
377 } => {
378 self
379 .hit_manager_state
380 .access_some_mut(|h| _ = h.add_hit(*hit));
381 }
384 ProcessStateUpdateEvent {
385 update: ProcessStateUpdate::Detached { hid, .. },
386 pid,
387 ..
388 } => {
389 if let Some(Err(e)) = self
390 .hit_manager_state
391 .as_mut()
392 .map(|h| h.react_on_process_detach(*hid, *pid))
393 {
394 action_tx.send(Action::SetActivePopup(ActivePopup::InfoPopup(
395 InfoPopupState::error(
396 "Detach Error".to_owned(),
397 vec![
398 Line::default().spans(vec![
399 "Failed to run custom command after detaching process ".into(),
400 pid.to_string().bold(),
401 ". Error: ".into(),
402 ]),
403 e.to_string().into(),
404 ],
405 ),
406 )));
407 }
408 }
409 ProcessStateUpdateEvent {
410 update: ProcessStateUpdate::ResumeError { hit, error },
411 ..
412 } => {
413 if *error != Errno::ESRCH {
414 self
415 .hit_manager_state
416 .access_some_mut(|h| _ = h.add_hit(*hit));
417 }
418 action_tx.send(Action::SetActivePopup(ActivePopup::InfoPopup(
419 InfoPopupState::error(
420 "Resume Error".to_owned(),
421 vec![
422 Line::default().spans(vec![
423 "Failed to resume process ".into(),
424 hit.pid.to_string().bold(),
425 ". Error: ".into(),
426 ]),
427 error.to_string().into(),
428 ],
429 ),
430 )));
431 handled = true;
432 }
433 ProcessStateUpdateEvent {
434 update: ProcessStateUpdate::DetachError { hit, error },
435 ..
436 } => {
437 if *error != Errno::ESRCH {
438 self
439 .hit_manager_state
440 .access_some_mut(|h| _ = h.add_hit(*hit));
441 }
442 action_tx.send(Action::SetActivePopup(ActivePopup::InfoPopup(
443 InfoPopupState::error(
444 "Detach Error".to_owned(),
445 vec![
446 Line::default().spans(vec![
447 "Failed to detach process ".into(),
448 hit.pid.to_string().bold(),
449 ". Error: ".into(),
450 ]),
451 error.to_string().into(),
452 ],
453 ),
454 )));
455 handled = true;
456 }
457 _ => (),
458 }
459 if !handled {
460 self.event_list.update(update);
461 }
462 }
463 TracerMessage::FatalError(e) => {
464 action_tx.send(Action::SetActivePopup(ActivePopup::InfoPopup(
465 InfoPopupState::error(
466 "FATAL ERROR in tracer thread".to_string(),
467 vec![
468 Line::raw("The tracer thread has died abnormally! error: "),
469 e.into(),
470 ],
471 ),
472 )));
473 }
474 }
475 }
477 Event::Render => {
478 action_tx.send(Action::Render);
479 }
480 Event::Resize { width, height } => {
481 action_tx.send(Action::Resize(Size { width, height }));
482 }
484 Event::Init => {
485 action_tx.send(Action::Resize(tui.size()?));
487 }
489 Event::Error => {}
490 }
491 }
492
493 if self.event_list.is_following() {
495 let t = Instant::now();
496 if t.duration_since(last_refresh_timestamp) > Duration::from_millis(100) {
497 last_refresh_timestamp = t;
498 action_tx.send(Action::ScrollToBottom);
499 }
500 }
501
502 while let Some(action) = action_rx.receive() {
504 if !matches!(action, Action::Render) {
505 debug!("action: {action:?}");
506 }
507 match action {
508 Action::Quit => {
509 return Ok(());
510 }
511 Action::Render => {
512 tui.draw(|f| {
513 self.render(f.area(), f.buffer_mut());
514 self
515 .query_builder
516 .as_ref()
517 .filter(|q| q.editing())
518 .inspect(|q| {
519 let (x, y) = q.cursor();
520 f.set_cursor_position(Position::new(x, y));
521 });
522 if let Some((x, y)) = self
523 .breakpoint_manager
524 .as_ref()
525 .and_then(|mgr| mgr.cursor())
526 {
527 f.set_cursor_position(Position::new(x, y));
528 }
529 if let Some((x, y)) = self.hit_manager_state.as_ref().and_then(|x| x.cursor()) {
530 f.set_cursor_position(Position::new(x, y));
531 }
532 })?;
533 }
534 Action::NextItem => {
535 self.active_event_list().next();
536 }
537 Action::PrevItem => {
538 self.active_event_list().previous();
539 }
540 Action::PageDown => {
541 self.active_event_list().page_down();
542 }
543 Action::PageUp => {
544 self.active_event_list().page_up();
545 }
546 Action::PageLeft => {
547 self.active_event_list().page_left();
548 }
549 Action::PageRight => {
550 self.active_event_list().page_right();
551 }
552 Action::HandleTerminalKeyPress(ke) => {
553 if let Some(term) = self.term.as_mut() {
554 term.handle_key_event(&ke).await;
555 }
556 }
557 Action::Resize(_size) => {
558 self.should_handle_internal_resize = true;
559 }
560 Action::ScrollLeft => {
561 self.active_event_list().scroll_left();
562 }
563 Action::ScrollRight => {
564 self.active_event_list().scroll_right();
565 }
566 Action::ScrollToTop => {
567 self.active_event_list().scroll_to_top();
568 }
569 Action::ScrollToBottom => {
570 self.active_event_list().scroll_to_bottom();
571 }
572 Action::ScrollToStart => {
573 self.active_event_list().scroll_to_start();
574 }
575 Action::ScrollToEnd => {
576 self.active_event_list().scroll_to_end();
577 }
578 Action::ScrollToId(id) => {
579 self.active_event_list().scroll_to_id(Some(id));
580 }
581 Action::ToggleFollow => {
582 self.event_list.toggle_follow();
583 if self.event_list.is_following() {
584 action_tx.send(Action::ScrollToBottom);
585 }
586 }
587 Action::ToggleEnvDisplay => {
588 self.active_event_list().toggle_env_display();
589 }
590 Action::ToggleCwdDisplay => {
591 self.active_event_list().toggle_cwd_display();
592 }
593 Action::StopFollow => {
594 self.event_list.stop_follow();
595 }
596 Action::ShrinkPane => {
597 self.shrink_pane();
598 self.should_handle_internal_resize = true;
599 }
600 Action::GrowPane => {
601 self.grow_pane();
602 self.should_handle_internal_resize = true;
603 }
604 Action::SwitchLayout => {
605 self.layout = match self.layout {
606 AppLayout::Horizontal => AppLayout::Vertical,
607 AppLayout::Vertical => AppLayout::Horizontal,
608 };
609 self.should_handle_internal_resize = true;
610 }
611 Action::SwitchActivePane => {
612 self.active_pane = match self.active_pane {
613 ActivePane::Events => {
614 if let Some(term) = self.term.as_mut() {
615 term.focus(true);
616 ActivePane::Terminal
617 } else {
618 if let Some(t) = self.term.as_mut() {
619 t.focus(false)
620 }
621 ActivePane::Events
622 }
623 }
624 ActivePane::Terminal => {
625 if let Some(t) = self.term.as_mut() {
626 t.focus(false)
627 }
628 ActivePane::Events
629 }
630 }
631 }
632 Action::ShowCopyDialog(e) => {
633 self
634 .popup
635 .push(ActivePopup::CopyTargetSelection(CopyPopupState::new(e)));
636 }
637 Action::CopyToClipboard { event, target } => {
638 let text = event.text_for_copy(
639 &self.event_list.baseline,
640 target,
641 &self.event_list.modifier_args,
642 self.event_list.runtime_modifier(),
643 );
644 if let Some(clipboard) = self.clipboard.as_mut() {
646 clipboard.set_text(text)?;
647 }
648 self.popup.pop();
650 }
651 Action::SetActivePopup(popup) => {
652 self.popup.push(popup);
653 }
654 Action::CancelCurrentPopup => {
655 self.popup.pop();
656 }
657 Action::BeginSearch => {
658 if let Some(query_builder) = self.query_builder.as_mut() {
659 query_builder.edit();
661 } else {
662 let mut query_builder = QueryBuilder::new(QueryKind::Search);
663 query_builder.edit();
665 self.query_builder = Some(query_builder);
666 }
667 }
668 Action::EndSearch => {
669 self.query_builder = None;
670 self.event_list.set_query(None).await;
671 }
672 Action::ExecuteSearch(query) => {
673 self.event_list.set_query(Some(query)).await;
674 }
675 Action::NextMatch => {
676 self.event_list.next_match().await;
677 }
678 Action::PrevMatch => {
679 self.event_list.prev_match().await;
680 }
681 Action::ShowBreakpointManager => {
682 if self.breakpoint_manager.is_none() {
683 self.breakpoint_manager = Some(BreakPointManagerState::new(
684 self
685 .tracer
686 .as_ref()
687 .expect("BreakPointManager doesn't work without PTracer!")
688 .clone(),
689 ));
690 }
691 }
692 Action::CloseBreakpointManager => {
693 self.breakpoint_manager = None;
694 }
695 Action::ShowHitManager => {
696 self.hit_manager_state.access_some_mut(|h| h.visible = true);
697 }
698 Action::HideHitManager => {
699 self
700 .hit_manager_state
701 .access_some_mut(|h| h.visible = false);
702 }
703 }
704 }
705 }
706 }
707
708 pub fn exit(&self) -> color_eyre::Result<()> {
709 self.term.as_ref().inspect(|t| t.exit());
711 match self.exit_handling {
713 ExitHandling::Kill => self.signal_root_process(Signal::SIGKILL)?,
714 ExitHandling::Terminate => self.signal_root_process(Signal::SIGTERM)?,
715 ExitHandling::Wait => (),
716 }
717 Ok(())
718 }
719
720 pub fn signal_root_process(&self, sig: Signal) -> color_eyre::Result<()> {
721 if let Some(root_pid) = self.root_pid {
722 nix::sys::signal::kill(root_pid, sig)?;
723 }
724 Ok(())
725 }
726
727 pub fn active_event_list(&mut self) -> &mut EventList {
728 self
729 .popup
730 .last_mut()
731 .and_then(|p| {
732 if let ActivePopup::Backtrace(b) = p {
733 Some(&mut b.list)
734 } else {
735 None
736 }
737 })
738 .unwrap_or(&mut self.event_list)
739 }
740
741 pub fn inspect_all_event_list_mut(&mut self, mut f: impl FnMut(&mut EventList)) {
742 f(&mut self.event_list);
743 for popup in self.popup.iter_mut() {
744 if let ActivePopup::Backtrace(b) = popup {
745 f(&mut b.list);
746 }
747 }
748 }
749}
750
751trait OptionalAccessMut<T> {
752 fn access_some_mut(&mut self, f: impl FnOnce(&mut T));
753}
754
755impl<T> OptionalAccessMut<T> for Option<T> {
756 fn access_some_mut(&mut self, f: impl FnOnce(&mut T)) {
757 if let Some(v) = self.as_mut() {
758 f(v)
759 }
760 }
761}