// Copyright 2018 The Crashpad Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "test/multiprocess_exec.h"

#include <lib/fdio/io.h>
#include <lib/fdio/spawn.h>
#include <lib/zx/process.h>
#include <lib/zx/time.h>
#include <zircon/processargs.h>

#include "base/files/scoped_file.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "gtest/gtest.h"

namespace crashpad {
namespace test {

namespace {

void AddPipe(fdio_spawn_action_t* action, int target_fd, int* fd_out) {
  zx_handle_t handle = ZX_HANDLE_INVALID;
  zx_status_t status = fdio_pipe_half(fd_out, &handle);
  ZX_CHECK(status == ZX_OK, status) << "fdio_pipe_half";
  action->action = FDIO_SPAWN_ACTION_ADD_HANDLE;
  action->h.id = PA_HND(PA_FD, target_fd);
  action->h.handle = handle;
}

}  // namespace

namespace internal {

struct MultiprocessInfo {
  MultiprocessInfo() {}
  base::ScopedFD stdin_write;
  base::ScopedFD stdout_read;
  zx::process child;
};

}  // namespace internal

Multiprocess::Multiprocess()
    : info_(nullptr), code_(EXIT_SUCCESS), reason_(kTerminationNormal) {}

void Multiprocess::Run() {
  // Set up and spawn the child process.
  ASSERT_NO_FATAL_FAILURE(PreFork());
  RunChild();

  // And then run the parent actions in this process.
  RunParent();

  // Wait until the child exits.
  zx_signals_t signals;
  ASSERT_EQ(
      info_->child.wait_one(ZX_TASK_TERMINATED, zx::time::infinite(), &signals),
      ZX_OK);
  ASSERT_EQ(signals, ZX_TASK_TERMINATED);

  // Get the child's exit code.
  zx_info_process_t proc_info;
  zx_status_t status = info_->child.get_info(
      ZX_INFO_PROCESS, &proc_info, sizeof(proc_info), nullptr, nullptr);
  if (status != ZX_OK) {
    ZX_LOG(ERROR, status) << "zx_object_get_info";
    ADD_FAILURE() << "Unable to get exit code of child";
  } else {
    if (code_ != proc_info.return_code) {
      ADD_FAILURE() << "Child exited with code " << proc_info.return_code
                    << ", expected exit with code " << code_;
    }
  }
}

void Multiprocess::SetExpectedChildTermination(TerminationReason reason,
                                               ReturnCodeType code) {
  EXPECT_EQ(info_, nullptr)
      << "SetExpectedChildTermination() must be called before Run()";
  reason_ = reason;
  code_ = code;
}

void Multiprocess::SetExpectedChildTerminationBuiltinTrap() {
  constexpr ReturnCodeType kExpectedReturnCode = ZX_TASK_RETCODE_EXCEPTION_KILL;
  SetExpectedChildTermination(kTerminationNormal, kExpectedReturnCode);
}

Multiprocess::~Multiprocess() {
  delete info_;
}

FileHandle Multiprocess::ReadPipeHandle() const {
  return info_->stdout_read.get();
}

FileHandle Multiprocess::WritePipeHandle() const {
  return info_->stdin_write.get();
}

void Multiprocess::CloseReadPipe() {
  info_->stdout_read.reset();
}

void Multiprocess::CloseWritePipe() {
  info_->stdin_write.reset();
}

void Multiprocess::RunParent() {
  MultiprocessParent();

  info_->stdout_read.reset();
  info_->stdin_write.reset();
}

void Multiprocess::RunChild() {
  MultiprocessChild();
}

MultiprocessExec::MultiprocessExec()
    : Multiprocess(), command_(), arguments_(), argv_() {}

void MultiprocessExec::SetChildCommand(
    const base::FilePath& command,
    const std::vector<std::string>* arguments) {
  command_ = command;
  if (arguments) {
    arguments_ = *arguments;
  } else {
    arguments_.clear();
  }
}

MultiprocessExec::~MultiprocessExec() {}

void MultiprocessExec::PreFork() {
  ASSERT_FALSE(command_.empty());

  ASSERT_TRUE(argv_.empty());

  argv_.push_back(command_.value().c_str());
  for (const std::string& argument : arguments_) {
    argv_.push_back(argument.c_str());
  }
  argv_.push_back(nullptr);

  ASSERT_EQ(info(), nullptr);
  set_info(new internal::MultiprocessInfo());
}

void MultiprocessExec::MultiprocessChild() {
  constexpr size_t kActionCount = 3;
  fdio_spawn_action_t actions[kActionCount];

  int stdin_parent_side = -1;
  AddPipe(&actions[0], STDIN_FILENO, &stdin_parent_side);
  info()->stdin_write.reset(stdin_parent_side);

  int stdout_parent_side = -1;
  AddPipe(&actions[1], STDOUT_FILENO, &stdout_parent_side);
  info()->stdout_read.reset(stdout_parent_side);

  actions[2].action = FDIO_SPAWN_ACTION_CLONE_FD;
  actions[2].fd.local_fd = STDERR_FILENO;
  actions[2].fd.target_fd = STDERR_FILENO;

  // Pass the filesystem namespace, parent environment, and default job to the
  // child, but don't include any other file handles, preferring to set them
  // up explicitly below.
  uint32_t flags = FDIO_SPAWN_CLONE_ALL & ~FDIO_SPAWN_CLONE_STDIO;

  char error_message[FDIO_SPAWN_ERR_MSG_MAX_LENGTH];
  zx::process child;
  zx_status_t status = fdio_spawn_etc(ZX_HANDLE_INVALID,
                                      flags,
                                      command_.value().c_str(),
                                      argv_.data(),
                                      nullptr,
                                      kActionCount,
                                      actions,
                                      child.reset_and_get_address(),
                                      error_message);
  ZX_CHECK(status == ZX_OK, status) << "fdio_spawn_etc: " << error_message;
  info()->child = std::move(child);
}

ProcessType MultiprocessExec::ChildProcess() {
  return zx::unowned_process(info()->child);
}

}  // namespace test
}  // namespace crashpad
