/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

#![cfg_attr(feature = "gazebo_lint", allow(deprecated))] // :(

use std::fs::File;
use std::path::PathBuf;
use std::process;
use std::sync::Arc;
use std::thread;
use std::time::Duration;

use allocative::Allocative;
use anyhow::Context as _;
use async_trait::async_trait;
use buck2_audit_server::server::server_audit_command;
use buck2_bxl::command::bxl_command;
use buck2_bxl::profile_command::bxl_profile_command;
use buck2_cli_proto::ConfiguredTargetsRequest;
use buck2_cli_proto::ConfiguredTargetsResponse;
use buck2_cli_proto::DaemonProcessInfo;
use buck2_client_ctx::argv::Argv;
use buck2_client_ctx::argv::SanitizedArgv;
use buck2_client_ctx::daemon_constraints::gen_daemon_constraints;
use buck2_client_ctx::version::BuckVersion;
use buck2_common::buckd_connection::ConnectionType;
use buck2_common::daemon_dir::DaemonDir;
use buck2_common::invocation_paths::InvocationPaths;
use buck2_common::legacy_configs::cells::DaemonStartupConfig;
use buck2_common::memory;
use buck2_core::env_helper::EnvHelper;
use buck2_core::fs::fs_util;
use buck2_core::logging::LogConfigurationReloadHandle;
use buck2_server::builtin_docs::docs::docs_command;
use buck2_server::daemon::daemon_tcp::create_listener;
use buck2_server::daemon::server::BuckdServer;
use buck2_server::daemon::server::BuckdServerDelegate;
use buck2_server::daemon::server::BuckdServerDependencies;
use buck2_server::daemon::server::BuckdServerInitPreferences;
use buck2_server::profile::profile_command;
use buck2_server_commands::commands::build::build_command;
use buck2_server_commands::commands::configured_targets::configured_targets_command;
use buck2_server_commands::commands::install::install_command;
use buck2_server_commands::commands::query::aquery::aquery_command;
use buck2_server_commands::commands::query::cquery::cquery_command;
use buck2_server_commands::commands::query::uquery::uquery_command;
use buck2_server_commands::commands::targets::targets_command;
use buck2_server_commands::commands::targets_show_outputs::targets_show_outputs_command;
use buck2_server_ctx::ctx::ServerCommandContextTrait;
use buck2_server_ctx::partial_result_dispatcher::NoPartialResult;
use buck2_server_ctx::partial_result_dispatcher::PartialResultDispatcher;
use buck2_starlark::server::server_starlark_command;
use buck2_test::command::test_command;
use futures::channel::mpsc;
use futures::channel::mpsc::UnboundedSender;
use futures::pin_mut;
use futures::select;
use futures::FutureExt;
use futures::StreamExt;
use rand::Rng;
use thiserror::Error;
use tokio::runtime::Builder;

use crate::commands::daemon_lower_priority::daemon_lower_priority;
use crate::commands::schedule_termination::maybe_schedule_termination;
use crate::DaemonBeforeSubcommandOptions;

#[derive(Debug, Error)]
enum DaemonError {
    #[error("The buckd pid file at `{0}` had a mismatched pid, expected `{1}`, got `{2}`")]
    PidFileMismatch(PathBuf, u32, u32),
}

/// Start or run buck daemon.
///
/// This is an internal command, not intended to be used directly.
/// Buck client invokes it to spawn a server process.
#[derive(Clone, Debug, clap::Parser)]
pub(crate) struct DaemonCommand {
    /// Sets the interval for how often the daemon performs consistency checks.
    /// These are used to ensure that the daemon is still the one referenced
    /// by files in the daemon dir.
    #[clap(long, default_value("60"))]
    checker_interval_seconds: u64,
    /// Run buck daemon but do not daemonize the process.
    #[clap(long)]
    dont_daemonize: bool,
    /// This flag is set to prevent infinite recursion when the process is restarted
    /// with lower priority.
    #[clap(long)]
    skip_macos_qos: bool,
    /// Early configs that the daemon needs at startup. Those are read by the client then passed to
    /// the daemon. The client will restart the daemon if they mismatch.
    #[clap(parse(try_from_str = DaemonStartupConfig::deserialize))]
    daemon_startup_config: DaemonStartupConfig,
}

impl DaemonCommand {
    /// Command instance for `--no-buckd`.
    pub(crate) fn new_in_process(daemon_startup_config: DaemonStartupConfig) -> DaemonCommand {
        DaemonCommand {
            checker_interval_seconds: 60,
            dont_daemonize: true,
            skip_macos_qos: true,
            daemon_startup_config,
        }
    }

    pub(crate) fn sanitize_argv(&self, argv: Argv) -> SanitizedArgv {
        argv.no_need_to_sanitize()
    }
}

struct BuckdServerDependenciesImpl;

#[async_trait]
impl BuckdServerDependencies for BuckdServerDependenciesImpl {
    async fn test(
        &self,
        ctx: &dyn ServerCommandContextTrait,
        partial_result_dispatcher: PartialResultDispatcher<NoPartialResult>,
        req: buck2_cli_proto::TestRequest,
    ) -> anyhow::Result<buck2_cli_proto::TestResponse> {
        test_command(ctx, partial_result_dispatcher, req).await
    }
    async fn build(
        &self,
        ctx: &dyn ServerCommandContextTrait,
        partial_result_dispatcher: PartialResultDispatcher<NoPartialResult>,
        req: buck2_cli_proto::BuildRequest,
    ) -> anyhow::Result<buck2_cli_proto::BuildResponse> {
        build_command(ctx, partial_result_dispatcher, req).await
    }
    async fn install(
        &self,
        ctx: &dyn ServerCommandContextTrait,
        partial_result_dispatcher: PartialResultDispatcher<NoPartialResult>,
        req: buck2_cli_proto::InstallRequest,
    ) -> anyhow::Result<buck2_cli_proto::InstallResponse> {
        install_command(ctx, partial_result_dispatcher, req).await
    }
    async fn bxl(
        &self,
        ctx: &dyn ServerCommandContextTrait,
        partial_result_dispatcher: PartialResultDispatcher<buck2_cli_proto::StdoutBytes>,
        req: buck2_cli_proto::BxlRequest,
    ) -> anyhow::Result<buck2_cli_proto::BxlResponse> {
        bxl_command(ctx, partial_result_dispatcher, req).await
    }
    async fn audit(
        &self,
        ctx: &dyn ServerCommandContextTrait,
        partial_result_dispatcher: PartialResultDispatcher<buck2_cli_proto::StdoutBytes>,
        req: buck2_cli_proto::GenericRequest,
    ) -> anyhow::Result<buck2_cli_proto::GenericResponse> {
        server_audit_command(ctx, partial_result_dispatcher, req).await
    }
    async fn starlark(
        &self,
        ctx: &dyn ServerCommandContextTrait,
        partial_result_dispatcher: PartialResultDispatcher<buck2_cli_proto::StdoutBytes>,
        req: buck2_cli_proto::GenericRequest,
    ) -> anyhow::Result<buck2_cli_proto::GenericResponse> {
        server_starlark_command(ctx, partial_result_dispatcher, req).await
    }
    async fn profile(
        &self,
        ctx: &dyn ServerCommandContextTrait,
        partial_result_dispatcher: PartialResultDispatcher<NoPartialResult>,
        req: buck2_cli_proto::ProfileRequest,
    ) -> anyhow::Result<buck2_cli_proto::ProfileResponse> {
        match req.profile_opts.as_ref().expect("Missing profile opts") {
            buck2_cli_proto::profile_request::ProfileOpts::TargetProfile(_) => {
                profile_command(ctx, partial_result_dispatcher, req).await
            }
            buck2_cli_proto::profile_request::ProfileOpts::BxlProfile(_) => {
                bxl_profile_command(ctx, partial_result_dispatcher, req).await
            }
        }
    }
    async fn uquery(
        &self,
        ctx: &dyn ServerCommandContextTrait,
        partial_result_dispatcher: PartialResultDispatcher<buck2_cli_proto::StdoutBytes>,
        req: buck2_cli_proto::UqueryRequest,
    ) -> anyhow::Result<buck2_cli_proto::UqueryResponse> {
        uquery_command(ctx, partial_result_dispatcher, req).await
    }
    async fn cquery(
        &self,
        ctx: &dyn ServerCommandContextTrait,
        partial_result_dispatcher: PartialResultDispatcher<buck2_cli_proto::StdoutBytes>,
        req: buck2_cli_proto::CqueryRequest,
    ) -> anyhow::Result<buck2_cli_proto::CqueryResponse> {
        cquery_command(ctx, partial_result_dispatcher, req).await
    }
    async fn aquery(
        &self,
        ctx: &dyn ServerCommandContextTrait,
        partial_result_dispatcher: PartialResultDispatcher<buck2_cli_proto::StdoutBytes>,
        req: buck2_cli_proto::AqueryRequest,
    ) -> anyhow::Result<buck2_cli_proto::AqueryResponse> {
        aquery_command(ctx, partial_result_dispatcher, req).await
    }
    async fn targets(
        &self,
        ctx: &dyn ServerCommandContextTrait,
        partial_result_dispatcher: PartialResultDispatcher<buck2_cli_proto::StdoutBytes>,
        req: buck2_cli_proto::TargetsRequest,
    ) -> anyhow::Result<buck2_cli_proto::TargetsResponse> {
        targets_command(ctx, partial_result_dispatcher, req).await
    }
    async fn targets_show_outputs(
        &self,
        ctx: &dyn ServerCommandContextTrait,
        partial_result_dispatcher: PartialResultDispatcher<NoPartialResult>,
        req: buck2_cli_proto::TargetsRequest,
    ) -> anyhow::Result<buck2_cli_proto::TargetsShowOutputsResponse> {
        targets_show_outputs_command(ctx, partial_result_dispatcher, req).await
    }
    async fn ctargets(
        &self,
        ctx: &dyn ServerCommandContextTrait,
        partial_result_dispatcher: PartialResultDispatcher<NoPartialResult>,
        req: ConfiguredTargetsRequest,
    ) -> anyhow::Result<ConfiguredTargetsResponse> {
        configured_targets_command(ctx, partial_result_dispatcher, req).await
    }
    async fn docs(
        &self,
        ctx: &dyn ServerCommandContextTrait,
        partial_result_dispatcher: PartialResultDispatcher<NoPartialResult>,
        req: buck2_cli_proto::UnstableDocsRequest,
    ) -> anyhow::Result<buck2_cli_proto::UnstableDocsResponse> {
        docs_command(ctx, partial_result_dispatcher, req).await
    }
}

pub(crate) fn init_listener() -> anyhow::Result<(std::net::TcpListener, ConnectionType)> {
    let (endpoint, listener) = create_listener()?;

    tracing::info!("Listener created on {}", &endpoint);

    Ok((listener, endpoint))
}

pub(crate) fn write_process_info(
    daemon_dir: &DaemonDir,
    process_info: &DaemonProcessInfo,
) -> anyhow::Result<()> {
    let file = File::create(daemon_dir.buckd_info())?;
    serde_json::to_writer(&file, &process_info)?;
    Ok(())
}

fn verify_current_daemon(daemon_dir: &DaemonDir) -> anyhow::Result<()> {
    let file = daemon_dir.buckd_pid();
    let my_pid = process::id();

    let recorded_pid: u32 = fs_util::read_to_string(&file)?.trim().parse()?;
    if recorded_pid != my_pid {
        return Err(
            DaemonError::PidFileMismatch(file.into_path_buf(), my_pid, recorded_pid).into(),
        );
    }

    Ok(())
}

fn gen_auth_token() -> String {
    (0..20)
        .map(|_| rand::thread_rng().gen_range('a'..='z'))
        .collect()
}

fn terminate_on_panic() {
    let orig_hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |panic_info| {
        orig_hook(panic_info);
        // We are using `_exit` instead of `exit` to avoid running global destructors.
        // This is similar to what default rust panic handler does
        // when there `panic=abort`: it does `abort`.
        unsafe { libc::_exit(1) }
    }));
}

impl DaemonCommand {
    fn run(
        self,
        fb: fbinit::FacebookInit,
        log_reload_handle: Arc<dyn LogConfigurationReloadHandle>,
        paths: InvocationPaths,
        before_subcommand_options: DaemonBeforeSubcommandOptions,
        in_process: bool,
        listener_created: impl FnOnce() + Send,
    ) -> anyhow::Result<()> {
        // NOTE: Do not create any threads before this point.
        //   Daemonize does not preserve threads.

        let server_init_ctx = BuckdServerInitPreferences {
            detect_cycles: before_subcommand_options.detect_cycles,
            which_dice: before_subcommand_options.which_dice,
            enable_trace_io: before_subcommand_options.enable_trace_io,
            reject_materializer_state: before_subcommand_options
                .reject_materializer_state
                .map(|s| s.into()),
            daemon_startup_config: self.daemon_startup_config,
        };

        let span = tracing::info_span!("daemon_listener");
        let span_guard = span.enter();

        let daemon_dir = paths.daemon_dir()?;
        let pid_path = daemon_dir.buckd_pid();
        let stdout_path = daemon_dir.buckd_stdout();
        let stderr_path = daemon_dir.buckd_stderr();
        // Even if we don't redirect output, we still need to create stdout/stderr files,
        // because tailer opens them. This is untidy.
        let stdout = File::create(stdout_path)?;
        let stderr = File::create(stderr_path)?;

        let auth_token = gen_auth_token();

        let (listener, process_info) = if !self.dont_daemonize {
            // We must create stdout/stderr before creating a listener,
            // otherwise it is race:
            // * daemon parent process exits
            // * client successfully connects to the unix socket
            // * but stdout/stderr may be not yet created, so tailer fails to open them
            let (listener, endpoint) = init_listener()?;

            Self::daemonize(stdout, stderr)?;

            fs_util::write(&pid_path, format!("{}", process::id()))?;

            let pid = process::id();
            let process_info = DaemonProcessInfo {
                pid: pid as i64,
                endpoint: endpoint.to_string(),
                version: BuckVersion::get().unique_id().to_owned(),
                auth_token,
            };

            // TODO(nga): this code is executed after server daemonization,
            //   so client has to retry to read it. Fix it.
            write_process_info(&daemon_dir, &process_info)?;

            tracing::info!("Daemonized.");

            (listener, process_info)
        } else {
            fs_util::write(&pid_path, format!("{}", process::id()))?;

            if !in_process {
                Self::redirect_output(stdout, stderr)?;
            }

            let (listener, endpoint) = init_listener()?;

            let process_info = DaemonProcessInfo {
                pid: process::id() as i64,
                endpoint: endpoint.to_string(),
                version: BuckVersion::get().unique_id().to_owned(),
                auth_token,
            };

            write_process_info(&daemon_dir, &process_info)?;

            (listener, process_info)
        };

        listener_created();

        terminate_on_panic();

        maybe_schedule_termination()?;

        // Higher performance for jemalloc, recommended (but may not have any effect on Mac)
        // https://github.com/jemalloc/jemalloc/blob/dev/TUNING.md#notable-runtime-options-for-performance-tuning
        memory::enable_background_threads()?;

        if cfg!(target_os = "linux") {
            #[cfg(fbcode_build)]
            {
                gflags::set_gflag_value(
                    fb,
                    "cgroup2_reader_update_interval_ms",
                    gflags::GflagValue::U32(2000),
                )
                .expect("failed to set gflag --cgroup2_reader_update_interval_ms");
            }
        }

        let mut builder = Builder::new_multi_thread();
        builder.enable_all();
        builder.thread_name("buck2-rt");

        static RUNTIME_THREADS: EnvHelper<usize> = EnvHelper::new("BUCK2_RUNTIME_THREADS");
        if let Some(threads) = RUNTIME_THREADS.get_copied()? {
            builder.worker_threads(threads);
        }

        static MAX_BLOCKING_THREADS: EnvHelper<usize> =
            EnvHelper::new("BUCK2_MAX_BLOCKING_THREADS");
        if let Some(threads) = MAX_BLOCKING_THREADS.get_copied()? {
            builder.max_blocking_threads(threads);
        }

        tracing::info!("Starting tokio runtime...");

        let rt = builder.build().context("Error creating Tokio runtime")?;

        rt.block_on(async move {
            // Once any item is received on the hard_shutdown_receiver, the daemon process will exit immediately.
            let (hard_shutdown_sender, mut hard_shutdown_receiver) = mpsc::unbounded();

            #[derive(Allocative)]
            struct Delegate {
                #[allocative(skip)]
                hard_shutdown_sender: UnboundedSender<String>,
            }

            impl BuckdServerDelegate for Delegate {
                fn force_shutdown_with_timeout(&self, reason: String, timeout: Duration) {
                    let sender = self.hard_shutdown_sender.clone();
                    tokio::spawn(async move {
                        tokio::time::sleep(timeout).await;
                        sender.unbounded_send(reason).expect("Shouldn't happen.");
                    });
                }
            }

            let delegate = Box::new(Delegate {
                hard_shutdown_sender: hard_shutdown_sender.clone(),
            });
            let daemon_dir = paths.daemon_dir()?;

            listener.set_nonblocking(true)?;
            let listener = tokio::net::TcpListener::from_std(listener)?;
            let listener = tokio_stream::wrappers::TcpListenerStream::new(listener);

            tracing::info!("Listening.");

            drop(span_guard);

            let daemon_constraints =
                gen_daemon_constraints(&server_init_ctx.daemon_startup_config)?;

            let buckd_server = BuckdServer::run(
                fb,
                log_reload_handle,
                paths,
                delegate,
                server_init_ctx,
                process_info,
                daemon_constraints,
                listener,
                &BuckdServerDependenciesImpl,
            )
            .fuse();
            let shutdown_future = async move { hard_shutdown_receiver.next().await }.fuse();
            pin_mut!(buckd_server);
            pin_mut!(shutdown_future);

            let checker_interval_seconds = self.checker_interval_seconds;

            thread::Builder::new()
                .name("check-daemon-dir".to_owned())
                .spawn(move || {
                    Self::check_daemon_dir_thread(
                        checker_interval_seconds,
                        daemon_dir,
                        hard_shutdown_sender,
                    )
                })?;

            tracing::info!("Initialization complete, running the server.");

            // clippy doesn't get along well with the select!
            #[allow(clippy::mut_mut)]
            {
                select! {
                    _ = buckd_server => {
                        tracing::info!("server shutdown");
                    }
                    reason = shutdown_future => {
                        let reason = reason.as_deref().unwrap_or("no reason available");
                        tracing::info!("server forced shutdown: {}", reason);
                    },
                };
            }

            anyhow::Ok(())
        })?;
        Ok(())
    }

    /// We start a dedicated thread to periodically check that the files in the daemon
    /// dir still reflect that we are the current buckd and verify that when you connect
    /// to the server it is our server.
    /// It gets a dedicated thread so that if somehow the main runtime gets all jammed up,
    /// this will still run (and presumably connecting to the server or our request would
    /// then fail and we'd do a hard shutdown).
    fn check_daemon_dir_thread(
        checker_interval_seconds: u64,
        daemon_dir: DaemonDir,
        hard_shutdown_sender: UnboundedSender<String>,
    ) {
        let this_rt = Builder::new_current_thread().enable_all().build().unwrap();

        this_rt.block_on(async move {
            loop {
                tokio::time::sleep(Duration::from_secs(checker_interval_seconds)).await;
                match verify_current_daemon(&daemon_dir) {
                    Ok(()) => {}
                    Err(e) => {
                        // This bit of code cannot relay errors, ignoring that we can't log
                        // a warning is reasonable.
                        let _ignored = buck2_client_ctx::eprintln!(
                            "daemon verification failed, forcing shutdown: {:#}",
                            e
                        );

                        // If this is already shutting down, we don't need to do it again.
                        let _ignored = hard_shutdown_sender
                            .unbounded_send("Daemon verification failed".to_owned());
                    }
                };
            }
        })
    }

    pub(crate) fn exec(
        self,
        init: fbinit::FacebookInit,
        log_reload_handle: Arc<dyn LogConfigurationReloadHandle>,
        paths: InvocationPaths,
        before_subcommand_options: DaemonBeforeSubcommandOptions,
        in_process: bool,
        listener_created: impl FnOnce() + Send,
    ) -> anyhow::Result<()> {
        daemon_lower_priority(self.skip_macos_qos)?;

        let project_root = paths.project_root();
        let daemon_dir = paths.daemon_dir()?;

        if !daemon_dir.path.is_dir() {
            fs_util::create_dir_all(&daemon_dir.path)?;
        }

        // TODO(nga): this breaks relative paths in `--no-buckd`.
        //   `--no-buckd` should capture correct directories earlier.
        //   Or even better, client should set current directory to project root,
        //   and resolve all paths relative to original cwd.
        fs_util::set_current_dir(project_root.root())?;

        self.run(
            init,
            log_reload_handle,
            paths,
            before_subcommand_options,
            in_process,
            listener_created,
        )?;
        Ok(())
    }

    #[cfg(unix)]
    fn redirect_output(stdout: File, stderr: File) -> anyhow::Result<()> {
        use std::os::unix::io::AsRawFd;

        nix::unistd::dup2(stdout.as_raw_fd(), nix::libc::STDOUT_FILENO)?;
        nix::unistd::dup2(stderr.as_raw_fd(), nix::libc::STDERR_FILENO)?;
        Ok(())
    }

    #[cfg(windows)]
    fn redirect_output(stdout: File, stderr: File) -> anyhow::Result<()> {
        use std::os::windows::io::AsRawHandle;

        unsafe {
            let stdout_fd = libc::open_osfhandle(stdout.as_raw_handle() as isize, libc::O_RDWR);
            let stderr_fd = libc::open_osfhandle(stderr.as_raw_handle() as isize, libc::O_RDWR);
            if stdout_fd == -1 || stderr_fd == -1 {
                return Err(anyhow::Error::msg(
                    "Can't get file descriptors for output files",
                ));
            }
            // MSVC libc doesn't export STDOUT_FILENO and STDERR_FILENO.
            let stdout_exit_code = libc::dup2(stdout_fd, 1);
            let stderr_exit_code = libc::dup2(stderr_fd, 2);
            if stdout_exit_code == -1 || stderr_exit_code == -1 {
                return Err(anyhow::Error::msg("Failed to redirect daemon output"));
            }
        }
        Ok(())
    }

    #[cfg(unix)]
    fn daemonize(stdout: File, stderr: File) -> anyhow::Result<()> {
        // TODO(cjhopman): Daemonize is pretty un-maintained. We may need to move
        // to something else or just do it ourselves.
        let daemonize = crate::commands::daemonize::Daemonize::new()
            .stdout(stdout)
            .stderr(stderr);
        daemonize.start()?;
        Ok(())
    }

    #[cfg(windows)]
    /// Restart current process in detached mode with '--dont-daemonize' flag.
    fn daemonize(_stdout: File, _stderr: File) -> anyhow::Result<()> {
        Err(anyhow::anyhow!("Cannot daemonize on Windows"))
    }
}

#[cfg(test)]
mod tests {
    use std::process;
    use std::time::Duration;

    use allocative::Allocative;
    use anyhow::Context;
    use buck2_cli_proto::DaemonProcessInfo;
    use buck2_cli_proto::KillRequest;
    use buck2_cli_proto::PingRequest;
    use buck2_client_ctx::daemon::client::connect::new_daemon_api_client;
    use buck2_client_ctx::daemon_constraints::gen_daemon_constraints;
    use buck2_common::invocation_paths::InvocationPaths;
    use buck2_common::invocation_roots::InvocationRoots;
    use buck2_common::legacy_configs::cells::DaemonStartupConfig;
    use buck2_core::fs::paths::file_name::FileNameBuf;
    use buck2_core::fs::project::ProjectRootTemp;
    use buck2_core::logging::LogConfigurationReloadHandle;
    use buck2_server::daemon::daemon_tcp::create_listener;
    use buck2_server::daemon::server::BuckdServer;
    use buck2_server::daemon::server::BuckdServerDelegate;
    use buck2_server::daemon::server::BuckdServerInitPreferences;
    use dupe::Dupe;
    use rand::RngCore;
    use rand::SeedableRng;

    use crate::commands::daemon::BuckdServerDependenciesImpl;

    // `fbinit_tokio` is not on crates, so we cannot use `#[fbinit::test]`.
    #[tokio::test]
    async fn test_daemon_smoke() {
        // TODO(nga): this should be `fbinit::perform_init`, but it is not on crates yet.
        let fbinit = unsafe { fbinit::assume_init() };

        let project_root = ProjectRootTemp::new().unwrap();

        let (endpoint, listener) = create_listener().unwrap();
        listener.set_nonblocking(true).unwrap();
        let listener = tokio::net::TcpListener::from_std(listener).unwrap();
        let listener = tokio_stream::wrappers::TcpListenerStream::new(listener);

        let invocation_paths = InvocationPaths {
            roots: InvocationRoots {
                cell_root: project_root.path().root().to_buf(),
                project_root: project_root.path().dupe(),
            },
            isolation: FileNameBuf::try_from("v2".to_owned()).unwrap(),
        };

        #[derive(Allocative)]
        struct Delegate;

        impl BuckdServerDelegate for Delegate {
            fn force_shutdown_with_timeout(&self, _reason: String, _timeout: Duration) {}
        }

        let process_info = DaemonProcessInfo {
            endpoint: endpoint.to_string(),
            pid: process::id() as i64,
            version: "13.17.19".to_owned(),
            auth_token: "abc".to_owned(),
        };

        let handle = tokio::spawn(BuckdServer::run(
            fbinit,
            <dyn LogConfigurationReloadHandle>::noop(),
            invocation_paths,
            Box::new(Delegate),
            BuckdServerInitPreferences {
                detect_cycles: None,
                which_dice: None,
                enable_trace_io: false,
                reject_materializer_state: None,
                daemon_startup_config: DaemonStartupConfig::testing_empty(),
            },
            process_info.clone(),
            gen_daemon_constraints(&DaemonStartupConfig::testing_empty()).unwrap(),
            listener,
            &BuckdServerDependenciesImpl,
        ));

        let mut client = new_daemon_api_client(endpoint.clone(), process_info.auth_token)
            .await
            .unwrap();

        client.ping(PingRequest::default()).await.unwrap();

        let mut client_with_wrong_token = new_daemon_api_client(endpoint, "wrong_token".to_owned())
            .await
            .unwrap();

        let err = format!(
            "{:#}",
            client_with_wrong_token
                .ping(PingRequest::default())
                .await
                .unwrap_err()
        );
        assert!(err.contains("invalid auth token"), "Error is: {}", err);

        client.ping(PingRequest::default()).await.unwrap();

        for req_size in [0, 1 << 10, 1 << 20, 10 << 20, 100 << 20] {
            let mut payload = vec![0; req_size];
            rand::rngs::SmallRng::seed_from_u64(20).fill_bytes(&mut payload);
            client
                .ping(PingRequest {
                    payload,
                    ..PingRequest::default()
                })
                .await
                .context(format!("req_size={}", req_size))
                .unwrap();
        }

        for resp_size in [0, 1 << 10, 1 << 20, 10 << 20, 100 << 20] {
            client
                .ping(PingRequest {
                    response_payload_size: resp_size,
                    ..PingRequest::default()
                })
                .await
                .context(format!("resp_size={}", resp_size))
                .unwrap();
        }

        client.kill(KillRequest::default()).await.unwrap();

        handle
            .await
            .expect("handle join failed")
            .expect("daemon returned error");
    }
}


// Copyright (c) 2016 Fedor Gogolev <knsd@knsd.net>

/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

// @lint-ignore-every LICENSELINT

//! This is a fork of [daemonize crate](https://github.com/knsd/daemonize/)
//! from revision f7be28efa1b4a70e43bb37b5f4ff4d664992edca.

#![cfg(unix)]

use std::fmt;
use std::fs::File;
use std::os::unix::io::AsRawFd;
use std::process::exit;

use dupe::Dupe;

#[derive(Debug)]
enum StdioImpl {
    Devnull,
    RedirectToFile(File),
}

/// Describes what to do with a standard I/O stream for a child process.
#[derive(Debug)]
pub(crate) struct Stdio {
    inner: StdioImpl,
}

impl Stdio {
    pub(crate) fn devnull() -> Self {
        Self {
            inner: StdioImpl::Devnull,
        }
    }
}

impl From<File> for Stdio {
    fn from(file: File) -> Self {
        Self {
            inner: StdioImpl::RedirectToFile(file),
        }
    }
}

/// Daemonization process outcome. Can be matched to check is it a parent process or a child
/// process.
#[derive(Debug)]
enum Outcome {
    Parent(anyhow::Result<()>),
    Child(anyhow::Result<()>),
}

/// Daemonization options.
///
/// Fork the process in the background, disassociate from its process group and the control terminal.
/// Change umask value to `0o027`, redirect all standard streams to `/dev/null`. Change working
/// directory to `/` or provided value.
///
/// Optionally:
///
///   * maintain and lock the pid-file;
///   * drop user privileges;
///   * drop group privileges;
///   * change root directory;
///   * change the pid-file ownership to provided user (and/or) group;
///   * execute any provided action just before dropping privileges.
///
pub(crate) struct Daemonize {
    stdin: Stdio,
    stdout: Stdio,
    stderr: Stdio,
}

impl fmt::Debug for Daemonize {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt.debug_struct("Daemonize")
            .field("stdin", &self.stdin)
            .field("stdout", &self.stdout)
            .field("stderr", &self.stderr)
            .finish()
    }
}

impl Default for Daemonize {
    fn default() -> Self {
        Self::new()
    }
}

impl Daemonize {
    pub(crate) fn new() -> Self {
        Daemonize {
            stdin: Stdio::devnull(),
            stdout: Stdio::devnull(),
            stderr: Stdio::devnull(),
        }
    }
}

impl Daemonize {
    /// Configuration for the child process's standard output stream.
    pub(crate) fn stdout<S: Into<Stdio>>(mut self, stdio: S) -> Self {
        self.stdout = stdio.into();
        self
    }

    /// Configuration for the child process's standard error stream.
    pub(crate) fn stderr<S: Into<Stdio>>(mut self, stdio: S) -> Self {
        self.stderr = stdio.into();
        self
    }
    /// Start daemonization process, terminate parent after first fork, returns privileged action
    /// result to the child.
    pub(crate) fn start(self) -> anyhow::Result<()> {
        match self.execute() {
            Outcome::Parent(Ok(_)) => exit(0),
            Outcome::Parent(Err(err)) => Err(err),
            Outcome::Child(Ok(())) => Ok(()),
            Outcome::Child(Err(err)) => Err(err),
        }
    }

    /// Execute daemonization process, don't terminate parent after first fork.
    fn execute(self) -> Outcome {
        unsafe {
            match perform_fork() {
                Ok(Some(_first_child_pid)) => Outcome::Parent(Ok(())),
                Err(err) => Outcome::Parent(Err(err)),
                Ok(None) => match self.execute_child() {
                    Ok(()) => Outcome::Child(Ok(())),
                    Err(err) => Outcome::Child(Err(err)),
                },
            }
        }
    }

    fn execute_child(self) -> anyhow::Result<()> {
        unsafe {
            set_sid()?;

            // This umask corresponds to a default of `rwxr-xr-x` (which is the default on Linux).
            libc::umask(0o022);

            if perform_fork()?.is_some() {
                exit(0)
            };

            redirect_standard_streams(self.stdin, self.stdout, self.stderr)?;

            Ok(())
        }
    }
}

unsafe fn perform_fork() -> anyhow::Result<Option<libc::pid_t>> {
    let pid = check_err(libc::fork(), ErrorKind::Fork)?;
    if pid == 0 { Ok(None) } else { Ok(Some(pid)) }
}

unsafe fn set_sid() -> anyhow::Result<()> {
    check_err(libc::setsid(), ErrorKind::DetachSession)?;
    Ok(())
}

unsafe fn redirect_standard_streams(
    stdin: Stdio,
    stdout: Stdio,
    stderr: Stdio,
) -> anyhow::Result<()> {
    let devnull_fd = check_err(
        libc::open(b"/dev/null\0" as *const [u8; 10] as _, libc::O_RDWR),
        ErrorKind::OpenDevnull,
    )?;

    let process_stdio = |fd, stdio: Stdio| match stdio.inner {
        StdioImpl::Devnull => check_err(libc::dup2(devnull_fd, fd), ErrorKind::RedirectStreams),
        StdioImpl::RedirectToFile(file) => {
            let raw_fd = file.as_raw_fd();
            check_err(libc::dup2(raw_fd, fd), ErrorKind::RedirectStreams)
        }
    };

    process_stdio(libc::STDIN_FILENO, stdin)?;
    process_stdio(libc::STDOUT_FILENO, stdout)?;
    process_stdio(libc::STDERR_FILENO, stderr)?;

    check_err(libc::close(devnull_fd), ErrorKind::CloseDevnull)?;

    Ok(())
}

type Errno = libc::c_int;

/// This error type for `Daemonize` `start` method.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Dupe)]
struct Error {
    kind: ErrorKind,
}

/// This error type for `Daemonize` `start` method.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Dupe)]
enum ErrorKind {
    Fork(Errno),
    DetachSession(Errno),
    OpenDevnull(Errno),
    RedirectStreams(Errno),
    CloseDevnull(Errno),
}

impl ErrorKind {
    fn description(&self) -> &str {
        match self {
            ErrorKind::Fork(_) => "unable to fork",
            ErrorKind::DetachSession(_) => "unable to create new session",
            ErrorKind::OpenDevnull(_) => "unable to open /dev/null",
            ErrorKind::RedirectStreams(_) => "unable to redirect standard streams to /dev/null",
            ErrorKind::CloseDevnull(_) => "unable to close /dev/null",
        }
    }

    fn errno(&self) -> Option<Errno> {
        match self {
            ErrorKind::Fork(errno) => Some(*errno),
            ErrorKind::DetachSession(errno) => Some(*errno),
            ErrorKind::OpenDevnull(errno) => Some(*errno),
            ErrorKind::RedirectStreams(errno) => Some(*errno),
            ErrorKind::CloseDevnull(errno) => Some(*errno),
        }
    }
}

impl std::fmt::Display for ErrorKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.description())?;
        if let Some(errno) = self.errno() {
            write!(f, ", errno {}", errno)?
        }
        Ok(())
    }
}

impl std::error::Error for ErrorKind {}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.kind)
    }
}

impl std::error::Error for Error {}

impl From<ErrorKind> for Error {
    fn from(kind: ErrorKind) -> Self {
        Self { kind }
    }
}

trait Num {
    fn is_err(&self) -> bool;
}

impl Num for i8 {
    fn is_err(&self) -> bool {
        *self == -1
    }
}

impl Num for i16 {
    fn is_err(&self) -> bool {
        *self == -1
    }
}

impl Num for i32 {
    fn is_err(&self) -> bool {
        *self == -1
    }
}

impl Num for i64 {
    fn is_err(&self) -> bool {
        *self == -1
    }
}

impl Num for isize {
    fn is_err(&self) -> bool {
        *self == -1
    }
}

fn check_err<N: Num, F: FnOnce(Errno) -> ErrorKind>(ret: N, f: F) -> anyhow::Result<N> {
    if ret.is_err() {
        Err(f(errno()).into())
    } else {
        Ok(ret)
    }
}

fn errno() -> Errno {
    std::io::Error::last_os_error()
        .raw_os_error()
        .expect("errno")
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use buck2_core::env_helper::EnvHelper;

/// Buck2 sets priority class = utility on macOS.
///
/// To disable this behavior, set this variable to `true`.
///
/// To experiment with other priority classes, set this variable to `true`,
/// and start `buck2` daemon like `taskpolicy -c utility buck2 ...`.
static DISABLE_MACOS_QOS: EnvHelper<bool> = EnvHelper::new("BUCK2_DISABLE_MACOS_QOS");

fn enable_macos_qos() -> anyhow::Result<bool> {
    let disable_qos = DISABLE_MACOS_QOS.get_copied()?.unwrap_or(false);
    Ok(!disable_qos)
}

pub(crate) fn daemon_lower_priority(skip_macos_qos_flag: bool) -> anyhow::Result<()> {
    if skip_macos_qos_flag {
        // Either:
        // * we already lowered priority or
        // * we are running in-process daemon.
        return Ok(());
    }

    if cfg!(target_os = "macos") && enable_macos_qos()? {
        #[cfg(target_os = "macos")]
        {
            do_lower_priority()?;
        }
    }
    Ok(())
}

/// On macOS we lower priority by restarting the daemon with QoS class = utility.
///
/// When a program is launched from command line, at least from iTerm2,
/// macOS seems to think it is interactive, so it gives it high priority.
/// buck2 daemon spawns processes like compilers which do not really need high priority.
/// When compilers run with high priority, they starve other processes.
/// Practically it results in very large ping and VPN disconnects on my machine.
///
/// Ideally instead of restarting the daemon we should just start the daemon with proper QoS class,
/// but rust `Command` API does not provide access to `posix_spawnattr_t`,
/// and `Command` provides convenient API to capture stdout/stderr.
///
/// This function never return `Ok`.
#[cfg(target_os = "macos")]
fn do_lower_priority() -> anyhow::Result<()> {
    use std::env;
    use std::ffi::CString;
    use std::io;
    use std::iter;
    use std::mem::MaybeUninit;
    use std::os::unix::ffi::OsStrExt;
    use std::ptr;

    use anyhow::Context;

    extern "C" {
        // https://github.com/rust-lang/libc/pull/3128
        pub fn posix_spawnattr_set_qos_class_np(
            attr: *mut libc::posix_spawnattr_t,
            qos_class: libc::qos_class_t,
        ) -> libc::c_int;
        // `environ` macro expands to this function call.
        // https://github.com/rust-lang/rust/blob/07c993eba8b76eae497e98433ae075b00f01be10/library/std/src/sys/unix/os.rs#L493
        fn _NSGetEnviron() -> *mut *mut *mut libc::c_char;
    }

    let exe = env::current_exe()?;
    let exe = CString::new(exe.into_os_string().as_bytes())?;

    struct Spawnattr(libc::posix_spawnattr_t);

    impl Drop for Spawnattr {
        fn drop(&mut self) {
            unsafe {
                let r = libc::posix_spawnattr_destroy(&mut self.0);
                assert_eq!(r, 0);
            }
        }
    }

    impl Spawnattr {
        fn new() -> anyhow::Result<Spawnattr> {
            unsafe {
                let mut spawnattr = MaybeUninit::zeroed();
                let r = libc::posix_spawnattr_init(spawnattr.as_mut_ptr());
                if r != 0 {
                    return Err(io::Error::from_raw_os_error(r)).context("posix_spawnattr_init");
                }
                Ok(Spawnattr(spawnattr.assume_init()))
            }
        }
    }

    let mut spawnattr = Spawnattr::new()?;

    let mut argv: Vec<CString> = env::args()
        .map(|s| Ok(CString::new(s)?))
        .chain(iter::once(Ok(CString::new("--skip-macos-qos")?)))
        .collect::<anyhow::Result<Vec<_>>>()?;
    let argv: Vec<*mut libc::c_char> = argv
        .iter_mut()
        .map(|s| s.as_ptr() as *mut libc::c_char)
        .chain(iter::once(ptr::null_mut()))
        .collect();

    unsafe {
        // Apple-specific flag: `posix_spawn` behaves like `exec` instead of `fork+exec`.
        let r = libc::posix_spawnattr_setflags(
            &mut spawnattr.0,
            libc::POSIX_SPAWN_SETEXEC as libc::c_short,
        );
        if r != 0 {
            return Err(io::Error::from_raw_os_error(r)).context("posix_spawnattr_setflags");
        }

        let r = posix_spawnattr_set_qos_class_np(
            &mut spawnattr.0,
            libc::qos_class_t::QOS_CLASS_UTILITY,
        );

        if r != 0 {
            return Err(io::Error::from_raw_os_error(r))
                .context("posix_spawnattr_set_qos_class_np");
        }

        let environ = *_NSGetEnviron();
        let mut pid = MaybeUninit::zeroed();

        let r = libc::posix_spawnp(
            pid.as_mut_ptr(),
            exe.as_ptr(),
            ptr::null(),
            &spawnattr.0,
            argv.as_ptr(),
            environ,
        );
        if r != 0 {
            return Err(io::Error::from_raw_os_error(r)).context("posix_spawnp");
        }
    }

    #[derive(Debug, thiserror::Error)]
    #[error("`posix_spawnp` with `POSIX_SPAWN_SETEXEC` flag should not return on success.")]
    struct Unreachable;

    Err(Unreachable.into())
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::path::PathBuf;
use std::sync::Arc;

use buck2_client_ctx::argv::Argv;
use buck2_client_ctx::argv::SanitizedArgv;
use buck2_client_ctx::client_ctx::ClientCommandContext;
use buck2_core::fs::fs_util;
use buck2_core::fs::paths::abs_norm_path::AbsNormPathBuf;
use buck2_core::logging::LogConfigurationReloadHandle;

#[cfg(unix)]
type RawFd = std::os::unix::io::RawFd;

#[cfg(windows)]
type RawFd = String;

#[derive(Debug, clap::Parser)]
#[clap(about = "run the forkserver")]
pub(crate) struct ForkserverCommand {
    #[clap(long)]
    fd: RawFd,

    #[clap(long)]
    state_dir: PathBuf,
}

impl ForkserverCommand {
    pub(crate) fn exec(
        self,
        _matches: &clap::ArgMatches,
        _ctx: ClientCommandContext<'_>,
        log_reload_handle: Arc<dyn LogConfigurationReloadHandle>,
    ) -> anyhow::Result<()> {
        let state_dir = AbsNormPathBuf::try_from(self.state_dir)?;

        fs_util::create_dir_all(&state_dir)?;

        #[cfg(unix)]
        {
            // For us to get this FD it must be non-CLOEXEC but we don't want our children to
            // inherit it.
            set_cloexec(self.fd)?;

            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()?;

            rt.block_on(buck2_forkserver::unix::run_forkserver(
                self.fd,
                log_reload_handle,
                state_dir,
            ))
        }

        #[cfg(not(unix))]
        {
            let _ignored = log_reload_handle;
            Err(anyhow::anyhow!("The forkserver is only available on UNIX"))
        }
    }

    pub(crate) fn sanitize_argv(&self, argv: Argv) -> SanitizedArgv {
        argv.no_need_to_sanitize()
    }
}

#[cfg(unix)]
fn set_cloexec(fd: RawFd) -> std::io::Result<()> {
    unsafe {
        let flags = libc::fcntl(fd, libc::F_GETFD);
        if flags < 0 {
            return Err(std::io::Error::last_os_error());
        }

        if libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) < 0 {
            return Err(std::io::Error::last_os_error());
        }
    }

    Ok(())
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use buck2_client_ctx::argv::Argv;
use buck2_client_ctx::argv::SanitizedArgv;
use buck2_client_ctx::client_ctx::ClientCommandContext;
use clap::Parser;
use tokio::runtime::Runtime;

#[derive(Debug, Parser)]
#[clap(about = "run the internal test runner")]
pub(crate) struct InternalTestRunnerCommand {
    #[cfg(unix)]
    #[clap(flatten)]
    unix_runner: buck2_test_runner::unix::Buck2TestRunnerUnix,

    #[cfg(not(unix))]
    #[clap(flatten)]
    tcp_runner: buck2_test_runner::tcp::Buck2TestRunnerTcp,
}

impl InternalTestRunnerCommand {
    pub(crate) fn exec(
        self,
        _matches: &clap::ArgMatches,
        _ctx: ClientCommandContext<'_>,
    ) -> anyhow::Result<()> {
        // Internal test runner should only be used in the open source version of Buck2.
        if buck2_core::is_open_source()
            || std::env::var("BUCK2_ALLOW_INTERNAL_TEST_RUNNER_DO_NOT_USE").is_ok()
        {
            let runtime = Runtime::new().expect("Failed to create Tokio runtime");
            runtime.block_on(async move {
                #[cfg(unix)]
                {
                    self.unix_runner.run().await
                }
                #[cfg(not(unix))]
                {
                    self.tcp_runner.run().await
                }
            })
        } else {
            anyhow::bail!(
                "Cannot use internal test runner. Config value must be provided for test.v2_test_executor."
            )
        }
    }

    pub(crate) fn sanitize_argv(&self, argv: Argv) -> SanitizedArgv {
        argv.no_need_to_sanitize()
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

pub mod daemon;
pub(crate) mod daemon_lower_priority;
pub(crate) mod daemonize;
pub mod docs;
pub mod forkserver;
pub mod internal_test_runner;
pub(crate) mod schedule_termination;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::thread;
use std::time::Duration;

use buck2_core::env_helper::EnvHelper;
use buck2_util::process_stats::process_cpu_time_us;

fn elapsed_cpu_time_as_percents(
    cpu_time_before_us: Option<u64>,
    cpu_time_after_us: Option<u64>,
    duration_seconds: u64,
) -> Option<u64> {
    let cpu_time_before = cpu_time_before_us?;
    let cpu_time_after = cpu_time_after_us?;
    let elapsed_cpu_time_us = cpu_time_after.checked_sub(cpu_time_before)?;
    let elapsed_cpu_time_us_avg_per_second = elapsed_cpu_time_us.checked_div(duration_seconds)?;
    elapsed_cpu_time_us_avg_per_second.checked_div(1_000_000 / 100)
}

/// Our tests sometimes don't exit Buck 2 cleanly, and they might not get an oppportunity to do so
/// if they are terminated. This allows the daemon to self-destruct.
pub(crate) fn maybe_schedule_termination() -> anyhow::Result<()> {
    static TERMINATE_AFTER: EnvHelper<u64> = EnvHelper::new("BUCK2_TERMINATE_AFTER");

    if let Some(duration) = TERMINATE_AFTER.get_copied()? {
        thread::Builder::new()
            .name("buck2-terminate-after".to_owned())
            .spawn(move || {
                const MEASURE_CPU_TIME_FOR: u64 = 10;
                let (sleep_before, sleep_after) = match duration.checked_sub(MEASURE_CPU_TIME_FOR) {
                    Some(sleep_before) => (sleep_before, MEASURE_CPU_TIME_FOR),
                    None => (0, duration),
                };

                thread::sleep(Duration::from_secs(sleep_before));
                let process_cpu_time_us_before = process_cpu_time_us();
                thread::sleep(Duration::from_secs(sleep_after));
                let process_cpu_time_us_after = process_cpu_time_us();

                let elapsed_cpu_time_avg_in_percents = elapsed_cpu_time_as_percents(
                    process_cpu_time_us_before,
                    process_cpu_time_us_after,
                    sleep_after,
                );
                if let Some(elapsed_cpu_time_avg_in_percents) = elapsed_cpu_time_avg_in_percents {
                    panic!(
                        "Buck is exiting after {}s elapsed; avg process CPU in the last {}s is {}%",
                        duration, sleep_after, elapsed_cpu_time_avg_in_percents
                    );
                } else {
                    panic!("Buck is exiting after {}s elapsed", duration);
                }
            })?;
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use crate::commands::schedule_termination::elapsed_cpu_time_as_percents;

    #[test]
    fn test_elapsed_cpu_time_as_percents() {
        // 12 seconds wall time
        // 6 seconds of CPU time
        // equivalent to 50% CPU usage
        assert_eq!(
            Some(50),
            elapsed_cpu_time_as_percents(Some(1_000_123), Some(7_000_123), 12)
        );
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use buck2_client_ctx::argv::Argv;
use buck2_client_ctx::argv::SanitizedArgv;
use buck2_client_ctx::client_ctx::ClientCommandContext;
use buck2_client_ctx::exit_result::ExitResult;
use buck2_client_ctx::streaming::BuckSubcommand;

use crate::commands::docs::query::DocsCqueryCommand;
use crate::commands::docs::query::DocsUqueryCommand;
use crate::commands::docs::starlark::DocsStarlarkCommand;

mod output;
mod query;
mod starlark;

#[allow(clippy::large_enum_variant)]
#[derive(Debug, clap::Parser)]
enum DocsKind {
    Starlark(DocsStarlarkCommand),
    Uquery(DocsUqueryCommand),
    Query(DocsUqueryCommand),
    Cquery(DocsCqueryCommand),
}

#[derive(Debug, clap::Parser)]
#[clap(name = "docs", about = "Print documentation of specified symbols")]
pub(crate) struct DocsCommand {
    #[clap(subcommand)]
    docs_kind: DocsKind,
}

impl DocsCommand {
    pub(crate) fn exec(
        self,
        matches: &clap::ArgMatches,
        ctx: ClientCommandContext<'_>,
    ) -> ExitResult {
        let submatches = match matches.subcommand().map(|s| s.1) {
            Some(submatches) => submatches,
            None => panic!("Parsed a subcommand but couldn't extract subcommand argument matches"),
        };
        match self.docs_kind {
            DocsKind::Starlark(cmd) => cmd.exec(submatches, ctx),
            DocsKind::Uquery(cmd) => cmd.exec(submatches, ctx),
            DocsKind::Query(cmd) => cmd.exec(submatches, ctx),
            DocsKind::Cquery(cmd) => cmd.exec(submatches, ctx),
        }
    }

    pub(crate) fn sanitize_argv(&self, argv: Argv) -> SanitizedArgv {
        argv.no_need_to_sanitize()
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use dupe::Dupe;

#[derive(Debug, Clone, Dupe, clap::ArgEnum)]
#[clap(rename_all = "snake_case")]
enum DocsOutputFormatArg {
    Markdown,
    Rendered,
}

#[derive(Debug, clap::Parser)]
pub(crate) struct DocsOutputFormatOptions {
    /// How to format the documentation
    #[clap(
        long = "format",
        default_value = "rendered",
        arg_enum,
        ignore_case = true
    )]
    format: DocsOutputFormatArg,
}

impl DocsOutputFormatOptions {
    pub fn emit_markdown(&self, markdown: &str) -> anyhow::Result<()> {
        match self.format {
            DocsOutputFormatArg::Markdown => {
                buck2_client_ctx::println!("{}", markdown)?;
            }
            DocsOutputFormatArg::Rendered => {
                let skin = termimad::MadSkin::default();
                let area = termimad::Area::full_screen();
                let width = std::cmp::min(100, area.width) as usize;
                let rendered = skin.text(markdown, Some(width));
                buck2_client_ctx::println!("{}", rendered)?;
            }
        }

        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use buck2_client_ctx::client_ctx::ClientCommandContext;
use buck2_client_ctx::exit_result::ExitResult;
use buck2_query::query::syntax::simple::functions::docs::MarkdownOptions;
use buck2_query::query::syntax::simple::functions::docs::QueryEnvironmentDescription;
use buck2_query_impls::cquery::environment::CqueryEnvironment;
use buck2_query_impls::uquery::environment::UqueryEnvironment;

use crate::commands::docs::output::DocsOutputFormatOptions;

#[derive(Debug, clap::Parser)]
#[clap(name = "docs-uquery", about = "Print documentation for uquery")]
pub(crate) struct DocsUqueryCommand {
    #[clap(flatten)]
    docs_options: DocsOutputFormatOptions,
}

#[derive(Debug, clap::Parser)]
#[clap(name = "docs-cquery", about = "Print documentation for cquery")]
pub(crate) struct DocsCqueryCommand {
    #[clap(flatten)]
    docs_options: DocsOutputFormatOptions,
}

fn output(
    options: DocsOutputFormatOptions,
    description: QueryEnvironmentDescription,
) -> ExitResult {
    let markdown = description.render_markdown(&MarkdownOptions {
        include_alt_text: true,
    });
    options.emit_markdown(&markdown)?;
    ExitResult::success()
}

impl DocsUqueryCommand {
    pub(crate) fn exec(
        self,
        _matches: &clap::ArgMatches,
        _ctx: ClientCommandContext<'_>,
    ) -> ExitResult {
        let description = UqueryEnvironment::describe();
        output(self.docs_options, description)
    }
}

impl DocsCqueryCommand {
    pub(crate) fn exec(
        self,
        _matches: &clap::ArgMatches,
        _ctx: ClientCommandContext<'_>,
    ) -> ExitResult {
        let description = CqueryEnvironment::describe();
        output(self.docs_options, description)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

mod markdown;

use anyhow::Context;
use async_trait::async_trait;
use buck2_cli_proto::UnstableDocsRequest;
use buck2_client_ctx::client_ctx::ClientCommandContext;
use buck2_client_ctx::common::CommonBuildConfigurationOptions;
use buck2_client_ctx::common::CommonConsoleOptions;
use buck2_client_ctx::common::CommonDaemonCommandOptions;
use buck2_client_ctx::daemon::client::BuckdClientConnector;
use buck2_client_ctx::daemon::client::NoPartialResultHandler;
use buck2_client_ctx::exit_result::ExitResult;
use buck2_client_ctx::streaming::StreamingCommand;
use dupe::Dupe;

use crate::commands::docs::starlark::markdown::MarkdownFileOptions;

#[derive(Debug, Clone, Dupe, clap::ArgEnum)]
#[clap(rename_all = "snake_case")]
enum DocsOutputFormatArg {
    Json,
    MarkdownFiles,
}

#[derive(Debug, clap::Parser)]
#[clap(
    name = "docs-starlark",
    about = "Print documentation of user-defined starlark symbols"
)]
pub(crate) struct DocsStarlarkCommand {
    #[clap(flatten)]
    pub config_opts: CommonBuildConfigurationOptions,

    #[clap(flatten)]
    console_opts: CommonConsoleOptions,

    #[clap(flatten)]
    event_log_opts: CommonDaemonCommandOptions,

    #[clap(flatten)]
    markdown_file_opts: MarkdownFileOptions,

    #[clap(
        long = "format",
        help = "how to format the returned documentation",
        default_value = "json",
        arg_enum,
        ignore_case = true
    )]
    format: DocsOutputFormatArg,

    #[clap(
        long = "builtins",
        help = "get documentation for built in functions, rules, and providers"
    )]
    builtins: bool,

    #[clap(
        long = "prelude",
        help = "get documentation for the prelude, if present"
    )]
    prelude: bool,

    #[clap(
        name = "SYMBOL_PATTERNS",
        help = "Patterns to interpret. //foo:bar.bzl is 'every symbol in //foo:bar.bzl', //foo:bar.bzl:baz only returns the documentation for the symbol 'baz' in //foo:bar.bzl"
    )]
    patterns: Vec<String>,
}

#[async_trait]
impl StreamingCommand for DocsStarlarkCommand {
    const COMMAND_NAME: &'static str = "docs starlark";
    async fn exec_impl(
        self,
        buckd: &mut BuckdClientConnector,
        matches: &clap::ArgMatches,
        ctx: &mut ClientCommandContext<'_>,
    ) -> ExitResult {
        let client_context =
            ctx.client_context(&self.config_opts, matches, ctx.sanitized_argv.argv.clone())?;

        let response = buckd
            .with_flushing()
            .unstable_docs(
                UnstableDocsRequest {
                    context: Some(client_context),
                    symbol_patterns: self.patterns.clone(),
                    retrieve_builtins: self.builtins,
                    retrieve_prelude: self.prelude,
                    format: match self.format {
                        DocsOutputFormatArg::Json => {
                            buck2_cli_proto::unstable_docs_request::Format::Json as i32
                        }
                        DocsOutputFormatArg::MarkdownFiles => {
                            buck2_cli_proto::unstable_docs_request::Format::Markdown as i32
                        }
                    },
                    markdown_output_path: self
                        .markdown_file_opts
                        .destination_dir
                        .as_ref()
                        .map(|d| {
                            anyhow::Ok(
                                d.resolve(&ctx.working_dir)
                                    .to_str()
                                    .context("path is not valid")?
                                    .to_owned(),
                            )
                        })
                        .transpose()?,
                    markdown_starlark_subdir: self.markdown_file_opts.starlark_subdir.clone(),
                    markdown_native_subdir: self.markdown_file_opts.native_subdir.clone(),
                },
                ctx.stdin().console_interaction_stream(&self.console_opts),
                &mut NoPartialResultHandler,
            )
            .await??;

        if let Some(json_output) = response.json_output {
            buck2_client_ctx::println!("{}", json_output.trim_end())?;
        }

        ExitResult::success()
    }

    fn console_opts(&self) -> &CommonConsoleOptions {
        &self.console_opts
    }

    fn event_log_opts(&self) -> &CommonDaemonCommandOptions {
        &self.event_log_opts
    }

    fn common_opts(&self) -> &CommonBuildConfigurationOptions {
        &self.config_opts
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use buck2_client_ctx::path_arg::PathArg;

#[derive(Debug, clap::Parser, serde::Serialize, serde::Deserialize)]
pub(crate) struct MarkdownFileOptions {
    #[structopt(
        long = "markdown-files-destination-dir",
        required_if_eq("format", "markdown_files")
    )]
    pub(crate) destination_dir: Option<PathArg>,
    #[structopt(long = "markdown-files-native-subdir", default_value = "native")]
    pub(crate) native_subdir: String,
    #[structopt(long = "markdown-files-starlark-subdir", default_value = "starlark")]
    pub(crate) starlark_subdir: String,
}