1use scx_utils::compat;
7use scxtop::bpf_skel::types::bpf_event;
8use scxtop::cli::{generate_completions, Cli, Commands, TraceArgs, TuiArgs};
9use scxtop::config::Config;
10use scxtop::edm::{ActionHandler, BpfEventActionPublisher, BpfEventHandler, EventDispatchManager};
11use scxtop::layered_util;
12use scxtop::mangoapp::poll_mangoapp;
13use scxtop::search;
14use scxtop::tracer::Tracer;
15use scxtop::util::{
16 check_bpf_capability, get_capability_warning_message, get_clock_value, is_root,
17 read_file_string,
18};
19use scxtop::Action;
20use scxtop::App;
21use scxtop::CpuStatTracker;
22use scxtop::Event;
23use scxtop::Key;
24use scxtop::KeyMap;
25use scxtop::MemStatSnapshot;
26use scxtop::PerfettoTraceManager;
27use scxtop::SystemStatAction;
28use scxtop::Tui;
29use scxtop::SCHED_NAME_PATH;
30use scxtop::{available_kprobe_events, UpdateColVisibilityAction};
31use scxtop::{bpf_skel::*, AppState};
32
33use anyhow::anyhow;
34use anyhow::Result;
35use clap::{CommandFactory, Parser};
36use futures::future::join_all;
37use libbpf_rs::skel::OpenSkel;
38use libbpf_rs::skel::SkelBuilder;
39use libbpf_rs::Link;
40use libbpf_rs::ProgramInput;
41use libbpf_rs::RingBufferBuilder;
42use libbpf_rs::UprobeOpts;
43use log::debug;
44use log::info;
45use ratatui::crossterm::event::{KeyCode::Char, KeyEvent};
46use simplelog::{
47 ColorChoice, Config as SimplelogConfig, LevelFilter, TermLogger, TerminalMode, WriteLogger,
48};
49use std::ffi::CString;
50use std::fs::File;
51use std::mem::MaybeUninit;
52use std::str::FromStr;
53use std::sync::atomic::AtomicBool;
54use std::sync::atomic::Ordering;
55use std::sync::Arc;
56use std::time::Duration;
57use sysinfo::System;
58use tokio::sync::mpsc;
59
60fn get_action(app: &App, keymap: &KeyMap, event: Event) -> Action {
61 match event {
62 Event::Error => Action::None,
63 Event::Tick => Action::Tick,
64 Event::TickRateChange(tick_rate_ms) => {
65 Action::TickRateChange(std::time::Duration::from_millis(tick_rate_ms))
66 }
67 Event::Key(key) => handle_key_event(app, keymap, key),
68 Event::Paste(paste) => handle_input_entry(app, paste).unwrap_or(Action::None),
69 _ => Action::None,
70 }
71}
72
73fn handle_key_event(app: &App, keymap: &KeyMap, key: KeyEvent) -> Action {
74 match key.code {
75 Char(c) => {
76 if let Some(action) = handle_input_entry(app, c.to_string()) {
78 action
79 } else {
80 keymap.action(&Key::Char(c))
81 }
82 }
83 _ => keymap.action(&Key::Code(key.code)),
84 }
85}
86
87fn handle_input_entry(app: &App, s: String) -> Option<Action> {
88 match app.state() {
89 AppState::PerfEvent | AppState::KprobeEvent => Some(Action::InputEntry(s)),
90 AppState::Default
91 | AppState::Llc
92 | AppState::Node
93 | AppState::Process
94 | AppState::Memory
95 | AppState::PerfTop
96 if app.filtering() =>
97 {
98 Some(Action::InputEntry(s))
99 }
100 _ => None,
101 }
102}
103
104fn attach_progs(skel: &mut BpfSkel) -> Result<(Vec<Link>, Vec<String>)> {
106 let mut links = Vec::new();
107 let mut warnings = Vec::new();
108
109 let has_bpf_cap = check_bpf_capability();
111
112 if !has_bpf_cap {
113 warnings
114 .push("BPF programs cannot be attached - scheduler monitoring disabled".to_string());
115 warnings.push("Try running as root or configure BPF permissions".to_string());
116 return Ok((links, warnings));
117 }
118
119 macro_rules! safe_attach {
121 ($prog:expr, $name:literal) => {
122 match $prog.attach() {
123 Ok(link) => {
124 links.push(link);
125 }
126 Err(e) => {
127 if is_root() {
128 return Err(anyhow!(
130 "Failed to attach {} (running as root): {}",
131 $name,
132 e
133 ));
134 } else {
135 warnings.push(format!("Failed to attach {}: {}", $name, e));
136 }
137 }
138 }
139 };
140 }
141
142 safe_attach!(skel.progs.on_sched_cpu_perf, "sched_cpu_perf");
144 safe_attach!(skel.progs.scx_sched_reg, "scx_sched_reg");
145 safe_attach!(skel.progs.scx_sched_unreg, "scx_sched_unreg");
146 safe_attach!(skel.progs.on_sched_switch, "sched_switch");
147 safe_attach!(skel.progs.on_sched_wakeup, "sched_wakeup");
148 safe_attach!(skel.progs.on_sched_wakeup_new, "sched_wakeup_new");
149 safe_attach!(skel.progs.on_sched_waking, "sched_waking");
150 safe_attach!(skel.progs.on_sched_migrate_task, "sched_migrate_task");
151 safe_attach!(skel.progs.on_sched_fork, "sched_fork");
152 safe_attach!(skel.progs.on_sched_exec, "sched_exec");
153 safe_attach!(skel.progs.on_sched_exit, "sched_exit");
154
155 if compat::ksym_exists("scx_bpf_dsq_insert_vtime")? {
157 safe_attach!(skel.progs.scx_insert_vtime, "scx_insert_vtime");
158 safe_attach!(skel.progs.scx_insert, "scx_insert");
159 safe_attach!(skel.progs.scx_dsq_move, "scx_dsq_move");
160 safe_attach!(skel.progs.scx_dsq_move_set_vtime, "scx_dsq_move_set_vtime");
161 safe_attach!(skel.progs.scx_dsq_move_set_slice, "scx_dsq_move_set_slice");
162 } else {
163 safe_attach!(skel.progs.scx_dispatch, "scx_dispatch");
164 safe_attach!(skel.progs.scx_dispatch_vtime, "scx_dispatch_vtime");
165 safe_attach!(
166 skel.progs.scx_dispatch_from_dsq_set_vtime,
167 "scx_dispatch_from_dsq_set_vtime"
168 );
169 safe_attach!(
170 skel.progs.scx_dispatch_from_dsq_set_slice,
171 "scx_dispatch_from_dsq_set_slice"
172 );
173 safe_attach!(skel.progs.scx_dispatch_from_dsq, "scx_dispatch_from_dsq");
174 }
175
176 safe_attach!(skel.progs.on_cpuhp_enter, "cpuhp_enter");
178 safe_attach!(skel.progs.on_cpuhp_exit, "cpuhp_exit");
179
180 if links.is_empty() && !is_root() {
182 warnings.extend(get_capability_warning_message());
183 }
184
185 Ok((links, warnings))
186}
187
188fn run_trace(trace_args: &TraceArgs) -> Result<()> {
189 if !is_root() {
191 return Err(anyhow!(
192 "Trace functionality requires root privileges. Please run as root"
193 ));
194 }
195
196 TermLogger::init(
197 match trace_args.verbose {
198 0 => simplelog::LevelFilter::Info,
199 1 => simplelog::LevelFilter::Debug,
200 _ => simplelog::LevelFilter::Trace,
201 },
202 SimplelogConfig::default(),
203 TerminalMode::Mixed,
204 ColorChoice::Auto,
205 )?;
206
207 let mut kprobe_events = available_kprobe_events()?;
208 kprobe_events.sort();
209 search::sorted_contains_all(&kprobe_events, &trace_args.kprobes)
210 .then_some(())
211 .ok_or_else(|| anyhow!("Invalid kprobe events"))?;
212
213 let config = Config::default_config();
214 let worker_threads = config.worker_threads() as usize;
215 tokio::runtime::Builder::new_multi_thread()
216 .enable_all()
217 .worker_threads(if worker_threads > 2 {
218 worker_threads
219 } else {
220 4
221 })
222 .build()
223 .unwrap()
224 .block_on(async {
225 let (action_tx, mut action_rx) = mpsc::unbounded_channel();
226
227 let mut open_object = MaybeUninit::uninit();
229 let mut builder = BpfSkelBuilder::default();
230 if trace_args.verbose > 2 {
231 builder.obj_builder.debug(true);
232 }
233
234 let skel = builder.open(&mut open_object)?;
235 compat::cond_kprobe_enable("gpu_memory_total", &skel.progs.on_gpu_memory_total)?;
236 compat::cond_kprobe_enable("hw_pressure_update", &skel.progs.on_hw_pressure_update)?;
237 compat::cond_tracepoint_enable("sched:sched_process_wait", &skel.progs.on_sched_wait)?;
238 compat::cond_tracepoint_enable("sched:sched_process_hang", &skel.progs.on_sched_hang)?;
239
240 let mut skel = skel.load()?;
242 skel.maps.data_data.as_mut().unwrap().enable_bpf_events = false;
243
244 let mut links = vec![
246 skel.progs.on_sched_cpu_perf.attach()?,
247 skel.progs.scx_sched_reg.attach()?,
248 skel.progs.scx_sched_unreg.attach()?,
249 skel.progs.on_sched_switch.attach()?,
250 skel.progs.on_sched_wakeup.attach()?,
251 skel.progs.on_sched_wakeup_new.attach()?,
252 skel.progs.on_sched_waking.attach()?,
253 skel.progs.on_sched_migrate_task.attach()?,
254 skel.progs.on_sched_fork.attach()?,
255 skel.progs.on_sched_exec.attach()?,
256 skel.progs.on_sched_exit.attach()?,
257 ];
258
259 if compat::ksym_exists("scx_bpf_dsq_insert_vtime")? {
261 if let Ok(link) = skel.progs.scx_insert_vtime.attach() {
262 links.push(link);
263 }
264 if let Ok(link) = skel.progs.scx_insert.attach() {
265 links.push(link);
266 }
267 if let Ok(link) = skel.progs.scx_dsq_move.attach() {
268 links.push(link);
269 }
270 if let Ok(link) = skel.progs.scx_dsq_move_set_vtime.attach() {
271 links.push(link);
272 }
273 if let Ok(link) = skel.progs.scx_dsq_move_set_slice.attach() {
274 links.push(link);
275 }
276 } else {
277 if let Ok(link) = skel.progs.scx_dispatch.attach() {
278 links.push(link);
279 }
280 if let Ok(link) = skel.progs.scx_dispatch_vtime.attach() {
281 links.push(link);
282 }
283 if let Ok(link) = skel.progs.scx_dispatch_from_dsq_set_vtime.attach() {
284 links.push(link);
285 }
286 if let Ok(link) = skel.progs.scx_dispatch_from_dsq_set_slice.attach() {
287 links.push(link);
288 }
289 if let Ok(link) = skel.progs.scx_dispatch_from_dsq.attach() {
290 links.push(link);
291 }
292 }
293 if let Ok(link) = skel.progs.on_cpuhp_enter.attach() {
294 links.push(link);
295 }
296 if let Ok(link) = skel.progs.on_cpuhp_exit.attach() {
297 links.push(link);
298 }
299
300 let bpf_publisher = BpfEventActionPublisher::new(action_tx.clone());
301
302 let mut event_rbb = RingBufferBuilder::new();
304 let mut edm = EventDispatchManager::new(None, None);
305 edm.register_bpf_handler(Box::new(bpf_publisher));
306 let event_handler = move |data: &[u8]| {
307 let mut event = bpf_event::default();
308 plain::copy_from_bytes(&mut event, data).expect("Event data buffer was too short");
309 let _ = edm.on_event(&event);
310 0
311 };
312 event_rbb.add(&skel.maps.events, event_handler)?;
313 let event_rb = event_rbb.build()?;
314
315 let shutdown = Arc::new(AtomicBool::new(false));
317 let stop_poll = shutdown.clone();
318 let stop_stats = shutdown.clone();
319
320 let mut handles = Vec::new();
321 handles.push(tokio::spawn(async move {
322 loop {
323 let _ = event_rb.poll(Duration::from_millis(1));
324 if stop_poll.load(Ordering::Relaxed) {
325 let _ = event_rb.consume();
327 debug!("polling stopped");
328 break;
329 }
330 }
331 }));
332
333 if trace_args.system_stats {
334 let mut cpu_stat_tracker = CpuStatTracker::default();
335 let mut mem_stats = MemStatSnapshot::default();
336 let mut system = System::new_all();
337 let action_tx_clone = action_tx.clone();
338
339 handles.push(tokio::spawn(async move {
340 loop {
341 if stop_stats.load(Ordering::Relaxed) {
342 break;
343 }
344 let ts = get_clock_value(libc::CLOCK_BOOTTIME);
345
346 cpu_stat_tracker
347 .update(&mut system)
348 .expect("Failed to update cpu stats");
349
350 mem_stats.update().expect("Failed to update mem stats");
351
352 let sys_stat_action = Action::SystemStat(SystemStatAction {
353 ts,
354 cpu_data_prev: cpu_stat_tracker.prev.clone(),
355 cpu_data_current: cpu_stat_tracker.current.clone(),
356 mem_info: mem_stats.clone(),
357 });
358 action_tx_clone
359 .send(sys_stat_action)
360 .expect("Failed to send CpuStat action");
361
362 tokio::time::sleep(Duration::from_millis(100)).await;
363 }
364 }));
365 }
366
367 let trace_file_prefix = config.trace_file_prefix().to_string();
368 let trace_file = trace_args.output_file.clone();
369 let mut trace_manager = PerfettoTraceManager::new(trace_file_prefix, None);
370
371 info!("starting trace for {}ms", trace_args.trace_ms);
372 trace_manager.start()?;
373 let mut tracer = Tracer::new(skel);
374 tracer.trace(&trace_args.kprobes)?;
375
376 handles.push(tokio::spawn(async move {
377 let mut count = 0;
378 loop {
379 let action = action_rx.recv().await;
380 if let Some(a) = action {
381 count += 1;
382 trace_manager
383 .on_action(&a)
384 .expect("Action should have been resolved");
385 } else {
386 trace_manager.stop(trace_file, None).unwrap();
387 info!("trace file compiled, collected {count} events");
388 break;
389 }
390 }
391 }));
392 tokio::time::sleep(Duration::from_millis(trace_args.trace_ms)).await;
393
394 shutdown.store(true, Ordering::Relaxed);
399 tracer.clear_links()?;
400 drop(links);
401 drop(action_tx);
402 info!("generating trace");
403 let results = join_all(handles).await;
404 for result in results {
405 if let Err(e) = result {
406 eprintln!("Task panicked: {e}");
407 }
408 }
409
410 let stats = tracer.stats()?;
411 info!("{stats:?}");
412
413 Ok(())
414 })
415}
416
417fn run_tui(tui_args: &TuiArgs) -> Result<()> {
418 if let Ok(log_path) = std::env::var("RUST_LOG_PATH") {
419 let log_level = match std::env::var("RUST_LOG") {
420 Ok(v) => LevelFilter::from_str(&v)?,
421 Err(_) => LevelFilter::Info,
422 };
423
424 WriteLogger::init(
425 log_level,
426 simplelog::Config::default(),
427 File::create(log_path)?,
428 )?;
429
430 log_panics::Config::new()
431 .backtrace_mode(log_panics::BacktraceMode::Resolved)
432 .install_panic_hook();
433 };
434
435 let config = Config::merge([
436 Config::from(tui_args.clone()),
437 Config::load_or_default().expect("Failed to load config or load default config"),
438 ]);
439 let keymap = config.active_keymap.clone();
440
441 tokio::runtime::Builder::new_multi_thread()
442 .enable_all()
443 .worker_threads(config.worker_threads() as usize)
444 .build()
445 .unwrap()
446 .block_on(async {
447 let mut open_object = MaybeUninit::uninit();
449
450 let (action_tx, mut action_rx) = mpsc::unbounded_channel();
451
452 let has_bpf_cap = check_bpf_capability();
454 let mut capability_warnings = Vec::new();
455 let mut _bpf_enabled = false;
456 let mut links = Vec::new();
457 let mut event_rb_opt = None;
458 let mut skel_opt = None;
459
460 if has_bpf_cap {
461 let mut builder = BpfSkelBuilder::default();
463 if config.debug() {
464 builder.obj_builder.debug(true);
465 }
466 let bpf_publisher = BpfEventActionPublisher::new(action_tx.clone());
467 let mut edm = EventDispatchManager::new(None, None);
468 edm.register_bpf_handler(Box::new(bpf_publisher));
469
470 match builder.open(&mut open_object) {
472 Ok(mut skel) => {
473 skel.maps.rodata_data.as_mut().unwrap().long_tail_tracing_min_latency_ns =
474 tui_args.experimental_long_tail_tracing_min_latency_ns;
475
476 let _map_handle = if tui_args.layered {
477 skel.maps.rodata_data.as_mut().unwrap().layered = true;
478 action_tx.send(Action::UpdateColVisibility(UpdateColVisibilityAction {
479 table: "Process".to_string(),
480 col: "Layer ID".to_string(),
481 visible: true,
482 }))?;
483 action_tx.send(Action::UpdateColVisibility(UpdateColVisibilityAction {
484 table: "Thread".to_string(),
485 col: "Layer ID".to_string(),
486 visible: true,
487 }))?;
488 match layered_util::attach_to_existing_map("task_ctxs", &mut skel.maps.task_ctxs) {
489 Ok(handle) => Some(handle),
490 Err(e) => {
491 capability_warnings.push(format!("Failed to attach to layered map: {e}"));
492 None
493 }
494 }
495 } else {
496 None
497 };
498
499 if let Err(e) = compat::cond_kprobe_enable("gpu_memory_total", &skel.progs.on_gpu_memory_total) {
500 capability_warnings.push(format!("Failed to enable gpu_memory_total kprobe: {e}"));
501 }
502 if let Err(e) = compat::cond_kprobe_enable("hw_pressure_update", &skel.progs.on_hw_pressure_update) {
503 capability_warnings.push(format!("Failed to enable hw_pressure_update kprobe: {e}"));
504 }
505 if let Err(e) = compat::cond_tracepoint_enable("sched:sched_process_wait", &skel.progs.on_sched_wait) {
506 capability_warnings.push(format!("Failed to enable sched_process_wait tracepoint: {e}"));
507 }
508 if let Err(e) = compat::cond_tracepoint_enable("sched:sched_process_hang", &skel.progs.on_sched_hang) {
509 capability_warnings.push(format!("Failed to enable sched_process_hang tracepoint: {e}"));
510 }
511
512 match skel.load() {
514 Ok(mut loaded_skel) => {
515 let (skel_links, attach_warnings) = attach_progs(&mut loaded_skel)?;
516 links = skel_links;
517 capability_warnings.extend(attach_warnings);
518
519 if !links.is_empty() || is_root() {
520 if let Err(e) = loaded_skel.progs.scxtop_init.test_run(ProgramInput::default()) {
522 capability_warnings.push(format!("Failed to initialize scxtop BPF program: {e}"));
523 }
524 }
525
526 if !links.is_empty() {
528 let mut event_rbb = RingBufferBuilder::new();
529 let event_handler = move |data: &[u8]| {
530 let mut event = bpf_event::default();
531 plain::copy_from_bytes(&mut event, data).expect("Event data buffer was too short");
532 let _ = edm.on_event(&event);
533 0
534 };
535 if let Err(e) = event_rbb.add(&loaded_skel.maps.events, event_handler) {
536 capability_warnings.push(format!("Failed to add event handler: {e}"));
537 } else {
538 match event_rbb.build() {
539 Ok(event_rb) => {
540 event_rb_opt = Some(event_rb);
541 _bpf_enabled = true;
542 }
543 Err(e) => {
544 capability_warnings.push(format!("Failed to build event ring buffer: {e}"));
545 }
546 }
547 }
548 }
549
550 skel_opt = Some(loaded_skel);
551 }
552 Err(e) => {
553 if is_root() {
554 return Err(anyhow!("Failed to load BPF skeleton (running as root): {e}"));
555 } else {
556 capability_warnings.push(format!("Failed to load BPF skeleton: {e}"));
557 capability_warnings.extend(get_capability_warning_message());
558 }
559 }
560 }
561 }
562 Err(e) => {
563 if is_root() {
564 return Err(anyhow!("Failed to open BPF skeleton (running as root): {e}"));
565 } else {
566 capability_warnings.push(format!("Failed to open BPF skeleton: {e}"));
567 capability_warnings.extend(get_capability_warning_message());
568 }
569 }
570 }
571 } else {
572 capability_warnings.extend(get_capability_warning_message());
574 }
575
576 if tui_args.experimental_long_tail_tracing {
578 if let Some(ref mut skel) = skel_opt {
579 skel.maps.data_data.as_mut().unwrap().trace_duration_ns = config.trace_duration_ns();
580 skel.maps.data_data.as_mut().unwrap().trace_warmup_ns = config.trace_warmup_ns();
581
582 let binary = tui_args
583 .experimental_long_tail_tracing_binary
584 .clone()
585 .unwrap();
586 let symbol = tui_args
587 .experimental_long_tail_tracing_symbol
588 .clone()
589 .unwrap();
590
591 match skel.progs.long_tail_tracker_exit.attach_uprobe_with_opts(
592 -1, binary.clone(),
594 0,
595 UprobeOpts {
596 retprobe: true,
597 func_name: Some(symbol.clone()),
598 ..Default::default()
599 },
600 ) {
601 Ok(link) => links.push(link),
602 Err(e) => capability_warnings.push(format!("Failed to attach long tail tracker exit: {e}"))
603 }
604
605 match skel.progs.long_tail_tracker_entry.attach_uprobe_with_opts(
606 -1, binary.clone(),
608 0,
609 UprobeOpts {
610 retprobe: false,
611 func_name: Some(symbol.clone()),
612 ..Default::default()
613 },
614 ) {
615 Ok(link) => links.push(link),
616 Err(e) => capability_warnings.push(format!("Failed to attach long tail tracker entry: {e}"))
617 }
618 } else {
619 capability_warnings.push("Long tail tracing requested but BPF skeleton not available".to_string());
620 }
621 }
622
623 let mut tui = Tui::new(keymap.clone(), config.tick_rate_ms(), config.frame_rate_ms())?;
624 let scheduler = read_file_string(SCHED_NAME_PATH).unwrap_or("".to_string());
625
626 let mut app = if let Some(skel) = skel_opt {
628 App::new(
629 config,
630 scheduler,
631 100,
632 tui_args.process_id,
633 tui_args.layered,
634 action_tx.clone(),
635 skel,
636 )?
637 } else {
638 App::new_without_bpf(
640 config,
641 scheduler,
642 100,
643 tui_args.process_id,
644 tui_args.layered,
645 action_tx.clone(),
646 )?
647 };
648
649 if !capability_warnings.is_empty() {
651 app.set_capability_warnings(capability_warnings);
652 }
653
654 tui.enter()?;
655
656 let shutdown = app.should_quit.clone();
658 if let Some(event_rb) = event_rb_opt {
659 tokio::spawn(async move {
660 loop {
661 let _ = event_rb.poll(Duration::from_millis(1));
662 if shutdown.load(Ordering::Relaxed) {
663 break;
664 }
665 }
666 });
667 }
668
669 if tui_args.mangoapp_tracing {
670 let stop_mangoapp = app.should_quit.clone();
671 let mangoapp_path = CString::new(tui_args.mangoapp_path.clone()).unwrap();
672 let poll_intvl_ms = tui_args.mangoapp_poll_intvl_ms;
673 let tx = action_tx.clone();
674 tokio::spawn(async move {
675 poll_mangoapp(
676 mangoapp_path,
677 poll_intvl_ms,
678 tx,
679 stop_mangoapp,
680 )
681 .await
682 });
683 }
684
685 loop {
686 tokio::select! {
687 ev = tui.next() => {
688 let ev = ev?;
689 match ev {
690 Event::Quit => { action_tx.send(Action::Quit)?; },
691 Event::Tick => action_tx.send(Action::Tick)?,
692 Event::TickRateChange(tick_rate_ms) => action_tx.send(
693 Action::TickRateChange(std::time::Duration::from_millis(tick_rate_ms)),
694 )?,
695 Event::Render => {
696 if app.should_quit.load(Ordering::Relaxed) {
697 break;
698 }
699 if app.state() != AppState::Pause {
700 tui.draw(|f| app.render(f).expect("Failed to render application"))?;
701 }
702 }
703 Event::Key(_) => {
704 let action = get_action(&app, &keymap, ev);
705 action_tx.send(action)?;
706 }
707 _ => {}
708 }}
709
710 ac = action_rx.recv() => {
711 let ac = ac.ok_or(anyhow!("actions channel closed"))?;
712 app.handle_action(&ac)?;
713 }
714 }
715 }
716 tui.exit()?;
717 drop(links);
718
719 Ok(())
720 })
721}
722
723fn main() -> Result<()> {
724 let args = Cli::parse();
725
726 match &args.command.unwrap_or(Commands::Tui(args.tui)) {
727 Commands::Tui(tui_args) => {
728 run_tui(tui_args)?;
729 }
730 Commands::Trace(trace_args) => {
731 run_trace(trace_args)?;
732 }
733 Commands::GenerateCompletions { shell, output } => {
734 generate_completions(Cli::command(), *shell, output.clone())
735 .unwrap_or_else(|_| panic!("Failed to generate completions for {shell}"));
736 }
737 }
738 Ok(())
739}