//! 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 std::fs::{File, OpenOptions}; use std::io::{self, LineWriter, Stdout, Write}; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::{env, process}; use glutin::event_loop::EventLoopProxy; use log::{self, Level, LevelFilter}; use time::macros::format_description; use time::{OffsetDateTime, UtcOffset}; use crate::cli::Options; use crate::event::{Event, EventType}; use crate::message_bar::{Message, MessageType}; /// Name for the environment variable containing the log file's path. const ALACRITTY_LOG_ENV: &str = "ALACRITTY_LOG"; /// List of targets which will be logged by Alacritty. const ALLOWED_TARGETS: [&str; 4] = ["alacritty_terminal", "alacritty_config_derive", "alacritty", "crossfont"]; pub fn initialize( options: &Options, event_proxy: EventLoopProxy, ) -> Result, log::SetLoggerError> { log::set_max_level(options.log_level()); let logger = Logger::new(event_proxy); let path = logger.file_path(); log::set_boxed_logger(Box::new(logger))?; Ok(path) } pub struct Logger { logfile: Mutex, stdout: Mutex>, event_proxy: Mutex>, tz_offset: UtcOffset, } impl Logger { fn new(event_proxy: EventLoopProxy) -> Self { let logfile = Mutex::new(OnDemandLogFile::new()); let stdout = Mutex::new(LineWriter::new(io::stdout())); Logger { logfile, stdout, event_proxy: Mutex::new(event_proxy), tz_offset: UtcOffset::current_local_offset().expect("local timezone offset"), } } fn file_path(&self) -> Option { if let Ok(logfile) = self.logfile.lock() { Some(logfile.path().clone()) } else { None } } /// Log a record to the message bar. fn message_bar_log(&self, record: &log::Record<'_>, logfile_path: &str) { let message_type = match record.level() { Level::Error => MessageType::Error, Level::Warn => MessageType::Warning, _ => return, }; let event_proxy = match self.event_proxy.lock() { Ok(event_proxy) => event_proxy, Err(_) => return, }; #[cfg(not(windows))] let env_var = format!("${}", ALACRITTY_LOG_ENV); #[cfg(windows)] let env_var = format!("%{}%", ALACRITTY_LOG_ENV); let message = format!( "[{}] See log at {} ({}):\n{}", record.level(), logfile_path, env_var, record.args(), ); let mut message = Message::new(message, message_type); message.set_target(record.target().to_owned()); let _ = event_proxy.send_event(Event::new(EventType::Message(message), None)); } } impl log::Log for Logger { fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { metadata.level() <= log::max_level() } fn log(&self, record: &log::Record<'_>) { // Get target crate. let index = record.target().find(':').unwrap_or_else(|| record.target().len()); let target = &record.target()[..index]; // Only log our own crates, except when logging at Level::Trace. if !self.enabled(record.metadata()) || !is_allowed_target(record.level(), target) { return; } // Create log message for the given `record` and `target`. let message = create_log_message(record, target, self.tz_offset); if let Ok(mut logfile) = self.logfile.lock() { // Write to logfile. let _ = logfile.write_all(message.as_ref()); // Log relevant entries to message bar. self.message_bar_log(record, &logfile.path.to_string_lossy()); } // Write to stdout. if let Ok(mut stdout) = self.stdout.lock() { let _ = stdout.write_all(message.as_ref()); } } fn flush(&self) {} } fn create_log_message(record: &log::Record<'_>, target: &str, tz_offset: UtcOffset) -> String { let time_format = format_description!( "[year]-[month]-[day] [hour repr:24]:[minute]:[second].[subsecond digits:9]" ); let now = OffsetDateTime::now_utc().to_offset(tz_offset).format(time_format).unwrap(); let mut message = format!("[{}] [{:<5}] [{}] ", now, record.level(), target); // Alignment for the lines after the first new line character in the payload. We don't deal // with fullwidth/unicode chars here, so just `message.len()` is sufficient. let alignment = message.len(); // Push lines with added extra padding on the next line, which is trimmed later. let lines = record.args().to_string(); for line in lines.split('\n') { let line = format!("{}\n{:width$}", line, "", width = alignment); message.push_str(&line); } // Drop extra trailing alignment. message.truncate(message.len() - alignment); message } /// Check if log messages from a crate should be logged. fn is_allowed_target(level: Level, target: &str) -> bool { match (level, log::max_level()) { (Level::Error, LevelFilter::Trace) | (Level::Warn, LevelFilter::Trace) => true, _ => ALLOWED_TARGETS.contains(&target), } } 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())); // Set log path as an environment variable. env::set_var(ALACRITTY_LOG_ENV, path.as_os_str()); 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.display()); }, Err(e) => { let _ = writeln!(io::stdout(), "Unable to create log file: {}", e); return Err(e); }, } } Ok(self.file.as_mut().unwrap()) } fn path(&self) -> &PathBuf { &self.path } } 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() } }