/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! This provides a way to direct rust logging into the gecko logger. #[macro_use] extern crate lazy_static; use app_services_logger::{AppServicesLogger, LOGGERS_BY_TARGET}; use log::Log; use log::{Level, LevelFilter}; use std::boxed::Box; use std::collections::HashMap; use std::ffi::{CStr, CString}; use std::os::raw::c_char; use std::os::raw::c_int; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::RwLock; use std::{cmp, env}; extern "C" { fn ExternMozLog(tag: *const c_char, prio: c_int, text: *const c_char); fn gfx_critical_note(msg: *const c_char); #[cfg(target_os = "android")] fn __android_log_write(prio: c_int, tag: *const c_char, text: *const c_char) -> c_int; } lazy_static! { // This could be a proper static once [1] is fixed or parking_lot's const fn // support is not nightly-only. // // [1]: https://github.com/rust-lang/rust/issues/73714 static ref LOG_MODULE_MAP: RwLock> = RwLock::new(HashMap::new()); } /// This tells us whether LOG_MODULE_MAP is possibly non-empty. static LOGGING_ACTIVE: AtomicBool = AtomicBool::new(false); /// This function searches for the module's name in the hashmap. If that is not /// found, it proceeds to search for the parent modules. /// It returns a tuple containing the matched string, if the matched module /// was a pattern match, and the level we found in the hashmap. /// If none is found, it will return the module's name and LevelFilter::Off fn get_level_for_module<'a>( map: &HashMap, key: &'a str, ) -> (&'a str, bool, LevelFilter) { if let Some((level, is_pattern_match)) = map.get(key) { return (key, *is_pattern_match, level.clone()); } let mut mod_name = &key[..]; while let Some(pos) = mod_name.rfind("::") { mod_name = &mod_name[..pos]; if let Some((level, is_pattern_match)) = map.get(mod_name) { return (mod_name, *is_pattern_match, level.clone()); } } return (key, false, LevelFilter::Off); } /// This function takes a record to maybe log to Gecko. /// It returns true if the record was handled by Gecko's logging, and false /// otherwise. pub fn log_to_gecko(record: &log::Record) -> bool { if !LOGGING_ACTIVE.load(Ordering::Relaxed) { return false; } let key = match record.module_path() { Some(key) => key, None => return false, }; let (mod_name, is_pattern_match, level) = { let map = LOG_MODULE_MAP.read().unwrap(); get_level_for_module(&map, &key) }; if level == LevelFilter::Off { return false; } if level < record.metadata().level() { return false; } // Map the log::Level to mozilla::LogLevel. let moz_log_level = match record.metadata().level() { Level::Error => 1, // Error Level::Warn => 2, // Warning Level::Info => 3, // Info Level::Debug => 4, // Debug Level::Trace => 5, // Verbose }; // If it was a pattern match, we need to append ::* to the matched string. let (tag, msg) = if is_pattern_match { ( CString::new(format!("{}::*", mod_name)).unwrap(), CString::new(format!("[{}] {}", key, record.args())).unwrap(), ) } else { ( CString::new(key).unwrap(), CString::new(format!("{}", record.args())).unwrap(), ) }; unsafe { ExternMozLog(tag.as_ptr(), moz_log_level, msg.as_ptr()); } return true; } #[no_mangle] pub unsafe extern "C" fn set_rust_log_level(module: *const c_char, level: u8) { // Convert the Gecko level to a rust LevelFilter. let rust_level = match level { 1 => LevelFilter::Error, 2 => LevelFilter::Warn, 3 => LevelFilter::Info, 4 => LevelFilter::Debug, 5 => LevelFilter::Trace, _ => LevelFilter::Off, }; // This is the name of the rust module that we're trying to log in Gecko. let mut mod_name = CStr::from_ptr(module) .to_string_lossy() .into_owned(); let is_pattern_match = mod_name.ends_with("::*"); // If this is a pattern, remove the last "::*" from it so we can search it // in the map. if is_pattern_match { let len = mod_name.len() - 3; mod_name.truncate(len); } LOGGING_ACTIVE.store(true, Ordering::Relaxed); let mut map = LOG_MODULE_MAP.write().unwrap(); map.insert(mod_name, (rust_level, is_pattern_match)); // Figure out the max level of all the modules. let max = map .values() .map(|(lvl, _)| lvl) .max() .unwrap_or(&LevelFilter::Off); log::set_max_level(*max); } pub struct GeckoLogger { logger: env_logger::Logger, } impl GeckoLogger { pub fn new() -> GeckoLogger { let mut builder = env_logger::Builder::new(); let default_level = if cfg!(debug_assertions) { "warn" } else { "error" }; let logger = match env::var("RUST_LOG") { Ok(v) => builder.parse_filters(&v).build(), _ => builder.parse_filters(default_level).build(), }; GeckoLogger { logger } } pub fn init() -> Result<(), log::SetLoggerError> { let gecko_logger = Self::new(); // The max level may have already been set by gecko_logger. Don't // set it to a lower level. let level = cmp::max(log::max_level(), gecko_logger.logger.filter()); log::set_max_level(level); log::set_boxed_logger(Box::new(gecko_logger)) } fn should_log_to_app_services(target: &str) -> bool { return AppServicesLogger::is_app_services_logger_registered(target.into()); } fn maybe_log_to_app_services(&self, record: &log::Record) { if Self::should_log_to_app_services(record.target()) { if let Some(l) = LOGGERS_BY_TARGET.read().unwrap().get(record.target()) { l.log(record); } } } fn should_log_to_gfx_critical_note(record: &log::Record) -> bool { record.level() == log::Level::Error && record.target().contains("webrender") } fn maybe_log_to_gfx_critical_note(&self, record: &log::Record) { if Self::should_log_to_gfx_critical_note(record) { let msg = CString::new(format!("{}", record.args())).unwrap(); unsafe { gfx_critical_note(msg.as_ptr()); } } } #[cfg(not(target_os = "android"))] fn log_out(&self, record: &log::Record) { // If the log wasn't handled by the gecko platform logger, just pass it // to the env_logger. if !log_to_gecko(record) { self.logger.log(record); } } #[cfg(target_os = "android")] fn log_out(&self, record: &log::Record) { if !self.logger.matches(record) { return; } let msg = CString::new(format!("{}", record.args())).unwrap(); let tag = CString::new(record.module_path().unwrap()).unwrap(); let prio = match record.metadata().level() { Level::Error => 6, /* ERROR */ Level::Warn => 5, /* WARN */ Level::Info => 4, /* INFO */ Level::Debug => 3, /* DEBUG */ Level::Trace => 2, /* VERBOSE */ }; // Output log directly to android log, since env_logger can output log // only to stderr or stdout. unsafe { __android_log_write(prio, tag.as_ptr(), msg.as_ptr()); } } } impl log::Log for GeckoLogger { fn enabled(&self, metadata: &log::Metadata) -> bool { self.logger.enabled(metadata) || GeckoLogger::should_log_to_app_services(metadata.target()) } fn log(&self, record: &log::Record) { // Forward log to gfxCriticalNote, if the log should be in gfx crash log. self.maybe_log_to_gfx_critical_note(record); self.maybe_log_to_app_services(record); self.log_out(record); } fn flush(&self) {} }