Implement Flatpak application type
This commit is contained in:
parent
b98fbc598f
commit
d823ed0a19
11 changed files with 216 additions and 68 deletions
16
README.md
16
README.md
|
|
@ -104,16 +104,16 @@ All behavior shown above is driven entirely by declarative configuration.
|
||||||
For the Minecraft example shown above:
|
For the Minecraft example shown above:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# Minecraft via mc-installer snap
|
# Prism Launcher - Minecraft launcher (Flatpak)
|
||||||
# Ubuntu: sudo snap install mc-installer
|
# Install: flatpak install flathub org.prismlauncher.PrismLauncher
|
||||||
[[entries]]
|
[[entries]]
|
||||||
id = "minecraft"
|
id = "prism-launcher"
|
||||||
label = "Minecraft"
|
label = "Prism Launcher"
|
||||||
icon = "minecraft"
|
icon = "org.prismlauncher.PrismLauncher"
|
||||||
|
|
||||||
[entries.kind]
|
[entries.kind]
|
||||||
type = "snap"
|
type = "flatpak"
|
||||||
snap_name = "mc-installer"
|
app_id = "org.prismlauncher.PrismLauncher"
|
||||||
|
|
||||||
[entries.availability]
|
[entries.availability]
|
||||||
[[entries.availability.windows]]
|
[[entries.availability.windows]]
|
||||||
|
|
@ -160,7 +160,7 @@ compatibility infrastructure:
|
||||||
|
|
||||||
* Wayland and Sway
|
* Wayland and Sway
|
||||||
* Rust
|
* Rust
|
||||||
* Snap
|
* Flatpak and Snap
|
||||||
* Proton and WINE
|
* Proton and WINE
|
||||||
|
|
||||||
This project was written with the assistance of generative AI-based coding
|
This project was written with the assistance of generative AI-based coding
|
||||||
|
|
|
||||||
|
|
@ -165,52 +165,6 @@ max_run_seconds = 0 # Unlimited
|
||||||
daily_quota_seconds = 0 # Unlimited
|
daily_quota_seconds = 0 # Unlimited
|
||||||
cooldown_seconds = 0 # No cooldown
|
cooldown_seconds = 0 # No cooldown
|
||||||
|
|
||||||
# Minecraft via mc-installer snap
|
|
||||||
# Ubuntu: sudo snap install mc-installer
|
|
||||||
[[entries]]
|
|
||||||
id = "minecraft"
|
|
||||||
label = "Minecraft"
|
|
||||||
icon = "~/.minecraft/launcher/icons/minecraft256.png"
|
|
||||||
|
|
||||||
[entries.kind]
|
|
||||||
type = "snap"
|
|
||||||
snap_name = "mc-installer"
|
|
||||||
|
|
||||||
[entries.availability]
|
|
||||||
[[entries.availability.windows]]
|
|
||||||
days = "weekdays"
|
|
||||||
start = "15:00"
|
|
||||||
end = "18:00"
|
|
||||||
|
|
||||||
[[entries.availability.windows]]
|
|
||||||
days = "weekends"
|
|
||||||
start = "10:00"
|
|
||||||
end = "20:00"
|
|
||||||
|
|
||||||
[entries.limits]
|
|
||||||
max_run_seconds = 1800 # 30 minutes (roughly 3 in-game days)
|
|
||||||
daily_quota_seconds = 3600 # 1 hour per day
|
|
||||||
cooldown_seconds = 600 # 10 minute cooldown
|
|
||||||
|
|
||||||
[[entries.warnings]]
|
|
||||||
seconds_before = 600
|
|
||||||
severity = "info"
|
|
||||||
message = "10 minutes left - start wrapping up!"
|
|
||||||
|
|
||||||
[[entries.warnings]]
|
|
||||||
seconds_before = 120
|
|
||||||
severity = "warn"
|
|
||||||
message = "2 minutes remaining - save your game!"
|
|
||||||
|
|
||||||
[[entries.warnings]]
|
|
||||||
seconds_before = 30
|
|
||||||
severity = "critical"
|
|
||||||
message = "30 seconds! Save NOW!"
|
|
||||||
|
|
||||||
# Entry-specific volume restrictions (overrides global)
|
|
||||||
[entries.volume]
|
|
||||||
max_volume = 60 # Limit volume during gaming sessions
|
|
||||||
|
|
||||||
## === Steam games ===
|
## === Steam games ===
|
||||||
# Steam can be used via Canonical's Steam snap package:
|
# Steam can be used via Canonical's Steam snap package:
|
||||||
# https://snapcraft.io/steam
|
# https://snapcraft.io/steam
|
||||||
|
|
@ -267,6 +221,76 @@ days = "weekends"
|
||||||
start = "10:00"
|
start = "10:00"
|
||||||
end = "20:00"
|
end = "20:00"
|
||||||
|
|
||||||
|
## === Flatpak-based applications ===
|
||||||
|
# Flatpak entries use the "flatpak" type for proper process management.
|
||||||
|
# Similar to Snap, Flatpak apps run in sandboxed environments and use
|
||||||
|
# systemd scopes for process management.
|
||||||
|
|
||||||
|
# Prism Launcher - Minecraft launcher (Flatpak)
|
||||||
|
# Install: flatpak install flathub org.prismlauncher.PrismLauncher
|
||||||
|
[[entries]]
|
||||||
|
id = "prism-launcher"
|
||||||
|
label = "Prism Launcher"
|
||||||
|
icon = "org.prismlauncher.PrismLauncher"
|
||||||
|
|
||||||
|
[entries.kind]
|
||||||
|
type = "flatpak"
|
||||||
|
app_id = "org.prismlauncher.PrismLauncher"
|
||||||
|
|
||||||
|
[entries.availability]
|
||||||
|
[[entries.availability.windows]]
|
||||||
|
days = "weekdays"
|
||||||
|
start = "15:00"
|
||||||
|
end = "18:00"
|
||||||
|
|
||||||
|
[[entries.availability.windows]]
|
||||||
|
days = "weekends"
|
||||||
|
start = "10:00"
|
||||||
|
end = "20:00"
|
||||||
|
|
||||||
|
[entries.limits]
|
||||||
|
max_run_seconds = 1800 # 30 minutes (roughly 3 in-game days)
|
||||||
|
daily_quota_seconds = 3600 # 1 hour per day
|
||||||
|
cooldown_seconds = 600 # 10 minute cooldown
|
||||||
|
|
||||||
|
[[entries.warnings]]
|
||||||
|
seconds_before = 600
|
||||||
|
severity = "info"
|
||||||
|
message = "10 minutes left - start wrapping up!"
|
||||||
|
|
||||||
|
[[entries.warnings]]
|
||||||
|
seconds_before = 120
|
||||||
|
severity = "warn"
|
||||||
|
message = "2 minutes remaining - save your game!"
|
||||||
|
|
||||||
|
[[entries.warnings]]
|
||||||
|
seconds_before = 30
|
||||||
|
severity = "critical"
|
||||||
|
message = "30 seconds! Save NOW!"
|
||||||
|
|
||||||
|
# Entry-specific volume restrictions (overrides global)
|
||||||
|
[entries.volume]
|
||||||
|
max_volume = 60 # Limit volume during gaming sessions
|
||||||
|
|
||||||
|
# Krita - digital painting (Flatpak)
|
||||||
|
# Install: flatpak install flathub org.kde.krita
|
||||||
|
[[entries]]
|
||||||
|
id = "krita"
|
||||||
|
label = "Krita"
|
||||||
|
icon = "org.kde.krita"
|
||||||
|
|
||||||
|
[entries.kind]
|
||||||
|
type = "flatpak"
|
||||||
|
app_id = "org.kde.krita"
|
||||||
|
|
||||||
|
[entries.availability]
|
||||||
|
always = true
|
||||||
|
|
||||||
|
[entries.limits]
|
||||||
|
max_run_seconds = 0 # Unlimited
|
||||||
|
daily_quota_seconds = 0 # Unlimited
|
||||||
|
cooldown_seconds = 0 # No cooldown
|
||||||
|
|
||||||
## === Media ===
|
## === Media ===
|
||||||
# Just use `mpv` to play media (for now).
|
# Just use `mpv` to play media (for now).
|
||||||
# Files can be local on your system or URLs (YouTube, etc).
|
# Files can be local on your system or URLs (YouTube, etc).
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use std::time::Duration;
|
||||||
pub enum EntryKindTag {
|
pub enum EntryKindTag {
|
||||||
Process,
|
Process,
|
||||||
Snap,
|
Snap,
|
||||||
|
Flatpak,
|
||||||
Vm,
|
Vm,
|
||||||
Media,
|
Media,
|
||||||
Custom,
|
Custom,
|
||||||
|
|
@ -46,6 +47,17 @@ pub enum EntryKind {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
env: HashMap<String, String>,
|
env: HashMap<String, String>,
|
||||||
},
|
},
|
||||||
|
/// Flatpak application - uses systemd scope-based process management
|
||||||
|
Flatpak {
|
||||||
|
/// The Flatpak application ID (e.g., "org.prismlauncher.PrismLauncher")
|
||||||
|
app_id: String,
|
||||||
|
/// Additional command-line arguments
|
||||||
|
#[serde(default)]
|
||||||
|
args: Vec<String>,
|
||||||
|
/// Additional environment variables
|
||||||
|
#[serde(default)]
|
||||||
|
env: HashMap<String, String>,
|
||||||
|
},
|
||||||
Vm {
|
Vm {
|
||||||
driver: String,
|
driver: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
@ -67,6 +79,7 @@ impl EntryKind {
|
||||||
match self {
|
match self {
|
||||||
EntryKind::Process { .. } => EntryKindTag::Process,
|
EntryKind::Process { .. } => EntryKindTag::Process,
|
||||||
EntryKind::Snap { .. } => EntryKindTag::Snap,
|
EntryKind::Snap { .. } => EntryKindTag::Snap,
|
||||||
|
EntryKind::Flatpak { .. } => EntryKindTag::Flatpak,
|
||||||
EntryKind::Vm { .. } => EntryKindTag::Vm,
|
EntryKind::Vm { .. } => EntryKindTag::Vm,
|
||||||
EntryKind::Media { .. } => EntryKindTag::Media,
|
EntryKind::Media { .. } => EntryKindTag::Media,
|
||||||
EntryKind::Custom { .. } => EntryKindTag::Custom,
|
EntryKind::Custom { .. } => EntryKindTag::Custom,
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,9 @@ fn main() -> ExitCode {
|
||||||
EntryKind::Snap { snap_name, .. } => {
|
EntryKind::Snap { snap_name, .. } => {
|
||||||
format!("snap ({})", snap_name)
|
format!("snap ({})", snap_name)
|
||||||
}
|
}
|
||||||
|
EntryKind::Flatpak { app_id, .. } => {
|
||||||
|
format!("flatpak ({})", app_id)
|
||||||
|
}
|
||||||
EntryKind::Vm { driver, .. } => {
|
EntryKind::Vm { driver, .. } => {
|
||||||
format!("vm ({})", driver)
|
format!("vm ({})", driver)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,7 @@ fn convert_entry_kind(raw: RawEntryKind) -> EntryKind {
|
||||||
match raw {
|
match raw {
|
||||||
RawEntryKind::Process { command, args, env, cwd } => EntryKind::Process { command, args, env, cwd },
|
RawEntryKind::Process { command, args, env, cwd } => EntryKind::Process { command, args, env, cwd },
|
||||||
RawEntryKind::Snap { snap_name, command, args, env } => EntryKind::Snap { snap_name, command, args, env },
|
RawEntryKind::Snap { snap_name, command, args, env } => EntryKind::Snap { snap_name, command, args, env },
|
||||||
|
RawEntryKind::Flatpak { app_id, args, env } => EntryKind::Flatpak { app_id, args, 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 {
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,17 @@ pub enum RawEntryKind {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
env: HashMap<String, String>,
|
env: HashMap<String, String>,
|
||||||
},
|
},
|
||||||
|
/// Flatpak application - uses systemd scope-based process management
|
||||||
|
Flatpak {
|
||||||
|
/// The Flatpak application ID (e.g., "org.prismlauncher.PrismLauncher")
|
||||||
|
app_id: String,
|
||||||
|
/// Additional command-line arguments
|
||||||
|
#[serde(default)]
|
||||||
|
args: Vec<String>,
|
||||||
|
/// Additional environment variables
|
||||||
|
#[serde(default)]
|
||||||
|
env: HashMap<String, String>,
|
||||||
|
},
|
||||||
Vm {
|
Vm {
|
||||||
driver: String,
|
driver: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,14 @@ fn validate_entry(entry: &RawEntry, config: &RawConfig) -> Vec<ValidationError>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
RawEntryKind::Flatpak { app_id, .. } => {
|
||||||
|
if app_id.is_empty() {
|
||||||
|
errors.push(ValidationError::EntryError {
|
||||||
|
entry_id: entry.id.clone(),
|
||||||
|
message: "app_id 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 {
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ impl HostCapabilities {
|
||||||
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::Snap);
|
||||||
|
spawn_kinds.insert(EntryKindTag::Flatpak);
|
||||||
spawn_kinds.insert(EntryKindTag::Vm);
|
spawn_kinds.insert(EntryKindTag::Vm);
|
||||||
spawn_kinds.insert(EntryKindTag::Media);
|
spawn_kinds.insert(EntryKindTag::Media);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use std::time::Duration;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::process::{init, kill_by_command, kill_snap_cgroup, ManagedProcess};
|
use crate::process::{init, kill_by_command, kill_flatpak_cgroup, kill_snap_cgroup, ManagedProcess};
|
||||||
|
|
||||||
/// Expand `~` at the beginning of a path to the user's home directory
|
/// Expand `~` at the beginning of a path to the user's home directory
|
||||||
fn expand_tilde(path: &str) -> String {
|
fn expand_tilde(path: &str) -> String {
|
||||||
|
|
@ -39,6 +39,7 @@ fn expand_args(args: &[String]) -> Vec<String> {
|
||||||
struct SessionInfo {
|
struct SessionInfo {
|
||||||
command_name: String,
|
command_name: String,
|
||||||
snap_name: Option<String>,
|
snap_name: Option<String>,
|
||||||
|
flatpak_app_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Linux host adapter
|
/// Linux host adapter
|
||||||
|
|
@ -132,15 +133,15 @@ impl HostAdapter for LinuxHost {
|
||||||
entry_kind: &EntryKind,
|
entry_kind: &EntryKind,
|
||||||
options: SpawnOptions,
|
options: SpawnOptions,
|
||||||
) -> HostResult<HostSessionHandle> {
|
) -> HostResult<HostSessionHandle> {
|
||||||
// Extract argv, env, cwd, and snap_name based on entry kind
|
// Extract argv, env, cwd, snap_name, and flatpak_app_id based on entry kind
|
||||||
let (argv, env, cwd, snap_name) = match entry_kind {
|
let (argv, env, cwd, snap_name, flatpak_app_id) = match entry_kind {
|
||||||
EntryKind::Process { command, args, env, cwd } => {
|
EntryKind::Process { command, args, env, cwd } => {
|
||||||
let mut argv = vec![expand_tilde(command)];
|
let mut argv = vec![expand_tilde(command)];
|
||||||
argv.extend(expand_args(args));
|
argv.extend(expand_args(args));
|
||||||
let expanded_cwd = cwd.as_ref().map(|c| {
|
let expanded_cwd = cwd.as_ref().map(|c| {
|
||||||
std::path::PathBuf::from(expand_tilde(&c.to_string_lossy()))
|
std::path::PathBuf::from(expand_tilde(&c.to_string_lossy()))
|
||||||
});
|
});
|
||||||
(argv, env.clone(), expanded_cwd, None)
|
(argv, env.clone(), expanded_cwd, None, None)
|
||||||
}
|
}
|
||||||
EntryKind::Snap { snap_name, command, args, env } => {
|
EntryKind::Snap { snap_name, command, args, env } => {
|
||||||
// For snap apps, we need to use 'snap run <snap_name>' to launch them.
|
// For snap apps, we need to use 'snap run <snap_name>' to launch them.
|
||||||
|
|
@ -153,7 +154,13 @@ impl HostAdapter for LinuxHost {
|
||||||
argv.push(cmd.clone());
|
argv.push(cmd.clone());
|
||||||
}
|
}
|
||||||
argv.extend(expand_args(args));
|
argv.extend(expand_args(args));
|
||||||
(argv, env.clone(), None, Some(snap_name.clone()))
|
(argv, env.clone(), None, Some(snap_name.clone()), None)
|
||||||
|
}
|
||||||
|
EntryKind::Flatpak { app_id, args, env } => {
|
||||||
|
// For Flatpak apps, we use 'flatpak run <app_id>' to launch them.
|
||||||
|
let mut argv = vec!["flatpak".to_string(), "run".to_string(), app_id.clone()];
|
||||||
|
argv.extend(expand_args(args));
|
||||||
|
(argv, env.clone(), None, None, Some(app_id.clone()))
|
||||||
}
|
}
|
||||||
EntryKind::Vm { driver, args } => {
|
EntryKind::Vm { driver, args } => {
|
||||||
// Construct command line from VM driver
|
// Construct command line from VM driver
|
||||||
|
|
@ -166,13 +173,13 @@ impl HostAdapter for LinuxHost {
|
||||||
argv.push(value.to_string());
|
argv.push(value.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(argv, HashMap::new(), None, None)
|
(argv, HashMap::new(), None, 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 argv = vec!["xdg-open".to_string(), expand_tilde(library_id)];
|
let argv = vec!["xdg-open".to_string(), expand_tilde(library_id)];
|
||||||
(argv, HashMap::new(), None, None)
|
(argv, HashMap::new(), None, None, None)
|
||||||
}
|
}
|
||||||
EntryKind::Custom { type_name: _, payload: _ } => {
|
EntryKind::Custom { type_name: _, payload: _ } => {
|
||||||
return Err(HostError::UnsupportedKind);
|
return Err(HostError::UnsupportedKind);
|
||||||
|
|
@ -180,19 +187,24 @@ impl HostAdapter for LinuxHost {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the command name for fallback killing
|
// Get the command name for fallback killing
|
||||||
// For snap apps, use the snap_name (not "snap") to avoid killing unrelated processes
|
// For snap/flatpak apps, use the app name (not "snap"/"flatpak") to avoid killing unrelated processes
|
||||||
let command_name = if let Some(ref snap) = snap_name {
|
let command_name = if let Some(ref snap) = snap_name {
|
||||||
snap.clone()
|
snap.clone()
|
||||||
|
} else if let Some(ref app_id) = flatpak_app_id {
|
||||||
|
app_id.clone()
|
||||||
} else {
|
} else {
|
||||||
argv.first().cloned().unwrap_or_default()
|
argv.first().cloned().unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Determine if this is a sandboxed app (snap or flatpak)
|
||||||
|
let sandboxed_app_name = snap_name.clone().or_else(|| flatpak_app_id.clone());
|
||||||
|
|
||||||
let proc = ManagedProcess::spawn(
|
let proc = ManagedProcess::spawn(
|
||||||
&argv,
|
&argv,
|
||||||
&env,
|
&env,
|
||||||
cwd.as_ref(),
|
cwd.as_ref(),
|
||||||
options.log_path.clone(),
|
options.log_path.clone(),
|
||||||
snap_name.clone(),
|
sandboxed_app_name,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let pid = proc.pid;
|
let pid = proc.pid;
|
||||||
|
|
@ -202,9 +214,10 @@ impl HostAdapter for LinuxHost {
|
||||||
let session_info_entry = SessionInfo {
|
let session_info_entry = SessionInfo {
|
||||||
command_name: command_name.clone(),
|
command_name: command_name.clone(),
|
||||||
snap_name: snap_name.clone(),
|
snap_name: snap_name.clone(),
|
||||||
|
flatpak_app_id: flatpak_app_id.clone(),
|
||||||
};
|
};
|
||||||
self.session_info.lock().unwrap().insert(session_id.clone(), session_info_entry);
|
self.session_info.lock().unwrap().insert(session_id.clone(), session_info_entry);
|
||||||
info!(session_id = %session_id, command = %command_name, snap = ?snap_name, "Tracking session info");
|
info!(session_id = %session_id, command = %command_name, snap = ?snap_name, flatpak = ?flatpak_app_id, "Tracking session info");
|
||||||
|
|
||||||
let handle = HostSessionHandle::new(
|
let handle = HostSessionHandle::new(
|
||||||
session_id,
|
session_id,
|
||||||
|
|
@ -238,13 +251,16 @@ impl HostAdapter for LinuxHost {
|
||||||
|
|
||||||
match mode {
|
match mode {
|
||||||
StopMode::Graceful { timeout } => {
|
StopMode::Graceful { timeout } => {
|
||||||
// If this is a snap app, use cgroup-based killing (most reliable)
|
// If this is a snap or flatpak app, use cgroup-based killing (most reliable)
|
||||||
if let Some(ref info) = session_info {
|
if let Some(ref info) = session_info {
|
||||||
if let Some(ref snap) = info.snap_name {
|
if let Some(ref snap) = info.snap_name {
|
||||||
kill_snap_cgroup(snap, nix::sys::signal::Signal::SIGTERM);
|
kill_snap_cgroup(snap, nix::sys::signal::Signal::SIGTERM);
|
||||||
info!(snap = %snap, "Sent SIGTERM via snap cgroup");
|
info!(snap = %snap, "Sent SIGTERM via snap cgroup");
|
||||||
|
} else if let Some(ref app_id) = info.flatpak_app_id {
|
||||||
|
kill_flatpak_cgroup(app_id, nix::sys::signal::Signal::SIGTERM);
|
||||||
|
info!(flatpak = %app_id, "Sent SIGTERM via flatpak cgroup");
|
||||||
} else {
|
} else {
|
||||||
// Fall back to command name for non-snap apps
|
// Fall back to command name for non-sandboxed apps
|
||||||
kill_by_command(&info.command_name, nix::sys::signal::Signal::SIGTERM);
|
kill_by_command(&info.command_name, nix::sys::signal::Signal::SIGTERM);
|
||||||
info!(command = %info.command_name, "Sent SIGTERM via command name");
|
info!(command = %info.command_name, "Sent SIGTERM via command name");
|
||||||
}
|
}
|
||||||
|
|
@ -262,11 +278,14 @@ impl HostAdapter for LinuxHost {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
loop {
|
loop {
|
||||||
if start.elapsed() >= timeout {
|
if start.elapsed() >= timeout {
|
||||||
// Force kill after timeout using snap cgroup or command name
|
// Force kill after timeout using snap/flatpak cgroup or command name
|
||||||
if let Some(ref info) = session_info {
|
if let Some(ref info) = session_info {
|
||||||
if let Some(ref snap) = info.snap_name {
|
if let Some(ref snap) = info.snap_name {
|
||||||
kill_snap_cgroup(snap, nix::sys::signal::Signal::SIGKILL);
|
kill_snap_cgroup(snap, nix::sys::signal::Signal::SIGKILL);
|
||||||
info!(snap = %snap, "Sent SIGKILL via snap cgroup (timeout)");
|
info!(snap = %snap, "Sent SIGKILL via snap cgroup (timeout)");
|
||||||
|
} else if let Some(ref app_id) = info.flatpak_app_id {
|
||||||
|
kill_flatpak_cgroup(app_id, nix::sys::signal::Signal::SIGKILL);
|
||||||
|
info!(flatpak = %app_id, "Sent SIGKILL via flatpak cgroup (timeout)");
|
||||||
} else {
|
} else {
|
||||||
kill_by_command(&info.command_name, nix::sys::signal::Signal::SIGKILL);
|
kill_by_command(&info.command_name, nix::sys::signal::Signal::SIGKILL);
|
||||||
info!(command = %info.command_name, "Sent SIGKILL via command name (timeout)");
|
info!(command = %info.command_name, "Sent SIGKILL via command name (timeout)");
|
||||||
|
|
@ -292,11 +311,14 @@ impl HostAdapter for LinuxHost {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StopMode::Force => {
|
StopMode::Force => {
|
||||||
// Force kill via snap cgroup or command name
|
// Force kill via snap/flatpak cgroup or command name
|
||||||
if let Some(ref info) = session_info {
|
if let Some(ref info) = session_info {
|
||||||
if let Some(ref snap) = info.snap_name {
|
if let Some(ref snap) = info.snap_name {
|
||||||
kill_snap_cgroup(snap, nix::sys::signal::Signal::SIGKILL);
|
kill_snap_cgroup(snap, nix::sys::signal::Signal::SIGKILL);
|
||||||
info!(snap = %snap, "Sent SIGKILL via snap cgroup");
|
info!(snap = %snap, "Sent SIGKILL via snap cgroup");
|
||||||
|
} else if let Some(ref app_id) = info.flatpak_app_id {
|
||||||
|
kill_flatpak_cgroup(app_id, nix::sys::signal::Signal::SIGKILL);
|
||||||
|
info!(flatpak = %app_id, "Sent SIGKILL via flatpak cgroup");
|
||||||
} else {
|
} else {
|
||||||
kill_by_command(&info.command_name, nix::sys::signal::Signal::SIGKILL);
|
kill_by_command(&info.command_name, nix::sys::signal::Signal::SIGKILL);
|
||||||
info!(command = %info.command_name, "Sent SIGKILL via command name");
|
info!(command = %info.command_name, "Sent SIGKILL via command name");
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,70 @@ pub fn kill_snap_cgroup(snap_name: &str, _signal: Signal) -> bool {
|
||||||
stopped_any
|
stopped_any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Kill all processes in a Flatpak app's cgroup using systemd
|
||||||
|
/// Flatpak apps create scopes at: app-flatpak-<app_id>-<number>.scope
|
||||||
|
/// For example: app-flatpak-org.prismlauncher.PrismLauncher-12345.scope
|
||||||
|
/// Similar to snap apps, we use systemctl --user to manage the scopes.
|
||||||
|
pub fn kill_flatpak_cgroup(app_id: &str, _signal: Signal) -> bool {
|
||||||
|
let uid = nix::unistd::getuid().as_raw();
|
||||||
|
let base_path = format!(
|
||||||
|
"/sys/fs/cgroup/user.slice/user-{}.slice/user@{}.service/app.slice",
|
||||||
|
uid, uid
|
||||||
|
);
|
||||||
|
|
||||||
|
// Flatpak uses a different naming pattern than snap
|
||||||
|
// The app_id dots are preserved: app-flatpak-org.example.App-<number>.scope
|
||||||
|
let pattern = format!("app-flatpak-{}-", app_id);
|
||||||
|
|
||||||
|
let base = std::path::Path::new(&base_path);
|
||||||
|
if !base.exists() {
|
||||||
|
debug!(path = %base_path, "Flatpak cgroup base path doesn't exist");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stopped_any = false;
|
||||||
|
|
||||||
|
if let Ok(entries) = std::fs::read_dir(base) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let name = entry.file_name();
|
||||||
|
let name_str = name.to_string_lossy();
|
||||||
|
|
||||||
|
if name_str.starts_with(&pattern) && name_str.ends_with(".scope") {
|
||||||
|
let scope_name = name_str.to_string();
|
||||||
|
|
||||||
|
// Always use SIGKILL for flatpak apps to prevent self-restart behavior
|
||||||
|
// Using systemctl kill --signal=KILL sends SIGKILL to all processes in scope
|
||||||
|
let result = Command::new("systemctl")
|
||||||
|
.args(["--user", "kill", "--signal=KILL", &scope_name])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
info!(scope = %scope_name, "Killed flatpak scope via systemctl SIGKILL");
|
||||||
|
stopped_any = true;
|
||||||
|
} else {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
warn!(scope = %scope_name, stderr = %stderr, "systemctl kill command failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!(scope = %scope_name, error = %e, "Failed to run systemctl");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if stopped_any {
|
||||||
|
info!(app_id = app_id, "Killed flatpak scope(s) via systemctl SIGKILL");
|
||||||
|
} else {
|
||||||
|
debug!(app_id = app_id, "No flatpak scope found to kill");
|
||||||
|
}
|
||||||
|
|
||||||
|
stopped_any
|
||||||
|
}
|
||||||
|
|
||||||
/// Kill processes by command name using pkill
|
/// Kill processes by command name using pkill
|
||||||
pub fn kill_by_command(command_name: &str, signal: Signal) -> bool {
|
pub fn kill_by_command(command_name: &str, signal: Signal) -> bool {
|
||||||
let signal_name = match signal {
|
let signal_name = match signal {
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ impl LauncherTile {
|
||||||
let fallback_icon = match entry.kind_tag {
|
let fallback_icon = 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::Snap => "application-x-executable",
|
||||||
|
shepherd_api::EntryKindTag::Flatpak => "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