systemg 0.58.1

An agent-friendly general process composer.
Documentation
//! Constants and configuration values for the systemg daemon.
//!
//! This module centralizes all magic numbers, strings, and configuration values
//! used throughout the daemon to improve maintainability and clarity.

use std::{cmp::Ordering, str::FromStr, time::Duration};

/// Permission mode for runtime directories: owner read/write/execute only (`rwx------`).
///
/// Applied to state and log directories so other local users cannot traverse or
/// read the control socket, PID file, or logs.
#[cfg(unix)]
pub const PRIVATE_DIR_MODE: u32 = 0o700;

/// Permission mode for sensitive runtime files: owner read/write only (`rw-------`).
///
/// Applied to the supervisor PID file, config hint, and control socket.
#[cfg(unix)]
pub const PRIVATE_FILE_MODE: u32 = 0o600;

/// Typed lock abstraction for enforcing proper lock acquisition order in the daemon.
///
/// This enum ensures that locks are always acquired in a consistent order to prevent
/// deadlocks. The ordering is enforced through the `Ord` trait implementation.
///
/// # Lock Acquisition Rules
///
/// Locks MUST be acquired in ascending order of their discriminant values:
/// 1. `Processes` - Child process management
/// 2. `PidFile` - Process ID persistence
/// 3. `StateFile` - Service state persistence
/// 4. `RestartCounts` - Restart attempt tracking
/// 5. `ManualStopFlags` - Manual stop flag tracking
/// 6. `RestartSuppressed` - Restart suppression flags
///
/// # Example
/// ```ignore
/// // Correct: Acquiring in order
/// let _proc_lock = daemon.lock(DaemonLock::Processes)?;
/// let _pid_lock = daemon.lock(DaemonLock::PidFile)?;
///
/// // Incorrect: Would cause deadlock potential
/// // let _pid_lock = daemon.lock(DaemonLock::PidFile)?;
/// // let _proc_lock = daemon.lock(DaemonLock::Processes)?; // WRONG!
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum DaemonLock {
    /// Lock for the shared map of running service processes.
    /// Priority: 1 (must be acquired first)
    Processes = 1,

    /// Lock for the PID file containing service to process ID mappings.
    /// Priority: 2
    PidFile = 2,

    /// Lock for the service state file containing status and metadata.
    /// Priority: 3
    StateFile = 3,

    /// Lock for tracking restart attempt counts per service.
    /// Priority: 4
    RestartCounts = 4,

    /// Lock for tracking which services were manually stopped.
    /// Priority: 5
    ManualStopFlags = 5,

    /// Lock for tracking services with suppressed restarts.
    /// Priority: 6
    RestartSuppressed = 6,

    /// Lock for tracking services with an in-flight reconcile-triggered restart.
    /// Priority: 7
    RestartInFlight = 7,

    /// Lock for tracking dependents stopped as casualties of a crashed dependency,
    /// so they can be revived once every felling dependency recovers.
    /// Priority: 8 (must be acquired last)
    StoppedForDependency = 8,
}

impl DaemonLock {
    /// Returns the numeric priority of this lock type.
    /// Lower numbers must be acquired before higher numbers.
    pub const fn priority(&self) -> u8 {
        *self as u8
    }

    /// Returns a human-readable name for this lock type.
    pub const fn name(&self) -> &'static str {
        match self {
            Self::Processes => "processes",
            Self::PidFile => "pid_file",
            Self::StateFile => "state_file",
            Self::RestartCounts => "restart_counts",
            Self::ManualStopFlags => "manual_stop_flags",
            Self::RestartSuppressed => "restart_suppressed",
            Self::RestartInFlight => "restart_in_flight",
            Self::StoppedForDependency => "stopped_for_dependency",
        }
    }

    /// Checks if acquiring `other` after `self` would violate lock ordering.
    /// Returns `true` if the acquisition order is valid.
    pub const fn can_acquire_after(&self, other: &Self) -> bool {
        self.priority() > other.priority()
    }
}

impl PartialOrd for DaemonLock {
    /// Handles partial cmp.
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for DaemonLock {
    /// Handles cmp.
    fn cmp(&self, other: &Self) -> Ordering {
        self.priority().cmp(&other.priority())
    }
}

/// Name of the PID file stored in the state directory.
/// Contains mappings of service names to process IDs.
pub const PID_FILE_NAME: &str = "pid.xml";

/// Lock file suffix for PID file to ensure exclusive access.
pub const PID_LOCK_SUFFIX: &str = ".lock";

/// Name of the service state file stored in the state directory.
/// Contains the current state and metadata for all managed services.
pub const STATE_FILE_NAME: &str = "state.xml";

/// Default shell used for executing service commands and hooks.
pub const DEFAULT_SHELL: &str = "sh";

/// `PATH` installed for a privilege-dropped service started from a clean
/// environment, so it can still resolve system binaries without inheriting the
/// supervisor's (root's) `PATH`.
pub const DEFAULT_SERVICE_PATH: &str = "/usr/local/bin:/usr/bin:/bin";

/// Shell argument flag for executing command strings.
pub const SHELL_COMMAND_FLAG: &str = "-c";

/// Caller/session-scoped environment variables that are stripped from
/// long-lived service environments by default. These describe the SSH session
/// of whoever ran `sysg` and must not leak into daemonized services, where they
/// pin a stale `ssh-agent` and orphan it under PID 1.
pub const SESSION_SCOPED_ENV_VARS: &[&str] = &[
    "SSH_AUTH_SOCK",
    "SSH_AGENT_PID",
    "SSH_CLIENT",
    "SSH_CONNECTION",
    "SSH_TTY",
];

/// Number of checks to perform when waiting for a process to become ready.
/// Used in conjunction with PROCESS_CHECK_INTERVAL.
pub const PROCESS_READY_CHECKS: usize = 10;

/// Interval between process readiness checks.
pub const PROCESS_CHECK_INTERVAL: Duration = Duration::from_millis(100);

/// Maximum time to wait for a service to start before timing out.
/// Applied during service initialization and health checks.
pub const SERVICE_START_TIMEOUT: Duration = Duration::from_secs(5);

/// Minimum continuous survival time required before a process without a health
/// check is reported ready. This prevents immediate bind and startup failures
/// from being mistaken for a successful launch between process probes.
pub const SERVICE_START_STABILITY: Duration = Duration::from_millis(250);

/// Default maximum duration of one health-check probe.
pub const DEFAULT_HEALTH_ATTEMPT_TIMEOUT: Duration = Duration::from_secs(30);

/// Default delay between health-check probes.
pub const DEFAULT_HEALTH_INTERVAL: Duration = Duration::from_secs(2);

/// Default minimum number of health-check probes before readiness fails.
pub const DEFAULT_HEALTH_RETRIES: u32 = 3;

/// Maximum time a `pre_start` command may run before it is killed and the start
/// fails. Pre-starts run inside the supervisor's single-writer owner thread, so
/// an UNBOUNDED pre-start that hangs (e.g. a network/proxy call that never
/// returns) would freeze EVERY subsequent mutation across ALL projects. Generous
/// enough for real migrations/DB-waits, but finite so one hung op cannot wedge
/// the whole supervisor.
pub const PRE_START_TIMEOUT: Duration = Duration::from_secs(300);

/// How long a foreground log-follow keeps retrying a project it has not seen
/// yet. The control socket (and so `supervisor_running()`) goes live before any
/// service is spawned, so a project absent from the first status snapshot is
/// still booting rather than gone. Must outlast a slow project's first unit
/// registration, including `pre_start` work such as builds and DB waits.
pub const FOREGROUND_ATTACH_GRACE: Duration = Duration::from_secs(120);

/// How long an attaching `start` waits for a queued project boot to settle
/// before reporting which services never came up.
///
/// Must exceed a realistic pre-start budget: a service whose `pre_start` waits
/// on a DB/tunnel can legitimately take minutes, and cutting across that window
/// reports a FALSE failure for a service that was still coming up. Observed
/// live: a 60s `wait-for-db.sh` plus a 5s restart backoff made a 120s grace
/// straddle the retry cycle, blaming a service that then ran healthily. Sized
/// against [`PRE_START_TIMEOUT`] plus room for one retry.
pub const START_SETTLE_GRACE: Duration = Duration::from_secs(360);

/// How long a synchronous project attach waits for the freshly-booted project
/// to record its first PID before seeding the status cache. Bounded so a project
/// whose services exit immediately cannot wedge the attaching caller.
pub const PROJECT_PID_SETTLE_TIMEOUT: Duration = Duration::from_secs(10);

/// How long a stop waits to CONFIRM a signalled process actually died before
/// reporting failure. A stop that records `stopped` without verifying death is
/// how a project came to report itself down while its services still held their
/// ports; the wait covers a slow shutdown without hiding a failed kill.
pub const STOP_VERIFY_TIMEOUT: Duration = Duration::from_secs(10);

/// Narrowest terminal width the progress spinner will believe. Anything smaller
/// is treated as a failed probe rather than a real terminal, so a zero/unset
/// winsize cannot truncate the status line down to a useless stub.
pub const MIN_SPINNER_WIDTH: usize = 40;

/// Terminal width assumed when the real width cannot be determined.
pub const DEFAULT_TERMINAL_WIDTH: usize = 80;

/// Polling interval when waiting for service state changes.
pub const SERVICE_POLL_INTERVAL: Duration = Duration::from_millis(50);

/// Number of attempts to verify a service is running after restart.
pub const POST_RESTART_VERIFY_ATTEMPTS: usize = 2;

/// Delay between post-restart verification attempts.
pub const POST_RESTART_VERIFY_DELAY: Duration = Duration::from_millis(200);

/// Maximum number of log lines to display in status output.
/// Prevents overwhelming the terminal with excessive log data.
pub const MAX_STATUS_LOG_LINES: usize = 50;

/// Number of recent service log lines shown when no explicit limit is given.
pub const DEFAULT_LOG_LINES: usize = 100;

/// Buffer size for log output streams (stdout/stderr).
pub const LOG_BUFFER_SIZE: usize = 8192;

/// Maximum size of a single newline-framed control-socket command.
///
/// Caps the buffer `read_command` allocates so one connection cannot exhaust
/// supervisor memory by streaming bytes without a newline.
pub const MAX_CONTROL_LINE: u64 = 1024 * 1024;

/// Format string for hook labels combining stage and outcome.
/// Example: "pre_start.pending", "post_start.success"
pub const HOOK_LABEL_FORMAT: &str = "{}.{}";

/// Error message for malformed environment file lines.
pub const ENV_FILE_MALFORMED_MSG: &str =
    "Ignoring malformed line in env file for '{}': {}";

/// Error message for environment file read failures.
pub const ENV_FILE_READ_ERROR_MSG: &str = "Failed to read env file for '{}': {}";

/// Error message for hook timeout parsing failures.
pub const HOOK_TIMEOUT_PARSE_ERROR_MSG: &str =
    "Invalid timeout '{}' for hook {} on '{}': {}";

/// Error message for insufficient process signal permissions.
pub const INSUFFICIENT_SIGNAL_PERMISSIONS_MSG: &str =
    "Insufficient permissions to signal process group {} for '{}'";

/// Error message for process tree termination failures.
pub const PROCESS_TREE_TERM_FAILURE_MSG: &str =
    "Failed to terminate process tree rooted at PID {} for '{}'";

/// Deployment strategies for service restarts.
///
/// This enum provides type-safe handling of deployment strategies, ensuring
/// that only valid strategies can be used throughout the codebase.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeploymentStrategy {
    /// Rolling deployment: Start new instance before stopping old one.
    /// Useful for zero-downtime deployments where port availability is managed.
    Rolling,

    /// Immediate deployment: Stop old instance then start new one.
    /// Traditional restart approach with potential brief downtime.
    Immediate,
}

impl DeploymentStrategy {
    /// Convert the deployment strategy to its string representation.
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Rolling => "rolling",
            Self::Immediate => "immediate",
        }
    }
}

impl FromStr for DeploymentStrategy {
    type Err = String;

    /// Handles from str.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "rolling" => Ok(Self::Rolling),
            "immediate" => Ok(Self::Immediate),
            _ => Err(format!("Unknown deployment strategy: {}", s)),
        }
    }
}

impl Default for DeploymentStrategy {
    /// Returns the default this item.
    fn default() -> Self {
        Self::Immediate
    }
}

/// Default deployment strategy when not specified in configuration.
pub const DEFAULT_DEPLOYMENT_STRATEGY: &str = "immediate";

/// Rolling deployment strategy identifier.
pub const ROLLING_DEPLOYMENT: &str = "rolling";

/// Immediate deployment strategy identifier.
pub const IMMEDIATE_DEPLOYMENT: &str = "immediate";

/// Message logged when skipping cron-managed services during bulk operations.
pub const SKIP_CRON_SERVICE_MSG: &str = "Skipping cron-managed service '{}' during bulk start; scheduled execution will launch it";

/// Message logged when skipping cron services during restart.
pub const SKIP_CRON_RESTART_MSG: &str = "Skipping cron-managed service '{}' during restart; scheduled execution will launch it";