tracexec_tui/
app.rs

1// Copyright (c) 2023 Ratatui Developers
2// Copyright (c) 2024 Levi Zim
3
4// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5// associated documentation files (the "Software"), to deal in the Software without restriction,
6// including without limitation the rights to use, copy, modify, merge, publish, distribute,
7// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9
10// The above copyright notice and this permission notice shall be included in all copies or substantial
11// portions of the Software.
12
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
16// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
17// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18
19use 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      // Handle events
225      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              // Cancel all popups
237              self.popup.clear();
238              // Cancel non-finished query
239              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              // Cancel breakpoint manager
244              if self.breakpoint_manager.is_some() {
245                self.breakpoint_manager = None;
246              }
247              // Cancel hit manager
248              if let Some(h) = self.hit_manager_state.as_mut()
249                && h.visible
250              {
251                h.hide();
252              }
253              // action_tx.send(Action::Render)?;
254            } else {
255              trace!("TUI: Active pane: {}", self.active_pane);
256              if self.active_pane == ActivePane::Events {
257                // Handle popups
258                // TODO: do this in a separate function
259                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                // Handle hit manager
288                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                // Handle breakpoint manager
298                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                // Handle query builder
306                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                        // Regex error
315                        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                // action_tx.send(Action::Render)?;
357              }
358            }
359          }
360          Event::Tracer(msg) => {
361            match msg {
362              TracerMessage::Event(e) => {
363                if let TracerEventDetails::TraceeSpawn { pid, .. } = &e.details {
364                  // FIXME: we should not rely on TracerMessage, which might be filtered.
365                  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                    // Warn: This grants CAP_SYS_ADMIN to not only the tracer but also the tracees
382                    // sudo -E env RUST_LOG=debug setpriv --reuid=$(id -u) --regid=$(id -g) --init-groups --inh-caps=+sys_admin --ambient-caps +sys_admin -- target/debug/tracexec tui -t --
383                  }
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            // action_tx.send(Action::Render)?;
476          }
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            // action_tx.send(Action::Render)?;
483          }
484          Event::Init => {
485            // Fix the size of the terminal
486            action_tx.send(Action::Resize(tui.size()?));
487            // action_tx.send(Action::Render)?;
488          }
489          Event::Error => {}
490        }
491      }
492
493      // Refresh list if following
494      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      // Handle actions
503      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            // TODO: don't crash the app if clipboard fails
645            if let Some(clipboard) = self.clipboard.as_mut() {
646              clipboard.set_text(text)?;
647            }
648            // TODO: find a better way to do this
649            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              // action_tx.send(query_builder.edit())?;
660              query_builder.edit();
661            } else {
662              let mut query_builder = QueryBuilder::new(QueryKind::Search);
663              // action_tx.send(query_builder.edit())?;
664              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    // Close pty master
710    self.term.as_ref().inspect(|t| t.exit());
711    // Terminate root process
712    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}