Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src-tauri/src/commands/assets_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type Result<T> = std::result::Result<T, CommandError>;
/// * `Result<String>` - The local file path as a string, or an error if the download fails.
#[command]
pub async fn get_or_download_asset_model(url: &str) -> Result<String> {
info!("get_or_download_asset_model called with URL: {}", url);
debug!("get_or_download_asset_model called with URL: {}", url);

// Parse the URL to extract the path components
let parsed_url = Url::parse(url).map_err(|e| {
Expand Down Expand Up @@ -106,7 +106,7 @@ pub async fn get_or_download_asset_model(url: &str) -> Result<String> {
}

// File doesn't exist, download it synchronously (blocking)
info!("Asset model not found locally, downloading from: {}", url);
debug!("Asset model not found locally, downloading from: {}", url);

// Ensure parent directories exist
if let Some(parent) = local_file_path.parent() {
Expand Down
20 changes: 17 additions & 3 deletions src-tauri/src/commands/nrc_commands.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::error::{AppError, CommandError};
use crate::logging;
use crate::minecraft::api::norisk_api::{AdventCalendarDay, CrashlogDto, NoRiskApi, ReferralInfo, Reward, UserNotification};
use crate::minecraft::api::wordpress_api::{BlogPost, WordPressApi};
use crate::minecraft::auth::minecraft_auth::Credentials;
Expand All @@ -17,10 +18,23 @@ use crate::utils::updater_utils;
/// * `Result<Vec<BlogPost>, CommandError>` - A vector of blog posts or an error.
#[tauri::command]
pub async fn get_news_and_changelogs_command() -> Result<Vec<BlogPost>, CommandError> {
info!("Executing get_news_and_changelogs_command");
debug!("Executing get_news_and_changelogs_command");
Ok(WordPressApi::get_news_and_changelogs().await?)
}

#[tauri::command]
pub async fn dump_debug_logs_command(reason: Option<String>) -> Result<String, CommandError> {
let dump_reason = reason
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| "manual_hotkey".to_string());

let path = logging::dump_debug_buffer_to_file(&format!("manual: {}", dump_reason))
.map_err(|e| AppError::Other(format!("Failed to dump debug logs: {}", e)))?;

info!("Debug ring buffer dumped to: {}", path.display());
Ok(path.to_string_lossy().to_string())
}

#[tauri::command]
pub async fn discord_auth_link(app: AppHandle) -> Result<(), CommandError> {
debug!("Executing discord_auth_link command");
Expand Down Expand Up @@ -402,10 +416,10 @@ pub async fn submit_crash_log_command(payload: CrashlogDto) -> Result<(), Comman
pub async fn log_message_command(level: String, message: String) -> Result<(), CommandError> {
match level.to_lowercase().as_str() {
"debug" => debug!("[Frontend] {}", message),
"info" => info!("[Frontend] {}", message),
"info" => debug!("[Frontend] {}", message),
"warn" => log::warn!("[Frontend] {}", message),
"error" => error!("[Frontend] {}", message),
_ => info!("[Frontend] {}", message),
_ => debug!("[Frontend] {}", message),
}
Ok(())
}
Expand Down
4 changes: 2 additions & 2 deletions src-tauri/src/commands/profile_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use crate::utils::{
shaderpack_utils,
};
use chrono::Utc;
use log::{error, info, trace, warn};
use log::{error, info, trace, warn, debug};
use sanitize_filename::sanitize;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -2258,7 +2258,7 @@ pub struct AllProfilesAndLastPlayed {

#[tauri::command]
pub async fn get_all_profiles_and_last_played() -> Result<AllProfilesAndLastPlayed, CommandError> {
info!("Executing get_all_profiles_and_last_played command");
debug!("Executing get_all_profiles_and_last_played command");
let state = State::get().await?;

// Fetch User Profiles (includes editable copies of standard profiles)
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod config;
pub mod error;
pub mod friends;
pub mod integrations;
pub mod logging;
pub mod minecraft;
pub mod state;
pub mod utils;
179 changes: 175 additions & 4 deletions src-tauri/src/logging.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,176 @@
use crate::config::{ProjectDirsExt, LAUNCHER_DIRECTORY};
use chrono::Utc;
use log::LevelFilter;
use log4rs::append::console::{ConsoleAppender, Target};
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::policy::compound::CompoundPolicy;
use log4rs::append::rolling_file::RollingFileAppender;
use log4rs::config::{Appender, Config, Root};
use log4rs::config::{Appender, Config, Logger, Root};
use log4rs::encode::writer::simple::SimpleWriter;
use log4rs::encode::Encode;
use log4rs::encode::pattern::PatternEncoder;
use log4rs::filter::threshold::ThresholdFilter;
use once_cell::sync::Lazy;
use std::collections::VecDeque;
use std::fs::OpenOptions;
use std::io::Write;
use std::sync::Mutex;
use tokio::fs;

const LOG_DIR_NAME: &str = "logs";
const LOG_FILE_NAME: &str = "launcher.log";
const DEBUG_DUMP_FILE_PREFIX: &str = "launcher-debug";
const LOG_PATTERN: &str = "{d(%Y-%m-%d %H:%M:%S%.3f)} | {({l}):5.5} | {m}{n}";
const CONSOLE_LOG_PATTERN: &str = "{d(%H:%M:%S)} | {h({l}):5.5} | {m}{n}"; // Slightly simpler pattern for console
const LOG_FILE_SIZE_LIMIT_BYTES: u64 = 4_800_000; // ~4.8MB to fit Discord's 8MB upload limit
const LOG_FILE_BACKUP_COUNT: u32 = 10;
const DEBUG_RING_BUFFER_MAX_BYTES: usize = LOG_FILE_SIZE_LIMIT_BYTES as usize;
const DEBUG_RING_BUFFER_MAX_LINE_BYTES: usize = 16 * 1024; // 16 KB per entry

#[derive(Debug, Default)]
struct DebugRingBufferState {
lines: VecDeque<String>,
bytes: usize,
}

static DEBUG_RING_BUFFER: Lazy<Mutex<DebugRingBufferState>> =
Lazy::new(|| Mutex::new(DebugRingBufferState::default()));

#[derive(Debug)]
struct DebugRingBufferAppender {
encoder: Box<dyn Encode>,
}

impl DebugRingBufferAppender {
fn new() -> Self {
Self {
encoder: Box::new(PatternEncoder::new(LOG_PATTERN)),
}
}

fn push_line(&self, line: String) {
let normalized_line = truncate_line_to_limit(&line, DEBUG_RING_BUFFER_MAX_LINE_BYTES);
if let Ok(mut state) = DEBUG_RING_BUFFER.lock() {
let line_bytes = normalized_line.as_bytes().len();

while state.bytes + line_bytes > DEBUG_RING_BUFFER_MAX_BYTES {
match state.lines.pop_front() {
Some(removed) => {
state.bytes = state.bytes.saturating_sub(removed.as_bytes().len());
}
None => break,
}
}

state.bytes += line_bytes;
state.lines.push_back(normalized_line);
}
}
}

fn truncate_line_to_limit(line: &str, max_bytes: usize) -> String {
if line.as_bytes().len() <= max_bytes {
return line.to_string();
}

const SUFFIX: &str = " ... [truncated]\n";
let suffix_bytes = SUFFIX.as_bytes().len();
if max_bytes <= suffix_bytes {
return SUFFIX[..max_bytes].to_string();
}

let mut end = max_bytes - suffix_bytes;
while end > 0 && !line.is_char_boundary(end) {
end -= 1;
}

let mut truncated = String::with_capacity(max_bytes);
truncated.push_str(&line[..end]);
truncated.push_str(SUFFIX);
truncated
}

impl log::Log for DebugRingBufferAppender {
fn enabled(&self, _metadata: &log::Metadata) -> bool {
true
}

fn log(&self, record: &log::Record) {
if !self.enabled(record.metadata()) {
return;
}

let mut encoded = Vec::new();
{
let mut writer = SimpleWriter(&mut encoded);
if let Err(e) = self.encoder.encode(&mut writer, record) {
eprintln!("[Logging] Failed to encode debug ring buffer line: {}", e);
return;
}
}

self.push_line(String::from_utf8_lossy(&encoded).into_owned());

if record.level() == log::Level::Error {
let reason = format!("error: {}", record.args());
if let Err(e) = dump_debug_buffer_to_file(&reason) {
eprintln!("[Logging] Failed to dump debug ring buffer on error: {}", e);
}
}
}

fn flush(&self) {}
}

pub fn dump_debug_buffer_to_file(
reason: &str,
) -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
let log_dir = LAUNCHER_DIRECTORY.root_dir().join(LOG_DIR_NAME);
std::fs::create_dir_all(&log_dir)?;

let timestamp = Utc::now().format("%Y%m%d-%H%M%S%.3f");
let dump_file_path = log_dir.join(format!("{}-{}.log", DEBUG_DUMP_FILE_PREFIX, timestamp));

let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&dump_file_path)?;

writeln!(file, "# NoRisk Launcher Debug Buffer Dump")?;
writeln!(file, "# Timestamp (UTC): {}", Utc::now().to_rfc3339())?;
writeln!(file, "# Reason: {}", reason)?;
let (current_buffer_len, current_buffer_bytes) = DEBUG_RING_BUFFER
.lock()
.map(|state| (state.lines.len(), state.bytes))
.unwrap_or((0, 0));
writeln!(
file,
"# Buffered Lines: {}",
current_buffer_len,
)?;
writeln!(
file,
"# Buffered Bytes: {} (max {})",
current_buffer_bytes,
DEBUG_RING_BUFFER_MAX_BYTES
)?;
writeln!(
file,
"# Max Line Bytes: {}",
DEBUG_RING_BUFFER_MAX_LINE_BYTES
)?;
writeln!(file)?;

if let Ok(state) = DEBUG_RING_BUFFER.lock() {
for line in state.lines.iter() {
file.write_all(line.as_bytes())?;
}
}

Ok(dump_file_path)
}

/// Initializes the logging system using log4rs.
/// Configures a rolling file appender and a console appender.
Expand Down Expand Up @@ -52,22 +208,37 @@ pub async fn setup_logging() -> Result<(), Box<dyn std::error::Error>> {
.target(Target::Stdout)
.build();

// --- Configure In-Memory Debug Ring Buffer Appender ---
let debug_ring_buffer_appender = DebugRingBufferAppender::new();

// --- Configure log4rs ---
let config = Config::builder()
.appender(Appender::builder().build("file", Box::new(file_appender)))
.appender(
Appender::builder()
.filter(Box::new(ThresholdFilter::new(LevelFilter::Info)))
.build("file", Box::new(file_appender)),
)
.appender(Appender::builder().build("stdout", Box::new(console_appender))) // Add console appender
.appender(
Appender::builder()
.build("debug_ring_buffer", Box::new(debug_ring_buffer_appender)),
)
// Suppress noisy event-loop warnings from window focus/tab-in transitions.
.logger(Logger::builder().build("winit", LevelFilter::Error)) // prevent winit from logging at all levels (WARN on window focus)
.logger(Logger::builder().build("tao", LevelFilter::Error)) // prevent tao from logging at all levels (WARN on window focus)
.build(
Root::builder()
.appender("file") // Log to file
.appender("stdout") // Log to console
.build(LevelFilter::Debug), // Log Debug and above to both
.appender("debug_ring_buffer") // Keep debug lines in RAM for crash/error dumps
.build(LevelFilter::Debug),
)?;

// Initialize log4rs
log4rs::init_config(config)?;

// Now we can use log::info!
log::info!("Logging initialized. Log directory: {}", log_dir.display());
log::debug!("Logging initialized. Log directory: {}", log_dir.display());

Ok(())
}
Loading