shepherdd shouldn't require root to run

This commit is contained in:
Albert Armea 2025-12-31 22:33:44 -05:00
parent 91e1547902
commit 3b1a2fb166
13 changed files with 208 additions and 39 deletions

View file

@ -4,10 +4,13 @@
config_version = 1
[service]
# Uncomment to customize paths
# socket_path = "/run/shepherdd/shepherdd.sock"
# log_dir = "/var/log/shepherdd"
# data_dir = "/var/lib/shepherdd"
# Uncomment to customize paths (defaults use XDG directories for user-writable access)
# socket_path defaults to $XDG_RUNTIME_DIR/shepherdd/shepherdd.sock
# log_dir defaults to $XDG_STATE_HOME/shepherdd or ~/.local/state/shepherdd
# data_dir defaults to $XDG_DATA_HOME/shepherdd or ~/.local/share/shepherdd
# socket_path = "/run/user/1000/shepherdd/shepherdd.sock"
# log_dir = "~/.local/state/shepherdd"
# data_dir = "~/.local/share/shepherdd"
# Default max run duration if not specified per entry (1 hour)
# Set to 0 for unlimited (no time limit)

View file

@ -3,7 +3,7 @@
use crate::schema::{RawConfig, RawEntry, RawEntryKind, RawVolumeConfig, RawServiceConfig, RawWarningThreshold};
use crate::validation::{parse_days, parse_time};
use shepherd_api::{EntryKind, WarningSeverity, WarningThreshold};
use shepherd_util::{DaysOfWeek, EntryId, TimeWindow, WallClock};
use shepherd_util::{DaysOfWeek, EntryId, TimeWindow, WallClock, default_data_dir, default_log_dir, socket_path_without_env};
use std::path::PathBuf;
use std::time::Duration;
@ -84,13 +84,13 @@ impl ServiceConfig {
Self {
socket_path: raw
.socket_path
.unwrap_or_else(|| PathBuf::from("/run/shepherdd/shepherdd.sock")),
.unwrap_or_else(socket_path_without_env),
log_dir: raw
.log_dir
.unwrap_or_else(|| PathBuf::from("/var/log/shepherdd")),
.unwrap_or_else(default_log_dir),
data_dir: raw
.data_dir
.unwrap_or_else(|| PathBuf::from("/var/lib/shepherdd")),
.unwrap_or_else(default_data_dir),
}
}
}
@ -98,9 +98,9 @@ impl ServiceConfig {
impl Default for ServiceConfig {
fn default() -> Self {
Self {
socket_path: PathBuf::from("/run/shepherdd/shepherdd.sock"),
log_dir: PathBuf::from("/var/log/shepherdd"),
data_dir: PathBuf::from("/var/lib/shepherdd"),
socket_path: socket_path_without_env(),
log_dir: default_log_dir(),
data_dir: default_data_dir(),
}
}
}

View file

@ -22,13 +22,13 @@ pub struct RawConfig {
/// Service-level settings
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct RawServiceConfig {
/// IPC socket path (default: /run/shepherdd/shepherdd.sock)
/// IPC socket path (default: $XDG_RUNTIME_DIR/shepherdd/shepherdd.sock)
pub socket_path: Option<PathBuf>,
/// Log directory
/// Log directory (default: $XDG_STATE_HOME/shepherdd)
pub log_dir: Option<PathBuf>,
/// Data directory for store
/// Data directory for store (default: $XDG_DATA_HOME/shepherdd)
pub data_dir: Option<PathBuf>,
/// Default warning thresholds (can be overridden per entry)

View file

@ -56,7 +56,7 @@ shepherd-hud --anchor top --height 48
| Option | Default | Description |
|--------|---------|-------------|
| `-s, --socket` | `/run/shepherdd/shepherdd.sock` | Service socket path |
| `-s, --socket` | `$XDG_RUNTIME_DIR/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 |

View file

@ -11,6 +11,7 @@ use gtk4::prelude::*;
use gtk4_layer_shell::{Edge, Layer, LayerShell};
use shepherd_api::Command;
use shepherd_ipc::IpcClient;
use shepherd_util::default_socket_path;
use std::path::PathBuf;
use std::sync::mpsc;
use std::time::Duration;
@ -329,12 +330,11 @@ fn build_hud_content(state: SharedState) -> gtk4::Box {
if let Some(session_id) = session_state.session_id() {
tracing::info!("Requesting end session for {}", session_id);
// Send StopCurrent command to shepherdd
let socket_path = std::env::var("SHEPHERD_SOCKET")
.unwrap_or_else(|_| "./dev-runtime/shepherd.sock".to_string());
let socket_path = default_socket_path();
std::thread::spawn(move || {
let rt = Runtime::new().expect("Failed to create runtime");
rt.block_on(async {
match IpcClient::connect(std::path::PathBuf::from(&socket_path)).await {
match IpcClient::connect(&socket_path).await {
Ok(mut client) => {
let cmd = Command::StopCurrent {
mode: shepherd_api::StopMode::Graceful,

View file

@ -11,6 +11,7 @@ mod volume;
use anyhow::Result;
use clap::Parser;
use shepherd_util::default_socket_path;
use std::path::PathBuf;
use tracing_subscriber::EnvFilter;
@ -20,8 +21,8 @@ use tracing_subscriber::EnvFilter;
#[command(about = "GTK4 layer-shell HUD for shepherdd", long_about = None)]
struct Args {
/// Socket path for shepherdd connection (or set SHEPHERD_SOCKET env var)
#[arg(short, long, env = "SHEPHERD_SOCKET", default_value = "/run/shepherdd/shepherdd.sock")]
socket: PathBuf,
#[arg(short, long, env = "SHEPHERD_SOCKET")]
socket: Option<PathBuf>,
/// Log level
#[arg(short, long, default_value = "info")]
@ -49,8 +50,11 @@ fn main() -> Result<()> {
tracing::info!("Starting Shepherd HUD");
// Determine socket path with fallback to default
let socket_path = args.socket.unwrap_or_else(default_socket_path);
// Run GTK application
let application = app::HudApp::new(args.socket, args.anchor, args.height);
let application = app::HudApp::new(socket_path, args.anchor, args.height);
let exit_code = application.run();
std::process::exit(exit_code);

View file

@ -5,19 +5,12 @@
use shepherd_api::{Command, ResponsePayload, VolumeInfo};
use shepherd_ipc::IpcClient;
use std::path::PathBuf;
use shepherd_util::default_socket_path;
use tokio::runtime::Runtime;
/// Get the default socket path from environment or fallback
fn get_socket_path() -> PathBuf {
std::env::var("SHEPHERD_SOCKET")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("./dev-runtime/shepherd.sock"))
}
/// Get current volume status from shepherdd
pub fn get_volume_status() -> Option<VolumeInfo> {
let socket_path = get_socket_path();
let socket_path = default_socket_path();
let rt = match Runtime::new() {
Ok(rt) => rt,
@ -54,7 +47,7 @@ pub fn get_volume_status() -> Option<VolumeInfo> {
/// Toggle mute state via shepherdd
pub fn toggle_mute() -> anyhow::Result<()> {
let socket_path = get_socket_path();
let socket_path = default_socket_path();
let rt = Runtime::new()?;
@ -77,7 +70,7 @@ pub fn toggle_mute() -> anyhow::Result<()> {
/// Set volume to a specific percentage via shepherdd
pub fn set_volume(percent: u8) -> anyhow::Result<()> {
let socket_path = get_socket_path();
let socket_path = default_socket_path();
let rt = Runtime::new()?;

View file

@ -49,14 +49,14 @@ This is what users see when no session is active—the "home screen" of the envi
shepherd-launcher
# With custom socket path
shepherd-launcher --socket /run/shepherdd/shepherdd.sock
shepherd-launcher --socket /custom/path/shepherdd.sock
```
### Command-Line Options
| Option | Default | Description |
|--------|---------|-------------|
| `-s, --socket` | `/run/shepherdd/shepherdd.sock` | Service socket path |
| `-s, --socket` | `$XDG_RUNTIME_DIR/shepherdd/shepherdd.sock` | Service socket path |
| `-l, --log-level` | `info` | Log verbosity |
## Grid Behavior

View file

@ -11,6 +11,7 @@ mod tile;
use anyhow::Result;
use clap::Parser;
use shepherd_util::default_socket_path;
use std::path::PathBuf;
use tracing_subscriber::EnvFilter;
@ -20,8 +21,8 @@ use tracing_subscriber::EnvFilter;
#[command(about = "GTK4 launcher UI for shepherdd", long_about = None)]
struct Args {
/// Socket path for shepherdd connection (or set SHEPHERD_SOCKET env var)
#[arg(short, long, env = "SHEPHERD_SOCKET", default_value = "/run/shepherdd/shepherdd.sock")]
socket: PathBuf,
#[arg(short, long, env = "SHEPHERD_SOCKET")]
socket: Option<PathBuf>,
/// Log level
#[arg(short, long, default_value = "info")]
@ -41,8 +42,11 @@ fn main() -> Result<()> {
tracing::info!("Starting Shepherd Launcher UI");
// Determine socket path with fallback to default
let socket_path = args.socket.unwrap_or_else(default_socket_path);
// Run GTK application
let application = app::LauncherApp::new(args.socket);
let application = app::LauncherApp::new(socket_path);
let exit_code = application.run();
std::process::exit(exit_code);

View file

@ -10,6 +10,7 @@ This crate provides common utilities and types used across all Shepherd crates,
- **Time utilities** - Monotonic time handling and duration helpers
- **Error types** - Common error definitions
- **Rate limiting** - Helpers for command rate limiting
- **Default paths** - XDG-compliant default paths for socket, data, and log directories
## Purpose
@ -56,6 +57,19 @@ if limiter.check(&client_id) {
}
```
### Default Paths
```rust
use shepherd_util::{default_socket_path, default_data_dir, default_log_dir};
// Get XDG-compliant paths (no root required)
let socket = default_socket_path(); // $XDG_RUNTIME_DIR/shepherdd/shepherdd.sock
let data = default_data_dir(); // $XDG_DATA_HOME/shepherdd or ~/.local/share/shepherdd
let logs = default_log_dir(); // $XDG_STATE_HOME/shepherdd or ~/.local/state/shepherdd
```
Environment variables `SHEPHERD_SOCKET` and `SHEPHERD_DATA_DIR` can override the defaults.
## Design Philosophy
- **No platform-specific code** - Pure Rust, works everywhere

View file

@ -5,13 +5,16 @@
//! - Time utilities (monotonic time, duration helpers)
//! - Error types
//! - Rate limiting helpers
//! - Default paths for socket, data, and log directories
mod error;
mod ids;
mod paths;
mod rate_limit;
mod time;
pub use error::*;
pub use ids::*;
pub use paths::*;
pub use rate_limit::*;
pub use time::*;

View file

@ -0,0 +1,148 @@
//! Default paths for shepherdd components
//!
//! Provides centralized path defaults that all crates can use.
//! Paths are user-writable by default (no root required):
//! - Socket: `$XDG_RUNTIME_DIR/shepherdd/shepherdd.sock` or `/tmp/shepherdd-$USER/shepherdd.sock`
//! - Data: `$XDG_DATA_HOME/shepherdd` or `~/.local/share/shepherdd`
//! - Logs: `$XDG_STATE_HOME/shepherdd` or `~/.local/state/shepherdd`
use std::path::PathBuf;
/// Environment variable for overriding the socket path
pub const SHEPHERD_SOCKET_ENV: &str = "SHEPHERD_SOCKET";
/// Environment variable for overriding the data directory
pub const SHEPHERD_DATA_DIR_ENV: &str = "SHEPHERD_DATA_DIR";
/// Socket filename within the socket directory
const SOCKET_FILENAME: &str = "shepherdd.sock";
/// Application subdirectory name
const APP_DIR: &str = "shepherdd";
/// Get the default socket path.
///
/// Order of precedence:
/// 1. `$SHEPHERD_SOCKET` environment variable (if set)
/// 2. `$XDG_RUNTIME_DIR/shepherdd/shepherdd.sock` (if XDG_RUNTIME_DIR is set)
/// 3. `/tmp/shepherdd-$USER/shepherdd.sock` (fallback)
pub fn default_socket_path() -> PathBuf {
// Check environment override first
if let Ok(path) = std::env::var(SHEPHERD_SOCKET_ENV) {
return PathBuf::from(path);
}
socket_path_without_env()
}
/// Get the socket path without checking SHEPHERD_SOCKET env var.
/// Used for default values in configs where the env var is checked separately.
pub fn socket_path_without_env() -> PathBuf {
// Try XDG_RUNTIME_DIR first (typically /run/user/<uid>)
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
return PathBuf::from(runtime_dir).join(APP_DIR).join(SOCKET_FILENAME);
}
// Fallback to /tmp with username
let username = std::env::var("USER").unwrap_or_else(|_| "unknown".to_string());
PathBuf::from(format!("/tmp/{}-{}", APP_DIR, username)).join(SOCKET_FILENAME)
}
/// Get the default data directory.
///
/// Order of precedence:
/// 1. `$SHEPHERD_DATA_DIR` environment variable (if set)
/// 2. `$XDG_DATA_HOME/shepherdd` (if XDG_DATA_HOME is set)
/// 3. `~/.local/share/shepherdd` (fallback)
pub fn default_data_dir() -> PathBuf {
// Check environment override first
if let Ok(path) = std::env::var(SHEPHERD_DATA_DIR_ENV) {
return PathBuf::from(path);
}
data_dir_without_env()
}
/// Get the data directory without checking SHEPHERD_DATA_DIR env var.
/// Used for default values in configs where the env var is checked separately.
pub fn data_dir_without_env() -> PathBuf {
// Try XDG_DATA_HOME first
if let Ok(data_home) = std::env::var("XDG_DATA_HOME") {
return PathBuf::from(data_home).join(APP_DIR);
}
// Fallback to ~/.local/share/shepherdd
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home)
.join(".local")
.join("share")
.join(APP_DIR);
}
// Last resort
PathBuf::from("/tmp").join(APP_DIR).join("data")
}
/// Get the default log directory.
///
/// Order of precedence:
/// 1. `$XDG_STATE_HOME/shepherdd` (if XDG_STATE_HOME is set)
/// 2. `~/.local/state/shepherdd` (fallback)
pub fn default_log_dir() -> PathBuf {
// Try XDG_STATE_HOME first
if let Ok(state_home) = std::env::var("XDG_STATE_HOME") {
return PathBuf::from(state_home).join(APP_DIR);
}
// Fallback to ~/.local/state/shepherdd
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home)
.join(".local")
.join("state")
.join(APP_DIR);
}
// Last resort
PathBuf::from("/tmp").join(APP_DIR).join("logs")
}
/// Get the parent directory of the socket (for creating it)
pub fn socket_dir() -> PathBuf {
let socket_path = socket_path_without_env();
socket_path.parent().map(|p| p.to_path_buf()).unwrap_or_else(|| {
// Should never happen with our paths, but just in case
PathBuf::from("/tmp").join(APP_DIR)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn socket_path_contains_shepherdd() {
// The socket path should always contain "shepherdd" regardless of environment
let path = socket_path_without_env();
assert!(path.to_string_lossy().contains("shepherdd"));
assert!(path.to_string_lossy().contains(".sock"));
}
#[test]
fn data_dir_contains_shepherdd() {
let path = data_dir_without_env();
assert!(path.to_string_lossy().contains("shepherdd"));
}
#[test]
fn log_dir_contains_shepherdd() {
let path = default_log_dir();
assert!(path.to_string_lossy().contains("shepherdd"));
}
#[test]
fn socket_dir_is_parent_of_socket_path() {
let socket = socket_path_without_env();
let dir = socket_dir();
assert_eq!(socket.parent().unwrap(), dir);
}
}

View file

@ -74,8 +74,8 @@ shepherdd --log-level debug
| Variable | Description |
|----------|-------------|
| `SHEPHERD_SOCKET` | Override socket path |
| `SHEPHERD_DATA_DIR` | Override data directory |
| `SHEPHERD_SOCKET` | Override socket path (default: `$XDG_RUNTIME_DIR/shepherdd/shepherdd.sock`) |
| `SHEPHERD_DATA_DIR` | Override data directory (default: `$XDG_DATA_HOME/shepherdd`) |
| `RUST_LOG` | Tracing filter (e.g., `shepherdd=debug`) |
## Main Loop