Skip to main content

blockworx/
cli.rs

1//! The command line: the three container subcommands, their reporting, and
2//! the tracing the whole binary logs through. Nothing here names a toolkit.
3
4use clap::Parser;
5use std::path::{Path, PathBuf};
6
7#[derive(Parser)]
8#[command(name = "blockworx")]
9pub(crate) struct Cli {
10    #[command(subcommand)]
11    pub(crate) command: Command,
12}
13
14#[derive(clap::Subcommand)]
15pub(crate) enum Command {
16    /// Print a container's commit log — rev, time, author, kind, op count,
17    /// label — oldest first, the order the file holds. Read-only: it takes
18    /// no lock and repairs nothing, so it is safe to run against a
19    /// container the editor has open. A log that stops verifying prints
20    /// the trail up to the break, then the break.
21    Log {
22        /// The `.bwx` container to read.
23        container: PathBuf,
24    },
25    /// Verify a container's log end to end: every chain link and every
26    /// folded-state stamp. Opening the editor samples the stamps —
27    /// recomputing one costs a pass over the whole document — so this is
28    /// the fsck that proves a log whole, and the only check that names a
29    /// drifted record exactly. Read-only, and exits non-zero on any fault.
30    Verify {
31        /// The `.bwx` container to check.
32        container: PathBuf,
33    },
34    /// Carry a container written when revs were zstd over to gzip, which
35    /// is what every build since writes and what a browser can read. Each
36    /// rev lands under its new name before the old one goes, the rows are
37    /// re-stamped against the bytes that are now there, and the
38    /// projection is refreshed; running it twice is running it once.
39    /// Takes the container's lock, so close the editor on it first.
40    Migrate {
41        /// The `.bwx` container to carry over.
42        container: PathBuf,
43    },
44}
45
46impl Command {
47    pub(crate) fn run(&self) -> ! {
48        match self {
49            Command::Log { container } => print_log(container),
50            Command::Verify { container } => print_verification(container),
51            Command::Migrate { container } => migrate_container(container),
52        }
53    }
54}
55
56/// Carry a container over to the rev encoding this build writes. Exits
57/// non-zero when anything in it did not come over, leaving the container
58/// as it was found or part-way, which the next run finishes.
59fn migrate_container(container: &Path) -> ! {
60    let started = std::time::Instant::now();
61    match blockworx_store::migrate::to_gzip(&blockworx_store::storage::Native::at(container)) {
62        Ok(migrated) => {
63            println!(
64                "{}: {migrated}, in {:.1?}",
65                container.display(),
66                started.elapsed(),
67            );
68            std::process::exit(0);
69        }
70        Err(e) => {
71            eprintln!("{}: {e}", container.display());
72            std::process::exit(1);
73        }
74    }
75}
76
77/// Print a container's trail. Exits non-zero when the log does not verify
78/// end to end, so a script can tell an intact trail from a damaged one.
79fn print_log(container: &Path) -> ! {
80    let dumped = match blockworx_store::dump::dump(&blockworx_store::storage::Native::at(container))
81    {
82        Ok(dumped) => dumped,
83        Err(e) => {
84            eprintln!("{}: {e}", container.display());
85            std::process::exit(1);
86        }
87    };
88    for line in &dumped.lines {
89        println!("{line}");
90    }
91    match dumped.broken {
92        None => std::process::exit(0),
93        Some(report) => {
94            eprintln!("{report:?}");
95            std::process::exit(1);
96        }
97    }
98}
99
100/// Check a container's whole log. Exits non-zero on any fault, so a
101/// script — or a bisect over app versions hunting fold drift — can use it
102/// as a gate.
103fn print_verification(container: &Path) -> ! {
104    let started = std::time::Instant::now();
105    let checked =
106        match blockworx_store::dump::verify(blockworx_store::storage::Native::at(container)) {
107            Ok(checked) => checked,
108            Err(e) => {
109                eprintln!("{}: {e}", container.display());
110                std::process::exit(1);
111            }
112        };
113    for line in &checked.lines {
114        println!("{line}");
115    }
116    println!("in {:.1?}", started.elapsed());
117    match checked.broken {
118        None => std::process::exit(0),
119        Some(report) => {
120            eprintln!("{report:?}");
121            std::process::exit(1);
122        }
123    }
124}
125
126/// Install the console subscriber the whole app logs through, so a failure
127/// reported with `tracing::error!`/`warn!` is not swallowed. `RUST_LOG`
128/// raises it: `RUST_LOG=blockworx=info` for what the store is doing,
129/// `RUST_LOG=edit=debug` to narrate every document mutation (the named
130/// `Drawing` setters), `edit=trace` for the per-frame drag writes.
131///
132/// There is no span-timing switch. `--trace` was one, and every span it
133/// could close was on the frame path, which nothing here runs.
134pub(crate) fn init_tracing() {
135    use tracing_subscriber::EnvFilter;
136    let filter =
137        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("blockworx=warn"));
138    tracing_subscriber::fmt()
139        .with_env_filter(filter)
140        .with_target(false)
141        .with_writer(std::io::stderr)
142        .with_ansi(std::io::IsTerminal::is_terminal(&std::io::stderr()))
143        .init();
144}
145
146#[cfg(test)]
147mod tests {
148    use super::{Cli, Command};
149    use clap::Parser as _;
150
151    /// Each subcommand takes the container as its one argument. The binary
152    /// opens nothing — there is no window to open it in — so a bare path is
153    /// not a command and must be refused rather than read as one.
154    #[test]
155    fn every_subcommand_names_a_container() {
156        let dumped = Cli::try_parse_from(["blockworx", "log", "doc.bwx"]).expect("the dump parses");
157        let Command::Log { container } = dumped.command else {
158            panic!("`log` was not read as the subcommand");
159        };
160        assert_eq!(container, std::path::Path::new("doc.bwx"));
161
162        let checked =
163            Cli::try_parse_from(["blockworx", "verify", "doc.bwx"]).expect("so does the fsck");
164        let Command::Verify { container } = checked.command else {
165            panic!("`verify` was not read as the subcommand");
166        };
167        assert_eq!(container, std::path::Path::new("doc.bwx"));
168
169        let carried = Cli::try_parse_from(["blockworx", "migrate", "doc.bwx"])
170            .expect("and so does the carry");
171        let Command::Migrate { container } = carried.command else {
172            panic!("`migrate` was not read as the subcommand");
173        };
174        assert_eq!(container, std::path::Path::new("doc.bwx"));
175
176        assert!(Cli::try_parse_from(["blockworx", "doc.bwx"]).is_err());
177        assert!(Cli::try_parse_from(["blockworx"]).is_err());
178    }
179}