Add crate documentation
This commit is contained in:
parent
f5d7d69578
commit
c444426507
13 changed files with 2156 additions and 1 deletions
|
|
@ -39,6 +39,7 @@ TODO: show the following running with some subset of the above features highligh
|
|||
|
||||
Contributions are welcome for improvements and not yet implemented backends,
|
||||
such as:
|
||||
* Content-aware media player [TODO: link to issue]
|
||||
* Pre-booted Steam to improve launch time [TODO: link to issue]
|
||||
* Android apps via Waydroid, including pre-booting Android if necessary [TODO: link to issue]
|
||||
* Legacy Win9x via DOSBox, QEMU, or PCem, including scripts to create a boot-to-app image [TODO: link to issue]
|
||||
|
|
|
|||
|
|
@ -236,10 +236,11 @@ end = "20:00"
|
|||
# Omitting limits entirely uses default_max_run_seconds
|
||||
|
||||
## === Media ===
|
||||
# Just use `mpv` to play media.
|
||||
# Just use `mpv` to play media (for now).
|
||||
# Files can be local on your system or URLs (YouTube, etc).
|
||||
|
||||
# "lofi hip hop radio 📚 beats to relax/study to" streamed live from YouTube
|
||||
# This should eventually be replaced with the media type.
|
||||
[[entries]]
|
||||
id = "lofi-beats"
|
||||
label = "Lofi Beats"
|
||||
|
|
|
|||
153
crates/shepherd-api/README.md
Normal file
153
crates/shepherd-api/README.md
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
# shepherd-api
|
||||
|
||||
Protocol types for Shepherd IPC communication.
|
||||
|
||||
## Overview
|
||||
|
||||
This crate defines the stable API between the Shepherd service (`shepherdd`) and its clients (launcher UI, HUD overlay, admin tools). It contains:
|
||||
|
||||
- **Commands** - Requests from clients to the service
|
||||
- **Responses** - Service replies to commands
|
||||
- **Events** - Asynchronous notifications from service to clients
|
||||
- **Shared types** - Entry views, session info, reason codes, etc.
|
||||
|
||||
## Purpose
|
||||
|
||||
`shepherd-api` establishes the contract between components, ensuring:
|
||||
|
||||
1. **Stability** - Versioned protocol with backward compatibility
|
||||
2. **Type safety** - Strongly typed messages prevent protocol errors
|
||||
3. **Decoupling** - Clients and service can evolve independently
|
||||
|
||||
## API Version
|
||||
|
||||
```rust
|
||||
use shepherd_api::API_VERSION;
|
||||
|
||||
// Current API version
|
||||
assert_eq!(API_VERSION, 1);
|
||||
```
|
||||
|
||||
## Key Types
|
||||
|
||||
### Commands
|
||||
|
||||
Commands are requests sent by clients to the service:
|
||||
|
||||
```rust
|
||||
use shepherd_api::Command;
|
||||
|
||||
// Request available entries
|
||||
let cmd = Command::ListEntries;
|
||||
|
||||
// Request to launch an entry
|
||||
let cmd = Command::Launch {
|
||||
entry_id: "minecraft".into()
|
||||
};
|
||||
|
||||
// Request to stop current session
|
||||
let cmd = Command::StopCurrent {
|
||||
mode: StopMode::Graceful
|
||||
};
|
||||
|
||||
// Subscribe to real-time events
|
||||
let cmd = Command::SubscribeEvents;
|
||||
```
|
||||
|
||||
Available commands:
|
||||
- `GetState` - Get full service state snapshot
|
||||
- `ListEntries` - List all entries with availability
|
||||
- `Launch { entry_id }` - Launch an entry
|
||||
- `StopCurrent { mode }` - Stop the current session
|
||||
- `ReloadConfig` - Reload configuration (admin only)
|
||||
- `SubscribeEvents` - Subscribe to event stream
|
||||
- `GetHealth` - Get service health status
|
||||
- `SetVolume { level }` - Set system volume
|
||||
- `GetVolume` - Get current volume
|
||||
|
||||
### Events
|
||||
|
||||
Events are pushed from the service to subscribed clients:
|
||||
|
||||
```rust
|
||||
use shepherd_api::{Event, EventPayload};
|
||||
|
||||
// Events received by clients
|
||||
match event.payload {
|
||||
EventPayload::StateChanged(snapshot) => { /* Update UI */ }
|
||||
EventPayload::SessionStarted(info) => { /* Show HUD */ }
|
||||
EventPayload::WarningIssued { threshold, remaining, severity, message } => { /* Alert user */ }
|
||||
EventPayload::SessionExpired { session_id } => { /* Time's up */ }
|
||||
EventPayload::SessionEnded { session_id, reason } => { /* Return to launcher */ }
|
||||
EventPayload::PolicyReloaded { entry_count } => { /* Refresh entry list */ }
|
||||
EventPayload::VolumeChanged(info) => { /* Update volume display */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Entry Views
|
||||
|
||||
Entries as presented to UIs:
|
||||
|
||||
```rust
|
||||
use shepherd_api::EntryView;
|
||||
|
||||
let view: EntryView = /* from service */;
|
||||
|
||||
if view.enabled {
|
||||
// Entry can be launched
|
||||
println!("Max run time: {:?}", view.max_run_if_started_now);
|
||||
} else {
|
||||
// Entry unavailable, show reasons
|
||||
for reason in &view.reasons {
|
||||
match reason {
|
||||
ReasonCode::OutsideTimeWindow { next_window_start } => { /* ... */ }
|
||||
ReasonCode::QuotaExhausted { used, quota } => { /* ... */ }
|
||||
ReasonCode::CooldownActive { available_at } => { /* ... */ }
|
||||
ReasonCode::SessionActive { entry_id, remaining } => { /* ... */ }
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Session Info
|
||||
|
||||
Information about active sessions:
|
||||
|
||||
```rust
|
||||
use shepherd_api::{SessionInfo, SessionState};
|
||||
|
||||
let session: SessionInfo = /* from snapshot */;
|
||||
|
||||
match session.state {
|
||||
SessionState::Launching => { /* Show spinner */ }
|
||||
SessionState::Running => { /* Show countdown */ }
|
||||
SessionState::Warned => { /* Highlight urgency */ }
|
||||
SessionState::Expiring => { /* Terminating... */ }
|
||||
SessionState::Ended => { /* Session over */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Reason Codes
|
||||
|
||||
Structured explanations for unavailability:
|
||||
|
||||
- `OutsideTimeWindow` - Not within allowed time window
|
||||
- `QuotaExhausted` - Daily time limit reached
|
||||
- `CooldownActive` - Must wait after previous session
|
||||
- `SessionActive` - Another session is running
|
||||
- `UnsupportedKind` - Host doesn't support this entry type
|
||||
- `Disabled` - Entry explicitly disabled in config
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
- **Service is authoritative** - Clients display state, service enforces policy
|
||||
- **Structured reasons** - UIs can explain "why is this unavailable?"
|
||||
- **Event-driven** - Clients subscribe and react to changes
|
||||
- **Serializable** - All types derive `Serialize`/`Deserialize` for JSON transport
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `serde` - Serialization/deserialization
|
||||
- `chrono` - Timestamp types
|
||||
- `shepherd-util` - ID types
|
||||
204
crates/shepherd-config/README.md
Normal file
204
crates/shepherd-config/README.md
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
# shepherd-config
|
||||
|
||||
Configuration parsing and validation for Shepherd.
|
||||
|
||||
## Overview
|
||||
|
||||
This crate handles loading, parsing, and validating the TOML configuration that defines what entries are available, when they're available, and for how long. It provides:
|
||||
|
||||
- **Schema definitions** - Raw configuration structure as parsed from TOML
|
||||
- **Policy objects** - Validated, ready-to-use policy structures
|
||||
- **Validation** - Detailed error messages for misconfiguration
|
||||
- **Hot reload support** - Configuration can be reloaded at runtime
|
||||
|
||||
## Configuration Format
|
||||
|
||||
Shepherd uses TOML for configuration. Here's a complete example:
|
||||
|
||||
```toml
|
||||
config_version = 1
|
||||
|
||||
[service]
|
||||
socket_path = "/run/shepherdd/shepherdd.sock"
|
||||
data_dir = "/var/lib/shepherdd"
|
||||
default_max_run_seconds = 1800 # 30 minutes default
|
||||
|
||||
# Global volume restrictions
|
||||
[service.volume]
|
||||
max_volume = 80
|
||||
allow_unmute = true
|
||||
|
||||
# Default warning thresholds (seconds before expiry)
|
||||
[[service.default_warnings]]
|
||||
seconds_before = 300 # 5 minutes
|
||||
severity = "info"
|
||||
|
||||
[[service.default_warnings]]
|
||||
seconds_before = 60 # 1 minute
|
||||
severity = "warn"
|
||||
|
||||
[[service.default_warnings]]
|
||||
seconds_before = 10
|
||||
severity = "critical"
|
||||
message_template = "Closing in {remaining} seconds!"
|
||||
|
||||
# Entry definitions
|
||||
[[entries]]
|
||||
id = "minecraft"
|
||||
label = "Minecraft"
|
||||
icon = "minecraft"
|
||||
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 per session
|
||||
daily_quota_seconds = 7200 # 2 hours per day
|
||||
cooldown_seconds = 600 # 10 minutes between sessions
|
||||
|
||||
[[entries]]
|
||||
id = "educational-game"
|
||||
label = "GCompris"
|
||||
icon = "gcompris-qt"
|
||||
kind = { type = "process", command = "gcompris-qt" }
|
||||
|
||||
[entries.availability]
|
||||
always = true # Always available
|
||||
|
||||
[entries.limits]
|
||||
max_run_seconds = 3600 # 1 hour
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Loading Configuration
|
||||
|
||||
```rust
|
||||
use shepherd_config::{load_config, parse_config, Policy};
|
||||
use std::path::Path;
|
||||
|
||||
// Load from file
|
||||
let policy = load_config("/etc/shepherdd/config.toml")?;
|
||||
|
||||
// Parse from string
|
||||
let toml_content = std::fs::read_to_string("config.toml")?;
|
||||
let policy = parse_config(&toml_content)?;
|
||||
|
||||
// Access entries
|
||||
for entry in &policy.entries {
|
||||
println!("{}: {:?}", entry.label, entry.kind);
|
||||
}
|
||||
```
|
||||
|
||||
### Entry Kinds
|
||||
|
||||
Entries can be of several types:
|
||||
|
||||
```toml
|
||||
# Regular process
|
||||
kind = { type = "process", command = "/usr/bin/game", args = ["--fullscreen"] }
|
||||
|
||||
# Snap application
|
||||
kind = { type = "snap", snap_name = "mc-installer" }
|
||||
|
||||
# Virtual machine (future)
|
||||
kind = { type = "vm", driver = "qemu", args = { disk = "game.qcow2" } }
|
||||
|
||||
# Media playback (future)
|
||||
kind = { type = "media", library_id = "movies" }
|
||||
|
||||
# Custom type
|
||||
kind = { type = "custom", type_name = "my-launcher", payload = { ... } }
|
||||
```
|
||||
|
||||
### Time Windows
|
||||
|
||||
Time windows control when entries are available:
|
||||
|
||||
```toml
|
||||
[entries.availability]
|
||||
[[entries.availability.windows]]
|
||||
days = "weekdays" # or "weekends", "all"
|
||||
start = "15:00"
|
||||
end = "18:00"
|
||||
|
||||
[[entries.availability.windows]]
|
||||
days = ["sat", "sun"] # Specific days
|
||||
start = "09:00"
|
||||
end = "21:00"
|
||||
```
|
||||
|
||||
### Limits
|
||||
|
||||
Control session duration and frequency:
|
||||
|
||||
```toml
|
||||
[entries.limits]
|
||||
max_run_seconds = 1800 # Max duration per session
|
||||
daily_quota_seconds = 7200 # Total daily limit
|
||||
cooldown_seconds = 600 # Wait time between sessions
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
The configuration is validated at load time. Validation catches:
|
||||
|
||||
- **Duplicate entry IDs** - Each entry must have a unique ID
|
||||
- **Empty commands** - Process entries must specify a command
|
||||
- **Invalid time windows** - Start time must be before end time
|
||||
- **Invalid thresholds** - Warning thresholds must be less than max run time
|
||||
- **Negative durations** - All durations must be positive
|
||||
- **Unknown kinds** - Entry types must be recognized (unless Custom)
|
||||
|
||||
```rust
|
||||
use shepherd_config::{parse_config, ConfigError};
|
||||
|
||||
let result = parse_config(toml_str);
|
||||
match result {
|
||||
Ok(policy) => { /* Use policy */ }
|
||||
Err(ConfigError::ValidationFailed { errors }) => {
|
||||
for error in errors {
|
||||
eprintln!("Config error: {}", error);
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("Failed to load config: {}", e),
|
||||
}
|
||||
```
|
||||
|
||||
## Hot Reload
|
||||
|
||||
Configuration can be reloaded at runtime via the service's `ReloadConfig` command or by sending `SIGHUP` to the service process. Reload is atomic: either the new configuration is fully applied or the old one remains.
|
||||
|
||||
Active sessions continue with their original time limits when configuration is reloaded.
|
||||
|
||||
## Key Types
|
||||
|
||||
- `Policy` - Validated policy ready for the core engine
|
||||
- `Entry` - A launchable entry definition
|
||||
- `AvailabilityPolicy` - Time window rules
|
||||
- `LimitsPolicy` - Duration and quota limits
|
||||
- `WarningPolicy` - Warning threshold configuration
|
||||
- `VolumePolicy` - Volume restrictions
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
- **Human-readable** - TOML is easy to read and write
|
||||
- **Strict validation** - Catch errors at load time, not runtime
|
||||
- **Versioned schema** - `config_version` enables future migrations
|
||||
- **Sensible defaults** - Minimal config is valid
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `toml` - TOML parsing
|
||||
- `serde` - Deserialization
|
||||
- `chrono` - Time types
|
||||
- `thiserror` - Error types
|
||||
204
crates/shepherd-core/README.md
Normal file
204
crates/shepherd-core/README.md
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
# shepherd-core
|
||||
|
||||
Core policy engine and session state machine for Shepherd.
|
||||
|
||||
## Overview
|
||||
|
||||
This crate is the heart of Shepherd, containing all policy evaluation and session management logic. It is completely platform-agnostic and makes no assumptions about the underlying operating system or display environment.
|
||||
|
||||
### Responsibilities
|
||||
|
||||
- **Policy evaluation** - Determine what entries are available, when, and for how long
|
||||
- **Session lifecycle** - Manage the state machine from launch to termination
|
||||
- **Warning scheduling** - Compute and emit warnings at configured thresholds
|
||||
- **Time enforcement** - Track deadlines using monotonic time
|
||||
- **Quota management** - Track daily usage and cooldowns
|
||||
|
||||
## Session State Machine
|
||||
|
||||
Sessions progress through the following states:
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Idle │ (no session)
|
||||
└──────┬──────┘
|
||||
│ Launch requested
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Launching │
|
||||
└──────┬──────┘
|
||||
│ Process spawned
|
||||
▼
|
||||
┌─────────────┐
|
||||
┌───────▶│ Running │◀──────┐
|
||||
│ └──────┬──────┘ │
|
||||
│ │ Warning threshold
|
||||
│ ▼
|
||||
│ ┌─────────────┐
|
||||
│ │ Warned │ (multiple levels)
|
||||
│ └──────┬──────┘
|
||||
│ │ Deadline reached
|
||||
│ ▼
|
||||
│ ┌─────────────┐
|
||||
│ │ Expiring │ (termination in progress)
|
||||
│ └──────┬──────┘
|
||||
│ │ Process ended
|
||||
│ ▼
|
||||
│ ┌─────────────┐
|
||||
└────────│ Ended │───────▶ (return to Idle)
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## Key Types
|
||||
|
||||
### CoreEngine
|
||||
|
||||
The main policy engine:
|
||||
|
||||
```rust
|
||||
use shepherd_core::CoreEngine;
|
||||
use shepherd_config::Policy;
|
||||
use shepherd_host_api::HostCapabilities;
|
||||
use shepherd_store::Store;
|
||||
use std::sync::Arc;
|
||||
|
||||
// Create the engine
|
||||
let engine = CoreEngine::new(
|
||||
policy, // Loaded configuration
|
||||
store, // Persistence layer
|
||||
host.capabilities().clone(), // What the host can do
|
||||
);
|
||||
|
||||
// List entries with current availability
|
||||
let entries = engine.list_entries(Local::now());
|
||||
|
||||
// Request to launch an entry
|
||||
match engine.request_launch(&entry_id, Local::now()) {
|
||||
LaunchDecision::Approved(plan) => {
|
||||
// Spawn via host adapter, then start session
|
||||
engine.start_session(plan, host_handle, MonotonicInstant::now());
|
||||
}
|
||||
LaunchDecision::Denied { reasons } => {
|
||||
// Cannot launch, explain why
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Session Plan
|
||||
|
||||
When a launch is approved, the engine computes a complete session plan:
|
||||
|
||||
```rust
|
||||
pub struct SessionPlan {
|
||||
pub session_id: SessionId,
|
||||
pub entry_id: EntryId,
|
||||
pub entry: Entry,
|
||||
pub started_at: DateTime<Local>,
|
||||
/// None means unlimited (no time limit)
|
||||
pub deadline: Option<MonotonicInstant>,
|
||||
pub warnings: Vec<ScheduledWarning>,
|
||||
}
|
||||
```
|
||||
|
||||
The plan is computed once at launch time. Deadlines and warnings are deterministic.
|
||||
|
||||
### Events
|
||||
|
||||
The engine emits events for the IPC layer and host adapter:
|
||||
|
||||
```rust
|
||||
pub enum CoreEvent {
|
||||
// Session lifecycle
|
||||
SessionStarted { session_id, entry_id, deadline },
|
||||
Warning { session_id, threshold_secs, remaining, severity, message },
|
||||
ExpireDue { session_id },
|
||||
SessionEnded { session_id, reason },
|
||||
|
||||
// Policy
|
||||
PolicyReloaded { entry_count },
|
||||
}
|
||||
```
|
||||
|
||||
### Tick Processing
|
||||
|
||||
The engine must be ticked periodically to check for warnings and expiry:
|
||||
|
||||
```rust
|
||||
// In the service main loop
|
||||
let events = engine.tick(MonotonicInstant::now());
|
||||
for event in events {
|
||||
match event {
|
||||
CoreEvent::Warning { .. } => { /* Notify clients */ }
|
||||
CoreEvent::ExpireDue { .. } => { /* Terminate session */ }
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Time Handling
|
||||
|
||||
The engine uses two time sources:
|
||||
|
||||
1. **Wall-clock time** (`DateTime<Local>`) - For availability windows and display
|
||||
2. **Monotonic time** (`MonotonicInstant`) - For countdown enforcement
|
||||
|
||||
This separation ensures:
|
||||
- Availability follows the user's local clock (correct behavior for "3pm-6pm" windows)
|
||||
- Session enforcement cannot be bypassed by changing the system clock
|
||||
|
||||
```rust
|
||||
// Availability uses wall-clock
|
||||
let is_available = entry.availability.is_available(&Local::now());
|
||||
|
||||
// Countdown uses monotonic
|
||||
let remaining = session.time_remaining(MonotonicInstant::now());
|
||||
```
|
||||
|
||||
## Policy Evaluation
|
||||
|
||||
For each entry, the engine evaluates:
|
||||
|
||||
1. **Explicit disable** - Entry may be disabled in config
|
||||
2. **Host capabilities** - Can the host run this entry kind?
|
||||
3. **Time window** - Is "now" within an allowed window?
|
||||
4. **Active session** - Is another session already running?
|
||||
5. **Cooldown** - Has enough time passed since the last session?
|
||||
6. **Daily quota** - Is there remaining quota for today?
|
||||
|
||||
Each check that fails adds a `ReasonCode` to the entry view, allowing UIs to explain unavailability.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
- **Determinism** - Given the same inputs, the engine produces the same outputs
|
||||
- **Platform agnosticism** - No OS-specific code
|
||||
- **Authority** - The engine is the single source of truth for policy
|
||||
- **Auditability** - All decisions can be explained via reason codes
|
||||
|
||||
## Testing
|
||||
|
||||
The engine is designed for testability:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn test_time_window_evaluation() {
|
||||
// Create engine with mock store
|
||||
// Set specific time
|
||||
// Verify entry availability
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_warning_schedule() {
|
||||
// Launch with known deadline
|
||||
// Tick at specific times
|
||||
// Verify warnings emitted at correct thresholds
|
||||
}
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `chrono` - Date/time handling
|
||||
- `shepherd-api` - Shared types
|
||||
- `shepherd-config` - Policy definitions
|
||||
- `shepherd-host-api` - Capability types
|
||||
- `shepherd-store` - Persistence trait
|
||||
- `shepherd-util` - ID and time utilities
|
||||
202
crates/shepherd-host-api/README.md
Normal file
202
crates/shepherd-host-api/README.md
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
# 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) { /* ... */ }
|
||||
|
||||
// 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
|
||||
191
crates/shepherd-host-linux/README.md
Normal file
191
crates/shepherd-host-linux/README.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# 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?;
|
||||
```
|
||||
|
||||
### 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
|
||||
184
crates/shepherd-hud/README.md
Normal file
184
crates/shepherd-hud/README.md
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
# shepherd-hud
|
||||
|
||||
Always-visible HUD overlay for Shepherd.
|
||||
|
||||
## Overview
|
||||
|
||||
`shepherd-hud` is a GTK4 layer-shell overlay that remains visible during active sessions. It provides essential information and controls that must always be accessible, regardless of what fullscreen application is running underneath.
|
||||
|
||||
## Features
|
||||
|
||||
- **Time remaining** - Authoritative countdown from the service
|
||||
- **Battery level** - Current charge percentage and status
|
||||
- **Volume control** - Adjust system volume with enforced limits
|
||||
- **Session controls** - End session button
|
||||
- **Power controls** - Suspend, shutdown, restart
|
||||
- **Warning display** - Visual and audio alerts for time warnings
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────┐
|
||||
│ Sway / Wayland Compositor │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ HUD (layer-shell overlay) │ │
|
||||
│ │ [Battery] [Volume] [Time Remaining] [Controls] │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ Running Application (fullscreen) │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The HUD uses Wayland's **wlr-layer-shell** protocol to remain above all other surfaces.
|
||||
|
||||
## Usage
|
||||
|
||||
### Running
|
||||
|
||||
```bash
|
||||
# With default socket path
|
||||
shepherd-hud
|
||||
|
||||
# With custom socket path
|
||||
shepherd-hud --socket /run/shepherdd/shepherdd.sock
|
||||
|
||||
# Custom position and size
|
||||
shepherd-hud --anchor top --height 48
|
||||
```
|
||||
|
||||
### Command-Line Options
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `-s, --socket` | `/run/shepherdd/shepherdd.sock` | Service socket path |
|
||||
| `-l, --log-level` | `info` | Log verbosity |
|
||||
| `-a, --anchor` | `top` | Screen edge (`top` or `bottom`) |
|
||||
| `--height` | `48` | HUD bar height in pixels |
|
||||
|
||||
## Display Elements
|
||||
|
||||
### Time Remaining
|
||||
|
||||
Shows the countdown timer for the current session:
|
||||
|
||||
- `MM:SS` format for times under 1 hour
|
||||
- `H:MM:SS` format for longer sessions
|
||||
- Visual emphasis when below warning thresholds
|
||||
- Shows "∞" for unlimited sessions
|
||||
|
||||
### Battery
|
||||
|
||||
Displays current battery status:
|
||||
|
||||
- Percentage (0-100%)
|
||||
- Charging/discharging indicator
|
||||
- Data sourced from UPower (not the service)
|
||||
|
||||
### Volume
|
||||
|
||||
Shows and controls system volume:
|
||||
|
||||
- Current level (0-100%)
|
||||
- Mute indicator
|
||||
- Click to adjust (sends commands to service)
|
||||
- Volume maximum may be restricted by policy
|
||||
|
||||
### Controls
|
||||
|
||||
- **End Session** - Stops the current session (if allowed)
|
||||
- **Power** - Opens menu with Suspend/Shutdown/Restart
|
||||
|
||||
## Event Handling
|
||||
|
||||
### Warnings
|
||||
|
||||
When the service emits a `WarningIssued` event:
|
||||
|
||||
1. Visual banner appears on the HUD
|
||||
2. Time display changes color based on severity
|
||||
3. Optional audio cue plays
|
||||
4. Banner auto-dismisses or requires acknowledgment
|
||||
|
||||
Severity levels:
|
||||
- `Info` (e.g., 5 minutes remaining) - Subtle notification
|
||||
- `Warn` (e.g., 1 minute remaining) - Prominent warning
|
||||
- `Critical` (e.g., 10 seconds remaining) - Urgent, full-width banner
|
||||
|
||||
### Session Expired
|
||||
|
||||
When time runs out:
|
||||
|
||||
1. "Time's Up" overlay appears
|
||||
2. Audio notification plays
|
||||
3. HUD remains visible until launcher reappears
|
||||
|
||||
### Disconnection
|
||||
|
||||
If the service connection is lost:
|
||||
|
||||
1. "Disconnected" indicator shown
|
||||
2. All controls disabled
|
||||
3. Automatic reconnection attempted
|
||||
4. **Time display frozen** (not fabricated)
|
||||
|
||||
## Styling
|
||||
|
||||
The HUD is designed to be:
|
||||
|
||||
- **Unobtrusive** - Small footprint, doesn't cover content
|
||||
- **High contrast** - Readable over any background
|
||||
- **Touch-friendly** - Large touch targets
|
||||
- **Minimal** - Icons over text where possible
|
||||
|
||||
## Layer-Shell Details
|
||||
|
||||
```rust
|
||||
// Layer-shell configuration
|
||||
layer: Overlay // Always above normal windows
|
||||
anchor: Top // Attached to top edge
|
||||
exclusive_zone: 48 // Reserves space (optional)
|
||||
keyboard_interactivity: OnDemand // Only when focused
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
The HUD maintains local state synchronized with the service:
|
||||
|
||||
```rust
|
||||
struct HudState {
|
||||
// From service
|
||||
session: Option<SessionInfo>,
|
||||
volume: VolumeInfo,
|
||||
|
||||
// Local
|
||||
battery: BatteryInfo, // From UPower
|
||||
connected: bool,
|
||||
}
|
||||
```
|
||||
|
||||
**Key principle**: The HUD never independently computes time remaining. All timing comes from the service.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `gtk4` - GTK4 bindings
|
||||
- `gtk4-layer-shell` - Wayland layer-shell support
|
||||
- `tokio` - Async runtime
|
||||
- `shepherd-api` - Protocol types
|
||||
- `shepherd-ipc` - Client implementation
|
||||
- `upower` - Battery monitoring
|
||||
- `clap` - Argument parsing
|
||||
- `tracing` - Logging
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cargo build --release -p shepherd-hud
|
||||
```
|
||||
|
||||
Requires GTK4 development libraries and a Wayland compositor with layer-shell support (e.g., Sway, Hyprland).
|
||||
234
crates/shepherd-ipc/README.md
Normal file
234
crates/shepherd-ipc/README.md
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
# shepherd-ipc
|
||||
|
||||
IPC layer for Shepherd.
|
||||
|
||||
## Overview
|
||||
|
||||
This crate provides the local inter-process communication infrastructure between the Shepherd service (`shepherdd`) and its clients (launcher UI, HUD overlay, admin tools). It includes:
|
||||
|
||||
- **Unix domain socket server** - Listens for client connections
|
||||
- **NDJSON protocol** - Newline-delimited JSON message framing
|
||||
- **Client management** - Connection tracking and cleanup
|
||||
- **Peer authentication** - UID-based role assignment
|
||||
- **Event broadcasting** - Push events to subscribed clients
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ shepherdd │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ IpcServer │ │
|
||||
│ │ ┌──────────┐ ┌─────────┐ ┌─────────┐ │ │
|
||||
│ │ │Client 1 │ │Client 2 │ │Client 3 │ ... │ │
|
||||
│ │ │(Launcher)│ │ (HUD) │ │ (Admin) │ │ │
|
||||
│ │ └────┬─────┘ └────┬────┘ └────┬────┘ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ └────────────┴───────────┘ │ │
|
||||
│ │ Unix Domain Socket │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│ │ │
|
||||
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
|
||||
│Launcher │ │ HUD │ │ Admin │
|
||||
│ UI │ │ Overlay │ │ Tool │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
## Server Usage
|
||||
|
||||
### Starting the Server
|
||||
|
||||
```rust
|
||||
use shepherd_ipc::IpcServer;
|
||||
|
||||
let mut server = IpcServer::new("/run/shepherdd/shepherdd.sock");
|
||||
server.start().await?;
|
||||
|
||||
// Get message receiver for the main loop
|
||||
let mut messages = server.take_message_receiver().await.unwrap();
|
||||
|
||||
// Accept connections in background
|
||||
tokio::spawn(async move {
|
||||
server.run().await
|
||||
});
|
||||
|
||||
// Process messages in main loop
|
||||
while let Some(msg) = messages.recv().await {
|
||||
match msg {
|
||||
ServerMessage::Request { client_id, request } => {
|
||||
// Handle request, send response
|
||||
let response = handle_request(request);
|
||||
server.send_response(&client_id, response).await?;
|
||||
}
|
||||
ServerMessage::ClientConnected { client_id, info } => {
|
||||
println!("Client {} connected as {:?}", client_id, info.role);
|
||||
}
|
||||
ServerMessage::ClientDisconnected { client_id } => {
|
||||
println!("Client {} disconnected", client_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Broadcasting Events
|
||||
|
||||
```rust
|
||||
use shepherd_api::Event;
|
||||
|
||||
// Send to all subscribed clients
|
||||
server.broadcast_event(Event::new(EventPayload::StateChanged(snapshot))).await;
|
||||
```
|
||||
|
||||
### Client Roles
|
||||
|
||||
Clients are assigned roles based on their peer UID:
|
||||
|
||||
| UID | Role | Permissions |
|
||||
|-----|------|-------------|
|
||||
| root (0) | `Admin` | All commands |
|
||||
| Service user | `Admin` | All commands |
|
||||
| Other | `Shell` | Read + Launch/Stop |
|
||||
|
||||
```rust
|
||||
// Role-based command filtering
|
||||
match (request.command, client_info.role) {
|
||||
(Command::ReloadConfig, ClientRole::Admin) => { /* allowed */ }
|
||||
(Command::ReloadConfig, ClientRole::Shell) => { /* denied */ }
|
||||
(Command::Launch { .. }, _) => { /* allowed for all */ }
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Client Usage
|
||||
|
||||
### Connecting
|
||||
|
||||
```rust
|
||||
use shepherd_ipc::IpcClient;
|
||||
|
||||
let mut client = IpcClient::connect("/run/shepherdd/shepherdd.sock").await?;
|
||||
```
|
||||
|
||||
### Sending Commands
|
||||
|
||||
```rust
|
||||
use shepherd_api::{Command, Response};
|
||||
|
||||
// Request current state
|
||||
client.send(Command::GetState).await?;
|
||||
let response: Response = client.recv().await?;
|
||||
|
||||
// Launch an entry
|
||||
client.send(Command::Launch {
|
||||
entry_id: "minecraft".into()
|
||||
}).await?;
|
||||
let response = client.recv().await?;
|
||||
```
|
||||
|
||||
### Subscribing to Events
|
||||
|
||||
```rust
|
||||
// Subscribe to event stream
|
||||
client.send(Command::SubscribeEvents).await?;
|
||||
|
||||
// Receive events
|
||||
loop {
|
||||
match client.recv_event().await {
|
||||
Ok(event) => {
|
||||
match event.payload {
|
||||
EventPayload::WarningIssued { remaining, .. } => {
|
||||
println!("Warning: {} seconds remaining", remaining.as_secs());
|
||||
}
|
||||
EventPayload::SessionEnded { .. } => {
|
||||
println!("Session ended");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Err(IpcError::ConnectionClosed) => break,
|
||||
Err(e) => eprintln!("Error: {}", e),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Protocol
|
||||
|
||||
### Message Format
|
||||
|
||||
Messages use NDJSON (newline-delimited JSON):
|
||||
|
||||
```
|
||||
{"type":"request","id":1,"command":"get_state"}\n
|
||||
{"type":"response","id":1,"payload":{"api_version":1,...}}\n
|
||||
{"type":"event","payload":{"type":"state_changed",...}}\n
|
||||
```
|
||||
|
||||
### Request/Response
|
||||
|
||||
Each request has an ID, matched in the response:
|
||||
|
||||
```json
|
||||
// Request
|
||||
{"type":"request","id":42,"command":{"type":"launch","entry_id":"minecraft"}}
|
||||
|
||||
// Response
|
||||
{"type":"response","id":42,"success":true,"payload":{...}}
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
Events are pushed without request IDs:
|
||||
|
||||
```json
|
||||
{"type":"event","payload":{"type":"warning_issued","threshold":60,"remaining":{"secs":60}}}
|
||||
```
|
||||
|
||||
## Socket Permissions
|
||||
|
||||
The socket is created with mode `0660`:
|
||||
- Owner can read/write
|
||||
- Group can read/write
|
||||
- Others have no access
|
||||
|
||||
This allows the service to run as a dedicated user while permitting group members (e.g., `shepherd` group) to connect.
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
Per-client rate limiting prevents buggy or malicious clients from overwhelming the service:
|
||||
|
||||
```rust
|
||||
// Default: 10 commands per second per client
|
||||
if rate_limiter.check(&client_id) {
|
||||
// Process command
|
||||
} else {
|
||||
// Respond with rate limit error
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```rust
|
||||
use shepherd_ipc::IpcError;
|
||||
|
||||
match result {
|
||||
Err(IpcError::ConnectionClosed) => {
|
||||
// Client disconnected
|
||||
}
|
||||
Err(IpcError::Json(e)) => {
|
||||
// Protocol error
|
||||
}
|
||||
Err(IpcError::Io(e)) => {
|
||||
// Socket error
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `tokio` - Async runtime
|
||||
- `serde` / `serde_json` - JSON serialization
|
||||
- `nix` - Unix socket peer credentials
|
||||
- `shepherd-api` - Message types
|
||||
- `shepherd-util` - Client IDs
|
||||
221
crates/shepherd-launcher-ui/README.md
Normal file
221
crates/shepherd-launcher-ui/README.md
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
# shepherd-launcher-ui
|
||||
|
||||
Main launcher grid interface for Shepherd.
|
||||
|
||||
## Overview
|
||||
|
||||
`shepherd-launcher-ui` is the primary user-facing shell for the Shepherd kiosk environment. It presents a grid of available entries (applications, games, media) and allows users to launch them.
|
||||
|
||||
This is what users see when no session is active—the "home screen" of the environment.
|
||||
|
||||
## Features
|
||||
|
||||
- **Entry grid** - Large, touch-friendly tiles for each available entry
|
||||
- **Availability display** - Visual indication of enabled/disabled entries
|
||||
- **Launch requests** - Send launch commands to the service
|
||||
- **State synchronization** - Always reflects service's authoritative state
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────┐
|
||||
│ Sway / Wayland Compositor │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ Launcher UI (fullscreen) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
|
||||
│ │ │ │ │ │ │ │ │ │ │ │
|
||||
│ │ │Minecraf│ │GCompris│ │ Movies │ │ Books │ │ │
|
||||
│ │ │ │ │ │ │ │ │ │ │ │
|
||||
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────┐ ┌────────┐ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ │ ScummVM│ │Bedtime │ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ └────────┘ └────────┘ │ │
|
||||
│ │ │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Running
|
||||
|
||||
```bash
|
||||
# With default socket path
|
||||
shepherd-launcher
|
||||
|
||||
# With custom socket path
|
||||
shepherd-launcher --socket /run/shepherdd/shepherdd.sock
|
||||
```
|
||||
|
||||
### Command-Line Options
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `-s, --socket` | `/run/shepherdd/shepherdd.sock` | Service socket path |
|
||||
| `-l, --log-level` | `info` | Log verbosity |
|
||||
|
||||
## Grid Behavior
|
||||
|
||||
### Entry Tiles
|
||||
|
||||
Each tile displays:
|
||||
|
||||
- **Icon** - Large, recognizable icon
|
||||
- **Label** - Entry name
|
||||
- **Status** - Enabled (bright) or disabled (dimmed)
|
||||
- **Time indicator** - Max duration if started now (e.g., "30 min")
|
||||
|
||||
### Enabled Entries
|
||||
|
||||
When an entry is enabled:
|
||||
1. Tile is fully visible and interactive
|
||||
2. Tapping sends `Launch` command to service
|
||||
3. Grid shows "Launching..." state
|
||||
4. On success: launcher hides, application starts
|
||||
|
||||
### Disabled Entries
|
||||
|
||||
When an entry is disabled it is not displayed.
|
||||
|
||||
### Launch Flow
|
||||
|
||||
```
|
||||
User taps tile
|
||||
│
|
||||
▼
|
||||
Launcher sends Launch command
|
||||
│
|
||||
▼
|
||||
Grid input disabled
|
||||
"Starting..." overlay shown
|
||||
│
|
||||
▼
|
||||
┌─────┴─────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
Success Failure
|
||||
│ │
|
||||
▼ ▼
|
||||
Launcher Error message
|
||||
hides Grid restored
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
The launcher maintains a reactive state model:
|
||||
|
||||
```rust
|
||||
struct LauncherState {
|
||||
entries: Vec<EntryView>, // From service
|
||||
current_session: Option<SessionInfo>,
|
||||
connected: bool,
|
||||
launching: Option<EntryId>,
|
||||
}
|
||||
```
|
||||
|
||||
### Event Handling
|
||||
|
||||
| Event | Launcher Response |
|
||||
|-------|-------------------|
|
||||
| `StateChanged` | Update entry grid |
|
||||
| `SessionStarted` | Hide launcher |
|
||||
| `SessionEnded` | Show launcher |
|
||||
| `PolicyReloaded` | Refresh entry list |
|
||||
|
||||
### Visibility Rules
|
||||
|
||||
The launcher is visible when:
|
||||
- No session is running, OR
|
||||
- User explicitly returns to home (via HUD)
|
||||
|
||||
The launcher hides when:
|
||||
- A session is actively running
|
||||
- (Fullscreen app is in front)
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Service Unavailable
|
||||
|
||||
If the service is not running at startup:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ │
|
||||
│ System Not Ready │
|
||||
│ │
|
||||
│ Waiting for shepherd service... │
|
||||
│ │
|
||||
│ [Retry] │
|
||||
│ │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Launch Failure
|
||||
|
||||
If launching fails:
|
||||
|
||||
1. Error notification appears
|
||||
2. Grid is restored to interactive state
|
||||
3. User can try again or choose another entry
|
||||
|
||||
### Connection Loss
|
||||
|
||||
If connection to service is lost:
|
||||
|
||||
1. Entries become disabled
|
||||
2. Reconnection attempted automatically
|
||||
3. State refreshed on reconnection
|
||||
|
||||
## Accessibility
|
||||
|
||||
- **Touch-first** - Large touch targets (minimum 44px)
|
||||
- **High contrast** - Clear visual hierarchy
|
||||
- **Minimal text** - Icon-first design
|
||||
- **Keyboard navigation** - Arrow keys and Enter
|
||||
- **No hover-only interactions** - All actions accessible via tap
|
||||
|
||||
## Styling
|
||||
|
||||
The launcher uses a child-friendly design:
|
||||
|
||||
- Large, colorful icons
|
||||
- Rounded corners
|
||||
- Clear enabled/disabled distinction
|
||||
- Smooth transitions
|
||||
- Dark background (for contrast)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `gtk4` - GTK4 bindings
|
||||
- `tokio` - Async runtime
|
||||
- `shepherd-api` - Protocol types
|
||||
- `shepherd-ipc` - Client implementation
|
||||
- `clap` - Argument parsing
|
||||
- `tracing` - Logging
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cargo build --release -p shepherd-launcher-ui
|
||||
```
|
||||
|
||||
The resulting binary is named `shepherd-launcher`.
|
||||
|
||||
## Relationship to Service
|
||||
|
||||
**Critical**: The launcher is purely a presentation layer. It:
|
||||
- Displays what the service allows
|
||||
- Sends launch requests
|
||||
- Shows service state
|
||||
|
||||
It does *not*:
|
||||
- Tracks time independently
|
||||
- Decide availability
|
||||
- Enforce policy
|
||||
|
||||
If the launcher crashes, the service continues enforcement. If the launcher is replaced, the system still works.
|
||||
220
crates/shepherd-store/README.md
Normal file
220
crates/shepherd-store/README.md
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
# shepherd-store
|
||||
|
||||
Persistence layer for Shepherd.
|
||||
|
||||
## Overview
|
||||
|
||||
This crate provides durable storage for the Shepherd service, including:
|
||||
|
||||
- **Audit log** - Append-only record of all significant events
|
||||
- **Usage accounting** - Track time used per entry per day
|
||||
- **Cooldown tracking** - Remember when entries become available again
|
||||
- **State snapshots** - Enable crash recovery
|
||||
|
||||
## Purpose
|
||||
|
||||
The store ensures:
|
||||
- **Time accounting correctness** - Usage is recorded durably
|
||||
- **Crash tolerance** - Service can resume after unexpected shutdown
|
||||
- **Auditability** - All actions are logged for later inspection
|
||||
|
||||
## Backend
|
||||
|
||||
The primary implementation uses **SQLite** for reliability:
|
||||
|
||||
```rust
|
||||
use shepherd_store::SqliteStore;
|
||||
|
||||
let store = SqliteStore::open("/var/lib/shepherdd/shepherdd.db")?;
|
||||
```
|
||||
|
||||
SQLite provides:
|
||||
- ACID transactions for usage accounting
|
||||
- Automatic crash recovery via WAL mode
|
||||
- Single-file database, easy to backup
|
||||
|
||||
## Store Trait
|
||||
|
||||
All storage operations go through the `Store` trait:
|
||||
|
||||
```rust
|
||||
pub trait Store: Send + Sync {
|
||||
// Audit log
|
||||
fn append_audit(&self, event: AuditEvent) -> StoreResult<()>;
|
||||
fn get_recent_audits(&self, limit: usize) -> StoreResult<Vec<AuditEvent>>;
|
||||
|
||||
// Usage accounting
|
||||
fn get_usage(&self, entry_id: &EntryId, day: NaiveDate) -> StoreResult<Duration>;
|
||||
fn add_usage(&self, entry_id: &EntryId, day: NaiveDate, duration: Duration) -> StoreResult<()>;
|
||||
|
||||
// Cooldown tracking
|
||||
fn get_cooldown_until(&self, entry_id: &EntryId) -> StoreResult<Option<DateTime<Local>>>;
|
||||
fn set_cooldown_until(&self, entry_id: &EntryId, until: DateTime<Local>) -> StoreResult<()>;
|
||||
fn clear_cooldown(&self, entry_id: &EntryId) -> StoreResult<()>;
|
||||
|
||||
// State snapshot
|
||||
fn load_snapshot(&self) -> StoreResult<Option<StateSnapshot>>;
|
||||
fn save_snapshot(&self, snapshot: &StateSnapshot) -> StoreResult<()>;
|
||||
|
||||
// Health
|
||||
fn is_healthy(&self) -> bool;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Recording Session Usage
|
||||
|
||||
```rust
|
||||
use chrono::Local;
|
||||
|
||||
// When a session ends
|
||||
let duration = session.actual_duration();
|
||||
let today = Local::now().date_naive();
|
||||
|
||||
store.add_usage(&entry_id, today, duration)?;
|
||||
```
|
||||
|
||||
### Checking Quota Remaining
|
||||
|
||||
```rust
|
||||
let today = Local::now().date_naive();
|
||||
let used = store.get_usage(&entry_id, today)?;
|
||||
|
||||
if let Some(quota) = entry.limits.daily_quota {
|
||||
let remaining = quota.saturating_sub(used);
|
||||
if remaining.is_zero() {
|
||||
// Quota exhausted
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Setting Cooldowns
|
||||
|
||||
```rust
|
||||
use chrono::{Duration, Local};
|
||||
|
||||
// After session ends, set cooldown
|
||||
let cooldown_until = Local::now() + Duration::minutes(10);
|
||||
store.set_cooldown_until(&entry_id, cooldown_until)?;
|
||||
```
|
||||
|
||||
### Checking Cooldown
|
||||
|
||||
```rust
|
||||
if let Some(until) = store.get_cooldown_until(&entry_id)? {
|
||||
if until > Local::now() {
|
||||
// Still in cooldown
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Audit Log
|
||||
|
||||
The audit log records significant events:
|
||||
|
||||
```rust
|
||||
use shepherd_store::{AuditEvent, AuditEventType};
|
||||
|
||||
// Event types logged
|
||||
store.append_audit(AuditEvent::new(AuditEventType::PolicyLoaded { entry_count: 5 }))?;
|
||||
store.append_audit(AuditEvent::new(AuditEventType::SessionStarted {
|
||||
session_id,
|
||||
entry_id
|
||||
}))?;
|
||||
store.append_audit(AuditEvent::new(AuditEventType::SessionEnded {
|
||||
session_id,
|
||||
reason: SessionEndReason::Expired
|
||||
}))?;
|
||||
store.append_audit(AuditEvent::new(AuditEventType::WarningIssued {
|
||||
session_id,
|
||||
threshold_secs: 60
|
||||
}))?;
|
||||
```
|
||||
|
||||
### Audit Event Types
|
||||
|
||||
- `PolicyLoaded` - Configuration loaded/reloaded
|
||||
- `SessionStarted` - New session began
|
||||
- `SessionEnded` - Session terminated (with reason)
|
||||
- `WarningIssued` - Time warning shown to user
|
||||
- `LaunchDenied` - Launch request rejected (with reasons)
|
||||
- `ConfigReloaded` - Configuration hot-reloaded
|
||||
- `ServiceStarted` - Service process started
|
||||
- `ServiceStopped` - Service process stopped
|
||||
|
||||
## State Snapshots
|
||||
|
||||
For crash recovery, the service can save state snapshots:
|
||||
|
||||
```rust
|
||||
use shepherd_store::{StateSnapshot, SessionSnapshot};
|
||||
|
||||
// Save current state
|
||||
let snapshot = StateSnapshot {
|
||||
timestamp: Local::now(),
|
||||
active_session: Some(SessionSnapshot {
|
||||
session_id,
|
||||
entry_id,
|
||||
started_at,
|
||||
deadline,
|
||||
warnings_issued: vec![300, 60],
|
||||
}),
|
||||
};
|
||||
store.save_snapshot(&snapshot)?;
|
||||
|
||||
// On startup, check for unfinished session
|
||||
if let Some(snapshot) = store.load_snapshot()? {
|
||||
if let Some(session) = snapshot.active_session {
|
||||
// Potentially recover or clean up
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
The SQLite store uses this schema:
|
||||
|
||||
```sql
|
||||
-- Audit log (append-only)
|
||||
CREATE TABLE audit_log (
|
||||
id INTEGER PRIMARY KEY,
|
||||
timestamp TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
event_data TEXT NOT NULL -- JSON
|
||||
);
|
||||
|
||||
-- Usage tracking (one row per entry per day)
|
||||
CREATE TABLE usage (
|
||||
entry_id TEXT NOT NULL,
|
||||
day TEXT NOT NULL, -- YYYY-MM-DD
|
||||
duration_secs INTEGER NOT NULL,
|
||||
PRIMARY KEY (entry_id, day)
|
||||
);
|
||||
|
||||
-- Cooldown tracking
|
||||
CREATE TABLE cooldowns (
|
||||
entry_id TEXT PRIMARY KEY,
|
||||
until TEXT NOT NULL -- ISO 8601 timestamp
|
||||
);
|
||||
|
||||
-- State snapshot (single row)
|
||||
CREATE TABLE snapshot (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
data TEXT NOT NULL -- JSON
|
||||
);
|
||||
```
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
- **Durability over performance** - Writes are synchronous by default
|
||||
- **Simple queries** - No complex joins or aggregations needed at runtime
|
||||
- **Append-only audit** - Never modify history
|
||||
- **Portable format** - JSON for event data enables future migration
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `rusqlite` - SQLite bindings
|
||||
- `serde` / `serde_json` - Event serialization
|
||||
- `chrono` - Timestamp handling
|
||||
- `thiserror` - Error types
|
||||
70
crates/shepherd-util/README.md
Normal file
70
crates/shepherd-util/README.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# shepherd-util
|
||||
|
||||
Shared utilities for the Shepherd ecosystem.
|
||||
|
||||
## Overview
|
||||
|
||||
This crate provides common utilities and types used across all Shepherd crates, including:
|
||||
|
||||
- **ID types** - Type-safe identifiers (`EntryId`, `SessionId`, `ClientId`)
|
||||
- **Time utilities** - Monotonic time handling and duration helpers
|
||||
- **Error types** - Common error definitions
|
||||
- **Rate limiting** - Helpers for command rate limiting
|
||||
|
||||
## Purpose
|
||||
|
||||
`shepherd-util` serves as the foundational layer that other Shepherd crates depend on. It ensures consistency across the codebase by providing:
|
||||
|
||||
1. **Unified ID management** - All identifiers are strongly typed to prevent mix-ups
|
||||
2. **Reliable time handling** - Monotonic time for enforcement (immune to wall-clock changes)
|
||||
3. **Common error patterns** - Consistent error handling across crates
|
||||
|
||||
## Key Types
|
||||
|
||||
### IDs
|
||||
|
||||
```rust
|
||||
use shepherd_util::{EntryId, SessionId, ClientId};
|
||||
|
||||
// Create IDs
|
||||
let entry_id = EntryId::new("minecraft");
|
||||
let session_id = SessionId::new(); // UUID-based
|
||||
let client_id = ClientId::new(); // UUID-based
|
||||
```
|
||||
|
||||
### Time
|
||||
|
||||
```rust
|
||||
use shepherd_util::MonotonicInstant;
|
||||
|
||||
// Monotonic time for countdown logic
|
||||
let start = MonotonicInstant::now();
|
||||
// ... later ...
|
||||
let elapsed = start.elapsed();
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
```rust
|
||||
use shepherd_util::RateLimiter;
|
||||
use std::time::Duration;
|
||||
|
||||
// Per-client command rate limiting
|
||||
let limiter = RateLimiter::new(10, Duration::from_secs(1));
|
||||
if limiter.check(&client_id) {
|
||||
// Process command
|
||||
}
|
||||
```
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
- **No platform-specific code** - Pure Rust, works everywhere
|
||||
- **Minimal dependencies** - Only essential crates
|
||||
- **Type safety** - Prefer typed wrappers over raw strings/numbers
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `uuid` - For generating unique session/client IDs
|
||||
- `chrono` - For time handling
|
||||
- `serde` - For serialization
|
||||
- `thiserror` - For error types
|
||||
270
crates/shepherdd/README.md
Normal file
270
crates/shepherdd/README.md
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
# shepherdd
|
||||
|
||||
The Shepherd background service.
|
||||
|
||||
## Overview
|
||||
|
||||
`shepherdd` is the authoritative policy and enforcement service for the Shepherd ecosystem. It is the central coordinator that:
|
||||
|
||||
- Loads and validates configuration
|
||||
- Evaluates policy to determine availability
|
||||
- Manages session lifecycles
|
||||
- Enforces time limits
|
||||
- Emits warnings and events
|
||||
- Serves multiple clients via IPC
|
||||
|
||||
**Key principle**: `shepherdd` is the single source of truth. User interfaces only request actions and display state—they never enforce policy independently.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ shepherdd │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌────────────────────┐ │
|
||||
│ │ Config │ │ Store │ │ Core Engine │ │
|
||||
│ │ Loader │──▶│ (SQLite) │──▶│ (Policy + Session) │ │
|
||||
│ └─────────────┘ └─────────────┘ └──────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ │ │
|
||||
│ │ Host │ │ IPC │◀──────────────┘ │
|
||||
│ │ Adapter │◀─│ Server │ │
|
||||
│ │ (Linux) │ │ │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ │
|
||||
│ │ │ │
|
||||
│ │ Unix Domain Socket │
|
||||
│ │ │ │
|
||||
└─────────┼────────────────┼───────────────────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
Supervised ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
Applications │Launcher │ │ HUD │ │ Admin │
|
||||
│ UI │ │ Overlay │ │ Tools │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Running
|
||||
|
||||
```bash
|
||||
# With default config location
|
||||
shepherdd
|
||||
|
||||
# With custom config
|
||||
shepherdd --config /path/to/config.toml
|
||||
|
||||
# Override socket and data paths
|
||||
shepherdd --socket /tmp/shepherdd.sock --data-dir /tmp/shepherdd-data
|
||||
|
||||
# Debug logging
|
||||
shepherdd --log-level debug
|
||||
```
|
||||
|
||||
### Command-Line Options
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `-c, --config` | `/etc/shepherdd/config.toml` | Configuration file path |
|
||||
| `-s, --socket` | From config | IPC socket path |
|
||||
| `-d, --data-dir` | From config | Data directory |
|
||||
| `-l, --log-level` | `info` | Log verbosity |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `SHEPHERD_SOCKET` | Override socket path |
|
||||
| `SHEPHERD_DATA_DIR` | Override data directory |
|
||||
| `RUST_LOG` | Tracing filter (e.g., `shepherdd=debug`) |
|
||||
|
||||
## Main Loop
|
||||
|
||||
The service runs an async event loop that processes:
|
||||
|
||||
1. **IPC messages** - Commands from clients
|
||||
2. **Host events** - Process exits, window events
|
||||
3. **Timer ticks** - Check for warnings and expiry
|
||||
4. **Signals** - SIGHUP for config reload, SIGTERM for shutdown
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ Main Loop │
|
||||
│ │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ IPC │ │ Host │ │ Timer │ │ Signal │ │
|
||||
│ │ Channel │ │ Events │ │ Tick │ │ Handler │ │
|
||||
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ └───────────┴─────┬─────┴───────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ Process Event │ │
|
||||
│ └──────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ Broadcast Events │ │
|
||||
│ └──────────────────┘ │
|
||||
└────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Command Handling
|
||||
|
||||
### Client Commands
|
||||
|
||||
| Command | Description | Role Required |
|
||||
|---------|-------------|---------------|
|
||||
| `GetState` | Get full state snapshot | Any |
|
||||
| `ListEntries` | Get available entries | Any |
|
||||
| `Launch` | Start a session | Shell/Admin |
|
||||
| `StopCurrent` | End current session | Shell/Admin |
|
||||
| `ReloadConfig` | Hot-reload configuration | Admin |
|
||||
| `SubscribeEvents` | Subscribe to event stream | Any |
|
||||
| `GetHealth` | Health check | Any |
|
||||
| `SetVolume` | Set system volume | Shell/Admin |
|
||||
| `GetVolume` | Get volume info | Any |
|
||||
|
||||
### Response Flow
|
||||
|
||||
```
|
||||
Client Request
|
||||
│
|
||||
▼
|
||||
Role Check ──────▶ Denied Response
|
||||
│
|
||||
▼
|
||||
Command Handler
|
||||
│
|
||||
▼
|
||||
Core Engine
|
||||
│
|
||||
▼
|
||||
Response + Events ──────▶ Broadcast to Subscribers
|
||||
```
|
||||
|
||||
## Session Lifecycle
|
||||
|
||||
### Launch
|
||||
|
||||
1. Client sends `Launch { entry_id }`
|
||||
2. Core engine evaluates policy
|
||||
3. If denied: respond with reasons
|
||||
4. If approved: create session plan
|
||||
5. Host adapter spawns process
|
||||
6. Session transitions to Running
|
||||
7. `SessionStarted` event broadcast
|
||||
|
||||
### Enforcement
|
||||
|
||||
1. Timer ticks every 100ms
|
||||
2. Core engine checks warnings and expiry
|
||||
3. At warning thresholds: `WarningIssued` event
|
||||
4. At deadline: initiate graceful stop
|
||||
5. After grace period: force kill
|
||||
6. `SessionEnded` event broadcast
|
||||
|
||||
### Termination
|
||||
|
||||
1. Stop triggered (expiry, user, admin, process exit)
|
||||
2. Host adapter signals process (SIGTERM)
|
||||
3. Wait for grace period
|
||||
4. Force kill if needed (SIGKILL)
|
||||
5. Record usage in store
|
||||
6. Set cooldown if configured
|
||||
7. Clear session state
|
||||
|
||||
## Configuration Reload
|
||||
|
||||
On SIGHUP or `ReloadConfig` command:
|
||||
|
||||
1. Parse new configuration file
|
||||
2. Validate completely
|
||||
3. If invalid: keep old config, log error
|
||||
4. If valid: atomic swap to new policy
|
||||
5. Emit `PolicyReloaded` event
|
||||
6. Current session continues with original plan
|
||||
|
||||
## Health Monitoring
|
||||
|
||||
The service exposes health status via `GetHealth`:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"policy_loaded": true,
|
||||
"store_healthy": true,
|
||||
"host_healthy": true,
|
||||
"uptime_seconds": 3600,
|
||||
"current_session": null
|
||||
}
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
Uses structured logging via `tracing`:
|
||||
|
||||
```
|
||||
2025-01-15T14:30:00.000Z INFO shepherdd: Starting shepherd service
|
||||
2025-01-15T14:30:00.050Z INFO shepherd_config: Configuration loaded entries=5
|
||||
2025-01-15T14:30:00.100Z INFO shepherd_ipc: IPC server listening path=/run/shepherdd/shepherdd.sock
|
||||
2025-01-15T14:30:15.000Z INFO shepherd_core: Session started session_id=abc123 entry_id=minecraft
|
||||
2025-01-15T14:59:45.000Z WARN shepherd_core: Warning issued session_id=abc123 threshold=60
|
||||
2025-01-15T15:00:45.000Z INFO shepherd_core: Session expired session_id=abc123
|
||||
```
|
||||
|
||||
## Persistence
|
||||
|
||||
State is persisted to SQLite:
|
||||
|
||||
```
|
||||
/var/lib/shepherdd/
|
||||
├── shepherdd.db # SQLite database
|
||||
└── logs/
|
||||
└── sessions/ # Session stdout/stderr
|
||||
```
|
||||
|
||||
## Signals
|
||||
|
||||
| Signal | Action |
|
||||
|--------|--------|
|
||||
| `SIGHUP` | Reload configuration |
|
||||
| `SIGTERM` | Graceful shutdown |
|
||||
| `SIGINT` | Graceful shutdown |
|
||||
|
||||
## Dependencies
|
||||
|
||||
This binary wires together all the library crates:
|
||||
|
||||
- `shepherd-config` - Configuration loading
|
||||
- `shepherd-core` - Policy engine
|
||||
- `shepherd-host-api` - Host adapter trait
|
||||
- `shepherd-host-linux` - Linux implementation
|
||||
- `shepherd-ipc` - IPC server
|
||||
- `shepherd-store` - Persistence
|
||||
- `shepherd-api` - Protocol types
|
||||
- `shepherd-util` - Utilities
|
||||
- `tokio` - Async runtime
|
||||
- `clap` - CLI parsing
|
||||
- `tracing` - Logging
|
||||
- `anyhow` - Error handling
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cargo build --release -p shepherdd
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
The service is typically started by the compositor:
|
||||
|
||||
`sway.conf`
|
||||
```conf
|
||||
# Start shepherdd FIRST - it needs to create the socket before HUD/launcher connect
|
||||
# Running inside sway ensures all spawned processes use the nested compositor
|
||||
exec ./target/debug/shepherdd -c ./config.example.toml
|
||||
```
|
||||
|
||||
See [CONTRIBUTING.md](../../CONTRIBUTING.md) for development setup.
|
||||
Loading…
Reference in a new issue