use std::sync::OnceLock;
use std::path::Path;
use std::env;
use clap::{ArgAction, Args};
use tracing::info;
use crate::error::{Error, ErrorKind};
use crate::name::{Trl, TrlObject};
use crate::testing::TestContext;
use super::terminate;
#[derive(Debug, Clone, Args)]
#[command(about = None, long_about = None)]
pub struct ServerArgs {
pub inst: String,
#[arg(short, long, action = ArgAction::Count)]
pub verbose: u8,
#[arg(long)]
pub host: Option<String>,
#[arg(long, value_name = "PORT[-PORT]", value_parser = parse_ports)]
pub port: Option<(u16, u16)>,
#[arg(long, env = "TANGO_ZMQ_HEARTBEAT_PORT", value_name = "PORT[-PORT]", value_parser = parse_ports)]
pub zmq_heartbeat_port: Option<(u16, u16)>,
#[arg(long, env = "TANGO_ZMQ_EVENT_PORT", value_name = "PORT[-PORT]", value_parser = parse_ports)]
pub zmq_event_port: Option<(u16, u16)>,
#[arg(long = "dev", value_name = "[CLASS::]NAME", value_parser = parse_dev)]
pub devs: Vec<(Option<String>, String)>,
#[arg(long = "prop", value_name = "DEV->NAME=VALUE", value_parser = parse_prop, requires = "devs")]
pub props: Vec<(String, String, String)>,
#[arg(long, value_name = "PORT[-PORT]", value_parser = parse_ports, requires = "devs")]
pub db_port: Option<(u16, u16)>,
}
static SIGNAL_HANDLER: OnceLock<crate::error::Result<()>> = OnceLock::new();
pub(super) fn install_signal_handler() -> crate::error::Result<()> {
SIGNAL_HANDLER.get_or_init(|| ctrlc::try_set_handler(terminate)
.map_err(|err| Error::new(ErrorKind::Internal)
.with_description("Failed to install the signal handler, which can be disabled on the server builder")
.with_source(err)))
.clone()
}
pub(super) fn exe() -> String {
let arg0 = env::args_os().next().unwrap_or_default();
let name = Path::new(&arg0).file_name().unwrap_or_default().to_string_lossy();
name.strip_suffix(env::consts::EXE_SUFFIX).unwrap_or(&name).to_string()
}
pub(super) fn test_context(exec: &str, args: &ServerArgs) -> crate::error::Result<TestContext> {
let (db_start, db_end) = args.db_port.unwrap_or((0, 0));
let ctx = TestContext::with_port(db_start..=db_end)?;
let devs = args.devs.iter()
.map(|(class, name)| (name.as_str(), class.as_deref().unwrap_or(exec)))
.collect::<Vec<_>>();
ctx.database().add_server(&format!("{exec}/{}", args.inst), &devs)?;
for (dev, name, value) in &args.props {
ctx.database().set_dev_prop(dev, (name.as_str(), value.as_str()))?;
}
info!(tango_host = %format_args!("{}:{}", ctx.host(), ctx.port()), "Serving the test database");
Ok(ctx)
}
fn parse_ports(arg: &str) -> Result<(u16, u16), String> {
let (start, end) = arg.split_once('-').unwrap_or((arg, arg));
let start = start.trim().parse::<u16>()
.map_err(|err| format!("{start} is not a port: {err}"))?;
let end = end.trim().parse::<u16>()
.map_err(|err| format!("{end} is not a port: {err}"))?;
if start > end {
return Err(format!("{start}-{end} is an empty range of ports"));
}
Ok((start, end))
}
fn parse_dev(arg: &str) -> Result<(Option<String>, String), String> {
match arg.split_once("::") {
Some((class, name)) => Ok((Some(class.to_string()), name.to_string())),
None => Ok((None, arg.to_string())),
}
}
fn parse_prop(arg: &str) -> Result<(String, String, String), String> {
let (trl, value) = arg.split_once('=')
.ok_or("expected DEV->NAME=VALUE")?;
let trl = Trl::parse(trl)
.map_err(|err| format!("{trl} is not a property of a device: {err:?}"))?;
match (trl.object(), trl.property()) {
(TrlObject::Dev(dev), Some(name)) => Ok((dev.to_string(), name.to_string(), value.to_string())),
_ => Err(format!("{trl} is not a property of a device")),
}
}