fm/io/
log.rs

1use std::fs::{metadata, remove_file, File, OpenOptions};
2use std::io::{BufWriter, Write};
3use std::sync::RwLock;
4
5use anyhow::Result;
6use chrono::Local;
7use log::{Level, LevelFilter, Metadata, Record, SetLoggerError};
8use parking_lot::Mutex;
9
10use crate::common::{extract_lines, tilde, ACTION_LOG_PATH, NORMAL_LOG_PATH};
11
12/// Holds the last action which is displayed to the user
13static LAST_LOG_LINE: RwLock<String> = RwLock::new(String::new());
14
15/// Holds the last line of the log
16static LAST_LOG_INFO: RwLock<String> = RwLock::new(String::new());
17
18/// Used to trigger a reset of log files.
19/// Once their size is bigger than `MAX_LOG_SIZE`, the log file is cleared.
20const MAX_LOG_SIZE: u64 = 50_000;
21
22/// Setup of 2 loggers
23/// - a normal one used directly with the macros like `log::info!(...)`, used for debugging
24/// - an action one used with `log::info!(target: "action", ...)` to be displayed in the application
25pub struct FMLogger {
26    normal_log: Mutex<BufWriter<std::fs::File>>,
27    action_log: Mutex<BufWriter<std::fs::File>>,
28}
29
30impl Default for FMLogger {
31    fn default() -> Self {
32        let normal_file = open_or_rotate(tilde(NORMAL_LOG_PATH).as_ref(), MAX_LOG_SIZE);
33        let action_file = open_or_rotate(tilde(ACTION_LOG_PATH).as_ref(), MAX_LOG_SIZE);
34        let normal_log = Mutex::new(BufWriter::new(normal_file));
35        let action_log = Mutex::new(BufWriter::new(action_file));
36        Self {
37            normal_log,
38            action_log,
39        }
40    }
41}
42
43impl FMLogger {
44    pub fn init(self) -> Result<(), SetLoggerError> {
45        log::set_boxed_logger(Box::new(self))?;
46        log::set_max_level(LevelFilter::Info);
47        log::info!("fm is starting with logs enabled");
48        Ok(())
49    }
50
51    fn write(&self, writer: &Mutex<BufWriter<File>>, record: &Record) {
52        let mut writer = writer.lock();
53        let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
54        let _ = writeln!(writer, "{timestamp} - {msg}", msg = record.args());
55        let _ = writer.flush();
56    }
57}
58
59impl log::Log for FMLogger {
60    fn enabled(&self, metadata: &Metadata) -> bool {
61        metadata.level() <= Level::Info
62    }
63
64    fn log(&self, record: &Record) {
65        if !self.enabled(record.metadata()) {
66            return;
67        }
68        if record.target() == "action" {
69            self.write(&self.action_log, record)
70        } else {
71            self.write(&self.normal_log, record)
72        }
73    }
74
75    fn flush(&self) {
76        let _ = self.normal_log.lock().flush();
77        let _ = self.action_log.lock().flush();
78    }
79}
80
81fn open_or_rotate(path: &str, max_size: u64) -> File {
82    if let Ok(meta) = metadata(path) {
83        if meta.len() > max_size {
84            let _ = remove_file(path);
85        }
86    }
87
88    OpenOptions::new()
89        .create(true)
90        .append(true)
91        .open(path)
92        .expect("cannot open log file")
93}
94
95/// Returns the last line of the log file.
96pub fn read_log() -> Result<Vec<String>> {
97    let log_path = tilde(ACTION_LOG_PATH).to_string();
98    let content = std::fs::read_to_string(log_path)?;
99    Ok(extract_lines(content))
100}
101
102/// Read the last value of the "log line".
103/// Fail silently if the global variable can't be read and returns an empty string.
104pub fn read_last_log_line() -> String {
105    let Ok(last_log_line) = LAST_LOG_LINE.read() else {
106        return "".to_owned();
107    };
108    last_log_line.to_owned()
109}
110
111/// Write a new log line to the global variable `LAST_LOG_LINE`.
112/// Fail silently if the global variable can't be written.
113fn write_last_log_line<S>(log: S)
114where
115    S: Into<String> + std::fmt::Display,
116{
117    let Ok(mut last_log_line) = LAST_LOG_LINE.write() else {
118        log::info!("Couldn't write to LAST_LOG_LINE");
119        return;
120    };
121    *last_log_line = log.to_string();
122}
123
124/// Write a line to both the global variable `LAST_LOG_LINE` and the action log
125/// which can be displayed with Alt+l
126pub fn write_log_line<S>(log_line: S)
127where
128    S: Into<String> + std::fmt::Display,
129{
130    log::info!(target: "action", "{log_line}");
131    write_last_log_line(log_line);
132}
133
134/// Writes the message to the global variable `LAST_LOG_LINE` and a the action log.
135/// It can be displayed with the default bind ALt+l and at the last line of the display.
136/// Every action which change the filetree, execute an external command or which returns an
137/// error should be logged this way.
138#[macro_export]
139macro_rules! log_line {
140    ($($arg:tt)+) => (
141    $crate::io::write_log_line(
142      format!($($arg)+)
143    )
144  );
145}
146
147/// Read the last value of the "log info".
148/// Fail silently if the global variable can't be read and returns an empty string.
149fn read_last_log_info() -> String {
150    let Ok(last_log_info) = LAST_LOG_INFO.read() else {
151        return "".to_owned();
152    };
153    last_log_info.to_owned()
154}
155
156/// Write a new log info to the global variable `LAST_LOG_INFO`.
157/// Fail silently if the global variable can't be written.
158fn write_last_log_info<S>(log: &S)
159where
160    S: Into<String> + std::fmt::Display,
161{
162    let Ok(mut last_log_info) = LAST_LOG_INFO.write() else {
163        log::info!("Couldn't write to LAST_LOG_LINE");
164        return;
165    };
166    *last_log_info = log.to_string();
167}
168
169/// Write a line to both the global variable `LAST_LOG_INFO` and the info log
170/// Won't write the same line multiple times during the same execution.
171pub fn write_log_info_once<S>(log_line: S)
172where
173    S: Into<String> + std::fmt::Display,
174{
175    if read_last_log_info() != log_line.to_string() {
176        write_last_log_info(&log_line);
177        log::info!("{log_line}");
178    }
179}
180
181/// Log a formated message to the default log.
182/// Won't write anything if the same message is sent multiple times.
183/// It uses `log::info!` internally.
184/// It accepts the same formatted messages as `format`.
185#[macro_export]
186macro_rules! log_info {
187    ($($arg:tt)+) => {{
188        fn __log_info_dummy() {}
189        let function = {
190            let full = std::any::type_name_of_val(&__log_info_dummy);
191            full.trim_end_matches("::__log_info_dummy")
192        };
193
194        $crate::io::write_log_info_once(format!(
195            "{file}:{line}:{column} [{function}] - {content}",
196            file=file!(),
197            line=line!(),
198            column=column!(),
199            content=format_args!($($arg)+)
200        ))
201    }};
202}