rstango 0.3.0

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

use std::borrow::Cow;
use std::{env, process};
use std::path::Path;

use clap::{ArgAction, Args, Command, FromArgMatches};

use tracing::info;

use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::EnvFilter;

use crate::name::{Trl, TrlObject};
use crate::testing::TestContext;

use super::ServerBuilder;


/// 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 [`ServerBuilder::from_raw_args`], and one with none of its own just calls
/// [`ServerBuilder::from_args`]. Either way the server is named after its executable,
/// unless it names itself, see [`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, one the system picks if absent.
    #[arg(long)]
    pub port: Option<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, see `--dev`, one the system picks if absent.
    #[arg(long, requires = "devs")]
    pub db_port: Option<u16>,
}

// We extend the [`ServerBuilder`] from here because this module is behind a feature.
impl ServerBuilder {

    /// Build the server from the command line of this process, see [`ServerArgs`], which
    /// is what launching a server usually takes. The process exits with its usage when
    /// the command line is invalid or asks for help.
    ///
    /// See [`Self::from_raw_args`] for what the server is named after.
    pub fn from_args() -> Self {
        Self::from_raw_args(None, None)
    }

    /// Build the server named after that executable and the instance of that command
    /// line, see [`ServerArgs`], which is also where it listens. The logs of the process
    /// are printed at the level it asks for.
    ///
    /// With no executable given, the server is named after the one of this process, as
    /// cppTango names it: the name the process was run under, without its directory nor
    /// the suffix of the platform's executables, which is what the Starter of a host runs
    /// a server by. Giving one keeps the name of the server whatever its file is called.
    ///
    /// With no command line given, the one of this process is parsed, as
    /// [`Self::from_args`] does; a server with options of its own parses it itself and
    /// gives what it flattened, see [`ServerArgs`].
    pub fn from_raw_args(exec: Option<&str>, args: Option<&ServerArgs>) -> Self {

        let exec = exec.map(Cow::Borrowed).unwrap_or_else(|| Cow::Owned(exe()));

        let args = match args {
            Some(args) => Cow::Borrowed(args),
            None => {
                let matches = ServerArgs::augment_args(Command::new(exec.to_string())).get_matches();
                let args = ServerArgs::from_arg_matches(&matches).unwrap_or_else(|err| err.exit());
                Cow::Owned(args)
            }
        };

        // The subscriber printing the logs of the process, at the level asked for unless
        // `RUST_LOG` says otherwise. An application that installed one of its own keeps it.
        let level = match args.verbose {
            0 => LevelFilter::INFO,
            1 => LevelFilter::DEBUG,
            _ => LevelFilter::TRACE,
        };

        let _ = tracing_subscriber::fmt()
            .with_target(false)
            .with_env_filter(EnvFilter::builder()
                .with_default_directive(level.into())
                .from_env_lossy())
            .try_init();

        let mut builder = Self::new(&*exec, &args.inst);

        if let Some(host) = &args.host {
            builder = builder.host(host);
        }

        if let Some(port) = args.port {
            builder = builder.port(port);
        }

        // The database for testing, served from now on, which the server is registered in
        // and owns: failing to serve it is failing to launch the server at all.
        if !args.devs.is_empty() {
            let ctx = test_context(&exec, &args).unwrap_or_else(|err| {
                eprintln!("Error: {err:#}");
                process::exit(1)
            });
            builder = builder.database(ctx.database().clone());
            builder.test_context = Some(ctx);
        }

        builder

    }

}

/// 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.
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.
fn test_context(exec: &str, args: &ServerArgs) -> crate::error::Result<TestContext> {

    let ctx = TestContext::with_port(args.db_port.unwrap_or(0))?;

    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 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")),
    }

}