use log::{Level, LevelFilter, Metadata, Record, SetLoggerError}; use std::{ ffi::OsStr, path::{Path, PathBuf}, sync::LazyLock, }; use test_rpc::logging::{Error, LogFile, LogOutput, Output}; use tokio::{ fs::File, io::{self, AsyncBufReadExt, BufReader}, sync::{ broadcast::{channel, Receiver, Sender}, Mutex, }, }; const MAX_OUTPUT_BUFFER: usize = 10_000; /// Only consider files that end with ".log" const INCLUDE_LOG_FILE_EXT: &str = "log"; /// Ignore log files that contain ".old" const EXCLUDE_LOG_FILE_CONTAIN: &str = ".old"; /// Maximum number of lines that each log file may contain const TRUNCATE_LOG_FILE_LINES: usize = 100; pub static LOGGER: LazyLock = LazyLock::new(|| { let (sender, listener) = channel(MAX_OUTPUT_BUFFER); StdOutBuffer(Mutex::new(listener), sender) }); pub struct StdOutBuffer(pub Mutex>, pub Sender); impl log::Log for StdOutBuffer { fn enabled(&self, metadata: &Metadata<'_>) -> bool { metadata.level() <= Level::Info } fn log(&self, record: &Record<'_>) { if self.enabled(record.metadata()) { match record.metadata().level() { Level::Error => { self.1 .send(Output::Error(format!("{}", record.args()))) .unwrap(); } Level::Warn => { self.1 .send(Output::Warning(format!("{}", record.args()))) .unwrap(); } Level::Info => { if !record.metadata().target().contains("tarpc") { self.1 .send(Output::Info(format!("{}", record.args()))) .unwrap(); } } _ => (), } println!("{}", record.args()); } } fn flush(&self) {} } pub fn init_logger() -> Result<(), SetLoggerError> { log::set_logger(&*LOGGER).map(|()| log::set_max_level(LevelFilter::Info)) } pub async fn get_mullvad_app_logs() -> LogOutput { LogOutput { settings_json: read_settings_file().await, log_files: read_log_files().await, } } async fn read_settings_file() -> Result { let mut settings_path = mullvad_paths::get_default_settings_dir() .map_err(|error| Error::Logs(format!("{}", error)))?; settings_path.push("settings.json"); read_truncated(&settings_path, None) .await .map_err(|error| Error::Logs(format!("{}: {}", settings_path.display(), error))) } async fn read_log_files() -> Result>, Error> { let log_dir = mullvad_paths::get_default_log_dir().map_err(|error| Error::Logs(format!("{}", error)))?; let paths = list_logs(log_dir) .await .map_err(|error| Error::Logs(format!("{}", error)))?; let mut log_files = Vec::new(); for path in paths { let log_file = read_truncated(&path, Some(TRUNCATE_LOG_FILE_LINES)) .await .map_err(|error| Error::Logs(format!("{}: {}", path.display(), error))) .map(|content| LogFile { content, name: path, }); log_files.push(log_file); } Ok(log_files) } async fn list_logs>(log_dir: T) -> Result, Error> { let mut dir_entries = tokio::fs::read_dir(&log_dir) .await .map_err(|e| Error::Logs(format!("{}: {}", log_dir.as_ref().display(), e)))?; let mut paths = Vec::new(); while let Ok(Some(entry)) = dir_entries.next_entry().await { let path = entry.path(); if let Some(u8_path) = path.to_str() { if u8_path.contains(EXCLUDE_LOG_FILE_CONTAIN) { continue; } } if path.extension() == Some(OsStr::new(INCLUDE_LOG_FILE_EXT)) { paths.push(path); } } Ok(paths) } /// Read the contents of a file to string, optionally truncating the result by given amount of lines. async fn read_truncated>( path: T, truncate_lines: Option, ) -> io::Result { let mut output = vec![]; let reader = BufReader::new(File::open(path).await?); let mut lines = reader.lines(); while let Some(line) = lines.next_line().await? { output.push(line); } if let Some(max_number_of_lines) = truncate_lines { output.truncate(max_number_of_lines); } Ok(output.join("\n")) }