Add launch crash detection with detailed error dialog, fix all warnings
This commit is contained in:
@@ -79,59 +79,67 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
|
||||
]);
|
||||
let record_id = record.id;
|
||||
let path = record.path.clone();
|
||||
let app_name_launch = record.app_name.clone().unwrap_or_else(|| record.filename.clone());
|
||||
let db_launch = db.clone();
|
||||
launch_button.connect_clicked(move |_| {
|
||||
let appimage_path = std::path::Path::new(&path);
|
||||
let result = launcher::launch_appimage(
|
||||
&db_launch,
|
||||
record_id,
|
||||
appimage_path,
|
||||
"gui_detail",
|
||||
&[],
|
||||
&[],
|
||||
);
|
||||
match result {
|
||||
launcher::LaunchResult::Started { child, method } => {
|
||||
let pid = child.id();
|
||||
log::info!("Launched AppImage: {} (PID: {}, method: {})", path, pid, method.as_str());
|
||||
let toast_launch = toast_overlay.clone();
|
||||
launch_button.connect_clicked(move |btn| {
|
||||
btn.set_sensitive(false);
|
||||
let btn_ref = btn.clone();
|
||||
let path = path.clone();
|
||||
let app_name = app_name_launch.clone();
|
||||
let db_launch = db_launch.clone();
|
||||
let toast_ref = toast_launch.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
let path_bg = path.clone();
|
||||
let result = gio::spawn_blocking(move || {
|
||||
let appimage_path = std::path::Path::new(&path_bg);
|
||||
launcher::launch_appimage(
|
||||
&Database::open().expect("DB open"),
|
||||
record_id,
|
||||
appimage_path,
|
||||
"gui_detail",
|
||||
&[],
|
||||
&[],
|
||||
)
|
||||
}).await;
|
||||
|
||||
let db_wayland = db_launch.clone();
|
||||
let path_clone = path.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
glib::timeout_future(std::time::Duration::from_secs(3)).await;
|
||||
btn_ref.set_sensitive(true);
|
||||
match result {
|
||||
Ok(launcher::LaunchResult::Started { child, method }) => {
|
||||
let pid = child.id();
|
||||
log::info!("Launched: {} (PID: {}, method: {})", path, pid, method.as_str());
|
||||
|
||||
let analysis_result = gio::spawn_blocking(move || {
|
||||
wayland::analyze_running_process(pid)
|
||||
}).await;
|
||||
|
||||
match analysis_result {
|
||||
Ok(Ok(analysis)) => {
|
||||
let status_label = analysis.status_label();
|
||||
let db_wayland = db_launch.clone();
|
||||
let path_clone = path.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
glib::timeout_future(std::time::Duration::from_secs(3)).await;
|
||||
let analysis_result = gio::spawn_blocking(move || {
|
||||
wayland::analyze_running_process(pid)
|
||||
}).await;
|
||||
if let Ok(Ok(analysis)) = analysis_result {
|
||||
let status_str = analysis.as_status_str();
|
||||
log::info!(
|
||||
"Runtime Wayland analysis for {} (PID {}): {} (wayland_socket={}, x11={}, env_vars={})",
|
||||
path_clone, analysis.pid, status_label,
|
||||
analysis.has_wayland_socket,
|
||||
analysis.has_x11_connection,
|
||||
analysis.env_vars.len(),
|
||||
);
|
||||
db_wayland.update_runtime_wayland_status(
|
||||
record_id, status_str,
|
||||
).ok();
|
||||
log::info!("Runtime Wayland: {} -> {}", path_clone, analysis.status_label());
|
||||
db_wayland.update_runtime_wayland_status(record_id, status_str).ok();
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::debug!("Runtime analysis failed for PID {}: {}", pid, e);
|
||||
}
|
||||
Err(_) => {
|
||||
log::debug!("Runtime analysis task failed for PID {}", pid);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Ok(launcher::LaunchResult::Crashed { exit_code, stderr, .. }) => {
|
||||
log::error!("App crashed on launch (exit {}): {}", exit_code.unwrap_or(-1), stderr);
|
||||
widgets::show_crash_dialog(&btn_ref, &app_name, exit_code, &stderr);
|
||||
}
|
||||
Ok(launcher::LaunchResult::Failed(msg)) => {
|
||||
log::error!("Failed to launch: {}", msg);
|
||||
let toast = adw::Toast::builder()
|
||||
.title(&format!("Could not launch: {}", msg))
|
||||
.timeout(5)
|
||||
.build();
|
||||
toast_ref.add_toast(toast);
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!("Launch task panicked");
|
||||
}
|
||||
}
|
||||
launcher::LaunchResult::Failed(msg) => {
|
||||
log::error!("Failed to launch: {}", msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
header.pack_end(&launch_button);
|
||||
|
||||
@@ -1025,7 +1033,7 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
|
||||
match result {
|
||||
Ok(analysis) => {
|
||||
let toolkit_label = analysis.toolkit.label();
|
||||
let lib_count = analysis.libraries_found.len();
|
||||
let _lib_count = analysis.libraries_found.len();
|
||||
row_clone.set_subtitle(&format!(
|
||||
"Built with: {}",
|
||||
toolkit_label,
|
||||
@@ -1062,13 +1070,10 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
|
||||
compat_group.add(&runtime_row);
|
||||
}
|
||||
|
||||
// FUSE status
|
||||
// FUSE status - always use live system detection (the stored fuse_status
|
||||
// is per-app AppImageFuseStatus, not the system-level FuseStatus)
|
||||
let fuse_system = fuse::detect_system_fuse();
|
||||
let fuse_status = record
|
||||
.fuse_status
|
||||
.as_deref()
|
||||
.map(FuseStatus::from_str)
|
||||
.unwrap_or(fuse_system.status.clone());
|
||||
let fuse_status = fuse_system.status.clone();
|
||||
|
||||
let fuse_row = adw::ActionRow::builder()
|
||||
.title("App mounting")
|
||||
@@ -1824,3 +1829,4 @@ fn fetch_favicon_async(url: &str, image: >k::Image) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use gtk::prelude::*;
|
||||
use adw::prelude::*;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Ensures the shared letter-icon CSS provider is registered on the default
|
||||
@@ -194,6 +194,141 @@ pub fn copy_button(text_to_copy: &str, toast_overlay: Option<&adw::ToastOverlay>
|
||||
btn
|
||||
}
|
||||
|
||||
/// Show a detailed crash dialog when an AppImage fails to start.
|
||||
/// Includes a plain-text explanation, the full error output in a copyable text view,
|
||||
/// and a button to copy the full error to clipboard.
|
||||
pub fn show_crash_dialog(
|
||||
parent: &impl gtk::prelude::IsA<gtk::Widget>,
|
||||
app_name: &str,
|
||||
exit_code: Option<i32>,
|
||||
stderr: &str,
|
||||
) {
|
||||
let explanation = crash_explanation(stderr);
|
||||
|
||||
let exit_str = exit_code
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
let body = format!("{}\n\nExit code: {}", explanation, exit_str);
|
||||
|
||||
let dialog = adw::AlertDialog::builder()
|
||||
.heading(&format!("{} failed to start", app_name))
|
||||
.body(&body)
|
||||
.close_response("close")
|
||||
.default_response("close")
|
||||
.build();
|
||||
|
||||
// Build the full text that gets copied
|
||||
let full_error = format!(
|
||||
"App: {}\nExit code: {}\n\n{}\n\nError output:\n{}",
|
||||
app_name,
|
||||
exit_str,
|
||||
explanation,
|
||||
stderr.trim(),
|
||||
);
|
||||
|
||||
// Extra content: scrollable text view with full stderr + copy button
|
||||
if !stderr.trim().is_empty() {
|
||||
let vbox = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.spacing(8)
|
||||
.build();
|
||||
|
||||
let heading = gtk::Label::builder()
|
||||
.label("Error output:")
|
||||
.xalign(0.0)
|
||||
.build();
|
||||
heading.add_css_class("heading");
|
||||
vbox.append(&heading);
|
||||
|
||||
let text_view = gtk::TextView::builder()
|
||||
.editable(false)
|
||||
.cursor_visible(false)
|
||||
.monospace(true)
|
||||
.wrap_mode(gtk::WrapMode::WordChar)
|
||||
.top_margin(8)
|
||||
.bottom_margin(8)
|
||||
.left_margin(8)
|
||||
.right_margin(8)
|
||||
.build();
|
||||
text_view.buffer().set_text(stderr.trim());
|
||||
text_view.add_css_class("card");
|
||||
|
||||
let scrolled = gtk::ScrolledWindow::builder()
|
||||
.child(&text_view)
|
||||
.min_content_height(120)
|
||||
.max_content_height(300)
|
||||
.build();
|
||||
vbox.append(&scrolled);
|
||||
|
||||
let copy_btn = gtk::Button::builder()
|
||||
.label("Copy to clipboard")
|
||||
.halign(gtk::Align::Start)
|
||||
.build();
|
||||
copy_btn.add_css_class("pill");
|
||||
let full_error_copy = full_error.clone();
|
||||
copy_btn.connect_clicked(move |btn| {
|
||||
let clipboard = btn.display().clipboard();
|
||||
clipboard.set_text(&full_error_copy);
|
||||
btn.set_label("Copied!");
|
||||
btn.set_sensitive(false);
|
||||
});
|
||||
vbox.append(©_btn);
|
||||
|
||||
dialog.set_extra_child(Some(&vbox));
|
||||
}
|
||||
|
||||
dialog.add_response("close", "Close");
|
||||
dialog.present(Some(parent));
|
||||
}
|
||||
|
||||
/// Generate a plain-text explanation of why an app crashed based on stderr patterns.
|
||||
fn crash_explanation(stderr: &str) -> String {
|
||||
if stderr.contains("Could not find the Qt platform plugin") || stderr.contains("qt.qpa.plugin") {
|
||||
return "The app couldn't find a required display plugin. This usually means \
|
||||
it needs a Qt library that isn't bundled inside the AppImage or \
|
||||
available on your system.".to_string();
|
||||
}
|
||||
if stderr.contains("cannot open shared object file") {
|
||||
if let Some(pos) = stderr.find("cannot open shared object file") {
|
||||
let before = &stderr[..pos];
|
||||
if let Some(start) = before.rfind(": ") {
|
||||
let lib = before[start + 2..].trim();
|
||||
if !lib.is_empty() {
|
||||
return format!(
|
||||
"The app needs a system library ({}) that isn't installed. \
|
||||
You may be able to fix this by installing the missing package.",
|
||||
lib,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "The app needs a system library that isn't installed on your system.".to_string();
|
||||
}
|
||||
if stderr.contains("Segmentation fault") || stderr.contains("SIGSEGV") {
|
||||
return "The app crashed due to a memory error. This is usually a bug \
|
||||
in the app itself, not something you can fix.".to_string();
|
||||
}
|
||||
if stderr.contains("Permission denied") {
|
||||
return "The app was blocked from accessing something it needs. \
|
||||
Check that the AppImage file has the right permissions.".to_string();
|
||||
}
|
||||
if stderr.contains("fatal IO error") || stderr.contains("display connection") {
|
||||
return "The app lost its connection to the display server. This can happen \
|
||||
with apps that don't fully support your display system.".to_string();
|
||||
}
|
||||
if stderr.contains("FATAL:") || stderr.contains("Aborted") {
|
||||
return "The app hit a fatal error and had to stop. The error details \
|
||||
below may help identify the cause.".to_string();
|
||||
}
|
||||
if stderr.contains("Failed to initialize") {
|
||||
return "The app couldn't set itself up properly. It may need additional \
|
||||
system components to run.".to_string();
|
||||
}
|
||||
"The app exited immediately after starting. The error details below \
|
||||
may help identify the cause.".to_string()
|
||||
}
|
||||
|
||||
/// Create a screen-reader live region announcement.
|
||||
/// Inserts a hidden label with AccessibleRole::Alert into the given container,
|
||||
/// which causes AT-SPI to announce the text to screen readers.
|
||||
|
||||
Reference in New Issue
Block a user