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.
203 lines
5.8 KiB
Markdown
203 lines
5.8 KiB
Markdown
# shepherd-host-api
|
|
|
|
Host adapter trait interfaces for Shepherd.
|
|
|
|
## Overview
|
|
|
|
This crate defines the capability-based interface between the Shepherd core and platform-specific implementations. It contains **no platform code itself**—only traits, types, and a mock implementation for testing.
|
|
|
|
### Purpose
|
|
|
|
Desktop operating systems have fundamentally different process control models:
|
|
- **Linux** can kill process groups and use cgroups
|
|
- **macOS** requires MDM for true kiosk mode
|
|
- **Windows** uses job objects and shell policies
|
|
- **Android** has managed launcher and device-owner workflows
|
|
|
|
`shepherd-host-api` acknowledges these differences honestly through a capability-based design rather than pretending all platforms are equivalent.
|
|
|
|
## Key Concepts
|
|
|
|
### Capabilities
|
|
|
|
The `HostCapabilities` struct declares what a host adapter can actually do:
|
|
|
|
```rust
|
|
use shepherd_host_api::HostCapabilities;
|
|
|
|
let caps = host.capabilities();
|
|
|
|
// Check supported entry kinds
|
|
if caps.supports_kind(EntryKindTag::Process) { /* ... */ }
|
|
if caps.supports_kind(EntryKindTag::Snap) { /* ... */ }
|
|
if caps.supports_kind(EntryKindTag::Steam) { /* ... */ }
|
|
|
|
// Check enforcement capabilities
|
|
if caps.can_kill_forcefully { /* Can use SIGKILL/TerminateProcess */ }
|
|
if caps.can_graceful_stop { /* Can request graceful shutdown */ }
|
|
if caps.can_group_process_tree { /* Can kill entire process tree */ }
|
|
|
|
// Check optional features
|
|
if caps.can_observe_window_ready { /* Get notified when GUI appears */ }
|
|
if caps.can_force_foreground { /* Can bring window to front */ }
|
|
if caps.can_force_fullscreen { /* Can set fullscreen mode */ }
|
|
```
|
|
|
|
The core engine uses these capabilities to:
|
|
- Filter available entries (don't show what can't be run)
|
|
- Choose termination strategies
|
|
- Decide whether to attempt optional behaviors
|
|
|
|
### Host Adapter Trait
|
|
|
|
Platform adapters implement this trait:
|
|
|
|
```rust
|
|
use shepherd_host_api::{HostAdapter, SpawnOptions, StopMode, HostEvent};
|
|
|
|
#[async_trait]
|
|
pub trait HostAdapter: Send + Sync {
|
|
/// Get the capabilities of this host adapter
|
|
fn capabilities(&self) -> &HostCapabilities;
|
|
|
|
/// Spawn a new session
|
|
async fn spawn(
|
|
&self,
|
|
session_id: SessionId,
|
|
entry_kind: &EntryKind,
|
|
options: SpawnOptions,
|
|
) -> HostResult<HostSessionHandle>;
|
|
|
|
/// Stop a running session
|
|
async fn stop(&self, handle: &HostSessionHandle, mode: StopMode) -> HostResult<()>;
|
|
|
|
/// Subscribe to host events (exits, window ready, etc.)
|
|
fn subscribe(&self) -> mpsc::UnboundedReceiver<HostEvent>;
|
|
|
|
// Optional methods with default implementations
|
|
async fn set_foreground(&self, handle: &HostSessionHandle) -> HostResult<()>;
|
|
async fn set_fullscreen(&self, handle: &HostSessionHandle) -> HostResult<()>;
|
|
async fn ensure_shell_visible(&self) -> HostResult<()>;
|
|
}
|
|
```
|
|
|
|
### Session Handles
|
|
|
|
`HostSessionHandle` is an opaque container for platform-specific identifiers:
|
|
|
|
```rust
|
|
use shepherd_host_api::HostSessionHandle;
|
|
|
|
// Created by spawn(), contains platform-specific data
|
|
let handle: HostSessionHandle = host.spawn(session_id, &entry_kind, options).await?;
|
|
|
|
// On Linux, internally contains pid, pgid
|
|
// On Windows, would contain job object handle
|
|
// On macOS, might contain bundle identifier
|
|
```
|
|
|
|
### Stop Modes
|
|
|
|
Session termination can be graceful or forced:
|
|
|
|
```rust
|
|
use shepherd_host_api::StopMode;
|
|
use std::time::Duration;
|
|
|
|
// Try graceful shutdown with timeout, then force
|
|
host.stop(&handle, StopMode::Graceful {
|
|
timeout: Duration::from_secs(5)
|
|
}).await?;
|
|
|
|
// Immediate termination
|
|
host.stop(&handle, StopMode::Force).await?;
|
|
```
|
|
|
|
### Host Events
|
|
|
|
Adapters emit events via an async channel:
|
|
|
|
```rust
|
|
use shepherd_host_api::HostEvent;
|
|
|
|
let mut events = host.subscribe();
|
|
while let Some(event) = events.recv().await {
|
|
match event {
|
|
HostEvent::Exited { handle, status } => {
|
|
// Process ended (normally or killed)
|
|
}
|
|
HostEvent::WindowReady { handle } => {
|
|
// GUI window appeared (if observable)
|
|
}
|
|
HostEvent::SpawnFailed { session_id, error } => {
|
|
// Launch failed after handle was created
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Volume Control
|
|
|
|
The crate also defines a volume controller interface:
|
|
|
|
```rust
|
|
use shepherd_host_api::VolumeController;
|
|
|
|
#[async_trait]
|
|
pub trait VolumeController: Send + Sync {
|
|
/// Get current volume (0-100)
|
|
async fn get_volume(&self) -> HostResult<u8>;
|
|
|
|
/// Set volume (0-100)
|
|
async fn set_volume(&self, level: u8) -> HostResult<()>;
|
|
|
|
/// Check if muted
|
|
async fn is_muted(&self) -> HostResult<bool>;
|
|
|
|
/// Set mute state
|
|
async fn set_muted(&self, muted: bool) -> HostResult<()>;
|
|
|
|
/// Subscribe to volume changes
|
|
fn subscribe(&self) -> mpsc::UnboundedReceiver<VolumeEvent>;
|
|
}
|
|
```
|
|
|
|
## Mock Implementation
|
|
|
|
For testing, the crate provides `MockHost`:
|
|
|
|
```rust
|
|
use shepherd_host_api::MockHost;
|
|
|
|
let mock = MockHost::new();
|
|
|
|
// Spawns will "succeed" with fake handles
|
|
let handle = mock.spawn(session_id, &entry_kind, options).await?;
|
|
|
|
// Inject events for testing
|
|
mock.inject_exit(handle.clone(), ExitStatus::Code(0));
|
|
```
|
|
|
|
## Design Philosophy
|
|
|
|
- **Honest capabilities** - Don't pretend all platforms are equal
|
|
- **Platform code stays out** - This crate is pure interface
|
|
- **Extensible** - New capabilities can be added without breaking existing adapters
|
|
- **Testable** - Mock implementation enables unit testing
|
|
|
|
## Available Implementations
|
|
|
|
| Adapter | Crate | Status |
|
|
|---------|-------|--------|
|
|
| Linux | `shepherd-host-linux` | Implemented |
|
|
| macOS | `shepherd-host-macos` | Planned |
|
|
| Windows | `shepherd-host-windows` | Planned |
|
|
| Android | `shepherd-host-android` | Planned |
|
|
|
|
## Dependencies
|
|
|
|
- `async-trait` - Async trait support
|
|
- `tokio` - Async runtime types
|
|
- `serde` - Serialization for handles
|
|
- `shepherd-api` - Entry kind types
|
|
- `shepherd-util` - ID types
|