//===-- DarwinProcessLauncher.cpp -------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

//
//  DarwinProcessLauncher.cpp
//  lldb
//
//  Created by Todd Fiala on 8/30/16.
//
//

#include "DarwinProcessLauncher.h"

// C includes
#include <spawn.h>
#include <sys/ptrace.h>
#include <sys/stat.h>
#include <sys/sysctl.h>

#ifndef _POSIX_SPAWN_DISABLE_ASLR
#define _POSIX_SPAWN_DISABLE_ASLR 0x0100
#endif

// LLDB includes
#include "lldb/lldb-enumerations.h"

#include "lldb/Host/PseudoTerminal.h"
#include "lldb/Target/ProcessLaunchInfo.h"
#include "lldb/Utility/Log.h"
#include "lldb/Utility/Status.h"
#include "lldb/Utility/StreamString.h"
#include "llvm/Support/Errno.h"

#include "CFBundle.h"
#include "CFString.h"

using namespace lldb;
using namespace lldb_private;
using namespace lldb_private::process_darwin;
using namespace lldb_private::darwin_process_launcher;

namespace {
static LaunchFlavor g_launch_flavor = LaunchFlavor::Default;
}

namespace lldb_private {
namespace darwin_process_launcher {

static uint32_t GetCPUTypeForLocalProcess(::pid_t pid) {
  int mib[CTL_MAXNAME] = {
      0,
  };
  size_t len = CTL_MAXNAME;
  if (::sysctlnametomib("sysctl.proc_cputype", mib, &len))
    return 0;

  mib[len] = pid;
  len++;

  cpu_type_t cpu;
  size_t cpu_len = sizeof(cpu);
  if (::sysctl(mib, static_cast<u_int>(len), &cpu, &cpu_len, 0, 0))
    cpu = 0;
  return cpu;
}

static bool ResolveExecutablePath(const char *path, char *resolved_path,
                                  size_t resolved_path_size) {
  if (path == NULL || path[0] == '\0')
    return false;

  char max_path[PATH_MAX];
  std::string result;
  CFString::GlobPath(path, result);

  if (result.empty())
    result = path;

  struct stat path_stat;
  if (::stat(path, &path_stat) == 0) {
    if ((path_stat.st_mode & S_IFMT) == S_IFDIR) {
      CFBundle bundle(path);
      CFReleaser<CFURLRef> url(bundle.CopyExecutableURL());
      if (url.get()) {
        if (::CFURLGetFileSystemRepresentation(
                url.get(), true, (UInt8 *)resolved_path, resolved_path_size))
          return true;
      }
    }
  }

  if (realpath(path, max_path)) {
    // Found the path relatively...
    ::strncpy(resolved_path, max_path, resolved_path_size);
    return strlen(resolved_path) + 1 < resolved_path_size;
  } else {
    // Not a relative path, check the PATH environment variable if the
    const char *PATH = getenv("PATH");
    if (PATH) {
      const char *curr_path_start = PATH;
      const char *curr_path_end;
      while (curr_path_start && *curr_path_start) {
        curr_path_end = strchr(curr_path_start, ':');
        if (curr_path_end == NULL) {
          result.assign(curr_path_start);
          curr_path_start = NULL;
        } else if (curr_path_end > curr_path_start) {
          size_t len = curr_path_end - curr_path_start;
          result.assign(curr_path_start, len);
          curr_path_start += len + 1;
        } else
          break;

        result += '/';
        result += path;
        struct stat s;
        if (stat(result.c_str(), &s) == 0) {
          ::strncpy(resolved_path, result.c_str(), resolved_path_size);
          return result.size() + 1 < resolved_path_size;
        }
      }
    }
  }
  return false;
}

// TODO check if we have a general purpose fork and exec.  We may be
// able to get rid of this entirely.
static Status ForkChildForPTraceDebugging(const char *path, char const *argv[],
                                          char const *envp[], ::pid_t *pid,
                                          int *pty_fd) {
  Status error;
  if (!path || !argv || !envp || !pid || !pty_fd) {
    error.SetErrorString("invalid arguments");
    return error;
  }

  // Use a fork that ties the child process's stdin/out/err to a pseudo
  // terminal so we can read it in our MachProcess::STDIOThread as unbuffered
  // io.
  PseudoTerminal pty;
  char fork_error[256];
  memset(fork_error, 0, sizeof(fork_error));
  *pid = static_cast<::pid_t>(pty.Fork(fork_error, sizeof(fork_error)));
  if (*pid < 0) {
    // Status during fork.
    *pid = static_cast<::pid_t>(LLDB_INVALID_PROCESS_ID);
    error.SetErrorStringWithFormat("%s(): fork failed: %s", __FUNCTION__,
                                   fork_error);
    return error;
  } else if (pid == 0) {
    // Child process

    // Debug this process.
    ::ptrace(PT_TRACE_ME, 0, 0, 0);

    // Get BSD signals as mach exceptions.
    ::ptrace(PT_SIGEXC, 0, 0, 0);

    // If our parent is setgid, lets make sure we don't inherit those extra
    // powers due to nepotism.
    if (::setgid(getgid()) == 0) {
      // Let the child have its own process group. We need to execute this call
      // in both the child and parent to avoid a race condition between the two
      // processes.

      // Set the child process group to match its pid.
      ::setpgid(0, 0);

      // Sleep a bit to before the exec call.
      ::sleep(1);

      // Turn this process into the given executable.
      ::execv(path, (char *const *)argv);
    }
    // Exit with error code. Child process should have taken over in above exec
    // call and if the exec fails it will exit the child process below.
    ::exit(127);
  } else {
    // Parent process
    // Let the child have its own process group. We need to execute this call
    // in both the child and parent to avoid a race condition between the two
    // processes.

    // Set the child process group to match its pid
    ::setpgid(*pid, *pid);
    if (pty_fd) {
      // Release our master pty file descriptor so the pty class doesn't close
      // it and so we can continue to use it in our STDIO thread
      *pty_fd = pty.ReleaseMasterFileDescriptor();
    }
  }
  return error;
}

static Status
CreatePosixSpawnFileAction(const FileAction &action,
                           posix_spawn_file_actions_t *file_actions) {
  Status error;

  // Log it.
  Log *log(GetLogIfAllCategoriesSet(LIBLLDB_LOG_PROCESS));
  if (log) {
    StreamString stream;
    stream.PutCString("converting file action for posix_spawn(): ");
    action.Dump(stream);
    stream.Flush();
    log->PutCString(stream.GetString().c_str());
  }

  // Validate args.
  if (!file_actions) {
    error.SetErrorString("mandatory file_actions arg is null");
    return error;
  }

  // Build the posix file action.
  switch (action.GetAction()) {
  case FileAction::eFileActionOpen: {
    const int error_code = ::posix_spawn_file_actions_addopen(
        file_actions, action.GetFD(), action.GetPath(),
        action.GetActionArgument(), 0);
    if (error_code != 0) {
      error.SetError(error_code, eErrorTypePOSIX);
      return error;
    }
    break;
  }

  case FileAction::eFileActionClose: {
    const int error_code =
        ::posix_spawn_file_actions_addclose(file_actions, action.GetFD());
    if (error_code != 0) {
      error.SetError(error_code, eErrorTypePOSIX);
      return error;
    }
    break;
  }

  case FileAction::eFileActionDuplicate: {
    const int error_code = ::posix_spawn_file_actions_adddup2(
        file_actions, action.GetFD(), action.GetActionArgument());
    if (error_code != 0) {
      error.SetError(error_code, eErrorTypePOSIX);
      return error;
    }
    break;
  }

  case FileAction::eFileActionNone:
  default:
    LLDB_LOGF(log, "%s(): unsupported file action %u", __FUNCTION__,
              action.GetAction());
    break;
  }

  return error;
}

static Status PosixSpawnChildForPTraceDebugging(const char *path,
                                                ProcessLaunchInfo &launch_info,
                                                ::pid_t *pid,
                                                cpu_type_t *actual_cpu_type) {
  Status error;
  Log *log(GetLogIfAllCategoriesSet(LIBLLDB_LOG_PROCESS));

  if (!pid) {
    error.SetErrorStringWithFormat("%s(): pid arg cannot be null",
                                   __FUNCTION__);
    return error;
  }

  posix_spawnattr_t attr;
  short flags;
  if (log) {
    StreamString stream;
    stream.Printf("%s(path='%s',...)\n", __FUNCTION__, path);
    launch_info.Dump(stream, nullptr);
    stream.Flush();
    log->PutCString(stream.GetString().c_str());
  }

  int error_code;
  if ((error_code = ::posix_spawnattr_init(&attr)) != 0) {
    LLDB_LOGF(log, "::posix_spawnattr_init(&attr) failed");
    error.SetError(error_code, eErrorTypePOSIX);
    return error;
  }

  // Ensure we clean up the spawnattr structure however we exit this function.
  std::unique_ptr<posix_spawnattr_t, int (*)(posix_spawnattr_t *)> spawnattr_up(
      &attr, ::posix_spawnattr_destroy);

  flags = POSIX_SPAWN_START_SUSPENDED | POSIX_SPAWN_SETSIGDEF |
          POSIX_SPAWN_SETSIGMASK;
  if (launch_info.GetFlags().Test(eLaunchFlagDisableASLR))
    flags |= _POSIX_SPAWN_DISABLE_ASLR;

  sigset_t no_signals;
  sigset_t all_signals;
  sigemptyset(&no_signals);
  sigfillset(&all_signals);
  ::posix_spawnattr_setsigmask(&attr, &no_signals);
  ::posix_spawnattr_setsigdefault(&attr, &all_signals);

  if ((error_code = ::posix_spawnattr_setflags(&attr, flags)) != 0) {
    LLDB_LOG(log,
             "::posix_spawnattr_setflags(&attr, "
             "POSIX_SPAWN_START_SUSPENDED{0}) failed: {1}",
             flags & _POSIX_SPAWN_DISABLE_ASLR ? " | _POSIX_SPAWN_DISABLE_ASLR"
                                               : "",
             llvm::sys::StrError(error_code));
    error.SetError(error_code, eErrorTypePOSIX);
    return error;
  }

#if !defined(__arm__)

  // We don't need to do this for ARM, and we really shouldn't now that we have
  // multiple CPU subtypes and no posix_spawnattr call that allows us to set
  // which CPU subtype to launch...
  cpu_type_t desired_cpu_type = launch_info.GetArchitecture().GetMachOCPUType();
  if (desired_cpu_type != LLDB_INVALID_CPUTYPE) {
    size_t ocount = 0;
    error_code =
        ::posix_spawnattr_setbinpref_np(&attr, 1, &desired_cpu_type, &ocount);
    if (error_code != 0) {
      LLDB_LOG(log,
               "::posix_spawnattr_setbinpref_np(&attr, 1, "
               "cpu_type = {0:x8}, count => {1}): {2}",
               desired_cpu_type, ocount, llvm::sys::StrError(error_code));
      error.SetError(error_code, eErrorTypePOSIX);
      return error;
    }
    if (ocount != 1) {
      error.SetErrorStringWithFormat("posix_spawnattr_setbinpref_np "
                                     "did not set the expected number "
                                     "of cpu_type entries: expected 1 "
                                     "but was %zu",
                                     ocount);
      return error;
    }
  }
#endif

  posix_spawn_file_actions_t file_actions;
  if ((error_code = ::posix_spawn_file_actions_init(&file_actions)) != 0) {
    LLDB_LOG(log, "::posix_spawn_file_actions_init(&file_actions) failed: {0}",
             llvm::sys::StrError(error_code));
    error.SetError(error_code, eErrorTypePOSIX);
    return error;
  }

  // Ensure we clean up file actions however we exit this.  When the
  // file_actions_up below goes out of scope, we'll get our file action
  // cleanup.
  std::unique_ptr<posix_spawn_file_actions_t,
                  int (*)(posix_spawn_file_actions_t *)>
      file_actions_up(&file_actions, ::posix_spawn_file_actions_destroy);

  // We assume the caller has setup the file actions appropriately.  We are not
  // in the business of figuring out what we really need here. lldb-server will
  // have already called FinalizeFileActions() as well to button these up
  // properly.
  const size_t num_actions = launch_info.GetNumFileActions();
  for (size_t action_index = 0; action_index < num_actions; ++action_index) {
    const FileAction *const action =
        launch_info.GetFileActionAtIndex(action_index);
    if (!action)
      continue;

    error = CreatePosixSpawnFileAction(*action, &file_actions);
    if (!error.Success()) {
      LLDB_LOGF(log,
                "%s(): error converting FileAction to posix_spawn "
                "file action: %s",
                __FUNCTION__, error.AsCString());
      return error;
    }
  }

  // TODO: Verify if we can set the working directory back immediately
  // after the posix_spawnp call without creating a race condition???
  const char *const working_directory =
      launch_info.GetWorkingDirectory().GetCString();
  if (working_directory && working_directory[0])
    ::chdir(working_directory);

  auto argv = launch_info.GetArguments().GetArgumentVector();
  auto envp = launch_info.GetEnvironmentEntries().GetArgumentVector();
  error_code = ::posix_spawnp(pid, path, &file_actions, &attr,
                              (char *const *)argv, (char *const *)envp);
  if (error_code != 0) {
    LLDB_LOG(log,
             "::posix_spawnp(pid => {0}, path = '{1}', file_actions "
             "= {2}, attr = {3}, argv = {4}, envp = {5}) failed: {6}",
             pid, path, &file_actions, &attr, argv, envp,
             llvm::sys::StrError(error_code));
    error.SetError(error_code, eErrorTypePOSIX);
    return error;
  }

  // Validate we got a pid.
  if (pid == LLDB_INVALID_PROCESS_ID) {
    error.SetErrorString("posix_spawn() did not indicate a failure but it "
                         "failed to return a pid, aborting.");
    return error;
  }

  if (actual_cpu_type) {
    *actual_cpu_type = GetCPUTypeForLocalProcess(*pid);
    LLDB_LOGF(log,
              "%s(): cpu type for launched process pid=%i: "
              "cpu_type=0x%8.8x",
              __FUNCTION__, *pid, *actual_cpu_type);
  }

  return error;
}

Status LaunchInferior(ProcessLaunchInfo &launch_info, int *pty_master_fd,
                      LaunchFlavor *launch_flavor) {
  Status error;
  Log *log(GetLogIfAllCategoriesSet(LIBLLDB_LOG_PROCESS));

  if (!launch_flavor) {
    error.SetErrorString("mandatory launch_flavor field was null");
    return error;
  }

  if (log) {
    StreamString stream;
    stream.Printf("NativeProcessDarwin::%s(): launching with the "
                  "following launch info:",
                  __FUNCTION__);
    launch_info.Dump(stream, nullptr);
    stream.Flush();
    log->PutCString(stream.GetString().c_str());
  }

  // Retrieve the binary name given to us.
  char given_path[PATH_MAX];
  given_path[0] = '\0';
  launch_info.GetExecutableFile().GetPath(given_path, sizeof(given_path));

  // Determine the manner in which we'll launch.
  *launch_flavor = g_launch_flavor;
  if (*launch_flavor == LaunchFlavor::Default) {
    // Our default launch method is posix spawn
    *launch_flavor = LaunchFlavor::PosixSpawn;
#if defined WITH_FBS
    // Check if we have an app bundle, if so launch using BackBoard Services.
    if (strstr(given_path, ".app")) {
      *launch_flavor = eLaunchFlavorFBS;
    }
#elif defined WITH_BKS
    // Check if we have an app bundle, if so launch using BackBoard Services.
    if (strstr(given_path, ".app")) {
      *launch_flavor = eLaunchFlavorBKS;
    }
#elif defined WITH_SPRINGBOARD
    // Check if we have an app bundle, if so launch using SpringBoard.
    if (strstr(given_path, ".app")) {
      *launch_flavor = eLaunchFlavorSpringBoard;
    }
#endif
  }

  // Attempt to resolve the binary name to an absolute path.
  char resolved_path[PATH_MAX];
  resolved_path[0] = '\0';

  LLDB_LOGF(log, "%s(): attempting to resolve given binary path: \"%s\"",
            __FUNCTION__, given_path);

  // If we fail to resolve the path to our executable, then just use what we
  // were given and hope for the best
  if (!ResolveExecutablePath(given_path, resolved_path,
                             sizeof(resolved_path))) {
    LLDB_LOGF(log,
              "%s(): failed to resolve binary path, using "
              "what was given verbatim and hoping for the best",
              __FUNCTION__);
    ::strncpy(resolved_path, given_path, sizeof(resolved_path));
  } else {
    LLDB_LOGF(log, "%s(): resolved given binary path to: \"%s\"", __FUNCTION__,
              resolved_path);
  }

  char launch_err_str[PATH_MAX];
  launch_err_str[0] = '\0';

  // TODO figure out how to handle QSetProcessEvent
  // const char *process_event = ctx.GetProcessEvent();

  // Ensure the binary is there.
  struct stat path_stat;
  if (::stat(resolved_path, &path_stat) == -1) {
    error.SetErrorToErrno();
    return error;
  }

  // Fork a child process for debugging
  // state_callback(eStateLaunching);

  const auto argv = launch_info.GetArguments().GetConstArgumentVector();
  const auto envp =
      launch_info.GetEnvironmentEntries().GetConstArgumentVector();

  switch (*launch_flavor) {
  case LaunchFlavor::ForkExec: {
    ::pid_t pid = LLDB_INVALID_PROCESS_ID;
    error = ForkChildForPTraceDebugging(resolved_path, argv, envp, &pid,
                                        pty_master_fd);
    if (error.Success()) {
      launch_info.SetProcessID(static_cast<lldb::pid_t>(pid));
    } else {
      // Reset any variables that might have been set during a failed launch
      // attempt.
      if (pty_master_fd)
        *pty_master_fd = -1;

      // We're done.
      return error;
    }
  } break;

#ifdef WITH_FBS
  case LaunchFlavor::FBS: {
    const char *app_ext = strstr(path, ".app");
    if (app_ext && (app_ext[4] == '\0' || app_ext[4] == '/')) {
      std::string app_bundle_path(path, app_ext + strlen(".app"));
      m_flags |= eMachProcessFlagsUsingFBS;
      if (BoardServiceLaunchForDebug(app_bundle_path.c_str(), argv, envp,
                                     no_stdio, disable_aslr, event_data,
                                     launch_err) != 0)
        return m_pid; // A successful SBLaunchForDebug() returns and assigns a
                      // non-zero m_pid.
      else
        break; // We tried a FBS launch, but didn't succeed lets get out
    }
  } break;
#endif

#ifdef WITH_BKS
  case LaunchFlavor::BKS: {
    const char *app_ext = strstr(path, ".app");
    if (app_ext && (app_ext[4] == '\0' || app_ext[4] == '/')) {
      std::string app_bundle_path(path, app_ext + strlen(".app"));
      m_flags |= eMachProcessFlagsUsingBKS;
      if (BoardServiceLaunchForDebug(app_bundle_path.c_str(), argv, envp,
                                     no_stdio, disable_aslr, event_data,
                                     launch_err) != 0)
        return m_pid; // A successful SBLaunchForDebug() returns and assigns a
                      // non-zero m_pid.
      else
        break; // We tried a BKS launch, but didn't succeed lets get out
    }
  } break;
#endif

#ifdef WITH_SPRINGBOARD
  case LaunchFlavor::SpringBoard: {
    //  .../whatever.app/whatever ?
    //  Or .../com.apple.whatever.app/whatever -- be careful of ".app" in
    //  "com.apple.whatever" here
    const char *app_ext = strstr(path, ".app/");
    if (app_ext == NULL) {
      // .../whatever.app ?
      int len = strlen(path);
      if (len > 5) {
        if (strcmp(path + len - 4, ".app") == 0) {
          app_ext = path + len - 4;
        }
      }
    }
    if (app_ext) {
      std::string app_bundle_path(path, app_ext + strlen(".app"));
      if (SBLaunchForDebug(app_bundle_path.c_str(), argv, envp, no_stdio,
                           disable_aslr, launch_err) != 0)
        return m_pid; // A successful SBLaunchForDebug() returns and assigns a
                      // non-zero m_pid.
      else
        break; // We tried a springboard launch, but didn't succeed lets get out
    }
  } break;
#endif

  case LaunchFlavor::PosixSpawn: {
    ::pid_t pid = LLDB_INVALID_PROCESS_ID;

    // Retrieve paths for stdin/stdout/stderr.
    cpu_type_t actual_cpu_type = 0;
    error = PosixSpawnChildForPTraceDebugging(resolved_path, launch_info, &pid,
                                              &actual_cpu_type);
    if (error.Success()) {
      launch_info.SetProcessID(static_cast<lldb::pid_t>(pid));
      if (pty_master_fd)
        *pty_master_fd = launch_info.GetPTY().ReleaseMasterFileDescriptor();
    } else {
      // Reset any variables that might have been set during a failed launch
      // attempt.
      if (pty_master_fd)
        *pty_master_fd = -1;

      // We're done.
      return error;
    }
    break;
  }

  default:
    // Invalid launch flavor.
    error.SetErrorStringWithFormat("NativeProcessDarwin::%s(): unknown "
                                   "launch flavor %d",
                                   __FUNCTION__, (int)*launch_flavor);
    return error;
  }

  if (launch_info.GetProcessID() == LLDB_INVALID_PROCESS_ID) {
    // If we don't have a valid process ID and no one has set the error, then
    // return a generic error.
    if (error.Success())
      error.SetErrorStringWithFormat("%s(): failed to launch, no reason "
                                     "specified",
                                     __FUNCTION__);
  }

  // We're done with the launch side of the operation.
  return error;
}
}
} // namespaces
