From e21f795e93ca1d7b213948de32cf8d5fa3a9cc24 Mon Sep 17 00:00:00 2001 From: Scott Lamb Date: Sat, 11 Feb 2023 11:38:12 -0800 Subject: [PATCH] switch from ancient clap/structopt release to bpaf Improves #70: this reduces binary size from 12.3 MiB to 11.9 MiB (3%) on macOS/arm64. The user experience is almost the same. (The help output's `Usage:` lines lack the e.g. `moonfire-nvr run` prefix of argv[0] and subcommand, which isn't ideal, but I guess it's pretty minor in the grand scheme of things.) --- server/Cargo.lock | 139 +++++++++++---------------------- server/Cargo.toml | 5 +- server/src/cmds/check.rs | 34 ++++---- server/src/cmds/config/mod.rs | 13 ++- server/src/cmds/init.rs | 13 ++- server/src/cmds/login.rs | 53 +++++++------ server/src/cmds/run/config.rs | 3 + server/src/cmds/run/mod.rs | 14 ++-- server/src/cmds/sql.rs | 51 +++++++++--- server/src/cmds/ts.rs | 8 +- server/src/cmds/upgrade/mod.rs | 41 +++++----- server/src/main.rs | 72 +++++++++++++---- 12 files changed, 229 insertions(+), 217 deletions(-) diff --git a/server/Cargo.lock b/server/Cargo.lock index dc3fc84..15fd33b 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -47,15 +47,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" version = "1.0.68" @@ -153,6 +144,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bpaf" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863c0b21775e45ebf9bbb3a6d0cd9b3421c88a036e825359e3d4015561f3e23c" +dependencies = [ + "bpaf_derive", + "owo-colors", +] + +[[package]] +name = "bpaf_derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ed63113bfb3a9bb25dd131acdf0044c7404000ea57dd9eec1f221e3547b4748" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bstr" version = "1.1.0" @@ -220,20 +232,6 @@ dependencies = [ "inout", ] -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "term_size", - "textwrap", - "unicode-width", -] - [[package]] name = "codespan-reporting" version = "0.11.1" @@ -756,15 +754,6 @@ dependencies = [ "hashbrown 0.13.1", ] -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "hermit-abi" version = "0.1.19" @@ -974,6 +963,12 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e" +[[package]] +name = "is_ci" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" + [[package]] name = "itertools" version = "0.10.5" @@ -1163,9 +1158,9 @@ version = "0.7.5" dependencies = [ "base64", "blake3", + "bpaf", "byteorder", "bytes", - "clap", "cursive", "failure", "fnv", @@ -1196,7 +1191,6 @@ dependencies = [ "serde", "serde_json", "smallvec", - "structopt", "sync_wrapper", "tempfile", "time 0.1.45", @@ -1412,6 +1406,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +dependencies = [ + "supports-color", +] + [[package]] name = "password-hash" version = "0.4.2" @@ -1488,30 +1491,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" version = "1.0.49" @@ -1967,36 +1946,22 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "structopt" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" -dependencies = [ - "clap", - "lazy_static", - "structopt-derive", -] - -[[package]] -name = "structopt-derive" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "subtle" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +[[package]] +name = "supports-color" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba6faf2ca7ee42fdd458f4347ae0a9bd6bcc445ad7cb57ad82b383f18870d6f" +dependencies = [ + "atty", + "is_ci", +] + [[package]] name = "syn" version = "1.0.107" @@ -2059,16 +2024,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "term_size", - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.38" diff --git a/server/Cargo.toml b/server/Cargo.toml index a6250ed..7a7a5fc 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -2,7 +2,7 @@ name = "moonfire-nvr" version = "0.7.5" authors = ["Scott Lamb "] -edition = "2018" +edition = "2021" resolver = "2" license-file = "../LICENSE.txt" rust-version = "1.64" @@ -24,9 +24,9 @@ members = ["base", "db"] base = { package = "moonfire-base", path = "base" } base64 = "0.13.0" blake3 = "1.0.0" +bpaf = { version = "0.7.8", features = ["autocomplete", "bright-color", "derive"] } bytes = "1" byteorder = "1.0" -clap = { version = "2.33.3", default-features = false, features = ["color", "wrap_help"] } cursive = "0.20.0" db = { package = "moonfire-db", path = "db" } failure = "0.1.1" @@ -53,7 +53,6 @@ rusqlite = "0.28.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" smallvec = { version = "1.7", features = ["union"] } -structopt = { version = "0.3.13", default-features = false } sync_wrapper = "0.1.0" time = "0.1" tokio = { version = "1.24", features = ["macros", "rt-multi-thread", "signal", "sync", "time"] } diff --git a/server/src/cmds/check.rs b/server/src/cmds/check.rs index ebf9498..a5e98c9 100644 --- a/server/src/cmds/check.rs +++ b/server/src/cmds/check.rs @@ -4,45 +4,39 @@ //! Subcommand to check the database and sample file dir for errors. +use bpaf::Bpaf; use db::check; use failure::Error; use std::path::PathBuf; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Bpaf, Debug)] pub struct Args { /// Directory holding the SQLite3 index database. - #[structopt( - long, - default_value = "/var/lib/moonfire-nvr/db", - value_name = "path", - parse(from_os_str) - )] + /// + /// default: `/var/lib/moonfire-nvr/db`. + #[bpaf(argument("PATH"), fallback_with(crate::default_db_dir))] db_dir: PathBuf, - /// Compare sample file lengths on disk to the database. - #[structopt(long)] + /// Compares sample file lengths on disk to the database. compare_lens: bool, - /// Trash sample files without matching recording rows in the database. - /// This addresses "Missing ... row" errors. + /// Trashes sample files without matching recording rows in the database. + /// This addresses `Missing ... row` errors. /// - /// The ids are added to the "garbage" table to indicate the files need to + /// The ids are added to the `garbage` table to indicate the files need to /// be deleted. Garbage is collected on normal startup. - #[structopt(long)] trash_orphan_sample_files: bool, - /// Delete recording rows in the database without matching sample files. - /// This addresses "Recording ... missing file" errors. - #[structopt(long)] + /// Deletes recording rows in the database without matching sample files. + /// + /// This addresses `Recording ... missing file` errors. delete_orphan_rows: bool, - /// Trash recordings when their database rows appear corrupt. + /// Trashes recordings when their database rows appear corrupt. /// This addresses "bad video_index" errors. /// - /// The ids are added to the "garbage" table to indicate their files need to + /// The ids are added to the `garbage` table to indicate their files need to /// be deleted. Garbage is collected on normal startup. - #[structopt(long)] trash_corrupt_rows: bool, } diff --git a/server/src/cmds/config/mod.rs b/server/src/cmds/config/mod.rs index d68892d..f078f6d 100644 --- a/server/src/cmds/config/mod.rs +++ b/server/src/cmds/config/mod.rs @@ -8,26 +8,23 @@ //! configuration will likely be almost entirely done through a web-based UI. use base::clock; +use bpaf::Bpaf; use cursive::views; use cursive::Cursive; use failure::Error; use std::path::PathBuf; use std::sync::Arc; -use structopt::StructOpt; mod cameras; mod dirs; mod users; -#[derive(StructOpt)] +#[derive(Bpaf, Debug)] pub struct Args { /// Directory holding the SQLite3 index database. - #[structopt( - long, - default_value = "/var/lib/moonfire-nvr/db", - value_name = "path", - parse(from_os_str) - )] + /// + /// default: `/var/lib/moonfire-nvr/db`. + #[bpaf(argument("PATH"), fallback_with(crate::default_db_dir))] db_dir: PathBuf, } diff --git a/server/src/cmds/init.rs b/server/src/cmds/init.rs index 6e97f7d..adc766e 100644 --- a/server/src/cmds/init.rs +++ b/server/src/cmds/init.rs @@ -2,20 +2,17 @@ // Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt. // SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception. +use bpaf::Bpaf; use failure::Error; use log::info; use std::path::PathBuf; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Bpaf, Debug)] pub struct Args { /// Directory holding the SQLite3 index database. - #[structopt( - long, - default_value = "/var/lib/moonfire-nvr/db", - value_name = "path", - parse(from_os_str) - )] + /// + /// default: `/var/lib/moonfire-nvr/db`. + #[bpaf(argument("PATH"), fallback_with(crate::default_db_dir))] db_dir: PathBuf, } diff --git a/server/src/cmds/login.rs b/server/src/cmds/login.rs index 06a05d3..fa1dc00 100644 --- a/server/src/cmds/login.rs +++ b/server/src/cmds/login.rs @@ -5,51 +5,58 @@ //! Subcommand to login a user (without requiring a password). use base::clock::{self, Clocks}; +use bpaf::Bpaf; use db::auth::SessionFlag; use failure::{format_err, Error}; use std::io::Write as _; use std::os::unix::fs::OpenOptionsExt as _; use std::path::PathBuf; -use structopt::StructOpt; +use std::str::FromStr; -#[derive(Debug, Default, StructOpt)] +fn parse_perms(perms: String) -> Result { + protobuf::text_format::parse_from_str(&perms) +} + +fn parse_flags(flags: String) -> Result, Error> { + flags.split(',').map(SessionFlag::from_str).collect() +} + +#[derive(Bpaf, Debug)] pub struct Args { /// Directory holding the SQLite3 index database. - #[structopt( - long, - default_value = "/var/lib/moonfire-nvr/db", - value_name = "path", - parse(from_os_str) - )] + /// + /// default: `/var/lib/moonfire-nvr/db`. + #[bpaf(argument("PATH"), fallback_with(crate::default_db_dir))] db_dir: PathBuf, - /// Create a session with the given permissions. + /// Creates a session with the given permissions. /// /// If unspecified, uses user's default permissions. - #[structopt(long, value_name="perms", - parse(try_from_str = protobuf::text_format::parse_from_str))] + #[bpaf(argument::("PERMS"), parse(parse_perms), optional)] permissions: Option, - /// Restrict this cookie to the given domain. - #[structopt(long)] + /// Restricts this cookie to the given domain. + #[bpaf(argument("DOMAIN"))] domain: Option, - /// Write the cookie to a new curl-compatible cookie-jar file. + /// Writes the cookie to a new curl-compatible cookie-jar file. /// - /// ---domain must be specified. This file can be used later with curl's --cookie flag. - #[structopt(long, requires("domain"), value_name = "path")] + /// `--domain` must be specified. This file can be used later with curl's `--cookie` flag. + #[bpaf(argument("PATH"))] curl_cookie_jar: Option, - /// Set the given db::auth::SessionFlags. - #[structopt( - long, - default_value = "http-only,secure,same-site,same-site-strict", - value_name = "flags", - use_delimiter = true + /// Sets the given db::auth::SessionFlags. + /// + /// default: `http-only,secure,same-site,same-site-strict`. + #[bpaf( + argument::("FLAGS"), + fallback_with(|| Ok::<_, std::convert::Infallible>("http-only,secure,same-site,same-site-strict".to_owned())), + parse(parse_flags), )] session_flags: Vec, /// Create the session for this username. + #[bpaf(argument("USERNAME"))] username: String, } @@ -87,7 +94,7 @@ pub fn run(args: Args) -> Result { let d = args .domain .as_ref() - .ok_or_else(|| format_err!("--cookiejar requires --domain"))?; + .ok_or_else(|| format_err!("--curl-cookie-jar requires --domain"))?; let mut f = std::fs::OpenOptions::new() .write(true) .create_new(true) diff --git a/server/src/cmds/run/config.rs b/server/src/cmds/run/config.rs index 0a45e36..cd3bc07 100644 --- a/server/src/cmds/run/config.rs +++ b/server/src/cmds/run/config.rs @@ -27,6 +27,9 @@ pub struct ConfigFile { pub binds: Vec, /// Directory holding the SQLite3 index database. + /// + /// + /// default: `/var/lib/moonfire-nvr/db`. #[serde(default = "default_db_dir")] pub db_dir: PathBuf, diff --git a/server/src/cmds/run/mod.rs b/server/src/cmds/run/mod.rs index 4becfb9..c8ef04f 100644 --- a/server/src/cmds/run/mod.rs +++ b/server/src/cmds/run/mod.rs @@ -6,6 +6,7 @@ use crate::streamer; use crate::web; use crate::web::accept::Listener; use base::clock; +use bpaf::Bpaf; use db::{dir, writer}; use failure::{bail, Error, ResultExt}; use fnv::FnvHashMap; @@ -18,23 +19,24 @@ use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use std::thread; -use structopt::StructOpt; use tokio::signal::unix::{signal, SignalKind}; use self::config::ConfigFile; mod config; -#[derive(StructOpt)] +#[derive(Bpaf, Debug)] pub struct Args { - #[structopt(short, long, default_value = "/etc/moonfire-nvr.toml")] + /// Path to configuration file. + /// + /// default: `/etc/moonfire-nvr.toml`. See `ref/config.md` for config file documentation. + #[bpaf(short, long, argument("PATH"), fallback_with(|| Ok::<_, Error>("/etc/moonfire-nvr.toml".into())))] config: PathBuf, - /// Open the database in read-only mode and disables recording. + /// Opens the database in read-only mode and disables recording. /// /// Note this is incompatible with session authentication; consider adding - /// a bind with `allow_unauthenticated_permissions` to your config. - #[structopt(long)] + /// a bind with `allowUnauthenticatedPermissions` to your config. read_only: bool, } diff --git a/server/src/cmds/sql.rs b/server/src/cmds/sql.rs index aa376b6..5ef1ca9 100644 --- a/server/src/cmds/sql.rs +++ b/server/src/cmds/sql.rs @@ -5,35 +5,31 @@ //! Subcommand to run a SQLite shell. use super::OpenMode; +use bpaf::Bpaf; use failure::Error; use std::ffi::OsString; use std::os::unix::process::CommandExt; use std::path::PathBuf; use std::process::Command; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Bpaf, Debug, PartialEq, Eq)] pub struct Args { /// Directory holding the SQLite3 index database. - #[structopt( - long, - default_value = "/var/lib/moonfire-nvr/db", - value_name = "path", - parse(from_os_str) - )] + /// + /// default: `/var/lib/moonfire-nvr/db`. + #[bpaf(fallback_with(crate::default_db_dir))] db_dir: PathBuf, /// Opens the database in read-only mode and locks it only for shared access. /// - /// This can be run simultaneously with "moonfire-nvr run --read-only". - #[structopt(long)] + /// This can be run simultaneously with `moonfire-nvr run --read-only`. read_only: bool, /// Arguments to pass to sqlite3. /// - /// Use the -- separator to pass sqlite3 options, as in - /// "moonfire-nvr sql -- -line 'select username from user'". - #[structopt(parse(from_os_str))] + /// Use the `--` separator to pass sqlite3 options, as in + /// `moonfire-nvr sql -- -line 'select username from user'`. + #[bpaf(positional)] arg: Vec, } @@ -58,3 +54,32 @@ pub fn run(args: Args) -> Result { .exec() .into()) } + +#[cfg(test)] +mod tests { + use super::*; + + use bpaf::Parser; + + #[test] + fn parse_args() { + let args = args() + .to_options() + .run_inner(bpaf::Args::from(&[ + "--db-dir", + "/foo/bar", + "--", + "-line", + "select username from user", + ])) + .unwrap(); + assert_eq!( + args, + Args { + db_dir: "/foo/bar".into(), + read_only: false, // default + arg: vec!["-line".into(), "select username from user".into()], + } + ); + } +} diff --git a/server/src/cmds/ts.rs b/server/src/cmds/ts.rs index 0feecdb..0b4ea6f 100644 --- a/server/src/cmds/ts.rs +++ b/server/src/cmds/ts.rs @@ -2,18 +2,18 @@ // Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt. // SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception. +use bpaf::Bpaf; use failure::Error; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Bpaf, Debug)] pub struct Args { /// Timestamp(s) to translate. /// /// May be either an integer or an RFC-3339-like string: /// `YYYY-mm-dd[THH:MM[:SS[:FFFFF]]][{Z,{+,-,}HH:MM}]`. /// - /// Eg: `142913484000000`, `2020-04-26`, `2020-04-26T12:00:00:00000-07:00`. - #[structopt(required = true)] + /// E.g.: `142913484000000`, `2020-04-26`, `2020-04-26T12:00:00:00000-07:00`. + #[bpaf(positional("TS"), some("must specify at least one timestamp"))] timestamps: Vec, } diff --git a/server/src/cmds/upgrade/mod.rs b/server/src/cmds/upgrade/mod.rs index 7bff52a..6813d97 100644 --- a/server/src/cmds/upgrade/mod.rs +++ b/server/src/cmds/upgrade/mod.rs @@ -2,41 +2,36 @@ // Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt. // SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception. +use bpaf::Bpaf; /// Upgrades the database schema. /// /// See `guide/schema.md` for more information. use failure::Error; -use structopt::StructOpt; -#[derive(StructOpt)] +#[derive(Bpaf, Debug)] pub struct Args { - #[structopt( - long, - help = "Directory holding the SQLite3 index database.", - default_value = "/var/lib/moonfire-nvr/db", - parse(from_os_str) - )] + /// Directory holding the SQLite3 index database. + /// + /// + /// default: `/var/lib/moonfire-nvr/db`. + #[bpaf(argument("PATH"), fallback_with(crate::default_db_dir))] db_dir: std::path::PathBuf, - #[structopt( - help = "When upgrading from schema version 1 to 2, the sample file directory.", - long, - parse(from_os_str) - )] + /// When upgrading from schema version 1 to 2, the sample file directory. + #[bpaf(argument("PATH"))] sample_file_dir: Option, - #[structopt( - help = "Resets the SQLite journal_mode to the specified mode prior to \ - the upgrade. The default, delete, is recommended. off is very \ - dangerous but may be desirable in some circumstances. See \ - guide/schema.md for more information. The journal mode will be \ - reset to wal after the upgrade.", - long, - default_value = "delete" - )] + /// Resets the SQLite journal_mode to the specified mode prior to + /// the upgrade. + /// + /// + /// default: `delete` (recommended). `off` is very dangerous but may be + /// desirable in some circumstances. See `guide/schema.md` for more + /// information. The journal mode will be reset to `wal` after the upgrade. + #[bpaf(argument("MODE"), fallback_with(|| Ok::<_, std::convert::Infallible>("delete".into())))] preset_journal: String, - #[structopt(help = "Skips the normal post-upgrade vacuum operation.", long)] + /// Skips the normal post-upgrade vacuum operation. no_vacuum: bool, } diff --git a/server/src/main.rs b/server/src/main.rs index 3dd6863..0c4bea5 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -4,10 +4,10 @@ #![cfg_attr(all(feature = "nightly", test), feature(test))] +use bpaf::Bpaf; use log::{debug, error}; use std::fmt::Write; use std::str::FromStr; -use structopt::StructOpt; mod body; mod cmds; @@ -19,43 +19,50 @@ mod stream; mod streamer; mod web; -#[derive(StructOpt)] -#[structopt( - name = "moonfire-nvr", - about = "security camera network video recorder", - global_settings(&[clap::AppSettings::ColoredHelp]) -)] +/// Moonfire NVR: security camera network video recorder. +#[derive(Bpaf, Debug)] +#[bpaf(options, version)] enum Args { /// Checks database integrity (like fsck). - Check(cmds::check::Args), + #[bpaf(command)] + Check(#[bpaf(external(cmds::check::args))] cmds::check::Args), /// Interactively edits configuration. - Config(cmds::config::Args), + #[bpaf(command)] + Config(#[bpaf(external(cmds::config::args))] cmds::config::Args), /// Initializes a database. - Init(cmds::init::Args), + #[bpaf(command)] + Init(#[bpaf(external(cmds::init::args))] cmds::init::Args), /// Logs in a user, returning the session cookie. /// + /// /// This is a privileged command that directly accesses the database. It doesn't check the /// user's password and even can be used to create sessions with permissions the user doesn't /// have. - Login(cmds::login::Args), + #[bpaf(command)] + Login(#[bpaf(external(cmds::login::args))] cmds::login::Args), /// Runs the server, saving recordings and allowing web access. - Run(cmds::run::Args), + #[bpaf(command)] + Run(#[bpaf(external(cmds::run::args))] cmds::run::Args), /// Runs a SQLite3 shell on Moonfire NVR's index database. /// + /// /// Note this locks the database to prevent simultaneous access with a running server. The /// server maintains cached state which could be invalidated otherwise. - Sql(cmds::sql::Args), + #[bpaf(command)] + Sql(#[bpaf(external(cmds::sql::args))] cmds::sql::Args), /// Translates between integer and human-readable timestamps. - Ts(cmds::ts::Args), + #[bpaf(command)] + Ts(#[bpaf(external(cmds::ts::args))] cmds::ts::Args), /// Upgrades to the latest database schema. - Upgrade(cmds::upgrade::Args), + #[bpaf(command)] + Upgrade(#[bpaf(external(cmds::upgrade::args))] cmds::upgrade::Args), } impl Args { @@ -73,6 +80,11 @@ impl Args { } } +/// Returns the default database dir, for use in argument parsing with `bpaf(fallback_with(...))`. +fn default_db_dir() -> Result { + Ok("/var/lib/moonfire-nvr/db".into()) +} + /// Custom panic hook that logs instead of directly writing to stderr. /// /// This means it includes a timestamp and is more recognizable as a serious @@ -107,12 +119,11 @@ fn main() { if let Err(e) = nix::time::clock_gettime(nix::time::ClockId::CLOCK_MONOTONIC) { eprintln!( "clock_gettime failed: {e}\n\n\ - This indicates a broken environment. See the troubleshooting guide." + This indicates a broken environment. See the troubleshooting guide." ); std::process::exit(1); } - let args = Args::from_args(); let mut h = mylog::Builder::new() .set_format( ::std::env::var("MOONFIRE_FORMAT") @@ -130,6 +141,22 @@ fn main() { .build(); h.clone().install().unwrap(); + // TODO: remove this when bpaf adds more direct support for defaulting to `--help`. + // See discussion: . + if std::env::args_os().len() < 2 { + match args().run_inner(bpaf::Args::from(&["--help"])) { + Ok(a) => panic!("bpaf --help should not return Ok: {a:#?}"), + Err(bpaf::ParseFailure::Stdout(msg)) => { + print!("{msg}"); + std::process::exit(0); + } + Err(bpaf::ParseFailure::Stderr(msg)) => { + eprint!("{msg}"); + std::process::exit(1); + } + } + } + let use_panic_hook = ::std::env::var("MOONFIRE_PANIC_HOOK") .map(|s| s != "false" && s != "0") .unwrap_or(true); @@ -137,6 +164,9 @@ fn main() { std::panic::set_hook(Box::new(&panic_hook)); } + let args = args().run(); + log::trace!("Parsed command-line arguments: {args:#?}"); + let r = { let _a = h.async_scope(); args.run() @@ -152,3 +182,11 @@ fn main() { } } } + +#[cfg(test)] +mod tests { + #[test] + fn bpaf_invariants() { + super::args().check_invariants(false); + } +}