/* -*- Mode: C++; tab-width: 8; c-basic-offset: 2; indent-tabs-mode: nil; -*- */

#include "ExportImportCheckpoints.h"

#include <fcntl.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/un.h>

#include <sstream>

#include "log.h"
#include "main.h"
#include "WaitManager.h"

using namespace std;

/* Clients connect to the checkpoints socket and may send a one-byte command.
   Currently the only valid command value is 0: create a checkpoint.
*/

namespace rr {

bool parse_export_checkpoints(const string& arg, FrameTime& export_checkpoints_event,
                              int& export_checkpoints_count, string& export_checkpoints_socket) {
  size_t first_comma = arg.find(',');
  if (first_comma == string::npos) {
    fprintf(stderr, "Missing <NUM> parameter for --export-checkpoints");
    return false;
  }
  size_t second_comma = arg.find(',', first_comma + 1);
  if (second_comma == string::npos) {
    fprintf(stderr, "Missing <FILE> parameter for --export-checkpoints");
    return false;
  }
  char* endptr;
  string event_str = arg.substr(0, first_comma);
  export_checkpoints_event = strtoul(event_str.c_str(), &endptr, 0);
  if (*endptr) {
    fprintf(stderr, "Invalid <EVENT> for --export-checkpoints: %s\n", event_str.c_str());
    return false;
  }
  string num_str = arg.substr(first_comma + 1, second_comma - (first_comma + 1));
  export_checkpoints_count = strtoul(num_str.c_str(), &endptr, 0);
  if (*endptr) {
    fprintf(stderr, "Invalid <NUM> for --export-checkpoints: %s\n", num_str.c_str());
    return false;
  }
  export_checkpoints_socket = arg.substr(second_comma + 1);
  return true;
}

ScopedFd bind_export_checkpoints_socket(int count, const string& socket_file_name) {
  unlink(socket_file_name.c_str());

  ScopedFd sock = ScopedFd(socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0));
  if (!sock.is_open()) {
    FATAL() << "Can't create Unix socket " << socket_file_name;
  }
  if (socket_file_name.size() + 1 > sizeof(sockaddr_un::sun_path)) {
    FATAL() << "Socket file name " << socket_file_name << " too long";
  }
  int reuse = 1;
  int ret = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
  if (ret < 0) {
    FATAL() << "Failed to set SO_REUSEADDR";
  }
  sockaddr_un addr;
  addr.sun_family = AF_UNIX;
  strcpy(addr.sun_path, socket_file_name.c_str());
  ret = ::bind(sock, (const sockaddr*)&addr, sizeof(addr));
  if (ret < 0) {
    FATAL() << "Can't bind Unix socket " << socket_file_name;
  }
  ret = listen(sock, count);
  if (ret < 0) {
    FATAL() << "Can't listen on Unix socket " << socket_file_name;
  }
  return sock;
}

static void send_all(ScopedFd& sock, const void* vbuf, size_t count) {
  const char* buf = static_cast<const char*>(vbuf);
  while (count > 0) {
    ssize_t ret = send(sock, buf, count, 0);
    if (ret <= 0) {
      FATAL() << "Failed to send complete message";
    }
    count -= ret;
    buf += ret;
  }
}

static void recv_all(ScopedFd& sock, void* vbuf, size_t count) {
  char* buf = static_cast<char*>(vbuf);
  while (count > 0) {
    ssize_t ret = recv(sock, buf, count, 0);
    if (ret <= 0) {
      FATAL() << "Failed to recv complete message";
    }
    count -= ret;
    buf += ret;
  }
}

static void setup_child_fds(vector<int> fds, CommandForCheckpoint& command_for_checkpoint) {
  command_for_checkpoint.exit_notification_fd = ScopedFd(fds[0]);
  for (int our_fd = 0; our_fd < 3; ++our_fd) {
    // We deliberately don't set CLOEXEC here since we might want these
    // to be inherited.
    int ret = dup2(fds[our_fd + 1], our_fd);
    if (ret < 0) {
      FATAL() << "Can't dup over stdin/stdout/stderr";
    }
    close(fds[our_fd + 1]);
  }

  for (size_t i = 4; i < fds.size(); ++i) {
    command_for_checkpoint.fds.push_back(ScopedFd(fds[i]));
  }
}

static void set_title(const vector<string>& args) {
  string line = "rr:";
  for (auto& a : args) {
    line += ' ';
    line += a;
  }

  char* arg0 = saved_argv0();
  size_t space = saved_argv0_space() - 1;
  if (space < 3) {
    return;
  }
  // To simplify things, instead of moving the environment around when the new command is too long,
  // we just truncate it.
  if (line.size() > space) {
    line[space - 3] = line[space - 2] = line[space - 1] = '.';
    line.resize(space);
  }
  memcpy(arg0, line.data(), line.size());
  memset(arg0 + line.size(), 0, space - line.size());
}

CommandForCheckpoint export_checkpoints(ReplaySession::shr_ptr session, int count, ScopedFd& sock,
    const std::string&) {
  if (!session->can_clone()) {
    FATAL() << "Can't create checkpoints at this time, aborting: " << session->current_frame_time();
  }

  CommandForCheckpoint command_for_checkpoint;

  vector<pid_t> children;
  for (int i = 0; i < count; ++i) {
    ScopedFd client = ScopedFd(accept4(sock, nullptr, nullptr, SOCK_CLOEXEC));
    if (!client.is_open()) {
      FATAL() << "Failed to accept client connection";
    }

    ssize_t priority;
    recv_all(client, &priority, sizeof(priority));
    ssize_t ret = setpriority(PRIO_PROCESS, 0, priority);
    if (ret < 0) {
      if (errno == EACCES) {
        LOG(warn) << "Failed to increase priority";
      } else {
        FATAL() << "Failed setpriority";
      }
    }

    size_t fds_size;
    recv_all(client, &fds_size, sizeof(fds_size));

    // Do the SCM_RIGHTS dance to receive file descriptors
    msghdr msg;
    memset(&msg, 0, sizeof(msg));
    char dummy_buf;
    iovec iov = { &dummy_buf, 1 };
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    vector<uint8_t> cbuf;
    size_t data_len = sizeof(int)*fds_size;
    cbuf.resize(CMSG_SPACE(data_len));
    msg.msg_control = cbuf.data();
    msg.msg_controllen = cbuf.size();
    ret = recvmsg(client, &msg, MSG_CMSG_CLOEXEC);
    if (ret != 1) {
      FATAL() << "Failed to read fds";
    }
    cmsghdr* cmsg = CMSG_FIRSTHDR(&msg);
    if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS || cmsg->cmsg_len != CMSG_LEN(data_len)) {
      FATAL() << "Invalid cmsg metadata";
    }
    vector<int> fds_data;
    fds_data.resize(fds_size);
    memcpy(fds_data.data(), CMSG_DATA(cmsg), data_len);

    size_t arg_count;
    recv_all(client, &arg_count, sizeof(arg_count));
    vector<string> args;
    for (size_t i = 0; i < arg_count; ++i) {
      size_t arg_size;
      recv_all(client, &arg_size, sizeof(arg_size));
      vector<char> arg;
      arg.resize(arg_size);
      recv_all(client, arg.data(), arg_size);
      args.push_back(string(arg.data(), arg.size()));
    }

    ReplaySession::shr_ptr checkpoint = session->clone();
    int parent_to_child_fds[2];
    ret = pipe(parent_to_child_fds);
    if (ret < 0) {
      FATAL() << "Can't pipe";
    }
    ScopedFd parent_to_child_read(parent_to_child_fds[0]);
    ScopedFd parent_to_child_write(parent_to_child_fds[1]);

    checkpoint->prepare_to_detach_tasks();

    // We need to create a new control socket for the child, we can't use the shared control socket
    // safely in multiple processes.
    int sockets[2];
    ret = socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sockets);
    if (ret < 0) {
      FATAL() << "socketpair failed";
    }
    ScopedFd new_tracee_socket(sockets[0]);
    ScopedFd new_tracee_socket_receiver(sockets[1]);

    pid_t child = fork();
    if (!child) {
      set_title(args);
      session->forget_tasks();
      command_for_checkpoint.args = std::move(args);
      command_for_checkpoint.session = std::move(checkpoint);
      setup_child_fds(fds_data, command_for_checkpoint);
      char ch;
      ret = read(parent_to_child_read, &ch, 1);
      if (ret != 1) {
        FATAL() << "Failed to read parent notification";
      }
      command_for_checkpoint.session->reattach_tasks(std::move(new_tracee_socket),
        std::move(new_tracee_socket_receiver));
      return command_for_checkpoint;
    }
    children.push_back(child);

    checkpoint->detach_tasks(child, new_tracee_socket_receiver);
    ret = write(parent_to_child_write, "x", 1);
    if (ret != 1) {
      FATAL() << "Failed to write parent notification";
    }
    for (auto d : fds_data) {
      close(d);
    }
  }

  // Wait for and reap all children
  for (size_t i = 0; i < children.size(); ++i) {
    WaitResult result = WaitManager::wait_exit(WaitOptions(children[i]));
    if (result.code != WAIT_OK) {
      FATAL() << "Failed to wait for child " << children[i];
    }
  }

  return command_for_checkpoint;
}

void notify_normal_exit(ScopedFd& exit_notification_fd) {
  ssize_t ret = write(exit_notification_fd, "", 1);
  if (ret != 1) {
    FATAL() << "Can't send exit notification";
  }
}

int invoke_checkpoint_command(const string& socket_file_name,
    vector<string> args, vector<ScopedFd> fds) {
  ScopedFd sock = ScopedFd(socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0));
  if (!sock.is_open()) {
    FATAL() << "Can't create Unix socket " << socket_file_name;
  }
  if (socket_file_name.size() + 1 > sizeof(sockaddr_un::sun_path)) {
    FATAL() << "Socket file name " << socket_file_name << " too long";
  }
  sockaddr_un addr;
  addr.sun_family = AF_UNIX;
  strcpy(addr.sun_path, socket_file_name.c_str());
  while (true) {
    ssize_t ret = connect(sock, (const sockaddr*)&addr, sizeof(addr));
    if (ret < 0) {
      // We might try to connect between the socket being bound and listen()ed on
      if (errno == ENOENT || errno == ECONNREFUSED) {
        sleep(1);
        continue;
      }
      FATAL() << "Can't connect socket " << socket_file_name;
    }
    break;
  }

  ssize_t ret = getpriority(PRIO_PROCESS, 0);
  if (ret < 0) {
    FATAL() << "Failed getpriority";
  }
  send_all(sock, &ret, sizeof(ret));

  size_t total_fds = 4 + fds.size();
  send_all(sock, &total_fds, sizeof(total_fds));

  int exit_notification_pipe_fds[2];
  ret = pipe(exit_notification_pipe_fds);
  if (ret < 0) {
    FATAL() << "Failed pipe";
  }

  // Do the SCM_RIGHTS dance to send file descriptors.
  msghdr msg;
  memset(&msg, 0, sizeof(msg));
  char ch = 'x';
  iovec iov = { &ch, 1 };
  msg.msg_iov = &iov;
  msg.msg_iovlen = 1;
  vector<uint8_t> cbuf;
  size_t data_len = sizeof(int)*total_fds;
  cbuf.resize(CMSG_SPACE(data_len));
  msg.msg_control = cbuf.data();
  msg.msg_controllen = cbuf.size();
  cmsghdr* cmsg = CMSG_FIRSTHDR(&msg);
  cmsg->cmsg_level = SOL_SOCKET;
  cmsg->cmsg_type = SCM_RIGHTS;
  cmsg->cmsg_len = CMSG_LEN(data_len);
  char* cmsg_data = (char*)CMSG_DATA(cmsg);
  vector<int> int_fds;
  int_fds.push_back(exit_notification_pipe_fds[1]);
  int_fds.push_back(0);
  int_fds.push_back(1);
  int_fds.push_back(2);
  for (auto& fd : fds) {
    int_fds.push_back(fd);
  }
  memcpy(cmsg_data, int_fds.data(), sizeof(int)*int_fds.size());
  ret = sendmsg(sock, &msg, 0);
  if (ret != 1) {
    FATAL() << "Can't send file descriptors";
  }
  close(exit_notification_pipe_fds[1]);
  // Close stdin but keep stdout/stderr open in case we need to print something ourselves.
  close(0);

  size_t arg_count = args.size();
  send_all(sock, &arg_count, sizeof(arg_count));
  for (auto& arg : args) {
    size_t arg_size = arg.size();
    send_all(sock, &arg_size, sizeof(arg_size));
    send_all(sock, arg.data(), arg_size);
  }

  ret = read(exit_notification_pipe_fds[0], &ch, 1);
  if (ret < 0) {
    FATAL() << "Can't read from notification pipe";
  }
  if (ret == 0) {
    // abnormal termination
    return 1;
  }
  return 0;
}

} // namespace rr
