Pull Snap process managment into its own entry kind
This commit is contained in:
parent
b179232a21
commit
252ee4dd8d
9 changed files with 62 additions and 43 deletions
|
|
@ -59,14 +59,16 @@ daily_quota_seconds = 7200 # 2 hours per day
|
||||||
cooldown_seconds = 300 # 5 minute cooldown after each session
|
cooldown_seconds = 300 # 5 minute cooldown after each session
|
||||||
|
|
||||||
# Example: Minecraft (via snap mc-installer)
|
# Example: Minecraft (via snap mc-installer)
|
||||||
|
# Uses the special "snap" entry type for proper process management
|
||||||
[[entries]]
|
[[entries]]
|
||||||
id = "minecraft"
|
id = "minecraft"
|
||||||
label = "Minecraft"
|
label = "Minecraft"
|
||||||
icon = "minecraft"
|
icon = "minecraft"
|
||||||
|
|
||||||
[entries.kind]
|
[entries.kind]
|
||||||
type = "process"
|
type = "snap"
|
||||||
argv = ["mc-installer"]
|
snap_name = "mc-installer"
|
||||||
|
# command = "mc-installer" # Optional: defaults to snap_name
|
||||||
|
|
||||||
[entries.availability]
|
[entries.availability]
|
||||||
always = true # No time restrictions
|
always = true # No time restrictions
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use std::time::Duration;
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum EntryKindTag {
|
pub enum EntryKindTag {
|
||||||
Process,
|
Process,
|
||||||
|
Snap,
|
||||||
Vm,
|
Vm,
|
||||||
Media,
|
Media,
|
||||||
Custom,
|
Custom,
|
||||||
|
|
@ -27,6 +28,17 @@ pub enum EntryKind {
|
||||||
env: HashMap<String, String>,
|
env: HashMap<String, String>,
|
||||||
cwd: Option<PathBuf>,
|
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 {
|
Vm {
|
||||||
driver: String,
|
driver: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
@ -47,6 +59,7 @@ impl EntryKind {
|
||||||
pub fn tag(&self) -> EntryKindTag {
|
pub fn tag(&self) -> EntryKindTag {
|
||||||
match self {
|
match self {
|
||||||
EntryKind::Process { .. } => EntryKindTag::Process,
|
EntryKind::Process { .. } => EntryKindTag::Process,
|
||||||
|
EntryKind::Snap { .. } => EntryKindTag::Snap,
|
||||||
EntryKind::Vm { .. } => EntryKindTag::Vm,
|
EntryKind::Vm { .. } => EntryKindTag::Vm,
|
||||||
EntryKind::Media { .. } => EntryKindTag::Media,
|
EntryKind::Media { .. } => EntryKindTag::Media,
|
||||||
EntryKind::Custom { .. } => EntryKindTag::Custom,
|
EntryKind::Custom { .. } => EntryKindTag::Custom,
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,7 @@ pub struct LimitsPolicy {
|
||||||
fn convert_entry_kind(raw: RawEntryKind) -> EntryKind {
|
fn convert_entry_kind(raw: RawEntryKind) -> EntryKind {
|
||||||
match raw {
|
match raw {
|
||||||
RawEntryKind::Process { argv, env, cwd } => EntryKind::Process { argv, env, cwd },
|
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::Vm { driver, args } => EntryKind::Vm { driver, args },
|
||||||
RawEntryKind::Media { library_id, args } => EntryKind::Media { library_id, args },
|
RawEntryKind::Media { library_id, args } => EntryKind::Media { library_id, args },
|
||||||
RawEntryKind::Custom { type_name, payload } => EntryKind::Custom {
|
RawEntryKind::Custom { type_name, payload } => EntryKind::Custom {
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,16 @@ pub enum RawEntryKind {
|
||||||
env: HashMap<String, String>,
|
env: HashMap<String, String>,
|
||||||
cwd: Option<PathBuf>,
|
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 {
|
Vm {
|
||||||
driver: String,
|
driver: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
|
||||||
|
|
@ -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, .. } => {
|
RawEntryKind::Vm { driver, .. } => {
|
||||||
if driver.is_empty() {
|
if driver.is_empty() {
|
||||||
errors.push(ValidationError::EntryError {
|
errors.push(ValidationError::EntryError {
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ impl HostCapabilities {
|
||||||
pub fn linux_full() -> Self {
|
pub fn linux_full() -> Self {
|
||||||
let mut spawn_kinds = HashSet::new();
|
let mut spawn_kinds = HashSet::new();
|
||||||
spawn_kinds.insert(EntryKindTag::Process);
|
spawn_kinds.insert(EntryKindTag::Process);
|
||||||
|
spawn_kinds.insert(EntryKindTag::Snap);
|
||||||
spawn_kinds.insert(EntryKindTag::Vm);
|
spawn_kinds.insert(EntryKindTag::Vm);
|
||||||
spawn_kinds.insert(EntryKindTag::Media);
|
spawn_kinds.insert(EntryKindTag::Media);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,8 +113,17 @@ impl HostAdapter for LinuxHost {
|
||||||
entry_kind: &EntryKind,
|
entry_kind: &EntryKind,
|
||||||
options: SpawnOptions,
|
options: SpawnOptions,
|
||||||
) -> HostResult<HostSessionHandle> {
|
) -> HostResult<HostSessionHandle> {
|
||||||
let (argv, env, cwd) = match entry_kind {
|
// Extract argv, env, cwd, and snap_name based on entry kind
|
||||||
EntryKind::Process { argv, env, cwd } => (argv.clone(), env.clone(), cwd.clone()),
|
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 } => {
|
EntryKind::Vm { driver, args } => {
|
||||||
// Construct command line from VM driver
|
// Construct command line from VM driver
|
||||||
let mut argv = vec![driver.clone()];
|
let mut argv = vec![driver.clone()];
|
||||||
|
|
@ -126,15 +135,15 @@ impl HostAdapter for LinuxHost {
|
||||||
argv.push(value.to_string());
|
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
|
// For media, we'd typically launch a media player
|
||||||
// This is a placeholder - real implementation would integrate with a player
|
// This is a placeholder - real implementation would integrate with a player
|
||||||
let mut argv = vec!["xdg-open".to_string(), library_id.clone()];
|
let argv = vec!["xdg-open".to_string(), library_id.clone()];
|
||||||
(argv, HashMap::new(), None)
|
(argv, HashMap::new(), None, None)
|
||||||
}
|
}
|
||||||
EntryKind::Custom { type_name, payload } => {
|
EntryKind::Custom { type_name: _, payload: _ } => {
|
||||||
return Err(HostError::UnsupportedKind);
|
return Err(HostError::UnsupportedKind);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -147,11 +156,11 @@ impl HostAdapter for LinuxHost {
|
||||||
&env,
|
&env,
|
||||||
cwd.as_ref(),
|
cwd.as_ref(),
|
||||||
options.capture_stdout || options.capture_stderr,
|
options.capture_stdout || options.capture_stderr,
|
||||||
|
snap_name.clone(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let pid = proc.pid;
|
let pid = proc.pid;
|
||||||
let pgid = proc.pgid;
|
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
|
// Store the session info so we can use it for killing even after process exits
|
||||||
let session_info_entry = SessionInfo {
|
let session_info_entry = SessionInfo {
|
||||||
|
|
|
||||||
|
|
@ -9,32 +9,6 @@ use tracing::{debug, info, warn};
|
||||||
|
|
||||||
use shepherd_host_api::{ExitStatus, HostError, HostResult};
|
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
|
/// Managed child process with process group tracking
|
||||||
pub struct ManagedProcess {
|
pub struct ManagedProcess {
|
||||||
pub child: Child,
|
pub child: Child,
|
||||||
|
|
@ -149,11 +123,15 @@ pub fn kill_by_command(command_name: &str, signal: Signal) -> bool {
|
||||||
|
|
||||||
impl ManagedProcess {
|
impl ManagedProcess {
|
||||||
/// Spawn a new process in its own process group
|
/// 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(
|
pub fn spawn(
|
||||||
argv: &[String],
|
argv: &[String],
|
||||||
env: &HashMap<String, String>,
|
env: &HashMap<String, String>,
|
||||||
cwd: Option<&std::path::PathBuf>,
|
cwd: Option<&std::path::PathBuf>,
|
||||||
capture_output: bool,
|
capture_output: bool,
|
||||||
|
snap_name: Option<String>,
|
||||||
) -> HostResult<Self> {
|
) -> HostResult<Self> {
|
||||||
if argv.is_empty() {
|
if argv.is_empty() {
|
||||||
return Err(HostError::SpawnFailed("Empty argv".into()));
|
return Err(HostError::SpawnFailed("Empty argv".into()));
|
||||||
|
|
@ -293,10 +271,6 @@ impl ManagedProcess {
|
||||||
|
|
||||||
let pid = child.id();
|
let pid = child.id();
|
||||||
let pgid = pid; // After setsid, pid == pgid
|
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");
|
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 argv = vec!["true".to_string()];
|
||||||
let env = HashMap::new();
|
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
|
// Wait for it to complete
|
||||||
let status = proc.wait().unwrap();
|
let status = proc.wait().unwrap();
|
||||||
|
|
@ -496,7 +470,7 @@ mod tests {
|
||||||
let argv = vec!["echo".to_string(), "hello".to_string()];
|
let argv = vec!["echo".to_string(), "hello".to_string()];
|
||||||
let env = HashMap::new();
|
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();
|
let status = proc.wait().unwrap();
|
||||||
assert!(status.is_success());
|
assert!(status.is_success());
|
||||||
}
|
}
|
||||||
|
|
@ -506,7 +480,7 @@ mod tests {
|
||||||
let argv = vec!["sleep".to_string(), "60".to_string()];
|
let argv = vec!["sleep".to_string(), "60".to_string()];
|
||||||
let env = HashMap::new();
|
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
|
// Give it a moment to start
|
||||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ impl LauncherTile {
|
||||||
// Default icon based on entry kind
|
// Default icon based on entry kind
|
||||||
let icon_name = match entry.kind_tag {
|
let icon_name = match entry.kind_tag {
|
||||||
shepherd_api::EntryKindTag::Process => "application-x-executable",
|
shepherd_api::EntryKindTag::Process => "application-x-executable",
|
||||||
|
shepherd_api::EntryKindTag::Snap => "application-x-executable",
|
||||||
shepherd_api::EntryKindTag::Vm => "computer",
|
shepherd_api::EntryKindTag::Vm => "computer",
|
||||||
shepherd_api::EntryKindTag::Media => "video-x-generic",
|
shepherd_api::EntryKindTag::Media => "video-x-generic",
|
||||||
shepherd_api::EntryKindTag::Custom => "applications-other",
|
shepherd_api::EntryKindTag::Custom => "applications-other",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue