use std::error::Error;
use std::fmt;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
pub const EXAMPLE_NAME: &str = "lifecycle";
pub const RUN_COMMAND: &str = "cargo run -p singbox-driver --example lifecycle";
pub const REQUIRED_ENV_KEYS: [&str; 3] = ["SINGBOX_BINARY", "SINGBOX_CONFIG", "SINGBOX_CONTROLLER"];
pub const OPTIONAL_ENV_KEYS: [&str; 1] = ["SINGBOX_SECRET"];
pub const PLAN_STEPS: [&str; 5] = [
"resolve the sing-box binary and config document from SINGBOX_BINARY / SINGBOX_CONFIG",
"start one standalone process through the CoreProcess port (SingBoxProcess)",
"wait for Clash API readiness through the CoreReadiness port at SINGBOX_CONTROLLER",
"read the live version and outbound inventory through CoreReadiness / OutboundPort",
"stop through CoreProcess and require CoreLifecycle::Stopped with no PID readback",
];
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EngineTarget {
binary: PathBuf,
config: PathBuf,
controller: SocketAddr,
secret: Option<String>,
}
impl EngineTarget {
pub fn new(
binary: impl Into<PathBuf>,
config: impl Into<PathBuf>,
controller: SocketAddr,
secret: Option<String>,
) -> Self {
Self {
binary: binary.into(),
config: config.into(),
controller,
secret,
}
}
pub fn binary(&self) -> &Path {
&self.binary
}
pub fn config(&self) -> &Path {
&self.config
}
pub const fn controller(&self) -> SocketAddr {
self.controller
}
pub fn secret(&self) -> Option<&str> {
self.secret.as_deref()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LifecyclePlan {
NotConfigured { missing: Vec<&'static str> },
Run(EngineTarget),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PlanError {
MissingEnv(&'static str),
InvalidController(String),
NonLoopbackController(String),
EphemeralControllerPort(String),
}
impl fmt::Display for PlanError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingEnv(key) => {
write!(formatter, "the engine is configured but {key} is not set")
}
Self::InvalidController(address) => write!(
formatter,
"SINGBOX_CONTROLLER must be a socket address such as 127.0.0.1:9090, got `{address}`"
),
Self::NonLoopbackController(address) => write!(
formatter,
"sing-box requires a loopback controller, got `{address}`"
),
Self::EphemeralControllerPort(address) => write!(
formatter,
"sing-box requires a fixed non-zero controller port, got `{address}`"
),
}
}
}
impl Error for PlanError {}
pub fn resolve_plan<F>(mut lookup: F) -> Result<LifecyclePlan, PlanError>
where
F: FnMut(&str) -> Option<String>,
{
let binary = normalize(lookup(REQUIRED_ENV_KEYS[0]));
let config = normalize(lookup(REQUIRED_ENV_KEYS[1]));
let mut missing = Vec::new();
if binary.is_none() {
missing.push(REQUIRED_ENV_KEYS[0]);
}
if config.is_none() {
missing.push(REQUIRED_ENV_KEYS[1]);
}
let (Some(binary), Some(config)) = (binary, config) else {
return Ok(LifecyclePlan::NotConfigured { missing });
};
let controller = normalize(lookup(REQUIRED_ENV_KEYS[2]))
.ok_or(PlanError::MissingEnv(REQUIRED_ENV_KEYS[2]))?;
let controller = validate_controller(&controller)?;
let secret = normalize(lookup(OPTIONAL_ENV_KEYS[0]));
Ok(LifecyclePlan::Run(EngineTarget::new(
binary, config, controller, secret,
)))
}
fn normalize(value: Option<String>) -> Option<String> {
value
.map(|value| value.trim().to_owned())
.filter(|value| !value.is_empty())
}
fn validate_controller(controller: &str) -> Result<SocketAddr, PlanError> {
let address = controller
.parse::<SocketAddr>()
.map_err(|_| PlanError::InvalidController(controller.to_owned()))?;
if !address.ip().is_loopback() {
return Err(PlanError::NonLoopbackController(controller.to_owned()));
}
if address.port() == 0 {
return Err(PlanError::EphemeralControllerPort(controller.to_owned()));
}
Ok(address)
}