#include "platform.h"
#include "gnunet_util_lib.h"
#include "gnunet_testbed_service.h"


/**
 * Generic logging shorthand
 */
#define LOG(kind, ...)                           \
  GNUNET_log (kind, __VA_ARGS__)

/**
 * Debug logging shorthand
 */
#define LOG_DEBUG(...)                          \
  LOG (GNUNET_ERROR_TYPE_DEBUG, __VA_ARGS__)

/**
 * Global result
 */
static int ret;

/**
 * The child process we spawn
 */
static struct GNUNET_OS_Process *child;

/**
 * The arguments including the binary to spawn
 */
static char **argv2;

/**
 * Pipe used to communicate shutdown via signal.
 */
static struct GNUNET_DISK_PipeHandle *sigpipe;

/**
 * Filename of the unique file
 */
static char *fn;

/**
 * Handle to the unique file
 */
static int fh;

/**
 * The return code of the binary
 */
static unsigned long child_exit_code;

/**
 * The process status of the child
 */
static enum GNUNET_OS_ProcessStatusType child_status;

/**
 * Task to kill the child
 */
static struct GNUNET_SCHEDULER_Task *terminate_task_id;

/**
 * Task to kill the child
 */
static struct GNUNET_SCHEDULER_Task *child_death_task_id;

/**
 * The shutdown task
 */
static void
shutdown_task (void *cls)
{
  if (0 != child_exit_code)
  {
    LOG (GNUNET_ERROR_TYPE_WARNING, "Child exited with error code: %lu\n",
         child_exit_code);
    ret = 128 + (int) child_exit_code;
  }
  if (0 != fh)
  {
    close (fh);
  }
  if ((NULL != fn) && (0 != unlink (fn)))
  {
    GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, "open");
    ret = GNUNET_SYSERR;
  }
}


static void
terminate_task (void *cls)
{
  static int hard_kill;

  GNUNET_assert (NULL != child);
  terminate_task_id =
    GNUNET_SCHEDULER_add_shutdown (&terminate_task, NULL);
  if (0 != hard_kill)
  {
    switch (hard_kill)
    {
    case 1:
    case 2:
      LOG (GNUNET_ERROR_TYPE_WARNING,
           "%d more interrupts needed to send SIGKILL to the child\n",
           3 - hard_kill);
      hard_kill++;
      return;

    case 3:
      GNUNET_break (0 == GNUNET_OS_process_kill (child, SIGKILL));
      return;
    }
  }
  hard_kill++;
  GNUNET_break (0 == GNUNET_OS_process_kill (child, GNUNET_TERM_SIG));
  LOG (GNUNET_ERROR_TYPE_INFO, _ ("Waiting for child to exit.\n"));
}


/**
 * Task triggered whenever we receive a SIGCHLD (child
 * process died).
 *
 * @param cls closure, NULL if we need to self-restart
 */
static void
child_death_task (void *cls)
{
  const struct GNUNET_DISK_FileHandle *pr;
  char c[16];
  const struct GNUNET_SCHEDULER_TaskContext *tc;


  pr = GNUNET_DISK_pipe_handle (sigpipe, GNUNET_DISK_PIPE_END_READ);
  child_death_task_id = NULL;
  tc = GNUNET_SCHEDULER_get_task_context ();
  if (0 == (tc->reason & GNUNET_SCHEDULER_REASON_READ_READY))
  {
    child_death_task_id =
      GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL,
                                      pr, &child_death_task, NULL);
    return;
  }
  /* consume the signal */
  GNUNET_break (0 < GNUNET_DISK_file_read (pr, &c, sizeof(c)));
  LOG_DEBUG ("Child died\n");
  GNUNET_SCHEDULER_cancel (terminate_task_id);
  terminate_task_id = NULL;
  GNUNET_assert (GNUNET_OK == GNUNET_OS_process_status (child, &child_status,
                                                        &child_exit_code));
  GNUNET_OS_process_destroy (child);
  child = NULL;
  GNUNET_SCHEDULER_add_now (&shutdown_task, NULL);
}


static void
destroy_hosts (struct GNUNET_TESTBED_Host **hosts, unsigned int nhosts)
{
  unsigned int host;

  GNUNET_assert (NULL != hosts);
  for (host = 0; host < nhosts; host++)
    if (NULL != hosts[host])
      GNUNET_TESTBED_host_destroy (hosts[host]);
  GNUNET_free (hosts);
  hosts = NULL;
}


/**
 * The main scheduler run task
 *
 * @param cls NULL
 */
static void
run (void *cls)
{
  struct GNUNET_TESTBED_Host **hosts;
  const struct GNUNET_CONFIGURATION_Handle *null_cfg;
  char *tmpdir;
  char *hostname;
  size_t hostname_len;
  unsigned int nhosts;

  null_cfg = GNUNET_CONFIGURATION_create ();
  nhosts = GNUNET_TESTBED_hosts_load_from_loadleveler (null_cfg, &hosts);
  if (0 == nhosts)
  {
    GNUNET_break (0);
    ret = GNUNET_SYSERR;
    return;
  }
  hostname_len = GNUNET_OS_get_hostname_max_length ();
  hostname = GNUNET_malloc (hostname_len);
  if (0 != gethostname (hostname, hostname_len))
  {
    LOG (GNUNET_ERROR_TYPE_ERROR, "Cannot get hostname.  Exiting\n");
    GNUNET_free (hostname);
    destroy_hosts (hosts, nhosts);
    ret = GNUNET_SYSERR;
    return;
  }
  if (NULL == strstr (GNUNET_TESTBED_host_get_hostname (hosts[0]), hostname))
  {
    LOG_DEBUG ("Exiting as `%s' is not the lowest host\n", hostname);
    GNUNET_free (hostname);
    ret = GNUNET_OK;
    return;
  }
  LOG_DEBUG ("Will be executing `%s' on host `%s'\n", argv2[0], hostname);
  GNUNET_free (hostname);
  destroy_hosts (hosts, nhosts);
  tmpdir = getenv ("TMPDIR");
  if (NULL == tmpdir)
    tmpdir = getenv ("TMP");
  if (NULL == tmpdir)
    tmpdir = getenv ("TEMP");
  if (NULL == tmpdir)
    tmpdir = "/tmp";
  (void) GNUNET_asprintf (&fn, "%s/gnunet-testbed-spawn.lock", tmpdir);
  /* Open the unique file; we can create it then we can spawn the child process
     else we exit */
  fh = open (fn, O_CREAT | O_EXCL | O_CLOEXEC,
             S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
  if (-1 == fh)
  {
    if (EEXIST == errno)
    {
      LOG_DEBUG ("Lock file already created by other process.  Exiting\n");
      ret = GNUNET_OK;
      return;
    }
    GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, "open");
    ret = GNUNET_SYSERR;
    return;
  }
  /* Spawn the new process here */
  LOG (GNUNET_ERROR_TYPE_INFO, _ ("Spawning process `%s'\n"), argv2[0]);
  child = GNUNET_OS_start_process_vap (GNUNET_NO, GNUNET_OS_INHERIT_STD_ALL,
                                       NULL,
                                       NULL, NULL,
                                       argv2[0], argv2);
  if (NULL == child)
  {
    GNUNET_break (0);
    ret = GNUNET_SYSERR;
    GNUNET_SCHEDULER_add_now (&shutdown_task, NULL);
    return;
  }
  ret = GNUNET_OK;
  terminate_task_id =
    GNUNET_SCHEDULER_add_shutdown (&terminate_task, NULL);
  child_death_task_id =
    GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL,
                                    GNUNET_DISK_pipe_handle (sigpipe,
                                                             GNUNET_DISK_PIPE_END_READ),
                                    &child_death_task, NULL);
}


/**
 * Signal handler called for SIGCHLD.
 */
static void
sighandler_child_death ()
{
  static char c;
  int old_errno = errno;        /* back-up errno */

  GNUNET_break (1 ==
                GNUNET_DISK_file_write (GNUNET_DISK_pipe_handle
                                          (sigpipe, GNUNET_DISK_PIPE_END_WRITE),
                                        &c, sizeof(c)));
  errno = old_errno;            /* restore errno */
}


/**
 * Execution start point
 */
int
main (int argc, char *argv[])
{
  struct GNUNET_SIGNAL_Context *shc_chld;
  unsigned int cnt;

  ret = -1;
  if (argc < 2)
  {
    printf ("Need arguments: gnunet-testbed-mpi-spawn <cmd> <cmd_args>");
    return 1;
  }
  if (GNUNET_OK != GNUNET_log_setup ("gnunet-testbed-spawn", NULL, NULL))
  {
    GNUNET_break (0);
    return 1;
  }
  if (NULL == (sigpipe = GNUNET_DISK_pipe (GNUNET_DISK_PF_NONE)))
  {
    GNUNET_break (0);
    ret = GNUNET_SYSERR;
    return 1;
  }
  shc_chld =
    GNUNET_SIGNAL_handler_install (GNUNET_SIGCHLD, &sighandler_child_death);
  if (NULL == shc_chld)
  {
    LOG (GNUNET_ERROR_TYPE_ERROR, "Cannot install a signal handler\n");
    return 1;
  }
  argv2 = GNUNET_malloc (sizeof(char *) * argc);
  for (cnt = 1; cnt < argc; cnt++)
    argv2[cnt - 1] = argv[cnt];
  GNUNET_SCHEDULER_run (run, NULL);
  GNUNET_free (argv2);
  GNUNET_SIGNAL_handler_uninstall (shc_chld);
  shc_chld = NULL;
  GNUNET_DISK_pipe_close (sigpipe);
  GNUNET_free (fn);
  if (GNUNET_OK != ret)
    return ret;
  return 0;
}
