pagers 0.2.2

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

use pagers_core::Cancellation;
use pagers_core::mincore::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) fn run_tui_command<
    O: ops::Op + Send + 'static,
    PM: PageMap + Clone + Send + Sync + 'static,
>(
    op: &O,
    common: &CommonArgs,
    cancellation: &Cancellation,
    lock: Option<&LockInner>,
) -> Result<(), Error>
where
    O::Output: 'static,
{
    install_signal_handler(cancellation)?;
    let (stats, _outputs, _) = run_tui::<O, PM>(op, common, cancellation)?;
    if let Some(lock) = lock {
        daemon::hold(&stats, lock, cancellation, None)?;
    }
    Ok(())
}

pub(crate) fn run_cli_command<O: ops::Op + Send + 'static, PM: PageMap + Send + Sync + 'static>(
    op: &O,
    common: &CommonArgs,
    cancellation: &Cancellation,
    format: Option<OutputFormatArg>,
    quiet: bool,
    lock: Option<&LockInner>,
) -> Result<(), Error>
where
    O::Output: 'static,
{
    install_signal_handler(cancellation)?;
    let (stats, _outputs, elapsed) = run_cli::<O, PM>(op, common, cancellation)?;
    if !quiet {
        print_summary::<O>(&stats, elapsed, format.unwrap_or_default());
    }
    if let Some(lock) = lock {
        daemon::hold(&stats, lock, cancellation, None)?;
    }
    Ok(())
}

pub(crate) fn run_daemon_command<O: ops::Op + Send + 'static, PM: PageMap + Send + Sync + 'static>(
    op: &O,
    common: &CommonArgs,
    cancellation: &Cancellation,
    lock: &LockInner,
) -> Result<(), Error>
where
    O::Output: 'static,
{
    let setup = common_setup(common)?;
    match daemon::go_daemon(lock.wait)? {
        daemon::ForkOutcome::Parent => Ok(()),
        daemon::ForkOutcome::Child(notify_fd) => {
            install_signal_handler(cancellation)?;
            let (stats, _locks, _) = match run_cli_with_setup::<O, PM>(op, setup, cancellation) {
                Ok(result) => result,
                Err(error) => {
                    eprintln!("{error}");
                    daemon::notify_and_redirect(notify_fd, 1);
                    return Err(error);
                }
            };
            daemon::hold(&stats, lock, cancellation, notify_fd)?;
            Ok(())
        }
    }
}

fn install_signal_handler(cancellation: &Cancellation) -> Result<(), Error> {
    let mut signals = signal_hook::iterator::Signals::new(signal_hook::consts::TERM_SIGNALS)
        .map_err(pagers_core::Error::from)?;
    let cancellation = cancellation.clone();
    std::thread::spawn(move || {
        for (index, _) in signals.forever().enumerate() {
            if index == 0 {
                cancellation.cancel();
            } else {
                std::process::exit(1);
            }
        }
    });
    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::new(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,
        threads: common.threads,
    };
    crawl::validate_patterns(&crawl_config)?;

    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,
    cancellation: &Cancellation,
) -> 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 tui_cancellation = cancellation.clone();
    let stats_clone = Arc::clone(&stats);
    let tui_label = O::LABEL.to_string();
    let action_sign = O::EFFECT.action_sign();
    let tui_handle = std::thread::spawn(move || {
        let cancel_on_error = tui_cancellation.clone();
        let result = pagers_tui::run(
            rx,
            tui_cancellation,
            stats_clone,
            &tui_label,
            action_sign,
            start,
        );
        if result.is_err() {
            cancel_on_error.cancel();
        }
        result
    });

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

    tui_handle
        .join()
        .map_err(|_| Error::TuiPanic)?
        .map_err(Error::Tui)?;

    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,
    cancellation: &Cancellation,
) -> RunResult<O::Output>
where
    O::Output: 'static,
{
    run_cli_with_setup::<O, PM>(op, common_setup(common)?, cancellation)
}

fn run_cli_with_setup<O: ops::Op + Send + 'static, PM: PageMap + Send + Sync + 'static>(
    op: &O,
    (range, extra_paths, crawl_config): (
        ops::FileRange,
        Vec<std::path::PathBuf>,
        crawl::CrawlConfig,
    ),
    cancellation: &Cancellation,
) -> RunResult<O::Output>
where
    O::Output: 'static,
{
    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,
        cancellation,
    );

    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::EFFECT.action_sign());
    fmt.print_summary(&summary, O::LABEL, O::EFFECT.has_action());
}