use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tracing::{info, warn};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct IpcConfig {
pub socket: SocketConfig,
pub limits: IpcLimitsConfig,
pub rate_limit: RateLimitConfig,
pub timeouts: IpcTimeoutsConfig,
pub shutdown: ShutdownConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SocketConfig {
pub path: Option<PathBuf>,
#[cfg(unix)]
pub mode: u32,
pub app_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct IpcLimitsConfig {
pub max_connections: usize,
pub max_message_size: usize,
pub push_buffer_size: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct IpcTimeoutsConfig {
#[serde(rename = "request_timeout_ms")]
pub request: u64,
#[serde(rename = "read_timeout_ms")]
pub read: u64,
#[serde(rename = "write_timeout_ms")]
pub write: u64,
#[serde(rename = "subscription_read_timeout_ms")]
pub subscription_read: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct RateLimitConfig {
pub enabled: bool,
pub requests_per_second: u32,
pub burst_size: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ShutdownConfig {
#[serde(rename = "drain_timeout_ms")]
pub drain_timeout: u64,
}
impl Default for SocketConfig {
fn default() -> Self {
Self {
path: None,
#[cfg(unix)]
mode: 0o660,
app_name: None,
}
}
}
impl Default for IpcLimitsConfig {
fn default() -> Self {
Self {
max_connections: 100,
max_message_size: 1_048_576, push_buffer_size: 100, }
}
}
impl Default for IpcTimeoutsConfig {
fn default() -> Self {
Self {
request: 30_000,
read: 60_000,
write: 30_000,
subscription_read: 0, }
}
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
enabled: true,
requests_per_second: 100,
burst_size: 50,
}
}
}
impl Default for ShutdownConfig {
fn default() -> Self {
Self {
drain_timeout: 5_000, }
}
}
impl IpcConfig {
#[must_use]
pub fn load() -> Self {
let xdg_dirs = match xdg::BaseDirectories::with_prefix("acton") {
Ok(dirs) => dirs,
Err(e) => {
warn!("Failed to initialize XDG directories for IPC config: {}", e);
return Self::default();
}
};
xdg_dirs.find_config_file("ipc.toml").map_or_else(
|| {
info!("No IPC configuration file found, using defaults");
Self::default()
},
|path| {
info!("Loading IPC configuration from: {}", path.display());
match std::fs::read_to_string(&path) {
Ok(config_str) => match toml::from_str::<Self>(&config_str) {
Ok(config) => {
info!("Successfully loaded IPC configuration");
config
}
Err(e) => {
warn!(
"Failed to parse IPC configuration file {}: {}",
path.display(),
e
);
Self::default()
}
},
Err(e) => {
warn!(
"Failed to read IPC configuration file {}: {}",
path.display(),
e
);
Self::default()
}
}
},
)
}
#[must_use]
pub fn app_name(&self) -> String {
self.socket
.app_name
.clone()
.unwrap_or_else(Self::default_app_name)
}
fn default_app_name() -> String {
std::env::current_exe()
.ok()
.and_then(|p| p.file_stem().map(|s| s.to_string_lossy().into_owned()))
.unwrap_or_else(|| "acton".to_string())
}
#[must_use]
pub fn socket_path(&self) -> PathBuf {
self.socket.path.clone().unwrap_or_else(|| {
let app_name = self.app_name();
let runtime_dir = std::env::var("XDG_RUNTIME_DIR")
.map_or_else(|_| PathBuf::from("/tmp"), PathBuf::from);
runtime_dir.join("acton").join(&app_name).join("ipc.sock")
})
}
#[must_use]
pub fn socket_dir(&self) -> PathBuf {
self.socket_path()
.parent()
.map_or_else(|| PathBuf::from("/tmp/acton"), PathBuf::from)
}
#[must_use]
pub const fn request_timeout(&self) -> std::time::Duration {
std::time::Duration::from_millis(self.timeouts.request)
}
#[must_use]
pub const fn read_timeout(&self) -> Option<std::time::Duration> {
if self.timeouts.read == 0 {
None
} else {
Some(std::time::Duration::from_millis(self.timeouts.read))
}
}
#[must_use]
pub const fn write_timeout(&self) -> std::time::Duration {
std::time::Duration::from_millis(self.timeouts.write)
}
#[must_use]
pub const fn subscription_read_timeout(&self) -> Option<std::time::Duration> {
if self.timeouts.subscription_read == 0 {
None
} else {
Some(std::time::Duration::from_millis(
self.timeouts.subscription_read,
))
}
}
#[must_use]
pub const fn drain_timeout(&self) -> std::time::Duration {
std::time::Duration::from_millis(self.shutdown.drain_timeout)
}
#[must_use]
pub const fn is_rate_limited(&self) -> bool {
self.rate_limit.enabled
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = IpcConfig::default();
assert_eq!(config.limits.max_connections, 100);
assert_eq!(config.limits.max_message_size, 1_048_576);
assert_eq!(config.timeouts.request, 30_000);
}
#[test]
fn test_socket_path_default() {
let config = IpcConfig::default();
let path = config.socket_path();
assert!(path.to_string_lossy().contains("acton"));
assert!(path.to_string_lossy().ends_with("ipc.sock"));
}
#[test]
fn test_socket_path_override() {
let mut config = IpcConfig::default();
config.socket.path = Some(PathBuf::from("/custom/path/socket.sock"));
assert_eq!(
config.socket_path(),
PathBuf::from("/custom/path/socket.sock")
);
}
#[test]
fn test_app_name_override() {
let mut config = IpcConfig::default();
config.socket.app_name = Some("my_custom_app".to_string());
assert_eq!(config.app_name(), "my_custom_app");
}
#[test]
fn test_timeout_duration() {
let config = IpcConfig::default();
assert_eq!(
config.request_timeout(),
std::time::Duration::from_secs(30)
);
}
#[test]
fn test_socket_dir() {
let config = IpcConfig::default();
let dir = config.socket_dir();
let path = config.socket_path();
assert_eq!(dir, path.parent().unwrap());
}
#[cfg(unix)]
#[test]
fn test_socket_mode() {
let config = IpcConfig::default();
assert_eq!(config.socket.mode, 0o660);
}
#[test]
fn test_config_serialization() {
let config = IpcConfig::default();
let toml_str = toml::to_string(&config).unwrap();
let parsed: IpcConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(parsed.limits.max_connections, config.limits.max_connections);
}
#[test]
fn test_rate_limit_defaults() {
let config = IpcConfig::default();
assert!(config.is_rate_limited());
assert_eq!(config.rate_limit.requests_per_second, 100);
assert_eq!(config.rate_limit.burst_size, 50);
}
#[test]
fn test_shutdown_defaults() {
let config = IpcConfig::default();
assert_eq!(config.shutdown.drain_timeout, 5_000);
assert_eq!(
config.drain_timeout(),
std::time::Duration::from_secs(5)
);
}
#[test]
fn test_rate_limit_disabled() {
let mut config = IpcConfig::default();
config.rate_limit.enabled = false;
assert!(!config.is_rate_limited());
}
}