use serde::{Deserialize, Serialize};
use super::state::ServiceState;
use crate::server::error::ProcessConflictInfo;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum State {
Inactive,
Blocked,
Starting,
Running,
Stopping,
Exited,
Failed,
}
impl State {
pub fn is_running(&self) -> bool {
matches!(self, State::Running)
}
pub fn is_active(&self) -> bool {
matches!(self, State::Starting | State::Running | State::Stopping)
}
}
impl std::fmt::Display for State {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
State::Inactive => write!(f, "inactive"),
State::Blocked => write!(f, "blocked"),
State::Starting => write!(f, "starting"),
State::Running => write!(f, "running"),
State::Stopping => write!(f, "stopping"),
State::Exited => write!(f, "exited"),
State::Failed => write!(f, "failed"),
}
}
}
impl From<&ServiceState> for State {
fn from(state: &ServiceState) -> Self {
match state {
ServiceState::Inactive => State::Inactive,
ServiceState::Blocked { .. } => State::Blocked,
ServiceState::Starting { .. } => State::Starting,
ServiceState::Running { .. } => State::Running,
ServiceState::Stopping { .. } => State::Stopping,
ServiceState::Exited { .. } => State::Exited,
ServiceState::Failed { .. } => State::Failed,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ServiceStatus {
pub name: String,
pub state: State,
#[serde(default)]
pub pid: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl ServiceStatus {
pub fn from_state(name: String, state: &ServiceState) -> Self {
let (state_enum, pid, exit_code, error) = match state {
ServiceState::Inactive => (State::Inactive, 0, None, None),
ServiceState::Blocked { waiting_on } => {
let err = if waiting_on.is_empty() {
None
} else {
Some(format!("waiting on: {}", waiting_on.join(", ")))
};
(State::Blocked, 0, None, err)
}
ServiceState::Starting { pid } => (State::Starting, *pid, None, None),
ServiceState::Running { pid } => (State::Running, *pid, None, None),
ServiceState::Stopping { pid } => (State::Stopping, *pid, None, None),
ServiceState::Exited { exit_code: code } => (State::Exited, 0, *code, None),
ServiceState::Failed { reason } => {
let (code, err) = match reason {
super::state::FailureReason::ExitCode { code } => (Some(*code), None),
super::state::FailureReason::Signal { signal } => {
(None, Some(format!("killed by signal {}", signal)))
}
super::state::FailureReason::StartTimeout => {
(None, Some("start timeout".to_string()))
}
super::state::FailureReason::StopTimeout => {
(None, Some("stop timeout".to_string()))
}
super::state::FailureReason::HealthCheckFailed { attempts } => (
None,
Some(format!("health check failed after {} attempts", attempts)),
),
super::state::FailureReason::DependencyFailed { service } => {
(None, Some(format!("dependency '{}' failed", service)))
}
super::state::FailureReason::SpawnError { message } => {
(None, Some(format!("spawn error: {}", message)))
}
super::state::FailureReason::MissingDependency { dependency } => {
(None, Some(format!("missing dependency '{}'", dependency)))
}
};
(State::Failed, 0, code, err)
}
};
Self {
name,
state: state_enum,
pid,
exit_code,
error,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ServiceStats {
pub pid: u32,
pub memory_bytes: u64,
pub cpu_percent: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct XinetStatus {
pub name: String,
pub running: bool,
pub connections: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PingResponse {
pub version: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OkResponse {
pub ok: bool,
}
impl Default for OkResponse {
fn default() -> Self {
Self { ok: true }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CreateResult {
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ServiceInfo {
pub name: String,
pub state: ServiceState,
pub is_target: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DependencyInfo {
pub name: String,
pub dep_type: DepType,
pub state: ServiceState,
pub satisfied: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LegacyServiceStatus {
pub name: String,
pub state: ServiceState,
pub is_target: bool,
pub dependencies: Vec<DependencyInfo>,
pub uptime_secs: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DepType {
After,
Requires,
Wants,
Conflicts,
}
impl std::fmt::Display for DepType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DepType::After => write!(f, "after"),
DepType::Requires => write!(f, "requires"),
DepType::Wants => write!(f, "wants"),
DepType::Conflicts => write!(f, "conflicts"),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LogLine {
pub timestamp_ms: u64,
pub service: String,
pub stream: LogStream,
pub content: String,
}
impl LogLine {
pub fn to_string_format(&self) -> String {
format!("{} [{}] {}", self.timestamp_ms, self.stream, self.content)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogStream {
Stdout,
Stderr,
Syslog,
}
impl std::fmt::Display for LogStream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogStream::Stdout => write!(f, "stdout"),
LogStream::Stderr => write!(f, "stderr"),
LogStream::Syslog => write!(f, "syslog"),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProcessConflictDetails {
pub filter: String,
pub processes: Vec<ProcessConflictInfo>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WhyBlocked {
pub name: String,
pub blocked: bool,
pub waiting_on: Vec<String>,
pub conflicts_with: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub port_conflict: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub process_conflict: Option<ProcessConflictDetails>,
pub ascii: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TreeResponse {
pub ascii: String,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ReloadResult {
pub added: Vec<String>,
pub removed: Vec<String>,
pub changed: Vec<String>,
#[serde(default)]
pub config_errors: Vec<(String, String)>,
}
impl ReloadResult {
pub fn has_changes(&self) -> bool {
!self.added.is_empty() || !self.removed.is_empty() || !self.changed.is_empty()
}
pub fn total_changes(&self) -> usize {
self.added.len() + self.removed.len() + self.changed.len()
}
pub fn has_config_errors(&self) -> bool {
!self.config_errors.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BulkStartResult {
pub started: Vec<String>,
pub count: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BulkStopResult {
pub stopped: Vec<String>,
pub count: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BulkDeleteResult {
pub deleted: Vec<String>,
pub count: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AddServiceResult {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(default)]
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddServiceParams {
pub config: super::config::ServiceConfig,
#[serde(default)]
pub persist: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PrepareRestartResult {
pub state_path: String,
pub ready: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChildProcessInfo {
pub pid: u32,
pub name: String,
pub memory_bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChildrenResponse {
pub children: Vec<ChildProcessInfo>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sdk::state::FailureReason;
#[test]
fn test_service_status_from_state() {
let status =
ServiceStatus::from_state("test".to_string(), &ServiceState::Running { pid: 123 });
assert_eq!(status.state, State::Running);
assert_eq!(status.pid, 123);
assert!(status.exit_code.is_none());
assert!(status.error.is_none());
let status = ServiceStatus::from_state(
"test".to_string(),
&ServiceState::Failed {
reason: FailureReason::ExitCode { code: 1 },
},
);
assert_eq!(status.state, State::Failed);
assert_eq!(status.exit_code, Some(1));
let status = ServiceStatus::from_state(
"test".to_string(),
&ServiceState::Blocked {
waiting_on: vec!["dep1".to_string(), "dep2".to_string()],
},
);
assert_eq!(status.state, State::Blocked);
assert!(status.error.as_ref().unwrap().contains("dep1"));
}
#[test]
fn test_ok_response() {
let resp = OkResponse::default();
assert!(resp.ok);
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"ok\":true"));
}
#[test]
fn test_service_stats() {
let stats = ServiceStats {
pid: 123,
memory_bytes: 1024 * 1024,
cpu_percent: 5.5,
};
let json = serde_json::to_string(&stats).unwrap();
assert!(json.contains("\"pid\":123"));
assert!(json.contains("\"memory_bytes\":1048576"));
}
#[test]
fn test_xinet_status() {
let status = XinetStatus {
name: "proxy1".to_string(),
running: true,
connections: 5,
};
let json = serde_json::to_string(&status).unwrap();
assert!(json.contains("\"running\":true"));
assert!(json.contains("\"connections\":5"));
}
#[test]
fn test_log_line_to_string() {
let line = LogLine {
timestamp_ms: 1234567890,
service: "test".to_string(),
stream: LogStream::Stdout,
content: "Hello".to_string(),
};
let s = line.to_string_format();
assert!(s.contains("1234567890"));
assert!(s.contains("stdout"));
assert!(s.contains("Hello"));
}
}