zinit 0.3.6

Process supervisor with dependency management
Documentation
//! Xinet types - Socket activation proxy configuration and status
//!
//! Shared types used by both client and server for xinet proxy management.

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// Socket address - either Unix socket or TCP
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum SocketAddr {
    /// Unix domain socket path
    Unix(PathBuf),
    /// TCP address (host:port)
    Tcp(String),
}

impl SocketAddr {
    /// Create a Unix socket address
    pub fn unix<P: Into<PathBuf>>(path: P) -> Self {
        SocketAddr::Unix(path.into())
    }

    /// Create a TCP socket address
    pub fn tcp<S: Into<String>>(addr: S) -> Self {
        SocketAddr::Tcp(addr.into())
    }

    /// Check if this is a Unix socket
    pub fn is_unix(&self) -> bool {
        matches!(self, SocketAddr::Unix(_))
    }

    /// Check if this is a TCP socket
    pub fn is_tcp(&self) -> bool {
        matches!(self, SocketAddr::Tcp(_))
    }

    /// Get the path for Unix sockets
    pub fn as_unix_path(&self) -> Option<&PathBuf> {
        match self {
            SocketAddr::Unix(p) => Some(p),
            _ => None,
        }
    }

    /// Get the address string for TCP sockets
    pub fn as_tcp_addr(&self) -> Option<&str> {
        match self {
            SocketAddr::Tcp(a) => Some(a),
            _ => None,
        }
    }
}

impl std::fmt::Display for SocketAddr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            SocketAddr::Unix(p) => write!(f, "unix:{}", p.display()),
            SocketAddr::Tcp(a) => write!(f, "tcp:{}", a),
        }
    }
}

/// Configuration for a single xinet proxy
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct XinetConfig {
    /// Name of this proxy (for logging/identification)
    pub name: String,

    /// Frontend sockets to listen on (clients connect here)
    /// Supports multiple listeners (e.g., both TCP and Unix socket)
    pub listen: Vec<SocketAddr>,

    /// Backend socket to connect to (the actual service)
    pub backend: SocketAddr,

    /// Zinit service name that provides the backend
    pub service: String,

    /// Timeout in seconds to wait for backend socket after starting service
    #[serde(default = "default_connect_timeout")]
    pub connect_timeout: u64,

    /// Idle timeout in seconds - stop service if no connections for this long
    /// 0 means never auto-stop
    #[serde(default)]
    pub idle_timeout: u64,

    /// Whether to allow only one connection at a time
    #[serde(default)]
    pub single_connection: bool,
}

fn default_connect_timeout() -> u64 {
    30
}

impl XinetConfig {
    /// Create a new xinet configuration with a single listener
    pub fn new<S: Into<String>>(
        name: S,
        listen: SocketAddr,
        backend: SocketAddr,
        service: S,
    ) -> Self {
        Self {
            name: name.into(),
            listen: vec![listen],
            backend,
            service: service.into(),
            connect_timeout: default_connect_timeout(),
            idle_timeout: 0,
            single_connection: false,
        }
    }

    /// Create a new xinet configuration with multiple listeners
    pub fn new_multi<S: Into<String>>(
        name: S,
        listen: Vec<SocketAddr>,
        backend: SocketAddr,
        service: S,
    ) -> Self {
        Self {
            name: name.into(),
            listen,
            backend,
            service: service.into(),
            connect_timeout: default_connect_timeout(),
            idle_timeout: 0,
            single_connection: false,
        }
    }

    /// Add a listener address
    pub fn add_listen(mut self, addr: SocketAddr) -> Self {
        self.listen.push(addr);
        self
    }

    /// Set the connect timeout
    pub fn with_connect_timeout(mut self, seconds: u64) -> Self {
        self.connect_timeout = seconds;
        self
    }

    /// Set the idle timeout (0 to disable auto-stop)
    pub fn with_idle_timeout(mut self, seconds: u64) -> Self {
        self.idle_timeout = seconds;
        self
    }

    /// Enable single connection mode
    pub fn with_single_connection(mut self, single: bool) -> Self {
        self.single_connection = single;
        self
    }

    /// Validate the configuration
    pub fn validate(&self) -> Result<(), String> {
        if self.name.is_empty() {
            return Err("name is required".to_string());
        }
        if self.listen.is_empty() {
            return Err("at least one listen address is required".to_string());
        }
        if self.service.is_empty() {
            return Err("service name is required".to_string());
        }
        Ok(())
    }

    /// Get all listen addresses as a formatted string
    pub fn listen_addrs_string(&self) -> String {
        self.listen
            .iter()
            .map(|a| a.to_string())
            .collect::<Vec<_>>()
            .join(", ")
    }
}

/// Status of a xinet proxy
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProxyStatus {
    /// Proxy name
    pub name: String,
    /// Frontend listen address(es)
    pub listen: String,
    /// Backend address
    pub backend: String,
    /// Backend service name
    pub service: String,
    /// Total connections handled
    pub total_connections: u64,
    /// Currently active connections
    pub active_connections: usize,
    /// Total bytes forwarded to backend
    pub bytes_to_backend: u64,
    /// Total bytes forwarded from backend
    pub bytes_from_backend: u64,
    /// Whether the proxy is running
    pub running: bool,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_socket_addr_unix() {
        let addr = SocketAddr::unix("/tmp/test.sock");
        assert!(addr.is_unix());
        assert!(!addr.is_tcp());
        assert_eq!(addr.as_unix_path(), Some(&PathBuf::from("/tmp/test.sock")));
        assert_eq!(format!("{}", addr), "unix:/tmp/test.sock");
    }

    #[test]
    fn test_socket_addr_tcp() {
        let addr = SocketAddr::tcp("127.0.0.1:8080");
        assert!(!addr.is_unix());
        assert!(addr.is_tcp());
        assert_eq!(addr.as_tcp_addr(), Some("127.0.0.1:8080"));
        assert_eq!(format!("{}", addr), "tcp:127.0.0.1:8080");
    }

    #[test]
    fn test_xinet_config() {
        let config = XinetConfig::new(
            "myproxy",
            SocketAddr::unix("/tmp/frontend.sock"),
            SocketAddr::tcp("127.0.0.1:5432"),
            "postgres",
        )
        .with_idle_timeout(300)
        .with_single_connection(true);

        assert_eq!(config.name, "myproxy");
        assert_eq!(config.service, "postgres");
        assert_eq!(config.listen.len(), 1);
        assert_eq!(config.idle_timeout, 300);
        assert!(config.single_connection);
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_xinet_config_multi_listen() {
        let config = XinetConfig::new(
            "myproxy",
            SocketAddr::unix("/tmp/frontend.sock"),
            SocketAddr::tcp("127.0.0.1:5432"),
            "postgres",
        )
        .add_listen(SocketAddr::tcp("127.0.0.1:5433"));

        assert_eq!(config.listen.len(), 2);
        assert_eq!(
            config.listen_addrs_string(),
            "unix:/tmp/frontend.sock, tcp:127.0.0.1:5433"
        );
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_proxy_status_serialize() {
        let status = ProxyStatus {
            name: "test".to_string(),
            listen: "tcp:127.0.0.1:8080".to_string(),
            backend: "tcp:127.0.0.1:5432".to_string(),
            service: "postgres".to_string(),
            total_connections: 100,
            active_connections: 5,
            bytes_to_backend: 1024,
            bytes_from_backend: 2048,
            running: true,
        };

        let json = serde_json::to_string(&status).unwrap();
        assert!(json.contains("\"name\":\"test\""));
    }
}