// Copyright 2016 Joe Wilm, The Alacritty Project Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //! Logging for alacritty. //! //! The main executable is supposed to call `initialize()` exactly once during //! startup. All logging messages are written to stdout, given that their //! log-level is sufficient for the level configured in `cli::Options`. use crate::cli; use log::{self, Level}; use time; use std::env; use std::fs::{self, File, OpenOptions}; use std::io::{self, LineWriter, Stdout, Write}; use std::path::PathBuf; use std::process; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; pub fn initialize(options: &cli::Options) -> Result { // Use env_logger if RUST_LOG environment variable is defined. Otherwise, // use the alacritty-only logger. if ::std::env::var("RUST_LOG").is_ok() { ::env_logger::try_init()?; Ok(LoggerProxy::default()) } else { let logger = Logger::new(options.log_level); let proxy = logger.proxy(); log::set_boxed_logger(Box::new(logger))?; Ok(proxy) } } /// Proxy object for bidirectional communicating with the global logger. #[derive(Clone, Default)] pub struct LoggerProxy { errors: Arc, warnings: Arc, logfile_proxy: OnDemandLogFileProxy, } impl LoggerProxy { /// Check for new logged errors. pub fn errors(&self) -> bool { self.errors.load(Ordering::Relaxed) } /// Check for new logged warnings. pub fn warnings(&self) -> bool { self.warnings.load(Ordering::Relaxed) } /// Get the path of the log file if it has been created. pub fn log_path(&self) -> Option<&str> { if self.logfile_proxy.created.load(Ordering::Relaxed) { Some(&self.logfile_proxy.path) } else { None } } /// Clear log warnings/errors from the Alacritty UI. pub fn clear(&mut self) { self.errors.store(false, Ordering::Relaxed); self.warnings.store(false, Ordering::Relaxed); } pub fn delete_log(&mut self) { self.logfile_proxy.delete_log(); } } struct Logger { level: log::LevelFilter, logfile: Mutex, stdout: Mutex>, errors: Arc, warnings: Arc, } impl Logger { // False positive, see: https://github.com/rust-lang-nursery/rust-clippy/issues/734 #[allow(clippy::new_ret_no_self)] fn new(level: log::LevelFilter) -> Self { log::set_max_level(level); let logfile = Mutex::new(OnDemandLogFile::new()); let stdout = Mutex::new(LineWriter::new(io::stdout())); Logger { level, logfile, stdout, errors: Arc::new(AtomicBool::new(false)), warnings: Arc::new(AtomicBool::new(false)), } } fn proxy(&self) -> LoggerProxy { LoggerProxy { errors: self.errors.clone(), warnings: self.warnings.clone(), logfile_proxy: self.logfile.lock().expect("").proxy(), } } } impl log::Log for Logger { fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { metadata.level() <= self.level } fn log(&self, record: &log::Record<'_>) { if self.enabled(record.metadata()) && record.target().starts_with("alacritty") { let now = time::strftime("%F %R", &time::now()).unwrap(); let msg = if record.level() >= Level::Trace { format!("[{}] [{}] [{}:{}] {}\n", now, record.level(), record.file().unwrap_or("?"), record.line() .map(|l| l.to_string()) .unwrap_or_else(|| "?".into()), record.args()) } else { format!("[{}] [{}] {}\n", now, record.level(), record.args()) }; if let Ok(ref mut logfile) = self.logfile.lock() { let _ = logfile.write_all(msg.as_ref()); } if let Ok(ref mut stdout) = self.stdout.lock() { let _ = stdout.write_all(msg.as_ref()); } match record.level() { Level::Error => self.errors.store(true, Ordering::Relaxed), Level::Warn => self.warnings.store(true, Ordering::Relaxed), _ => (), } } } fn flush(&self) {} } #[derive(Clone, Default)] struct OnDemandLogFileProxy { created: Arc, path: String, } impl OnDemandLogFileProxy { fn delete_log(&mut self) { if self.created.load(Ordering::Relaxed) && fs::remove_file(&self.path).is_ok() { let _ = writeln!(io::stdout(), "Deleted log file at {:?}", self.path); self.created.store(false, Ordering::Relaxed); } } } struct OnDemandLogFile { file: Option>, created: Arc, path: PathBuf, } impl OnDemandLogFile { fn new() -> Self { let mut path = env::temp_dir(); path.push(format!("Alacritty-{}.log", process::id())); OnDemandLogFile { path, file: None, created: Arc::new(AtomicBool::new(false)), } } fn file(&mut self) -> Result<&mut LineWriter, io::Error> { // Allow to recreate the file if it has been deleted at runtime if self.file.is_some() && !self.path.as_path().exists() { self.file = None; } // Create the file if it doesn't exist yet if self.file.is_none() { let file = OpenOptions::new() .append(true) .create(true) .open(&self.path); match file { Ok(file) => { self.file = Some(io::LineWriter::new(file)); self.created.store(true, Ordering::Relaxed); let _ = writeln!(io::stdout(), "Created log file at {:?}", self.path); } Err(e) => { let _ = writeln!(io::stdout(), "Unable to create log file: {}", e); return Err(e); } } } Ok(self.file.as_mut().unwrap()) } fn proxy(&self) -> OnDemandLogFileProxy { OnDemandLogFileProxy { created: self.created.clone(), path: self.path.to_string_lossy().to_string(), } } } impl Write for OnDemandLogFile { fn write(&mut self, buf: &[u8]) -> Result { self.file()?.write(buf) } fn flush(&mut self) -> Result<(), io::Error> { self.file()?.flush() } }