Skip to main content

tango/server/
cli.rs

1//! Launching a server from its command line, see [`ServerArgs`] and
2//! [`ServerBuilder::from_args`](super::ServerBuilder::from_args).
3
4use std::sync::OnceLock;
5use std::path::Path;
6use std::env;
7
8use clap::{ArgAction, Args};
9
10use tracing::info;
11
12use crate::error::{Error, ErrorKind};
13use crate::name::{Trl, TrlObject};
14use crate::testing::TestContext;
15
16use super::terminate;
17
18
19/// The command line every server is launched with: the instance it runs as, how much it
20/// logs, and where it listens.
21///
22/// A server with options of its own flattens this into its own parser and builds itself
23/// with [`super::ServerBuilder::from_raw_args`], and one with none of its own just calls
24/// [`super::ServerBuilder::from_args`]. Either way the server is named after its executable,
25/// unless it names itself, see [`super::ServerBuilder::from_raw_args`].
26///
27/// ```no_run
28/// use clap::Parser;
29/// use tango::server::{ServerArgs, ServerBuilder};
30///
31/// #[derive(Parser)]
32/// struct Cli {
33///     #[command(flatten)]
34///     server: ServerArgs,
35///     /// Simulate the hardware.
36///     #[arg(long)]
37///     simulate: bool,
38/// }
39///
40/// let cli = Cli::parse();
41/// let builder = ServerBuilder::from_raw_args(None, Some(&cli.server));
42/// ```
43#[derive(Debug, Clone, Args)]
44#[command(about = None, long_about = None)]
45pub struct ServerArgs {
46    /// The instance of the server, which is the second half of the `exe/inst` name it is
47    /// registered in the database under.
48    pub inst: String,
49    /// Log more, once per level: the default is info, `-v` is debug and `-vv` is trace.
50    /// The `RUST_LOG` environment variable has the last word.
51    #[arg(short, long, action = ArgAction::Count)]
52    pub verbose: u8,
53    /// The host to listen at, which is also the one the server registers its devices
54    /// under in the database.
55    #[arg(long)]
56    pub host: Option<String>,
57    /// The port to listen at, or the inclusive range of ports to pick one from, one the
58    /// system picks if absent.
59    #[arg(long, value_name = "PORT[-PORT]", value_parser = parse_ports)]
60    pub port: Option<(u16, u16)>,
61    /// The port the heartbeat of the events is published at, or the inclusive range of
62    /// ports to pick one from, one the system picks if absent.
63    #[arg(long, env = "TANGO_ZMQ_HEARTBEAT_PORT", value_name = "PORT[-PORT]", value_parser = parse_ports)]
64    pub zmq_heartbeat_port: Option<(u16, u16)>,
65    /// The port the events are published at, or the inclusive range of ports to pick one
66    /// from, one the system picks if absent.
67    #[arg(long, env = "TANGO_ZMQ_EVENT_PORT", value_name = "PORT[-PORT]", value_parser = parse_ports)]
68    pub zmq_event_port: Option<(u16, u16)>,
69    /// Serve a database for testing in this process, with that device of this server in
70    /// it, instead of reaching the one of `TANGO_HOST`: clients reach it at the
71    /// `TANGO_HOST` the server logs. The class defaults to the name of the executable.
72    #[arg(long = "dev", value_name = "[CLASS::]NAME", value_parser = parse_dev)]
73    pub devs: Vec<(Option<String>, String)>,
74    /// A property of a device of the test database, see `--dev`.
75    #[arg(long = "prop", value_name = "DEV->NAME=VALUE", value_parser = parse_prop, requires = "devs")]
76    pub props: Vec<(String, String, String)>,
77    /// The port the test database listens at, or the inclusive range of ports to pick one
78    /// from, see `--dev`, one the system picks if absent.
79    #[arg(long, value_name = "PORT[-PORT]", value_parser = parse_ports, requires = "devs")]
80    pub db_port: Option<(u16, u16)>,
81}
82
83/// Whether the signal handler was installed, which is only ever tried once: what it failed
84/// with is kept, so that every server asking for it fails alike.
85static SIGNAL_HANDLER: OnceLock<crate::error::Result<()>> = OnceLock::new();
86
87/// Install the signal handler of this process, see [`ServerBuilder::terminate_on_signals`](super::ServerBuilder::terminate_on_signals), if
88/// it is not already, failing if it cannot be, an application having installed one of
89/// its own above all.
90pub(super) fn install_signal_handler() -> crate::error::Result<()> {
91    SIGNAL_HANDLER.get_or_init(|| ctrlc::try_set_handler(terminate)
92        .map_err(|err| Error::new(ErrorKind::Internal)
93            .with_description("Failed to install the signal handler, which can be disabled on the server builder")
94            .with_source(err)))
95        .clone()
96}
97
98/// The name of the executable of this process, which is the first half of the
99/// `exe/inst` name of a server launched from its command line: the name it was run
100/// under, without its directory nor the suffix of the platform's executables, the way
101/// cppTango names it. A link to an executable therefore names another server.
102pub(super) fn exe() -> String {
103    let arg0 = env::args_os().next().unwrap_or_default();
104    let name = Path::new(&arg0).file_name().unwrap_or_default().to_string_lossy();
105    // The suffix is empty where executables have none, which strips nothing.
106    name.strip_suffix(env::consts::EXE_SUFFIX).unwrap_or(&name).to_string()
107}
108
109/// Serve the database for testing of that command line, see [`ServerArgs::devs`], the
110/// server of that executable being registered in it.
111pub(super) fn test_context(exec: &str, args: &ServerArgs) -> crate::error::Result<TestContext> {
112
113    let (db_start, db_end) = args.db_port.unwrap_or((0, 0));
114    let ctx = TestContext::with_port(db_start..=db_end)?;
115
116    let devs = args.devs.iter()
117        .map(|(class, name)| (name.as_str(), class.as_deref().unwrap_or(exec)))
118        .collect::<Vec<_>>();
119    ctx.database().add_server(&format!("{exec}/{}", args.inst), &devs)?;
120
121    for (dev, name, value) in &args.props {
122        ctx.database().set_dev_prop(dev, (name.as_str(), value.as_str()))?;
123    }
124
125    info!(tango_host = %format_args!("{}:{}", ctx.host(), ctx.port()), "Serving the test database");
126    Ok(ctx)
127
128}
129
130/// A port or an inclusive range of ports, given as `PORT` or `START-END`, see
131/// [`ServerArgs`], which is also what the variables of cppTango giving the ports of the
132/// events are read as. A range including port 0 is the system picking any port, see
133/// [`crate::backend::BackendServerConfig`].
134fn parse_ports(arg: &str) -> Result<(u16, u16), String> {
135
136    let (start, end) = arg.split_once('-').unwrap_or((arg, arg));
137    let start = start.trim().parse::<u16>()
138        .map_err(|err| format!("{start} is not a port: {err}"))?;
139    let end = end.trim().parse::<u16>()
140        .map_err(|err| format!("{end} is not a port: {err}"))?;
141
142    if start > end {
143        return Err(format!("{start}-{end} is an empty range of ports"));
144    }
145
146    Ok((start, end))
147
148}
149
150/// A device of the test database, given as `CLASS::NAME` or `NAME`, see [`ServerArgs`].
151fn parse_dev(arg: &str) -> Result<(Option<String>, String), String> {
152    match arg.split_once("::") {
153        Some((class, name)) => Ok((Some(class.to_string()), name.to_string())),
154        None => Ok((None, arg.to_string())),
155    }
156}
157
158/// A property of the test database, given as `DEV->NAME=VALUE`, see [`ServerArgs`]: the
159/// property is named as a Tango Resource Locator names it, which the value follows.
160fn parse_prop(arg: &str) -> Result<(String, String, String), String> {
161
162    let (trl, value) = arg.split_once('=')
163        .ok_or("expected DEV->NAME=VALUE")?;
164    let trl = Trl::parse(trl)
165        .map_err(|err| format!("{trl} is not a property of a device: {err:?}"))?;
166
167    match (trl.object(), trl.property()) {
168        (TrlObject::Dev(dev), Some(name)) => Ok((dev.to_string(), name.to_string(), value.to_string())),
169        _ => Err(format!("{trl} is not a property of a device")),
170    }
171
172}