This implementation allows each platform to choose how to launch Steam (on Linux, we use the snap as the examples suggested before), and keeps Steam alive after an activity exits so that save sync, game updates, etc. can continue to run. Change written by Codex 5.2 on medium: Consider this GitHub issue https://github.com/aarmea/shepherd-launcher/issues/4. On Linux, an activity that uses the "steam" type should launch Steam via the snap as shown in the example configuration in this repository. Go ahead and implement the feature. I'm expecting one of the tricky bits to be killing the activity while keeping Steam alive, as we can no longer just kill the Steam snap cgroup.
206 lines
4.9 KiB
Markdown
206 lines
4.9 KiB
Markdown
# shepherd-host-linux
|
|
|
|
Linux host adapter for Shepherd.
|
|
|
|
## Overview
|
|
|
|
This crate implements the `HostAdapter` trait for Linux systems, providing:
|
|
|
|
- **Process spawning** with process group isolation
|
|
- **Process termination** via graceful (SIGTERM) and forceful (SIGKILL) signals
|
|
- **Exit observation** through async process monitoring
|
|
- **Snap application support** via systemd scope-based management
|
|
- **stdout/stderr capture** to log files
|
|
- **Volume control** with auto-detection of sound systems (PipeWire, PulseAudio, ALSA)
|
|
|
|
## Capabilities
|
|
|
|
The Linux adapter reports these capabilities:
|
|
|
|
```rust
|
|
HostCapabilities {
|
|
// Supported entry kinds
|
|
spawn_kind_supported: [Process, Snap],
|
|
|
|
// Enforcement capabilities
|
|
can_kill_forcefully: true, // SIGKILL
|
|
can_graceful_stop: true, // SIGTERM
|
|
can_group_process_tree: true, // Process groups (pgid)
|
|
can_observe_exit: true, // async wait
|
|
|
|
// Optional features (not yet implemented)
|
|
can_observe_window_ready: false,
|
|
can_force_foreground: false,
|
|
can_force_fullscreen: false,
|
|
can_lock_to_single_app: false,
|
|
}
|
|
```
|
|
|
|
## Usage
|
|
|
|
### Creating the Adapter
|
|
|
|
```rust
|
|
use shepherd_host_linux::LinuxHost;
|
|
|
|
let host = LinuxHost::new();
|
|
|
|
// Check capabilities
|
|
let caps = host.capabilities();
|
|
assert!(caps.can_kill_forcefully);
|
|
```
|
|
|
|
### Spawning Processes
|
|
|
|
```rust
|
|
use shepherd_host_api::{SpawnOptions, EntryKind};
|
|
|
|
let entry_kind = EntryKind::Process {
|
|
command: "/usr/bin/game".to_string(),
|
|
args: vec!["--fullscreen".to_string()],
|
|
env: Default::default(),
|
|
cwd: None,
|
|
};
|
|
|
|
let options = SpawnOptions {
|
|
capture_stdout: true,
|
|
capture_stderr: true,
|
|
log_path: Some("/var/log/shepherdd/sessions".into()),
|
|
fullscreen: false,
|
|
foreground: false,
|
|
};
|
|
|
|
let handle = host.spawn(session_id, &entry_kind, options).await?;
|
|
```
|
|
|
|
### Spawning Snap Applications
|
|
|
|
Snap applications are managed using systemd scopes for proper process tracking:
|
|
|
|
```rust
|
|
let entry_kind = EntryKind::Snap {
|
|
snap_name: "mc-installer".to_string(),
|
|
command: None, // Defaults to snap_name
|
|
args: vec![],
|
|
env: Default::default(),
|
|
};
|
|
|
|
// Spawns via: snap run mc-installer
|
|
// Process group is isolated within a systemd scope
|
|
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
|
|
|
|
```rust
|
|
use shepherd_host_api::StopMode;
|
|
use std::time::Duration;
|
|
|
|
// Graceful: SIGTERM, wait 5s, then SIGKILL
|
|
host.stop(&handle, StopMode::Graceful {
|
|
timeout: Duration::from_secs(5),
|
|
}).await?;
|
|
|
|
// Force: immediate SIGKILL
|
|
host.stop(&handle, StopMode::Force).await?;
|
|
```
|
|
|
|
### Monitoring Exits
|
|
|
|
```rust
|
|
let mut events = host.subscribe();
|
|
|
|
tokio::spawn(async move {
|
|
while let Some(event) = events.recv().await {
|
|
match event {
|
|
HostEvent::Exited { handle, status } => {
|
|
println!("Session {} exited: {:?}", handle.session_id(), status);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
## Volume Control
|
|
|
|
The crate includes `LinuxVolumeController` which auto-detects the available sound system:
|
|
|
|
```rust
|
|
use shepherd_host_linux::LinuxVolumeController;
|
|
|
|
let controller = LinuxVolumeController::new().await?;
|
|
|
|
// Get current volume (0-100)
|
|
let volume = controller.get_volume().await?;
|
|
|
|
// Set volume with enforcement of configured maximum
|
|
controller.set_volume(75).await?;
|
|
|
|
// Mute/unmute
|
|
controller.set_muted(true).await?;
|
|
```
|
|
|
|
### Sound System Detection Order
|
|
|
|
1. **PipeWire** (`wpctl` or `pw-cli`) - Modern default on Ubuntu 22.04+, Fedora
|
|
2. **PulseAudio** (`pactl`) - Legacy but widely available
|
|
3. **ALSA** (`amixer`) - Fallback for systems without a sound server
|
|
|
|
## Process Group Handling
|
|
|
|
All spawned processes are placed in their own process group:
|
|
|
|
```rust
|
|
// Internally uses setsid() or setpgid()
|
|
// This allows killing the entire process tree
|
|
```
|
|
|
|
When stopping a session:
|
|
1. SIGTERM is sent to the process group (`-pgid`)
|
|
2. After timeout, SIGKILL is sent to the process group
|
|
3. Orphaned children are cleaned up
|
|
|
|
## Log Capture
|
|
|
|
stdout and stderr can be captured to session log files:
|
|
|
|
```
|
|
/var/log/shepherdd/sessions/
|
|
├── 2025-01-15-abc123-minecraft.log
|
|
├── 2025-01-15-def456-gcompris.log
|
|
└── ...
|
|
```
|
|
|
|
## Future Enhancements
|
|
|
|
Planned features (hooks are designed in):
|
|
|
|
- **cgroups v2** - CPU/memory/IO limits per session
|
|
- **Namespace isolation** - Optional sandboxing
|
|
- **Sway/Wayland integration** - Focus and fullscreen control
|
|
- **D-Bus monitoring** - Window readiness detection
|
|
|
|
## Dependencies
|
|
|
|
- `nix` - Unix system calls
|
|
- `tokio` - Async runtime
|
|
- `tracing` - Logging
|
|
- `serde` - Serialization
|
|
- `shepherd-host-api` - Trait definitions
|
|
- `shepherd-api` - Entry types
|