/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! edenfsctl du

use std::collections::BTreeSet;
use std::collections::HashSet;
use std::fmt;
use std::fs;
use std::path::PathBuf;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
use colored::Colorize;
use comfy_table::Cell;
use comfy_table::CellAlignment;
use comfy_table::Color;
use comfy_table::Row;
use comfy_table::Table;
use edenfs_client::checkout::find_checkout;
use edenfs_client::checkout::EdenFsCheckout;
use edenfs_client::redirect;
use edenfs_client::redirect::get_effective_redirections;
use edenfs_client::redirect::scratch::usage_for_dir;
use edenfs_client::EdenFsClient;
use edenfs_client::EdenFsInstance;
use edenfs_utils::bytes_from_path;
use edenfs_utils::get_buck_command;
use edenfs_utils::get_env_with_buck_version;
use edenfs_utils::get_environment_suitable_for_subprocess;
use edenfs_utils::path_from_bytes;
use serde::Serialize;
use subprocess::Exec;
use subprocess::Redirection as SubprocessRedirection;

use crate::ExitCode;

#[derive(Parser, Debug)]
#[clap(about = "Show disk space usage for a checkout")]
pub struct DiskUsageCmd {
    #[clap(help = "Names of the mount points")]
    mounts: Vec<PathBuf>,

    #[clap(
        long,
        help = "Performs automated cleanup",
        conflicts_with = "deep-clean",
        conflicts_with = "json"
    )]
    clean: bool,

    #[clap(
        long,
        help = "Performs automated cleanup (--clean) and removes fsck dirs. \
        Unlike --clean this will destroy unrecoverable data. If you have any \
        local changes you hope to recover, recover them before you run this command.",
        conflicts_with = "json"
    )]
    deep_clean: bool,

    #[clap(
        long,
        help = "Performs automated cleanup of the orphaned redirections. \
        This is a subset of --clean that is safe to run without affecting \
        running tools relying on redirections.",
        conflicts_with = "deep-clean",
        conflicts_with = "clean",
        conflicts_with = "json"
    )]
    clean_orphaned: bool,

    #[clap(long, help = "Print the output in JSON format")]
    json: bool,
}

#[derive(Serialize)]
struct AggregatedUsageCounts {
    materialized: u64,
    ignored: u64,
    redirection: u64,
    orphaned_redirections: u64,
    backing: u64,
    shared: u64,
    fsck: u64,
}

impl AggregatedUsageCounts {
    fn new() -> AggregatedUsageCounts {
        AggregatedUsageCounts {
            materialized: 0,
            ignored: 0,
            redirection: 0,
            orphaned_redirections: 0,
            backing: 0,
            shared: 0,
            fsck: 0,
        }
    }
}

fn format_size(size: u64) -> String {
    if size > 1000000000 {
        format!("{:.1} GB", size as f64 / 1000000000.0)
    } else if size > 1000000 {
        format!("{:.1} MB", size as f64 / 1000000.0)
    } else if size > 1000 {
        format!("{:.1} KB", size as f64 / 1000.0)
    } else if size > 0 {
        format!("{} B", size)
    } else {
        "0".to_string()
    }
}

impl fmt::Display for AggregatedUsageCounts {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut table = Table::new();
        table.load_preset(comfy_table::presets::NOTHING);

        if self.materialized > 0 {
            let mut row = Row::new();
            row.add_cell(Cell::new("Materialized files:").set_alignment(CellAlignment::Right));
            row.add_cell(Cell::new(format_size(self.materialized)));
            if f.alternate() {
                row.add_cell(Cell::new("Not cleaned. Please see WARNING above").fg(Color::Yellow));
            }
            table.add_row(row);
        }
        if self.redirection > 0 {
            let mut row = Row::new();
            row.add_cell(Cell::new("Redirections:").set_alignment(CellAlignment::Right));
            row.add_cell(Cell::new(format_size(self.redirection)));
            if f.alternate() {
                row.add_cell(Cell::new("Cleaned").fg(Color::Green));
            }
            table.add_row(row);
        }
        if self.orphaned_redirections > 0 {
            let mut row = Row::new();
            row.add_cell(Cell::new("Orphaned redirections:").set_alignment(CellAlignment::Right));
            row.add_cell(Cell::new(format_size(self.orphaned_redirections)));
            if f.alternate() || f.sign_minus() {
                row.add_cell(Cell::new("Cleaned").fg(Color::Green));
            }
            table.add_row(row);
        }
        if self.ignored > 0 {
            let mut row = Row::new();
            row.add_cell(Cell::new("Ignored files:").set_alignment(CellAlignment::Right));
            row.add_cell(Cell::new(format_size(self.ignored)));
            if f.alternate() {
                row.add_cell(Cell::new("Not cleaned. Please see WARNING above").fg(Color::Yellow));
            }
            table.add_row(row);
        }
        if self.backing > 0 {
            let mut row = Row::new();
            row.add_cell(Cell::new("Backing repos:").set_alignment(CellAlignment::Right));
            row.add_cell(Cell::new(format_size(self.backing)));
            if f.alternate() {
                row.add_cell(Cell::new("Not cleaned. Please see CAUTION above").fg(Color::Yellow));
            }
            table.add_row(row);
        }
        if self.shared > 0 {
            let mut row = Row::new();
            row.add_cell(Cell::new("Shared space:").set_alignment(CellAlignment::Right));
            row.add_cell(Cell::new(format_size(self.shared)));
            if f.alternate() {
                row.add_cell(Cell::new("Cleaned").fg(Color::Green));
            }
            table.add_row(row);
        }
        if self.fsck > 0 {
            let mut row = Row::new();
            row.add_cell(
                Cell::new("Filesystem Check recovered files:").set_alignment(CellAlignment::Right),
            );
            row.add_cell(Cell::new(format_size(self.fsck)));
            if f.alternate() {
                if f.sign_plus() {
                    row.add_cell(Cell::new("Cleaned").fg(Color::Green));
                } else {
                    row.add_cell(
                        Cell::new(
                            "Not cleaned. Directories listed above. Check and remove manually",
                        )
                        .fg(Color::Yellow),
                    );
                }
            }
            table.add_row(row);
        }

        write!(f, "{}", table)
    }
}

async fn ignored_usage_counts_for_mount(
    checkout: &EdenFsCheckout,
    client: &EdenFsClient,
) -> Result<u64> {
    let scm_status = client
        .getScmStatus(
            &bytes_from_path(checkout.path())?,
            true,
            &checkout
                .get_snapshot()?
                .working_copy_parent
                .as_bytes()
                .to_vec(),
        )
        .await?;

    let mut aggregated_usage_counts_ignored = 0;
    for (rel_path, _file_status) in scm_status.entries {
        let path = checkout.path().join(path_from_bytes(&rel_path)?);
        aggregated_usage_counts_ignored += match fs::symlink_metadata(path) {
            Ok(metadata) => Ok(metadata.len()),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
                // Status can show files that were present in the overlay
                // before a redirection was mounted over the top of it,
                // which makes them inaccessible here.  Alternatively,
                // someone may have raced with us and removed the file
                // between the status call and our attempt to stat it.
                // Just absorb the error here and ignore it.
                Ok(0)
            }
            Err(e) => Err(e),
        }?;
    }
    Ok(aggregated_usage_counts_ignored)
}

fn get_hg_cache_path() -> Result<PathBuf> {
    let output = Exec::cmd("hg")
        .args(&["config", "remotefilelog.cachepath"])
        .stdout(SubprocessRedirection::Pipe)
        .stderr(SubprocessRedirection::Pipe)
        .env_clear()
        .env_extend(&get_environment_suitable_for_subprocess())
        .capture()?;

    if output.success() {
        let raw_path = output.stdout_str();
        let raw_path = raw_path.trim();
        assert!(!raw_path.is_empty());
        Ok(PathBuf::from(raw_path))
    } else {
        Err(anyhow!(
            "Failed to execute `hg config remotefilelog.cachepath`, stderr: {}, exit status: {:?}",
            output.stderr_str(),
            output.exit_status,
        ))
    }
}

fn write_title(title: &str) {
    println!("\n{}", title);
    println!("{}", "-".repeat(title.len()));
}

fn write_failed_to_check_files_message(file_paths: &HashSet<PathBuf>) {
    if !file_paths.is_empty() {
        println!(
            "\n{}",
            "Warning: failed to check paths due to file not found or permission errors:".yellow()
        );
        for f in file_paths {
            println!("{}", format!("{}", f.display()).yellow());
        }
        println!(
            "\n{}",
            "Note that we also cannot clean these paths.".yellow()
        );
    }
}

impl DiskUsageCmd {
    fn should_clean(&self) -> bool {
        self.clean || self.deep_clean
    }

    /// Get all the EdenFS mount that `du` should run on.
    ///
    /// This is either the mounts passed as an argument, or all the mounts known to the EdenFS
    /// instance.
    fn get_mounts(&self, instance: &EdenFsInstance) -> Result<Vec<PathBuf>> {
        if !self.mounts.is_empty() {
            Ok((&self.mounts).to_vec())
        } else {
            let config_paths: Vec<PathBuf> = instance
                .get_configured_mounts_map()?
                .keys()
                .cloned()
                .collect();
            Ok(config_paths)
        }
    }

    /// Remove all the fsck directories if --deep-clean is used.
    fn clean_fsck_directories(&self, fsck_dirs: Vec<PathBuf>) {
        if self.deep_clean {
            println!();
            for dir in &fsck_dirs {
                println!(
                    "\n{}",
                    format!("Reclaiming space from directory: {}", dir.display()).blue()
                );
                match fs::remove_dir_all(&dir) {
                    Ok(_) => println!("{}", "Space reclaimed. Directory removed.".blue()),
                    Err(e) => println!(
                        "{}",
                        format!("Failed to remove {} : {:?}", dir.display(), e).yellow()
                    ),
                };
            }
        } else if self.clean {
            let fsck_dir_strings: Vec<String> = fsck_dirs
                .iter()
                .map(|path| path.display().to_string())
                .collect();

            if !fsck_dir_strings.is_empty() {
                println!(
                            "\n{}",
                            format!(
                                "A filesytem check recovered data and stored it at:
- {}

If you have recovered all that you need from these locations, you can remove that directory to reclaim the disk space.

To automatically remove this directory, run `eden du --deep-clean`.",
                                fsck_dir_strings.join("\n- ")
                            )
                            .blue()
                        )
            }
        }
    }
}

/// Get all the backing repositories associated with the passed in checkouts.
fn get_backing_repos(checkouts: &[EdenFsCheckout]) -> HashSet<PathBuf> {
    checkouts
        .iter()
        .filter_map(|checkout| checkout.backing_repo())
        .collect()
}

/// Get the target folder for all the redirections.
///
/// This returns 2 sets: the target of all redirections, and all the Buck redirections.
fn get_redirections(checkouts: &[EdenFsCheckout]) -> Result<(BTreeSet<PathBuf>, HashSet<PathBuf>)> {
    let mut redirections = BTreeSet::new();
    let mut buck_redirections = HashSet::new();

    for checkout in checkouts.iter() {
        for (_, redir) in get_effective_redirections(checkout).with_context(|| {
            format!(
                "Failed to get redirections for {}",
                checkout.path().display()
            )
        })? {
            if let Some(target) = redir.expand_target_abspath(checkout).with_context(|| {
                format!(
                    "Failed to get redirection destination for {}",
                    redir.repo_path.display()
                )
            })? {
                redirections.insert(target);
            }

            let repo_path = redir.repo_path();
            if let Some(file_name) = repo_path.file_name() {
                if file_name == "buck-out" {
                    buck_redirections.insert(checkout.path().join(repo_path));
                }
            }
        }
    }
    Ok((redirections, buck_redirections))
}

/// Get all the checkous associated with the passed in mounts.
fn get_checkouts(mounts: &[PathBuf], instance: &EdenFsInstance) -> Result<Vec<EdenFsCheckout>> {
    Ok(mounts
        .iter()
        .map(|mount| {
            find_checkout(instance, mount)
                .with_context(|| format!("Failed to find checkout for {}", mount.display()))
        })
        .collect::<Result<_, anyhow::Error>>()?)
}

/// Get all the fsck directories for the pssed in checkouts.
///
/// Some checkouts do not have a fsck directory, the returned Vec will not included them.
fn get_fsck_dirs(checkouts: &[EdenFsCheckout]) -> Vec<PathBuf> {
    checkouts
        .iter()
        .filter_map(|checkout| {
            let fsck_dir = checkout.fsck_dir();
            if fsck_dir.exists() {
                Some(fsck_dir)
            } else {
                None
            }
        })
        .collect()
}

/// Warn about backing repositories that are non-empty working copy.
fn warn_about_working_copy_for_backing_repo(backing_repos: &HashSet<PathBuf>) -> Result<()> {
    let mut warned = false;
    for backing_repo in backing_repos.iter() {
        // A non-empty working copy will contain more than just the .hg at the root.
        if backing_repo.join(".hg").is_dir() && fs::read_dir(backing_repo)?.count() > 1 {
            if !warned {
                println!(
                    "\nWorking copy detected in backing repo. This is not generally useful \
                    and just takes up space.  You can make this a bare repo to reclaim \
                    space by running:\n"
                );
                warned = true;
            }
            println!("hg -R {} checkout null", backing_repo.display());
        }
    }
    Ok(())
}

/// Run `buck clean` to reduce disk space usage of the buck-out directories.
fn clean_buck_redirections(buck_redirections: HashSet<PathBuf>) -> Result<()> {
    for redir in buck_redirections {
        println!(
            "\n{}",
            format!("Reclaiming space from redirection: {}", redir.display()).blue()
        );
        if let Some(basename) = redir.parent() {
            let output = Exec::cmd(get_buck_command())
                .arg("clean")
                .stderr(SubprocessRedirection::Pipe)
                .cwd(basename)
                .env_clear()
                .env_extend(&get_env_with_buck_version(basename)?)
                .capture()
                .with_context(|| format!("Failed to run buck {}", get_buck_command()))?;

            if output.success() {
                println!("{}", "Space reclaimed".blue());
            } else {
                println!(
                    "{}",
                    format!(
                        "Failed to execute buck clean from {}, stderr: {}",
                        basename.display(),
                        output.stderr_str().trim()
                    )
                    .yellow()
                );
            }
        } else {
            return Err(anyhow!("Found invalid redirection: {}", redir.display()));
        };
    }
    Ok(())
}

#[async_trait]
impl crate::Subcommand for DiskUsageCmd {
    async fn run(&self) -> Result<ExitCode> {
        let instance = EdenFsInstance::global();
        let client = instance.connect(None).await?;

        let mounts = self
            .get_mounts(&instance)
            .context("Failed to get EdenFS mounts")?;
        let checkouts =
            get_checkouts(&mounts, &instance).context("Failed to get EdenFS checkouts")?;
        let backing_repos = get_backing_repos(&checkouts);
        let (redirections, buck_redirections) =
            get_redirections(&checkouts).context("Failed to get EdenFS redirections")?;
        let orphaned_redirections =
            redirect::scratch::get_orphaned_redirection_targets(&redirections)
                .unwrap_or_else(|_| Vec::new());
        let fsck_dirs = get_fsck_dirs(&checkouts);

        let mut aggregated_usage_counts = AggregatedUsageCounts::new();

        let mut backing_failed_file_checks = HashSet::new();
        for b in backing_repos.iter() {
            // GET SUMMARY INFO for backing counts
            let (usage_count, failed_file_checks) = usage_for_dir(b, None).with_context(|| {
                format!(
                    "Failed to measure disk space usage for backing repository {}",
                    b.display()
                )
            })?;
            aggregated_usage_counts.backing += usage_count;
            backing_failed_file_checks.extend(failed_file_checks);
        }

        let mut mount_failed_file_checks = HashSet::new();
        for checkout in checkouts.iter() {
            // GET SUMMARY INFO for materialized counts
            let overlay_dir = checkout.data_dir().join("local");
            let (usage_count, failed_file_checks) = usage_for_dir(&overlay_dir, None)
                .with_context(|| {
                    format!(
                        "Failed to measure disk space usage for overlay {}",
                        overlay_dir.display()
                    )
                })?;
            aggregated_usage_counts.materialized += usage_count;
            mount_failed_file_checks.extend(failed_file_checks);

            // GET SUMMARY INFO for ignored counts
            aggregated_usage_counts.ignored +=
                ignored_usage_counts_for_mount(&checkout, &client).await?;
        }

        for fsck_dir in fsck_dirs.iter() {
            let (usage_count, failed_file_checks) =
                usage_for_dir(fsck_dir, None).with_context(|| {
                    format!(
                        "Failed to measure disk space usage for fsck directory {}",
                        fsck_dir.display()
                    )
                })?;
            aggregated_usage_counts.fsck += usage_count;
            mount_failed_file_checks.extend(failed_file_checks);
        }

        let mut redirection_failed_file_checks = HashSet::new();
        for target in redirections {
            // GET SUMMARY INFO for redirections
            let (usage_count, failed_file_checks) =
                usage_for_dir(&target, None).with_context(|| {
                    format!(
                        "Failed to measure disk space usage for redirection {}",
                        target.display()
                    )
                })?;
            aggregated_usage_counts.redirection += usage_count;
            redirection_failed_file_checks.extend(failed_file_checks);
        }

        let mut orphaned_redirection_failed_file_checks = HashSet::new();
        for orphaned in orphaned_redirections.iter() {
            let (usage_count, failed_file_checks) =
                usage_for_dir(orphaned, None).with_context(|| {
                    format!(
                        "Failed to measure disk usage for orphaned redirection {}",
                        orphaned.display()
                    )
                })?;
            aggregated_usage_counts.orphaned_redirections += usage_count;
            orphaned_redirection_failed_file_checks.extend(failed_file_checks);
        }

        // Make immutable
        let backing_failed_file_checks = backing_failed_file_checks;
        let mount_failed_file_checks = mount_failed_file_checks;
        let redirection_failed_file_checks = redirection_failed_file_checks;
        let buck_redirections = buck_redirections;

        // GET SUMMARY INFO for shared usage
        let mut shared_failed_file_checks = HashSet::new();
        let (logs_dir_usage, failed_logs_dir_file_checks) =
            usage_for_dir(&instance.logs_dir(), None).with_context(|| {
                format!(
                    "Failed to measure disk space usage for EdenFS logs {}",
                    instance.logs_dir().display()
                )
            })?;
        aggregated_usage_counts.shared += logs_dir_usage;
        shared_failed_file_checks.extend(failed_logs_dir_file_checks);
        let (storage_dir_usage, failed_storage_dir_file_checks) =
            usage_for_dir(&instance.storage_dir(), None).with_context(|| {
                format!(
                    "Failed to measure disk space usage for EdenFS LocalStore {}",
                    instance.storage_dir().display()
                )
            })?;
        aggregated_usage_counts.shared += storage_dir_usage;
        shared_failed_file_checks.extend(failed_storage_dir_file_checks);

        // Make immutable
        let shared_failed_file_checks = shared_failed_file_checks;
        let aggregated_usage_counts = aggregated_usage_counts;

        // PRINT OUTPUT
        if self.json {
            println!(
                "{}",
                serde_json::to_string(&aggregated_usage_counts)
                    .context("Failed to serialize usage counts")?
            );
        } else {
            if self.should_clean() {
                println!(
                    "{}",
                    "WARNING: --clean/--deep-clean options don't remove ignored files. \
                    Materialized files will be de-materialized once committed. \
                    Use `hg status -i` to see Ignored files, `hg clean --all` \
                    to remove them but be careful: it will remove untracked files as well! \
                    It is best to use `eden redirect` or the `mkscratch` utility to relocate \
                    files outside the repo rather than to ignore and clean them up."
                        .yellow()
                );
            }

            // PRINT MOUNTS
            write_title("Mounts");
            for path in &mounts {
                println!("{}", path.display());
            }
            write_failed_to_check_files_message(&mount_failed_file_checks);

            if self.should_clean() {
                self.clean_fsck_directories(fsck_dirs);
            }

            // PRINT REDIRECTIONS
            write_title("Buck redirections");
            if buck_redirections.is_empty() {
                println!("No Buck redirections");
            } else {
                for redir in &buck_redirections {
                    println!("{}", redir.display());
                }
            }
            write_failed_to_check_files_message(&redirection_failed_file_checks);

            if !buck_redirections.is_empty() {
                if self.should_clean() {
                    clean_buck_redirections(buck_redirections)
                        .context("Failed to clean Buck redirections")?;
                } else {
                    println!(
                        "\nTo reclaim space from buck-out directories, run `buck clean` from the \
                        parent of the buck-out directory."
                    );
                }
            }

            write_title("Orphaned redirections");
            if orphaned_redirections.is_empty() {
                println!("No orphaned redirections");
            } else {
                for redir in orphaned_redirections.iter() {
                    println!("{}", redir.display());
                }
            }
            write_failed_to_check_files_message(&orphaned_redirection_failed_file_checks);

            if !orphaned_redirections.is_empty() {
                if self.clean_orphaned || self.should_clean() {
                    for redir in orphaned_redirections.iter() {
                        fs::remove_dir_all(&redir).with_context(|| {
                            format!("Failed to recursively remove {}", redir.display())
                        })?;
                    }
                }
            }

            // PRINT BACKING REPOS
            if !backing_repos.is_empty() || !backing_failed_file_checks.is_empty() {
                write_title("Backing repos");
            }
            if !backing_repos.is_empty() {
                for backing in backing_repos.iter() {
                    println!("{}", backing.display());
                }
                println!(
                    "\n{}",
                    "CAUTION: You can lose work and break things by manually deleting data \
                    from the backing repo directory!"
                        .yellow()
                );
            }
            write_failed_to_check_files_message(&backing_failed_file_checks);

            // GET HGCACHE PATH
            let hg_cache_path = get_hg_cache_path().context("Failed to get hgcache path")?;

            println!("\nTo reclaim space from the hgcache directory, run:");
            if cfg!(windows) {
                println!("\n`rmdir {}`", hg_cache_path.display());
            } else {
                println!("\n`rm -rf {}/*`", hg_cache_path.display());
            }
            println!(
                "\nNOTE: The hgcache should manage its size itself. You should only run the command \
                above if you are completely out of space and quickly need to reclaim some space \
                temporarily. This will affect other users if you run this command on a shared machine."
            );

            warn_about_working_copy_for_backing_repo(&backing_repos)
                .context("Failed to warn about working copy in backing repo")?;

            // PRINT SHARED SPACE
            write_title("Shared space");
            if self.should_clean() {
                println!(
                    "{}",
                    "Cleaning shared space used by the storage engine...".blue()
                );
                let output = Exec::cmd("eden")
                    .arg("gc")
                    .stdout(SubprocessRedirection::Pipe)
                    .stderr(SubprocessRedirection::Pipe)
                    .capture()?;

                if output.success() {
                    println!("{}", "Finished cleaning shared space.".blue())
                } else {
                    return Err(anyhow!(
                        "Failed to execute `eden gc`, stderr: {}, exit status: {:?}",
                        output.stderr_str(),
                        output.exit_status,
                    ));
                }
            } else {
                println!("Run `eden gc` to reduce the space used by the storage engine.");
            }
            write_failed_to_check_files_message(&shared_failed_file_checks);

            // PRINT SUMMARY
            write_title("Summary");
            if self.clean_orphaned {
                println!("{:-}", aggregated_usage_counts);
            } else if self.deep_clean {
                println!("{:+#}", aggregated_usage_counts);
            } else if self.clean {
                println!("{:#}", aggregated_usage_counts);
            } else {
                println!("{}", aggregated_usage_counts);
            }

            if !self.should_clean() {
                println!(
                    "{}",
                    "\nTo perform automated cleanup, run `eden du --clean`".blue()
                );
            }
        }
        Ok(0)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! edenfsctl gc

use std::io::stderr;
use std::io::Write;

use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
use edenfs_client::EdenFsInstance;

use crate::ExitCode;

#[derive(Parser, Debug)]
#[clap(about = "Minimize disk and memory usage by freeing caches")]
pub struct GcCmd {}

#[async_trait]
impl crate::Subcommand for GcCmd {
    async fn run(&self) -> Result<ExitCode> {
        let client = EdenFsInstance::global().connect(None).await?;

        // TODO: unload inodes

        eprint!("Clearing and compacting local caches...");
        stderr().flush()?;
        client.clearAndCompactLocalStore().await?;
        eprintln!();

        // TODO: clear kernel caches here

        Ok(0)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::env;
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;

use anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
use edenfs_client::EdenFsInstance;
use hg_util::path::expand_path;
use tracing::event;
use tracing::Level;

mod config;
mod debug;
mod du;
mod gc;
mod list;
mod minitop;
mod pid;
mod prefetch_profile;
mod redirect;
mod status;
mod top;
mod uptime;
mod util;

#[cfg(unix)]
const DEFAULT_CONFIG_DIR: &str = "~/local/.eden";
#[cfg(unix)]
const DEFAULT_ETC_EDEN_DIR: &str = "/etc/eden";

#[cfg(windows)]
const DEFAULT_CONFIG_DIR: &str = "~\\.eden";
#[cfg(windows)]
const DEFAULT_ETC_EDEN_DIR: &str = "C:\\ProgramData\\facebook\\eden";

// Used to determine whether we should gate off certain oxidized edenfsctl commands
const ROLLOUT_JSON: &str = "edenfsctl_rollout.json";
const EXPERIMENTAL_COMMANDS: &[&str] = &["redirect"];

type ExitCode = i32;

#[derive(Parser, Debug)]
#[clap(
    name = "edenfsctl",
    disable_version_flag = true,
    disable_help_flag = false
)]
pub struct MainCommand {
    /// The path to the directory where edenfs stores its internal state.
    #[clap(global = true, long, parse(from_str = expand_path))]
    config_dir: Option<PathBuf>,

    /// Path to directory that holds the system configuration files.
    #[clap(global = true, long, parse(from_str = expand_path))]
    etc_eden_dir: Option<PathBuf>,

    /// Path to directory where .edenrc config file is stored.
    #[clap(global = true, long, parse(from_str = expand_path))]
    home_dir: Option<PathBuf>,

    /// Path to directory within a checkout.
    #[clap(global = true, long, parse(from_str = expand_path), hide = true)]
    checkout_dir: Option<PathBuf>,

    #[clap(global = true, long)]
    pub debug: bool,

    #[clap(subcommand)]
    pub subcommand: TopLevelSubcommand,
}

/// The first level of edenfsctl subcommands.
#[async_trait]
pub trait Subcommand: Send + Sync {
    async fn run(&self) -> Result<ExitCode>;

    fn get_mount_path_override(&self) -> Option<PathBuf> {
        None
    }
}

/**
 * The first level of edenfsctl subcommands.
 */
#[derive(Parser, Debug)]
pub enum TopLevelSubcommand {
    Config(crate::config::CliConfigCmd),
    Debug(crate::debug::DebugCmd),
    Du(crate::du::DiskUsageCmd),
    Fsconfig(crate::config::FsConfigCmd),
    // Gc(crate::gc::GcCmd),
    List(crate::list::ListCmd),
    Minitop(crate::minitop::MinitopCmd),
    Pid(crate::pid::PidCmd),
    #[clap(subcommand, alias = "pp")]
    PrefetchProfile(crate::prefetch_profile::PrefetchCmd),
    #[clap(subcommand, alias = "redir")]
    Redirect(crate::redirect::RedirectCmd),
    #[clap(alias = "health")]
    Status(crate::status::StatusCmd),
    // Top(crate::top::TopCmd),
    Uptime(crate::uptime::UptimeCmd),
}

impl TopLevelSubcommand {
    fn subcommand(&self) -> &dyn Subcommand {
        use TopLevelSubcommand::*;

        match self {
            Config(cmd) => cmd,
            Fsconfig(cmd) => cmd,
            Debug(cmd) => cmd,
            Du(cmd) => cmd,
            // Gc(cmd) => cmd,
            List(cmd) => cmd,
            Minitop(cmd) => cmd,
            Pid(cmd) => cmd,
            PrefetchProfile(cmd) => cmd,
            Redirect(cmd) => cmd,
            Status(cmd) => cmd,
            // Top(cmd) => cmd,
            Uptime(cmd) => cmd,
        }
    }

    fn name(&self) -> &'static str {
        // TODO: Is there a way to extract the subcommand's name from clap?
        // Otherwise, there is a risk of divergence with clap's own attributes.
        match self {
            TopLevelSubcommand::Config(_) => "config",
            TopLevelSubcommand::Debug(_) => "debug",
            TopLevelSubcommand::Du(_) => "du",
            TopLevelSubcommand::Fsconfig(_) => "fsconfig",
            //TopLevelSubcommand::Gc(_) => "gc",
            TopLevelSubcommand::List(_) => "list",
            TopLevelSubcommand::Minitop(_) => "minitop",
            TopLevelSubcommand::Pid(_) => "pid",
            TopLevelSubcommand::PrefetchProfile(_) => "prefetch-profile",
            TopLevelSubcommand::Redirect(_) => "redirect",
            TopLevelSubcommand::Status(_) => "status",
            //TopLevelSubcommand::Top(_) => "top",
            TopLevelSubcommand::Uptime(_) => "uptime",
        }
    }
}

#[async_trait]
impl Subcommand for TopLevelSubcommand {
    async fn run(&self) -> Result<ExitCode> {
        self.subcommand().run().await
    }

    fn get_mount_path_override(&self) -> Option<PathBuf> {
        self.subcommand().get_mount_path_override()
    }
}

impl MainCommand {
    fn get_config_dir(&self) -> Result<PathBuf> {
        // A config dir might be provided as a top-level argument. Top-level arguments take
        // precedent over sub-command args.
        if let Some(config_dir) = &self.config_dir {
            if config_dir.as_os_str().is_empty() {
                bail!("empty --config-dir path specified")
            }
            Ok(config_dir.clone())
        // Then check if the optional mount path provided by some subcommands is an EdenFS mount.
        // If it's provided and is a valid EdenFS mount, use the mounts config dir.
        } else if let Some(config_dir) = self
            .subcommand
            .get_mount_path_override()
            .and_then(|x| util::locate_eden_config_dir(&x))
        {
            Ok(config_dir)
        // Then check if the current working directory is an EdenFS mount. If not, we should
        // default to the default config-dir location which varies by platform.
        } else {
            Ok(env::current_dir()
                .map_err(From::from)
                .and_then(|cwd| {
                    util::locate_eden_config_dir(&cwd)
                        .ok_or_else(|| anyhow!("cwd is not in an eden mount"))
                })
                .unwrap_or(expand_path(DEFAULT_CONFIG_DIR)))
        }
    }

    fn get_home_dir(&self) -> Option<PathBuf> {
        if let Some(home_dir) = &self.home_dir {
            Some(home_dir.clone())
        } else {
            dirs::home_dir()
        }
    }

    fn set_working_directory(&self) -> Result<()> {
        if let Some(checkout_dir) = &self.checkout_dir {
            std::env::set_current_dir(checkout_dir).with_context(|| {
                format!(
                    "Unable to change to checkout directory: {}",
                    checkout_dir.display()
                )
            })?;
        }
        Ok(())
    }

    /// For experimental commands, we should check whether Chef enabled the command for our shard. If not, fall back to python cli
    pub fn is_enabled(&self) -> bool {
        is_command_enabled(self.subcommand.name(), &self.etc_eden_dir)
    }

    pub fn run(self) -> Result<ExitCode> {
        self.set_working_directory()?;

        // For command line program, we don't really need concurrency. Schedule everything in
        // current thread should be sufficient.
        let runtime = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .context("unable to start async runtime")?;

        runtime.block_on(self.dispatch())
    }

    /// Execute subcommands. This function returns only a return code since all the error handling
    /// should be taken care of by each sub-command.
    async fn dispatch(self) -> Result<ExitCode> {
        event!(Level::TRACE, cmd = ?self, "Dispatching");

        EdenFsInstance::init(
            self.get_config_dir()?,
            get_etc_eden_dir(&self.etc_eden_dir),
            self.get_home_dir(),
        );
        // Use EdenFsInstance::global() to access the instance from now on
        self.subcommand.run().await
    }
}

pub fn is_command_enabled(name: &str, etc_eden_dir_override: &Option<PathBuf>) -> bool {
    is_command_enabled_in_json(name, etc_eden_dir_override)
        .unwrap_or_else(|| !EXPERIMENTAL_COMMANDS.contains(&name))
}

fn is_command_enabled_in_json(name: &str, etc_eden_dir_override: &Option<PathBuf>) -> Option<bool> {
    let rollout_json_path = get_etc_eden_dir(etc_eden_dir_override).join(ROLLOUT_JSON);
    if !rollout_json_path.exists() {
        return None;
    }

    // Open the file in read-only mode with buffer.
    let file = File::open(rollout_json_path).ok()?;
    let reader = BufReader::new(file);
    let json: serde_json::Value = serde_json::from_reader(reader).ok()?;
    let map = json.as_object()?;

    map.get(name).and_then(|v| v.as_bool())
}

fn get_etc_eden_dir(etc_eden_dir_override: &Option<PathBuf>) -> PathBuf {
    if let Some(etc_eden_dir) = etc_eden_dir_override {
        etc_eden_dir.clone()
    } else {
        DEFAULT_ETC_EDEN_DIR.into()
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! edenfsctl list

use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
use edenfs_client::checkout::get_mounts;
use edenfs_client::EdenFsInstance;

use crate::ExitCode;

#[derive(Parser, Debug)]
#[clap(about = "List available checkouts")]
pub struct ListCmd {
    #[clap(long, help = "Print the output in JSON format")]
    json: bool,
}

#[async_trait]
impl crate::Subcommand for ListCmd {
    async fn run(&self) -> Result<ExitCode> {
        let mounts = get_mounts(EdenFsInstance::global()).await?;
        if self.json {
            println!("{}", serde_json::to_string_pretty(&mounts)?);
        } else {
            for (_, mount) in mounts {
                println!("{}", mount);
            }
        }

        Ok(0)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! edenfsctl minitop

use std::collections::BTreeMap;
use std::io::stdout;
use std::io::Stdout;
use std::io::Write;
use std::time::Duration;
use std::time::Instant;

use anyhow::anyhow;
use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
use comfy_table::presets::UTF8_BORDERS_ONLY;
use comfy_table::ContentArrangement;
use comfy_table::Row;
use comfy_table::Table;
use crossterm::cursor;
use crossterm::event::Event;
use crossterm::event::EventStream;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
use crossterm::queue;
use crossterm::style;
use crossterm::terminal;
use edenfs_client::EdenFsClient;
use edenfs_client::EdenFsInstance;
use edenfs_utils::humantime::HumanTime;
use edenfs_utils::humantime::TimeUnit;
use edenfs_utils::path_from_bytes;
use futures::FutureExt;
use futures::StreamExt;
use sysinfo::Pid;
use sysinfo::System;
use sysinfo::SystemExt;
use thrift_types::edenfs::types::pid_t;
use thrift_types::edenfs::types::AccessCounts;
use thrift_types::edenfs::types::GetAccessCountsResult;

#[cfg(unix)]
use self::unix::trim_cmd_binary_path;
#[cfg(windows)]
use self::windows::trim_cmd_binary_path;
use crate::ExitCode;

#[derive(Parser, Debug)]
#[clap(about = "Simple monitoring of EdenFS accesses by process.")]
pub struct MinitopCmd {
    // TODO: For minitop, we may want to allow querying for < 1s, but this
    // requires modifying the thrift call and the eden service itself.
    // < 1s may be more useful for the realtime stats we see in minitop/top.
    #[clap(
        long,
        short,
        help = "Specify the rate (in seconds) at which eden top updates.",
        default_value = "1",
        parse(from_str = parse_refresh_rate),
    )]
    refresh_rate: Duration,

    #[clap(long, help = "Enable minitop interactive mode.")]
    interactive: bool,

    #[clap(long, help = "Show full executable path")]
    full_cmd: bool,
}

fn parse_refresh_rate(arg: &str) -> Duration {
    let seconds = arg
        .parse::<u64>()
        .expect("Please enter a valid whole positive number for refresh_rate.");

    Duration::new(seconds, 0)
}

const PENDING_COUNTER_REGEX: &str = r"store\.hg\.pending_import\..*";
const LIVE_COUNTER_REGEX: &str = r"store\.hg\.live_import\..*";
const IMPORT_OBJECT_TYPES: &[&str] = &["blob", "tree", "blobmeta"];
const STATS_NOT_AVAILABLE: i64 = 0;

const UNKNOWN_COMMAND: &str = "<unknown>";
const COLUMN_TITLES: &[&str] = &[
    "PID",
    "MOUNT",
    "READS",
    "WRITES",
    "TOTAL COUNT",
    "FETCHES",
    "MEMORY",
    "DISK",
    "IMPORTS",
    "TIME SPENT",
    "LAST ACCESS",
    "CMD",
];

trait GetAccessCountsResultExt {
    fn get_cmd_for_pid(&self, pid: pid_t, full_cmd: bool) -> Result<String>;
}

impl GetAccessCountsResultExt for GetAccessCountsResult {
    fn get_cmd_for_pid(&self, pid: pid_t, full_cmd: bool) -> Result<String> {
        match self.cmdsByPid.get(&pid) {
            Some(cmd) => {
                let cmd = String::from_utf8(cmd.to_vec())?;

                // remove trailing null which would cause the command to show up with an
                // extra empty string on the end
                let cmd = cmd.trim_end_matches(char::from(0));

                if full_cmd {
                    Ok(cmd.to_owned())
                } else {
                    // Show only the binary's filename, not its full path.
                    Ok(trim_cmd_binary_path(cmd)
                        .unwrap_or_else(|e| format!("{}: {}", UNKNOWN_COMMAND, e)))
                }
            }
            None => Ok(String::from(UNKNOWN_COMMAND)),
        }
    }
}

trait AccessCountsExt {
    fn add(&mut self, other: &AccessCounts);
}

impl AccessCountsExt for AccessCounts {
    fn add(&mut self, other: &AccessCounts) {
        self.fsChannelTotal += other.fsChannelTotal;
        self.fsChannelReads += other.fsChannelReads;
        self.fsChannelWrites += other.fsChannelWrites;
        self.fsChannelBackingStoreImports += other.fsChannelBackingStoreImports;
        self.fsChannelDurationNs += other.fsChannelDurationNs;
        self.fsChannelMemoryCacheImports += other.fsChannelMemoryCacheImports;
        self.fsChannelDiskCacheImports += other.fsChannelDiskCacheImports;
    }
}

#[derive(Clone)]
struct Process {
    pid: pid_t,
    mount_name: String,
    cmd: String,
    access_counts: AccessCounts,
    fetch_counts: i64,
    last_access_time: Instant,
}

impl Process {
    fn new(pid: pid_t, mount_name: String) -> Self {
        Self {
            pid,
            mount_name,
            cmd: "<unknown>".to_string(),
            access_counts: AccessCounts::default(),
            fetch_counts: 0,
            last_access_time: Instant::now(),
        }
    }

    fn set_cmd(&mut self, cmd: String) -> &mut Self {
        self.cmd = cmd;
        self
    }

    /// Update this `Process` access counts.
    ///
    /// Since the `getAccessCounts` API gives us an incremental `AccessCounts`, this is simply
    /// incrementing the current counts with the passed ones. The last access time is also
    /// incremented.
    fn increment_access_counts(&mut self, counts: &AccessCounts) {
        self.access_counts.add(counts);
        self.last_access_time = Instant::now();
    }

    /// Update this `Process` fetch counts.
    ///
    /// As opposed to the access counts, this is an absolute value since EdenFS started, thus this
    /// will only update the last access time if the fetch counts also changed.
    fn set_fetch_counts(&mut self, fetch_counts: i64) {
        if self.fetch_counts != fetch_counts {
            self.fetch_counts = fetch_counts;
            self.last_access_time = Instant::now();
        }
    }

    /// Test if this `Process` is still running.
    fn is_running(&self, system: &System) -> bool {
        #[cfg(not(windows))]
        let pid = Pid::from(self.pid);
        #[cfg(windows)]
        let pid = Pid::from(self.pid as usize);
        system.process(pid).is_some()
    }
}

/// Get the last component of the passed in byte slice representing a Path.
///
/// The path is eagerly converted from an `OsString` to a `String` for ease of use.
fn get_mount_name(mount_path: &[u8]) -> anyhow::Result<String> {
    let path = path_from_bytes(mount_path)?;
    let filename = path
        .file_name()
        .ok_or_else(|| anyhow!("filename is missing"))?;

    filename
        .to_os_string()
        .into_string()
        .map_err(|_| anyhow!("mount name is not UTF-8"))
}

type TrackedProcesses = BTreeMap<pid_t, Process>;

/// We aggregate all tracked processes in a separate step right before rendering
/// (as opposed to aggregating eagerly as we receive process logs in `update_process`)
/// because tracked processes could stop running which may change the top_pid.
fn aggregate_processes(processes: &TrackedProcesses, system: &System) -> Vec<Process> {
    // Technically, it's more correct to aggregate this by TGID
    // Because that's hard to get, we instead aggregate by mount & cmd
    // (mount, cmd) => Process
    let mut aggregated_processes = BTreeMap::<(&str, &str), Process>::new();

    for (_pid, process) in processes.iter() {
        match aggregated_processes.get_mut(&(&process.mount_name, &process.cmd)) {
            Some(agg_proc) => {
                // We aggregate access counts, but we don't change fetch counts
                // (this matches behavior in original python implementation)
                agg_proc.access_counts.add(&process.access_counts);

                // Figure out what the most relevant process id is
                if process.is_running(system)
                    || agg_proc.last_access_time < process.last_access_time
                {
                    agg_proc.pid = process.pid;
                    agg_proc.last_access_time = process.last_access_time;
                }
            }
            None => {
                aggregated_processes.insert((&process.mount_name, &process.cmd), process.clone());
            }
        }
    }

    let mut sorted_processes = aggregated_processes.into_values().collect::<Vec<Process>>();
    sorted_processes.sort_by(|a, b| b.last_access_time.cmp(&a.last_access_time));
    sorted_processes
}

struct ImportStat {
    count: i64,
    max_duration_us: i64,
}

async fn get_pending_import_counts(client: &EdenFsClient) -> Result<BTreeMap<String, ImportStat>> {
    let mut imports = BTreeMap::<String, ImportStat>::new();

    let counters = client.getRegexCounters(PENDING_COUNTER_REGEX).await?;
    for import_type in IMPORT_OBJECT_TYPES {
        let counter_prefix = format!("store.hg.pending_import.{}", import_type);
        let number_requests = counters
            .get(&format!("{}.count", counter_prefix))
            .unwrap_or(&STATS_NOT_AVAILABLE);
        let longest_outstanding_request_us = counters
            .get(&format!("{}.max_duration_us", counter_prefix))
            .unwrap_or(&STATS_NOT_AVAILABLE);

        imports.insert(
            import_type.to_string(),
            ImportStat {
                count: *number_requests,
                max_duration_us: *longest_outstanding_request_us,
            },
        );
    }

    Ok(imports)
}

async fn get_live_import_counts(client: &EdenFsClient) -> Result<BTreeMap<String, ImportStat>> {
    let mut imports = BTreeMap::<String, ImportStat>::new();
    let counters = client.getRegexCounters(LIVE_COUNTER_REGEX).await?;
    for import_type in IMPORT_OBJECT_TYPES {
        let single_prefix = format!("store.hg.live_import.{}", import_type);
        let batched_prefix = format!("store.hg.live_import.batched_{}", import_type);

        let count = counters
            .get(&format!("{}.count", single_prefix))
            .unwrap_or(&STATS_NOT_AVAILABLE)
            + counters
                .get(&format!("{}.count", batched_prefix))
                .unwrap_or(&STATS_NOT_AVAILABLE);
        let max_duration_us = std::cmp::max(
            counters
                .get(&format!("{}.max_duration_us", single_prefix))
                .unwrap_or(&STATS_NOT_AVAILABLE),
            counters
                .get(&format!("{}.max_duration_us", batched_prefix))
                .unwrap_or(&STATS_NOT_AVAILABLE),
        );

        imports.insert(
            import_type.to_string(),
            ImportStat {
                count,
                max_duration_us: *max_duration_us,
            },
        );
    }

    Ok(imports)
}

struct TerminalAttributes {
    line_wrap_disabled: bool,
    alt_screen_entered: bool,
    raw_mode_entered: bool,
    stdout: std::io::Stdout,
}

impl TerminalAttributes {
    fn new() -> TerminalAttributes {
        let stdout = stdout();
        Self {
            line_wrap_disabled: false,
            alt_screen_entered: false,
            raw_mode_entered: false,
            stdout,
        }
    }

    fn disable_line_wrap(mut self) -> Result<TerminalAttributes> {
        queue!(self.stdout, terminal::DisableLineWrap)?;
        self.line_wrap_disabled = true;
        Ok(self)
    }

    fn enter_alt_screen(mut self) -> Result<TerminalAttributes> {
        queue!(self.stdout, terminal::EnterAlternateScreen)?;
        self.alt_screen_entered = true;
        Ok(self)
    }

    fn enter_raw_mode(mut self) -> Result<TerminalAttributes> {
        terminal::enable_raw_mode()?;
        self.raw_mode_entered = true;
        Ok(self)
    }
}

impl Drop for TerminalAttributes {
    fn drop(&mut self) {
        if self.line_wrap_disabled {
            let _ = queue!(self.stdout, terminal::EnableLineWrap);
        }

        if self.alt_screen_entered {
            let _ = queue!(self.stdout, terminal::LeaveAlternateScreen);
        }

        if self.raw_mode_entered {
            let _ = terminal::disable_raw_mode();
        }

        let _ = self.stdout.flush();
    }
}

struct Cursor {
    row: u16,
    terminal_rows: u16,
}

impl Cursor {
    fn new() -> Result<Self> {
        let (_, row) = cursor::position()?;
        let (_, terminal_rows) = terminal::size()?;

        Ok(Self { row, terminal_rows })
    }

    fn new_line(&mut self, stdout: &mut Stdout) -> Result<(), std::io::Error> {
        if self.row == self.terminal_rows {
            queue!(stdout, terminal::ScrollUp(1), cursor::MoveToColumn(1))
        } else {
            self.row += 1;
            queue!(stdout, cursor::MoveToNextLine(1))
        }
    }

    fn refresh_terminal_size(&mut self) -> Result<()> {
        let (_, terminal_rows) = terminal::size()?;
        self.terminal_rows = terminal_rows;

        // In the case where the terminal was resized and the cursor was on the last line, we want
        // to make sure we stay on the last line.
        if self.row > self.terminal_rows {
            self.row = self.terminal_rows;
        }

        Ok(())
    }
}

#[async_trait]
impl crate::Subcommand for MinitopCmd {
    async fn run(&self) -> Result<ExitCode> {
        let client = EdenFsInstance::global().connect(None).await?;
        let mut tracked_processes = TrackedProcesses::new();

        let mut system = System::new();

        // Setup rendering
        let mut attributes = TerminalAttributes::new()
            .disable_line_wrap()?
            .enter_raw_mode()?;
        if self.interactive {
            attributes = attributes.enter_alt_screen()?;
        }
        let _ = attributes; // silence warning

        let mut stdout = stdout();
        let mut cursor = Cursor::new()?;
        let mut events = EventStream::new();

        loop {
            if self.interactive {
                queue!(stdout, terminal::Clear(terminal::ClearType::All))?;
            }
            client.flushStatsNow();
            system.refresh_processes();
            cursor.refresh_terminal_size()?;

            // Update pending imports summary stats
            let (pending_imports, live_imports) = tokio::try_join!(
                get_pending_import_counts(&client),
                get_live_import_counts(&client)
            )?;

            // Update currently tracked processes (and add new ones if they haven't been tracked yet)
            let counts = client
                .getAccessCounts(self.refresh_rate.as_secs().try_into()?)
                .await?;

            for (mount, accesses) in &counts.accessesByMount {
                let mount_name = get_mount_name(mount)?;

                for (pid, access_counts) in &accesses.accessCountsByPid {
                    tracked_processes
                        .entry(*pid)
                        .or_insert_with(|| Process::new(*pid, mount_name.clone()))
                        .set_cmd(counts.get_cmd_for_pid(*pid, self.full_cmd)?)
                        .increment_access_counts(access_counts);
                }

                for (pid, fetch_counts) in &accesses.fetchCountsByPid {
                    tracked_processes
                        .entry(*pid)
                        .or_insert_with(|| Process::new(*pid, mount_name.clone()))
                        .set_cmd(counts.get_cmd_for_pid(*pid, self.full_cmd)?)
                        .set_fetch_counts(*fetch_counts);
                }
            }

            // Render pending trees/blobs
            for import_type in IMPORT_OBJECT_TYPES {
                let pending_counts = pending_imports
                    .get(&import_type.to_string())
                    .ok_or_else(|| anyhow!("Did not fetch pending {} info", import_type))?;
                let live_counts = live_imports
                    .get(&import_type.to_string())
                    .ok_or_else(|| anyhow!("Did not fetch live {} info", import_type))?;
                let pending_string = format!(
                    "total pending {}: {} ({:.3}s)",
                    import_type,
                    pending_counts.count,
                    pending_counts.max_duration_us as f64 / 1000000.0
                );
                let live_string = format!(
                    "total live {}: {} ({:.3}s)",
                    import_type,
                    live_counts.count,
                    live_counts.max_duration_us as f64 / 1000000.0
                );
                queue!(
                    stdout,
                    style::Print(format!("{:<40} {}", pending_string, live_string)),
                )?;
                cursor.new_line(&mut stdout)?;
            }

            // Render aggregated processes
            let mut table = Table::new();
            table.set_header(COLUMN_TITLES);
            table.load_preset(UTF8_BORDERS_ONLY);
            table.set_content_arrangement(ContentArrangement::Dynamic);
            for aggregated_process in aggregate_processes(&tracked_processes, &system) {
                let mut row = Row::from(vec![
                    aggregated_process.pid.to_string(),
                    aggregated_process.mount_name.clone(),
                    aggregated_process.access_counts.fsChannelReads.to_string(),
                    aggregated_process.access_counts.fsChannelWrites.to_string(),
                    aggregated_process.access_counts.fsChannelTotal.to_string(),
                    aggregated_process.fetch_counts.to_string(),
                    aggregated_process
                        .access_counts
                        .fsChannelMemoryCacheImports
                        .to_string(),
                    aggregated_process
                        .access_counts
                        .fsChannelDiskCacheImports
                        .to_string(),
                    aggregated_process
                        .access_counts
                        .fsChannelBackingStoreImports
                        .to_string(),
                    HumanTime::from(Duration::from_nanos(
                        aggregated_process
                            .access_counts
                            .fsChannelDurationNs
                            .try_into()?,
                    ))
                    .simple_human_time(TimeUnit::Nanoseconds),
                    HumanTime::from(aggregated_process.last_access_time.elapsed())
                        .simple_human_time(TimeUnit::Seconds),
                    aggregated_process.cmd,
                ]);
                row.max_height(1);
                table.add_row(row);
            }

            for line in table.lines() {
                queue!(stdout, style::Print(line),)?;
                cursor.new_line(&mut stdout)?;
            }
            cursor.new_line(&mut stdout)?;
            cursor.new_line(&mut stdout)?;
            stdout.flush()?;

            loop {
                let delay = tokio::time::sleep(self.refresh_rate);
                let event = events.next().fuse();

                tokio::select! {
                    _ = delay => { break }
                    maybe_event = event => {
                        match maybe_event {
                            Some(event) => {
                                let event = event?;

                                let q = Event::Key(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE));
                                let ctrlc = Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));
                                if event == q || event == ctrlc {
                                    return Ok(0);
                                }
                            },
                            None => break,
                        }
                    }
                };
            }
        }
    }
}

#[cfg(unix)]
mod unix {
    use std::path::Path;

    use anyhow::anyhow;
    use anyhow::Result;
    use shlex::quote;

    pub fn trim_cmd_binary_path(cmd: &str) -> Result<String> {
        let mut parts: Vec<&str> = cmd.split(char::from(0)).collect();
        let path = Path::new(parts[0]);
        if path.is_absolute() {
            parts[0] = path
                .file_name()
                .ok_or_else(|| anyhow!("cmd filename is missing"))?
                .to_str()
                .ok_or_else(|| anyhow!("cmd is not UTF-8"))?;
        }

        Ok(parts
            .into_iter()
            .enumerate()
            .map(|(i, part)| {
                if i == 0 {
                    // the first item is the cmd
                    String::from(part)
                } else {
                    quote(part).into_owned()
                }
            })
            .collect::<Vec<String>>()
            .join(" "))
    }
}

#[cfg(windows)]
mod windows {
    use std::ffi::OsStr;
    use std::path::Path;

    use anyhow::anyhow;
    use anyhow::Result;
    use edenfs_utils::winargv::argv_to_command_line;
    use edenfs_utils::winargv::command_line_to_argv;

    pub fn trim_cmd_binary_path(cmd: &str) -> Result<String> {
        let argv = command_line_to_argv(OsStr::new(cmd))?;

        let truncated_argv = argv
            .iter()
            .enumerate()
            .map(|part| match part {
                (0, binary) => binary_filename_only(&binary),
                (_, arg) => Ok(arg.as_os_str()),
            })
            .collect::<Result<Vec<_>>>()?;

        Ok(argv_to_command_line(truncated_argv.as_slice())?
            .to_string_lossy()
            .into_owned())
    }

    fn binary_filename_only(binary: &OsStr) -> Result<&OsStr> {
        Ok(Path::new(binary)
            .file_name()
            .ok_or(anyhow!("cmd filename is missing"))?)
    }

    #[cfg(test)]
    mod tests {
        use anyhow::Result;

        use super::trim_cmd_binary_path;

        #[test]
        fn test_trim_cmd_binary_path() -> Result<()> {
            assert_eq!(trim_cmd_binary_path("rustc.exe")?, "rustc.exe");
            assert_eq!(trim_cmd_binary_path("\"rustc.exe\"")?, "rustc.exe");
            assert_eq!(
                trim_cmd_binary_path("\"C:\\Program Files\\foo\\bar.exe\" baz.txt")?,
                "bar.exe baz.txt"
            );
            assert_eq!(
                trim_cmd_binary_path("\"C:\\Program Files\\foo\\bar baz.exe\" baz.txt")?,
                "\"bar baz.exe\" baz.txt"
            );

            Ok(())
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! edenfsctl pid

use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
use edenfs_client::EdenFsInstance;

use crate::ExitCode;

#[derive(Parser, Debug)]
#[clap(about = "Print the daemon's process ID if running")]
pub struct PidCmd {}

#[async_trait]
impl crate::Subcommand for PidCmd {
    async fn run(&self) -> Result<ExitCode> {
        let health = EdenFsInstance::global().get_health(None).await;

        Ok(match health {
            Ok(health) => {
                println!("{}", health.pid);
                0
            }
            Err(cause) => {
                eprintln!("edenfs not healthy: {}", cause);
                1
            }
        })
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! edenfsctl prefetch-profile

use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::str;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
use edenfs_client::checkout::find_checkout;
use edenfs_client::checkout::CheckoutConfig;
use edenfs_client::EdenFsInstance;
#[cfg(fbcode_build)]
use edenfs_telemetry::prefetch_profile::PrefetchProfileSample;
#[cfg(fbcode_build)]
use edenfs_telemetry::send;
#[cfg(fbcode_build)]
use fbinit::expect_init;
use hg_util::path::expand_path;

use crate::util::expand_path_or_cwd;
use crate::ExitCode;
use crate::Subcommand;

#[derive(Parser, Debug)]
pub struct ActivationOptions {
    #[clap(
        short,
        long,
        help = "Print extra info including warnings and the names of the matching files to fetch."
    )]
    verbose: bool,
    #[clap(
        long,
        parse(try_from_str = expand_path_or_cwd),
        default_value = "",
        help = "The checkout for which you want to activate this profile"
    )]
    checkout: PathBuf,
    #[clap(
        short,
        long,
        help = "DEPRECATED: Do not prefetch profiles only find all the files that match \
    them. This will still list the names of matching files when the \
    verbose flag is also used, and will activate the profile when running \
    `activate`."
    )]
    skip_prefetch: bool,
    #[clap(
        short,
        long,
        help = "Do not prefetch files, only fetch corresponding directories. \
    This will still activate the profile when running `activate`."
    )]
    directories_only: bool,
    #[clap(
        short,
        long,
        help = "Run the prefetch in the main thread rather than in the \
    background. Normally this command will return once the prefetch \
    has been kicked off, but when this flag is used it to block until \
    all of the files are prefetched."
    )]
    foreground: bool,
}

#[derive(Parser, Debug)]
pub struct FetchOptions {
    #[clap(flatten)]
    options: ActivationOptions,
    #[clap(
        long,
        multiple_values = true,
        help = "Commit hashes of the commits for which globs should be \
        evaluated. Note that the current commit in the checkout is used \
        if this is not specified. Note that the prefetch profiles are \
        always read from the current commit, not the commits specified \
        here."
    )]
    commits: Vec<String>,
    #[clap(
        long,
        help = "Predict the commits a user is likely to checkout. Evaluate \
        the active prefetch profiles against those commits and fetch the \
        resulting files in those commits. Note that the prefetch profiles \
        are always read from the current commit, not the commits \
        predicted here. This is intended to be used post pull."
    )]
    predict_commits: bool,
}

#[derive(Parser, Debug)]
#[clap(name = "prefetch-profile")]
#[clap(about = "Create, manage, and use Prefetch Profiles. This command is \
    primarily for use in automation.")]
pub enum PrefetchCmd {
    #[clap(about = "Stop recording fetched file paths and save previously \
        collected fetched file paths in the output prefetch profile")]
    Finish {
        #[clap(
            long,
            parse(from_str = expand_path),
            default_value = "prefetch_profile.txt",
            help = "The output path to store the prefetch profile"
        )]
        output_path: PathBuf,
    },
    #[clap(about = "Start recording fetched file paths.")]
    Record,
    #[clap(about = "List all of the currenly activated prefetch profiles for a checkout.")]
    List {
        #[clap(
            long,
            parse(try_from_str = expand_path_or_cwd),
            default_value = "",
            help = "The checkout for which you want to see all the profiles"
        )]
        checkout: PathBuf,
    },
    #[clap(about = "Tell EdenFS to smart prefetch the files specified by the \
        prefetch profile. (EdenFS will prefetch the files in this profile \
        immediately, when checking out a new commit, and for some pulls).")]
    Activate {
        #[clap(flatten)]
        options: ActivationOptions,
        #[clap(help = "Profile to activate.")]
        profile_name: String,
        #[clap(
            long,
            help = "Fetch the profile even if the profile has already been activated"
        )]
        force_fetch: bool,
    },
    #[clap(hide = true)]
    ActivatePredictive {
        #[clap(flatten)]
        options: ActivationOptions,
        #[clap(
            default_value = "0",
            help = "Optionally set the number of top accessed directories to \
                prefetch, overriding the default."
        )]
        num_dirs: u32,
    },
    #[clap(
        about = "Tell EdenFS to STOP smart prefetching the files specified by the prefetch profile."
    )]
    Deactivate {
        #[clap(flatten)]
        options: ActivationOptions,
        #[clap(help = "Profile to deactivate.")]
        profile_name: String,
    },
    #[clap(hide = true)]
    DeactivatePredictive {
        #[clap(flatten)]
        options: ActivationOptions,
    },
    #[clap(
        about = "Prefetch all the active prefetch profiles or specified prefetch profiles. \
        This is intended for use after checkout and pull."
    )]
    Fetch {
        #[clap(flatten)]
        options: FetchOptions,
        #[clap(
            required = false,
            help = "Fetch only these named profiles instead of the active set of profiles."
        )]
        profile_names: Vec<String>,
    },
    #[clap(hide = true)]
    FetchPredictive {
        #[clap(flatten)]
        options: FetchOptions,
        #[clap(
            default_value = "0",
            help = "Optionally set the number of top accessed directories to \
                prefetch, overriding the default."
        )]
        num_dirs: u32,
        #[clap(
            long,
            help = "Only run the fetch if activate-predictive has been run. \
                Uses num_dirs set by activate-predictive, or the default."
        )]
        if_active: bool,
    },
}

impl PrefetchCmd {
    async fn finish(&self, output_path: &PathBuf) -> Result<ExitCode> {
        let client = EdenFsInstance::global()
            .connect(None)
            .await
            .with_context(|| anyhow!("Could not connect to EdenFS server"))?;
        let files = client
            .stopRecordingBackingStoreFetch()
            .await
            .with_context(|| anyhow!("stopRecordingBackingStoreFetch thrift call failed"))?;
        let fetched_files = files
            .fetchedFilePaths
            .get("HgQueuedBackingStore")
            .ok_or_else(|| anyhow!("no Path vector found"))?;
        let mut out_file = File::create(output_path).context("unable to create output file")?;
        for path_bytes in fetched_files {
            out_file
                .write_all(path_bytes)
                .context("failed to write to output file")?;
            out_file
                .write_all(b"\n")
                .context("failed to write to output file")?;
        }
        Ok(0)
    }

    async fn record(&self) -> Result<ExitCode> {
        let client = EdenFsInstance::global().connect(None).await?;
        client
            .startRecordingBackingStoreFetch()
            .await
            .with_context(|| anyhow!("startRecordingBackingStoreFetch thrift call failed"))?;
        Ok(0)
    }

    async fn list(&self, checkout: &Path) -> Result<ExitCode> {
        let instance = EdenFsInstance::global();
        let client_name = instance.client_name(checkout).with_context(|| {
            anyhow!(
                "Failed to get client name for checkout {}",
                checkout.display()
            )
        })?;
        let config_dir = instance.config_directory(&client_name);
        let checkout_config = CheckoutConfig::parse_config(config_dir);
        match checkout_config {
            Ok(checkout_config) => {
                println!("NAME");
                checkout_config.print_prefetch_profiles();
                Ok(0)
            }
            Err(_) => Err(anyhow!(
                "Could not print prefetch profile data for {}",
                client_name
            )),
        }
    }

    async fn activate(
        &self,
        options: &ActivationOptions,
        profile_name: &str,
        force_fetch: &bool,
    ) -> Result<ExitCode> {
        let instance = EdenFsInstance::global();
        let client_name = instance.client_name(&options.checkout).with_context(|| {
            anyhow!(
                "Failed to get client name for checkout {}",
                &options.checkout.display()
            )
        })?;

        let directories_only = options.directories_only || options.skip_prefetch;

        #[cfg(fbcode_build)]
        let mut sample = {
            let sample = PrefetchProfileSample::activate_event(
                expect_init(),
                profile_name,
                client_name.as_str(),
                directories_only,
            );
            sample
        };

        let config_dir = instance.config_directory(&client_name);
        let checkout_config = CheckoutConfig::parse_config(config_dir.clone());
        let result = checkout_config
            .and_then(|mut config| config.activate_profile(profile_name, config_dir, force_fetch));
        match result {
            Ok(res) => {
                #[cfg(fbcode_build)]
                send(sample.builder);
                if !res {
                    return Ok(0);
                }
            }
            Err(e) => {
                #[cfg(fbcode_build)]
                {
                    sample.fail(&e.to_string());
                    send(sample.builder);
                }
                return Err(anyhow::Error::new(e));
            }
        };

        let checkout = find_checkout(instance, &options.checkout).with_context(|| {
            anyhow!(
                "Failed to find checkout with path {}",
                &options.checkout.display()
            )
        })?;
        let result_globs = checkout
            .prefetch_profiles(
                instance,
                &vec![profile_name.to_string()],
                !options.foreground,
                directories_only,
                !options.verbose,
                None,
                false,
                false,
                0,
            )
            .await?;
        // there will only every be one commit used to query globFiles here,
        // so no need to list which commit a file is fetched for, it will
        // be the current commit.
        if options.verbose {
            for result in result_globs {
                for name in result.matchingFiles {
                    println!("{}", String::from_utf8_lossy(&name));
                }
            }
        }

        Ok(0)
    }

    async fn activate_predictive(
        &self,
        options: &ActivationOptions,
        num_dirs: u32,
    ) -> Result<ExitCode> {
        let instance = EdenFsInstance::global();
        let client_name = instance.client_name(&options.checkout).with_context(|| {
            anyhow!(
                "Failed to get client name for checkout {}",
                &options.checkout.display()
            )
        })?;

        let directories_only = options.directories_only || options.skip_prefetch;

        #[cfg(fbcode_build)]
        let mut sample = {
            let sample = PrefetchProfileSample::activate_predictive_event(
                expect_init(),
                client_name.as_str(),
                directories_only,
                num_dirs,
            );
            sample
        };

        let config_dir = instance.config_directory(&client_name);
        let checkout_config = CheckoutConfig::parse_config(config_dir.clone());

        let result = checkout_config
            .and_then(|mut config| config.activate_predictive_profile(config_dir, num_dirs));
        if let Err(e) = result {
            #[cfg(fbcode_build)]
            {
                sample.fail(&e.to_string());
                send(sample.builder);
            }
            return Err(anyhow::Error::new(e));
        }
        #[cfg(fbcode_build)]
        send(sample.builder);

        let checkout = find_checkout(instance, &options.checkout).with_context(|| {
            anyhow!(
                "Failed to find checkout with path {}",
                &options.checkout.display()
            )
        })?;
        let result_globs = checkout
            .prefetch_profiles(
                instance,
                &vec![],
                !options.foreground,
                directories_only,
                !options.verbose,
                None,
                false,
                true,
                num_dirs,
            )
            .await?;
        // there will only every be one commit used to query globFiles here,
        // so no need to list which commit a file is fetched for, it will
        // be the current commit.
        if options.verbose {
            for result in result_globs {
                for name in result.matchingFiles {
                    println!("{}", String::from_utf8_lossy(&name));
                }
            }
        }

        Ok(0)
    }

    async fn deactivate(
        &self,
        options: &ActivationOptions,
        profile_name: &str,
    ) -> Result<ExitCode> {
        let instance = EdenFsInstance::global();
        let client_name = instance.client_name(&options.checkout).with_context(|| {
            anyhow!(
                "Failed to get client name for checkout {}",
                &options.checkout.display()
            )
        })?;

        #[cfg(fbcode_build)]
        let mut sample = {
            let sample = PrefetchProfileSample::deactivate_event(
                expect_init(),
                profile_name,
                client_name.as_str(),
            );
            sample
        };

        let config_dir = instance.config_directory(&client_name);
        let checkout_config = CheckoutConfig::parse_config(config_dir.clone());
        let result = checkout_config
            .and_then(|mut config| config.deactivate_profile(profile_name, config_dir));
        if let Err(e) = result {
            #[cfg(fbcode_build)]
            {
                sample.fail(&e.to_string());
                send(sample.builder);
            }
            return Err(anyhow::Error::new(e));
        }
        #[cfg(fbcode_build)]
        send(sample.builder);
        Ok(0)
    }

    async fn deactivate_predictive(&self, options: &ActivationOptions) -> Result<ExitCode> {
        let instance = EdenFsInstance::global();
        let client_name = instance.client_name(&options.checkout).with_context(|| {
            anyhow!(
                "Failed to get client name for checkout {}",
                &options.checkout.display()
            )
        })?;

        #[cfg(fbcode_build)]
        let mut sample = {
            let sample = PrefetchProfileSample::deactivate_predictive_event(
                expect_init(),
                client_name.as_str(),
            );
            sample
        };

        let config_dir = instance.config_directory(&client_name);
        let checkout_config = CheckoutConfig::parse_config(config_dir.clone());
        let result =
            checkout_config.and_then(|mut config| config.deactivate_predictive_profile(config_dir));
        if let Err(e) = result {
            #[cfg(fbcode_build)]
            {
                sample.fail(&e.to_string());
                send(sample.builder);
            }
            return Err(anyhow::Error::new(e));
        }
        #[cfg(fbcode_build)]
        send(sample.builder);
        Ok(0)
    }

    async fn fetch(&self, profile_names: &Vec<String>, options: &FetchOptions) -> Result<ExitCode> {
        let instance = EdenFsInstance::global();
        let checkout_path = &options.options.checkout;
        let client_name = instance.client_name(checkout_path).with_context(|| {
            anyhow!(
                "Failed to get client name for checkout {}",
                checkout_path.display()
            )
        })?;
        let config_dir = instance.config_directory(&client_name);
        let checkout_config =
            CheckoutConfig::parse_config(config_dir.clone()).with_context(|| {
                anyhow!(
                    "Failed to parse config located in config_dir: {}",
                    &config_dir.display()
                )
            })?;
        let profiles_to_prefetch = if profile_names.is_empty() {
            match checkout_config.get_prefetch_profiles() {
                Ok(res) => res,
                Err(_) => {
                    if options.options.verbose {
                        println!("No profiles to fetch")
                    }
                    return Ok(0);
                }
            }
        } else {
            profile_names
        };

        if profiles_to_prefetch.is_empty() {
            if options.options.verbose {
                println!("No profiles to fetch")
            }
            return Ok(0);
        }

        let directories_only = options.options.directories_only || options.options.skip_prefetch;

        let checkout = find_checkout(instance, checkout_path).with_context(|| {
            anyhow!(
                "Failed to find checkout with path {}",
                checkout_path.display()
            )
        })?;
        let result_globs = checkout
            .prefetch_profiles(
                instance,
                profiles_to_prefetch,
                !options.options.foreground,
                directories_only,
                !options.options.verbose,
                Some(&options.commits),
                options.predict_commits,
                false,
                0,
            )
            .await?;
        // there will only every be one commit used to query globFiles here,
        // so no need to list which commit a file is fetched for, it will
        // be the current commit.
        if options.options.verbose {
            for result in result_globs {
                for name in result.matchingFiles {
                    println!("{}", String::from_utf8_lossy(&name));
                }
            }
        }

        Ok(0)
    }

    async fn fetch_predictive(
        &self,
        options: &FetchOptions,
        num_dirs: u32,
        if_active: bool,
    ) -> Result<ExitCode> {
        let instance = EdenFsInstance::global();
        let checkout_path = &options.options.checkout;
        let client_name = instance.client_name(checkout_path).with_context(|| {
            anyhow!(
                "Failed to get client name for checkout {}",
                checkout_path.display()
            )
        })?;
        let config_dir = instance.config_directory(&client_name);
        let checkout_config =
            CheckoutConfig::parse_config(config_dir.clone()).with_context(|| {
                anyhow!(
                    "Failed to parse config located in config_dir: {}",
                    &config_dir.display()
                )
            })?;

        if if_active && !checkout_config.predictive_prefetch_is_active() {
            if options.options.verbose {
                println!(
                    "Predictive prefetch profiles have not been activated and \
                    --if-active was specified. Skipping fetch."
                );
            }
            return Ok(0);
        }

        // If num_dirs is given, use the specified num_dirs. If num_dirs is
        // not given (args.num_dirs == 0), predictive fetch with default num
        // dirs unless there is an active num dirs saved in the checkout config
        let predictive_num_dirs = if num_dirs == 0 && checkout_config.get_predictive_num_dirs() != 0
        {
            checkout_config.get_predictive_num_dirs()
        } else {
            0
        };

        let directories_only = options.options.directories_only || options.options.skip_prefetch;

        let checkout = find_checkout(instance, checkout_path).with_context(|| {
            anyhow!(
                "Failed to find checkout with path {}",
                checkout_path.display()
            )
        })?;
        let result_globs = checkout
            .prefetch_profiles(
                instance,
                &vec![],
                !options.options.foreground,
                directories_only,
                !options.options.verbose,
                Some(&options.commits),
                options.predict_commits,
                true,
                predictive_num_dirs,
            )
            .await?;

        // there will only every be one commit used to query globFiles here,
        // so no need to list which commit a file is fetched for, it will
        // be the current commit.
        if options.options.verbose {
            for result in result_globs {
                for name in result.matchingFiles {
                    println!("{}", String::from_utf8_lossy(&name));
                }
            }
        }

        Ok(0)
    }
}

#[async_trait]
impl Subcommand for PrefetchCmd {
    async fn run(&self) -> Result<ExitCode> {
        match self {
            Self::Finish { output_path } => self.finish(output_path).await,
            Self::Record {} => self.record().await,
            Self::List { checkout } => self.list(checkout).await,
            Self::Activate {
                options,
                profile_name,
                force_fetch,
            } => self.activate(options, profile_name, force_fetch).await,
            Self::ActivatePredictive { options, num_dirs } => {
                self.activate_predictive(options, *num_dirs).await
            }
            Self::Deactivate {
                options,
                profile_name,
            } => self.deactivate(options, profile_name).await,
            Self::DeactivatePredictive { options } => self.deactivate_predictive(options).await,
            Self::Fetch {
                profile_names,
                options,
            } => self.fetch(profile_names, options).await,
            Self::FetchPredictive {
                options,
                num_dirs,
                if_active,
            } => self.fetch_predictive(options, *num_dirs, *if_active).await,
        }
    }
}