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