Add launch crash detection with detailed error dialog, fix all warnings

Detect AppImages that crash immediately after spawning (within 1.5s) by
capturing stderr and using try_wait(). Show a full AlertDialog with a
plain-text explanation, scrollable error output, and a copy-to-clipboard
button. Covers Qt plugin errors, missing libraries, segfaults, permission
issues, and display connection failures.

Move launch operations to background threads in both the detail view and
context menu to avoid blocking the UI during the 1.5s crash detection
window.

Suppress all 57 compiler warnings across future-use modules (backup,
notification, report, watcher) and individual unused fields/variants in
other core modules.
This commit is contained in:
lashman
2026-02-27 20:23:10 +02:00
parent c9c9c0341b
commit d493516efa
16 changed files with 324 additions and 70 deletions

View File

@@ -621,6 +621,14 @@ fn cmd_launch(db: &Database, path: &str) -> ExitCode {
); );
ExitCode::SUCCESS ExitCode::SUCCESS
} }
launcher::LaunchResult::Crashed { stderr, exit_code, .. } => {
eprintln!(
"App crashed immediately (exit code: {})\n{}",
exit_code.map(|c| c.to_string()).unwrap_or_else(|| "unknown".into()),
stderr,
);
ExitCode::FAILURE
}
launcher::LaunchResult::Failed(msg) => { launcher::LaunchResult::Failed(msg) => {
eprintln!("Error: {}", msg); eprintln!("Error: {}", msg);
ExitCode::FAILURE ExitCode::FAILURE
@@ -633,6 +641,14 @@ fn cmd_launch(db: &Database, path: &str) -> ExitCode {
println!("Launched {} ({})", path, method.as_str()); println!("Launched {} ({})", path, method.as_str());
ExitCode::SUCCESS ExitCode::SUCCESS
} }
launcher::LaunchResult::Crashed { stderr, exit_code, .. } => {
eprintln!(
"App crashed immediately (exit code: {})\n{}",
exit_code.map(|c| c.to_string()).unwrap_or_else(|| "unknown".into()),
stderr,
);
ExitCode::FAILURE
}
launcher::LaunchResult::Failed(msg) => { launcher::LaunchResult::Failed(msg) => {
eprintln!("Error: {}", msg); eprintln!("Error: {}", msg);
ExitCode::FAILURE ExitCode::FAILURE

View File

@@ -15,6 +15,7 @@ const MAX_CONCURRENT_ANALYSES: usize = 2;
static RUNNING_ANALYSES: AtomicUsize = AtomicUsize::new(0); static RUNNING_ANALYSES: AtomicUsize = AtomicUsize::new(0);
/// Returns the number of currently running background analyses. /// Returns the number of currently running background analyses.
#[allow(dead_code)]
pub fn running_count() -> usize { pub fn running_count() -> usize {
RUNNING_ANALYSES.load(Ordering::Relaxed) RUNNING_ANALYSES.load(Ordering::Relaxed)
} }

View File

@@ -405,6 +405,7 @@ fn summarize_content_rating(attrs: &[(String, String)]) -> String {
// AppStream catalog generation - writes catalog XML for GNOME Software/Discover // AppStream catalog generation - writes catalog XML for GNOME Software/Discover
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
#[allow(dead_code)]
/// Generate an AppStream catalog XML from the Driftwood database. /// Generate an AppStream catalog XML from the Driftwood database.
/// This allows GNOME Software / KDE Discover to see locally managed AppImages. /// This allows GNOME Software / KDE Discover to see locally managed AppImages.
pub fn generate_catalog(db: &Database) -> Result<String, AppStreamError> { pub fn generate_catalog(db: &Database) -> Result<String, AppStreamError> {
@@ -462,6 +463,7 @@ pub fn generate_catalog(db: &Database) -> Result<String, AppStreamError> {
Ok(xml) Ok(xml)
} }
#[allow(dead_code)]
/// Install the AppStream catalog to the local swcatalog directory. /// Install the AppStream catalog to the local swcatalog directory.
/// GNOME Software reads from `~/.local/share/swcatalog/xml/`. /// GNOME Software reads from `~/.local/share/swcatalog/xml/`.
pub fn install_catalog(db: &Database) -> Result<PathBuf, AppStreamError> { pub fn install_catalog(db: &Database) -> Result<PathBuf, AppStreamError> {
@@ -482,6 +484,7 @@ pub fn install_catalog(db: &Database) -> Result<PathBuf, AppStreamError> {
Ok(catalog_path) Ok(catalog_path)
} }
#[allow(dead_code)]
/// Remove the AppStream catalog from the local swcatalog directory. /// Remove the AppStream catalog from the local swcatalog directory.
pub fn uninstall_catalog() -> Result<(), AppStreamError> { pub fn uninstall_catalog() -> Result<(), AppStreamError> {
let catalog_path = dirs::data_dir() let catalog_path = dirs::data_dir()
@@ -498,6 +501,7 @@ pub fn uninstall_catalog() -> Result<(), AppStreamError> {
Ok(()) Ok(())
} }
#[allow(dead_code)]
/// Check if the AppStream catalog is currently installed. /// Check if the AppStream catalog is currently installed.
pub fn is_catalog_installed() -> bool { pub fn is_catalog_installed() -> bool {
let catalog_path = dirs::data_dir() let catalog_path = dirs::data_dir()
@@ -511,6 +515,7 @@ pub fn is_catalog_installed() -> bool {
// --- Utility functions --- // --- Utility functions ---
#[allow(dead_code)]
fn make_component_id(name: &str) -> String { fn make_component_id(name: &str) -> String {
name.chars() name.chars()
.map(|c| if c.is_alphanumeric() || c == '-' || c == '.' { c.to_ascii_lowercase() } else { '_' }) .map(|c| if c.is_alphanumeric() || c == '-' || c == '.' { c.to_ascii_lowercase() } else { '_' })
@@ -519,6 +524,7 @@ fn make_component_id(name: &str) -> String {
.to_string() .to_string()
} }
#[allow(dead_code)]
fn xml_escape(s: &str) -> String { fn xml_escape(s: &str) -> String {
s.replace('&', "&amp;") s.replace('&', "&amp;")
.replace('<', "&lt;") .replace('<', "&lt;")
@@ -530,6 +536,7 @@ fn xml_escape(s: &str) -> String {
// --- Error types --- // --- Error types ---
#[derive(Debug)] #[derive(Debug)]
#[allow(dead_code)]
pub enum AppStreamError { pub enum AppStreamError {
Database(String), Database(String),
Io(String), Io(String),

View File

@@ -1,3 +1,5 @@
#![allow(dead_code)]
use std::fs; use std::fs;
use std::io::Read; use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};

View File

@@ -185,6 +185,7 @@ pub struct ConfigBackupRecord {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct CatalogSourceRecord { pub struct CatalogSourceRecord {
pub id: i64, pub id: i64,
pub name: String, pub name: String,
@@ -196,6 +197,7 @@ pub struct CatalogSourceRecord {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct CatalogAppRecord { pub struct CatalogAppRecord {
pub id: i64, pub id: i64,
pub source_id: i64, pub source_id: i64,

View File

@@ -38,6 +38,7 @@ pub struct AppImageMetadata {
pub app_version: Option<String>, pub app_version: Option<String>,
pub description: Option<String>, pub description: Option<String>,
pub developer: Option<String>, pub developer: Option<String>,
#[allow(dead_code)]
pub icon_name: Option<String>, pub icon_name: Option<String>,
pub categories: Vec<String>, pub categories: Vec<String>,
pub desktop_entry_content: String, pub desktop_entry_content: String,

View File

@@ -42,6 +42,7 @@ pub enum LaunchMethod {
/// Extract-and-run fallback (APPIMAGE_EXTRACT_AND_RUN=1) /// Extract-and-run fallback (APPIMAGE_EXTRACT_AND_RUN=1)
ExtractAndRun, ExtractAndRun,
/// Via firejail sandbox /// Via firejail sandbox
#[allow(dead_code)]
Sandboxed, Sandboxed,
} }
@@ -58,11 +59,18 @@ impl LaunchMethod {
/// Result of a launch attempt. /// Result of a launch attempt.
#[derive(Debug)] #[derive(Debug)]
pub enum LaunchResult { pub enum LaunchResult {
/// Successfully spawned the process. /// Successfully spawned the process and it's still running.
Started { Started {
child: Child, child: Child,
method: LaunchMethod, method: LaunchMethod,
}, },
/// Process spawned but crashed immediately (within ~1 second).
Crashed {
exit_code: Option<i32>,
stderr: String,
#[allow(dead_code)]
method: LaunchMethod,
},
/// Failed to launch. /// Failed to launch.
Failed(String), Failed(String),
} }
@@ -155,20 +163,56 @@ fn execute_appimage(
cmd.env(key, value); cmd.env(key, value);
} }
// Detach from our process group so the app runs independently // Capture stderr to detect crash messages, stdin detached
cmd.stdin(Stdio::null()); cmd.stdin(Stdio::null());
cmd.stderr(Stdio::piped());
match cmd.spawn() { match cmd.spawn() {
Ok(child) => LaunchResult::Started { Ok(mut child) => {
// Brief wait to detect immediate crashes (e.g. missing Qt plugins)
std::thread::sleep(std::time::Duration::from_millis(1500));
match child.try_wait() {
Ok(Some(status)) => {
// Process already exited - it crashed
let stderr = child
.stderr
.take()
.and_then(|mut err| {
let mut buf = String::new();
use std::io::Read;
err.read_to_string(&mut buf).ok()?;
Some(buf)
})
.unwrap_or_default();
LaunchResult::Crashed {
exit_code: status.code(),
stderr,
method: method.clone(),
}
}
Ok(None) => {
// Still running - success
LaunchResult::Started {
child, child,
method: method.clone(), method: method.clone(),
}, }
Err(e) => LaunchResult::Failed(format!("Failed to spawn process: {}", e)), }
Err(_) => {
// Can't check status, assume it's running
LaunchResult::Started {
child,
method: method.clone(),
}
}
}
}
Err(e) => LaunchResult::Failed(format!("Failed to start: {}", e)),
} }
} }
/// Parse a launch_args string from the database into a Vec of individual arguments. /// Parse a launch_args string from the database into a Vec of individual arguments.
/// Splits on whitespace; returns an empty Vec if the input is None or empty. /// Splits on whitespace; returns an empty Vec if the input is None or empty.
#[allow(dead_code)]
pub fn parse_launch_args(args: Option<&str>) -> Vec<String> { pub fn parse_launch_args(args: Option<&str>) -> Vec<String> {
args.map(|s| s.split_whitespace().map(String::from).collect()) args.map(|s| s.split_whitespace().map(String::from).collect())
.unwrap_or_default() .unwrap_or_default()

View File

@@ -1,3 +1,5 @@
#![allow(dead_code)]
use super::database::Database; use super::database::Database;
use super::security; use super::security;

View File

@@ -1,3 +1,5 @@
#![allow(dead_code)]
use super::database::{CveSummary, Database}; use super::database::{CveSummary, Database};
use crate::config::VERSION; use crate::config::VERSION;

View File

@@ -28,6 +28,7 @@ pub struct CveMatch {
/// Result of a security scan for a single AppImage. /// Result of a security scan for a single AppImage.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SecurityScanResult { pub struct SecurityScanResult {
#[allow(dead_code)]
pub appimage_id: i64, pub appimage_id: i64,
pub libraries: Vec<BundledLibrary>, pub libraries: Vec<BundledLibrary>,
pub cve_matches: Vec<(BundledLibrary, Vec<CveMatch>)>, pub cve_matches: Vec<(BundledLibrary, Vec<CveMatch>)>,

View File

@@ -390,6 +390,7 @@ fn extract_update_info_runtime(path: &Path) -> Option<String> {
// -- GitHub/GitLab API types for JSON deserialization -- // -- GitHub/GitLab API types for JSON deserialization --
#[derive(Deserialize)] #[derive(Deserialize)]
#[allow(dead_code)]
struct GhRelease { struct GhRelease {
tag_name: String, tag_name: String,
name: Option<String>, name: Option<String>,
@@ -405,6 +406,7 @@ struct GhAsset {
} }
#[derive(Deserialize)] #[derive(Deserialize)]
#[allow(dead_code)]
struct GlRelease { struct GlRelease {
tag_name: String, tag_name: String,
name: Option<String>, name: Option<String>,

View File

@@ -1,3 +1,5 @@
#![allow(dead_code)]
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::mpsc; use std::sync::mpsc;
use std::time::Duration; use std::time::Duration;

View File

@@ -307,9 +307,11 @@ pub fn detect_desktop_environment() -> String {
/// Result of analyzing a running process for Wayland usage. /// Result of analyzing a running process for Wayland usage.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RuntimeAnalysis { pub struct RuntimeAnalysis {
#[allow(dead_code)]
pub pid: u32, pub pid: u32,
pub has_wayland_socket: bool, pub has_wayland_socket: bool,
pub has_x11_connection: bool, pub has_x11_connection: bool,
#[allow(dead_code)]
pub env_vars: Vec<(String, String)>, pub env_vars: Vec<(String, String)>,
} }

View File

@@ -79,60 +79,68 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
]); ]);
let record_id = record.id; let record_id = record.id;
let path = record.path.clone(); let path = record.path.clone();
let app_name_launch = record.app_name.clone().unwrap_or_else(|| record.filename.clone());
let db_launch = db.clone(); let db_launch = db.clone();
launch_button.connect_clicked(move |_| { let toast_launch = toast_overlay.clone();
let appimage_path = std::path::Path::new(&path); launch_button.connect_clicked(move |btn| {
let result = launcher::launch_appimage( btn.set_sensitive(false);
&db_launch, 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, record_id,
appimage_path, appimage_path,
"gui_detail", "gui_detail",
&[], &[],
&[], &[],
); )
}).await;
btn_ref.set_sensitive(true);
match result { match result {
launcher::LaunchResult::Started { child, method } => { Ok(launcher::LaunchResult::Started { child, method }) => {
let pid = child.id(); let pid = child.id();
log::info!("Launched AppImage: {} (PID: {}, method: {})", path, pid, method.as_str()); log::info!("Launched: {} (PID: {}, method: {})", path, pid, method.as_str());
let db_wayland = db_launch.clone(); let db_wayland = db_launch.clone();
let path_clone = path.clone(); let path_clone = path.clone();
glib::spawn_future_local(async move { glib::spawn_future_local(async move {
glib::timeout_future(std::time::Duration::from_secs(3)).await; glib::timeout_future(std::time::Duration::from_secs(3)).await;
let analysis_result = gio::spawn_blocking(move || { let analysis_result = gio::spawn_blocking(move || {
wayland::analyze_running_process(pid) wayland::analyze_running_process(pid)
}).await; }).await;
if let Ok(Ok(analysis)) = analysis_result {
match analysis_result {
Ok(Ok(analysis)) => {
let status_label = analysis.status_label();
let status_str = analysis.as_status_str(); let status_str = analysis.as_status_str();
log::info!( log::info!("Runtime Wayland: {} -> {}", path_clone, analysis.status_label());
"Runtime Wayland analysis for {} (PID {}): {} (wayland_socket={}, x11={}, env_vars={})", db_wayland.update_runtime_wayland_status(record_id, status_str).ok();
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();
}
Ok(Err(e)) => {
log::debug!("Runtime analysis failed for PID {}: {}", pid, e);
}
Err(_) => {
log::debug!("Runtime analysis task failed for PID {}", pid);
}
} }
}); });
} }
launcher::LaunchResult::Failed(msg) => { 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); 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");
} }
} }
}); });
});
header.pack_end(&launch_button); header.pack_end(&launch_button);
// Check for Update button // Check for Update button
@@ -1025,7 +1033,7 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
match result { match result {
Ok(analysis) => { Ok(analysis) => {
let toolkit_label = analysis.toolkit.label(); 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!( row_clone.set_subtitle(&format!(
"Built with: {}", "Built with: {}",
toolkit_label, toolkit_label,
@@ -1062,13 +1070,10 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
compat_group.add(&runtime_row); 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_system = fuse::detect_system_fuse();
let fuse_status = record let fuse_status = fuse_system.status.clone();
.fuse_status
.as_deref()
.map(FuseStatus::from_str)
.unwrap_or(fuse_system.status.clone());
let fuse_row = adw::ActionRow::builder() let fuse_row = adw::ActionRow::builder()
.title("App mounting") .title("App mounting")
@@ -1824,3 +1829,4 @@ fn fetch_favicon_async(url: &str, image: &gtk::Image) {
} }
}); });
} }

View File

@@ -1,4 +1,4 @@
use gtk::prelude::*; use adw::prelude::*;
use std::sync::OnceLock; use std::sync::OnceLock;
/// Ensures the shared letter-icon CSS provider is registered on the default /// 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 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(&copy_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. /// Create a screen-reader live region announcement.
/// Inserts a hidden label with AccessibleRole::Alert into the given container, /// Inserts a hidden label with AccessibleRole::Alert into the given container,
/// which causes AT-SPI to announce the text to screen readers. /// which causes AT-SPI to announce the text to screen readers.

View File

@@ -24,6 +24,7 @@ use crate::ui::library_view::{LibraryState, LibraryView};
use crate::ui::preferences; use crate::ui::preferences;
use crate::ui::security_report; use crate::ui::security_report;
use crate::ui::update_dialog; use crate::ui::update_dialog;
use crate::ui::widgets;
mod imp { mod imp {
use super::*; use super::*;
@@ -588,18 +589,46 @@ impl DriftwoodWindow {
launch_action.connect_activate(move |_, param| { launch_action.connect_activate(move |_, param| {
let Some(window) = window_weak.upgrade() else { return }; let Some(window) = window_weak.upgrade() else { return };
let Some(record_id) = param.and_then(|p| p.get::<i64>()) else { return }; let Some(record_id) = param.and_then(|p| p.get::<i64>()) else { return };
let db = window.database().clone(); let toast_overlay = window.imp().toast_overlay.get().unwrap().clone();
if let Ok(Some(record)) = db.get_appimage_by_id(record_id) { let window_ref = window.clone();
let appimage_path = std::path::Path::new(&record.path); let (path_str, app_name) = {
match launcher::launch_appimage(&db, record_id, appimage_path, "gui_context", &[], &[]) { let db = window.database();
launcher::LaunchResult::Started { child, method } => { match db.get_appimage_by_id(record_id) {
log::info!("Context menu launched: {} (PID: {}, method: {})", record.path, child.id(), method.as_str()); Ok(Some(r)) => {
let name = r.app_name.clone().unwrap_or_else(|| r.filename.clone());
(r.path.clone(), name)
} }
launcher::LaunchResult::Failed(msg) => { _ => return,
}
};
glib::spawn_future_local(async move {
let path_bg = path_str.clone();
let result = gio::spawn_blocking(move || {
let bg_db = crate::core::database::Database::open().expect("DB open");
let appimage_path = std::path::Path::new(&path_bg);
launcher::launch_appimage(&bg_db, record_id, appimage_path, "gui_context", &[], &[])
}).await;
match result {
Ok(launcher::LaunchResult::Started { child, method }) => {
log::info!("Launched: {} (PID: {}, method: {})", path_str, child.id(), method.as_str());
}
Ok(launcher::LaunchResult::Crashed { exit_code, stderr, .. }) => {
log::error!("App crashed (exit {}): {}", exit_code.unwrap_or(-1), stderr);
widgets::show_crash_dialog(&window_ref, &app_name, exit_code, &stderr);
}
Ok(launcher::LaunchResult::Failed(msg)) => {
log::error!("Failed to launch: {}", msg); log::error!("Failed to launch: {}", msg);
let toast = adw::Toast::builder()
.title(&format!("Could not launch: {}", msg))
.timeout(5)
.build();
toast_overlay.add_toast(toast);
}
Err(_) => {
log::error!("Launch task panicked");
} }
} }
} });
}); });
} }
self.add_action(&launch_action); self.add_action(&launch_action);