rstango 0.4.0

Implementation of the Tango distributed control system
Documentation
//! Launching a server from its command line, see [`ServerArgs`] and
//! [`ServerBuilder::from_args`](super::ServerBuilder::from_args).

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;


/// The command line every server is launched with: the instance it runs as, how much it
/// logs, and where it listens.
///
/// A server with options of its own flattens this into its own parser and builds itself
/// with [`super::ServerBuilder::from_raw_args`], and one with none of its own just calls
/// [`super::ServerBuilder::from_args`]. Either way the server is named after its executable,
/// unless it names itself, see [`super::ServerBuilder::from_raw_args`].
///
/// ```no_run
/// use clap::Parser;
/// use tango::server::{ServerArgs, ServerBuilder};
///
/// #[derive(Parser)]
/// struct Cli {
///     #[command(flatten)]
///     server: ServerArgs,
///     /// Simulate the hardware.
///     #[arg(long)]
///     simulate: bool,
/// }
///
/// let cli = Cli::parse();
/// let builder = ServerBuilder::from_raw_args(None, Some(&cli.server));
/// ```
#[derive(Debug, Clone, Args)]
#[command(about = None, long_about = None)]
pub struct ServerArgs {
    /// The instance of the server, which is the second half of the `exe/inst` name it is
    /// registered in the database under.
    pub inst: String,
    /// Log more, once per level: the default is info, `-v` is debug and `-vv` is trace.
    /// The `RUST_LOG` environment variable has the last word.
    #[arg(short, long, action = ArgAction::Count)]
    pub verbose: u8,
    /// The host to listen at, which is also the one the server registers its devices
    /// under in the database.
    #[arg(long)]
    pub host: Option<String>,
    /// The port to listen at, or the inclusive range of ports to pick one from, one the
    /// system picks if absent.
    #[arg(long, value_name = "PORT[-PORT]", value_parser = parse_ports)]
    pub port: Option<(u16, u16)>,
    /// The port the heartbeat of the events is published at, or the inclusive range of
    /// ports to pick one from, one the system picks if absent.
    #[arg(long, env = "TANGO_ZMQ_HEARTBEAT_PORT", value_name = "PORT[-PORT]", value_parser = parse_ports)]
    pub zmq_heartbeat_port: Option<(u16, u16)>,
    /// The port the events are published at, or the inclusive range of ports to pick one
    /// from, one the system picks if absent.
    #[arg(long, env = "TANGO_ZMQ_EVENT_PORT", value_name = "PORT[-PORT]", value_parser = parse_ports)]
    pub zmq_event_port: Option<(u16, u16)>,
    /// Serve a database for testing in this process, with that device of this server in
    /// it, instead of reaching the one of `TANGO_HOST`: clients reach it at the
    /// `TANGO_HOST` the server logs. The class defaults to the name of the executable.
    #[arg(long = "dev", value_name = "[CLASS::]NAME", value_parser = parse_dev)]
    pub devs: Vec<(Option<String>, String)>,
    /// A property of a device of the test database, see `--dev`.
    #[arg(long = "prop", value_name = "DEV->NAME=VALUE", value_parser = parse_prop, requires = "devs")]
    pub props: Vec<(String, String, String)>,
    /// The port the test database listens at, or the inclusive range of ports to pick one
    /// from, see `--dev`, one the system picks if absent.
    #[arg(long, value_name = "PORT[-PORT]", value_parser = parse_ports, requires = "devs")]
    pub db_port: Option<(u16, u16)>,
}

/// Whether the signal handler was installed, which is only ever tried once: what it failed
/// with is kept, so that every server asking for it fails alike.
static SIGNAL_HANDLER: OnceLock<crate::error::Result<()>> = OnceLock::new();

/// Install the signal handler of this process, see [`ServerBuilder::terminate_on_signals`](super::ServerBuilder::terminate_on_signals), if
/// it is not already, failing if it cannot be, an application having installed one of
/// its own above all.
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()
}

/// The name of the executable of this process, which is the first half of the
/// `exe/inst` name of a server launched from its command line: the name it was run
/// under, without its directory nor the suffix of the platform's executables, the way
/// cppTango names it. A link to an executable therefore names another server.
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();
    // The suffix is empty where executables have none, which strips nothing.
    name.strip_suffix(env::consts::EXE_SUFFIX).unwrap_or(&name).to_string()
}

/// Serve the database for testing of that command line, see [`ServerArgs::devs`], the
/// server of that executable being registered in it.
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)

}

/// A port or an inclusive range of ports, given as `PORT` or `START-END`, see
/// [`ServerArgs`], which is also what the variables of cppTango giving the ports of the
/// events are read as. A range including port 0 is the system picking any port, see
/// [`crate::backend::BackendServerConfig`].
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))

}

/// A device of the test database, given as `CLASS::NAME` or `NAME`, see [`ServerArgs`].
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())),
    }
}

/// A property of the test database, given as `DEV->NAME=VALUE`, see [`ServerArgs`]: the
/// property is named as a Tango Resource Locator names it, which the value follows.
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")),
    }

}