diff --git a/Cargo.lock b/Cargo.lock index c7548a3e..7cea91d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4009,6 +4009,7 @@ dependencies = [ "arti-client", "async-std", "backtrace", + "built", "bytes 1.11.1", "chrono", "curve25519-dalek 4.1.3", @@ -4048,6 +4049,7 @@ dependencies = [ "lazy_static", "local-ip-address", "log", + "log4rs", "nokhwa", "num-bigint 0.4.6", "parking_lot 0.12.5", diff --git a/Cargo.toml b/Cargo.toml index 4e139930..03f1c78d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ egui-async = "0.3.4" rust-i18n = "3.1.5" ## other +log4rs = "1.4.0" anyhow = "1.0.97" pin-project = "1.1.10" backtrace = "0.3.76" @@ -136,4 +137,7 @@ android_logger = "0.15.0" jni = "0.21.1" android-activity = { version = "0.6.0", features = ["game-activity"] } winit = { version = "0.30.12", features = ["android-game-activity"] } -eframe = { version = "0.33.2", default-features = false, features = ["glow", "android-game-activity"] } \ No newline at end of file +eframe = { version = "0.33.2", default-features = false, features = ["glow", "android-game-activity"] } + +[build-dependencies] +built = "0.8.0" diff --git a/build.rs b/build.rs index a26cde23..84cf9cf2 100644 --- a/build.rs +++ b/build.rs @@ -2,6 +2,8 @@ use std::process::Command; use std::{env, fs}; fn main() { + built::write_built_file().expect("Failed to acquire build-time information"); + let out_dir = env::var("OUT_DIR").unwrap(); let tor_out_dir = format!("{}/tor", out_dir); let mut webtunnel_file = format!("{}/webtunnel", tor_out_dir); diff --git a/src/gui/views/content.rs b/src/gui/views/content.rs index f67e1946..fb9d478a 100644 --- a/src/gui/views/content.rs +++ b/src/gui/views/content.rs @@ -117,7 +117,7 @@ impl ContentContainer for Content { if self.first_draw { // Show crash report or integrated node Android warning. - if Settings::crash_report_path().exists() { + if Settings::crash_check_path().exists() { Modal::new(CRASH_REPORT_MODAL) .closeable(false) .position(ModalPosition::Center) @@ -276,14 +276,14 @@ impl Content { .size(16.0) .color(Colors::text(false))); ui.add_space(6.0); - // Draw button to share crash report. + // Draw button to share log file. let text = format!("{} {}", FILE_X, t!("share")); View::colored_text_button(ui, text, Colors::blue(), Colors::white_or_black(false), || { - if let Ok(data) = fs::read_to_string(Settings::crash_report_path()) { - let name = Settings::CRASH_REPORT_FILE_NAME.to_string(); + if let Ok(data) = fs::read_to_string(Settings::log_path()) { + let name = Settings::LOG_FILE_NAME.to_string(); let _ = cb.share_data(name, data.as_bytes().to_vec()); } - Settings::delete_crash_report(); + Settings::delete_crash_check(); Modal::close(); }); }); @@ -292,7 +292,7 @@ impl Content { ui.add_space(8.0); ui.vertical_centered_justified(|ui| { View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - Settings::delete_crash_report(); + Settings::delete_crash_check(); Modal::close(); }); }); diff --git a/src/lib.rs b/src/lib.rs index aaed0fff..a755d888 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,8 +19,8 @@ rust_i18n::i18n!("locales"); use eframe::NativeOptions; use egui::{Context, Stroke, Theme}; use lazy_static::lazy_static; -use std::sync::Arc; use parking_lot::RwLock; +use std::sync::Arc; #[cfg(target_os = "android")] use winit::platform::android::activity::AndroidApp; @@ -28,9 +28,9 @@ use winit::platform::android::activity::AndroidApp; pub use settings::AppConfig; pub use settings::Settings; -use crate::gui::{Colors, App}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::View; +use crate::gui::{App, Colors}; use crate::node::Node; mod node; @@ -39,6 +39,7 @@ mod tor; mod settings; mod http; pub mod gui; +pub mod logger; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -47,13 +48,8 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); #[cfg(target_os = "android")] #[no_mangle] fn android_main(app: AndroidApp) { - { - std::env::set_var("RUST_BACKTRACE", "full"); - let log_config = android_logger::Config::default() - .with_max_level(log::LevelFilter::Info) - .with_tag("grim"); - android_logger::init_once(log_config); - } + // Setup logger. + logger::init_logger(); use gui::platform::Android; let platform = Android::new(app.clone()); diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 00000000..3c8b7938 --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,144 @@ +// Copyright 2026 The Grim Developers +// +// 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. + +use std::{panic, thread}; +use std::fs::File; +use backtrace::Backtrace; +use log4rs::append::Append; +use log4rs::append::console::ConsoleAppender; +use log4rs::append::rolling_file::policy::compound::CompoundPolicy; +use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller; +use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger; +use log4rs::append::rolling_file::RollingFileAppender; +use log4rs::Config; +use log4rs::config::{Appender, Root}; +use log4rs::encode::pattern::PatternEncoder; +use log4rs::filter::threshold::ThresholdFilter; +use log::{error, LevelFilter}; + +use crate::Settings; + +const LOGGING_PATTERN: &str = "{d(%Y%m%d %H:%M:%S%.3f)} {h({l})} {M} - {m}{n}"; + +/// 32 log files to rotate over by default. +const ROTATE_LOG_FILES: u32 = 32; +/// Size of the log in bytes to rotate over (6 megabytes). +const MAX_FILE_SIZE: u64 = 1024 * 1024 * 6; + +/// Include build information. +pub mod built_info { + include!(concat!(env!("OUT_DIR"), "/built.rs")); +} + +/// Initialize the logger. +pub fn init_logger() { + let stdout = ConsoleAppender::builder() + .encoder(Box::new(PatternEncoder::new(&LOGGING_PATTERN))) + .build(); + + let mut root = Root::builder(); + + let mut app = vec![]; + app.push( + Appender::builder() + .filter(Box::new(ThresholdFilter::new(LevelFilter::Info))) + .build("stdout", Box::new(stdout)), + ); + root = root.appender("stdout"); + + // Setup file logging. + let filter = Box::new(ThresholdFilter::new(LevelFilter::Info)); + let file: Box = { + let path = Settings::log_path(); + let roller = FixedWindowRoller::builder() + .build(&format!("{}.{{}}.gz", path), ROTATE_LOG_FILES) + .unwrap(); + let trigger = SizeTrigger::new(MAX_FILE_SIZE); + let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller)); + Box::new( + RollingFileAppender::builder() + .append(true) + .encoder(Box::new(PatternEncoder::new(&LOGGING_PATTERN))) + .build(path, Box::new(policy)) + .expect("Failed to create logfile"), + ) + }; + app.push( + Appender::builder() + .filter(filter) + .build("file", file), + ); + root = root.appender("file"); + + let config = Config::builder() + .appenders(app) + .build(root.build(LevelFilter::Info)) + .unwrap(); + let _ = log4rs::init_config(config).unwrap(); + + log::info!("{}", build_info()); + + send_panic_to_log(); +} + +/// Get information about application build. +fn build_info() -> String { + format!( + "This is Grim version {}, built for {} by {}.", + built_info::PKG_VERSION, + built_info::TARGET, + built_info::RUSTC_VERSION, + ) +} + +/// Hook to send panics to logs as well as stderr. +fn send_panic_to_log() { + panic::set_hook(Box::new(|info| { + let backtrace = Backtrace::new(); + + let thread = thread::current(); + let thread = thread.name().unwrap_or("unnamed"); + + let msg = match info.payload().downcast_ref::<&'static str>() { + Some(s) => *s, + None => match info.payload().downcast_ref::() { + Some(s) => &**s, + None => "Box", + }, + }; + + match info.location() { + Some(location) => { + error!( + "{}\nThread '{}' panicked at '{}': {}:{}{:?}\n\n", + build_info(), + thread, + msg, + location.file(), + location.line(), + backtrace + ); + } + None => error!("Thread '{}' panicked at '{}'{:?}", thread, msg, backtrace), + } + // Also print to stderr. + eprintln!( + "Thread '{}' panicked with message:\n\"{}\"\nSee {} for further details.", + thread, msg, Settings::log_path() + ); + // Create file to show report send on launch. + let log = Settings::crash_check_path(); + let _ = File::create(log); + })); +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 72c3253f..5d7cec1b 100755 --- a/src/main.rs +++ b/src/main.rs @@ -23,11 +23,8 @@ pub fn main() { #[allow(dead_code)] #[cfg(not(target_os = "android"))] fn real_main() { - env_logger::builder() - .filter_level(log::LevelFilter::Info) - .parse_default_env() - .init(); - + // Initialize logger. + grim::logger::init_logger(); // Handle file path argument passing. let args: Vec<_> = std::env::args().collect(); let mut data = None; @@ -40,50 +37,15 @@ fn real_main() { data = content } - // Setup callback on panic crash. - std::panic::set_hook(Box::new(|info| { - // Format error. - let backtrace = backtrace::Backtrace::new(); - let time = grim::gui::views::View::format_time(chrono::Utc::now().timestamp()); - let os = egui::os::OperatingSystem::from_target_os(); - let ver = grim::VERSION; - let msg = panic_info_message(info); - let loc = if let Some(location) = info.location() { - format!("{}:{}:{}", location.file(), location.line(), location.column()) - } else { - "no location found.".parse().unwrap() - }; - let err = format!("{} - {:?} - v{}\n{}\n{}\n\n{:?}", time, os, ver, msg, loc, backtrace); - // Save backtrace to file. - let log = grim::Settings::crash_report_path(); - if log.exists() { - use std::io::{Seek, SeekFrom, Write}; - let mut file = std::fs::OpenOptions::new() - .write(true) - .append(true) - .open(log) - .unwrap(); - if file.seek(SeekFrom::End(0)).is_ok() { - file.write(err.as_bytes()).unwrap_or_default(); - } - } else { - std::fs::write(log, err.as_bytes()).unwrap_or_default(); - } - // Print message error. - println!("{}\n{}", msg, loc); - })); - // Start GUI. - let _ = std::panic::catch_unwind(|| { - if is_app_running(&data) { - return; - } else if let Some(data) = data { - grim::on_data(data); - } - let platform = grim::gui::platform::Desktop::new(); - start_app_socket(platform.clone()); - start_desktop_gui(platform); - }); + if is_app_running(&data) { + return; + } else if let Some(data) = data { + grim::on_data(data); + } + let platform = grim::gui::platform::Desktop::new(); + start_app_socket(platform.clone()); + start_desktop_gui(platform); } /// Get panic message from crash payload. diff --git a/src/settings/settings.rs b/src/settings/settings.rs index 49c23738..a335a0e1 100644 --- a/src/settings/settings.rs +++ b/src/settings/settings.rs @@ -48,10 +48,10 @@ pub struct Settings { impl Settings { /// Main application directory name. pub const MAIN_DIR_NAME: &'static str = ".grim"; - /// Crash report file name. - pub const CRASH_REPORT_FILE_NAME: &'static str = "crash.log"; /// Application socket name. pub const SOCKET_NAME: &'static str = "grim.sock"; + /// Log file name. + pub const LOG_FILE_NAME: &'static str = "grim.log"; /// Initialize settings with app and node configs. fn init() -> Self { @@ -148,6 +148,13 @@ impl Settings { path } + /// Get log file path. + pub fn log_path() -> String { + let mut log_path = Self::base_path(None); + log_path.push(Self::LOG_FILE_NAME); + log_path.to_str().unwrap().into() + } + /// Get desktop application socket path. pub fn socket_path() -> PathBuf { let mut socket_path = Self::base_path(None); @@ -162,16 +169,16 @@ impl Settings { path } - /// Get configuration file path from provided name and subdirectory if needed. - pub fn crash_report_path() -> PathBuf { + /// Get path of file created when application crashed. + pub fn crash_check_path() -> PathBuf { let mut path = Self::base_path(None); - path.push(Self::CRASH_REPORT_FILE_NAME); + path.push("crashed"); path } - /// Delete crash report file. - pub fn delete_crash_report() { - let log = Self::crash_report_path(); + /// Delete crash file. + pub fn delete_crash_check() { + let log = Self::crash_check_path(); if log.exists() { let _ = fs::remove_file(log.clone()); }