Add launch crash detection with detailed error dialog, fix all warnings
This commit is contained in:
16
src/cli.rs
16
src/cli.rs
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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('&', "&")
|
s.replace('&', "&")
|
||||||
.replace('<', "<")
|
.replace('<', "<")
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -181,6 +181,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,
|
||||||
@@ -192,6 +193,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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) => {
|
||||||
child,
|
// Brief wait to detect immediate crashes (e.g. missing Qt plugins)
|
||||||
method: method.clone(),
|
std::thread::sleep(std::time::Duration::from_millis(1500));
|
||||||
},
|
match child.try_wait() {
|
||||||
Err(e) => LaunchResult::Failed(format!("Failed to spawn process: {}", e)),
|
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,
|
||||||
|
method: method.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use super::database::Database;
|
use super::database::Database;
|
||||||
use super::security;
|
use super::security;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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>)>,
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,59 +79,67 @@ 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();
|
||||||
record_id,
|
let path = path.clone();
|
||||||
appimage_path,
|
let app_name = app_name_launch.clone();
|
||||||
"gui_detail",
|
let db_launch = db_launch.clone();
|
||||||
&[],
|
let toast_ref = toast_launch.clone();
|
||||||
&[],
|
glib::spawn_future_local(async move {
|
||||||
);
|
let path_bg = path.clone();
|
||||||
match result {
|
let result = gio::spawn_blocking(move || {
|
||||||
launcher::LaunchResult::Started { child, method } => {
|
let appimage_path = std::path::Path::new(&path_bg);
|
||||||
let pid = child.id();
|
launcher::launch_appimage(
|
||||||
log::info!("Launched AppImage: {} (PID: {}, method: {})", path, pid, method.as_str());
|
&Database::open().expect("DB open"),
|
||||||
|
record_id,
|
||||||
|
appimage_path,
|
||||||
|
"gui_detail",
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
}).await;
|
||||||
|
|
||||||
let db_wayland = db_launch.clone();
|
btn_ref.set_sensitive(true);
|
||||||
let path_clone = path.clone();
|
match result {
|
||||||
glib::spawn_future_local(async move {
|
Ok(launcher::LaunchResult::Started { child, method }) => {
|
||||||
glib::timeout_future(std::time::Duration::from_secs(3)).await;
|
let pid = child.id();
|
||||||
|
log::info!("Launched: {} (PID: {}, method: {})", path, pid, method.as_str());
|
||||||
|
|
||||||
let analysis_result = gio::spawn_blocking(move || {
|
let db_wayland = db_launch.clone();
|
||||||
wayland::analyze_running_process(pid)
|
let path_clone = path.clone();
|
||||||
}).await;
|
glib::spawn_future_local(async move {
|
||||||
|
glib::timeout_future(std::time::Duration::from_secs(3)).await;
|
||||||
match analysis_result {
|
let analysis_result = gio::spawn_blocking(move || {
|
||||||
Ok(Ok(analysis)) => {
|
wayland::analyze_running_process(pid)
|
||||||
let status_label = analysis.status_label();
|
}).await;
|
||||||
|
if let Ok(Ok(analysis)) = analysis_result {
|
||||||
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);
|
}
|
||||||
}
|
Ok(launcher::LaunchResult::Crashed { exit_code, stderr, .. }) => {
|
||||||
Err(_) => {
|
log::error!("App crashed on launch (exit {}): {}", exit_code.unwrap_or(-1), stderr);
|
||||||
log::debug!("Runtime analysis task failed for PID {}", pid);
|
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);
|
header.pack_end(&launch_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: >k::Image) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(©_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.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user