Skip to main content

spvirit_tools/spvirit_client/
cli.rs

1use std::net::SocketAddr;
2use std::time::Duration;
3
4use argparse::{ArgumentParser, Store, StoreTrue};
5
6use super::search::{is_auto_addr_list_enabled, parse_name_servers};
7use super::types::PvGetOptions;
8
9/// Common CLI arguments shared across all PVA client tools.
10///
11/// Usage:
12/// ```ignore
13/// let mut common = CommonClientArgs::new();
14/// // optionally adjust defaults before parsing:
15/// // common.timeout_secs = 3;
16/// {
17///     let mut ap = ArgumentParser::new();
18///     common.add_to_parser(&mut ap);
19///     // add tool-specific args here
20///     ap.parse_args_or_exit();
21/// }
22/// common.init_tracing();
23/// let opts = common.into_pv_get_options("myPV".to_string())?;
24/// ```
25pub struct CommonClientArgs {
26    pub timeout_secs: u64,
27    pub server: String,
28    pub search_addr: String,
29    pub bind_addr: String,
30    pub name_server: String,
31    pub udp_port: u16,
32    pub tcp_port: u16,
33    pub debug: bool,
34    pub no_broadcast: bool,
35    pub authnz_user: String,
36    pub authnz_host: String,
37    /// Comma-separated dotted field paths (e.g. "value,alarm.severity").
38    /// Empty means "all fields".
39    pub fields: String,
40}
41
42impl CommonClientArgs {
43    pub fn new() -> Self {
44        Self {
45            timeout_secs: 5,
46            server: String::new(),
47            search_addr: String::new(),
48            bind_addr: String::new(),
49            name_server: String::new(),
50            udp_port: 5076,
51            tcp_port: 5075,
52            debug: false,
53            no_broadcast: false,
54            authnz_user: String::new(),
55            authnz_host: String::new(),
56            fields: String::new(),
57        }
58    }
59
60    /// Register common flags on the given parser.
61    ///
62    /// Short flags `-w` and `-d` are included so that tools like `pvlist`
63    /// that historically accepted them keep working.
64    pub fn add_to_parser<'a, 'b>(&'a mut self, ap: &'b mut ArgumentParser<'a>) {
65        ap.refer(&mut self.timeout_secs).add_option(
66            &["-w", "--timeout"],
67            Store,
68            "Timeout in seconds",
69        );
70        ap.refer(&mut self.server)
71            .add_option(&["--server"], Store, "Server address (ip:port)");
72        ap.refer(&mut self.search_addr).add_option(
73            &["--search-addr"],
74            Store,
75            "Search target IP (default EPICS_PVA_ADDR_LIST/auto broadcast)",
76        );
77        ap.refer(&mut self.bind_addr).add_option(
78            &["--bind-addr"],
79            Store,
80            "Local bind IP for UDP search",
81        );
82        ap.refer(&mut self.name_server).add_option(
83            &["--name-server"],
84            Store,
85            "PVA name server address (host:port, repeatable via EPICS_PVA_NAME_SERVERS)",
86        );
87        ap.refer(&mut self.udp_port)
88            .add_option(&["--udp-port"], Store, "UDP search port");
89        ap.refer(&mut self.tcp_port)
90            .add_option(&["--tcp-port"], Store, "TCP server default port");
91        ap.refer(&mut self.debug)
92            .add_option(&["-d", "--debug"], StoreTrue, "Enable debug logging");
93        ap.refer(&mut self.no_broadcast).add_option(
94            &["--no-broadcast"],
95            StoreTrue,
96            "Disable UDP broadcast/multicast search (also set via EPICS_PVA_AUTO_ADDR_LIST=NO)",
97        );
98        ap.refer(&mut self.authnz_user).add_option(
99            &["--authnz-user"],
100            Store,
101            "AuthNZ user override (takes precedence over env)",
102        );
103        ap.refer(&mut self.authnz_host).add_option(
104            &["--authnz-host"],
105            Store,
106            "AuthNZ host override (takes precedence over env)",
107        );
108        ap.refer(&mut self.fields).add_option(
109            &["-F", "--fields"],
110            Store,
111            "Comma-separated dotted field paths to request (e.g. value,alarm.severity). Empty = all fields.",
112        );
113    }
114
115    /// Initialise the `tracing_subscriber` based on `--debug`.
116    pub fn init_tracing(&self) {
117        let max_level = if self.debug {
118            tracing::Level::DEBUG
119        } else {
120            tracing::Level::INFO
121        };
122        tracing_subscriber::fmt().with_max_level(max_level).init();
123    }
124
125    /// Convert the parsed CLI strings into a ready-to-use `PvGetOptions`.
126    pub fn into_pv_get_options(
127        self,
128        pv_name: String,
129    ) -> Result<PvGetOptions, Box<dyn std::error::Error>> {
130        let mut opts = PvGetOptions::new(pv_name);
131        opts.timeout = Duration::from_secs(self.timeout_secs);
132        opts.udp_port = self.udp_port;
133        opts.tcp_port = self.tcp_port;
134        opts.debug = self.debug;
135        opts.no_broadcast = self.no_broadcast || !is_auto_addr_list_enabled();
136
137        if !self.server.is_empty() {
138            let addr: SocketAddr = self.server.parse()?;
139            opts.server_addr = Some(addr);
140        }
141        if !self.search_addr.is_empty() {
142            opts.search_addr = Some(self.search_addr.parse()?);
143        }
144        if !self.bind_addr.is_empty() {
145            opts.bind_addr = Some(self.bind_addr.parse()?);
146        }
147
148        let mut ns = parse_name_servers(&self.name_server);
149        if let Ok(env) = std::env::var("EPICS_PVA_NAME_SERVERS") {
150            ns.extend(parse_name_servers(&env));
151        }
152        opts.name_servers = ns;
153
154        if !self.authnz_user.is_empty() {
155            opts.authnz_user = Some(self.authnz_user);
156        }
157        if !self.authnz_host.is_empty() {
158            opts.authnz_host = Some(self.authnz_host);
159        }
160
161        Ok(opts)
162    }
163
164    /// Parsed `--fields` value as a list of dotted paths (read-only).
165    pub fn fields_list(&self) -> Vec<String> {
166        parse_fields_arg(&self.fields)
167    }
168}
169
170/// Parse the comma-separated `--fields` value into a list of dotted paths.
171/// Whitespace around each entry is trimmed; empty entries are dropped.
172pub fn parse_fields_arg(raw: &str) -> Vec<String> {
173    raw.split(',')
174        .map(|s| s.trim().to_string())
175        .filter(|s| !s.is_empty())
176        .collect()
177}