Skip to main content

common/
lib.rs

1//! Common utilities and types for RCP file operation tools
2//!
3//! This crate provides shared functionality used across all RCP tools (`rcp`, `rrm`, `rlink`, `rcmp`).
4//! It includes core operations (copy, remove, link, compare), progress reporting, metadata preservation, and runtime configuration.
5//!
6//! # Core Modules
7//!
8//! - [`mod@copy`] - File copying operations with metadata preservation and error handling
9//! - [`mod@rm`] - File removal operations
10//! - [`mod@link`] - Hard-linking operations
11//! - [`mod@cmp`] - File comparison operations (metadata-based)
12//! - [`mod@preserve`] - Metadata preservation settings and operations
13//! - [`mod@progress`] - Progress tracking and reporting
14//! - [`mod@filecmp`] - File metadata comparison utilities
15//! - [`mod@remote_tracing`] - Remote tracing support for distributed operations
16//!
17//! # Key Types
18//!
19//! ## `RcpdType`
20//!
21//! Identifies the role of a remote copy daemon:
22//! - `Source` - reads files from source host
23//! - `Destination` - writes files to destination host
24//!
25//! ## `ProgressType`
26//!
27//! Controls progress reporting display:
28//! - `Auto` - automatically choose based on terminal type
29//! - `ProgressBar` - animated progress bar (for interactive terminals)
30//! - `TextUpdates` - periodic text updates (for logging/non-interactive)
31//!
32//! # Progress Reporting
33//!
34//! The crate provides a global progress tracking system accessible via [`get_progress()`].
35//! Progress can be displayed in different formats depending on the execution context.
36//!
37//! Progress output goes to stderr, while logs go to stdout, allowing users to redirect logs to a file while still viewing interactive progress.
38//!
39//! # Runtime Configuration
40//!
41//! The [`run`] function provides a unified entry point for all RCP tools with support for:
42//! - Progress tracking and reporting
43//! - Logging configuration (quiet/verbose modes)
44//! - Resource limits (max workers, leaf-operation descriptor admission, throttling)
45//! - Tokio runtime setup
46//! - Remote tracing integration
47//!
48//! # Examples
49//!
50//! ## Basic Copy Operation
51//!
52//! ```rust,no_run
53//! use std::path::Path;
54//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
55//! let src = Path::new("/source");
56//! let dst = Path::new("/destination");
57//!
58//! let settings = common::copy::Settings {
59//!     dereference: false,
60//!     fail_early: false,
61//!     overwrite: false,
62//!     overwrite_compare: Default::default(),
63//!     overwrite_filter: None,
64//!     ignore_existing: false,
65//!     chunk_size: 0,
66//!     skip_specials: false,
67//!     remote_copy_buffer_size: 0,
68//!     filter: None,
69//!     dry_run: None,
70//!     delete: None,
71//! };
72//! let preserve = common::preserve::preserve_none();
73//!
74//! let summary = common::copy(src, dst, &settings, &preserve).await?;
75//! println!("Copied {} files", summary.files_copied);
76//! # Ok(())
77//! # }
78//! ```
79//!
80//! ## Metadata Comparison
81//!
82//! ```rust,no_run
83//! use std::path::Path;
84//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
85//! let src = Path::new("/path1");
86//! let dst = Path::new("/path2");
87//!
88//! // output differences to stdout (use false for quiet mode)
89//! let log = common::cmp::LogWriter::new(None, true, common::cmp::OutputFormat::default()).await?;
90//! let settings = common::cmp::Settings {
91//!     fail_early: false,
92//!     exit_early: false,
93//!     expand_missing: false,
94//!     compare: Default::default(),
95//!     filter: None,
96//! };
97//!
98//! let summary = common::cmp(src, dst, &log, &settings).await?;
99//! println!("Comparison complete: {}", summary);
100//! # Ok(())
101//! # }
102//! ```
103
104use anyhow::Context;
105use std::io::IsTerminal;
106use tracing::instrument;
107
108mod auto_meta;
109pub mod chmod;
110pub mod cli;
111pub mod cmp;
112pub mod config;
113pub mod copy;
114pub mod copy_data;
115pub mod delete;
116pub mod dry_run;
117pub mod error;
118pub mod error_collector;
119pub mod filegen;
120pub mod filter;
121pub mod histogram_logger;
122pub mod histogram_panel;
123pub mod link;
124pub mod observability;
125pub mod preserve;
126pub mod remote_tracing;
127pub mod rm;
128mod runtime_setup;
129pub mod safedir;
130mod settings_parse;
131pub mod version;
132
133pub mod filecmp;
134pub mod progress;
135mod testutils;
136pub mod toctou_check;
137pub mod walk;
138pub mod walk_driver;
139
140pub use config::{
141    AutoMetaThrottleConfig, ConcurrencyLimit, DryRunMode, DryRunWarnings, FilesInFlightSource,
142    MIN_DEFAULT_FILES_IN_FLIGHT, OutputConfig, ResolvedFilesInFlight, RuntimeConfig,
143    ThrottleConfig, TracingConfig,
144};
145// Re-export `Side` from the congestion crate so downstream binaries
146// (rcp, rrm, …) and integration tests can pass `common::Side::Source` /
147// `common::Side::Destination` to `walk::next_entry_probed` and friends
148// without taking a direct dependency on `congestion`.
149pub use congestion::{MetadataOp, Side};
150pub use progress::{RcpdProgressPrinter, SerializableProgress};
151// Re-export the runtime-stat / trace-filename helpers that moved into
152// `runtime_setup` so downstream binaries keep reaching them as
153// `common::collect_runtime_stats`, etc.
154pub use runtime_setup::{
155    NOTICE_TARGET, collect_runtime_stats, generate_debug_log_filename, generate_trace_filename,
156};
157pub use settings_parse::{
158    parse_compare_settings, parse_metadata_cmp_settings, parse_preserve_settings,
159    validate_update_compare_vs_preserve,
160};
161
162// Define RcpdType in common since remote depends on common
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, enum_map::Enum)]
164pub enum RcpdType {
165    Source,
166    Destination,
167}
168
169impl std::fmt::Display for RcpdType {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        match self {
172            RcpdType::Source => write!(f, "source"),
173            RcpdType::Destination => write!(f, "destination"),
174        }
175    }
176}
177
178// Type alias for progress snapshots
179pub type ProgressSnapshot<T> = enum_map::EnumMap<RcpdType, T>;
180
181/// runtime statistics collected from a process (CPU time, memory usage)
182#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
183pub struct RuntimeStats {
184    /// user-mode CPU time in milliseconds
185    pub cpu_time_user_ms: u64,
186    /// kernel-mode CPU time in milliseconds
187    pub cpu_time_kernel_ms: u64,
188    /// peak resident set size in bytes
189    pub peak_rss_bytes: u64,
190}
191
192/// runtime stats collected from remote rcpd processes for display at the end of a remote copy
193#[derive(Debug, Default)]
194pub struct RemoteRuntimeStats {
195    pub source_host: String,
196    pub source_stats: RuntimeStats,
197    pub dest_host: String,
198    pub dest_stats: RuntimeStats,
199}
200
201/// checks if a host string refers to the local machine.
202/// returns true for `localhost`, `127.0.0.1`, `::1`, `[::1]`, or the actual hostname
203#[must_use]
204pub fn is_localhost(host: &str) -> bool {
205    if host == "localhost" || host == "127.0.0.1" || host == "::1" || host == "[::1]" {
206        return true;
207    }
208    // check against actual hostname using gethostname
209    let mut buf = [0u8; 256];
210    // Safety: gethostname writes to buf and returns 0 on success
211    let result = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) };
212    if result == 0
213        && let Ok(hostname_cstr) = std::ffi::CStr::from_bytes_until_nul(&buf)
214        && let Ok(hostname) = hostname_cstr.to_str()
215        && host == hostname
216    {
217        return true;
218    }
219    false
220}
221
222pub(crate) static PROGRESS: std::sync::LazyLock<progress::Progress> =
223    std::sync::LazyLock::new(progress::Progress::new);
224pub(crate) static PBAR: std::sync::LazyLock<indicatif::ProgressBar> =
225    std::sync::LazyLock::new(indicatif::ProgressBar::new_spinner);
226pub(crate) static REMOTE_RUNTIME_STATS: std::sync::LazyLock<
227    std::sync::Mutex<Option<RemoteRuntimeStats>>,
228> = std::sync::LazyLock::new(|| std::sync::Mutex::new(None));
229static HISTOGRAM_LOGGER_CANCEL: std::sync::Mutex<Option<tokio::sync::watch::Sender<bool>>> =
230    std::sync::Mutex::new(None);
231static HISTOGRAM_LOGGER_HANDLE: std::sync::Mutex<Option<tokio::task::JoinHandle<()>>> =
232    std::sync::Mutex::new(None);
233
234pub(crate) fn store_logger_cancel(tx: tokio::sync::watch::Sender<bool>) {
235    *HISTOGRAM_LOGGER_CANCEL
236        .lock()
237        .expect("histogram logger cancel mutex poisoned") = Some(tx);
238}
239
240pub(crate) fn store_logger_handle(handle: tokio::task::JoinHandle<()>) {
241    *HISTOGRAM_LOGGER_HANDLE
242        .lock()
243        .expect("histogram logger handle mutex poisoned") = Some(handle);
244}
245
246fn take_logger_handle() -> Option<tokio::task::JoinHandle<()>> {
247    HISTOGRAM_LOGGER_HANDLE
248        .lock()
249        .expect("histogram logger handle mutex poisoned")
250        .take()
251}
252
253fn signal_logger_cancel() {
254    if let Some(tx) = HISTOGRAM_LOGGER_CANCEL
255        .lock()
256        .expect("histogram logger cancel mutex poisoned")
257        .take()
258        && let Err(err) = tx.send(true)
259    {
260        tracing::debug!("histogram-logger cancel send failed (already gone): {err:#}");
261    }
262}
263
264#[must_use]
265pub fn get_progress() -> &'static progress::Progress {
266    &PROGRESS
267}
268
269/// stores remote runtime stats for display at the end of a remote copy operation
270pub fn set_remote_runtime_stats(stats: RemoteRuntimeStats) {
271    *REMOTE_RUNTIME_STATS.lock().unwrap() = Some(stats);
272}
273
274struct ProgressTracker {
275    lock_cvar: std::sync::Arc<(std::sync::Mutex<bool>, std::sync::Condvar)>,
276    pbar_thread: Option<std::thread::JoinHandle<()>>,
277}
278
279#[derive(Copy, Clone, Debug, Default, clap::ValueEnum)]
280pub enum ProgressType {
281    #[default]
282    #[value(name = "auto", alias = "Auto")]
283    Auto,
284    #[value(name = "ProgressBar", alias = "progress-bar")]
285    ProgressBar,
286    #[value(name = "TextUpdates", alias = "text-updates")]
287    TextUpdates,
288}
289
290pub enum GeneralProgressType {
291    User {
292        progress_type: ProgressType,
293        kind: progress::LocalProgressKind,
294    },
295    Remote(tokio::sync::mpsc::UnboundedSender<remote_tracing::TracingMessage>),
296    RemoteMaster {
297        progress_type: ProgressType,
298        get_progress_snapshot:
299            Box<dyn Fn() -> ProgressSnapshot<SerializableProgress> + Send + 'static>,
300    },
301}
302
303impl std::fmt::Debug for GeneralProgressType {
304    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305        match self {
306            GeneralProgressType::User {
307                progress_type,
308                kind,
309            } => write!(f, "User(progress_type: {progress_type:?}, kind: {kind:?})"),
310            GeneralProgressType::Remote(_) => write!(f, "Remote(<sender>)"),
311            GeneralProgressType::RemoteMaster { progress_type, .. } => {
312                write!(
313                    f,
314                    "RemoteMaster(progress_type: {progress_type:?}, <function>)"
315                )
316            }
317        }
318    }
319}
320
321#[derive(Debug)]
322pub struct ProgressSettings {
323    pub progress_type: GeneralProgressType,
324    pub progress_delay: Option<String>,
325}
326
327fn progress_bar(
328    lock: &std::sync::Mutex<bool>,
329    cvar: &std::sync::Condvar,
330    delay_opt: &Option<std::time::Duration>,
331    kind: progress::LocalProgressKind,
332) {
333    let delay = delay_opt.unwrap_or(std::time::Duration::from_millis(200));
334    PBAR.set_style(
335        indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg}")
336            .unwrap()
337            .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
338    );
339    let mut prog_printer = progress::make_local_printer(kind, &PROGRESS);
340    let mut is_done = lock.lock().unwrap();
341    loop {
342        PBAR.set_position(PBAR.position() + 1); // do we need to update?
343        let mut msg = prog_printer.print().unwrap();
344        msg.push_str(&observability::render_lines());
345        msg.push_str(&render_panel_from_registry());
346        PBAR.set_message(msg);
347        let result = cvar.wait_timeout(is_done, delay).unwrap();
348        is_done = result.0;
349        if *is_done {
350            break;
351        }
352    }
353    PBAR.finish_and_clear();
354}
355
356fn get_datetime_prefix() -> String {
357    chrono::Local::now()
358        .format("%Y-%m-%dT%H:%M:%S%.3f%:z")
359        .to_string()
360}
361
362fn text_updates(
363    lock: &std::sync::Mutex<bool>,
364    cvar: &std::sync::Condvar,
365    delay_opt: &Option<std::time::Duration>,
366    kind: progress::LocalProgressKind,
367) {
368    let delay = delay_opt.unwrap_or(std::time::Duration::from_secs(10));
369    let mut prog_printer = progress::make_local_printer(kind, &PROGRESS);
370    let mut is_done = lock.lock().unwrap();
371    loop {
372        eprintln!("=======================");
373        eprintln!(
374            "{}\n--{}{}{}",
375            get_datetime_prefix(),
376            prog_printer.print().unwrap(),
377            observability::render_lines(),
378            render_panel_from_registry(),
379        );
380        let result = cvar.wait_timeout(is_done, delay).unwrap();
381        is_done = result.0;
382        if *is_done {
383            break;
384        }
385    }
386}
387
388fn rcpd_updates(
389    lock: &std::sync::Mutex<bool>,
390    cvar: &std::sync::Condvar,
391    delay_opt: &Option<std::time::Duration>,
392    sender: tokio::sync::mpsc::UnboundedSender<remote_tracing::TracingMessage>,
393) {
394    tracing::debug!("Starting rcpd progress updates");
395    let delay = delay_opt.unwrap_or(std::time::Duration::from_millis(200));
396    let mut is_done = lock.lock().unwrap();
397    loop {
398        if remote_tracing::send_progress_update(&sender, &PROGRESS).is_err() {
399            // channel closed, receiver is done
400            tracing::debug!("Progress update channel closed, stopping progress updates");
401            break;
402        }
403        let result = cvar.wait_timeout(is_done, delay).unwrap();
404        is_done = result.0;
405        if *is_done {
406            break;
407        }
408    }
409}
410
411fn remote_master_updates<F>(
412    lock: &std::sync::Mutex<bool>,
413    cvar: &std::sync::Condvar,
414    delay_opt: &Option<std::time::Duration>,
415    get_progress_snapshot: F,
416    progress_type: ProgressType,
417) where
418    F: Fn() -> ProgressSnapshot<SerializableProgress> + Send + 'static,
419{
420    let interactive = match progress_type {
421        ProgressType::Auto => std::io::stderr().is_terminal(),
422        ProgressType::ProgressBar => true,
423        ProgressType::TextUpdates => false,
424    };
425    if interactive {
426        PBAR.set_style(
427            indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg}")
428                .unwrap()
429                .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
430        );
431        let delay = delay_opt.unwrap_or(std::time::Duration::from_millis(200));
432        let mut printer = RcpdProgressPrinter::new();
433        let mut is_done = lock.lock().unwrap();
434        loop {
435            let progress_map = get_progress_snapshot();
436            let source_progress = &progress_map[RcpdType::Source];
437            let destination_progress = &progress_map[RcpdType::Destination];
438            PBAR.set_position(PBAR.position() + 1); // do we need to update?
439            let mut msg = printer
440                .print(source_progress, destination_progress)
441                .unwrap();
442            msg.push_str(&render_panel_from_registry());
443            PBAR.set_message(msg);
444            let result = cvar.wait_timeout(is_done, delay).unwrap();
445            is_done = result.0;
446            if *is_done {
447                break;
448            }
449        }
450        PBAR.finish_and_clear();
451    } else {
452        let delay = delay_opt.unwrap_or(std::time::Duration::from_secs(10));
453        let mut printer = RcpdProgressPrinter::new();
454        let mut is_done = lock.lock().unwrap();
455        loop {
456            let progress_map = get_progress_snapshot();
457            let source_progress = &progress_map[RcpdType::Source];
458            let destination_progress = &progress_map[RcpdType::Destination];
459            eprintln!("=======================");
460            eprintln!(
461                "{}\n--{}{}",
462                get_datetime_prefix(),
463                printer
464                    .print(source_progress, destination_progress)
465                    .unwrap(),
466                render_panel_from_registry(),
467            );
468            let result = cvar.wait_timeout(is_done, delay).unwrap();
469            is_done = result.0;
470            if *is_done {
471                break;
472            }
473        }
474    }
475}
476
477impl ProgressTracker {
478    pub fn new(progress_type: GeneralProgressType, delay_opt: Option<std::time::Duration>) -> Self {
479        let lock_cvar =
480            std::sync::Arc::new((std::sync::Mutex::new(false), std::sync::Condvar::new()));
481        let lock_cvar_clone = lock_cvar.clone();
482        let pbar_thread = std::thread::spawn(move || {
483            let (lock, cvar) = &*lock_cvar_clone;
484            match progress_type {
485                GeneralProgressType::Remote(sender) => {
486                    rcpd_updates(lock, cvar, &delay_opt, sender);
487                }
488                GeneralProgressType::RemoteMaster {
489                    progress_type,
490                    get_progress_snapshot,
491                } => {
492                    remote_master_updates(
493                        lock,
494                        cvar,
495                        &delay_opt,
496                        get_progress_snapshot,
497                        progress_type,
498                    );
499                }
500                GeneralProgressType::User {
501                    progress_type,
502                    kind,
503                } => {
504                    let interactive = match progress_type {
505                        ProgressType::Auto => std::io::stderr().is_terminal(),
506                        ProgressType::ProgressBar => true,
507                        ProgressType::TextUpdates => false,
508                    };
509                    if interactive {
510                        progress_bar(lock, cvar, &delay_opt, kind);
511                    } else {
512                        text_updates(lock, cvar, &delay_opt, kind);
513                    }
514                }
515            }
516        });
517        Self {
518            lock_cvar,
519            pbar_thread: Some(pbar_thread),
520        }
521    }
522}
523
524impl Drop for ProgressTracker {
525    fn drop(&mut self) {
526        let (lock, cvar) = &*self.lock_cvar;
527        let mut is_done = lock.lock().unwrap();
528        *is_done = true;
529        cvar.notify_one();
530        drop(is_done);
531        if let Some(pbar_thread) = self.pbar_thread.take() {
532            pbar_thread.join().unwrap();
533        }
534    }
535}
536
537pub async fn cmp(
538    src: &std::path::Path,
539    dst: &std::path::Path,
540    log: &cmp::LogWriter,
541    settings: &cmp::Settings,
542) -> Result<cmp::Summary, anyhow::Error> {
543    cmp::cmp(&PROGRESS, src, dst, log, settings).await
544}
545
546pub async fn copy(
547    src: &std::path::Path,
548    dst: &std::path::Path,
549    settings: &copy::Settings,
550    preserve: &preserve::Settings,
551) -> Result<copy::Summary, copy::Error> {
552    copy::copy(&PROGRESS, src, dst, settings, preserve, false).await
553}
554
555pub async fn rm(path: &std::path::Path, settings: &rm::Settings) -> Result<rm::Summary, rm::Error> {
556    rm::rm(&PROGRESS, path, settings).await
557}
558
559pub async fn chmod(
560    path: &std::path::Path,
561    settings: &chmod::Settings,
562) -> Result<chmod::Summary, chmod::Error> {
563    chmod::chmod(&PROGRESS, path, settings).await
564}
565
566pub async fn link(
567    src: &std::path::Path,
568    dst: &std::path::Path,
569    update: &Option<std::path::PathBuf>,
570    settings: &link::Settings,
571) -> Result<link::Summary, link::Error> {
572    let cwd = std::env::current_dir()
573        .with_context(|| "failed to get current working directory")
574        .map_err(|err| link::Error::new(err, link::Summary::default()))?;
575    link::link(&PROGRESS, &cwd, src, dst, update, settings, false).await
576}
577
578fn render_panel_from_registry() -> String {
579    let entries = observability::registered_histograms();
580    if entries.is_empty() {
581        return String::new();
582    }
583    let snapshots: Vec<hdrhistogram::Histogram<u64>> = entries
584        .iter()
585        .map(|e| (*e.snapshot_rx.borrow()).clone())
586        .collect();
587    let units: Vec<histogram_panel::PanelUnit> = entries
588        .iter()
589        .zip(snapshots.iter())
590        .map(|(e, snap)| histogram_panel::PanelUnit {
591            label: e.label,
592            histogram: snap,
593            interval: e.interval,
594        })
595        .collect();
596    histogram_panel::render_histogram_panel(&units)
597}
598
599/// Format one pre-tracing diagnostic, optionally as a machine-readable startup record.
600#[must_use]
601pub fn format_startup_diagnostic(prefix: Option<&str>, diagnostic: &str) -> String {
602    let one_line = diagnostic
603        .lines()
604        .map(str::trim)
605        .filter(|line| !line.is_empty())
606        .collect::<Vec<_>>()
607        .join(" ");
608    format!("{}{one_line}", prefix.unwrap_or_default())
609}
610
611fn print_startup_configuration_error(
612    startup_error_prefix: Option<&str>,
613    error: &impl std::fmt::Display,
614) {
615    eprintln!(
616        "{}",
617        format_startup_diagnostic(
618            startup_error_prefix,
619            &format!("Configuration error: {error:#}"),
620        )
621    );
622}
623
624#[instrument(skip(func))] // "func" is not Debug printable
625pub fn run<Fut, Summary, Error>(
626    progress: Option<ProgressSettings>,
627    output: OutputConfig,
628    runtime_config: RuntimeConfig,
629    throttle_config: ThrottleConfig,
630    tracing_config: TracingConfig,
631    func: impl FnOnce() -> Fut,
632) -> Option<Summary>
633// we return an Option rather than a Result to indicate that callers of this function should NOT print the error
634where
635    Summary: std::fmt::Display,
636    Error: std::fmt::Display + std::fmt::Debug,
637    Fut: std::future::Future<Output = Result<Summary, Error>>,
638{
639    // force initialization of PROGRESS to set start_time at the beginning of the run
640    // (for remote master operations, PROGRESS is otherwise only accessed at the end in
641    // print_runtime_stats(), leading to near-zero walltime)
642    let _ = get_progress();
643    let OutputConfig {
644        quiet,
645        verbose,
646        print_summary,
647        suppress_runtime_stats,
648        startup_error_prefix,
649    } = output;
650    if let Err(error) = throttle_config.validate() {
651        print_startup_configuration_error(startup_error_prefix, &error);
652        return None;
653    }
654    // tracing guards must outlive the runtime so chrome/flame traces flush
655    // extract trace_identifier before install_tracing_subscriber consumes tracing_config
656    let trace_identifier = tracing_config.trace_identifier.clone();
657    if let Err(error) =
658        runtime_setup::validate_histogram_log_target(&throttle_config, &trace_identifier)
659    {
660        print_startup_configuration_error(startup_error_prefix, &error);
661        return None;
662    }
663    let _tracing_guards =
664        match runtime_setup::install_tracing_subscriber(quiet, verbose, tracing_config) {
665            Ok(guards) => guards,
666            Err(error) => {
667                print_startup_configuration_error(startup_error_prefix, &error);
668                return None;
669            }
670        };
671    if let Some(warning) = throttle_config.deprecated_max_open_files_warning() {
672        tracing::warn!(target: NOTICE_TARGET, "{warning}");
673    }
674    let res = {
675        let runtime = match runtime_setup::build_tokio_runtime(&runtime_config, &throttle_config) {
676            Ok(runtime) => runtime,
677            Err(error) => {
678                print_startup_configuration_error(startup_error_prefix, &error);
679                return None;
680            }
681        };
682        runtime_setup::spawn_throttle_replenishers(&runtime, &throttle_config, &trace_identifier);
683        let res = {
684            let _progress_tracker = progress.map(|settings| {
685                tracing::debug!("Requesting progress updates {settings:?}");
686                let delay = settings.progress_delay.map(|delay_str| {
687                    humantime::parse_duration(&delay_str)
688                        .expect("Couldn't parse duration out of --progress-delay")
689                });
690                ProgressTracker::new(settings.progress_type, delay)
691            });
692            runtime.block_on(func())
693        };
694        match &res {
695            Ok(summary) => {
696                if print_summary || verbose > 0 {
697                    println!("{summary}");
698                }
699            }
700            Err(err) => {
701                if !quiet {
702                    println!("{err:?}");
703                }
704            }
705        }
706        if (print_summary || verbose > 0)
707            && !suppress_runtime_stats
708            && let Err(err) = runtime_setup::print_runtime_stats()
709        {
710            println!("Failed to print runtime stats: {err:?}");
711        }
712        // Signal the histogram logger to exit cleanly so its final
713        // snapshot is written before the runtime drops and aborts it.
714        // No-op when histograms are disabled.
715        signal_logger_cancel();
716        if let Some(handle) = take_logger_handle() {
717            // Bound the wait so a stuck logger can't hang shutdown — 1s is
718            // generous: the logger only does a snapshot+flush on cancel.
719            let _ = runtime.block_on(async {
720                tokio::time::timeout(std::time::Duration::from_secs(1), handle).await
721            });
722        }
723        // Bounded shutdown instead of a plain drop, which WAITS for every running blocking-pool
724        // closure. A blocking syscall parked on dead media (an NFS readdir that never returns, the
725        // stdin watchdog's read(2) that only unblocks when sshd notices) would otherwise keep the
726        // process alive indefinitely after the operation itself already failed and reported.
727        // Async tasks are still dropped normally — destructors (reused-directory ACL rollbacks
728        // included) run before the wait begins; only stuck blocking threads are abandoned to
729        // process exit once the deadline passes.
730        runtime.shutdown_timeout(std::time::Duration::from_secs(10));
731        res
732    };
733    // Clear process-wide state so a second `run()` in the same process
734    // starts on a clean slate. Without this, a later run inherits the
735    // previous auto-meta sample sink and ops-in-flight cap, and its
736    // probes acquire against stale limits even when auto_meta is off.
737    reset_process_throttle_state();
738    res.ok()
739}
740
741/// Reset process-wide throttle + congestion state to its pre-`run()`
742/// defaults. Called by [`run`] on exit so callers that invoke it more
743/// than once in a single process (library users, integration tests
744/// outside this crate) aren't affected by the previous invocation's
745/// decisions.
746fn reset_process_throttle_state() {
747    congestion::clear_sample_sink();
748    observability::clear();
749    for &side in &throttle::Side::ALL {
750        for &op in &throttle::MetadataOp::ALL {
751            throttle::set_max_ops_in_flight(throttle::Resource::meta(side, op), 0);
752        }
753    }
754    throttle::disable_ops_throttle();
755    // without these resets, a second run() in the same process inherits the previous run's
756    // file-admission caps and iops-throttle. `None` disables the admission semaphores, so the next
757    // run sees a clean slate and can either configure a fresh value or leave admission unlimited.
758    throttle::set_admission_limits(None);
759    throttle::init_iops_tokens(0);
760}