bp3d_logger/
internal.rs

1// Copyright (c) 2021, BlockProject 3D
2//
3// All rights reserved.
4//
5// Redistribution and use in source and binary forms, with or without modification,
6// are permitted provided that the following conditions are met:
7//
8//     * Redistributions of source code must retain the above copyright notice,
9//       this list of conditions and the following disclaimer.
10//     * Redistributions in binary form must reproduce the above copyright notice,
11//       this list of conditions and the following disclaimer in the documentation
12//       and/or other materials provided with the distribution.
13//     * Neither the name of BlockProject 3D nor the names of its contributors
14//       may be used to endorse or promote products derived from this software
15//       without specific prior written permission.
16//
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
21// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29use crate::backend::Backend;
30use crate::{LogMsg, Logger};
31use crossbeam_channel::{bounded, Receiver, Sender};
32use log::{Level, Log, Metadata, Record};
33use std::sync::atomic::{AtomicBool, Ordering};
34use std::sync::Mutex;
35use time::macros::format_description;
36use time::OffsetDateTime;
37use time_tz::OffsetDateTimeExt;
38
39const BUF_SIZE: usize = 128; // The maximum count of log messages in the channel.
40
41enum Command {
42    Flush,
43    Log(LogMsg),
44    Terminate,
45}
46
47fn log<T: Backend>(
48    backend: Option<&mut T>,
49    target: &str,
50    msg: &str,
51    level: Level,
52) -> Result<(), T::Error> {
53    if let Some(back) = backend {
54        back.write(target, msg, level)
55    } else {
56        Ok(())
57    }
58}
59
60fn exec_commad(cmd: Command, logger: &mut Logger) -> bool {
61    match cmd {
62        Command::Terminate => true,
63        Command::Flush => {
64            if let Some(file) = &mut logger.file {
65                if let Err(e) = file.flush() {
66                    let _ = log(
67                        logger.std.as_mut(),
68                        "bp3d-logger",
69                        &format!("Could not flush file backend: {}", e),
70                        Level::Error,
71                    );
72                }
73            }
74            false
75        }
76        Command::Log(LogMsg { target, msg, level }) => {
77            if let Err(e) = log(logger.file.as_mut(), &target, &msg, level) {
78                let _ = log(
79                    logger.std.as_mut(),
80                    "bp3d-logger",
81                    &format!("Could not write to file backend: {}", e),
82                    Level::Error,
83                );
84            }
85            let _ = log(logger.std.as_mut(), &target, &msg, level);
86            false
87        }
88    }
89}
90
91pub struct LoggerImpl {
92    thread: Mutex<Option<std::thread::JoinHandle<()>>>,
93    send_ch: Sender<Command>,
94    recv_ch: Receiver<Command>,
95    log_buffer_send_ch: Sender<LogMsg>,
96    log_buffer_recv_ch: Receiver<LogMsg>,
97    enable_log_buffer: AtomicBool,
98    enabled: AtomicBool,
99}
100
101impl LoggerImpl {
102    pub fn new() -> LoggerImpl {
103        let (send_ch, recv_ch) = bounded(BUF_SIZE);
104        let (log_buffer_send_ch, log_buffer_recv_ch) = bounded(BUF_SIZE);
105        LoggerImpl {
106            thread: Mutex::new(None),
107            send_ch,
108            recv_ch,
109            log_buffer_send_ch,
110            log_buffer_recv_ch,
111            enable_log_buffer: AtomicBool::new(false),
112            enabled: AtomicBool::new(false),
113        }
114    }
115
116    pub fn enable(&self, flag: bool) {
117        self.enabled.store(flag, Ordering::Release);
118    }
119
120    pub fn enable_log_buffer(&self, flag: bool) {
121        self.enable_log_buffer.store(flag, Ordering::Release);
122    }
123
124    pub fn clear_log_buffer(&self) {
125        while self.log_buffer_recv_ch.try_recv().is_ok() {} //Clear the entire log buffer.
126    }
127
128    pub fn get_log_buffer(&self) -> Receiver<LogMsg> {
129        self.log_buffer_recv_ch.clone()
130    }
131
132    pub fn terminate(&self) {
133        // This should never panic as there's no way another call would have panicked!
134        let mut thread = self.thread.lock().unwrap();
135        if let Some(handle) = thread.take() {
136            // This cannot panic as send_ch is owned by LoggerImpl which is intended
137            // to be statically allocated.
138            unsafe {
139                self.send_ch.send(Command::Flush).unwrap_unchecked();
140                self.send_ch.send(Command::Terminate).unwrap_unchecked();
141            }
142            // Join the logging thread; this will lock until the thread is completely terminated.
143            handle.join().unwrap();
144        }
145    }
146
147    pub fn start_new_thread(&self, logger: Logger) {
148        let mut flag = false;
149        {
150            // This should never panic as there's no way another call would have panicked!
151            let mut thread = self.thread.lock().unwrap();
152            if let Some(handle) = thread.take() {
153                // This cannot panic as send_ch is owned by LoggerImpl which is intended
154                // to be statically allocated.
155                unsafe {
156                    self.send_ch.send(Command::Terminate).unwrap_unchecked();
157                }
158                if handle.join().is_err() {
159                    flag = true;
160                }
161            }
162            let recv_ch = self.recv_ch.clone();
163            *thread = Some(std::thread::spawn(move || {
164                let mut logger = logger;
165                while let Ok(v) = recv_ch.recv() {
166                    let flag = exec_commad(v, &mut logger);
167                    if flag {
168                        // The thread has requested to exit itself; drop out of the main loop.
169                        break;
170                    }
171                }
172            }));
173        }
174        if flag {
175            // Somehow the previous thread has panicked; log that panic...
176            unsafe {
177                // This cannot panic as send_ch is owned by LoggerImpl which is intended
178                // to be statically allocated.
179                self.send_ch
180                    .send(Command::Log(LogMsg {
181                        level: Level::Error,
182                        msg: "The logging thread has panicked!".into(),
183                        target: "bp3d-logger".into(),
184                    }))
185                    .unwrap_unchecked();
186            }
187        }
188    }
189
190    pub fn low_level_log(&self, msg: LogMsg) {
191        if self.enable_log_buffer.load(Ordering::Acquire) {
192            unsafe {
193                // This cannot panic as both send_ch and log_buffer_send_ch are owned by LoggerImpl
194                // which is intended to be statically allocated.
195                self.send_ch
196                    .send(Command::Log(msg.clone()))
197                    .unwrap_unchecked();
198                self.log_buffer_send_ch.send(msg).unwrap_unchecked();
199            }
200        } else {
201            unsafe {
202                // This cannot panic as send_ch is owned by LoggerImpl which is intended
203                // to be statically allocated.
204                self.send_ch.send(Command::Log(msg)).unwrap_unchecked();
205            }
206        }
207    }
208
209    pub fn is_enabled(&self) -> bool {
210        self.enabled.load(Ordering::Acquire)
211    }
212}
213
214fn extract_target_module<'a>(record: &'a Record) -> (&'a str, Option<&'a str>) {
215    let base_string = record.module_path().unwrap_or_else(|| record.target());
216    let target = base_string
217        .find("::")
218        .map(|v| &base_string[..v])
219        .unwrap_or(base_string);
220    let module = base_string.find("::").map(|v| &base_string[(v + 2)..]);
221    (target, module)
222}
223
224impl Log for LoggerImpl {
225    fn enabled(&self, _: &Metadata) -> bool {
226        self.is_enabled()
227    }
228
229    fn log(&self, record: &Record) {
230        // Apparently the log crate is defective: the enabled function is ignored...
231        if !self.enabled(record.metadata()) {
232            return;
233        }
234        let (target, module) = extract_target_module(record);
235        //In the future attempt to not update all the time https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=17c218f318826f55ab64535bfcd28ec6
236        let system_tz =
237            time_tz::system::get_timezone().unwrap_or(time_tz::timezones::db::us::CENTRAL);
238        let format = format_description!("[weekday repr:short] [month repr:short] [day] [hour repr:12]:[minute]:[second] [period case:upper]");
239        //<error> is very unlikely to occur (only possibility is a weird io error).
240        let formatted = OffsetDateTime::now_utc()
241            .to_timezone(system_tz)
242            .format(format)
243            .unwrap_or_else(|_| "<error>".into());
244        let msg = LogMsg {
245            msg: format!(
246                "({}) {}: {}",
247                formatted,
248                module.unwrap_or("main"),
249                record.args()
250            ),
251            target: target.into(),
252            level: record.level(),
253        };
254        self.low_level_log(msg);
255    }
256
257    fn flush(&self) {
258        if !self.is_enabled() {
259            return;
260        }
261        unsafe {
262            // This cannot panic as send_ch is owned by LoggerImpl which is intended
263            // to be statically allocated.
264            self.send_ch.send(Command::Flush).unwrap_unchecked();
265            while !self.send_ch.is_empty() {}
266        }
267    }
268}