309 lines
9.0 KiB
Rust
309 lines
9.0 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
|
|
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<i32>,
|
|
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<i32>, 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<String> {
|
|
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<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,
|
|
}
|
|
}
|