Merge pull request #25 from aarmea/u/aarmea/4/steam-type
Add "steam"-specific type
This commit is contained in:
commit
6e64e8e69d
13 changed files with 267 additions and 33 deletions
|
|
@ -180,9 +180,8 @@ label = "Celeste"
|
||||||
icon = "~/Games/Icons/Celeste.png"
|
icon = "~/Games/Icons/Celeste.png"
|
||||||
|
|
||||||
[entries.kind]
|
[entries.kind]
|
||||||
type = "snap"
|
type = "steam"
|
||||||
snap_name = "steam"
|
app_id = 504230 # Steam App ID
|
||||||
args = ["steam://rungameid/504230"] # Steam App ID (passed to 'snap run steam')
|
|
||||||
|
|
||||||
[entries.availability]
|
[entries.availability]
|
||||||
[[entries.availability.windows]]
|
[[entries.availability.windows]]
|
||||||
|
|
@ -206,9 +205,8 @@ label = "A Short Hike"
|
||||||
icon = "~/Games/Icons/A_Short_Hike.png"
|
icon = "~/Games/Icons/A_Short_Hike.png"
|
||||||
|
|
||||||
[entries.kind]
|
[entries.kind]
|
||||||
type = "snap"
|
type = "steam"
|
||||||
snap_name = "steam"
|
app_id = 1055540 # Steam App ID
|
||||||
args = ["steam://rungameid/1055540"] # Steam App ID (passed to 'snap run steam')
|
|
||||||
|
|
||||||
[entries.availability]
|
[entries.availability]
|
||||||
[[entries.availability.windows]]
|
[[entries.availability.windows]]
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use std::time::Duration;
|
||||||
pub enum EntryKindTag {
|
pub enum EntryKindTag {
|
||||||
Process,
|
Process,
|
||||||
Snap,
|
Snap,
|
||||||
|
Steam,
|
||||||
Flatpak,
|
Flatpak,
|
||||||
Vm,
|
Vm,
|
||||||
Media,
|
Media,
|
||||||
|
|
@ -47,6 +48,17 @@ pub enum EntryKind {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
env: HashMap<String, String>,
|
env: HashMap<String, String>,
|
||||||
},
|
},
|
||||||
|
/// Steam game launched via the Steam snap (Linux)
|
||||||
|
Steam {
|
||||||
|
/// Steam App ID (e.g., 504230 for Celeste)
|
||||||
|
app_id: u32,
|
||||||
|
/// Additional command-line arguments passed to Steam
|
||||||
|
#[serde(default)]
|
||||||
|
args: Vec<String>,
|
||||||
|
/// Additional environment variables
|
||||||
|
#[serde(default)]
|
||||||
|
env: HashMap<String, String>,
|
||||||
|
},
|
||||||
/// Flatpak application - uses systemd scope-based process management
|
/// Flatpak application - uses systemd scope-based process management
|
||||||
Flatpak {
|
Flatpak {
|
||||||
/// The Flatpak application ID (e.g., "org.prismlauncher.PrismLauncher")
|
/// The Flatpak application ID (e.g., "org.prismlauncher.PrismLauncher")
|
||||||
|
|
@ -79,6 +91,7 @@ impl EntryKind {
|
||||||
match self {
|
match self {
|
||||||
EntryKind::Process { .. } => EntryKindTag::Process,
|
EntryKind::Process { .. } => EntryKindTag::Process,
|
||||||
EntryKind::Snap { .. } => EntryKindTag::Snap,
|
EntryKind::Snap { .. } => EntryKindTag::Snap,
|
||||||
|
EntryKind::Steam { .. } => EntryKindTag::Steam,
|
||||||
EntryKind::Flatpak { .. } => EntryKindTag::Flatpak,
|
EntryKind::Flatpak { .. } => EntryKindTag::Flatpak,
|
||||||
EntryKind::Vm { .. } => EntryKindTag::Vm,
|
EntryKind::Vm { .. } => EntryKindTag::Vm,
|
||||||
EntryKind::Media { .. } => EntryKindTag::Media,
|
EntryKind::Media { .. } => EntryKindTag::Media,
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,9 @@ kind = { type = "process", command = "/usr/bin/game", args = ["--fullscreen"] }
|
||||||
# Snap application
|
# Snap application
|
||||||
kind = { type = "snap", snap_name = "mc-installer" }
|
kind = { type = "snap", snap_name = "mc-installer" }
|
||||||
|
|
||||||
|
# Steam game (via Steam snap)
|
||||||
|
kind = { type = "steam", app_id = 504230 }
|
||||||
|
|
||||||
# Virtual machine (future)
|
# Virtual machine (future)
|
||||||
kind = { type = "vm", driver = "qemu", args = { disk = "game.qcow2" } }
|
kind = { type = "vm", driver = "qemu", args = { disk = "game.qcow2" } }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,9 @@ fn main() -> ExitCode {
|
||||||
EntryKind::Snap { snap_name, .. } => {
|
EntryKind::Snap { snap_name, .. } => {
|
||||||
format!("snap ({})", snap_name)
|
format!("snap ({})", snap_name)
|
||||||
}
|
}
|
||||||
|
EntryKind::Steam { app_id, .. } => {
|
||||||
|
format!("steam ({})", app_id)
|
||||||
|
}
|
||||||
EntryKind::Flatpak { app_id, .. } => {
|
EntryKind::Flatpak { app_id, .. } => {
|
||||||
format!("flatpak ({})", app_id)
|
format!("flatpak ({})", app_id)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::Steam { app_id, args, env } => EntryKind::Steam { app_id, args, env },
|
||||||
RawEntryKind::Flatpak { app_id, args, env } => EntryKind::Flatpak { app_id, 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 },
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,17 @@ pub enum RawEntryKind {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
env: HashMap<String, String>,
|
env: HashMap<String, String>,
|
||||||
},
|
},
|
||||||
|
/// Steam game launched via the Steam snap (Linux)
|
||||||
|
Steam {
|
||||||
|
/// Steam App ID (e.g., 504230 for Celeste)
|
||||||
|
app_id: u32,
|
||||||
|
/// Additional command-line arguments passed to Steam
|
||||||
|
#[serde(default)]
|
||||||
|
args: Vec<String>,
|
||||||
|
/// Additional environment variables
|
||||||
|
#[serde(default)]
|
||||||
|
env: HashMap<String, String>,
|
||||||
|
},
|
||||||
/// Flatpak application - uses systemd scope-based process management
|
/// Flatpak application - uses systemd scope-based process management
|
||||||
Flatpak {
|
Flatpak {
|
||||||
/// The Flatpak application ID (e.g., "org.prismlauncher.PrismLauncher")
|
/// The Flatpak application ID (e.g., "org.prismlauncher.PrismLauncher")
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,14 @@ fn validate_entry(entry: &RawEntry, config: &RawConfig) -> Vec<ValidationError>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
RawEntryKind::Steam { app_id, .. } => {
|
||||||
|
if *app_id == 0 {
|
||||||
|
errors.push(ValidationError::EntryError {
|
||||||
|
entry_id: entry.id.clone(),
|
||||||
|
message: "app_id must be > 0".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
RawEntryKind::Flatpak { app_id, .. } => {
|
RawEntryKind::Flatpak { app_id, .. } => {
|
||||||
if app_id.is_empty() {
|
if app_id.is_empty() {
|
||||||
errors.push(ValidationError::EntryError {
|
errors.push(ValidationError::EntryError {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ let caps = host.capabilities();
|
||||||
// Check supported entry kinds
|
// Check supported entry kinds
|
||||||
if caps.supports_kind(EntryKindTag::Process) { /* ... */ }
|
if caps.supports_kind(EntryKindTag::Process) { /* ... */ }
|
||||||
if caps.supports_kind(EntryKindTag::Snap) { /* ... */ }
|
if caps.supports_kind(EntryKindTag::Snap) { /* ... */ }
|
||||||
|
if caps.supports_kind(EntryKindTag::Steam) { /* ... */ }
|
||||||
|
|
||||||
// Check enforcement capabilities
|
// Check enforcement capabilities
|
||||||
if caps.can_kill_forcefully { /* Can use SIGKILL/TerminateProcess */ }
|
if caps.can_kill_forcefully { /* Can use SIGKILL/TerminateProcess */ }
|
||||||
|
|
|
||||||
|
|
@ -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::Steam);
|
||||||
spawn_kinds.insert(EntryKindTag::Flatpak);
|
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);
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,21 @@ let entry_kind = EntryKind::Snap {
|
||||||
let handle = host.spawn(session_id, &entry_kind, options).await?;
|
let handle = host.spawn(session_id, &entry_kind, options).await?;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Spawning Steam Games
|
||||||
|
|
||||||
|
Steam games are launched via the Steam snap:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let entry_kind = EntryKind::Steam {
|
||||||
|
app_id: 504230,
|
||||||
|
args: vec![],
|
||||||
|
env: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spawns via: snap run steam steam://rungameid/504230
|
||||||
|
let handle = host.spawn(session_id, &entry_kind, options).await?;
|
||||||
|
```
|
||||||
|
|
||||||
### Stopping Sessions
|
### Stopping Sessions
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,20 @@
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use shepherd_api::EntryKind;
|
use shepherd_api::EntryKind;
|
||||||
use shepherd_host_api::{
|
use shepherd_host_api::{
|
||||||
HostAdapter, HostCapabilities, HostError, HostEvent, HostHandlePayload,
|
ExitStatus, HostAdapter, HostCapabilities, HostError, HostEvent, HostHandlePayload,
|
||||||
HostResult, HostSessionHandle, SpawnOptions, StopMode,
|
HostResult, HostSessionHandle, SpawnOptions, StopMode,
|
||||||
};
|
};
|
||||||
use shepherd_util::SessionId;
|
use shepherd_util::SessionId;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
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_flatpak_cgroup, kill_snap_cgroup, ManagedProcess};
|
use crate::process::{
|
||||||
|
find_steam_game_pids, init, kill_by_command, kill_flatpak_cgroup, kill_snap_cgroup,
|
||||||
|
kill_steam_game_processes, 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 {
|
||||||
|
|
@ -40,6 +43,15 @@ struct SessionInfo {
|
||||||
command_name: String,
|
command_name: String,
|
||||||
snap_name: Option<String>,
|
snap_name: Option<String>,
|
||||||
flatpak_app_id: Option<String>,
|
flatpak_app_id: Option<String>,
|
||||||
|
steam_app_id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct SteamSession {
|
||||||
|
pid: u32,
|
||||||
|
pgid: u32,
|
||||||
|
app_id: u32,
|
||||||
|
seen_game: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Linux host adapter
|
/// Linux host adapter
|
||||||
|
|
@ -48,6 +60,7 @@ pub struct LinuxHost {
|
||||||
processes: Arc<Mutex<HashMap<u32, ManagedProcess>>>,
|
processes: Arc<Mutex<HashMap<u32, ManagedProcess>>>,
|
||||||
/// Track session info for killing
|
/// Track session info for killing
|
||||||
session_info: Arc<Mutex<HashMap<SessionId, SessionInfo>>>,
|
session_info: Arc<Mutex<HashMap<SessionId, SessionInfo>>>,
|
||||||
|
steam_sessions: Arc<Mutex<HashMap<u32, SteamSession>>>,
|
||||||
event_tx: mpsc::UnboundedSender<HostEvent>,
|
event_tx: mpsc::UnboundedSender<HostEvent>,
|
||||||
event_rx: Arc<Mutex<Option<mpsc::UnboundedReceiver<HostEvent>>>>,
|
event_rx: Arc<Mutex<Option<mpsc::UnboundedReceiver<HostEvent>>>>,
|
||||||
}
|
}
|
||||||
|
|
@ -63,6 +76,7 @@ impl LinuxHost {
|
||||||
capabilities: HostCapabilities::linux_full(),
|
capabilities: HostCapabilities::linux_full(),
|
||||||
processes: Arc::new(Mutex::new(HashMap::new())),
|
processes: Arc::new(Mutex::new(HashMap::new())),
|
||||||
session_info: Arc::new(Mutex::new(HashMap::new())),
|
session_info: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
steam_sessions: Arc::new(Mutex::new(HashMap::new())),
|
||||||
event_tx: tx,
|
event_tx: tx,
|
||||||
event_rx: Arc::new(Mutex::new(Some(rx))),
|
event_rx: Arc::new(Mutex::new(Some(rx))),
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +85,7 @@ impl LinuxHost {
|
||||||
/// Start the background process monitor
|
/// Start the background process monitor
|
||||||
pub fn start_monitor(&self) -> tokio::task::JoinHandle<()> {
|
pub fn start_monitor(&self) -> tokio::task::JoinHandle<()> {
|
||||||
let processes = self.processes.clone();
|
let processes = self.processes.clone();
|
||||||
|
let steam_sessions = self.steam_sessions.clone();
|
||||||
let event_tx = self.event_tx.clone();
|
let event_tx = self.event_tx.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
|
@ -78,13 +93,22 @@ impl LinuxHost {
|
||||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
|
||||||
let mut exited = Vec::new();
|
let mut exited = Vec::new();
|
||||||
|
let steam_pids: HashSet<u32> = {
|
||||||
|
steam_sessions
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.keys()
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut procs = processes.lock().unwrap();
|
let mut procs = processes.lock().unwrap();
|
||||||
for (pid, proc) in procs.iter_mut() {
|
for (pid, proc) in procs.iter_mut() {
|
||||||
match proc.try_wait() {
|
match proc.try_wait() {
|
||||||
Ok(Some(status)) => {
|
Ok(Some(status)) => {
|
||||||
exited.push((*pid, proc.pgid, status));
|
let is_steam = steam_pids.contains(pid);
|
||||||
|
exited.push((*pid, proc.pgid, status, is_steam));
|
||||||
}
|
}
|
||||||
Ok(None) => {}
|
Ok(None) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -93,12 +117,16 @@ impl LinuxHost {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (pid, _, _) in &exited {
|
for (pid, _, _, _) in &exited {
|
||||||
procs.remove(pid);
|
procs.remove(pid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (pid, pgid, status) in exited {
|
for (pid, pgid, status, is_steam) in exited {
|
||||||
|
if is_steam {
|
||||||
|
info!(pid = pid, pgid = pgid, status = ?status, "Steam launch process exited");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
info!(pid = pid, pgid = pgid, status = ?status, "Process exited - sending HostEvent::Exited");
|
info!(pid = pid, pgid = pgid, status = ?status, "Process exited - sending HostEvent::Exited");
|
||||||
|
|
||||||
// We don't have the session_id here, so we use a placeholder
|
// We don't have the session_id here, so we use a placeholder
|
||||||
|
|
@ -110,6 +138,49 @@ impl LinuxHost {
|
||||||
|
|
||||||
let _ = event_tx.send(HostEvent::Exited { handle, status });
|
let _ = event_tx.send(HostEvent::Exited { handle, status });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track Steam sessions by Steam App ID instead of process exit
|
||||||
|
let steam_snapshot: Vec<SteamSession> = {
|
||||||
|
steam_sessions
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ended = Vec::new();
|
||||||
|
|
||||||
|
for session in &steam_snapshot {
|
||||||
|
let has_game = !find_steam_game_pids(session.app_id).is_empty();
|
||||||
|
if has_game {
|
||||||
|
if let Ok(mut map) = steam_sessions.lock() {
|
||||||
|
map.entry(session.pid)
|
||||||
|
.and_modify(|entry| entry.seen_game = true);
|
||||||
|
}
|
||||||
|
} else if session.seen_game {
|
||||||
|
ended.push((session.pid, session.pgid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ended.is_empty() {
|
||||||
|
let mut map = steam_sessions.lock().unwrap();
|
||||||
|
let mut procs = processes.lock().unwrap();
|
||||||
|
|
||||||
|
for (pid, pgid) in ended {
|
||||||
|
map.remove(&pid);
|
||||||
|
procs.remove(&pid);
|
||||||
|
|
||||||
|
let handle = HostSessionHandle::new(
|
||||||
|
SessionId::new(),
|
||||||
|
HostHandlePayload::Linux { pid, pgid },
|
||||||
|
);
|
||||||
|
let _ = event_tx.send(HostEvent::Exited {
|
||||||
|
handle,
|
||||||
|
status: ExitStatus::success(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -133,15 +204,15 @@ impl HostAdapter for LinuxHost {
|
||||||
entry_kind: &EntryKind,
|
entry_kind: &EntryKind,
|
||||||
options: SpawnOptions,
|
options: SpawnOptions,
|
||||||
) -> HostResult<HostSessionHandle> {
|
) -> HostResult<HostSessionHandle> {
|
||||||
// Extract argv, env, cwd, snap_name, and flatpak_app_id based on entry kind
|
// Extract argv, env, cwd, snap_name, flatpak_app_id, and steam_app_id based on entry kind
|
||||||
let (argv, env, cwd, snap_name, flatpak_app_id) = match entry_kind {
|
let (argv, env, cwd, snap_name, flatpak_app_id, steam_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, None)
|
(argv, env.clone(), expanded_cwd, None, 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.
|
||||||
|
|
@ -154,13 +225,24 @@ 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()), None)
|
(argv, env.clone(), None, Some(snap_name.clone()), None, None)
|
||||||
|
}
|
||||||
|
EntryKind::Steam { app_id, args, env } => {
|
||||||
|
// Steam games are launched via the Steam snap: snap run steam steam://rungameid/<app_id>
|
||||||
|
let mut argv = vec![
|
||||||
|
"snap".to_string(),
|
||||||
|
"run".to_string(),
|
||||||
|
"steam".to_string(),
|
||||||
|
format!("steam://rungameid/{}", app_id),
|
||||||
|
];
|
||||||
|
argv.extend(expand_args(args));
|
||||||
|
(argv, env.clone(), None, None, None, Some(*app_id))
|
||||||
}
|
}
|
||||||
EntryKind::Flatpak { app_id, args, env } => {
|
EntryKind::Flatpak { app_id, args, env } => {
|
||||||
// For Flatpak apps, we use 'flatpak run <app_id>' to launch them.
|
// 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()];
|
let mut argv = vec!["flatpak".to_string(), "run".to_string(), app_id.clone()];
|
||||||
argv.extend(expand_args(args));
|
argv.extend(expand_args(args));
|
||||||
(argv, env.clone(), None, None, Some(app_id.clone()))
|
(argv, env.clone(), None, None, Some(app_id.clone()), None)
|
||||||
}
|
}
|
||||||
EntryKind::Vm { driver, args } => {
|
EntryKind::Vm { driver, args } => {
|
||||||
// Construct command line from VM driver
|
// Construct command line from VM driver
|
||||||
|
|
@ -173,13 +255,13 @@ impl HostAdapter for LinuxHost {
|
||||||
argv.push(value.to_string());
|
argv.push(value.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(argv, HashMap::new(), None, None, None)
|
(argv, HashMap::new(), None, 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, None)
|
(argv, HashMap::new(), None, None, None, None)
|
||||||
}
|
}
|
||||||
EntryKind::Custom { type_name: _, payload: _ } => {
|
EntryKind::Custom { type_name: _, payload: _ } => {
|
||||||
return Err(HostError::UnsupportedKind);
|
return Err(HostError::UnsupportedKind);
|
||||||
|
|
@ -190,6 +272,8 @@ impl HostAdapter for LinuxHost {
|
||||||
// For snap/flatpak apps, use the app name (not "snap"/"flatpak") 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 steam_app_id.is_some() {
|
||||||
|
"steam".to_string()
|
||||||
} else if let Some(ref app_id) = flatpak_app_id {
|
} else if let Some(ref app_id) = flatpak_app_id {
|
||||||
app_id.clone()
|
app_id.clone()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -215,6 +299,7 @@ impl HostAdapter for LinuxHost {
|
||||||
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(),
|
flatpak_app_id: flatpak_app_id.clone(),
|
||||||
|
steam_app_id,
|
||||||
};
|
};
|
||||||
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, flatpak = ?flatpak_app_id, "Tracking session info");
|
info!(session_id = %session_id, command = %command_name, snap = ?snap_name, flatpak = ?flatpak_app_id, "Tracking session info");
|
||||||
|
|
@ -226,6 +311,18 @@ impl HostAdapter for LinuxHost {
|
||||||
|
|
||||||
self.processes.lock().unwrap().insert(pid, proc);
|
self.processes.lock().unwrap().insert(pid, proc);
|
||||||
|
|
||||||
|
if let Some(app_id) = steam_app_id {
|
||||||
|
self.steam_sessions.lock().unwrap().insert(
|
||||||
|
pid,
|
||||||
|
SteamSession {
|
||||||
|
pid,
|
||||||
|
pgid,
|
||||||
|
app_id,
|
||||||
|
seen_game: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
info!(pid = pid, pgid = pgid, "Spawned process");
|
info!(pid = pid, pgid = pgid, "Spawned process");
|
||||||
|
|
||||||
Ok(handle)
|
Ok(handle)
|
||||||
|
|
@ -256,6 +353,12 @@ impl HostAdapter for LinuxHost {
|
||||||
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(app_id) = info.steam_app_id {
|
||||||
|
let _ = kill_steam_game_processes(app_id, nix::sys::signal::Signal::SIGTERM);
|
||||||
|
if let Ok(mut map) = self.steam_sessions.lock() {
|
||||||
|
map.entry(pid).and_modify(|entry| entry.seen_game = true);
|
||||||
|
}
|
||||||
|
info!(steam_app_id = app_id, "Sent SIGTERM to Steam game processes");
|
||||||
} else if let Some(ref app_id) = info.flatpak_app_id {
|
} else if let Some(ref app_id) = info.flatpak_app_id {
|
||||||
kill_flatpak_cgroup(app_id, nix::sys::signal::Signal::SIGTERM);
|
kill_flatpak_cgroup(app_id, nix::sys::signal::Signal::SIGTERM);
|
||||||
info!(flatpak = %app_id, "Sent SIGTERM via flatpak cgroup");
|
info!(flatpak = %app_id, "Sent SIGTERM via flatpak cgroup");
|
||||||
|
|
@ -266,8 +369,9 @@ impl HostAdapter for LinuxHost {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also send SIGTERM via process handle
|
// Also send SIGTERM via process handle (skip for Steam sessions)
|
||||||
{
|
let is_steam = session_info.as_ref().and_then(|info| info.steam_app_id).is_some();
|
||||||
|
if !is_steam {
|
||||||
let procs = self.processes.lock().unwrap();
|
let procs = self.processes.lock().unwrap();
|
||||||
if let Some(p) = procs.get(&pid) {
|
if let Some(p) = procs.get(&pid) {
|
||||||
let _ = p.terminate();
|
let _ = p.terminate();
|
||||||
|
|
@ -283,6 +387,9 @@ impl HostAdapter for LinuxHost {
|
||||||
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(app_id) = info.steam_app_id {
|
||||||
|
let _ = kill_steam_game_processes(app_id, nix::sys::signal::Signal::SIGKILL);
|
||||||
|
info!(steam_app_id = app_id, "Sent SIGKILL to Steam game processes (timeout)");
|
||||||
} else if let Some(ref app_id) = info.flatpak_app_id {
|
} else if let Some(ref app_id) = info.flatpak_app_id {
|
||||||
kill_flatpak_cgroup(app_id, nix::sys::signal::Signal::SIGKILL);
|
kill_flatpak_cgroup(app_id, nix::sys::signal::Signal::SIGKILL);
|
||||||
info!(flatpak = %app_id, "Sent SIGKILL via flatpak cgroup (timeout)");
|
info!(flatpak = %app_id, "Sent SIGKILL via flatpak cgroup (timeout)");
|
||||||
|
|
@ -292,16 +399,25 @@ impl HostAdapter for LinuxHost {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also force kill via process handle
|
// Also force kill via process handle (skip for Steam sessions)
|
||||||
|
if !is_steam {
|
||||||
let procs = self.processes.lock().unwrap();
|
let procs = self.processes.lock().unwrap();
|
||||||
if let Some(p) = procs.get(&pid) {
|
if let Some(p) = procs.get(&pid) {
|
||||||
let _ = p.kill();
|
let _ = p.kill();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if process is still running
|
// Check if process is still running
|
||||||
let still_running = self.processes.lock().unwrap().contains_key(&pid);
|
let still_running = if is_steam {
|
||||||
|
let app_id = session_info.as_ref().and_then(|info| info.steam_app_id);
|
||||||
|
app_id
|
||||||
|
.map(|id| !find_steam_game_pids(id).is_empty())
|
||||||
|
.unwrap_or(false)
|
||||||
|
} else {
|
||||||
|
self.processes.lock().unwrap().contains_key(&pid)
|
||||||
|
};
|
||||||
|
|
||||||
if !still_running {
|
if !still_running {
|
||||||
break;
|
break;
|
||||||
|
|
@ -316,6 +432,12 @@ impl HostAdapter for LinuxHost {
|
||||||
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(app_id) = info.steam_app_id {
|
||||||
|
let _ = kill_steam_game_processes(app_id, nix::sys::signal::Signal::SIGKILL);
|
||||||
|
if let Ok(mut map) = self.steam_sessions.lock() {
|
||||||
|
map.entry(pid).and_modify(|entry| entry.seen_game = true);
|
||||||
|
}
|
||||||
|
info!(steam_app_id = app_id, "Sent SIGKILL to Steam game processes");
|
||||||
} else if let Some(ref app_id) = info.flatpak_app_id {
|
} else if let Some(ref app_id) = info.flatpak_app_id {
|
||||||
kill_flatpak_cgroup(app_id, nix::sys::signal::Signal::SIGKILL);
|
kill_flatpak_cgroup(app_id, nix::sys::signal::Signal::SIGKILL);
|
||||||
info!(flatpak = %app_id, "Sent SIGKILL via flatpak cgroup");
|
info!(flatpak = %app_id, "Sent SIGKILL via flatpak cgroup");
|
||||||
|
|
@ -325,13 +447,16 @@ impl HostAdapter for LinuxHost {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also force kill via process handle
|
// Also force kill via process handle (skip for Steam sessions)
|
||||||
|
let is_steam = session_info.as_ref().and_then(|info| info.steam_app_id).is_some();
|
||||||
|
if !is_steam {
|
||||||
let procs = self.processes.lock().unwrap();
|
let procs = self.processes.lock().unwrap();
|
||||||
if let Some(p) = procs.get(&pid) {
|
if let Some(p) = procs.get(&pid) {
|
||||||
let _ = p.kill();
|
let _ = p.kill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up the session info tracking
|
// Clean up the session info tracking
|
||||||
self.session_info.lock().unwrap().remove(&session_id);
|
self.session_info.lock().unwrap().remove(&session_id);
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,60 @@ pub fn kill_flatpak_cgroup(app_id: &str, _signal: Signal) -> bool {
|
||||||
stopped_any
|
stopped_any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find Steam game process IDs by Steam App ID (from environment variables)
|
||||||
|
pub fn find_steam_game_pids(app_id: u32) -> Vec<i32> {
|
||||||
|
let mut pids = Vec::new();
|
||||||
|
let target = app_id.to_string();
|
||||||
|
let keys = ["SteamAppId", "SteamAppID", "STEAM_APP_ID"];
|
||||||
|
|
||||||
|
if let Ok(entries) = std::fs::read_dir("/proc") {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let name = entry.file_name();
|
||||||
|
let name_str = name.to_string_lossy();
|
||||||
|
if let Ok(pid) = name_str.parse::<i32>() {
|
||||||
|
let env_path = format!("/proc/{}/environ", pid);
|
||||||
|
let Ok(env_bytes) = std::fs::read(&env_path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
for var in env_bytes.split(|b| *b == 0) {
|
||||||
|
if var.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok(var_str) = std::str::from_utf8(var) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for key in &keys {
|
||||||
|
let prefix = format!("{}=", key);
|
||||||
|
if var_str
|
||||||
|
.strip_prefix(&prefix)
|
||||||
|
.is_some_and(|val| val == target)
|
||||||
|
{
|
||||||
|
pids.push(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pids
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kill Steam game processes by Steam App ID
|
||||||
|
pub fn kill_steam_game_processes(app_id: u32, signal: Signal) -> bool {
|
||||||
|
let pids = find_steam_game_pids(app_id);
|
||||||
|
if pids.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for pid in pids {
|
||||||
|
let _ = signal::kill(Pid::from_raw(pid), signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// 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::Steam => "application-x-executable",
|
||||||
shepherd_api::EntryKindTag::Flatpak => "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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue