Pull Snap process managment into its own entry kind

This commit is contained in:
Albert Armea 2025-12-27 14:27:34 -05:00
parent b179232a21
commit 252ee4dd8d
9 changed files with 62 additions and 43 deletions

View file

@ -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

View file

@ -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<String, String>,
cwd: Option<PathBuf>,
},
/// 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<String>,
/// Additional environment variables
#[serde(default)]
env: HashMap<String, String>,
},
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,

View file

@ -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 {

View file

@ -83,6 +83,16 @@ pub enum RawEntryKind {
env: HashMap<String, String>,
cwd: Option<PathBuf>,
},
/// 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<String>,
/// Additional environment variables
#[serde(default)]
env: HashMap<String, String>,
},
Vm {
driver: String,
#[serde(default)]

View file

@ -63,6 +63,14 @@ fn validate_entry(entry: &RawEntry, config: &RawConfig) -> Vec<ValidationError>
});
}
}
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 {

View file

@ -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);

View file

@ -113,8 +113,17 @@ impl HostAdapter for LinuxHost {
entry_kind: &EntryKind,
options: SpawnOptions,
) -> HostResult<HostSessionHandle> {
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 {

View file

@ -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<String> {
// Check if it's a path starting with /snap/
if program.starts_with("/snap/") {
// Format: /snap/<snap-name>/<revision>/...
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<String, String>,
cwd: Option<&std::path::PathBuf>,
capture_output: bool,
snap_name: Option<String>,
) -> HostResult<Self> {
if argv.is_empty() {
return Err(HostError::SpawnFailed("Empty argv".into()));
@ -294,10 +272,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/<snap-name>/... 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");
Ok(Self { child, pid, pgid, command_name, snap_name })
@ -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));

View file

@ -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",