pagers 0.1.5

Page cache diagnostics and control tool for Linux and macOS
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::time::Instant;

use pagers_core::mincore::{DefaultPageMap, PageMap};
use pagers_core::mode;
use pagers_core::output::Summary;
use pagers_core::{crawl, ops};

use crate::Error;
use crate::cli::{CommonArgs, LockInner, OutputFormatArg};
use crate::daemon;

pub(crate) trait Run<D, M> {
    fn run(self) -> Result<(), Error>;
}

pub(crate) struct Cmd<'a, O, PM: PageMap = DefaultPageMap> {
    pub op: O,
    pub common: &'a CommonArgs,
    pub term: &'a Arc<AtomicBool>,
    pub format: Option<OutputFormatArg>,
    pub quiet: bool,
    pub lock: Option<&'a LockInner>,
    _phantom: std::marker::PhantomData<PM>,
}

impl<'a, O, PM: PageMap> Cmd<'a, O, PM> {
    pub fn new(
        op: O,
        common: &'a CommonArgs,
        term: &'a Arc<AtomicBool>,
        format: Option<OutputFormatArg>,
        quiet: bool,
        lock: Option<&'a LockInner>,
    ) -> Self {
        Self {
            op,
            common,
            term,
            format,
            quiet,
            lock,
            _phantom: std::marker::PhantomData,
        }
    }
}

impl<O: ops::Op + Send + 'static, PM: PageMap + Clone + Send + Sync + 'static>
    Run<mode::NoDaemon, mode::TuiMode> for Cmd<'_, O, PM>
where
    O::Output: 'static,
{
    fn run(self) -> Result<(), Error> {
        let (stats, _outputs, _) = run_tui::<O, PM>(&self.op, self.common, self.term)?;
        if let Some(lock) = self.lock {
            daemon::hold(&stats, lock, self.term, None);
        }
        Ok(())
    }
}

impl<O: ops::Op + Send + 'static, PM: PageMap + Send + Sync + 'static>
    Run<mode::NoDaemon, mode::CliMode> for Cmd<'_, O, PM>
where
    O::Output: 'static,
{
    fn run(self) -> Result<(), Error> {
        let (stats, _, elapsed) = run_cli::<O, PM>(&self.op, self.common)?;
        if !self.quiet {
            print_summary::<O>(&stats, elapsed, self.format.unwrap_or_default());
        }
        if let Some(lock) = self.lock {
            daemon::hold(&stats, lock, self.term, None);
        }
        Ok(())
    }
}

impl<O: ops::Op + Send + 'static, PM: PageMap + Clone + Send + Sync + 'static>
    Run<mode::Daemon, mode::CliMode> for Cmd<'_, O, PM>
where
    O::Output: 'static,
{
    fn run(self) -> Result<(), Error> {
        let lock = self.lock.expect("daemon requires LockInner");
        match daemon::go_daemon(lock.wait)? {
            daemon::ForkOutcome::Parent => Ok(()),
            daemon::ForkOutcome::Child(notify_fd) => {
                let (stats, _locks, _) = run_cli::<O, PM>(&self.op, self.common)?;
                daemon::hold(&stats, lock, self.term, notify_fd);
                Ok(())
            }
        }
    }
}

pub(crate) type RunResult<O> = Result<(Arc<ops::Stats>, Vec<O>, f64), Error>;

fn common_setup(
    common: &CommonArgs,
) -> Result<(ops::FileRange, Vec<std::path::PathBuf>, crawl::CrawlConfig), Error> {
    let (offset, max_len) = if let Some(ref range) = common.range {
        let page_size = *pagers_core::pagesize::PAGE_SIZE as u64;
        let aligned = (range.start_b / page_size) * page_size;
        let max_len = match range.end_b {
            Some(end) if end <= aligned => return Err(Error::RangeOrder),
            Some(end) => Some(end - aligned),
            None => None,
        };
        (aligned, max_len)
    } else {
        (0, None)
    };

    let range = ops::FileRange { offset, max_len };

    let stdin_is_batch = common
        .batch
        .as_deref()
        .is_some_and(|p| p == std::path::Path::new("-"));
    let mut extra_paths = common.paths.clone();
    let batch = if stdin_is_batch {
        let stdin_paths = crawl::read_batch_paths(std::path::Path::new("-"), common.nul_delim)
            .map_err(pagers_core::Error::from)?;
        extra_paths.extend(stdin_paths);
        None
    } else {
        common.batch.clone()
    };

    let crawl_config = crawl::CrawlConfig {
        follow_symlinks: common.follow_symlinks,
        single_filesystem: common.single_filesystem,
        count_hardlinks: common.count_hardlinks,
        ignore_patterns: common.filter.ignore.clone(),
        filter_patterns: common.filter.filter.clone(),
        max_file_size: common.max_file_size,
        batch,
        nul_delim: common.nul_delim,
        #[cfg(feature = "rayon")]
        threads: common.threads,
    };

    Ok((range, extra_paths, crawl_config))
}

pub(crate) fn run_tui<O: ops::Op + Send + 'static, PM: PageMap + Clone + Send + Sync + 'static>(
    op: &O,
    common: &CommonArgs,
    term: &Arc<AtomicBool>,
) -> RunResult<O::Output>
where
    O::Output: 'static,
{
    let (range, extra_paths, crawl_config) = common_setup(common)?;
    let stats = Arc::new(ops::Stats::new());
    let start = Instant::now();

    let (tx, rx) = std::sync::mpsc::channel::<pagers_core::events::Event<PM>>();
    let display = mode::Tui::new(tx);

    let term_clone = Arc::clone(term);
    let stats_clone = Arc::clone(&stats);
    let tui_label = O::LABEL.to_string();
    let action_sign = O::ACTION_SIGN;
    let tui_handle = std::thread::spawn(move || {
        if let Err(e) = pagers_tui::run(rx, term_clone, stats_clone, &tui_label, action_sign, start)
        {
            ::tracing::error!("TUI error: {e}");
        }
    });

    let outputs = crawl::crawl_and_process::<O, PM, _>(
        &extra_paths,
        &crawl_config,
        op,
        &range,
        &stats,
        &display,
    );

    tui_handle.join().expect("TUI thread panicked");

    let elapsed = start.elapsed().as_secs_f64();
    Ok((stats, outputs?, elapsed))
}

pub(crate) fn run_cli<O: ops::Op + Send + 'static, PM: PageMap + Send + Sync + 'static>(
    op: &O,
    common: &CommonArgs,
) -> RunResult<O::Output>
where
    O::Output: 'static,
{
    let (range, extra_paths, crawl_config) = common_setup(common)?;
    let stats = Arc::new(ops::Stats::new());
    let start = Instant::now();

    let display = mode::Cli;
    let outputs = crawl::crawl_and_process::<O, PM, _>(
        &extra_paths,
        &crawl_config,
        op,
        &range,
        &stats,
        &display,
    );

    let elapsed = start.elapsed().as_secs_f64();
    Ok((stats, outputs?, elapsed))
}

fn print_summary<O: ops::Op>(stats: &ops::Stats, elapsed: f64, fmt: OutputFormatArg) {
    let summary = Summary::from_stats(stats, elapsed, O::ACTION_SIGN);
    fmt.print_summary(&summary, O::LABEL, O::ACTION_SIGN != 0);
}