sprites 0.1.0

Official Rust SDK for Sprites - stateful sandbox environments from Fly.io
Documentation
//! Service management for sprites
//!
//! This module provides management of long-running services inside sprites.
//! Services are managed processes that can be started, stopped, and signaled.
//!
//! # Example
//!
//! ```no_run
//! use sprites::{SpritesClient, ServiceRequest};
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//!     let client = SpritesClient::new("token");
//!     let sprite = client.sprite("my-sprite");
//!
//!     // Create a service
//!     let request = ServiceRequest {
//!         cmd: "python".to_string(),
//!         args: vec!["-m".into(), "http.server".into(), "8000".into()],
//!         needs: vec![],
//!         http_port: Some(8000),
//!     };
//!     sprite.create_service("web", request).await?;
//!
//!     // Start the service
//!     sprite.start_service("web").await?;
//!
//!     // Check service status
//!     let state = sprite.get_service("web").await?;
//!     println!("Service status: {:?}", state.status);
//!
//!     // Stop the service
//!     sprite.stop_service("web").await?;
//!
//!     Ok(())
//! }
//! ```

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

/// Request to create or update a service
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceRequest {
    /// Command to run
    pub cmd: String,

    /// Command arguments
    #[serde(default)]
    pub args: Vec<String>,

    /// Service dependencies (names of other services that must be running)
    #[serde(default)]
    pub needs: Vec<String>,

    /// HTTP port for proxy routing (optional)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub http_port: Option<u16>,
}

/// Current state of a service
#[derive(Debug, Clone, Deserialize)]
pub struct ServiceState {
    /// Service name
    pub name: String,

    /// Current status
    pub status: ServiceStatus,

    /// Process ID (when running)
    #[serde(default)]
    pub pid: Option<u32>,

    /// When the service was started
    #[serde(default)]
    pub started_at: Option<DateTime<Utc>>,

    /// Error message (if failed)
    #[serde(default)]
    pub error: Option<String>,

    /// Number of times the service has been restarted
    #[serde(default)]
    pub restart_count: Option<u32>,

    /// When the next restart will occur (if scheduled)
    #[serde(default)]
    pub next_restart_at: Option<DateTime<Utc>>,

    /// Command being run
    #[serde(default)]
    pub cmd: Option<String>,

    /// Command arguments
    #[serde(default)]
    pub args: Option<Vec<String>>,

    /// Service dependencies
    #[serde(default)]
    pub needs: Option<Vec<String>>,

    /// HTTP port
    #[serde(default)]
    pub http_port: Option<u16>,
}

/// Service status
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ServiceStatus {
    /// Service is not running
    #[default]
    Stopped,
    /// Service is starting up
    Starting,
    /// Service is running
    Running,
    /// Service is shutting down
    Stopping,
    /// Service failed to start or crashed
    Failed,
}

/// Unix signal to send to a service
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Signal {
    /// Hangup signal (SIGHUP = 1)
    SIGHUP,
    /// Interrupt signal (SIGINT = 2)
    SIGINT,
    /// Quit signal (SIGQUIT = 3)
    SIGQUIT,
    /// Kill signal (SIGKILL = 9)
    SIGKILL,
    /// Termination signal (SIGTERM = 15)
    SIGTERM,
    /// User-defined signal 1 (SIGUSR1 = 10)
    SIGUSR1,
    /// User-defined signal 2 (SIGUSR2 = 12)
    SIGUSR2,
}

impl Signal {
    /// Get the numeric signal value
    pub fn as_i32(&self) -> i32 {
        match self {
            Signal::SIGHUP => 1,
            Signal::SIGINT => 2,
            Signal::SIGQUIT => 3,
            Signal::SIGKILL => 9,
            Signal::SIGUSR1 => 10,
            Signal::SIGUSR2 => 12,
            Signal::SIGTERM => 15,
        }
    }

    /// Get the signal name
    pub fn name(&self) -> &'static str {
        match self {
            Signal::SIGHUP => "SIGHUP",
            Signal::SIGINT => "SIGINT",
            Signal::SIGQUIT => "SIGQUIT",
            Signal::SIGKILL => "SIGKILL",
            Signal::SIGUSR1 => "SIGUSR1",
            Signal::SIGUSR2 => "SIGUSR2",
            Signal::SIGTERM => "SIGTERM",
        }
    }
}

impl std::fmt::Display for Signal {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.name())
    }
}

impl Serialize for Signal {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_i32(self.as_i32())
    }
}

/// A log entry from a service
#[derive(Debug, Clone, Deserialize)]
pub struct ServiceLogEntry {
    /// Log message type
    #[serde(rename = "type")]
    pub log_type: String,

    /// Log data/message
    #[serde(default)]
    pub data: Option<String>,

    /// Timestamp
    #[serde(default)]
    pub timestamp: Option<DateTime<Utc>>,
}

/// Stream of service log entries
pub struct ServiceLogStream {
    reader: tokio::sync::mpsc::Receiver<ServiceLogEntry>,
    _task: tokio::task::JoinHandle<()>,
}

impl ServiceLogStream {
    /// Create a new service log stream from a WebSocket connection
    pub(crate) fn new(
        reader: tokio::sync::mpsc::Receiver<ServiceLogEntry>,
        task: tokio::task::JoinHandle<()>,
    ) -> Self {
        Self {
            reader,
            _task: task,
        }
    }

    /// Get the next log entry
    pub async fn next(&mut self) -> Option<ServiceLogEntry> {
        self.reader.recv().await
    }

    /// Process all log entries with a callback
    pub async fn process_all<F>(&mut self, mut callback: F)
    where
        F: FnMut(ServiceLogEntry),
    {
        while let Some(entry) = self.next().await {
            callback(entry);
        }
    }
}

impl ServiceState {
    /// Check if the service is currently running
    pub fn is_running(&self) -> bool {
        self.status == ServiceStatus::Running
    }

    /// Check if the service has failed
    pub fn is_failed(&self) -> bool {
        self.status == ServiceStatus::Failed
    }

    /// Check if the service is stopped
    pub fn is_stopped(&self) -> bool {
        self.status == ServiceStatus::Stopped
    }

    /// Get the service name
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Get the error message if the service failed
    pub fn error(&self) -> Option<&str> {
        self.error.as_deref()
    }
}