/*
 * 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 async_condvar_fair::Condvar;
use futures::future::Future;
use futures::future::FutureExt;
use parking_lot::Mutex;

/// A semaphore-like structure that locks up and stops emitting permits *at all* if the number of
/// requests exceeds the permit limit. This puts an upper bound on callers entering the critical
/// section at the capacity, but unlike a semaphore, the lower bound is zero, not the capacity.
pub struct LowPassFilter {
    /// Keeps track of the outstanding requests.
    state: Mutex<LowPassFilterState>,

    /// This is used to notify waiters when a new permit can be handed out.
    cv: Condvar,

    /// The (configured) limit for allowed concurrent accessors. If this is exceeded, no further
    /// permits will be issued until the accessors count goes back below the capacity. Permits that
    /// were issued aren't relinquished, but as they are released, no replacement permits will be
    /// issued until the accessors count goes below the capacity.
    capacity: usize,
}

struct LowPassFilterState {
    /// The total weight that has been requested through `access()`. This may be greater than the
    /// capacity.
    accessors: usize,
}

impl LowPassFilterState {
    fn can_dispatch_more(&self, capacity: usize) -> bool {
        self.accessors <= capacity
    }
}

impl LowPassFilter {
    pub fn new(capacity: usize) -> Self {
        Self {
            state: Mutex::new(LowPassFilterState { accessors: 0 }),
            cv: Condvar::new(),
            capacity,
        }
    }

    pub fn capacity(&self) -> usize {
        self.capacity
    }

    /// Request a permit to enter the critical section.
    ///
    /// To make things predictable, we synchronously increment the accessors count. This ensures
    /// that there is no ambiguity about whether an accessor has already incremented the count or
    /// not depending on whether it was polled.
    #[allow(clippy::await_holding_lock)] // wait() actually releases the lock.
    #[allow(clippy::manual_async_fn)] // so you don't need to poll before access() takes effect
    pub fn access(&self, weight: usize) -> impl Future<Output = LowPassFilterGuard<'_>> {
        let go = {
            let mut state = self.state.lock();
            state.accessors += weight;
            state.can_dispatch_more(self.capacity)
        };

        // This needs to be created *here* and not at the return point so that if this future gets
        // dropped while we await (or possible en before we await), the guard will re-acquire the
        // mutex and update the accessors count.
        let guard = LowPassFilterGuard {
            filter: self,
            weight,
        };

        if go {
            return futures::future::ready(guard).left_future();
        }

        async move {
            let mut state = self.state.lock();
            loop {
                if state.can_dispatch_more(self.capacity) {
                    return guard;
                }
                state = self.cv.wait(state).await;
            }
        }
        .right_future()
    }
}

pub struct LowPassFilterGuard<'a> {
    filter: &'a LowPassFilter,
    weight: usize,
}

impl Drop for LowPassFilterGuard<'_> {
    /// When a guard is released, lower the accessors count, and if we are now low enough to
    /// proceed, notify a task. Note that this happens regardless of whether the owner of the
    /// LowPassFilterGuard had entered the critical section or not, since there is no difference
    /// between dropping a permit and dropping an outstading request for access.
    fn drop(&mut self) {
        let mut state = self.filter.state.lock();
        state.accessors -= self.weight;
        if state.can_dispatch_more(self.filter.capacity) {
            self.filter.cv.notify_one();
        }
    }
}

#[cfg(test)]
mod tests {
    use futures::future::FutureExt;

    use super::*;

    #[tokio::test]
    async fn test_access() {
        let filter = LowPassFilter::new(1);
        filter.access(1).await;
    }

    #[tokio::test]
    async fn test_access_weight() {
        let filter = LowPassFilter::new(2);
        let t0 = filter.access(2).await;

        let t1 = filter.access(2);
        futures::pin_mut!(t1);
        assert!(futures::poll!(t1.as_mut()).is_pending());

        drop(t0);
        assert!(futures::poll!(t1.as_mut()).is_ready());
    }

    #[tokio::test]
    async fn test_access_does_not_require_poll() {
        let filter = LowPassFilter::new(1);
        let t1 = filter.access(1).await;

        let t2 = filter.access(1);
        futures::pin_mut!(t2);
        assert!(futures::poll!(t2.as_mut()).is_pending());

        drop(t1);
        assert!(futures::poll!(t2.as_mut()).is_ready());
    }

    #[tokio::test]
    async fn test_access_many() {
        let filter = LowPassFilter::new(2);
        let _t1 = filter.access(1).await;
        let _t2 = filter.access(1).await;
    }

    #[tokio::test]
    async fn test_release() {
        let filter = LowPassFilter::new(1);
        let t0 = filter.access(1).await;
        let t1 = filter.access(1);
        futures::pin_mut!(t1);

        assert!(futures::poll!(t1.as_mut()).is_pending());

        drop(t0);
        assert!(futures::poll!(t1.as_mut()).is_ready());
    }

    #[tokio::test]
    async fn test_release_cancel() {
        let filter = LowPassFilter::new(1);
        let t0 = filter.access(1).await;

        // NOTE: We don't use pin_mut like above since we want to exercise cancellation here so we
        // need to be able to drop those futures.
        let mut t1 = filter.access(1).boxed();
        let mut t2 = filter.access(1).boxed();

        // We have 3 accessors, so we block.
        assert!(futures::poll!(t1.as_mut()).is_pending());
        assert!(futures::poll!(t2.as_mut()).is_pending());

        // We still have 2 accessors, so we block.
        drop(t0);
        assert!(futures::poll!(t1.as_mut()).is_pending());
        assert!(futures::poll!(t2.as_mut()).is_pending());

        // Now that we have only 1 accessor, we can proceed.
        drop(t1);
        assert!(futures::poll!(t2.as_mut()).is_ready());
    }
}


/*
 * 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::ops::ControlFlow;
use std::sync::Arc;

use async_trait::async_trait;
use buck2_common::executor_config::RemoteExecutorUseCase;
use buck2_core::fs::artifact_path_resolver::ArtifactFs;
use buck2_core::fs::project_rel_path::ProjectRelativePath;
use buck2_execute::execute::executor_stage_async;
use buck2_execute::execute::manager::CommandExecutionManager;
use buck2_execute::execute::manager::CommandExecutionManagerExt;
use buck2_execute::execute::prepared::PreparedCommand;
use buck2_execute::execute::prepared::PreparedCommandOptionalExecutor;
use buck2_execute::execute::result::CommandExecutionResult;
use buck2_execute::knobs::ExecutorGlobalKnobs;
use buck2_execute::materialize::materializer::Materializer;
use buck2_execute::re::action_identity::ReActionIdentity;
use buck2_execute::re::manager::ManagedRemoteExecutionClient;
use dupe::Dupe;
use more_futures::cancellation::CancellationContext;
use tracing::info;

use crate::re::download::download_action_results;
use crate::re::download::DownloadResult;

pub struct ActionCacheChecker {
    pub artifact_fs: ArtifactFs,
    pub materializer: Arc<dyn Materializer>,
    pub re_client: ManagedRemoteExecutionClient,
    pub re_use_case: RemoteExecutorUseCase,
    pub re_action_key: Option<String>,
    pub upload_all_actions: bool,
    pub knobs: ExecutorGlobalKnobs,
}

#[async_trait]
impl PreparedCommandOptionalExecutor for ActionCacheChecker {
    async fn maybe_execute(
        &self,
        command: &PreparedCommand<'_, '_>,
        manager: CommandExecutionManager,
        cancellations: &CancellationContext,
    ) -> ControlFlow<CommandExecutionResult, CommandExecutionManager> {
        let request = command.request;
        let action_digest = &command.prepared_action.action;
        let action_blobs = &command.prepared_action.blobs;
        let digest_config = command.digest_config;
        let re_client = &self.re_client;

        let action_cache_response = executor_stage_async(
            buck2_data::CacheQuery {
                action_digest: action_digest.to_string(),
            },
            re_client.action_cache(action_digest.dupe(), self.re_use_case),
        )
        .await;

        if self.upload_all_actions {
            match re_client
                .upload(
                    self.artifact_fs.fs(),
                    &self.materializer,
                    action_blobs,
                    ProjectRelativePath::empty(),
                    request.paths().input_directory(),
                    self.re_use_case,
                    digest_config,
                )
                .await
            {
                Err(e) => {
                    return ControlFlow::Break(manager.error("upload", e));
                }
                Ok(_) => {}
            };
        }

        let response = match action_cache_response {
            Err(e) => {
                return ControlFlow::Break(manager.error("remote_action_cache", e));
            }
            Ok(Some(response)) => {
                // we were able to go to the action cache, so can skip uploading and running
                info!(
                    "Action result is cached, skipping execution of:\n```\n$ {}\n```\n for action `{}`",
                    request.all_args_str(),
                    action_digest,
                );
                response
            }
            Ok(None) => return ControlFlow::Continue(manager),
        };

        let action_key = if self.knobs.log_action_keys {
            let identity = ReActionIdentity::new(
                command.target,
                self.re_action_key.as_deref(),
                request.paths(),
            );
            Some(identity.action_key)
        } else {
            None
        };

        let res = download_action_results(
            request,
            &*self.materializer,
            &self.re_client,
            self.re_use_case,
            digest_config,
            manager,
            // TODO (torozco): We should deduplicate this and ActionExecutionKind.
            buck2_data::CacheHit {
                action_digest: action_digest.to_string(),
                action_key,
            }
            .into(),
            request.paths(),
            request.outputs(),
            action_digest,
            &response,
            cancellations,
        )
        .await;

        let DownloadResult::Result(res) = res;

        ControlFlow::Break(res)
    }
}


/*
 * 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::sync::Arc;
use std::time::SystemTime;

use anyhow::Context as _;
use async_trait::async_trait;
use buck2_common::executor_config::RemoteExecutorUseCase;
use buck2_core::directory::DirectoryEntry;
use buck2_core::env_helper::EnvHelper;
use buck2_core::fs::artifact_path_resolver::ArtifactFs;
use buck2_events::dispatch::span_async;
use buck2_execute::digest::CasDigestToReExt;
use buck2_execute::digest_config::DigestConfig;
use buck2_execute::directory::directory_to_re_tree;
use buck2_execute::directory::ActionDirectoryMember;
use buck2_execute::execute::action_digest::ActionDigest;
use buck2_execute::execute::blobs::ActionBlobs;
use buck2_execute::execute::cache_uploader::CacheUploadInfo;
use buck2_execute::execute::cache_uploader::UploadCache;
use buck2_execute::execute::kind::CommandExecutionKind;
use buck2_execute::execute::result::CommandExecutionResult;
use buck2_execute::execute::result::CommandExecutionStatus;
use buck2_execute::execute::target::CommandExecutionTarget;
use buck2_execute::knobs::ExecutorGlobalKnobs;
use buck2_execute::materialize::materializer::Materializer;
use buck2_execute::re::manager::ManagedRemoteExecutionClient;
use derive_more::Display;
use dupe::Dupe;
use futures::future;
use futures::future::FutureExt;
use remote_execution::DigestWithStatus;
use remote_execution::NamedDigest;
use remote_execution::REClientError;
use remote_execution::TActionResult2;
use remote_execution::TCode;
use remote_execution::TDirectory2;
use remote_execution::TExecutedActionMetadata;
use remote_execution::TFile;
use remote_execution::TStatus;
use remote_execution::TTimestamp;

// Whether to throw errors when cache uploads fail (primarily for tests).
static ERROR_ON_CACHE_UPLOAD: EnvHelper<bool> = EnvHelper::new("BUCK2_TEST_ERROR_ON_CACHE_UPLOAD");

/// A PreparedCommandExecutor that will write to cache after invoking the inner executor
pub struct CacheUploader {
    pub artifact_fs: ArtifactFs,
    pub materializer: Arc<dyn Materializer>,
    pub re_client: ManagedRemoteExecutionClient,
    pub re_use_case: RemoteExecutorUseCase,
    pub knobs: ExecutorGlobalKnobs,
    pub max_bytes: Option<u64>,
}

impl CacheUploader {
    /// Upload an action result to the RE action cache, assuming conditions for the upload are met:
    /// the action must have been successful and must have run locally (not much point in caching
    /// something that ran on RE and is already cached), and cache uploads must be enabled for this particular action.
    /// The CacheUploader should only be used if cache uploads are enabled.
    async fn maybe_perform_cache_upload(
        &self,
        target: &dyn CommandExecutionTarget,
        digest: &ActionDigest,
        result: &CommandExecutionResult,
        digest_config: DigestConfig,
    ) -> anyhow::Result<Option<CacheUploadOutcome>> {
        let output_bytes = result.calc_output_size_bytes();

        match &result.report.status {
            CommandExecutionStatus::Success {
                execution_kind: CommandExecutionKind::Local { .. },
            } => {
                // Fall through to the upload.
            }
            _ => {
                // In all other cases, skip.
                return Ok(None);
            }
        }

        let outcome = span_async(
            buck2_data::CacheUploadStart {
                key: Some(target.as_proto_action_key()),
                name: Some(target.as_proto_action_name()),
                action_digest: digest.to_string(),
            },
            async move {
                let mut file_digests = Vec::new();
                let mut tree_digests = Vec::new();

                let res = async {
                    // NOTE: If the size exceeds the limit, we still log that attempt, since
                    // whoever is configuring this can't easily anticipate how large the outputs
                    // will be, so some logging is useful.
                    if let Some(max_bytes) = self.max_bytes {
                        if output_bytes > max_bytes {
                            return Ok(CacheUploadOutcome::Rejected(
                                CacheUploadRejectionReason::OutputExceedsLimit { max_bytes },
                            ));
                        }
                    }

                    self.perform_cache_upload(
                        digest,
                        result,
                        &mut file_digests,
                        &mut tree_digests,
                        digest_config,
                    )
                    .await
                }
                .await;

                let (success, error, re_error_code) = match &res {
                    Ok(CacheUploadOutcome::Success) => (true, String::new(), None),
                    Ok(CacheUploadOutcome::Rejected(reason)) => {
                        (false, format!("Rejected: {}", reason), None)
                    }
                    Err(e) => (
                        false,
                        format!("{:#}", e),
                        e.downcast_ref::<REClientError>()
                            .map(|e| e.code.to_string()),
                    ),
                };

                (
                    res,
                    Box::new(buck2_data::CacheUploadEnd {
                        key: Some(target.as_proto_action_key()),
                        name: Some(target.as_proto_action_name()),
                        action_digest: digest.to_string(),
                        success,
                        error,
                        re_error_code,
                        file_digests,
                        tree_digests,
                        output_bytes: Some(output_bytes),
                    }),
                )
            },
        )
        .await?;

        Ok(Some(outcome))
    }

    async fn perform_cache_upload(
        &self,
        digest: &ActionDigest,
        result: &CommandExecutionResult,
        file_digests: &mut Vec<String>,
        tree_digests: &mut Vec<String>,
        digest_config: DigestConfig,
    ) -> anyhow::Result<CacheUploadOutcome> {
        tracing::debug!("Uploading action result for `{}`", digest);

        let timing = result.report.timing;

        let mut upload_futs = vec![];
        let mut output_files = vec![];
        let mut output_directories = vec![];

        for (output, value) in result.resolve_outputs(&self.artifact_fs) {
            match value.entry().as_ref() {
                DirectoryEntry::Leaf(ActionDirectoryMember::File(f)) => {
                    output_files.push(TFile {
                        digest: DigestWithStatus {
                            digest: f.digest.to_re(),
                            status: TStatus {
                                code: TCode::OK,
                                message: String::new(),
                                ..Default::default()
                            },
                            ..Default::default()
                        },
                        name: output.path().to_string(),
                        executable: f.is_executable,
                        ..Default::default()
                    });

                    let fut = async move {
                        let name = self
                            .artifact_fs
                            .fs()
                            .resolve(output.path())
                            .as_maybe_relativized_str()?
                            .to_owned();

                        self.re_client
                            .upload_files_and_directories(
                                vec![NamedDigest {
                                    name,
                                    digest: f.digest.to_re(),
                                    ..Default::default()
                                }],
                                vec![],
                                vec![],
                                self.re_use_case,
                            )
                            .await
                    };

                    file_digests.push(f.digest.to_string());
                    upload_futs.push(fut.boxed());
                }
                DirectoryEntry::Dir(d) => {
                    let tree = directory_to_re_tree(d);
                    let mut action_blobs = ActionBlobs::new(digest_config);
                    let tree_digest = action_blobs.add_protobuf_message(&tree, digest_config);

                    output_directories.push(TDirectory2 {
                        path: output.path().to_string(),
                        tree_digest: tree_digest.to_re(),
                        root_directory_digest: d.fingerprint().to_re(),
                        ..Default::default()
                    });

                    let fut = async move {
                        self.re_client
                            .upload(
                                self.artifact_fs.fs(),
                                &self.materializer,
                                &action_blobs,
                                output.path(),
                                &d.dupe().as_immutable(),
                                self.re_use_case,
                                digest_config,
                            )
                            .await
                            .map(|_| ())
                    };

                    upload_futs.push(fut.boxed());
                    tree_digests.push(tree_digest.to_string());
                }
                DirectoryEntry::Leaf(..) => {
                    // Bail, there is something that is not a file here and we don't handle this.
                    // This will happen if the value is a symlink. The primary output of a command
                    // being a symlink is probably unlikely. Unfortunately, we can't represent this
                    // in RE's action output, so we either have to lie about the output and pretend
                    // it's a file, or bail.
                    return Ok(CacheUploadOutcome::Rejected(
                        CacheUploadRejectionReason::SymlinkOutput,
                    ));
                }
            }
        }

        let uploads = async {
            future::try_join_all(upload_futs)
                .await
                .context("Error uploading outputs")?;

            Ok(())
        };

        let std_streams = async {
            result
                .report
                .std_streams
                .clone()
                .into_re(&self.re_client, self.re_use_case)
                .await
                .context("Error accessing std_streams")
        };

        let ((), std_streams) = future::try_join(uploads, std_streams).await?;

        let worker;

        #[cfg(fbcode_build)]
        {
            let hostname = hostname::get()?;
            worker = hostname.to_string_lossy().into_owned();
        }

        #[cfg(not(fbcode_build))]
        {
            worker = "".to_owned();
        }

        let (stdout_raw, stdout_digest) = std_streams.stdout.into_raw_or_digest();
        let (stderr_raw, stderr_digest) = std_streams.stderr.into_raw_or_digest();

        let result = TActionResult2 {
            output_files,
            output_directories,
            exit_code: 0,
            stdout_raw,
            stdout_digest,
            stderr_raw,
            stderr_digest,
            execution_metadata: TExecutedActionMetadata {
                worker,
                execution_dir: "".to_owned(),
                execution_start_timestamp: systemtime_to_ttimestamp(timing.start_time)?,
                execution_completed_timestamp: systemtime_to_ttimestamp(
                    timing.start_time + timing.wall_time,
                )?,
                execution_attempts: 1,
                ..Default::default()
            },
            ..Default::default()
        };

        self.re_client
            .write_action_result(digest.to_re(), result, self.re_use_case)
            .await?;

        Ok(CacheUploadOutcome::Success)
    }
}

/// Whether we completed a cache upload.
#[derive(Copy, Clone, Dupe, Debug)]
enum CacheUploadOutcome {
    Success,
    Rejected(CacheUploadRejectionReason),
}

/// A reason why we chose not to upload.
#[derive(Copy, Clone, Dupe, Debug, Display)]
enum CacheUploadRejectionReason {
    #[display(fmt = "SymlinkOutput")]
    SymlinkOutput,
    #[display(fmt = "OutputExceedsLimit({})", max_bytes)]
    OutputExceedsLimit { max_bytes: u64 },
}

#[async_trait]
impl UploadCache for CacheUploader {
    async fn upload(
        &self,
        info: &CacheUploadInfo<'_>,
        res: &CommandExecutionResult,
    ) -> anyhow::Result<bool> {
        let error_on_cache_upload = match ERROR_ON_CACHE_UPLOAD.get_copied() {
            Ok(r) => r.unwrap_or_default(),
            Err(e) => return Err(e).context("cache_upload"),
        };
        let action = &info.action_digest;

        // TODO(bobyf, torozco) should these be critical sections?
        let upload_res = self
            .maybe_perform_cache_upload(info.target.dupe(), action, res, info.digest_config)
            .await;

        match upload_res {
            Ok(Some(CacheUploadOutcome::Success)) => {
                tracing::info!("Cache upload for `{}` succeeded", action);
                Ok(true)
            }
            Ok(Some(CacheUploadOutcome::Rejected(reason))) => {
                tracing::info!("Cache upload for `{}` rejected: {:#}", action, reason);
                Ok(false)
            }
            Ok(None) => {
                tracing::info!("Cache upload for `{}` not attempted", action);
                Ok(false)
            }
            Err(error) => {
                if error_on_cache_upload {
                    return Err(error).context("cache_upload");
                } else {
                    tracing::warn!("Cache upload for `{}` failed: {:#}", action, error);
                }
                Ok(false)
            }
        }
    }
}

fn systemtime_to_ttimestamp(time: SystemTime) -> anyhow::Result<TTimestamp> {
    let duration = time.duration_since(SystemTime::UNIX_EPOCH)?;
    Ok(TTimestamp {
        seconds: duration.as_secs().try_into().context("Invalid duration")?,
        // Max 1B so it won't wrap around.
        nanos: duration.subsec_nanos() as _,
        ..Default::default()
    })
}


/*
 * 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::sync::Arc;

use anyhow::Context;
use async_trait::async_trait;
use buck2_common::executor_config::HybridExecutionLevel;
use buck2_common::liveliness_observer::CancelledLivelinessGuard;
use buck2_common::liveliness_observer::LivelinessGuard;
use buck2_common::liveliness_observer::LivelinessObserver;
use buck2_common::liveliness_observer::LivelinessObserverExt;
use buck2_events::dispatch::EventDispatcher;
use buck2_execute::execute::claim::Claim;
use buck2_execute::execute::claim::ClaimManager;
use buck2_execute::execute::claim::MutexClaimManager;
use buck2_execute::execute::manager::CommandExecutionManager;
use buck2_execute::execute::manager::CommandExecutionManagerExt;
use buck2_execute::execute::prepared::PreparedCommand;
use buck2_execute::execute::prepared::PreparedCommandExecutor;
use buck2_execute::execute::request::ExecutorPreference;
use buck2_execute::execute::result::CommandExecutionResult;
use buck2_execute::execute::result::CommandExecutionStatus;
use derivative::Derivative;
use dupe::Dupe;
use futures::future::BoxFuture;
use futures::future::Either;
use futures::future::Future;
use futures::FutureExt;
use host_sharing::HostSharingRequirements;
use more_futures::cancellation::CancellationContext;

use crate::executors::local::LocalExecutor;
use crate::executors::re::ReExecutor;
use crate::low_pass_filter::LowPassFilter;

/// The [HybridExecutor] will accept requests and dispatch them to both a local and remote delegate
/// executor, unless the CommandExecutionRequest expresses a preference. That will allow them to
/// race and whichever claims the request first will get to execute it.
///
/// If the remote executor claims the request but does not produce a successful response, we will
/// enqueue the request again to the local executor.
pub struct HybridExecutor {
    pub local: LocalExecutor,
    pub remote: ReExecutor,
    pub level: HybridExecutionLevel,
    pub executor_preference: ExecutorPreference,
    pub low_pass_filter: Arc<LowPassFilter>,
}

impl HybridExecutor {
    async fn local_exec_cmd(
        &self,
        command: &PreparedCommand<'_, '_>,
        claim_manager: Box<dyn ClaimManager>,
        events: EventDispatcher,
        liveliness_observer: Arc<dyn LivelinessObserver>,
        cancellations: &CancellationContext<'_>,
    ) -> CommandExecutionResult {
        let local_manager =
            CommandExecutionManager::new(claim_manager, events, liveliness_observer);
        self.local
            .exec_cmd(command, local_manager, cancellations)
            .await
    }

    async fn remote_exec_cmd(
        &self,
        command: &PreparedCommand<'_, '_>,
        claim_manager: Box<dyn ClaimManager>,
        events: EventDispatcher,
        liveliness_observer: Arc<dyn LivelinessObserver>,
        cancellations: &CancellationContext<'_>,
    ) -> CommandExecutionResult {
        let remote_manager =
            CommandExecutionManager::new(claim_manager, events, liveliness_observer);
        self.remote
            .exec_cmd(command, remote_manager, cancellations)
            .await
    }

    fn command_executor_preference(
        &self,
        command: &PreparedCommand<'_, '_>,
    ) -> anyhow::Result<ExecutorPreference> {
        self.executor_preference
            .and(command.request.executor_preference())
    }
}

#[async_trait]
impl PreparedCommandExecutor for HybridExecutor {
    async fn exec_cmd(
        &self,
        command: &PreparedCommand<'_, '_>,
        manager: CommandExecutionManager,
        cancellations: &CancellationContext,
    ) -> CommandExecutionResult {
        let executor_preference = self.command_executor_preference(command);

        let executor_preference = match executor_preference {
            Ok(executor_preference) => executor_preference,
            Err(e) => return manager.error("prepare_hybrid", e),
        };

        // inspect that and construct our own result. Especially in the case of secondary fallback,
        // the current approach effectively loses the data from the primary result (for example, we
        // should have stage events the reflect the full duration not just the fallback part).

        // Construct our claim manager and a liveliness guard for local commands. The way this
        // works is as follows: when RE takes a claim, we cancel local commands. This means that we
        // can truly race RE and local: if RE finishes even after local started, it'll cancel the
        // local execution, we'll get back here with a ClaimCancelled from local execution, cancel
        // local's claim, and then resume RE.
        let (local_execution_liveliness_observer, local_execution_liveliness_guard) =
            LivelinessGuard::create();

        // Used to monitor that the RE execution is alive.
        let (remote_execution_liveliness_observer, remote_execution_liveliness_guard) =
            LivelinessGuard::create();

        let claim_manager = MutexClaimManager::new();

        // Note that this only sets up these futures, nothing will happen until they are awaited
        // (this is important in the case where we shouldn't be sending one of them).
        let local_result = self.local_exec_cmd(
            command,
            Box::new(claim_manager.dupe()),
            manager.events.dupe(),
            Arc::new(
                manager
                    .liveliness_observer
                    .dupe()
                    .and(local_execution_liveliness_observer.dupe()),
            ),
            cancellations,
        );

        let remote_result = self.remote_exec_cmd(
            command,
            Box::new(ReClaimManager::new(
                local_execution_liveliness_guard,
                Box::new(claim_manager),
                remote_execution_liveliness_guard,
            )),
            manager.events.dupe(),
            manager.liveliness_observer.dupe(),
            cancellations,
        );

        if executor_preference.requires_local()
            || self.remote.is_action_too_large(command.request.paths())
        {
            return local_result.await;
        };

        if executor_preference.requires_remote() {
            return remote_result.await;
        }

        let jobs = HybridExecutorJobs {
            local: local_result.map(|r| (r, JobPriority(1))),
            remote: remote_result.map(|r| (r, JobPriority(0))),
            executor_preference,
        };

        let (is_limited, fallback_only, fallback_on_failure, low_pass_filter) = match self.level {
            HybridExecutionLevel::Limited => (true, false, false, false),
            HybridExecutionLevel::Fallback {
                fallback_on_failure,
            } => (false, true, fallback_on_failure, false),
            HybridExecutionLevel::Full {
                fallback_on_failure,
                low_pass_filter,
            } => (
                false,
                false,
                fallback_on_failure,
                low_pass_filter && command.request.low_pass_filter(),
            ),
        };

        if is_limited {
            return jobs.into_primary().await.0;
        }

        let weight = match command.request.host_sharing_requirements() {
            HostSharingRequirements::ExclusiveAccess => self.low_pass_filter.capacity(),
            HostSharingRequirements::OnePerToken(.., class) => {
                self.local.host_sharing_broker.requested_permits(class)
            }
            HostSharingRequirements::Shared(class) => {
                self.local.host_sharing_broker.requested_permits(class)
            }
        };

        let is_retryable_status = move |r: &CommandExecutionResult| {
            match &r.report.status {
                // This does need be retried since if we get a cancelled result that would
                // typically mean the other result asked for cancellation and we're about to
                // receive the result here, or it could mean we're being asked to cancel by our
                // caller.
                CommandExecutionStatus::Cancelled => true,
                // If the execution is successful, use the result.
                CommandExecutionStatus::Success { .. } => false,
                // Retry commands that failed (i.e. exit 1) only if we're instructed to do so.
                CommandExecutionStatus::Failure { .. } => fallback_on_failure,
                // Don't retry timeouts. They are used for tests and falling back on a timeout is
                // sort of the opposite of what's been requested.
                CommandExecutionStatus::TimedOut { .. } => false,
                // Errors are infra errors and are always retried because that is the point of
                // falling back.
                CommandExecutionStatus::Error { .. } => true,
            }
        };

        let fallback_only = fallback_only && !command.request.force_full_hybrid_if_capable();

        let ((mut first_res, first_priority), second) =
            if executor_preference.prefers_local() || executor_preference.prefers_remote() {
                // Don't race in this scenario, since this is typically used for
                // actions that are too expensive to run on RE.
                jobs.execute_sequential().await
            } else {
                // In the full-hybrid case, we do race both executors. If the low-pass filter is in
                // use, then we wrap the local execution with that.
                let jobs = if fallback_only {
                    jobs.map_local(move |local| {
                        async move {
                            // Block local until the remote executor aborts (that's remote_execution_liveliness_guard)
                            // The claim actually comes back to us via the execution report so there's no race condition
                            // where local unblocks just when RE finishes
                            remote_execution_liveliness_observer.while_alive().await;
                            local.await
                        }
                        .boxed()
                    })
                } else if low_pass_filter {
                    jobs.map_local(move |local| {
                        async move {
                            // Block local until either condition is met:
                            // - we only have a few actions (that's low_pass_filter)
                            // - the remote executor aborts (that's remote_execution_liveliness_guard)
                            let access = self.low_pass_filter.access(weight);
                            let alive = remote_execution_liveliness_observer.while_alive();
                            futures::pin_mut!(access);
                            futures::pin_mut!(alive);
                            let _guard = futures::future::select(access, alive).await;
                            local.await
                        }
                        .boxed()
                    })
                } else {
                    jobs.map_local(|local| local.boxed())
                };
                jobs.execute_concurrent().await
            };

        let mut res = if is_retryable_status(&first_res) {
            // If the first result had made a claim, then cancel it now to let the other result
            // proceed.
            if let Some(claim) = first_res.report.claim.take() {
                if let Err(e) = claim.release() {
                    return manager.error(
                        "hybrid",
                        e.context("Local execution started executing without a Claim"),
                    );
                }
            }

            let (second_res, second_priority) = second.await;

            // For the purposes of giving users a good UX, if both things failed, give them the
            // local executor's error, which is likely to not have failed because of e.g.
            // sandboxing.
            let (mut primary_res, secondary_res) = if is_retryable_status(&second_res) {
                if first_priority > second_priority {
                    (first_res, second_res)
                } else {
                    (second_res, first_res)
                }
            } else {
                (second_res, first_res)
            };

            primary_res.rejected_execution = Some(secondary_res.report);
            primary_res
        } else {
            // Everyone is happy, we got our result.
            first_res
        };

        res.eligible_for_full_hybrid = !fallback_only;
        res
    }

    fn is_local_execution_possible(&self, executor_preference: ExecutorPreference) -> bool {
        let executor_preference = match self.executor_preference.and(executor_preference) {
            Ok(p) => p,
            Err(..) => return false,
        };
        if executor_preference.requires_remote() {
            return false;
        }
        match self.level {
            HybridExecutionLevel::Limited => !executor_preference.prefers_remote(),
            HybridExecutionLevel::Fallback { .. } | HybridExecutionLevel::Full { .. } => true,
        }
    }
}

struct ReClaimManager {
    inner: Option<ReClaimManagerInner>,
}

impl ReClaimManager {
    fn new(
        local_execution_liveliness_guard: LivelinessGuard,
        claim_manager: Box<dyn ClaimManager>,
        remote_execution_liveliness_guard: LivelinessGuard,
    ) -> Self {
        Self {
            inner: Some(ReClaimManagerInner {
                local_execution_liveliness_guard,
                claim_manager,
                remote_execution_liveliness_guard: Some(remote_execution_liveliness_guard),
            }),
        }
    }
}

impl Drop for ReClaimManager {
    /// If RE decides to not execute, then allow local execution to proceed.
    fn drop(&mut self) {
        if let Some(inner) = self.inner.take() {
            inner.local_execution_liveliness_guard.forget();
        }
    }
}

struct ReClaimManagerInner {
    /// Used to control and potentially cancel the execution of local commands.
    local_execution_liveliness_guard: LivelinessGuard,
    claim_manager: Box<dyn ClaimManager>,

    /// Only kept alive while the ReClaimManager (or its Claim) is alive.
    remote_execution_liveliness_guard: Option<LivelinessGuard>,
}

#[async_trait::async_trait]
impl ClaimManager for ReClaimManager {
    async fn claim(mut self: Box<Self>) -> Box<dyn Claim> {
        let inner = self.inner.take().expect("This is only taken once");

        // Kill in-flight local commands.
        let released_liveliness_guard = inner.local_execution_liveliness_guard.cancel();

        // Ask for the lock. If we never get it then we just exit as that would mean local
        // execution finished.
        let claim = inner.claim_manager.claim().await;

        Box::new(ReClaim {
            released_liveliness_guard,
            claim,
            _remote_execution_liveliness_guard: inner.remote_execution_liveliness_guard,
        })
    }

    fn on_result_delayed(&mut self) {
        if let Some(inner) = &mut self.inner {
            inner.remote_execution_liveliness_guard.take();
        }
    }
}

#[derive(Derivative)]
#[derivative(Debug)] // None of the fields have meaningful debug here.
struct ReClaim {
    #[derivative(Debug = "ignore")]
    released_liveliness_guard: CancelledLivelinessGuard,
    #[derivative(Debug = "ignore")]
    claim: Box<dyn Claim>,

    /// Only used for its lifetime.
    #[derivative(Debug = "ignore")]
    _remote_execution_liveliness_guard: Option<LivelinessGuard>,
}

impl Claim for ReClaim {
    /// Releasing the ReClaim. When we do this, we need to let local execution proceed again. This
    /// can happen if we do a fallback without full hybrid: RE goes, and fails, and then we want to
    /// proceed with local.
    ///
    /// NOTE: this does NOT work in the following scenario:
    ///
    /// - Local claims
    /// - RE has a result, asks local to cancel (successfully)
    /// - RE claims
    /// - RE fails
    ///
    /// In that case, RE will have failed. To fix this, we need local to release its claim instead
    /// of returning ClaimCancelled.
    fn release(self: Box<Self>) -> anyhow::Result<()> {
        // An error here should only occur if local execution had started without the claim.
        self.released_liveliness_guard
            .restore()
            .context("Unable to restore CancelledLivelinessGuard!")?
            .forget();

        self.claim.release()?;

        Ok(())
    }
}

/// The local and remote side of the executor, and a preference to select which future is the
/// primary and which is the secondary.
struct HybridExecutorJobs<L, R> {
    local: L,
    remote: R,
    executor_preference: ExecutorPreference,
}

impl<L, R, O> HybridExecutorJobs<L, R>
where
    L: Future<Output = O>,
    R: Future<Output = O>,
{
    /// Modify the local side future.
    fn map_local<L2>(self, f: impl FnOnce(L) -> L2) -> HybridExecutorJobs<L2, R> {
        HybridExecutorJobs {
            local: f(self.local),
            remote: self.remote,
            executor_preference: self.executor_preference,
        }
    }

    /// Return only the primary future.
    fn into_primary(self) -> Either<L, R> {
        self.into_futures().0
    }

    /// Return both futures, but box them, which makes them Upin and also erases their type.
    fn into_boxfutures<'a>(self) -> (BoxFuture<'a, O>, BoxFuture<'a, O>)
    where
        L: Send + 'a,
        R: Send + 'a,
    {
        let (primary, secondary) = self.into_futures();
        (primary.boxed(), secondary.boxed())
    }

    /// Return both futures.
    fn into_futures(self) -> (Either<L, R>, Either<L, R>) {
        let local = self.local.left_future();
        let remote = self.remote.right_future();

        if self.executor_preference.prefers_local() {
            (local, remote)
        } else {
            (remote, local)
        }
    }

    /// Race both futures, return the first result and the second future.
    async fn execute_concurrent<'a>(self) -> (O, BoxFuture<'a, O>)
    where
        L: Send + 'a,
        R: Send + 'a,
    {
        let (primary, secondary) = self.into_boxfutures();
        let (out, futs) = Either::factor_first(futures::future::select(primary, secondary).await);
        (out, futs.into_inner())
    }

    /// Run primary then return secondary.
    async fn execute_sequential<'a>(self) -> (O, BoxFuture<'a, O>)
    where
        L: Send + 'a,
        R: Send + 'a,
    {
        let (primary, secondary) = self.into_boxfutures();
        (primary.await, secondary)
    }
}

#[derive(PartialOrd, Ord, PartialEq, Eq)]
struct JobPriority(u8);


/*
 * 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::borrow::Cow;
use std::ffi::OsStr;
use std::ffi::OsString;
use std::ops::ControlFlow;
use std::process::Command;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
use std::time::SystemTime;

use anyhow::Context as _;
use async_trait::async_trait;
use buck2_common::file_ops::FileDigestConfig;
use buck2_common::liveliness_observer::LivelinessObserver;
use buck2_common::liveliness_observer::LivelinessObserverExt;
use buck2_common::local_resource_state::LocalResourceHolder;
use buck2_core::fs::artifact_path_resolver::ArtifactFs;
use buck2_core::fs::fs_util;
use buck2_core::fs::paths::abs_norm_path::AbsNormPathBuf;
use buck2_core::fs::paths::abs_path::AbsPath;
use buck2_core::fs::project_rel_path::ProjectRelativePath;
use buck2_core::tag_error;
use buck2_core::tag_result;
use buck2_events::dispatch::get_dispatcher_opt;
use buck2_events::dispatch::EventDispatcher;
use buck2_execute::artifact_value::ArtifactValue;
use buck2_execute::digest_config::DigestConfig;
use buck2_execute::directory::extract_artifact_value;
use buck2_execute::directory::insert_entry;
use buck2_execute::entry::build_entry_from_disk;
use buck2_execute::execute::action_digest::ActionDigest;
use buck2_execute::execute::blocking::BlockingExecutor;
use buck2_execute::execute::clean_output_paths::CleanOutputPaths;
use buck2_execute::execute::environment_inheritance::EnvironmentInheritance;
use buck2_execute::execute::executor_stage_async;
use buck2_execute::execute::inputs_directory::inputs_directory;
use buck2_execute::execute::kind::CommandExecutionKind;
use buck2_execute::execute::manager::CommandExecutionManager;
use buck2_execute::execute::manager::CommandExecutionManagerExt;
use buck2_execute::execute::manager::CommandExecutionManagerWithClaim;
use buck2_execute::execute::output::CommandStdStreams;
use buck2_execute::execute::prepared::PreparedCommand;
use buck2_execute::execute::prepared::PreparedCommandExecutor;
use buck2_execute::execute::request::CommandExecutionInput;
use buck2_execute::execute::request::CommandExecutionOutput;
use buck2_execute::execute::request::CommandExecutionOutputRef;
use buck2_execute::execute::request::CommandExecutionRequest;
use buck2_execute::execute::request::ExecutorPreference;
use buck2_execute::execute::result::CommandExecutionMetadata;
use buck2_execute::execute::result::CommandExecutionResult;
use buck2_execute::knobs::ExecutorGlobalKnobs;
use buck2_execute::materialize::materializer::MaterializationError;
use buck2_execute::materialize::materializer::Materializer;
use buck2_forkserver::client::ForkserverClient;
use buck2_forkserver::run::gather_output;
use buck2_forkserver::run::maybe_absolutize_exe;
use buck2_forkserver::run::timeout_into_cancellation;
use buck2_forkserver::run::GatherOutputStatus;
use buck2_util::process::background_command;
use derive_more::From;
use dupe::Dupe;
use futures::future;
use futures::future::select;
use futures::future::FutureExt;
use futures::stream::StreamExt;
use gazebo::prelude::*;
use host_sharing::HostSharingBroker;
use indexmap::IndexMap;
use more_futures::cancellable_future::CancellationObserver;
use more_futures::cancellation::CancellationContext;
use thiserror::Error;
use tracing::info;

use crate::executors::worker::WorkerHandle;
use crate::executors::worker::WorkerPool;

#[derive(Debug, Error)]
enum LocalExecutionError {
    #[error("Args list was empty")]
    NoArgs,

    #[error("Trying to execute a remote-only action on a local executor")]
    RemoteOnlyAction,
}

#[derive(Clone)]
pub struct LocalExecutor {
    artifact_fs: ArtifactFs,
    materializer: Arc<dyn Materializer>,
    blocking_executor: Arc<dyn BlockingExecutor>,
    pub(crate) host_sharing_broker: Arc<HostSharingBroker>,
    root: AbsNormPathBuf,
    #[cfg_attr(not(unix), allow(unused))]
    forkserver: Option<ForkserverClient>,
    #[allow(unused)]
    knobs: ExecutorGlobalKnobs,
    #[allow(unused)]
    worker_pool: Option<Arc<WorkerPool>>,
}

impl LocalExecutor {
    pub fn new(
        artifact_fs: ArtifactFs,
        materializer: Arc<dyn Materializer>,
        blocking_executor: Arc<dyn BlockingExecutor>,
        host_sharing_broker: Arc<HostSharingBroker>,
        root: AbsNormPathBuf,
        forkserver: Option<ForkserverClient>,
        knobs: ExecutorGlobalKnobs,
        worker_pool: Option<Arc<WorkerPool>>,
    ) -> Self {
        Self {
            artifact_fs,
            materializer,
            blocking_executor,
            host_sharing_broker,
            root,
            forkserver,
            knobs,
            worker_pool,
        }
    }

    // Compiler gets confused (on the not(unix) branch only, weirdly) if you use an async fn.
    #[allow(clippy::manual_async_fn)]
    fn exec<'a>(
        &'a self,
        exe: &'a str,
        args: impl IntoIterator<Item = impl AsRef<OsStr> + Send> + Send + 'a,
        env: impl IntoIterator<Item = (impl AsRef<OsStr> + Send, impl AsRef<OsStr> + Send)> + Send + 'a,
        working_directory: Option<&'a ProjectRelativePath>,
        timeout: Option<Duration>,
        env_inheritance: Option<&'a EnvironmentInheritance>,
        liveliness_observer: impl LivelinessObserver + 'static,
        disable_miniperf: bool,
    ) -> impl futures::future::Future<
        Output = anyhow::Result<(GatherOutputStatus, Vec<u8>, Vec<u8>)>,
    > + Send
    + 'a {
        async move {
            let working_directory = match working_directory {
                Some(d) => Cow::Owned(self.root.join(d)),
                None => Cow::Borrowed(&self.root),
            };

            match &self.forkserver {
                Some(forkserver) => {
                    #[cfg(unix)]
                    {
                        unix::exec_via_forkserver(
                            forkserver,
                            exe,
                            args,
                            env,
                            &working_directory,
                            timeout,
                            env_inheritance,
                            liveliness_observer,
                            self.knobs.enable_miniperf && !disable_miniperf,
                        )
                        .await
                    }

                    #[cfg(not(unix))]
                    {
                        let _unused = (forkserver, disable_miniperf);
                        Err(anyhow::anyhow!("Forkserver is not supported off-UNIX"))
                    }
                }

                None => {
                    let exe = maybe_absolutize_exe(exe, &working_directory)?;
                    let mut cmd = background_command(exe.as_ref());
                    cmd.current_dir(working_directory.as_path());
                    cmd.args(args);
                    apply_local_execution_environment(
                        &mut cmd,
                        &working_directory,
                        env,
                        env_inheritance,
                    );
                    let timeout = timeout_into_cancellation(timeout);

                    let alive = liveliness_observer
                        .while_alive()
                        .map(|()| anyhow::Ok(GatherOutputStatus::Cancelled));

                    let cancellation =
                        select(timeout.boxed(), alive.boxed()).map(|r| r.factor_first().0);

                    gather_output(cmd, cancellation).await
                }
                .with_context(|| format!("Failed to gather output from command: {}", exe)),
            }
        }
    }

    async fn exec_request(
        &self,
        action_digest: &ActionDigest,
        request: &CommandExecutionRequest,
        manager: CommandExecutionManager,
        cancellation: CancellationObserver,
        cancellations: &CancellationContext<'_>,
        digest_config: DigestConfig,
        local_resource_holders: &[LocalResourceHolder],
    ) -> CommandExecutionResult {
        let args = &request.all_args_vec();
        if args.is_empty() {
            return manager.error("no_args", LocalExecutionError::NoArgs);
        }

        let input_materialization_duration = match executor_stage_async(
            buck2_data::LocalStage {
                stage: Some(buck2_data::LocalMaterializeInputs {}.into()),
            },
            async {
                let start = Instant::now();

                let (r1, r2) = future::join(
                    async {
                        materialize_inputs(&self.artifact_fs, &self.materializer, request).await
                    },
                    async {
                        // When user requests to not perform a cleanup for a specific action
                        // output from previous run of that action could actually be used as the
                        // input during current run (e.g. extra output which is an incremental state describing the actual output).
                        if !request.outputs_cleanup {
                            materialize_build_outputs_from_previous_run(
                                &self.artifact_fs,
                                &self.materializer,
                                request,
                            )
                            .await
                        } else {
                            Ok(())
                        }
                    },
                )
                .await;

                r1.and(r2)?;
                anyhow::Ok(start.elapsed())
            },
        )
        .await
        {
            Ok(input_materialization_duration) => input_materialization_duration,
            Err(e) => return manager.error("materialize_inputs_failed", e),
        };

        // TODO: Release here.
        let manager = manager.claim().await;

        let scratch_dir = request.custom_tmpdir().as_ref().map(|tmpdir| {
            self.artifact_fs
                .buck_out_path_resolver()
                .resolve_scratch(tmpdir)
        });

        let scratch_dir = &scratch_dir; // So it doesn't move in the block below.

        if let Err(e) = executor_stage_async(
            buck2_data::LocalStage {
                stage: Some(buck2_data::LocalPrepareOutputDirs {}.into()),
            },
            async move {
                // TODO(cjhopman): This should be getting the action exec context so it get use io_blocking_section
                if let Some(scratch_dir) = scratch_dir {
                    let project_fs = self.artifact_fs.fs();
                    project_fs.remove_path_recursive(scratch_dir)?;
                    fs_util::create_dir_all(&*project_fs.resolve(scratch_dir))?;
                }

                create_output_dirs(
                    &self.artifact_fs,
                    request,
                    self.materializer.dupe(),
                    self.blocking_executor.dupe(),
                    cancellations,
                )
                .await
                .context("Error creating output directories")?;

                anyhow::Ok(())
            },
        )
        .await
        {
            return manager.error("prepare_output_dirs_failed", e);
        };

        info!(
            "Local execution command line:\n```\n$ {}\n```",
            args.join(" "),
        );

        let scratch_dir_abs;

        let tmpdirs = if let Some(scratch_dir) = scratch_dir {
            // For the $TMPDIR - important it is absolute
            scratch_dir_abs = self.artifact_fs.fs().resolve(scratch_dir);

            if cfg!(windows) {
                const MAX_PATH: usize = 260;
                if scratch_dir_abs.as_os_str().len() > MAX_PATH {
                    return manager.error(
                        "scratch_dir_too_long",
                        anyhow::anyhow!(
                            "Scratch directory path is longer than MAX_PATH: {}",
                            scratch_dir_abs
                        ),
                    );
                }
                vec![
                    ("TEMP", scratch_dir_abs.as_os_str()),
                    ("TMP", scratch_dir_abs.as_os_str()),
                ]
            } else {
                vec![("TMPDIR", scratch_dir_abs.as_os_str())]
            }
        } else {
            vec![]
        };

        let local_resource_env_vars: Vec<(&str, StrOrOsStr)> = local_resource_holders
            .iter()
            .flat_map(|h| {
                h.as_ref().0.iter().map(|env_var| {
                    (
                        env_var.key.as_str(),
                        StrOrOsStr::from(env_var.value.as_str()),
                    )
                })
            })
            .collect();

        let daemon_uuid: &str = &buck2_events::daemon_id::DAEMON_UUID.to_string();
        let dispatcher = match get_dispatcher_opt() {
            Some(dispatcher) => dispatcher,
            None => {
                return manager.error("no_dispatcher", anyhow::anyhow!("No dispatcher available"));
            }
        };
        let build_id: &str = &dispatcher.trace_id().to_string();

        let iter_env = || {
            tmpdirs
                .iter()
                .map(|(k, v)| (*k, StrOrOsStr::from(*v)))
                .chain(
                    request
                        .env()
                        .iter()
                        .map(|(k, v)| (k.as_str(), StrOrOsStr::from(v.as_str()))),
                )
                .chain(local_resource_env_vars.iter().cloned())
                .chain(std::iter::once((
                    "BUCK2_DAEMON_UUID",
                    StrOrOsStr::from(daemon_uuid),
                )))
                .chain(std::iter::once((
                    "BUCK_BUILD_ID",
                    StrOrOsStr::from(build_id),
                )))
        };
        let liveliness_observer = manager.liveliness_observer.dupe().and(cancellation);

        let (worker, manager) = self.initialize_worker(request, manager, dispatcher).await?;

        let execution_kind = match worker {
            None => CommandExecutionKind::Local {
                digest: action_digest.dupe(),
                command: args.to_vec(),
                env: request.env().clone(),
            },
            Some(_) => CommandExecutionKind::LocalWorker {
                digest: action_digest.dupe(),
                command: request.args().to_vec(),
                env: request.env().clone(),
                fallback_exe: request.exe().to_vec(),
            },
        };

        let (mut timing, res) = executor_stage_async(
            {
                let env = iter_env()
                    .map(|(k, v)| buck2_data::EnvironmentEntry {
                        key: k.to_owned(),
                        value: v.into_string_lossy(),
                    })
                    .collect();
                let stage = match worker {
                    None => buck2_data::LocalExecute {
                        command: Some(buck2_data::LocalCommand {
                            action_digest: action_digest.to_string(),
                            argv: args.to_vec(),
                            env,
                        }),
                    }
                    .into(),
                    Some(_) => buck2_data::WorkerExecute {
                        command: Some(buck2_data::WorkerCommand {
                            action_digest: action_digest.to_string(),
                            argv: request.args().to_vec(),
                            env,
                            fallback_exe: request.exe().to_vec(),
                        }),
                    }
                    .into(),
                };
                buck2_data::LocalStage { stage: Some(stage) }
            },
            async move {
                let execution_start = Instant::now();
                let start_time = SystemTime::now();

                let env = iter_env().map(|(k, v)| (k, v.into_os_str()));
                let r = if let Some(worker) = worker {
                    let env: Vec<(OsString, OsString)> = env
                        .into_iter()
                        .map(|(k, v)| (OsString::from(k), v.to_owned()))
                        .collect();
                    Ok(worker.exec_cmd(request.args(), env).await)
                } else {
                    self.exec(
                        &args[0],
                        &args[1..],
                        env,
                        request.working_directory(),
                        request.timeout(),
                        request.local_environment_inheritance(),
                        liveliness_observer,
                        request.disable_miniperf(),
                    )
                    .await
                };

                let execution_time = execution_start.elapsed();

                let timing = CommandExecutionMetadata {
                    wall_time: execution_time,
                    re_queue_time: None,
                    execution_time,
                    start_time,
                    execution_stats: None, // We fill this in later if available.
                    input_materialization_duration,
                };

                (timing, r)
            },
        )
        .await;

        let (status, stdout, stderr) = match res {
            Ok(res) => res,
            Err(e) => return manager.error("exec_failed", e), // TODO (torozco): Can this take CommandExecutionKind? Should this be a failure?
        };

        let std_streams = CommandStdStreams::Local { stdout, stderr };

        match status {
            GatherOutputStatus::Finished {
                exit_code,
                execution_stats,
            } => {
                let outputs = match self
                    .calculate_and_declare_output_values(request, digest_config)
                    .await
                {
                    Ok(output_values) => output_values,
                    Err(e) => return manager.error("calculate_output_values_failed", e),
                };

                timing.execution_stats = execution_stats;

                if exit_code == 0 {
                    manager.success(execution_kind, outputs, std_streams, timing)
                } else {
                    let manager = check_inputs(
                        manager,
                        &self.artifact_fs,
                        self.blocking_executor.as_ref(),
                        request,
                    )
                    .await?;

                    manager.failure(
                        execution_kind,
                        outputs,
                        std_streams,
                        Some(exit_code),
                        timing,
                    )
                }
            }
            GatherOutputStatus::SpawnFailed(reason) => {
                let manager = check_inputs(
                    manager,
                    &self.artifact_fs,
                    self.blocking_executor.as_ref(),
                    request,
                )
                .await?;

                // We are lying about the std streams here because we don't have a good mechanism
                // to report that the command does not exist, and because that's exactly what RE
                // also does when this happens.
                manager.failure(
                    execution_kind,
                    Default::default(),
                    CommandStdStreams::Local {
                        stdout: Default::default(),
                        stderr: format!("Spawning executable `{}` failed: {}", args[0], reason)
                            .into_bytes(),
                    },
                    None,
                    timing,
                )
            }
            GatherOutputStatus::TimedOut(duration) => {
                manager.timeout(execution_kind, duration, std_streams, timing)
            }
            GatherOutputStatus::Cancelled => manager.cancel_claim(),
        }
    }

    async fn calculate_and_declare_output_values(
        &self,
        request: &CommandExecutionRequest,
        digest_config: DigestConfig,
    ) -> anyhow::Result<IndexMap<CommandExecutionOutput, ArtifactValue>> {
        let mut builder = inputs_directory(request.inputs(), &self.artifact_fs)?;

        // Read outputs from disk and add them to the builder
        let mut entries = Vec::new();
        for output in request.outputs() {
            let path = output.resolve(&self.artifact_fs).into_path();
            let abspath = self.root.join(&path);
            let entry = build_entry_from_disk(
                abspath,
                FileDigestConfig::build(digest_config.cas_digest_config()),
            )
            .with_context(|| format!("collecting output {:?}", path))?;
            if let Some(entry) = entry {
                insert_entry(&mut builder, &path, entry)?;
                entries.push((output.cloned(), path));
            }
        }

        let mut to_declare = vec![];
        let mut mapped_outputs = IndexMap::with_capacity(entries.len());

        for (output, path) in entries {
            let value = extract_artifact_value(&builder, &path, digest_config)?;
            if let Some(value) = value {
                match output {
                    CommandExecutionOutput::BuildArtifact { .. } => {
                        to_declare.push((path, value.dupe()));
                    }
                    CommandExecutionOutput::TestPath { .. } => {
                        // Don't declare those as we don't currently have any form of GC so this
                        // would take up space for nothing, and most importantly, we will never
                        // need them to be in materializer state for e.g. matching as nothing
                        // should depend on them.
                    }
                }

                mapped_outputs.insert(output, value);
            }
        }

        self.materializer.declare_existing(to_declare).await?;

        Ok(mapped_outputs)
    }

    async fn initialize_worker(
        &self,
        request: &CommandExecutionRequest,
        manager: CommandExecutionManagerWithClaim,
        dispatcher: EventDispatcher,
    ) -> ControlFlow<
        CommandExecutionResult,
        (Option<Arc<WorkerHandle>>, CommandExecutionManagerWithClaim),
    > {
        if let (Some(worker_spec), Some(worker_pool), Some(forkserver), true) = (
            request.worker(),
            self.worker_pool.dupe(),
            self.forkserver.dupe(),
            cfg!(unix),
        ) {
            match executor_stage_async(
                {
                    let stage = buck2_data::InitializeWorker {
                        command: Some(buck2_data::WorkerInitCommand {
                            argv: worker_spec.exe.clone(),
                            env: request
                                .env()
                                .iter()
                                .map(|(k, v)| buck2_data::EnvironmentEntry {
                                    key: k.to_owned(),
                                    value: v.to_owned(),
                                })
                                .collect(),
                        }),
                    };
                    buck2_data::LocalStage {
                        stage: Some(stage.into()),
                    }
                },
                async move {
                    // TODO(ctolliday - T155351378) set worker specific env via WorkerInfo, not from the action
                    let env = request
                        .env()
                        .iter()
                        .map(|(k, v)| (OsString::from(k), OsString::from(v)));
                    worker_pool
                        .get_or_create_worker(
                            worker_spec,
                            env,
                            &self.root,
                            forkserver.clone(),
                            dispatcher,
                        )
                        .await
                },
            )
            .await
            {
                Ok(worker) => ControlFlow::Continue((Some(worker), manager)),
                Err(e) => {
                    let res = {
                        let manager = check_inputs(
                            manager,
                            &self.artifact_fs,
                            self.blocking_executor.as_ref(),
                            request,
                        )
                        .await?;

                        e.to_command_execution_result(request, manager)
                    };
                    ControlFlow::Break(res)
                }
            }
        } else {
            ControlFlow::Continue((None, manager))
        }
    }
}

#[async_trait]
impl PreparedCommandExecutor for LocalExecutor {
    async fn exec_cmd(
        &self,
        command: &PreparedCommand<'_, '_>,
        manager: CommandExecutionManager,
        cancellations: &CancellationContext,
    ) -> CommandExecutionResult {
        if command.request.executor_preference().requires_remote() {
            return manager.error("local_prepare", LocalExecutionError::RemoteOnlyAction);
        }

        let PreparedCommand {
            request,
            target: _,
            prepared_action,
            digest_config,
        } = command;

        let local_resource_holders = executor_stage_async(
            {
                let a = buck2_data::AcquireLocalResource {};
                buck2_data::LocalStage {
                    stage: Some(a.into()),
                }
            },
            async move {
                let mut holders = vec![];
                // Acquire resources in a sorted way to avoid deadlock.
                // It might happen if 2 tests both requiring resources A and B are run simultaneously and there is only 1 instance of resource per type.
                // If tests are not acquiring them in a sorted way the following situation might happen:
                // Test 1 acquires resource B and test 2 acquires resource A.
                // Now test 1 is waiting on resource B and test 2 is waiting on resource A.
                for r in request.required_local_resources() {
                    holders.push(r.acquire_resource().await);
                }
                holders
            },
        )
        .await;

        let _permit = executor_stage_async(
            buck2_data::LocalStage {
                stage: Some(buck2_data::LocalQueued {}.into()),
            },
            self.host_sharing_broker
                .acquire(request.host_sharing_requirements()),
        )
        .await;

        // If we start running something, we don't want this task to get dropped, because if we do
        // we might interfere with e.g. clean up.
        cancellations
            .with_structured_cancellation(|cancellation| {
                Self::exec_request(
                    self,
                    &prepared_action.action,
                    request,
                    manager,
                    cancellation,
                    cancellations,
                    *digest_config,
                    &local_resource_holders,
                )
            })
            .await
    }

    fn is_local_execution_possible(&self, _executor_preference: ExecutorPreference) -> bool {
        true
    }
}

/// Either a str or a OsStr, so that we can turn it back into a String without having to check for
/// valid utf-8, while using the same struct.
#[derive(Copy, Clone, Dupe, From)]
enum StrOrOsStr<'a> {
    Str(&'a str),
    OsStr(&'a OsStr),
}

impl<'a> StrOrOsStr<'a> {
    fn into_string_lossy(self) -> String {
        match self {
            Self::Str(s) => s.to_owned(),
            Self::OsStr(s) => s.to_string_lossy().into_owned(),
        }
    }

    fn into_os_str(self) -> &'a OsStr {
        match self {
            Self::Str(s) => OsStr::new(s),
            Self::OsStr(s) => s,
        }
    }
}

/// Materialize all inputs artifact for CommandExecutionRequest so the command can be executed locally.
pub async fn materialize_inputs(
    artifact_fs: &ArtifactFs,
    materializer: &Arc<dyn Materializer>,
    request: &CommandExecutionRequest,
) -> anyhow::Result<()> {
    let mut paths = vec![];

    for input in request.inputs() {
        match input {
            CommandExecutionInput::Artifact(group) => {
                for (artifact, _) in group.iter() {
                    if !artifact.is_source() {
                        paths.push(artifact.resolve_path(artifact_fs)?);
                    }
                }
            }
            CommandExecutionInput::ActionMetadata(metadata) => {
                let path = artifact_fs
                    .buck_out_path_resolver()
                    .resolve_gen(&metadata.path);
                CleanOutputPaths::clean(std::iter::once(path.as_ref()), artifact_fs.fs())?;
                artifact_fs.fs().write_file(&path, &metadata.data, false)?;
            }
        }
    }

    let mut stream = materializer.materialize_many(paths).await?;
    while let Some(res) = stream.next().await {
        match res {
            Ok(()) => {}
            Err(MaterializationError::NotFound { path, info, debug }) => {
                let corrupted = info.origin.guaranteed_by_action_cache();

                return Err(tag_error!(
                    "cas_missing_fatal",
                    MaterializationError::NotFound { path, info, debug }.into(),
                    quiet: true,
                    task: false,
                    daemon_in_memory_state_is_corrupted: true,
                    action_cache_is_corrupted: corrupted
                ));
            }
            Err(e) => {
                return Err(e.into());
            }
        }
    }

    Ok(())
}

async fn check_inputs(
    manager: CommandExecutionManagerWithClaim,
    artifact_fs: &ArtifactFs,
    blocking_executor: &dyn BlockingExecutor,
    request: &CommandExecutionRequest,
) -> ControlFlow<CommandExecutionResult, CommandExecutionManagerWithClaim> {
    let res = blocking_executor
        .execute_io_inline(|| {
            for input in request.inputs() {
                match input {
                    CommandExecutionInput::Artifact(group) => {
                        for (artifact, _) in group.iter() {
                            if !artifact.is_source() {
                                let path = artifact.resolve_path(artifact_fs)?;
                                let abs_path = artifact_fs.fs().resolve(&path);

                                // We ignore the result here because while we want to tag it, we'd
                                // prefer to just show the normal error to the user, so we don't
                                // want to propagate it.
                                let _ignored = tag_result!(
                                    "missing_local_inputs",
                                    fs_util::symlink_metadata(&abs_path).context("Missing input"),
                                    quiet: true,
                                    task: false,
                                    daemon_materializer_state_is_corrupted: true
                                );
                            }
                        }
                    }
                    CommandExecutionInput::ActionMetadata(..) => {
                        // Ignore those here.
                    }
                }
            }

            Ok(())
        })
        .await;

    match res {
        Ok(()) => ControlFlow::Continue(manager),
        Err(err) => ControlFlow::Break(manager.error("local_check_inputs", err)),
    }
}

/// Materialize build outputs from the previous run of the same command.
/// Useful when executing incremental actions first remotely and then locally.
/// In that case output from remote execution which is incremental state should be materialized prior local execution.
/// Such incremental state in fact serves as the input while being output as well.
pub async fn materialize_build_outputs_from_previous_run(
    artifact_fs: &ArtifactFs,
    materializer: &Arc<dyn Materializer>,
    request: &CommandExecutionRequest,
) -> anyhow::Result<()> {
    let mut paths = vec![];

    for output in request.outputs() {
        match output {
            CommandExecutionOutputRef::BuildArtifact {
                path,
                output_type: _,
            } => {
                paths.push(artifact_fs.resolve_build(path));
            }
            CommandExecutionOutputRef::TestPath { path: _, create: _ } => {}
        }
    }

    materializer.ensure_materialized(paths).await
}

/// Create any output dirs requested by the command. Note that this makes no effort to delete
/// the output paths first. Eventually it should, but right now this happens earlier. This
/// would be a separate refactor.
pub async fn create_output_dirs(
    artifact_fs: &ArtifactFs,
    request: &CommandExecutionRequest,
    materializer: Arc<dyn Materializer>,
    blocking_executor: Arc<dyn BlockingExecutor>,
    cancellations: &CancellationContext<'_>,
) -> anyhow::Result<()> {
    let outputs: Vec<_> = request
        .outputs()
        .map(|output| output.resolve(artifact_fs))
        .collect();

    // Invalidate all the output paths this action might provide. Note that this is a bit
    // approximative: we might have previous instances of this action that declared
    // different outputs with a different materialization method that will become invalid
    // now. However, nothing should reference those stale outputs, so while this does not
    // do a good job of cleaning up garbage, it prevents using invalid artifacts.
    let output_paths = outputs.map(|output| output.path.to_owned());
    materializer.invalidate_many(output_paths.clone()).await?;

    if request.outputs_cleanup {
        // TODO(scottcao): Move this deletion logic into materializer itself.
        // Use Eden's clean up API if possible, it is significantly faster on Eden compared with
        // the native method as the API does not load and materialize files or folders
        if let Some(eden_buck_out) = materializer.eden_buck_out() {
            eden_buck_out
                .remove_paths_recursive(artifact_fs.fs(), output_paths, cancellations)
                .await?;
        } else {
            blocking_executor
                .execute_io(
                    Box::new(CleanOutputPaths {
                        paths: output_paths,
                    }),
                    cancellations,
                )
                .await
                .context("Failed to cleanup output directory")?;
        }
    }

    let project_fs = artifact_fs.fs();
    for output in outputs {
        if let Some(path) = output.path_to_create() {
            fs_util::create_dir_all(project_fs.resolve(path))?;
        }
    }

    Ok(())
}

pub fn apply_local_execution_environment(
    builder: &mut impl EnvironmentBuilder,
    working_directory: &AbsPath,
    env: impl IntoIterator<Item = (impl AsRef<OsStr>, impl AsRef<OsStr>)>,
    env_inheritance: Option<&EnvironmentInheritance>,
) {
    if let Some(env_inheritance) = env_inheritance {
        if env_inheritance.clear() {
            builder.clear();
        }

        for key in env_inheritance.exclusions() {
            builder.remove(key);
        }

        for (key, val) in env_inheritance.values() {
            builder.set(key, val);
        }
    }
    for (key, val) in env {
        builder.set(key, val);
    }
    builder.set("PWD", working_directory.as_path());
}

pub trait EnvironmentBuilder {
    fn clear(&mut self);

    fn set<K, V>(&mut self, key: K, val: V)
    where
        K: AsRef<OsStr>,
        V: AsRef<OsStr>;

    fn remove<K>(&mut self, key: K)
    where
        K: AsRef<OsStr>;
}

impl EnvironmentBuilder for Command {
    fn clear(&mut self) {
        Command::env_clear(self);
    }

    fn set<K, V>(&mut self, key: K, val: V)
    where
        K: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        Command::env(self, key, val);
    }

    fn remove<K>(&mut self, key: K)
    where
        K: AsRef<OsStr>,
    {
        Command::env_remove(self, key);
    }
}

#[cfg(unix)]
mod unix {
    use std::os::unix::ffi::OsStrExt;

    use buck2_execute::execute::environment_inheritance::EnvironmentInheritance;

    use super::*;

    pub async fn exec_via_forkserver(
        forkserver: &ForkserverClient,
        exe: impl AsRef<OsStr>,
        args: impl IntoIterator<Item = impl AsRef<OsStr>>,
        env: impl IntoIterator<Item = (impl AsRef<OsStr>, impl AsRef<OsStr>)>,
        working_directory: &AbsPath,
        command_timeout: Option<Duration>,
        env_inheritance: Option<&EnvironmentInheritance>,
        liveliness_observer: impl LivelinessObserver + 'static,
        enable_miniperf: bool,
    ) -> anyhow::Result<(GatherOutputStatus, Vec<u8>, Vec<u8>)> {
        let exe = exe.as_ref();

        let mut req = buck2_forkserver_proto::CommandRequest {
            exe: exe.as_bytes().to_vec(),
            argv: args
                .into_iter()
                .map(|s| s.as_ref().as_bytes().to_vec())
                .collect(),
            cwd: Some(buck2_forkserver_proto::WorkingDirectory {
                path: working_directory.as_path().as_os_str().as_bytes().to_vec(),
            }),
            env: vec![],
            timeout: command_timeout.try_map(|d| d.try_into())?,
            enable_miniperf,
            std_redirects: None,
        };
        apply_local_execution_environment(&mut req, working_directory, env, env_inheritance);
        forkserver
            .execute(req, async move { liveliness_observer.while_alive().await })
            .await
    }

    trait CommandRequestExt {
        fn push_env_directive<D>(&mut self, directive: D)
        where
            D: Into<buck2_forkserver_proto::env_directive::Data>;
    }

    impl CommandRequestExt for buck2_forkserver_proto::CommandRequest {
        fn push_env_directive<D>(&mut self, directive: D)
        where
            D: Into<buck2_forkserver_proto::env_directive::Data>,
        {
            self.env.push(buck2_forkserver_proto::EnvDirective {
                data: Some(directive.into()),
            });
        }
    }

    impl EnvironmentBuilder for buck2_forkserver_proto::CommandRequest {
        fn clear(&mut self) {
            self.push_env_directive(buck2_forkserver_proto::EnvClear {});
        }

        fn set<K, V>(&mut self, key: K, val: V)
        where
            K: AsRef<OsStr>,
            V: AsRef<OsStr>,
        {
            self.push_env_directive(buck2_forkserver_proto::EnvSet {
                key: key.as_ref().as_bytes().to_vec(),
                value: val.as_ref().as_bytes().to_vec(),
            })
        }

        fn remove<K>(&mut self, key: K)
        where
            K: AsRef<OsStr>,
        {
            self.push_env_directive(buck2_forkserver_proto::EnvRemove {
                key: key.as_ref().as_bytes().to_vec(),
            })
        }
    }
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;
    use std::str;
    use std::sync::Arc;
    use std::time::Instant;

    use buck2_common::liveliness_observer::NoopLivelinessObserver;
    use buck2_core::cells::cell_root_path::CellRootPathBuf;
    use buck2_core::cells::name::CellName;
    use buck2_core::cells::CellResolver;
    use buck2_core::fs::artifact_path_resolver::ArtifactFs;
    use buck2_core::fs::buck_out_path::BuckOutPathResolver;
    use buck2_core::fs::project::ProjectRoot;
    use buck2_core::fs::project::ProjectRootTemp;
    use buck2_core::fs::project_rel_path::ProjectRelativePathBuf;
    use buck2_execute::execute::blocking::testing::DummyBlockingExecutor;
    use buck2_execute::materialize::nodisk::NoDiskMaterializer;
    use host_sharing::HostSharingStrategy;

    use super::*;

    #[tokio::test]
    async fn test_gather_output() -> anyhow::Result<()> {
        let mut cmd = if cfg!(windows) {
            background_command("powershell")
        } else {
            background_command("sh")
        };
        cmd.args(["-c", "echo hello"]);

        let (status, stdout, stderr) = gather_output(cmd, futures::future::pending()).await?;
        assert!(matches!(status, GatherOutputStatus::Finished{ exit_code, .. } if exit_code == 0));
        assert_eq!(str::from_utf8(&stdout)?.trim(), "hello");
        assert_eq!(stderr, b"");

        Ok(())
    }

    #[tokio::test]
    async fn test_gather_does_not_wait_for_children() -> anyhow::Result<()> {
        // If we wait for sleep, this will time out.
        let mut cmd = if cfg!(windows) {
            background_command("powershell")
        } else {
            background_command("sh")
        };
        if cfg!(windows) {
            cmd.args([
                "-c",
                "Start-Job -ScriptBlock {sleep 10} | Out-Null; echo hello",
            ]);
        } else {
            cmd.args(["-c", "(sleep 10 &) && echo hello"]);
        }

        let timeout = if cfg!(windows) { 9 } else { 1 };
        let (status, stdout, stderr) = gather_output(
            cmd,
            timeout_into_cancellation(Some(Duration::from_secs(timeout))),
        )
        .await?;
        assert!(
            matches!(status, GatherOutputStatus::Finished { exit_code, .. } if exit_code == 0),
            "status: {:?}",
            status
        );
        assert_eq!(str::from_utf8(&stdout)?.trim(), "hello");
        assert_eq!(stderr, b"");

        Ok(())
    }

    #[tokio::test]
    async fn test_gather_output_timeout() -> anyhow::Result<()> {
        let now = Instant::now();

        let mut cmd = if cfg!(windows) {
            background_command("powershell")
        } else {
            background_command("sh")
        };
        cmd.args(["-c", "echo hello; sleep 10; echo bye"]);

        let timeout = if cfg!(windows) { 5 } else { 1 };
        let (status, stdout, stderr) = gather_output(
            cmd,
            timeout_into_cancellation(Some(Duration::from_secs(timeout))),
        )
        .await?;
        assert!(
            matches!(status, GatherOutputStatus::TimedOut(..)),
            "status: {:?}",
            status
        );
        assert_eq!(str::from_utf8(&stdout)?.trim(), "hello");
        assert_eq!(stderr, b"");

        assert!(now.elapsed() < Duration::from_secs(9)); // Lots of leeway here.

        Ok(())
    }

    fn artifact_fs(project_fs: ProjectRoot) -> ArtifactFs {
        ArtifactFs::new(
            CellResolver::testing_with_name_and_path(
                CellName::testing_new("cell"),
                CellRootPathBuf::new(ProjectRelativePathBuf::unchecked_new("cell_path".into())),
            ),
            BuckOutPathResolver::new(ProjectRelativePathBuf::unchecked_new("buck_out/v2".into())),
            project_fs,
        )
    }

    fn test_executor() -> anyhow::Result<(LocalExecutor, AbsNormPathBuf, ProjectRootTemp)> {
        let temp = ProjectRootTemp::new().unwrap();
        let project_fs = temp.path();
        let artifact_fs = artifact_fs(project_fs.dupe());

        let executor = LocalExecutor::new(
            artifact_fs,
            Arc::new(NoDiskMaterializer),
            Arc::new(DummyBlockingExecutor {
                fs: project_fs.dupe(),
            }),
            Arc::new(HostSharingBroker::new(
                HostSharingStrategy::SmallerTasksFirst,
                1,
            )),
            temp.path().root().to_buf(),
            None,
            ExecutorGlobalKnobs::default(),
            None,
        );

        Ok((executor, temp.path().root().to_buf(), temp))
    }

    #[tokio::test]
    async fn test_exec_cmd_environment() -> anyhow::Result<()> {
        let (executor, root, _tmpdir) = test_executor()?;

        let interpreter = if cfg!(windows) { "powershell" } else { "sh" };
        let (status, stdout, _) = executor
            .exec(
                interpreter,
                ["-c", "echo $PWD; pwd"],
                &HashMap::<String, String>::default(),
                None,
                None,
                None,
                NoopLivelinessObserver::create(),
                false,
            )
            .await?;
        assert!(matches!(status, GatherOutputStatus::Finished { exit_code, .. } if exit_code == 0));

        let stdout = std::str::from_utf8(&stdout).context("Invalid stdout")?;

        if cfg!(windows) {
            let lines: Vec<&str> = stdout.split("\r\n").collect();
            let expected_path = format!("{}", root);

            assert_eq!(lines[3], expected_path);
            assert_eq!(lines[4], expected_path);
        } else {
            assert_eq!(stdout, format!("{}\n{}\n", root, root));
        }

        Ok(())
    }

    #[cfg(unix)] // TODO: something similar on Windows: T123279320
    #[tokio::test]
    async fn test_exec_cmd_environment_filtering() -> anyhow::Result<()> {
        use buck2_execute::execute::environment_inheritance::EnvironmentInheritance;

        let (executor, _root, _tmpdir) = test_executor()?;

        let (status, stdout, _) = executor
            .exec(
                "sh",
                ["-c", "echo $USER"],
                &HashMap::<String, String>::default(),
                None,
                None,
                Some(&EnvironmentInheritance::empty()),
                NoopLivelinessObserver::create(),
                false,
            )
            .await?;
        assert!(matches!(status, GatherOutputStatus::Finished { exit_code, .. } if exit_code == 0));
        assert_eq!(stdout, b"\n");

        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.
 */

pub mod action_cache;
pub mod caching;
pub mod hybrid;
pub mod local;
pub mod re;
pub mod worker;