From 252ee4dd8d488b778022258392b2a22184fcd75c Mon Sep 17 00:00:00 2001 From: Albert Armea Date: Sat, 27 Dec 2025 14:27:34 -0500 Subject: [PATCH] Pull Snap process managment into its own entry kind --- config.example.toml | 6 ++- crates/shepherd-api/src/types.rs | 13 +++++++ crates/shepherd-config/src/policy.rs | 1 + crates/shepherd-config/src/schema.rs | 10 +++++ crates/shepherd-config/src/validation.rs | 8 ++++ crates/shepherd-host-api/src/capabilities.rs | 1 + crates/shepherd-host-linux/src/adapter.rs | 25 ++++++++---- crates/shepherd-host-linux/src/process.rs | 40 ++++---------------- crates/shepherd-launcher-ui/src/tile.rs | 1 + 9 files changed, 62 insertions(+), 43 deletions(-) diff --git a/config.example.toml b/config.example.toml index 046a97d..66d1d2e 100644 --- a/config.example.toml +++ b/config.example.toml @@ -59,14 +59,16 @@ daily_quota_seconds = 7200 # 2 hours per day cooldown_seconds = 300 # 5 minute cooldown after each session # Example: Minecraft (via snap mc-installer) +# Uses the special "snap" entry type for proper process management [[entries]] id = "minecraft" label = "Minecraft" icon = "minecraft" [entries.kind] -type = "process" -argv = ["mc-installer"] +type = "snap" +snap_name = "mc-installer" +# command = "mc-installer" # Optional: defaults to snap_name [entries.availability] always = true # No time restrictions diff --git a/crates/shepherd-api/src/types.rs b/crates/shepherd-api/src/types.rs index 9895bf5..1dd60b3 100644 --- a/crates/shepherd-api/src/types.rs +++ b/crates/shepherd-api/src/types.rs @@ -12,6 +12,7 @@ use std::time::Duration; #[serde(rename_all = "snake_case")] pub enum EntryKindTag { Process, + Snap, Vm, Media, Custom, @@ -27,6 +28,17 @@ pub enum EntryKind { env: HashMap, cwd: Option, }, + /// Snap application - uses systemd scope-based process management + Snap { + /// The snap name (e.g., "mc-installer") + snap_name: String, + /// Command to run (defaults to snap_name if not specified) + #[serde(default)] + command: Option, + /// Additional environment variables + #[serde(default)] + env: HashMap, + }, Vm { driver: String, #[serde(default)] @@ -47,6 +59,7 @@ impl EntryKind { pub fn tag(&self) -> EntryKindTag { match self { EntryKind::Process { .. } => EntryKindTag::Process, + EntryKind::Snap { .. } => EntryKindTag::Snap, EntryKind::Vm { .. } => EntryKindTag::Vm, EntryKind::Media { .. } => EntryKindTag::Media, EntryKind::Custom { .. } => EntryKindTag::Custom, diff --git a/crates/shepherd-config/src/policy.rs b/crates/shepherd-config/src/policy.rs index 3e1202c..4a80fe6 100644 --- a/crates/shepherd-config/src/policy.rs +++ b/crates/shepherd-config/src/policy.rs @@ -192,6 +192,7 @@ pub struct LimitsPolicy { fn convert_entry_kind(raw: RawEntryKind) -> EntryKind { match raw { RawEntryKind::Process { argv, env, cwd } => EntryKind::Process { argv, env, cwd }, + RawEntryKind::Snap { snap_name, command, env } => EntryKind::Snap { snap_name, command, env }, RawEntryKind::Vm { driver, args } => EntryKind::Vm { driver, args }, RawEntryKind::Media { library_id, args } => EntryKind::Media { library_id, args }, RawEntryKind::Custom { type_name, payload } => EntryKind::Custom { diff --git a/crates/shepherd-config/src/schema.rs b/crates/shepherd-config/src/schema.rs index 03a1de4..5133c49 100644 --- a/crates/shepherd-config/src/schema.rs +++ b/crates/shepherd-config/src/schema.rs @@ -83,6 +83,16 @@ pub enum RawEntryKind { env: HashMap, cwd: Option, }, + /// Snap application - uses systemd scope-based process management + Snap { + /// The snap name (e.g., "mc-installer") + snap_name: String, + /// Command to run (defaults to snap_name if not specified) + command: Option, + /// Additional environment variables + #[serde(default)] + env: HashMap, + }, Vm { driver: String, #[serde(default)] diff --git a/crates/shepherd-config/src/validation.rs b/crates/shepherd-config/src/validation.rs index e337ed3..66493d8 100644 --- a/crates/shepherd-config/src/validation.rs +++ b/crates/shepherd-config/src/validation.rs @@ -63,6 +63,14 @@ fn validate_entry(entry: &RawEntry, config: &RawConfig) -> Vec }); } } + RawEntryKind::Snap { snap_name, .. } => { + if snap_name.is_empty() { + errors.push(ValidationError::EntryError { + entry_id: entry.id.clone(), + message: "snap_name cannot be empty".into(), + }); + } + } RawEntryKind::Vm { driver, .. } => { if driver.is_empty() { errors.push(ValidationError::EntryError { diff --git a/crates/shepherd-host-api/src/capabilities.rs b/crates/shepherd-host-api/src/capabilities.rs index 968e363..4d5ca22 100644 --- a/crates/shepherd-host-api/src/capabilities.rs +++ b/crates/shepherd-host-api/src/capabilities.rs @@ -58,6 +58,7 @@ impl HostCapabilities { pub fn linux_full() -> Self { let mut spawn_kinds = HashSet::new(); spawn_kinds.insert(EntryKindTag::Process); + spawn_kinds.insert(EntryKindTag::Snap); spawn_kinds.insert(EntryKindTag::Vm); spawn_kinds.insert(EntryKindTag::Media); diff --git a/crates/shepherd-host-linux/src/adapter.rs b/crates/shepherd-host-linux/src/adapter.rs index 66dbbe9..c609a8a 100644 --- a/crates/shepherd-host-linux/src/adapter.rs +++ b/crates/shepherd-host-linux/src/adapter.rs @@ -113,8 +113,17 @@ impl HostAdapter for LinuxHost { entry_kind: &EntryKind, options: SpawnOptions, ) -> HostResult { - let (argv, env, cwd) = match entry_kind { - EntryKind::Process { argv, env, cwd } => (argv.clone(), env.clone(), cwd.clone()), + // Extract argv, env, cwd, and snap_name based on entry kind + let (argv, env, cwd, snap_name) = match entry_kind { + EntryKind::Process { argv, env, cwd } => { + (argv.clone(), env.clone(), cwd.clone(), None) + } + EntryKind::Snap { snap_name, command, env } => { + // For snap apps, the command defaults to the snap name + let cmd = command.clone().unwrap_or_else(|| snap_name.clone()); + let argv = vec![cmd]; + (argv, env.clone(), None, Some(snap_name.clone())) + } EntryKind::Vm { driver, args } => { // Construct command line from VM driver let mut argv = vec![driver.clone()]; @@ -126,15 +135,15 @@ impl HostAdapter for LinuxHost { argv.push(value.to_string()); } } - (argv, HashMap::new(), None) + (argv, HashMap::new(), None, None) } - EntryKind::Media { library_id, args } => { + EntryKind::Media { library_id, args: _ } => { // For media, we'd typically launch a media player // This is a placeholder - real implementation would integrate with a player - let mut argv = vec!["xdg-open".to_string(), library_id.clone()]; - (argv, HashMap::new(), None) + let argv = vec!["xdg-open".to_string(), library_id.clone()]; + (argv, HashMap::new(), None, None) } - EntryKind::Custom { type_name, payload } => { + EntryKind::Custom { type_name: _, payload: _ } => { return Err(HostError::UnsupportedKind); } }; @@ -147,11 +156,11 @@ impl HostAdapter for LinuxHost { &env, cwd.as_ref(), options.capture_stdout || options.capture_stderr, + snap_name.clone(), )?; let pid = proc.pid; let pgid = proc.pgid; - let snap_name = proc.snap_name.clone(); // Store the session info so we can use it for killing even after process exits let session_info_entry = SessionInfo { diff --git a/crates/shepherd-host-linux/src/process.rs b/crates/shepherd-host-linux/src/process.rs index bf3b9c5..d3c75c1 100644 --- a/crates/shepherd-host-linux/src/process.rs +++ b/crates/shepherd-host-linux/src/process.rs @@ -9,32 +9,6 @@ use tracing::{debug, info, warn}; use shepherd_host_api::{ExitStatus, HostError, HostResult}; -/// Extract the snap name from a command path -/// Examples: -/// - "/snap/mc-installer/279/bin/mc-installer" -> Some("mc-installer") -/// - "mc-installer" (if it's a snap) -> Some("mc-installer") -/// - "/usr/bin/firefox" -> None -fn extract_snap_name(program: &str) -> Option { - // Check if it's a path starting with /snap/ - if program.starts_with("/snap/") { - // Format: /snap///... - let parts: Vec<&str> = program.split('/').collect(); - if parts.len() >= 3 { - return Some(parts[2].to_string()); - } - } - - // Check if it looks like a snap command (no path, and we can verify via snap path) - if !program.contains('/') { - let snap_path = format!("/snap/bin/{}", program); - if std::path::Path::new(&snap_path).exists() { - return Some(program.to_string()); - } - } - - None -} - /// Managed child process with process group tracking pub struct ManagedProcess { pub child: Child, @@ -149,11 +123,15 @@ pub fn kill_by_command(command_name: &str, signal: Signal) -> bool { impl ManagedProcess { /// Spawn a new process in its own process group + /// + /// If `snap_name` is provided, the process is treated as a snap app and will use + /// systemd scope-based killing instead of signal-based killing. pub fn spawn( argv: &[String], env: &HashMap, cwd: Option<&std::path::PathBuf>, capture_output: bool, + snap_name: Option, ) -> HostResult { if argv.is_empty() { return Err(HostError::SpawnFailed("Empty argv".into())); @@ -293,10 +271,6 @@ impl ManagedProcess { let pid = child.id(); let pgid = pid; // After setsid, pid == pgid - - // Extract snap name from command if it's a snap app - // Format: /snap//... or just the snap command name - let snap_name = extract_snap_name(program); info!(pid = pid, pgid = pgid, program = %program, snap = ?snap_name, "Process spawned"); @@ -484,7 +458,7 @@ mod tests { let argv = vec!["true".to_string()]; let env = HashMap::new(); - let mut proc = ManagedProcess::spawn(&argv, &env, None, false).unwrap(); + let mut proc = ManagedProcess::spawn(&argv, &env, None, false, None).unwrap(); // Wait for it to complete let status = proc.wait().unwrap(); @@ -496,7 +470,7 @@ mod tests { let argv = vec!["echo".to_string(), "hello".to_string()]; let env = HashMap::new(); - let mut proc = ManagedProcess::spawn(&argv, &env, None, false).unwrap(); + let mut proc = ManagedProcess::spawn(&argv, &env, None, false, None).unwrap(); let status = proc.wait().unwrap(); assert!(status.is_success()); } @@ -506,7 +480,7 @@ mod tests { let argv = vec!["sleep".to_string(), "60".to_string()]; let env = HashMap::new(); - let proc = ManagedProcess::spawn(&argv, &env, None, false).unwrap(); + let proc = ManagedProcess::spawn(&argv, &env, None, false, None).unwrap(); // Give it a moment to start std::thread::sleep(std::time::Duration::from_millis(50)); diff --git a/crates/shepherd-launcher-ui/src/tile.rs b/crates/shepherd-launcher-ui/src/tile.rs index 63e6650..8c24332 100644 --- a/crates/shepherd-launcher-ui/src/tile.rs +++ b/crates/shepherd-launcher-ui/src/tile.rs @@ -82,6 +82,7 @@ impl LauncherTile { // Default icon based on entry kind let icon_name = match entry.kind_tag { shepherd_api::EntryKindTag::Process => "application-x-executable", + shepherd_api::EntryKindTag::Snap => "application-x-executable", shepherd_api::EntryKindTag::Vm => "computer", shepherd_api::EntryKindTag::Media => "video-x-generic", shepherd_api::EntryKindTag::Custom => "applications-other",