diff --git a/crates/shepherdd/src/main.rs b/crates/shepherdd/src/main.rs index b702531..01ceb40 100644 --- a/crates/shepherdd/src/main.rs +++ b/crates/shepherdd/src/main.rs @@ -25,6 +25,7 @@ use shepherd_util::{ClientId, MonotonicInstant, RateLimiter}; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; +use tokio::signal::unix::{signal, SignalKind}; use tokio::sync::Mutex; use tracing::{debug, error, info, warn}; use tracing_subscriber::EnvFilter; @@ -163,6 +164,14 @@ impl Service { } }); + // Set up signal handlers + let mut sigterm = signal(SignalKind::terminate()) + .context("Failed to create SIGTERM handler")?; + let mut sigint = signal(SignalKind::interrupt()) + .context("Failed to create SIGINT handler")?; + let mut sighup = signal(SignalKind::hangup()) + .context("Failed to create SIGHUP handler")?; + // Main event loop let tick_interval = Duration::from_millis(100); let mut tick_timer = tokio::time::interval(tick_interval); @@ -171,6 +180,22 @@ impl Service { loop { tokio::select! { + // Signal: SIGTERM or SIGINT - graceful shutdown + _ = sigterm.recv() => { + info!("Received SIGTERM, shutting down gracefully"); + break; + } + _ = sigint.recv() => { + info!("Received SIGINT, shutting down gracefully"); + break; + } + + // Signal: SIGHUP - graceful shutdown (sent by sway on exit) + _ = sighup.recv() => { + info!("Received SIGHUP, shutting down gracefully"); + break; + } + // Tick timer - check warnings and expiry _ = tick_timer.tick() => { let now_mono = MonotonicInstant::now(); @@ -197,6 +222,30 @@ impl Service { } } } + + // Graceful shutdown + info!("Shutting down shepherdd"); + + // Stop all running sessions + { + let engine = engine.lock().await; + if let Some(session) = engine.current_session() { + info!(session_id = %session.plan.session_id, "Stopping active session"); + if let Some(handle) = &session.host_handle && let Err(e) = host.stop(handle, HostStopMode::Graceful { + timeout: Duration::from_secs(5), + }).await { + warn!(error = %e, "Failed to stop session gracefully"); + } + } + } + + // Log shutdown + if let Err(e) = store.append_audit(AuditEvent::new(AuditEventType::ServiceStopped)) { + warn!(error = %e, "Failed to log service shutdown"); + } + + info!("Shutdown complete"); + Ok(()) } async fn handle_core_event( diff --git a/docs/INSTALL.md b/docs/INSTALL.md index a5e1982..3bcae68 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -26,7 +26,6 @@ This installs: - Binaries to `/usr/local/bin/` - System Sway configuration to `/etc/sway/shepherd.conf` - Display manager desktop entry ("Shepherd Kiosk" session) -- System config template to `/etc/shepherd/config.toml` - User config to `~kiosk/.config/shepherd/config.toml` For custom installation paths: diff --git a/scripts/lib/install.sh b/scripts/lib/install.sh index 8c8ae0b..8ddc171 100755 --- a/scripts/lib/install.sh +++ b/scripts/lib/install.sh @@ -86,7 +86,7 @@ install_sway_config() { -e "s|./target/debug/shepherd-launcher|$bindir/shepherd-launcher|g" \ -e "s|./target/debug/shepherd-hud|$bindir/shepherd-hud|g" \ -e "s|./target/debug/shepherdd|$bindir/shepherdd|g" \ - -e "s|./config.example.toml|/etc/shepherd/config.toml|g" \ + -e "s|./config.example.toml|~/.config/shepherd/config.toml|g" \ -e "s|-c ./sway.conf|-c $dst_config|g" \ "$src_config" > "$dst_config" @@ -167,34 +167,6 @@ install_config() { success "Installed user configuration for $user" } -# Install the system-wide shepherd config directory -install_system_config() { - local destdir="${DESTDIR:-}" - local repo_root - repo_root="$(get_repo_root)" - - require_root - - local src_config="$repo_root/config.example.toml" - local dst_dir="$destdir/etc/shepherd" - local dst_config="$dst_dir/config.toml" - - if [[ ! -f "$src_config" ]]; then - die "Source config not found: $src_config" - fi - - info "Installing system config to $dst_config..." - - ensure_dir "$dst_dir" 0755 - - if [[ ! -f "$dst_config" ]]; then - install -m 0644 "$src_config" "$dst_config" - success "Installed system configuration" - else - info "System configuration already exists, not overwriting" - fi -} - # Install everything install_all() { local user="${1:-}" @@ -211,17 +183,15 @@ install_all() { install_bins "$prefix" install_sway_config "$prefix" - install_system_config install_desktop_entry "$prefix" install_config "$user" success "Installation complete!" info "" info "Next steps:" - info " 1. Edit /etc/shepherd/config.toml for your system" - info " 2. Edit user config at ~$user/.config/shepherd/config.toml" - info " 3. Select 'Shepherd Kiosk' session at login" - info " 4. Optionally run 'shepherd harden apply --user $user' for kiosk mode" + info " 1. Edit user config at ~$user/.config/shepherd/config.toml" + info " 2. Select 'Shepherd Kiosk' session at login" + info " 3. Optionally run 'shepherd harden apply --user $user' for kiosk mode" } # Main install command dispatcher @@ -267,9 +237,6 @@ install_main() { desktop-entry) install_desktop_entry "$prefix" ;; - system-config) - install_system_config - ;; all) install_all "$user" "$prefix" ;; @@ -282,7 +249,6 @@ Commands: config Deploy user configuration sway-config Install sway configuration desktop-entry Install display manager desktop entry - system-config Install system-wide configuration all Install everything Options: diff --git a/sway.conf b/sway.conf index c80e28c..f29153d 100644 --- a/sway.conf +++ b/sway.conf @@ -44,8 +44,11 @@ input type:pointer { # Add this to kernel parameters: vt.handoff=0 # Or in /etc/systemd/logind.conf: NAutoVTs=0 and ReserveVT=0 -# Emergency exit for admin (Super+Shift+Escape) - REMOVE IN PRODUCTION -bindsym Mod4+Shift+Escape exit +# Exit keybinding - works for both nested sway (dev) and production logout +# Kill shepherdd first to ensure clean shutdown, then exit sway +# In dev: exits the nested sway instance +# In production: logs out of the session (REMOVE or change to something harder to press) +bindsym Mod4+Shift+Escape exec pkill -TERM shepherdd && swaymsg exit ### Window rules for kiosk behavior @@ -134,6 +137,7 @@ workspace 1 output * # Start shepherdd FIRST - it needs to create the socket before HUD/launcher connect # Running inside sway ensures all spawned processes use the nested compositor +# Shepherdd handles SIGHUP for graceful shutdown when sway exits exec ./target/debug/shepherdd -c ./config.example.toml # Give shepherdd a moment to initialize, then start UI components