Skip to main content

talon/
entry.rs

1//! Entry point shared by the `talahon` binary and its deprecated `talon`
2//! alias: parse the command line, set up logging and dispatch.
3
4use std::ffi::OsStr;
5use std::path::{Path, PathBuf};
6use std::process::ExitCode;
7use std::sync::Arc;
8
9use crate::cli::{AuditCommand, Cli, Command, EvalCommand};
10use crate::logging::{self, LogFormat, LogTarget};
11use talon_core::config::TalonConfig;
12use talon_core::env::EnvFile;
13
14fn exit_code(code: i32) -> ExitCode {
15    ExitCode::from(u8::try_from(code).unwrap_or(1))
16}
17
18/// How the binary was started.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum InvokedAs {
21    /// `talahon`.
22    Current,
23    /// The deprecated `talon` alias (the alias binary or a link named so).
24    Legacy,
25}
26
27impl InvokedAs {
28    /// From `argv[0]`: [`InvokedAs::Legacy`] when its file name is
29    /// [`LEGACY_BINARY_NAME`] (with or without `.exe`).
30    pub fn from_arg0(arg0: Option<&OsStr>) -> Self {
31        let stem = arg0
32            .map(Path::new)
33            .and_then(Path::file_stem)
34            .and_then(OsStr::to_str);
35        if stem == Some(LEGACY_BINARY_NAME) {
36            InvokedAs::Legacy
37        } else {
38            InvokedAs::Current
39        }
40    }
41}
42
43/// Name of the host binary.
44pub const BINARY_NAME: &str = "talahon";
45/// Its name up to 2.x, still installed as an alias for one major version.
46pub const LEGACY_BINARY_NAME: &str = "talon";
47
48/// The one-line notice for a start under the old name.
49pub const LEGACY_NAME_NOTICE: &str = "talon: this command is now called `talahon`; the `talon` name is \
50     deprecated and will be removed in the next major version";
51
52/// Tell the operator once that `talon` is now `talahon`.
53///
54/// Called after logging is set up, and only by commands whose output is
55/// read by people or goes to the service log: `version` (and clap's
56/// `--help` / `--version`) print nothing extra, their stdout is what scripts
57/// compare. With `TALAHON_LOG_FORMAT=json` the notice is a regular (JSON)
58/// log record, so a consumer of the log stream never sees a foreign line;
59/// otherwise it is one plain line on stderr.
60fn legacy_name_notice(invoked: InvokedAs, format: LogFormat) {
61    if invoked != InvokedAs::Legacy {
62        return;
63    }
64    match format {
65        LogFormat::Json => tracing::warn!(
66            deprecated = LEGACY_BINARY_NAME,
67            replacement = BINARY_NAME,
68            "{LEGACY_NAME_NOTICE}"
69        ),
70        LogFormat::Pretty => eprintln!("{LEGACY_NAME_NOTICE}"),
71    }
72}
73
74/// Load `.env`, install logging (redacting, format from
75/// `TALAHON_LOG_FORMAT`) and resolve the configuration.
76fn bootstrap(
77    root: &Path,
78    target: LogTarget,
79    default_filter: &str,
80    invoked: InvokedAs,
81) -> (Arc<TalonConfig>, Arc<talon_security::redact::Redactor>) {
82    let env = EnvFile::load(root);
83    let format = LogFormat::parse(env.get("TALAHON_LOG_FORMAT").as_deref());
84    let redactor = crate::secrets::redactor_for(&env);
85    logging::init(format, target, default_filter, redactor.clone());
86    legacy_name_notice(invoked, format);
87    let config = Arc::new(TalonConfig::from_env(root, env));
88    // Old `TALON_*` keys read before the logger existed are reported now;
89    // later ones as they are first read.
90    talon_core::env::flush_legacy_key_warnings();
91    (config, redactor)
92}
93
94async fn dispatch(cli: Cli, root: PathBuf, invoked: InvokedAs) -> i32 {
95    match cli.command() {
96        Command::Version => {
97            println!("{BINARY_NAME} {}", crate::VERSION);
98            0
99        }
100        Command::Run => {
101            let (config, redactor) = bootstrap(&root, LogTarget::Stdout, "info", invoked);
102            match crate::app::run(config, redactor).await {
103                Ok(()) => 0,
104                Err(e) => {
105                    tracing::error!(error = %format!("{e:#}"), "Failed to start Talahon");
106                    1
107                }
108            }
109        }
110        Command::Setup(args) => {
111            let _ = bootstrap(&root, LogTarget::Stderr, "info", invoked);
112            let ctx = talon_setup::SetupContext::detect(&root);
113            match talon_setup::run_step_by_name(&args.step, &args.args, &ctx).await {
114                Ok(outcome) => {
115                    println!("{}", outcome.render());
116                    outcome.exit_code
117                }
118                Err(unknown) => {
119                    eprintln!("{unknown}");
120                    1
121                }
122            }
123        }
124        Command::Doctor => {
125            let (config, _) = bootstrap(&root, LogTarget::Stderr, "warn", invoked);
126            crate::doctor::run(&root, &config).await
127        }
128        Command::Eval { command } => {
129            let (config, _) = bootstrap(&root, LogTarget::Stderr, "warn", invoked);
130            match command {
131                EvalCommand::Score(args) => crate::eval_cmd::score(&root, args),
132                EvalCommand::Run(args) => crate::eval_cmd::run(&root, &config, args).await,
133            }
134        }
135        Command::Audit { command } => {
136            let (config, _) = bootstrap(&root, LogTarget::Stderr, "warn", invoked);
137            let result = match command {
138                AuditCommand::Verify => crate::audit_cmd::verify(&config),
139                AuditCommand::Head => crate::audit_cmd::head(&config),
140                AuditCommand::Tail { lines, kind } => {
141                    crate::audit_cmd::tail(&config, *lines, kind.as_deref())
142                }
143            };
144            result.unwrap_or_else(|e| {
145                eprintln!("{BINARY_NAME} audit: {e:#}");
146                1
147            })
148        }
149    }
150}
151
152/// Entry point of the `talahon` binary and of its deprecated `talon` alias:
153/// parse the command line, set up logging and dispatch. `alias` is what the
154/// calling binary was built as; a link named `talon` counts as the alias too.
155pub fn main(alias: InvokedAs) -> ExitCode {
156    let invoked = match alias {
157        InvokedAs::Legacy => InvokedAs::Legacy,
158        InvokedAs::Current => InvokedAs::from_arg0(std::env::args_os().next().as_deref()),
159    };
160    let cli = Cli::parse_with_legacy_env();
161    let root = match cli.resolve_project_root() {
162        Ok(root) => root,
163        Err(e) => {
164            eprintln!("{BINARY_NAME}: cannot determine the project directory: {e}");
165            return ExitCode::from(1);
166        }
167    };
168    let runtime = match tokio::runtime::Builder::new_multi_thread()
169        .enable_all()
170        .thread_name("talahon")
171        .build()
172    {
173        Ok(rt) => rt,
174        Err(e) => {
175            eprintln!("{BINARY_NAME}: cannot start the async runtime: {e}");
176            return ExitCode::from(1);
177        }
178    };
179    exit_code(runtime.block_on(dispatch(cli, root, invoked)))
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn the_old_binary_name_is_recognised_from_arg0() {
188        for legacy in ["talon", "/usr/local/bin/talon", "./talon", "talon.exe"] {
189            assert_eq!(
190                InvokedAs::from_arg0(Some(OsStr::new(legacy))),
191                InvokedAs::Legacy,
192                "{legacy}"
193            );
194        }
195        for current in [
196            "talahon",
197            "/opt/talahon/bin/talahon",
198            "/opt/talon/bin/talahon",
199            "talon-agent",
200            "",
201        ] {
202            assert_eq!(
203                InvokedAs::from_arg0(Some(OsStr::new(current))),
204                InvokedAs::Current,
205                "{current}"
206            );
207        }
208        assert_eq!(InvokedAs::from_arg0(None), InvokedAs::Current);
209    }
210
211    #[test]
212    fn the_notice_is_one_line_and_names_both_commands() {
213        assert_eq!(LEGACY_NAME_NOTICE.lines().count(), 1);
214        assert!(LEGACY_NAME_NOTICE.contains("`talahon`"));
215        assert!(LEGACY_NAME_NOTICE.contains("`talon`"));
216        assert!(LEGACY_NAME_NOTICE.contains("next major version"));
217    }
218}