systemctl_tui/
utils.rs

1use std::{io::Write, path::PathBuf, sync::atomic::AtomicBool};
2
3use anyhow::{anyhow, Context, Result};
4use better_panic::Settings;
5use directories::ProjectDirs;
6use lazy_static::lazy_static;
7use tracing::{error, level_filters::LevelFilter};
8use tracing_appender::{
9  non_blocking::WorkerGuard,
10  rolling::{RollingFileAppender, Rotation},
11};
12use tracing_subscriber::{
13  self, filter::EnvFilter, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer,
14};
15
16lazy_static! {
17  static ref TRACE_FILE_NAME: PathBuf = {
18    let directory = get_data_dir().expect("Unable to get data directory");
19    let timestamp_iso8601 = chrono::Local::now().format("%Y-%m-%d-%H-%M-%S");
20    directory.join(format!("systemctl-tui-trace-{}.log", timestamp_iso8601))
21  };
22}
23
24static TRACING_ENABLED: AtomicBool = AtomicBool::new(false);
25
26pub fn initialize_panic_handler() {
27  std::panic::set_hook(Box::new(|panic_info| {
28    if let Err(r) = crate::terminal::exit() {
29      error!("Unable to exit Terminal: {r:?}");
30    }
31
32    Settings::auto().most_recent_first(false).lineno_suffix(true).create_panic_handler()(panic_info);
33    std::process::exit(libc::EXIT_FAILURE);
34  }));
35}
36
37pub fn get_data_dir() -> Result<PathBuf> {
38  let directory = if let Ok(s) = std::env::var("SYSTEMCTL_TUI_DATA") {
39    PathBuf::from(s)
40  } else if let Some(proj_dirs) = ProjectDirs::from("com", "rgwood", "systemctl-tui") {
41    proj_dirs.data_local_dir().to_path_buf()
42  } else {
43    return Err(anyhow!("Unable to find data directory for systemctl-tui"));
44  };
45  Ok(directory)
46}
47
48pub fn get_config_dir() -> Result<PathBuf> {
49  let directory = if let Ok(s) = std::env::var("SYSTEMCTL_TUI_CONFIG") {
50    PathBuf::from(s)
51  } else if let Some(proj_dirs) = ProjectDirs::from("com", "rgwood", "systemctl-tui") {
52    proj_dirs.config_local_dir().to_path_buf()
53  } else {
54    return Err(anyhow!("Unable to find config directory for systemctl-tui"));
55  };
56  Ok(directory)
57}
58
59pub fn initialize_logging(enable_tracing: bool) -> Result<WorkerGuard> {
60  let directory = get_data_dir()?;
61  std::fs::create_dir_all(directory.clone()).context(format!("{directory:?} could not be created"))?;
62  // let log_path = directory.join("systemctl-tui.log");
63
64  // create a file appender that rolls daily
65  let file_appender = RollingFileAppender::new(Rotation::DAILY, &directory, "systemctl-tui.log");
66
67  // create a non-blocking writer
68  let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
69
70  // create a layer for the file logger
71  let file_layer = tracing_subscriber::fmt::layer()
72    .with_writer(non_blocking)
73    .with_file(true)
74    .with_line_number(true)
75    .with_target(false)
76    .with_ansi(false)
77    .with_filter(EnvFilter::builder().with_default_directive(LevelFilter::INFO.into()).from_env_lossy());
78
79  let tui_layer = tui_logger::tracing_subscriber_layer()
80    .with_filter(EnvFilter::builder().with_default_directive(LevelFilter::INFO.into()).from_env_lossy());
81
82  tracing_subscriber::registry().with(file_layer).with(tui_layer).init();
83
84  if enable_tracing {
85    TRACING_ENABLED.store(true, std::sync::atomic::Ordering::Relaxed);
86    let mut trace_file = std::fs::File::create(&*TRACE_FILE_NAME).unwrap();
87    trace_file.write_all(b"[\n").unwrap(); // start of chrome://tracing file
88  }
89
90  let directory = directory.to_string_lossy().into_owned();
91  tracing::info!(directory, "Logging initialized");
92
93  Ok(guard)
94}
95
96// Write an event in chrome://tracing format
97// This is currently very basic+hacky, I'm mostly doing it to experiment with Perfetto
98// Reference: https://thume.ca/2023/12/02/tracing-methods/
99pub fn log_perf_event(event: &str, duration: std::time::Duration) {
100  if !TRACING_ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
101    return;
102  }
103  let log_path = &*TRACE_FILE_NAME;
104  let system_time = std::time::SystemTime::now();
105
106  let event = format!(
107    r#"{{
108  "name": "{}",
109  "cat": "PERF",
110  "ph": "X",
111  "ts": {},
112  "dur": {}
113}}"#,
114    event,
115    system_time.duration_since(std::time::UNIX_EPOCH).unwrap().as_micros(),
116    duration.as_micros()
117  );
118
119  let mut file = std::fs::OpenOptions::new().append(true).create(true).open(log_path).unwrap();
120  file.write_all(event.as_bytes()).unwrap();
121  file.write_all(b",\n").unwrap();
122}
123
124/// Similar to the `std::dbg!` macro, but generates `tracing` events rather
125/// than printing to stdout.
126///
127/// By default, the verbosity level for the generated events is `DEBUG`, but
128/// this can be customized.
129#[macro_export]
130macro_rules! trace_dbg {
131    (target: $target:expr, level: $level:expr, $ex:expr) => {{
132        match $ex {
133            value => {
134                tracing::event!(target: $target, $level, ?value, stringify!($ex));
135                value
136            }
137        }
138    }};
139    (level: $level:expr, $ex:expr) => {
140        trace_dbg!(target: module_path!(), level: $level, $ex)
141    };
142    (target: $target:expr, $ex:expr) => {
143        trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex)
144    };
145    ($ex:expr) => {
146        trace_dbg!(level: tracing::Level::DEBUG, $ex)
147    };
148}
149
150pub fn version() -> String {
151  let author = clap::crate_authors!();
152
153  let version = env!("CARGO_PKG_VERSION");
154
155  let config_dir_path = get_config_dir().unwrap().display().to_string();
156  let data_dir_path = get_data_dir().unwrap().display().to_string();
157
158  format!(
159    "\
160{version}
161
162Authors: {author}
163
164Config directory: {config_dir_path}
165Data directory: {data_dir_path}"
166  )
167}