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.
248 lines
7.3 KiB
Rust
248 lines
7.3 KiB
Rust
use std::path::Path;
|
|
use std::process::{Child, Command, Stdio};
|
|
|
|
use super::database::Database;
|
|
use super::fuse::{detect_system_fuse, determine_app_fuse_status, AppImageFuseStatus};
|
|
|
|
/// Sandbox mode for running AppImages.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum SandboxMode {
|
|
None,
|
|
Firejail,
|
|
}
|
|
|
|
impl SandboxMode {
|
|
pub fn from_str(s: &str) -> Self {
|
|
match s {
|
|
"firejail" => Self::Firejail,
|
|
_ => Self::None,
|
|
}
|
|
}
|
|
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Self::None => "none",
|
|
Self::Firejail => "firejail",
|
|
}
|
|
}
|
|
|
|
pub fn display_label(&self) -> &'static str {
|
|
match self {
|
|
Self::None => "None",
|
|
Self::Firejail => "Firejail",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Launch method used for the AppImage.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum LaunchMethod {
|
|
/// Direct execution via FUSE mount
|
|
Direct,
|
|
/// Extract-and-run fallback (APPIMAGE_EXTRACT_AND_RUN=1)
|
|
ExtractAndRun,
|
|
/// Via firejail sandbox
|
|
#[allow(dead_code)]
|
|
Sandboxed,
|
|
}
|
|
|
|
impl LaunchMethod {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Self::Direct => "direct",
|
|
Self::ExtractAndRun => "extract_and_run",
|
|
Self::Sandboxed => "sandboxed",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Result of a launch attempt.
|
|
#[derive(Debug)]
|
|
pub enum LaunchResult {
|
|
/// Successfully spawned the process and it's still running.
|
|
Started {
|
|
child: Child,
|
|
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(String),
|
|
}
|
|
|
|
/// Launch an AppImage, recording the event in the database.
|
|
/// Automatically selects the best launch method based on FUSE status.
|
|
pub fn launch_appimage(
|
|
db: &Database,
|
|
record_id: i64,
|
|
appimage_path: &Path,
|
|
source: &str,
|
|
extra_args: &[String],
|
|
extra_env: &[(&str, &str)],
|
|
) -> LaunchResult {
|
|
// Determine launch method based on FUSE status
|
|
let fuse_info = detect_system_fuse();
|
|
let fuse_status = determine_app_fuse_status(&fuse_info, appimage_path);
|
|
|
|
let method = match fuse_status {
|
|
AppImageFuseStatus::NativeFuse | AppImageFuseStatus::StaticRuntime => LaunchMethod::Direct,
|
|
AppImageFuseStatus::ExtractAndRun => LaunchMethod::ExtractAndRun,
|
|
AppImageFuseStatus::CannotLaunch => {
|
|
return LaunchResult::Failed(
|
|
"Cannot launch: FUSE is not available and extract-and-run is not supported".into(),
|
|
);
|
|
}
|
|
};
|
|
|
|
let result = execute_appimage(appimage_path, &method, extra_args, extra_env);
|
|
|
|
// Record the launch event regardless of success
|
|
if let Err(e) = db.record_launch(record_id, source) {
|
|
log::warn!("Failed to record launch event: {}", e);
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Launch an AppImage without database tracking (for standalone use).
|
|
pub fn launch_appimage_simple(
|
|
appimage_path: &Path,
|
|
extra_args: &[String],
|
|
) -> LaunchResult {
|
|
let fuse_info = detect_system_fuse();
|
|
let fuse_status = determine_app_fuse_status(&fuse_info, appimage_path);
|
|
|
|
let method = match fuse_status {
|
|
AppImageFuseStatus::NativeFuse | AppImageFuseStatus::StaticRuntime => LaunchMethod::Direct,
|
|
AppImageFuseStatus::ExtractAndRun => LaunchMethod::ExtractAndRun,
|
|
AppImageFuseStatus::CannotLaunch => {
|
|
return LaunchResult::Failed(
|
|
"Cannot launch: FUSE is not available and this AppImage doesn't support extract-and-run".into(),
|
|
);
|
|
}
|
|
};
|
|
|
|
execute_appimage(appimage_path, &method, extra_args, &[])
|
|
}
|
|
|
|
/// Execute the AppImage process with the given method.
|
|
fn execute_appimage(
|
|
appimage_path: &Path,
|
|
method: &LaunchMethod,
|
|
args: &[String],
|
|
extra_env: &[(&str, &str)],
|
|
) -> LaunchResult {
|
|
let mut cmd = match method {
|
|
LaunchMethod::Direct => {
|
|
let mut c = Command::new(appimage_path);
|
|
c.args(args);
|
|
c
|
|
}
|
|
LaunchMethod::ExtractAndRun => {
|
|
let mut c = Command::new(appimage_path);
|
|
c.env("APPIMAGE_EXTRACT_AND_RUN", "1");
|
|
c.args(args);
|
|
c
|
|
}
|
|
LaunchMethod::Sandboxed => {
|
|
let mut c = Command::new("firejail");
|
|
c.arg("--appimage");
|
|
c.arg(appimage_path);
|
|
c.args(args);
|
|
c
|
|
}
|
|
};
|
|
|
|
// Apply extra environment variables
|
|
for (key, value) in extra_env {
|
|
cmd.env(key, value);
|
|
}
|
|
|
|
// Capture stderr to detect crash messages, stdin detached
|
|
cmd.stdin(Stdio::null());
|
|
cmd.stderr(Stdio::piped());
|
|
|
|
match cmd.spawn() {
|
|
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,
|
|
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.
|
|
/// 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> {
|
|
args.map(|s| s.split_whitespace().map(String::from).collect())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Check if firejail is available for sandboxed launches.
|
|
pub fn has_firejail() -> bool {
|
|
Command::new("firejail")
|
|
.arg("--version")
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status()
|
|
.map(|s| s.success())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
/// Get launch statistics for an AppImage from the database.
|
|
#[derive(Debug, Clone)]
|
|
pub struct LaunchStats {
|
|
pub total_launches: u64,
|
|
pub last_launched: Option<String>,
|
|
}
|
|
|
|
pub fn get_launch_stats(db: &Database, record_id: i64) -> LaunchStats {
|
|
let total_launches = db.get_launch_count(record_id).unwrap_or(0) as u64;
|
|
let last_launched = db.get_last_launched(record_id).unwrap_or(None);
|
|
|
|
LaunchStats {
|
|
total_launches,
|
|
last_launched,
|
|
}
|
|
}
|