Implement Driftwood AppImage manager - Phases 1 and 2
Phase 1 - Application scaffolding: - GTK4/libadwaita application window with AdwNavigationView - GSettings-backed window state persistence - GResource-compiled CSS and schema - Library view with grid/list toggle, search, sorting, filtering - Detail view with file info, desktop integration controls - Preferences window with scan directories, theme, behavior settings - CLI with list, scan, integrate, remove, clean, inspect commands - AppImage discovery, metadata extraction, desktop integration - Orphaned desktop entry detection and cleanup - AppImage packaging script Phase 2 - Intelligence layer: - Database schema v2 with migration for status tracking columns - FUSE detection engine (libfuse2/3, fusermount, /dev/fuse, AppImageLauncher) - Wayland awareness engine (session type, toolkit detection, XWayland) - Update info parsing from AppImage ELF sections (.upd_info) - GitHub/GitLab Releases API integration for update checking - Update download with progress tracking and atomic apply - Launch wrapper with FUSE auto-detection and usage tracking - Duplicate and multi-version detection with recommendations - Dashboard with system health, library stats, disk usage - Update check dialog (single and batch) - Duplicate resolution dialog - Status badges on library cards and detail view - Extended CLI: status, check-updates, duplicates, launch commands 49 tests passing across all modules.
This commit is contained in:
166
src/core/launcher.rs
Normal file
166
src/core/launcher.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
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};
|
||||
|
||||
/// 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 {
|
||||
/// Successfully spawned the process.
|
||||
Started {
|
||||
child: Child,
|
||||
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);
|
||||
}
|
||||
|
||||
// Detach from our process group so the app runs independently
|
||||
cmd.stdin(Stdio::null());
|
||||
|
||||
match cmd.spawn() {
|
||||
Ok(child) => LaunchResult::Started {
|
||||
child,
|
||||
method: method.clone(),
|
||||
},
|
||||
Err(e) => LaunchResult::Failed(format!("Failed to spawn process: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user