/*
 * bg.c -- handles:
 *   moving the process to the background, i.e. forking, while keeping threads
 *   happy.
 */
/*
 * Copyright (C) 1997 Robey Pointer
 * Copyright (C) 1999 - 2025 Eggheads Development Team
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

#include "main.h"
#include <signal.h>
#include "bg.h"

extern char pid_file[];

/* When threads are started during eggdrop's init phase, we can't simply
 * fork() later on, because that only copies the VM space over and
 * doesn't actually duplicate the threads.
 *
 * To work around this, we fork() very early and let the parent process
 * wait in an event loop. As soon as the init phase is completed and we
 * would normally move to the background, the child process simply
 * messages it's parent that it may now quit. This allows us to control
 * the terminal long enough to, e.g. properly feed error messages to
 * cron scripts and let the user abort the loading process by hitting
 * CTRL+C.
 *
 *
 * [ Parent process                  ] [ Child process                   ]
 *
 *   main()
 *     ...
 *     bg_prepare_split()
 *       fork()                              - created -
 *       waiting in event loop.            continuing execution in main()
 *                                         ...
 *                                         completed init.
 *                                         bg_do_detach()
 *                                           message parent new PID file-
 *                                            name.
 *       receives new PID filename
 *         message.
 *                                           message parent to quit.
 *       receives quit message.            continues to main loop.
 *       writes PID to file.               ...
 *       exits.
 */

/* Format of messages sent from the newly forked process to the
 * original process, connected to the terminal.
 */
typedef struct {
  enum {
    BG_COMM_QUIT,      /* Quit original process. Write PID file, detach. */
    BG_COMM_ABORT,     /* Quit original process.                         */
    BG_COMM_TRANSFERPF /* Sending pid_file.                              */
  } comm_type;
  union {
    struct {           /* Data for BG_COMM_TRANSFERPF.                   */
      int len;         /* Length of the file name.                       */
    } transferpf;
  } comm_data;
} bg_comm_t;

typedef enum {
  BG_NONE = 0,         /* No forking has taken place yet.                */
  BG_SPLIT,            /* I'm the newly forked process.                  */
  BG_PARENT            /* I'm the original process.                      */
} bg_state_t;

typedef struct {
  int comm_recv;        /* Receives messages from the child process.     */
  int comm_send;        /* Sends messages to the parent process.         */
  bg_state_t state;     /* Current state, see above enum descriptions.   */
  pid_t child_pid;      /* PID of split process.                         */
} bg_t;

static bg_t bg = { 0 };


/* Do everything we normally do after we have split off a new
 * process to the background. This includes writing a PID file
 * and informing the user of the split.
 */
static void bg_do_detach(pid_t p)
{
  FILE *fp;

  /* Need to attempt to write pid now, not later. */
  unlink(pid_file);
  fp = fopen(pid_file, "w");
  if (fp != NULL) {
    fprintf(fp, "%li\n", (long) p);
    if (fflush(fp)) {
      /* Kill bot in case a botchk is run from crond. */
      printf(EGG_NOWRITE, pid_file);
      printf("  Try freeing some disk space\n");
      fclose(fp);
      unlink(pid_file);
      exit(1);
    }
    fclose(fp);
  } else
    printf(EGG_NOWRITE, pid_file);
  printf("Launched into the background  (pid: %li)\n\n", (long) p);
  setpgid(p, p);
  exit(0);
}

void bg_prepare_split(void)
{
  pid_t p;
  bg_comm_t message;

  if (!fork_before_tcl())
    return;

  /* Create a pipe between parent and split process, fork to create a
   * parent and a split process and wait for messages on the pipe. */
  {
    int comm_pair[2];

    if (pipe(comm_pair) < 0)
      fatal("CANNOT OPEN PIPE.", 0);

    bg.comm_recv = comm_pair[0];
    bg.comm_send = comm_pair[1];
  }

  p = fork();
  if (p == -1)
    fatal("CANNOT FORK PROCESS.", 0);
  if (p == 0) {
    bg.state = BG_SPLIT;
    return;
  } else {
    bg.child_pid = p;
    bg.state = BG_PARENT;
  }

  while (read(bg.comm_recv, &message, sizeof(message)) > 0) {
    switch (message.comm_type) {
    case BG_COMM_QUIT:
      bg_do_detach(p);
      break;
    case BG_COMM_ABORT:
      exit(1);
      break;
    case BG_COMM_TRANSFERPF:
      /* Now transferring file from split process.
       */
      if (message.comm_data.transferpf.len >= 40)
        message.comm_data.transferpf.len = 40 - 1;
      /* Next message contains data. */
      if (read(bg.comm_recv, pid_file, message.comm_data.transferpf.len) <= 0)
        goto error;
      pid_file[message.comm_data.transferpf.len] = 0;
      break;
    }
  }

error:
  /* We only reach this point in case of an error.
   */
  fatal("COMMUNICATION THROUGH PIPE BROKE.", 0);
}

/* Transfer contents of pid_file to parent process. This is necessary,
 * as the pid_file[] buffer has changed in this fork by now, but the
 * parent needs an up-to-date version.
 */
static void bg_send_pidfile(void)
{
  bg_comm_t message;

  message.comm_type = BG_COMM_TRANSFERPF;
  message.comm_data.transferpf.len = strlen(pid_file);

  /* Send type message. */
  if (write(bg.comm_send, &message, sizeof(message)) < 0)
    goto error;
  /* Send data. */
  if (write(bg.comm_send, pid_file, message.comm_data.transferpf.len) < 0)
    goto error;
  return;
error:
  fatal("COMMUNICATION THROUGH PIPE BROKE.", 0);
}

void bg_send_quit(bg_quit_t q)
{
  if (!fork_before_tcl())
    return;

  if (bg.state == BG_PARENT) {
    kill(bg.child_pid, SIGKILL);
  } else if (bg.state == BG_SPLIT) {
    bg_comm_t message;

    if (q == BG_QUIT) {
      bg_send_pidfile();
      message.comm_type = BG_COMM_QUIT;
    } else
      message.comm_type = BG_COMM_ABORT;
    /* Send message. */
    if (write(bg.comm_send, &message, sizeof(message)) < 0)
      fatal("COMMUNICATION THROUGH PIPE BROKE.", 0);
  }
}

void bg_do_split(void)
{
  if (fork_before_tcl()) {
    /* Tell our parent process to go away now, as we don't need it anymore. */
    bg_send_quit(BG_QUIT);

  } else {
    /* Split off a new process. */
    int xx = fork();

    if (xx == -1)
      fatal("CANNOT FORK PROCESS.", 0);
    if (xx != 0)
      bg_do_detach(xx);
  }
}
