feat: add concurrent working threads option to CLI args
[ouch.git] / src / utils / logger.rs
blob5156308e15969c1f01e6bdda0ccd7cc910b5f117
1 use std::sync::{mpsc, Arc, Barrier, OnceLock};
3 pub use logger_thread::spawn_logger_thread;
5 use super::colors::{ORANGE, RESET, YELLOW};
6 use crate::accessible::is_running_in_accessible_mode;
8 /// Asks logger to shutdown and waits till it flushes all pending messages.
9 #[track_caller]
10 pub fn shutdown_logger_and_wait() {
11     logger_thread::send_shutdown_command_and_wait();
14 /// Asks logger to flush all messages, useful before starting STDIN interaction.
15 #[track_caller]
16 pub fn flush_messages() {
17     logger_thread::send_flush_command_and_wait();
20 /// An `[INFO]` log to be displayed if we're not running accessibility mode.
21 ///
22 /// Same as `.info_accessible()`, but only displayed if accessibility mode
23 /// is turned off, which is detected by the function
24 /// `is_running_in_accessible_mode`.
25 ///
26 /// Read more about accessibility mode in `accessible.rs`.
27 #[track_caller]
28 pub fn info(contents: String) {
29     info_with_accessibility(contents, false);
32 /// An `[INFO]` log to be displayed.
33 ///
34 /// Same as `.info()`, but also displays if `is_running_in_accessible_mode`
35 /// returns `true`.
36 ///
37 /// Read more about accessibility mode in `accessible.rs`.
38 #[track_caller]
39 pub fn info_accessible(contents: String) {
40     info_with_accessibility(contents, true);
43 #[track_caller]
44 fn info_with_accessibility(contents: String, accessible: bool) {
45     logger_thread::send_print_command(PrintMessage {
46         contents,
47         accessible,
48         level: MessageLevel::Info,
49     });
52 #[track_caller]
53 pub fn warning(contents: String) {
54     logger_thread::send_print_command(PrintMessage {
55         contents,
56         // Warnings are important and unlikely to flood, so they should be displayed
57         accessible: true,
58         level: MessageLevel::Warning,
59     });
62 #[derive(Debug)]
63 enum LoggerCommand {
64     Print(PrintMessage),
65     Flush { finished_barrier: Arc<Barrier> },
66     FlushAndShutdown { finished_barrier: Arc<Barrier> },
69 /// Message object used for sending logs from worker threads to a logging thread via channels.
70 /// See <https://github.com/ouch-org/ouch/issues/643>
71 #[derive(Debug)]
72 struct PrintMessage {
73     contents: String,
74     accessible: bool,
75     level: MessageLevel,
78 impl PrintMessage {
79     fn to_formatted_message(&self) -> Option<String> {
80         match self.level {
81             MessageLevel::Info => {
82                 if self.accessible {
83                     if is_running_in_accessible_mode() {
84                         Some(format!("{}Info:{} {}", *YELLOW, *RESET, self.contents))
85                     } else {
86                         Some(format!("{}[INFO]{} {}", *YELLOW, *RESET, self.contents))
87                     }
88                 } else if !is_running_in_accessible_mode() {
89                     Some(format!("{}[INFO]{} {}", *YELLOW, *RESET, self.contents))
90                 } else {
91                     None
92                 }
93             }
94             MessageLevel::Warning => {
95                 if is_running_in_accessible_mode() {
96                     Some(format!("{}Warning:{} {}", *ORANGE, *RESET, self.contents))
97                 } else {
98                     Some(format!("{}[WARNING]{} {}", *ORANGE, *RESET, self.contents))
99                 }
100             }
101         }
102     }
105 #[derive(Debug, PartialEq)]
106 enum MessageLevel {
107     Info,
108     Warning,
111 mod logger_thread {
112     use std::{
113         sync::{mpsc::RecvTimeoutError, Arc, Barrier},
114         time::Duration,
115     };
117     use super::*;
119     type LogReceiver = mpsc::Receiver<LoggerCommand>;
120     type LogSender = mpsc::Sender<LoggerCommand>;
122     static SENDER: OnceLock<LogSender> = OnceLock::new();
124     #[track_caller]
125     fn setup_channel() -> LogReceiver {
126         let (tx, rx) = mpsc::channel();
127         SENDER.set(tx).expect("`setup_channel` should only be called once");
128         rx
129     }
131     #[track_caller]
132     fn get_sender() -> &'static LogSender {
133         SENDER.get().expect("No sender, you need to call `setup_channel` first")
134     }
136     #[track_caller]
137     pub(super) fn send_print_command(msg: PrintMessage) {
138         get_sender()
139             .send(LoggerCommand::Print(msg))
140             .expect("Failed to send print command");
141     }
143     #[track_caller]
144     pub(super) fn send_flush_command_and_wait() {
145         let barrier = Arc::new(Barrier::new(2));
147         get_sender()
148             .send(LoggerCommand::Flush {
149                 finished_barrier: barrier.clone(),
150             })
151             .expect("Failed to send flush command");
153         barrier.wait();
154     }
156     #[track_caller]
157     pub(super) fn send_shutdown_command_and_wait() {
158         let barrier = Arc::new(Barrier::new(2));
160         get_sender()
161             .send(LoggerCommand::FlushAndShutdown {
162                 finished_barrier: barrier.clone(),
163             })
164             .expect("Failed to send shutdown command");
166         barrier.wait();
167     }
169     pub fn spawn_logger_thread() {
170         let log_receiver = setup_channel();
171         rayon::spawn(move || run_logger(log_receiver));
172     }
174     fn run_logger(log_receiver: LogReceiver) {
175         const FLUSH_TIMEOUT: Duration = Duration::from_millis(200);
177         let mut buffer = Vec::<String>::with_capacity(16);
179         loop {
180             let msg = match log_receiver.recv_timeout(FLUSH_TIMEOUT) {
181                 Ok(msg) => msg,
182                 Err(RecvTimeoutError::Timeout) => {
183                     flush_logs_to_stderr(&mut buffer);
184                     continue;
185                 }
186                 Err(RecvTimeoutError::Disconnected) => unreachable!("sender is static"),
187             };
189             match msg {
190                 LoggerCommand::Print(msg) => {
191                     // Append message to buffer
192                     if let Some(msg) = msg.to_formatted_message() {
193                         buffer.push(msg);
194                     }
196                     if buffer.len() == buffer.capacity() {
197                         flush_logs_to_stderr(&mut buffer);
198                     }
199                 }
200                 LoggerCommand::Flush { finished_barrier } => {
201                     flush_logs_to_stderr(&mut buffer);
202                     finished_barrier.wait();
203                 }
204                 LoggerCommand::FlushAndShutdown { finished_barrier } => {
205                     flush_logs_to_stderr(&mut buffer);
206                     finished_barrier.wait();
207                     return;
208                 }
209             }
210         }
211     }
213     fn flush_logs_to_stderr(buffer: &mut Vec<String>) {
214         if !buffer.is_empty() {
215             let text = buffer.join("\n");
216             eprintln!("{text}");
217             buffer.clear();
218         }
219     }