fix: never panic when the log directory is unwritable; disable file logging instead [#67]
Check / stable / fmt (push) Failing after 26s
Check / beta / clippy (push) Failing after 1m10s
Check / stable / clippy (push) Failing after 1m10s
Check / nightly / doc (push) Successful in 1m0s
Check / 1.95.0 / check (push) Successful in 1m7s
Check / dockerfile / msrv (push) Successful in 5s
Test Suite / ubuntu / beta (push) Successful in 2m15s
Test Suite / ubuntu / stable (push) Successful in 2m14s
Test Suite / ubuntu / stable / coverage (push) Failing after 1m57s
Test Suite / macos-latest / stable (push) Canceled after 0s
Test Suite / windows-latest / stable (push) Canceled after 0s

This commit is contained in:
Dark-Alex-17 committed 2026-10-12 11:16:11 -06:00
1 parent f6071f73b5
commit 0f230a1a4f
4 files changed
+341 -28

No files matched your search

+65 -20
View File
@@ -2,7 +2,7 @@ use std::fs::{self, File};
use std::io::{BufRead, BufReader, Seek, SeekFrom};
use std::path::PathBuf;
use std::process;
use std::sync::Arc;
use std::sync::{Arc, Once};
use std::time::Duration;
use anyhow::Result;
@@ -34,52 +34,92 @@ use crate::ui::theme::ThemeDefinitionsWrapper;
mod utils_tests;
pub fn get_log_path() -> PathBuf {
let mut log_path = if cfg!(target_os = "linux") {
let preferred = if cfg!(target_os = "linux") {
dirs_next::cache_dir().unwrap_or_else(|| PathBuf::from("~/.cache"))
} else if cfg!(target_os = "macos") {
dirs_next::home_dir().unwrap().join("Library/Logs")
dirs_next::home_dir()
.map(|h| h.join("Library/Logs"))
.unwrap_or_else(std::env::temp_dir)
} else {
dirs_next::data_local_dir().unwrap_or_else(|| PathBuf::from("C:\\Logs"))
};
}
.join("managarr");
log_path.push("managarr");
resolve_log_dir(preferred).join("managarr.log")
}
if let Err(e) = fs::create_dir_all(&log_path) {
eprintln!("Failed to create log directory: {e:?}");
fn resolve_log_dir(preferred: PathBuf) -> PathBuf {
static WARN_ONCE: Once = Once::new();
if let Err(e) = fs::create_dir_all(&preferred) {
WARN_ONCE.call_once(|| {
eprintln!(
"Failed to create log directory {}: {e}; file logging will be disabled",
preferred.display()
);
});
}
log_path.push("managarr.log");
log_path
preferred
}
pub fn init_logging_config() -> log4rs::Config {
let log_path = get_log_path();
build_logging_config(get_log_path())
}
fn build_logging_config(log_path: PathBuf) -> log4rs::Config {
let archive_pattern = log_path
.with_file_name("managarr.{}.log")
.to_string_lossy()
.into_owned();
let trigger = SizeTrigger::new(10 * 1024 * 1024);
let roller = FixedWindowRoller::builder()
.build(&archive_pattern, 3)
.expect("Failed to build log roller");
let roller = match FixedWindowRoller::builder().build(&archive_pattern, 3) {
Ok(roller) => roller,
Err(e) => {
eprintln!(
"Failed to build log roller for {archive_pattern}: {e}; file logging will be disabled"
);
return disabled_logging_config();
}
};
let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller));
let logfile = RollingFileAppender::builder()
let logfile = match RollingFileAppender::builder()
.encoder(Box::new(PatternEncoder::new(
"{d(%Y-%m-%d %H:%M:%S%.3f)(utc)} <{i}> [{l}] {f}:{L} - {m}{n}",
)))
.build(log_path, Box::new(policy))
.expect("Failed to build rolling file appender");
.build(&log_path, Box::new(policy))
{
Ok(logfile) => logfile,
Err(e) => {
eprintln!(
"Failed to build rolling file appender for {}: {e}; file logging will be disabled",
log_path.display()
);
return disabled_logging_config();
}
};
log4rs::Config::builder()
match log4rs::Config::builder()
.appender(Appender::builder().build("logfile", Box::new(logfile)))
.build(
Root::builder()
.appender("logfile")
.build(LevelFilter::Debug),
)
.unwrap()
) {
Ok(config) => config,
Err(e) => {
eprintln!("Failed to build logging config: {e}; file logging will be disabled");
disabled_logging_config()
}
}
}
fn disabled_logging_config() -> log4rs::Config {
log4rs::Config::builder()
.build(Root::builder().build(LevelFilter::Debug))
.expect("appender-free logging config is infallibly valid")
}
pub fn format_size(bytes: i64, precision: usize) -> String {
@@ -102,7 +142,12 @@ pub fn convert_runtime(runtime: i64) -> (i64, i64) {
pub async fn tail_logs(no_color: bool) -> Result<()> {
let re = Regex::new(r"^(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s+<(?P<opid>[^\s>]+)>\s+\[(?P<level>[A-Z]+)]\s+(?P<logger>[^:]+):(?P<line>\d+)\s+-\s+(?P<message>.*)$").unwrap();
let file_path = get_log_path();
let file = File::open(&file_path).expect("Cannot open file");
let file = File::open(&file_path).with_context(|| {
format!(
"Unable to open log file {}; file logging may be disabled",
file_path.display()
)
})?;
let mut reader = BufReader::new(file);
reader
+77 -1
View File
@@ -15,7 +15,8 @@ mod tests {
app::{App, AppConfig, ServarrConfig},
network::NetworkError,
utils::{
convert_runtime, format_cli_error, format_size, select_cli_configuration, was_log_rotated,
build_logging_config, convert_runtime, format_cli_error, format_size, resolve_log_dir,
select_cli_configuration, was_log_rotated,
},
};
@@ -236,4 +237,79 @@ mod tests {
fs::remove_file(&path).unwrap();
}
#[test]
fn test_resolve_log_dir_returns_preferred_dir_when_creatable() {
let preferred = std::env::temp_dir().join(format!(
"managarr_test_preferred_log_dir_{}",
std::process::id()
));
let _ = fs::remove_dir_all(&preferred);
let resolved = resolve_log_dir(preferred.clone());
assert_eq!(resolved, preferred);
assert!(preferred.is_dir());
fs::remove_dir_all(&preferred).unwrap();
}
#[test]
fn test_resolve_log_dir_keeps_preferred_dir_and_disables_file_logging_when_uncreatable() {
let blocker = std::env::temp_dir().join(format!(
"managarr_test_log_dir_blocker_{}",
std::process::id()
));
let _ = fs::remove_file(&blocker);
fs::write(&blocker, "not a directory").unwrap();
let preferred = blocker.join("logs");
let resolved = resolve_log_dir(preferred.clone());
assert_eq!(resolved, preferred);
assert!(!resolved.exists());
let config = build_logging_config(resolved.join("managarr.log"));
assert!(config.appenders().is_empty());
assert!(config.root().appenders().is_empty());
fs::remove_file(&blocker).unwrap();
}
#[test]
fn test_build_logging_config_uses_logfile_appender_when_log_path_is_writable() {
let log_dir = std::env::temp_dir().join(format!(
"managarr_test_logging_config_{}",
std::process::id()
));
let _ = fs::remove_dir_all(&log_dir);
fs::create_dir_all(&log_dir).unwrap();
let config = build_logging_config(log_dir.join("managarr.log"));
assert_eq!(config.appenders().len(), 1);
assert_str_eq!(config.appenders()[0].name(), "logfile");
assert_eq!(config.root().appenders(), ["logfile"]);
drop(config);
fs::remove_dir_all(&log_dir).unwrap();
}
#[test]
fn test_build_logging_config_disables_logging_when_appender_is_unbuildable() {
let blocker = std::env::temp_dir().join(format!(
"managarr_test_appender_blocker_{}",
std::process::id()
));
let _ = fs::remove_file(&blocker);
fs::write(&blocker, "not a directory").unwrap();
let config = build_logging_config(blocker.join("logs").join("managarr.log"));
assert!(config.appenders().is_empty());
assert!(config.root().appenders().is_empty());
fs::remove_file(&blocker).unwrap();
}
}