// Copyright 2010-2021, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

// The IPC implementation using core Mach APIs.
#ifdef __APPLE__

#include <launch.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>

#include <map>

#include "base/logging.h"
#include "base/mac_util.h"
#include "base/singleton.h"
#include "base/thread.h"
#include "ipc/ipc.h"
#include "ipc/ipc_path_manager.h"

namespace mozc {
namespace {
// Returns the name of the service corresponding to the name.
// Since the behavior of launch_msg() is changed from Yosemite (10.10),
// this function no longer relies on the information from launch_msg().
// When we add new services, we should update this function too.
bool GetMachPortName(const std::string &name, std::string *port_name) {
  // Defined in data/mac/com.google.inputmethod.Japanese.Converter.plist
  if (name == "session") {
    port_name->assign(MacUtil::GetLabelForSuffix("") + "Converter.session");
    return true;
  }
  // Defined in data/mac/com.google.inputmethod.Japanese.Renderer.plist
  if (name == "renderer") {
    port_name->assign(MacUtil::GetLabelForSuffix("") + "Renderer.renderer");
    return true;
  }
  LOG(ERROR) << "Port not found: " << name;
  return false;
}

// The default port manager for clients: using bootstrap_look_up.
// Please take care of calling this manager because bootstrap_look_up
// automatically starts the server processes.  We want to delay
// starting server as far as possible.
class DefaultClientMachPortManager : public MachPortManagerInterface {
 public:
  virtual bool GetMachPort(const std::string &name, mach_port_t *port) {
    std::string port_name;
    if (!GetMachPortName(name, &port_name)) {
      LOG(ERROR) << "Failed to get the port name";
      return false;
    }

    kern_return_t kr = bootstrap_look_up(
        bootstrap_port, const_cast<char *>(port_name.c_str()), port);

    return kr == BOOTSTRAP_SUCCESS;
  }

  virtual bool IsServerRunning(const std::string &name) const {
    std::string server_label = MacUtil::GetLabelForSuffix("");
    if (name == "session") {
      server_label += "Converter";
    } else if (name == "renderer") {
      server_label += "Renderer";
    } else {
      LOG(ERROR) << "Unknown server name: " << name;
      server_label.assign(MacUtil::GetLabelForSuffix(name));
    }

    launch_data_t request = launch_data_alloc(LAUNCH_DATA_DICTIONARY);
    launch_data_dict_insert(request,
                            launch_data_new_string(server_label.c_str()),
                            LAUNCH_KEY_GETJOB);
    launch_data_t job = launch_msg(request);
    launch_data_free(request);
    if (job == nullptr) {
      LOG(ERROR) << "Server job not found";
      return false;
    }
    if (launch_data_get_type(job) != LAUNCH_DATA_DICTIONARY) {
      LOG(ERROR) << "Something goes wrong with getting server information: "
                 << launch_data_get_type(job);
      launch_data_free(job);
      return false;
    }

    launch_data_t pid_data = launch_data_dict_lookup(job, LAUNCH_JOBKEY_PID);
    if (pid_data == nullptr ||
        launch_data_get_type(pid_data) != LAUNCH_DATA_INTEGER) {
      // PID information is unavailable, which means server is not running.
      VLOG(2) << "Returned job is formatted wrongly: cannot find PID data.";
      launch_data_free(job);
      return false;
    }

    VLOG(2) << "Server is running with PID "
            << launch_data_get_integer(pid_data);
    launch_data_free(job);
    return true;
  }
};

// The default port manager for servers: using bootstrap_check_in.  It
// won't succeed if the port name is not registered by launchd or the
// process is not invoked by launchd.
class DefaultServerMachPortManager : public MachPortManagerInterface {
 public:
  ~DefaultServerMachPortManager() {
    for (std::map<std::string, mach_port_t>::const_iterator it =
             mach_ports_.begin();
         it != mach_ports_.end(); ++it) {
      mach_port_destroy(mach_task_self(), it->second);
    }
    mach_ports_.clear();
  }

  virtual bool GetMachPort(const std::string &name, mach_port_t *port) {
    std::string port_name;
    if (!GetMachPortName(name, &port_name)) {
      LOG(ERROR) << "Failed to get the port name";
      return false;
    }

    DLOG(INFO) << "\"" << port_name << "\"";

    std::map<std::string, mach_port_t>::const_iterator it =
        mach_ports_.find(port_name);
    if (it != mach_ports_.end()) {
      *port = it->second;
      return true;
    }

    kern_return_t kr = bootstrap_check_in(
        bootstrap_port, const_cast<char *>(port_name.c_str()), port);
    mach_ports_[port_name] = *port;
    return kr == BOOTSTRAP_SUCCESS;
  }

  // In the server side, it always return "true" because the caller
  // itself is the server.
  virtual bool IsServerRunning(const std::string &name) const { return true; }

 private:
  std::map<std::string, mach_port_t> mach_ports_;
};

struct mach_ipc_send_message {
  mach_msg_header_t header;
  mach_msg_body_t body;
  mach_msg_ool_descriptor_t data;
  mach_msg_type_number_t count;
};

struct mach_ipc_receive_message {
  mach_msg_header_t header;
  mach_msg_body_t body;
  mach_msg_ool_descriptor_t data;
  mach_msg_type_number_t count;
  mach_msg_trailer_t trailer;
};

}  // namespace

// Client implementation
IPCClient::IPCClient(const std::string &name)
    : name_(name), mach_port_manager_(nullptr), ipc_path_manager_(nullptr) {
  Init(name, "");
}

IPCClient::IPCClient(const std::string &name, const std::string &server_path)
    : name_(name), mach_port_manager_(nullptr), ipc_path_manager_(nullptr) {
  Init(name, server_path);
}

IPCClient::~IPCClient() {
  // Do nothing
}

void IPCClient::Init(const std::string &name,
                     const std::string & /*server_path*/) {
  ipc_path_manager_ = IPCPathManager::GetIPCPathManager(name);
  if (!ipc_path_manager_->LoadPathName()) {
    LOG(ERROR) << "Cannot load IPC path name";
  }
}

bool IPCClient::Call(const char *request_, size_t input_length, char *response_,
                     size_t *response_size, int32 timeout) {
  last_ipc_error_ = IPC_NO_ERROR;
  MachPortManagerInterface *manager = mach_port_manager_;
  if (manager == nullptr) {
    manager = Singleton<DefaultClientMachPortManager>::get();
  }

  // Obtain the server port
  mach_port_t server_port;
  if (manager == nullptr || !manager->GetMachPort(name_, &server_port)) {
    last_ipc_error_ = IPC_NO_CONNECTION;
    LOG(ERROR) << "Cannot connect to the server";
    return false;
  }

  // Creating sending port
  kern_return_t kr;
  mach_port_t client_port = MACH_PORT_NULL;
  kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE,
                          &client_port);
  if (kr != KERN_SUCCESS) {
    last_ipc_error_ = IPC_WRITE_ERROR;
    LOG(ERROR) << "Cannot allocate the client port: " << kr;
    return false;
  }

  // Prepare the sending message with OOL.  Out-of-Line is a message
  // sending which doesn't copy the message data but share the memory
  // area between client and server.
  mach_ipc_send_message send_message;
  mach_msg_header_t *send_header = &(send_message.header);
  send_header->msgh_bits =
      MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND) |
      MACH_MSGH_BITS_COMPLEX;  // To enable OOL message
  send_header->msgh_size = sizeof(send_message);
  send_header->msgh_remote_port = server_port;
  send_header->msgh_local_port = client_port;
  send_header->msgh_reserved = 0;
  send_header->msgh_id = IPC_PROTOCOL_VERSION;
  send_message.body.msgh_descriptor_count = 1;
  send_message.data.address = const_cast<char *>(request_);
  send_message.data.size = input_length;
  send_message.data.deallocate = false;  // Doesn't deallocate data immediately
  send_message.data.copy = MACH_MSG_VIRTUAL_COPY;  // Copy on write
  send_message.data.type = MACH_MSG_OOL_DESCRIPTOR;
  send_message.count = send_message.data.size;

  // Actually send the message
  kr = mach_msg(send_header,
                MACH_SEND_MSG | MACH_SEND_TIMEOUT,  // send with timeout
                send_header->msgh_size,             // send size
                0,                // receive size is 0 because sending
                MACH_PORT_NULL,   // receive port
                timeout,          // timeout in msec
                MACH_PORT_NULL);  // notoicication port in case of error
  if (kr == MACH_SEND_TIMED_OUT) {
    LOG(ERROR) << "sending message timeout";
    last_ipc_error_ = IPC_TIMEOUT_ERROR;
    mach_port_destroy(mach_task_self(), client_port);
    return false;
  } else if (kr != MACH_MSG_SUCCESS) {
    LOG(ERROR) << "unknown error on sending request: " << kr;
    last_ipc_error_ = IPC_WRITE_ERROR;
    mach_port_destroy(mach_task_self(), client_port);
    return false;
  }

  // Receive server response
  mach_ipc_receive_message receive_message;
  mach_msg_header_t *receive_header;
  // try to receive multiple messages because more than one processes
  // send responses.
  const int trial = 2;
  for (int i = 0; i < trial; ++i) {
    receive_header = &(receive_message.header);
    receive_header->msgh_remote_port = server_port;
    receive_header->msgh_local_port = client_port;
    receive_header->msgh_size = sizeof(receive_message);
    kr = mach_msg(receive_header,
                  MACH_RCV_MSG | MACH_RCV_TIMEOUT,  // receive with timeout
                  0,  // send size is 0 because receiving
                  receive_header->msgh_size,  // receive size
                  client_port,                // receive port
                  timeout,                    // timeout in msec
                  MACH_PORT_NULL);  // notification port in case of error
    if (kr == MACH_RCV_TIMED_OUT) {
      LOG(ERROR) << "receiving message timeout";
      last_ipc_error_ = IPC_TIMEOUT_ERROR;
      break;
    } else if (kr != MACH_MSG_SUCCESS) {
      LOG(ERROR) << "unknown error on receiving response: " << kr;
      last_ipc_error_ = IPC_READ_ERROR;
      // This can be a wrong message.  Try to receive again.
      continue;
    }

    if (receive_header->msgh_id == IPC_PROTOCOL_VERSION) {
      last_ipc_error_ = IPC_NO_ERROR;
      if (*response_size > receive_message.data.size) {
        *response_size = receive_message.data.size;
      }
      ::memcpy(response_, receive_message.data.address, *response_size);
      mach_port_destroy(mach_task_self(), client_port);
      vm_deallocate(mach_task_self(),
                    (vm_address_t)receive_message.data.address,
                    receive_message.data.size);
      return true;
    }
  }

  LOG(ERROR) << "Receiving message failed";
  if (last_ipc_error_ == IPC_NO_ERROR) {
    last_ipc_error_ = IPC_READ_ERROR;
  }
  mach_port_destroy(mach_task_self(), client_port);
  return false;
}

bool IPCClient::Connected() const {
  if (!ipc_path_manager_->LoadPathName()) {
    // No server files found: not running server or not initialized yet.
    return false;
  }

  MachPortManagerInterface *manager = mach_port_manager_;
  if (manager == nullptr) {
    manager = Singleton<DefaultClientMachPortManager>::get();
  }

  return manager->IsServerRunning(name_);
}

// Server implementation
IPCServer::IPCServer(const std::string &name, int32 num_connections,
                     int32 timeout)
    : name_(name), mach_port_manager_(nullptr), timeout_(timeout) {
  // This is a fake IPC path manager: it just stores the server
  // version and IPC name but we don't use the stored IPC name itself.
  // It's just for compatibility.
  IPCPathManager *manager = IPCPathManager::GetIPCPathManager(name);
  // The IPC server uses old path name at this time.
  // TODO(mukai): Switch to new path name when the new IPC client is
  // widely distributed.
  LOG(INFO) << "Server created";
  if (!manager->CreateNewPathName()) {
    LOG(ERROR) << "Cannot make IPC path name";
    return;
  }

  if (!manager->SavePathName()) {
    LOG(ERROR) << "Cannot save IPC path name";
  }
}

IPCServer::~IPCServer() {
  // Do nothing
}

bool IPCServer::Connected() const {
  MachPortManagerInterface *manager = mach_port_manager_;
  if (manager == nullptr) {
    manager = Singleton<DefaultServerMachPortManager>::get();
  }

  return manager->IsServerRunning(name_);
}

void IPCServer::Loop() {
  MachPortManagerInterface *manager = mach_port_manager_;
  if (manager == nullptr) {
    manager = Singleton<DefaultServerMachPortManager>::get();
  }

  // Obtain the server port
  mach_port_t server_port;
  if (manager == nullptr || !manager->GetMachPort(name_, &server_port)) {
    LOG(ERROR) << "Failed to reserve the port.";
    return;
  }

  mach_ipc_send_message send_message;
  mach_ipc_receive_message receive_message;
  mach_msg_header_t *send_header, *receive_header;
  kern_return_t kr;
  bool finished = false;
  char response[IPC_RESPONSESIZE];
  while (!finished) {
    // Receive request
    receive_header = &(receive_message.header);
    receive_header->msgh_local_port = server_port;
    receive_header->msgh_size = sizeof(receive_message);
    kr = mach_msg(receive_header,
                  MACH_RCV_MSG,  // no timeout when receiving clients
                  0,             // send size is 0 because receiving
                  receive_header->msgh_size,  // receive size
                  server_port,                // receive port
                  MACH_MSG_TIMEOUT_NONE,      // no timeout
                  MACH_PORT_NULL);  // notification port in case of error

    if (kr != MACH_MSG_SUCCESS) {
      LOG(ERROR) << "Something around mach ports goes wrong: " << kr;
      break;
    }
    if (receive_header->msgh_id != IPC_PROTOCOL_VERSION) {
      LOG(ERROR) << "Invalid message";
      continue;
    }

    size_t response_size = IPC_RESPONSESIZE;
    if (!Process(static_cast<char *>(receive_message.data.address),
                 receive_message.data.size, response, &response_size)) {
      LOG(INFO) << "Process() returns false.  Quit the wait loop.";
      finished = true;
    }

    vm_deallocate(mach_task_self(), (vm_address_t)receive_message.data.address,
                  receive_message.data.size);
    // Send response
    send_header = &(send_message.header);
    send_header->msgh_bits = MACH_MSGH_BITS_LOCAL(receive_header->msgh_bits) |
                             MACH_MSGH_BITS_COMPLEX;  // To enable OOL message
    send_header->msgh_size = sizeof(send_message);
    send_header->msgh_local_port = MACH_PORT_NULL;
    send_header->msgh_remote_port = receive_header->msgh_remote_port;
    send_header->msgh_id = receive_header->msgh_id;
    send_message.body.msgh_descriptor_count = 1;
    send_message.data.address = response;
    send_message.data.size = response_size;
    // Doesn't deallocate data immediately
    send_message.data.deallocate = false;
    send_message.data.copy = MACH_MSG_VIRTUAL_COPY;  // Copy on write
    send_message.data.type = MACH_MSG_OOL_DESCRIPTOR;
    send_message.count = send_message.data.size;

    kr = mach_msg(send_header,
                  MACH_SEND_MSG | MACH_SEND_TIMEOUT,  // send with timeout
                  send_header->msgh_size,             // send size
                  0,                // receive size is 0 because sending
                  MACH_PORT_NULL,   // receive port
                  timeout_,         // timeout
                  MACH_PORT_NULL);  // notification port in case of error
    if (kr != MACH_MSG_SUCCESS) {
      LOG(ERROR) << "Something around mach ports goes wrong: " << kr;
      continue;
    }
  }
}

void IPCServer::Terminate() { server_thread_->Terminate(); }

}  // namespace mozc

#endif  // __APPLE__
