// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "build/build_config.h"

#if BUILDFLAG(IS_IOS_TVOS)
#include <TargetConditionals.h>

#if TARGET_OS_SIMULATOR
// On tvOS, all posix_spawn*() functions except for posix_spawnp() are marked
// unavailable, but the symbols and the implementation are present (but
// unusable on tvOS device builds targeted for distribution due to App Store
// restrictions on the use of multiple processes). posix_spawnp() is required
// for the test launcher code to be able to launch multiple processes in the
// simulator, but it is not fully usable without the functions marked
// unavailable.
//
// Work around it by changing the availability annotation of the functions used
// in this file before including <spawn.h>. This is done as early as possible
// (i.e. before even including base/process/launch.h) to prevent <spawn.h> from
// being indirectly included before we are able to declare a different
// availability.
//
// Note: <spawn.h> is included as a system header together with the other
// regular headers outside this block. The inclusion as a system header turns
// off the availability warning that would normally be thrown by LLVM when the
// header's function declarations with different availability annotations were
// added. See the discussion in
// https://chromium-review.googlesource.com/c/chromium/src/+/6687371/comment/6baf4b4c_8a60d02a/
#include <Availability.h>
#include <inttypes.h>
#include <sys/types.h>

extern "C" {

using posix_spawnattr_t = void*;
using posix_spawn_file_actions_t = void*;

int posix_spawnattr_init(posix_spawnattr_t*) __API_AVAILABLE(tvos(1.0));
int posix_spawnattr_destroy(posix_spawnattr_t*) __API_AVAILABLE(tvos(1.0));
int posix_spawn_file_actions_destroy(posix_spawn_file_actions_t*)
    __API_AVAILABLE(tvos(1.0));
int posix_spawn_file_actions_init(posix_spawn_file_actions_t*)
    __API_AVAILABLE(tvos(1.0));
int posix_spawn_file_actions_adddup2(posix_spawn_file_actions_t*, int, int)
    __API_AVAILABLE(tvos(1.0));
int posix_spawn_file_actions_addopen(posix_spawn_file_actions_t*,
                                     int,
                                     const char*,
                                     int,
                                     mode_t) __API_AVAILABLE(tvos(1.0));
int posix_spawn_file_actions_addinherit_np(posix_spawn_file_actions_t*, int)
    __API_AVAILABLE(tvos(1.0));
int posix_spawnattr_setpgroup(posix_spawnattr_t*, pid_t)
    __API_AVAILABLE(tvos(1.0));
int posix_spawnattr_setflags(posix_spawnattr_t*, short)
    __API_AVAILABLE(tvos(1.0));
int posix_spawnattr_set_csm_np(const posix_spawnattr_t*, uint32_t)
    __API_AVAILABLE(tvos(1.0));

}  // extern "C"

#else
#error This file is not supported on tvOS device builds.
#endif  // TARGET_OS_SIMULATOR
#endif  // BUILDFLAG(IS_IOS_TVOS)

#include "base/process/launch.h"

#include <crt_externs.h>
#include <mach/mach.h>
#include <spawn.h>
#include <string.h>
#include <sys/wait.h>

#include "base/apple/mach_port_rendezvous.h"
#include "base/command_line.h"
#include "base/files/scoped_file.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/posix/eintr_wrapper.h"
#include "base/process/environment_internal.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/threading/thread_restrictions.h"
#include "base/trace_event/trace_event.h"

#if BUILDFLAG(IS_MAC)
#include "base/apple/mach_port_rendezvous_mac.h"
#endif

extern "C" {
// Changes the current thread's directory to a path or directory file
// descriptor.
int pthread_chdir_np(const char* dir);

int pthread_fchdir_np(int fd);

int responsibility_spawnattrs_setdisclaim(posix_spawnattr_t attrs,
                                          int disclaim);
}  // extern "C"

namespace base {

void CheckPThreadStackMinIsSafe() {
  static_assert(__builtin_constant_p(PTHREAD_STACK_MIN),
                "Always constant on mac");
}

namespace {

// DPSXCHECK is a Debug Posix Spawn Check macro. The posix_spawn* family of
// functions return an errno value, as opposed to setting errno directly. This
// macro emulates a DPCHECK().
#define DPSXCHECK(expr)                                              \
  do {                                                               \
    int rv = (expr);                                                 \
    DCHECK_EQ(rv, 0) << #expr << ": -" << rv << " " << strerror(rv); \
  } while (0)

class PosixSpawnAttr {
 public:
  PosixSpawnAttr() { DPSXCHECK(posix_spawnattr_init(&attr_)); }

  ~PosixSpawnAttr() { DPSXCHECK(posix_spawnattr_destroy(&attr_)); }

  posix_spawnattr_t* get() { return &attr_; }

 private:
  posix_spawnattr_t attr_;
};

class PosixSpawnFileActions {
 public:
  PosixSpawnFileActions() {
    DPSXCHECK(posix_spawn_file_actions_init(&file_actions_));
  }

  PosixSpawnFileActions(const PosixSpawnFileActions&) = delete;
  PosixSpawnFileActions& operator=(const PosixSpawnFileActions&) = delete;

  ~PosixSpawnFileActions() {
    DPSXCHECK(posix_spawn_file_actions_destroy(&file_actions_));
  }

  void Open(int filedes, const char* path, int mode) {
    DPSXCHECK(posix_spawn_file_actions_addopen(&file_actions_, filedes, path,
                                               mode, 0));
  }

  void Dup2(int filedes, int newfiledes) {
    DPSXCHECK(
        posix_spawn_file_actions_adddup2(&file_actions_, filedes, newfiledes));
  }

  void Inherit(int filedes) {
    DPSXCHECK(posix_spawn_file_actions_addinherit_np(&file_actions_, filedes));
  }

#if BUILDFLAG(IS_MAC)
  void Chdir(const char* path) {
    DPSXCHECK(posix_spawn_file_actions_addchdir_np(&file_actions_, path));
  }
#endif

  const posix_spawn_file_actions_t* get() const { return &file_actions_; }

 private:
  posix_spawn_file_actions_t file_actions_;
};

#if !BUILDFLAG(IS_MAC)
int ChangeCurrentThreadDirectory(const char* path) {
  return pthread_chdir_np(path);
}

// The recommended way to unset a per-thread cwd is to set a new value to an
// invalid file descriptor, per libpthread-218.1.3/private/private.h.
int ResetCurrentThreadDirectory() {
  return pthread_fchdir_np(-1);
}
#endif

struct GetAppOutputOptions {
  // Whether to pipe stderr to stdout in |output|.
  bool include_stderr = false;
  // Caller-supplied string poiter for the output.
  raw_ptr<std::string> output = nullptr;
  // Result exit code of Process::Wait().
  int exit_code = 0;
};

bool GetAppOutputInternal(const std::vector<std::string>& argv,
                          GetAppOutputOptions* gao_options) {
  TRACE_EVENT0("base", "GetAppOutput");

  ScopedFD read_fd, write_fd;
  {
    int pipefds[2];
    if (pipe(pipefds) != 0) {
      DPLOG(ERROR) << "pipe";
      return false;
    }
    read_fd.reset(pipefds[0]);
    write_fd.reset(pipefds[1]);
  }

  LaunchOptions launch_options;
  launch_options.fds_to_remap.emplace_back(write_fd.get(), STDOUT_FILENO);
  if (gao_options->include_stderr) {
    launch_options.fds_to_remap.emplace_back(write_fd.get(), STDERR_FILENO);
  }

  Process process = LaunchProcess(argv, launch_options);
  if (!process.IsValid()) {
    return false;
  }

  // Close the parent process' write descriptor, so that EOF is generated in
  // read loop below.
  write_fd.reset();

  // Read the child's output before waiting for its exit, otherwise the pipe
  // buffer may fill up if the process is producing a lot of output.
  std::string* output = gao_options->output;
  output->clear();

  const size_t kBufferSize = 1024;
  size_t total_bytes_read = 0;
  ssize_t read_this_pass = 0;
  do {
    output->resize(output->size() + kBufferSize);
    read_this_pass = HANDLE_EINTR(
        read(read_fd.get(), &(*output)[total_bytes_read], kBufferSize));
    if (read_this_pass >= 0) {
      total_bytes_read += static_cast<size_t>(read_this_pass);
      output->resize(total_bytes_read);
    }
  } while (read_this_pass > 0);

  // It is okay to allow this process to wait on the launched process as a
  // process launched with GetAppOutput*() shouldn't wait back on the process
  // that launched it.
  internal::GetAppOutputScopedAllowBaseSyncPrimitives allow_wait;
  if (!process.WaitForExit(&gao_options->exit_code)) {
    return false;
  }

  return read_this_pass == 0;
}

}  // namespace

Process LaunchProcess(const CommandLine& cmdline,
                      const LaunchOptions& options) {
  return LaunchProcess(cmdline.argv(), options);
}

Process LaunchProcess(const std::vector<std::string>& argv,
                      const LaunchOptions& options) {
  TRACE_EVENT0("base", "LaunchProcess");

  PosixSpawnAttr attr;

  short flags = POSIX_SPAWN_CLOEXEC_DEFAULT;
  if (options.new_process_group) {
    flags |= POSIX_SPAWN_SETPGROUP;
    DPSXCHECK(posix_spawnattr_setpgroup(attr.get(), 0));
  }
  DPSXCHECK(posix_spawnattr_setflags(attr.get(), flags));

  PosixSpawnFileActions file_actions;

  // Process file descriptors for the child. By default, LaunchProcess will
  // open stdin to /dev/null and inherit stdout and stderr.
  bool inherit_stdout = true, inherit_stderr = true;
  bool null_stdin = true;
  for (const auto& dup2_pair : options.fds_to_remap) {
    if (dup2_pair.second == STDIN_FILENO) {
      null_stdin = false;
    } else if (dup2_pair.second == STDOUT_FILENO) {
      inherit_stdout = false;
    } else if (dup2_pair.second == STDERR_FILENO) {
      inherit_stderr = false;
    }

    if (dup2_pair.first == dup2_pair.second) {
      file_actions.Inherit(dup2_pair.second);
    } else {
      file_actions.Dup2(dup2_pair.first, dup2_pair.second);
    }
  }

  if (null_stdin) {
    file_actions.Open(STDIN_FILENO, "/dev/null", O_RDONLY);
  }
  if (inherit_stdout) {
    file_actions.Inherit(STDOUT_FILENO);
  }
  if (inherit_stderr) {
    file_actions.Inherit(STDERR_FILENO);
  }

#if BUILDFLAG(IS_MAC)
  if (options.disclaim_responsibility) {
    DPSXCHECK(responsibility_spawnattrs_setdisclaim(attr.get(), 1));
  }

  EnvironmentMap new_environment_map = options.environment;
  MachPortRendezvousServerMac::AddFeatureStateToEnvironment(
      new_environment_map);
#else
  const EnvironmentMap& new_environment_map = options.environment;
#endif

  std::vector<char*> argv_cstr;
  argv_cstr.reserve(argv.size() + 1);
  for (const auto& arg : argv) {
    argv_cstr.push_back(const_cast<char*>(arg.c_str()));
  }
  argv_cstr.push_back(nullptr);

  base::HeapArray<char*> owned_environ;
  char* empty_environ = nullptr;
  char** new_environ =
      options.clear_environment ? &empty_environ : *_NSGetEnviron();
  if (!new_environment_map.empty()) {
    owned_environ =
        internal::AlterEnvironment(new_environ, new_environment_map);
    new_environ = owned_environ.data();
  }

  const char* executable_path = !options.real_path.empty()
                                    ? options.real_path.value().c_str()
                                    : argv_cstr[0];

  if (options.enable_cpu_security_mitigations) {
    DPSXCHECK(posix_spawnattr_set_csm_np(attr.get(), POSIX_SPAWN_NP_CSM_ALL));
  }

  if (!options.current_directory.empty()) {
    const char* chdir_str = options.current_directory.value().c_str();
#if BUILDFLAG(IS_MAC)
    file_actions.Chdir(chdir_str);
#else
    // If the chdir posix_spawn_file_actions extension is not available,
    // change the thread-specific working directory. The new process will
    // inherit it during posix_spawnp().
    int rv = ChangeCurrentThreadDirectory(chdir_str);
    if (rv != 0) {
      DPLOG(ERROR) << "pthread_chdir_np";
      return Process();
    }
#endif
  }

  int rv;
  pid_t pid;
  {
    const bool has_mach_ports_for_rendezvous =
#if BUILDFLAG(IS_IOS_TVOS)
        false
#else
        !options.mach_ports_for_rendezvous.empty()
#endif  // BUILDFLAG(IS_IOS_TVOS)
        ;

#if BUILDFLAG(IS_IOS)
    // This code is only used for the iOS simulator to launch tests. We do not
    // support setting MachPorts on launch. You should look at
    // content::ChildProcessLauncherHelper (for iOS) if you are trying to spawn
    // a non-test and need ports.
    CHECK(!has_mach_ports_for_rendezvous);
#else
    // If `options.mach_ports_for_rendezvous` or `options.process_requirement`
    // is specified : the server's lock must be held for the duration of
    // posix_spawnp() so that new child's PID can be recorded with the set of
    // ports or process requirement.
    bool needs_rendezvous_lock =
        has_mach_ports_for_rendezvous || options.process_requirement;

    AutoLockMaybe rendezvous_lock(
        needs_rendezvous_lock
            ? &MachPortRendezvousServerMac::GetInstance()->GetLock()
            : nullptr);
#endif
    // Use posix_spawnp as some callers expect to have PATH consulted.
    rv = posix_spawnp(&pid, executable_path, file_actions.get(), attr.get(),
                      &argv_cstr[0], new_environ);

#if !BUILDFLAG(IS_IOS)
    if (needs_rendezvous_lock) {
      if (rv == 0) {
        MachPortRendezvousServerMac::GetInstance()->GetLock().AssertAcquired();
        if (has_mach_ports_for_rendezvous) {
          MachPortRendezvousServerMac::GetInstance()->RegisterPortsForPid(
              pid, options.mach_ports_for_rendezvous);
        }
        if (options.process_requirement) {
          MachPortRendezvousServerMac::GetInstance()
              ->SetProcessRequirementForPid(pid, *options.process_requirement);
        }
      } else {
        // Because |options| is const-ref, the collection has to be copied here.
        // The caller expects to relinquish ownership of any strong rights if
        // LaunchProcess() were to succeed, so these rights should be manually
        // destroyed on failure.
        MachPortsForRendezvous ports = options.mach_ports_for_rendezvous;
        for (auto& port : ports) {
          port.second.Destroy();
        }
      }
    }
#endif
  }

#if !BUILDFLAG(IS_MAC)
  // Restore the thread's working directory if it was changed.
  if (!options.current_directory.empty()) {
    ResetCurrentThreadDirectory();
  }
#endif

  if (rv != 0) {
    DLOG(ERROR) << "posix_spawnp(" << executable_path << "): -" << rv << " "
                << strerror(rv);
    return Process();
  }

  if (options.wait) {
    // While this isn't strictly disk IO, waiting for another process to
    // finish is the sort of thing ThreadRestrictions is trying to prevent.
    ScopedBlockingCall scoped_blocking_call(FROM_HERE, BlockingType::MAY_BLOCK);
    pid_t ret = HANDLE_EINTR(waitpid(pid, nullptr, 0));
    DPCHECK(ret > 0);
  }

  return Process(pid);
}

bool GetAppOutput(const CommandLine& cl, std::string* output) {
  return GetAppOutput(cl.argv(), output);
}

bool GetAppOutputAndError(const CommandLine& cl, std::string* output) {
  return GetAppOutputAndError(cl.argv(), output);
}

bool GetAppOutputWithExitCode(const CommandLine& cl,
                              std::string* output,
                              int* exit_code) {
  return GetAppOutputWithExitCode(cl.argv(), output, exit_code);
}

bool GetAppOutput(const std::vector<std::string>& argv, std::string* output) {
  GetAppOutputOptions options;
  options.output = output;
  return GetAppOutputInternal(argv, &options) &&
         options.exit_code == EXIT_SUCCESS;
}

bool GetAppOutputAndError(const std::vector<std::string>& argv,
                          std::string* output) {
  GetAppOutputOptions options;
  options.include_stderr = true;
  options.output = output;
  return GetAppOutputInternal(argv, &options) &&
         options.exit_code == EXIT_SUCCESS;
}

bool GetAppOutputWithExitCode(const std::vector<std::string>& argv,
                              std::string* output,
                              int* exit_code) {
  GetAppOutputOptions options;
  options.output = output;
  bool rv = GetAppOutputInternal(argv, &options);
  *exit_code = options.exit_code;
  return rv;
}

void RaiseProcessToHighPriority() {
  // Historically this has not been implemented on POSIX and macOS. This could
  // influence the Mach task policy in the future.
}

}  // namespace base
