arti_relay/
cli.rs

1//! The command-line interface.
2//!
3//! See [`Cli`].
4
5use std::ffi::OsString;
6use std::path::PathBuf;
7
8use clap::{Args, Command, Parser, Subcommand, ValueEnum};
9use fs_mistrust::anon_home::PathExt as _;
10use std::sync::LazyLock;
11use tor_config::{ConfigurationSource, ConfigurationSources};
12use tor_config_path::CfgPathError;
13
14use crate::config::default_config_paths;
15
16/// A cached copy of the default config paths.
17///
18/// We cache the values to ensure they are consistent between the help text and the values used.
19static DEFAULT_CONFIG_PATHS: LazyLock<Result<Vec<PathBuf>, CfgPathError>> =
20    LazyLock::new(default_config_paths);
21
22/// A Rust Tor relay implementation.
23#[derive(Clone, Debug, Parser)]
24#[command(author = "The Tor Project Developers")]
25#[command(version)]
26#[command(defer = cli_cmd_post_processing)]
27pub(crate) struct Cli {
28    /// Sub-commands.
29    #[command(subcommand)]
30    pub(crate) command: Commands,
31
32    /// Global arguments available for all sub-commands.
33    ///
34    /// These arguments may be specified before or after the subcommand argument.
35    #[clap(flatten)]
36    pub(crate) global: GlobalArgs,
37}
38
39/// Perform post-processing on the [`Command`] generated by clap for [`Cli`].
40///
41/// We use this to append the default config paths to the help text.
42fn cli_cmd_post_processing(cli: Command) -> Command {
43    /// Append the paths to the help text.
44    fn fmt_help(help: Option<&str>, paths: &[PathBuf]) -> String {
45        let help = help.map(|x| format!("{x}\n\n")).unwrap_or("".to_string());
46        let paths: Vec<_> = paths
47            .iter()
48            .map(|x| x.anonymize_home().to_string())
49            .collect();
50        let paths = paths.join("\n");
51
52        const DESC: &str =
53            "If no paths are provided, the following config paths will be used if they exist:";
54        format!("{help}{DESC}\n\n{paths}")
55    }
56
57    // Show the default paths in the "--help" text.
58    match &*DEFAULT_CONFIG_PATHS {
59        Ok(paths) => cli.mut_arg("config", |arg| {
60            if let Some(help) = arg.get_long_help() {
61                let help = help.to_string();
62                arg.long_help(fmt_help(Some(&help), paths))
63            } else if let Some(help) = arg.get_help() {
64                let help = help.to_string();
65                arg.long_help(fmt_help(Some(&help), paths))
66            } else {
67                arg.long_help(fmt_help(None, paths))
68            }
69        }),
70        Err(_e) => cli,
71    }
72}
73
74/// Main subcommands.
75#[derive(Clone, Debug, Subcommand)]
76pub(crate) enum Commands {
77    /// Run the relay.
78    Run(RunArgs),
79    /// Print build information.
80    BuildInfo,
81}
82
83/// Global arguments for all commands.
84// NOTE: `global = true` should be set for each field
85#[derive(Clone, Debug, Args)]
86pub(crate) struct GlobalArgs {
87    /// Override the log level from the configuration.
88    #[arg(long, short, global = true)]
89    #[arg(value_name = "LEVEL")]
90    pub(crate) log_level: Option<LogLevel>,
91
92    /// Don't check permissions on the files we use.
93    #[arg(long, global = true)]
94    pub(crate) disable_fs_permission_checks: bool,
95
96    /// Override config file parameters, using TOML-like syntax.
97    #[arg(long = "option", short, global = true)]
98    #[arg(value_name = "KEY=VALUE")]
99    pub(crate) options: Vec<String>,
100
101    /// Config files and directories to read.
102    // NOTE: We append the default config paths to the help text in `cli_cmd_post_processing`.
103    // NOTE: This value does not take into account the default config paths,
104    // so this is private while the `GlobalArgs::config()` method is public instead.
105    #[arg(long, short, global = true)]
106    #[arg(value_name = "PATH")]
107    config: Vec<OsString>,
108}
109
110impl GlobalArgs {
111    /// Get the configuration sources.
112    ///
113    /// You may also want to set a [`Mistrust`](fs_mistrust::Mistrust)
114    /// and any additional configuration option overrides
115    /// using [`push_option`](ConfigurationSources::push_option).
116    pub(crate) fn config(&self) -> Result<ConfigurationSources, CfgPathError> {
117        // Use `try_from_cmdline` to be consistent with Arti.
118        let mut cfg_sources = ConfigurationSources::try_from_cmdline(
119            || {
120                Ok(DEFAULT_CONFIG_PATHS
121                    .as_ref()
122                    .map_err(Clone::clone)?
123                    .iter()
124                    .map(ConfigurationSource::from_path))
125            },
126            &self.config,
127            &self.options,
128        )?;
129
130        // TODO: These text strings may become stale if the configuration structure changes,
131        // and they're not checked at compile time.
132        // Can we change `ConfigurationSources` in some way to allow overrides from an existing
133        // builder?
134        if self.disable_fs_permission_checks {
135            cfg_sources.push_option("storage.permissions.dangerously_trust_everyone=true");
136        }
137
138        if let Some(log_level) = self.log_level {
139            cfg_sources.push_option(format!("logging.console={log_level}"));
140        }
141
142        Ok(cfg_sources)
143    }
144}
145
146/// Arguments when running an Arti relay.
147#[derive(Clone, Debug, Args)]
148pub(crate) struct RunArgs {}
149
150/// Log levels allowed by the cli.
151#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
152pub(crate) enum LogLevel {
153    /// See [`tracing::Level::ERROR`].
154    #[value(help = None)]
155    Error,
156    /// See [`tracing::Level::WARN`].
157    #[value(help = None)]
158    Warn,
159    /// See [`tracing::Level::INFO`].
160    #[value(help = None)]
161    Info,
162    /// See [`tracing::Level::DEBUG`].
163    #[value(help = None)]
164    Debug,
165    /// See [`tracing::Level::TRACE`].
166    #[value(help = None)]
167    Trace,
168}
169
170impl std::fmt::Display for LogLevel {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        match self {
173            Self::Error => write!(f, "error"),
174            Self::Warn => write!(f, "warn"),
175            Self::Info => write!(f, "info"),
176            Self::Debug => write!(f, "debug"),
177            Self::Trace => write!(f, "trace"),
178        }
179    }
180}
181
182impl From<LogLevel> for tracing::metadata::Level {
183    fn from(x: LogLevel) -> Self {
184        match x {
185            LogLevel::Error => Self::ERROR,
186            LogLevel::Warn => Self::WARN,
187            LogLevel::Info => Self::INFO,
188            LogLevel::Debug => Self::DEBUG,
189            LogLevel::Trace => Self::TRACE,
190        }
191    }
192}
193
194#[cfg(test)]
195mod test {
196    // @@ begin test lint list maintained by maint/add_warning @@
197    #![allow(clippy::bool_assert_comparison)]
198    #![allow(clippy::clone_on_copy)]
199    #![allow(clippy::dbg_macro)]
200    #![allow(clippy::mixed_attributes_style)]
201    #![allow(clippy::print_stderr)]
202    #![allow(clippy::print_stdout)]
203    #![allow(clippy::single_char_pattern)]
204    #![allow(clippy::unwrap_used)]
205    #![allow(clippy::unchecked_duration_subtraction)]
206    #![allow(clippy::useless_vec)]
207    #![allow(clippy::needless_pass_by_value)]
208    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
209
210    use super::*;
211
212    #[test]
213    fn common_flags() {
214        Cli::parse_from(["arti-relay", "build-info"]);
215        Cli::parse_from(["arti-relay", "run"]);
216
217        let cli = Cli::parse_from(["arti-relay", "--log-level", "warn", "run"]);
218        assert_eq!(cli.global.log_level, Some(LogLevel::Warn));
219        let cli = Cli::parse_from(["arti-relay", "run", "--log-level", "warn"]);
220        assert_eq!(cli.global.log_level, Some(LogLevel::Warn));
221
222        let cli = Cli::parse_from(["arti-relay", "--disable-fs-permission-checks", "run"]);
223        assert!(cli.global.disable_fs_permission_checks);
224        let cli = Cli::parse_from(["arti-relay", "run", "--disable-fs-permission-checks"]);
225        assert!(cli.global.disable_fs_permission_checks);
226    }
227
228    #[test]
229    fn clap_bug() {
230        let cli = Cli::parse_from(["arti-relay", "-o", "foo=1", "run"]);
231        assert_eq!(cli.global.options, vec!["foo=1"]);
232
233        let cli = Cli::parse_from(["arti-relay", "-o", "foo=1", "-o", "bar=2", "run"]);
234        assert_eq!(cli.global.options, vec!["foo=1", "bar=2"]);
235
236        // this is https://github.com/clap-rs/clap/issues/3938
237        // TODO: this is a footgun, and we should consider alternatives to clap's 'global' args
238        let cli = Cli::parse_from(["arti-relay", "-o", "foo=1", "run", "-o", "bar=2"]);
239        assert_eq!(cli.global.options, vec!["bar=2"]);
240    }
241
242    #[test]
243    fn global_args_are_global() {
244        let cmd = Command::new("test");
245        let cmd = GlobalArgs::augment_args(cmd);
246
247        // check that each argument in `GlobalArgs` has "global" set
248        for arg in cmd.get_arguments() {
249            assert!(
250                arg.is_global_set(),
251                "'global' must be set for {:?}",
252                arg.get_long()
253            );
254        }
255    }
256}