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}