Skip to main content

tango/server/
cli.rs

1//! Launching a server from its command line, see [`ServerArgs`] and
2//! [`ServerBuilder::from_args`].
3
4use std::borrow::Cow;
5use std::{env, process};
6use std::path::Path;
7
8use clap::{ArgAction, Args, Command, FromArgMatches};
9
10use tracing::info;
11
12use tracing_subscriber::filter::LevelFilter;
13use tracing_subscriber::EnvFilter;
14
15use crate::name::{Trl, TrlObject};
16use crate::testing::TestContext;
17
18use super::ServerBuilder;
19
20
21/// The command line every server is launched with: the instance it runs as, how much it
22/// logs, and where it listens.
23///
24/// A server with options of its own flattens this into its own parser and builds itself
25/// with [`ServerBuilder::from_raw_args`], and one with none of its own just calls
26/// [`ServerBuilder::from_args`]. Either way the server is named after its executable,
27/// unless it names itself, see [`ServerBuilder::from_raw_args`].
28///
29/// ```no_run
30/// use clap::Parser;
31/// use tango::server::{ServerArgs, ServerBuilder};
32///
33/// #[derive(Parser)]
34/// struct Cli {
35///     #[command(flatten)]
36///     server: ServerArgs,
37///     /// Simulate the hardware.
38///     #[arg(long)]
39///     simulate: bool,
40/// }
41///
42/// let cli = Cli::parse();
43/// let builder = ServerBuilder::from_raw_args(None, Some(&cli.server));
44/// ```
45#[derive(Debug, Clone, Args)]
46#[command(about = None, long_about = None)]
47pub struct ServerArgs {
48    /// The instance of the server, which is the second half of the `exe/inst` name it is
49    /// registered in the database under.
50    pub inst: String,
51    /// Log more, once per level: the default is info, `-v` is debug and `-vv` is trace.
52    /// The `RUST_LOG` environment variable has the last word.
53    #[arg(short, long, action = ArgAction::Count)]
54    pub verbose: u8,
55    /// The host to listen at, which is also the one the server registers its devices
56    /// under in the database.
57    #[arg(long)]
58    pub host: Option<String>,
59    /// The port to listen at, one the system picks if absent.
60    #[arg(long)]
61    pub port: Option<u16>,
62    /// Serve a database for testing in this process, with that device of this server in
63    /// it, instead of reaching the one of `TANGO_HOST`: clients reach it at the
64    /// `TANGO_HOST` the server logs. The class defaults to the name of the executable.
65    #[arg(long = "dev", value_name = "[CLASS::]NAME", value_parser = parse_dev)]
66    pub devs: Vec<(Option<String>, String)>,
67    /// A property of a device of the test database, see `--dev`.
68    #[arg(long = "prop", value_name = "DEV->NAME=VALUE", value_parser = parse_prop, requires = "devs")]
69    pub props: Vec<(String, String, String)>,
70    /// The port the test database listens at, see `--dev`, one the system picks if absent.
71    #[arg(long, requires = "devs")]
72    pub db_port: Option<u16>,
73}
74
75// We extend the [`ServerBuilder`] from here because this module is behind a feature.
76impl ServerBuilder {
77
78    /// Build the server from the command line of this process, see [`ServerArgs`], which
79    /// is what launching a server usually takes. The process exits with its usage when
80    /// the command line is invalid or asks for help.
81    ///
82    /// See [`Self::from_raw_args`] for what the server is named after.
83    pub fn from_args() -> Self {
84        Self::from_raw_args(None, None)
85    }
86
87    /// Build the server named after that executable and the instance of that command
88    /// line, see [`ServerArgs`], which is also where it listens. The logs of the process
89    /// are printed at the level it asks for.
90    ///
91    /// With no executable given, the server is named after the one of this process, as
92    /// cppTango names it: the name the process was run under, without its directory nor
93    /// the suffix of the platform's executables, which is what the Starter of a host runs
94    /// a server by. Giving one keeps the name of the server whatever its file is called.
95    ///
96    /// With no command line given, the one of this process is parsed, as
97    /// [`Self::from_args`] does; a server with options of its own parses it itself and
98    /// gives what it flattened, see [`ServerArgs`].
99    pub fn from_raw_args(exec: Option<&str>, args: Option<&ServerArgs>) -> Self {
100
101        let exec = exec.map(Cow::Borrowed).unwrap_or_else(|| Cow::Owned(exe()));
102
103        let args = match args {
104            Some(args) => Cow::Borrowed(args),
105            None => {
106                let matches = ServerArgs::augment_args(Command::new(exec.to_string())).get_matches();
107                let args = ServerArgs::from_arg_matches(&matches).unwrap_or_else(|err| err.exit());
108                Cow::Owned(args)
109            }
110        };
111
112        // The subscriber printing the logs of the process, at the level asked for unless
113        // `RUST_LOG` says otherwise. An application that installed one of its own keeps it.
114        let level = match args.verbose {
115            0 => LevelFilter::INFO,
116            1 => LevelFilter::DEBUG,
117            _ => LevelFilter::TRACE,
118        };
119
120        let _ = tracing_subscriber::fmt()
121            .with_target(false)
122            .with_env_filter(EnvFilter::builder()
123                .with_default_directive(level.into())
124                .from_env_lossy())
125            .try_init();
126
127        let mut builder = Self::new(&*exec, &args.inst);
128
129        if let Some(host) = &args.host {
130            builder = builder.host(host);
131        }
132
133        if let Some(port) = args.port {
134            builder = builder.port(port);
135        }
136
137        // The database for testing, served from now on, which the server is registered in
138        // and owns: failing to serve it is failing to launch the server at all.
139        if !args.devs.is_empty() {
140            let ctx = test_context(&exec, &args).unwrap_or_else(|err| {
141                eprintln!("Error: {err:#}");
142                process::exit(1)
143            });
144            builder = builder.database(ctx.database().clone());
145            builder.test_context = Some(ctx);
146        }
147
148        builder
149
150    }
151
152}
153
154/// The name of the executable of this process, which is the first half of the
155/// `exe/inst` name of a server launched from its command line: the name it was run
156/// under, without its directory nor the suffix of the platform's executables, the way
157/// cppTango names it. A link to an executable therefore names another server.
158fn exe() -> String {
159    let arg0 = env::args_os().next().unwrap_or_default();
160    let name = Path::new(&arg0).file_name().unwrap_or_default().to_string_lossy();
161    // The suffix is empty where executables have none, which strips nothing.
162    name.strip_suffix(env::consts::EXE_SUFFIX).unwrap_or(&name).to_string()
163}
164
165/// Serve the database for testing of that command line, see [`ServerArgs::devs`], the
166/// server of that executable being registered in it.
167fn test_context(exec: &str, args: &ServerArgs) -> crate::error::Result<TestContext> {
168
169    let ctx = TestContext::with_port(args.db_port.unwrap_or(0))?;
170
171    let devs = args.devs.iter()
172        .map(|(class, name)| (name.as_str(), class.as_deref().unwrap_or(exec)))
173        .collect::<Vec<_>>();
174    ctx.database().add_server(&format!("{exec}/{}", args.inst), &devs)?;
175
176    for (dev, name, value) in &args.props {
177        ctx.database().set_dev_prop(dev, (name.as_str(), value.as_str()))?;
178    }
179
180    info!(tango_host = %format_args!("{}:{}", ctx.host(), ctx.port()), "Serving the test database");
181    Ok(ctx)
182
183}
184
185/// A device of the test database, given as `CLASS::NAME` or `NAME`, see [`ServerArgs`].
186fn parse_dev(arg: &str) -> Result<(Option<String>, String), String> {
187    match arg.split_once("::") {
188        Some((class, name)) => Ok((Some(class.to_string()), name.to_string())),
189        None => Ok((None, arg.to_string())),
190    }
191}
192
193/// A property of the test database, given as `DEV->NAME=VALUE`, see [`ServerArgs`]: the
194/// property is named as a Tango Resource Locator names it, which the value follows.
195fn parse_prop(arg: &str) -> Result<(String, String, String), String> {
196
197    let (trl, value) = arg.split_once('=')
198        .ok_or("expected DEV->NAME=VALUE")?;
199    let trl = Trl::parse(trl)
200        .map_err(|err| format!("{trl} is not a property of a device: {err:?}"))?;
201
202    match (trl.object(), trl.property()) {
203        (TrlObject::Dev(dev), Some(name)) => Ok((dev.to_string(), name.to_string(), value.to_string())),
204        _ => Err(format!("{trl} is not a property of a device")),
205    }
206
207}