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 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 { /// Process spawned and survived the startup crash-check window. Started { pid: u32, method: LaunchMethod, }, /// Process spawned but exited during the startup crash-check window. Crashed { exit_code: Option, stderr: String, method: LaunchMethod, }, /// Failed to launch (binary not found, permission denied, etc.). 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(), ); } }; // Override with sandboxed launch if the user enabled firejail for this app let method = if has_firejail() { let sandbox = db .get_appimage_by_id(record_id) .ok() .flatten() .and_then(|r| r.sandbox_mode); if sandbox.as_deref() == Some("firejail") { LaunchMethod::Sandboxed } else { method } } else { method }; 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); } // Detach stdin, pipe stderr so we can capture crash messages cmd.stdin(Stdio::null()); cmd.stderr(Stdio::piped()); match cmd.spawn() { Ok(mut child) => { let pid = child.id(); // Monitor for early crash (2s window). This blocks the current // thread, so callers should run this inside gio::spawn_blocking. match check_early_crash(&mut child, std::time::Duration::from_secs(2)) { Some((exit_code, stderr)) => { LaunchResult::Crashed { exit_code, stderr, method: method.clone(), } } None => { LaunchResult::Started { pid, method: method.clone(), } } } } Err(e) => LaunchResult::Failed(format!("Failed to start: {}", e)), } } /// Check if a recently-launched child process crashed during startup. /// Waits up to `timeout` for the process to exit. If it exits within that window, /// reads stderr and returns a Crashed result. If still running, drops the stderr /// pipe (to prevent pipe buffer deadlock) and returns None. /// /// Call this from a background thread after spawning the process. pub fn check_early_crash( child: &mut Child, timeout: std::time::Duration, ) -> Option<(Option, String)> { let start = std::time::Instant::now(); loop { match child.try_wait() { Ok(Some(status)) => { // Process exited - read stderr for crash details let stderr_text = child.stderr.take().map(|mut pipe| { let mut buf = String::new(); use std::io::Read; let mut limited = (&mut pipe).take(64 * 1024); let _ = limited.read_to_string(&mut buf); buf }).unwrap_or_default(); return Some((status.code(), stderr_text)); } Ok(None) => { if start.elapsed() >= timeout { // Still running - drop stderr pipe to avoid deadlock drop(child.stderr.take()); return None; } std::thread::sleep(std::time::Duration::from_millis(50)); } Err(_) => return None, } } } /// Parse launch arguments with basic quote support. /// Splits on whitespace, respecting double-quoted strings. /// Returns an empty Vec if the input is None or empty. pub fn parse_launch_args(args: Option<&str>) -> Vec { let Some(s) = args else { return Vec::new(); }; let s = s.trim(); if s.is_empty() { return Vec::new(); } let mut result = Vec::new(); let mut current = String::new(); let mut in_quotes = false; for c in s.chars() { match c { '"' => in_quotes = !in_quotes, ' ' | '\t' if !in_quotes => { if !current.is_empty() { result.push(std::mem::take(&mut current)); } } _ => current.push(c), } } if !current.is_empty() { result.push(current); } result } /// 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, } 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, } }