Skip to content
Merged
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
9 changes: 8 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,19 @@ jobs:
# --prerelease on its own only sets the flag on a still-unpublished
# draft. Nothing here goes the other way: a release that is already
# published is never pushed back to a draft.
#
# A draft has no tag yet: Publish makes it from the draft's target.
# Rebuilding a draft from a newer commit has to move the target
# too, or Publish tags the commit before the change while the
# binaries were built after it.
target=""
if [ "$state" = "--draft" ]; then target="--target $GITHUB_SHA"; fi
if [ "${{ inputs.prerelease }}" = "true" ] && [ "$state" = "--draft" ]; then
echo "release $V is a draft and a prerelease was asked for — publishing it"
state="--prerelease --draft=false"
fi
echo "release $V exists ($state) — updating in place"
gh release edit "$V" $state --title "$V" --notes-file RELEASE_NOTES.md
gh release edit "$V" $state $target --title "$V" --notes-file RELEASE_NOTES.md
# Cleared by name first: a run cancelled mid-upload leaves a
# half-created asset, and the API then answers 400 for a fresh
# upload of that name rather than replacing it.
Expand Down
63 changes: 49 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ image = { version = "0.25", default-features = false, features = [
"png",
] }
qrcode = { version = "0.14", default-features = false }
rfd = { version = "0.15", default-features = false }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["rt-multi-thread", "sync"] }
Expand Down
6 changes: 6 additions & 0 deletions crates/haru/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ path = "src/main.rs"
[dependencies]
eframe = { workspace = true }
egui = { workspace = true }
haru-core = { workspace = true }
haru-ui = { workspace = true }
image = { workspace = true }

# The message box that says why haru stopped, since a release build on
# Windows has no console to print to.
[target.'cfg(windows)'.dependencies]
rfd = { workspace = true }

[lints]
workspace = true
122 changes: 122 additions & 0 deletions crates/haru/src/failure.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//! Saying why haru stopped, somewhere a person will see it.
//!
//! A release build on Windows has no console, so everything haru printed on
//! the way out went nowhere: a window that could not open, or a panic, looked
//! exactly like double-clicking and nothing happening. Every way `main` gives
//! up now goes through `report`, which still prints, also appends to a log in
//! haru's data folder, and on Windows puts up a message box.

use std::io::Write as _;
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::Mutex;

/// The last panic, kept for `main` to report once the event loop has unwound.
static LAST_PANIC: Mutex<Option<String>> = Mutex::new(None);

/// Log every panic, and keep the last one for `panicked`.
///
/// Rust's own hook still runs first, so a console, when there is one, shows
/// what it always did. The message box is left to `main`: putting one up from
/// inside the hook would run a modal loop while winit is still unwinding.
pub fn record_panics() {
let default = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
default(info);
let thread = std::thread::current();
let said = format!(
"haru crashed in thread {}: {info}",
thread.name().unwrap_or("unnamed")
);
let _ = log(&said);
if let Ok(mut last) = LAST_PANIC.lock() {
*last = Some(said);
}
}));
}

/// Report the panic that ended the event loop.
pub fn panicked() -> ExitCode {
let said = LAST_PANIC
.lock()
.ok()
.and_then(|mut last| last.take())
.unwrap_or_else(|| "haru crashed".to_owned());
// Already logged by the hook.
show(&said, log_path().as_deref());
ExitCode::FAILURE
}

/// Say why haru could not go on, everywhere it might be seen.
pub fn report(message: &str) -> ExitCode {
eprintln!("haru: {message}");
let logged = log(message).ok();
show(message, logged.as_deref());
ExitCode::FAILURE
}

/// Why eframe could not open the window, in words that point at the fix.
pub fn window_error(error: &eframe::Error) -> String {
match error {
eframe::Error::Wgpu(_) => format!(
"haru could not find a graphics driver it can draw with (Vulkan or OpenGL). \
Updating the graphics driver usually fixes this.\n\n({error})"
),
error => format!("haru could not open its window.\n\n({error})"),
}
}

fn log_path() -> Option<PathBuf> {
Some(haru_core::data_home()?.join("haru").join("haru.log"))
}

/// Append to haru's log, returning where it went.
fn log(message: &str) -> std::io::Result<PathBuf> {
let path = log_path().ok_or_else(|| std::io::Error::other("no data folder"))?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let seconds = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|since| since.as_secs())
.unwrap_or(0);
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)?;
writeln!(file, "[unix time {seconds}] {message}")?;
Ok(path)
}

#[cfg(windows)]
fn show(message: &str, logged: Option<&std::path::Path>) {
let description = match logged {
Some(path) => format!("{message}\n\nThis was also saved to {}", path.display()),
None => message.to_owned(),
};
let _ = rfd::MessageDialog::new()
.set_level(rfd::MessageLevel::Error)
.set_title("haru")
.set_description(description)
.set_buttons(rfd::MessageButtons::Ok)
.show();
}

// Elsewhere haru is started from a terminal often enough, and a desktop
// launcher keeps what it printed in the journal or Console.
#[cfg(not(windows))]
const fn show(_message: &str, _logged: Option<&std::path::Path>) {}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn a_missing_graphics_driver_is_named_as_such() {
let said = window_error(&eframe::Error::Wgpu(
eframe::egui_wgpu::WgpuError::NoSuitableAdapterFound,
));
assert!(said.contains("graphics driver"), "{said}");
assert!(said.contains("no suitable adapter"), "{said}");
}
}
Loading
Loading