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, 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 { 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, } 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, } }