use std::io;
use crate::sdk::{ConfigError, ServiceState};
use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error)]
pub enum SupervisorError {
#[error("service not found: {0}")]
ServiceNotFound(String),
#[error("service already running: {0}")]
ServiceAlreadyRunning(String),
#[error("service not running: {0}")]
ServiceNotRunning(String),
#[error("invalid state for service '{service}': expected {expected}, got {actual}")]
InvalidState {
service: String,
expected: String,
actual: ServiceState,
},
#[error("cannot start service '{service}': {reason}")]
CannotStart { service: String, reason: String },
#[error("cyclic dependency detected: {}", .0.join(" -> "))]
CyclicDependency(Vec<String>),
#[error("config error: {0}")]
Config(#[from] ConfigError),
#[error("spawn error for service '{service}': {message}")]
SpawnError { service: String, message: String },
#[error("signal error for service '{service}' (signal {signal}): {message}")]
SignalError {
service: String,
signal: String,
message: String,
},
#[error("validation error: {0}")]
Validation(String),
#[error("graph error: {0}")]
Graph(#[from] GraphError),
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error(
"cannot {operation} service '{service}': failed to stop dependent '{dependent}': {reason}"
)]
CascadeStopFailed {
operation: String,
service: String,
dependent: String,
reason: String,
},
#[error("failed to delete config file for service '{service}': {reason}")]
ConfigDeleteFailed { service: String, reason: String },
}
#[derive(Debug, thiserror::Error)]
pub enum GraphError {
#[error("service not found: {0}")]
ServiceNotFound(String),
#[error("service already exists: {0}")]
ServiceAlreadyExists(String),
#[error("dependency not found: {0}")]
DependencyNotFound(String),
#[error("cyclic dependency detected: {}", .0.join(" -> "))]
CyclicDependency(Vec<String>),
#[error("cannot remove service '{service}': still has dependents: {}", dependents.join(", "))]
HasDependents {
service: String,
dependents: Vec<String>,
},
#[error("conflict: service '{service}' conflicts with running service '{conflicts_with}'")]
Conflict {
service: String,
conflicts_with: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProcessConflictInfo {
pub pid: u32,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cmdline: Option<String>,
}
#[derive(Debug, Clone)]
pub enum BlockedReason {
WaitingOn(Vec<String>),
ConflictsWith(Vec<String>),
Both {
waiting_on: Vec<String>,
conflicts_with: Vec<String>,
},
PortConflict {
ports: Vec<u16>,
services: Vec<String>,
},
ExternalPortConflict {
port: u16,
pid: Option<u32>,
process_name: Option<String>,
},
ProcessNameConflict {
filter: String,
processes: Vec<ProcessConflictInfo>,
},
}
impl std::fmt::Display for BlockedReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BlockedReason::WaitingOn(services) => {
write!(f, "waiting on: {}", services.join(", "))
}
BlockedReason::ConflictsWith(services) => {
write!(f, "conflicts with: {}", services.join(", "))
}
BlockedReason::Both {
waiting_on,
conflicts_with,
} => {
write!(
f,
"waiting on: {}; conflicts with: {}",
waiting_on.join(", "),
conflicts_with.join(", ")
)
}
BlockedReason::PortConflict { ports, services } => {
let port_str = ports
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(", ");
write!(
f,
"port conflict on {}: already in use by {}",
port_str,
services.join(", ")
)
}
BlockedReason::ExternalPortConflict {
port,
pid,
process_name,
} => {
let process_info = match (pid, process_name) {
(Some(p), Some(name)) => format!("{} (PID {})", name, p),
(Some(p), None) => format!("PID {}", p),
(None, Some(name)) => name.clone(),
(None, None) => "unknown process".to_string(),
};
write!(
f,
"port {} is in use by external process: {}",
port, process_info
)
}
BlockedReason::ProcessNameConflict { filter, processes } => {
write!(
f,
"process filter '{}' matched {} running process(es): ",
filter,
processes.len()
)?;
for (i, proc) in processes.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{} (PID {})", proc.name, proc.pid)?;
}
Ok(())
}
}
}
}
impl BlockedReason {
pub fn waiting_on(&self) -> Vec<String> {
match self {
BlockedReason::WaitingOn(v) => v.clone(),
BlockedReason::Both { waiting_on, .. } => waiting_on.clone(),
BlockedReason::ConflictsWith(_)
| BlockedReason::PortConflict { .. }
| BlockedReason::ExternalPortConflict { .. }
| BlockedReason::ProcessNameConflict { .. } => vec![],
}
}
pub fn conflicts_with(&self) -> Vec<String> {
match self {
BlockedReason::ConflictsWith(v) => v.clone(),
BlockedReason::Both { conflicts_with, .. } => conflicts_with.clone(),
BlockedReason::WaitingOn(_)
| BlockedReason::PortConflict { .. }
| BlockedReason::ExternalPortConflict { .. }
| BlockedReason::ProcessNameConflict { .. } => vec![],
}
}
}
pub type SupervisorResult<T> = Result<T, SupervisorError>;
pub type GraphResult<T> = Result<T, GraphError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_supervisor_error_display() {
let err = SupervisorError::ServiceNotFound("test".to_string());
assert_eq!(err.to_string(), "service not found: test");
let err = SupervisorError::CyclicDependency(vec!["a".to_string(), "b".to_string()]);
assert_eq!(err.to_string(), "cyclic dependency detected: a -> b");
}
#[test]
fn test_blocked_reason_display() {
let reason = BlockedReason::WaitingOn(vec!["a".to_string(), "b".to_string()]);
assert_eq!(reason.to_string(), "waiting on: a, b");
let reason = BlockedReason::ConflictsWith(vec!["c".to_string()]);
assert_eq!(reason.to_string(), "conflicts with: c");
let reason = BlockedReason::PortConflict {
ports: vec![8080, 9000],
services: vec!["svc1".to_string()],
};
assert_eq!(
reason.to_string(),
"port conflict on 8080, 9000: already in use by svc1"
);
}
#[test]
fn test_blocked_reason_accessors() {
let reason = BlockedReason::Both {
waiting_on: vec!["a".to_string()],
conflicts_with: vec!["b".to_string()],
};
assert_eq!(reason.waiting_on(), vec!["a"]);
assert_eq!(reason.conflicts_with(), vec!["b"]);
}
}