Skip to content

Commit a70bf21

Browse files
committed
Log crash and CLI parse errors
1 parent 31dd174 commit a70bf21

File tree

5 files changed

+85
-54
lines changed

5 files changed

+85
-54
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* CLI: When using the `--gui` option of any command that supports it,
1919
errors at the end of the process will also be reported via dialogs.
2020
This does not apply to CLI parse errors.
21+
* Application crash and CLI parse errors are now logged.
2122
* Fixed:
2223
* If a custom game's title begins or ends with a space,
2324
that custom game will now be ignored.

Cargo.lock

-30
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ clap_complete = "4.5.28"
1717
dialoguer = "0.11.0"
1818
dirs = "5.0.1"
1919
filetime = "0.2.25"
20-
flexi_logger = { version = "0.29.3", features = ["async"] }
20+
flexi_logger = { version = "0.29.3", features = ["textfilter"], default-features = false }
2121
fluent = "0.16.1"
2222
fuzzy-matcher = "0.3.7"
2323
globetter = "0.2.0"

src/cli.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,9 @@ pub fn evaluate_games(
119119
Ok(valid.into_iter().collect())
120120
}
121121

122-
pub fn parse() -> Cli {
122+
pub fn parse() -> Result<Cli, clap::Error> {
123123
use clap::Parser;
124-
Cli::parse()
124+
Cli::try_parse()
125125
}
126126

127127
pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool) -> Result<(), Error> {
@@ -135,7 +135,6 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)
135135
let mut duplicate_detector = DuplicateDetector::default();
136136

137137
log::debug!("Config on startup: {config:?}");
138-
log::debug!("Invocation: {sub:?}");
139138

140139
match sub {
141140
Subcommand::Backup {

src/main.rs

+81-20
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,13 @@ use crate::{
2020
prelude::{app_dir, CONFIG_DIR, VERSION},
2121
};
2222

23-
/// The logger must be assigned to a variable because we're using async logging.
24-
/// We should also avoid doing this if we're just going to relaunch into detached mode anyway.
23+
/// The logger handle must be retained until the application closes.
2524
/// https://docs.rs/flexi_logger/0.23.1/flexi_logger/error_info/index.html#write
2625
fn prepare_logging() -> Result<flexi_logger::LoggerHandle, flexi_logger::FlexiLoggerError> {
2726
flexi_logger::Logger::try_with_env_or_str("ludusavi=warn")
2827
.unwrap()
2928
.log_to_file(flexi_logger::FileSpec::default().directory(app_dir().as_std_path_buf().unwrap()))
30-
.write_mode(flexi_logger::WriteMode::Async)
29+
.write_mode(flexi_logger::WriteMode::BufferAndFlush)
3130
.rotate(
3231
flexi_logger::Criterion::Size(1024 * 1024 * 10),
3332
flexi_logger::Naming::Timestamps,
@@ -47,6 +46,37 @@ fn prepare_logging() -> Result<flexi_logger::LoggerHandle, flexi_logger::FlexiLo
4746
.start()
4847
}
4948

49+
/// Based on: https://github.com/Traverse-Research/panic-log/blob/874a61b24a8bc8f9b07f9c26dc10b13cbc2622f9/src/lib.rs#L26
50+
/// Modified to flush a provided log handle.
51+
fn prepare_panic_hook(handle: Option<flexi_logger::LoggerHandle>) {
52+
let original_hook = std::panic::take_hook();
53+
std::panic::set_hook(Box::new(move |info| {
54+
let thread_name = std::thread::current().name().unwrap_or("<unnamed thread>").to_owned();
55+
56+
let location = if let Some(panic_location) = info.location() {
57+
format!(
58+
"{}:{}:{}",
59+
panic_location.file(),
60+
panic_location.line(),
61+
panic_location.column()
62+
)
63+
} else {
64+
"<unknown location>".to_owned()
65+
};
66+
let message = info.payload().downcast_ref::<&str>().unwrap_or(&"");
67+
68+
let backtrace = std::backtrace::Backtrace::force_capture();
69+
70+
log::error!("thread '{thread_name}' panicked at {location}:\n{message}\nstack backtrace:\n{backtrace}");
71+
72+
if let Some(handle) = handle.clone() {
73+
handle.flush();
74+
}
75+
76+
original_hook(info);
77+
}));
78+
}
79+
5080
/// Detach the current process from its console on Windows.
5181
///
5282
/// ## Testing
@@ -87,33 +117,69 @@ fn prepare_logging() -> Result<flexi_logger::LoggerHandle, flexi_logger::FlexiLo
87117
/// ("The request is not supported (os error 50)"),
88118
/// but that has been solved by resetting the standard device handles:
89119
/// https://github.com/rust-lang/rust/issues/113277
120+
///
121+
/// Watch out for non-obvious code paths that may defeat detachment.
122+
/// flexi_logger's `colors` feature would cause the console to stick around
123+
/// if logging was enabled before detaching.
90124
#[cfg(target_os = "windows")]
91125
unsafe fn detach_console() {
92126
use windows::Win32::{
93127
Foundation::HANDLE,
94128
System::Console::{FreeConsole, SetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE},
95129
};
96130

131+
fn tell(msg: &str) {
132+
eprintln!("{}", msg);
133+
log::error!("{}", msg);
134+
}
135+
97136
if FreeConsole().is_err() {
98-
eprintln!("Unable to detach the console");
137+
tell("Unable to detach the console");
99138
std::process::exit(1);
100139
}
101140
if SetStdHandle(STD_INPUT_HANDLE, HANDLE::default()).is_err() {
102-
eprintln!("Unable to reset stdin handle");
141+
tell("Unable to reset stdin handle");
103142
std::process::exit(1);
104143
}
105144
if SetStdHandle(STD_OUTPUT_HANDLE, HANDLE::default()).is_err() {
106-
eprintln!("Unable to reset stdout handle");
145+
tell("Unable to reset stdout handle");
107146
std::process::exit(1);
108147
}
109148
if SetStdHandle(STD_ERROR_HANDLE, HANDLE::default()).is_err() {
110-
eprintln!("Unable to reset stderr handle");
149+
tell("Unable to reset stderr handle");
111150
std::process::exit(1);
112151
}
113152
}
114153

115154
fn main() {
116-
let args = cli::parse();
155+
let mut failed = false;
156+
157+
let mut logger = prepare_logging();
158+
#[allow(clippy::useless_asref)]
159+
prepare_panic_hook(logger.as_ref().map(|x| x.clone()).ok());
160+
let mut flush_logger = || {
161+
if let Ok(logger) = &mut logger {
162+
logger.flush();
163+
}
164+
};
165+
166+
log::debug!("Version: {}", *VERSION);
167+
log::debug!("Invocation: {:?}", std::env::args());
168+
169+
let args = match cli::parse() {
170+
Ok(x) => x,
171+
Err(e) => {
172+
match e.kind() {
173+
clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {}
174+
_ => {
175+
log::error!("CLI failed to parse: {e}");
176+
}
177+
}
178+
flush_logger();
179+
e.exit()
180+
}
181+
};
182+
117183
if let Some(config_dir) = args.config.as_deref() {
118184
*CONFIG_DIR.lock().unwrap() = Some(config_dir.to_path_buf());
119185
}
@@ -126,30 +192,25 @@ fn main() {
126192
}
127193
}
128194

129-
// We must do this after detaching the console, or else it will still be present, somehow.
130-
#[allow(unused)]
131-
let logger = prepare_logging();
132-
133-
log::debug!("Version: {}", *VERSION);
134-
135195
let flags = Flags {
136196
update_manifest: !args.no_manifest_update,
137197
};
138198
gui::run(flags);
139199
}
140200
Some(sub) => {
141-
#[allow(unused)]
142-
let logger = prepare_logging();
143-
144201
let gui = sub.gui();
145202
let force = sub.force();
146203

147-
log::debug!("Version: {}", *VERSION);
148-
149204
if let Err(e) = cli::run(sub, args.no_manifest_update, args.try_manifest_update) {
205+
failed = true;
150206
cli::show_error(&e, gui, force);
151-
std::process::exit(1);
152207
}
153208
}
154209
};
210+
211+
flush_logger();
212+
213+
if failed {
214+
std::process::exit(1);
215+
}
155216
}

0 commit comments

Comments
 (0)