/*

Copyright (C) 2002 Hayato Fujiwara <h_fujiwara@users.sourceforge.net>
Copyright (C) 2010-2020 Olaf Till <i7tiol@t-online.de>

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 3 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, see <http://www.gnu.org/licenses/>.

*/

#define EXTERNAL_BINARY
#include "config.h"

#include <stdio.h>
#include <error.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <iostream>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/poll.h>
#include <errno.h>
#include <netdb.h>
#include <sys/utsname.h>
#include <netinet/in.h> // reported necessary for FreeBSD-8
#include <signal.h>
#include <string.h>
#include <pthread.h>
#include <sstream>

#if HAVE_UNISTD_H
#include <unistd.h>
#endif

#include "parallel-gnutls-nonoctave.h"
#include "octave-pserver.h"

struct option p_cmdline_options::known_long_opts[N_LONGOPTS + 1] =
  {
    {
      .name    = "use-tls",
      .has_arg = 0, // 0: has no argument
      .flag    = NULL,
      .val     = 1
    },

    {
      .name    = "no-auth",
      .has_arg = 0, // 0: has no argument
      .flag    = NULL,
      .val     = 2
    },

    {
      .name    = "auth-file",
      .has_arg = 1, // 1: requires an argument
      .flag    = NULL,
      .val     = 3
    },

    {
      .name    = "octave-binary",
      .has_arg = 1, // 1: requires an argument
      .flag    = NULL,
      .val     = 4
    },

    {
      .name    = "cmd-port",
      .has_arg = 1, // 1: requires an argument
      .flag    = NULL,
      .val     = 5
    },
    
    {
      .name    = "data-port",
      .has_arg = 1, // 1: requires an argument
      .flag    = NULL,
      .val     = 6
    },

    {
      .name    = "help",
      .has_arg = 0, // 0: has no argument
      .flag    = NULL,
      .val     = 7
    },

    // end marker, must be filled with zeros
    {
      .name    = NULL,
      .has_arg = 0,
      .flag    = NULL,
      .val     = 0
    }
  }; // remember to adjust N_LONGOPTS when adding options

p_cmdline_options::p_cmdline_options (int argc, char * const *argv)
  : opt_use_tls (true), opt_print_help (false),
    opt_cmd_port_string (DEFAULT_CMD_PORT),
    opt_data_port_string (DEFAULT_DATA_PORT)
{
  extern char *optarg; // provided by glibc for getopt and related functions

  opt_cmd_port = atoi (DEFAULT_CMD_PORT);
  opt_data_port = atoi (DEFAULT_DATA_PORT);

  for (;;)
    {
      int opt = getopt_long (argc, argv, "h", known_long_opts, NULL);

      if (opt == -1)
        break;

      switch (opt)
        {
        case '?':
          // getopt_long has already printed an error message
          exit (1);
          break;
        case 1:
          opt_use_tls = true;
          break;
        case 2:
          opt_use_tls = false;
          break;
        case 3:
          opt_auth_file = optarg;
          break;
        case 4:
          opt_octave_binary = optarg;
          break;
        case 5:
          opt_cmd_port = atoi (optarg);
          opt_cmd_port_string = optarg;
          break;
        case 6:
          opt_data_port = atoi (optarg);
          opt_data_port_string = optarg;
          break;
        case 'h':
        case 7:
          opt_print_help = true;
          break;
        default:
          error (0, 0, "internal error, unhandled known option");
          exit (1);
          break;
        }
    }
}

static int check_and_write_pidfile (const char *fname, int pid)
{
  int fd, ret, nread;

  while ((fd = open (fname, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR)) < 0 &&
         errno == EINTR);
  if (fd < 0)
    return -1;

  struct flock fl;
  fl.l_type = F_WRLCK;
  fl.l_whence = SEEK_SET;
  fl.l_start = 0;
  fl.l_len = 0;
  while ((ret = fcntl (fd, F_SETLKW, &fl)) < 0 && errno == EINTR);
  if (ret < 0)
    {
      close (fd);
      return -1;
    }

  char b;
  while ((nread = read (fd, &b, 1)) < 0 && errno == EINTR);
  if (nread < 0 || nread == 1)
    {
      close (fd);
      return -1;
    }

  if (lseek (fd, 0, SEEK_SET))
    {
      close (fd);
      return -1;
    }

  FILE *fstr;
  if (! (fstr = fdopen (fd, "w+")))
    {
      close (fd);
      return -1;
    }

  if (fprintf (fstr, "%i", pid) < 0)
    {
      fclose (fstr);
      return -1;
    }

  if (fclose (fstr) == EOF)
    return -1;
  else
    return 0;
}

static
const std::string &stash_or_get_hostname (std::string hn)
{
  static std::string stored_hostname;
  static std::string empty_string;

  if (hn.length ())
    {
      stored_hostname = hn;

      return empty_string;
    }

  return stored_hostname;
}

void print_hostname_atexit (void)
{
  printf ("exiting, %s\n", stash_or_get_hostname (std::string ()).c_str ());
}

static
const std::string& stash_or_get_pidfile (std::string path)
{
  static std::string stored_pidfile;
  static std::string empty_string;

  if (path.length ())
    {
      stored_pidfile = path;

      return empty_string;
    }

  return stored_pidfile;
}

void unlink_pidfile_atexit (void)
{
  unlink (stash_or_get_pidfile (std::string ()).c_str ());
}

void * pull_signals (void *arg)
{
  pthread_t parent_thread = *((pthread_t *) arg);

  int sig = 0;

  sigset_t allsigs;

  if (! sigfillset (&allsigs))
    while (! (sigwait (&allsigs, &sig)
              || sig == SIGTERM
              || sig == SIGHUP));

  pthread_cancel (parent_thread);

  if (pthread_join (parent_thread, NULL))
    {
      // this should not happen and may not exit cleanly
      error (0, 0, "couldn't join parent thread, emergency exit");
      exit (1);
    }

  pthread_exit (0);
}

static
void check_options (p_cmdline_options &opts, int &oct_fd)
{
  if (opts.print_help ())
    {
      std::cout << "octave-pserver is an executable distruted with the package 'parallel'," << std::endl
                << "version " << "..." << ", for GNU/Octave." << std::endl
                << "Copyright (C) 2002 Hayato Fujiwara." << std::endl
                << "Copyright (C) 2010-2019 Olaf Till." << std::endl
                << "This is free software; see the source code for copying conditions." << std::endl
                << "There is ABSOLUTLY NO WARRANTY; not even for MERCHANTABILITY or" << std::endl 
                << "FITNESS FOR A PARTICULAR PURPOSE." << std::endl
                << std::endl
                << "Usage:" << std::endl
                << "See documentation of package 'parallel'." << std::endl
                << "call indirectly from Octave with 'pserver (...)'" << std::endl
                << "or call it directly: octave-pserver --octave-binary <octave binary> [further options]" << std::endl
                << std::endl
                << "Options:" << std::endl
                << std::endl
                << "-h/--help       : prints this helptext" << std::endl
                << "--octave-binary : absolute path to octave binary" << std::endl
                << "--use-tls       : use TLS (default) for authentication and ecryption" << std::endl
                << "--no-auth       : no authentication or encryption" << std::endl
                << "--auth-file     : path to authentication file" << std::endl
                << "--cmd-port      : port for command channel, default "
                << DEFAULT_CMD_PORT << std::endl
                << "--data-port     : port for data channel, default "
                << DEFAULT_DATA_PORT << std::endl;
      exit (0);
    }
  if (opts.octave_binary ().size () < 2
      || opts.octave_binary ()[0] != '/')
    {
      error (0, 0, "octave_binary must be given and must be an absolute filename");
      exit (1);
    }
  // open the binary to avoid racing conditions
  oct_fd = open (opts.octave_binary ().c_str (), O_RDONLY | O_CLOEXEC);
  if (oct_fd == -1)
    {
      perror ("open");
      exit (1);
    }
  struct stat statbuf;
  if (fstat (oct_fd, &statbuf))
    {
      perror ("fstat");
      exit (1);
    }
  if (! S_ISREG (statbuf.st_mode))
    {
      error (0, 0, "octave_binary is no regular file");
      exit (1);
    }
  if (! (statbuf.st_mode & S_IXUSR))
    {
      error (0, 0, "octave_binary is not executable");
      exit (1);
    }
  if (statbuf.st_mode & S_ISUID)
    {
      error (0, 0, "octave_binary must not have the set-user-ID bit set");
      exit (1);
    }
  if (opts.cmd_port () < 1024 || opts.cmd_port () > 65535)
    {
      error (0, 0, "cmd_port must be >= 1024 and <= 65535");
      exit (1);
    }
  if (opts.data_port () < 1024 || opts.data_port () > 65535)
    {
      error (0, 0, "data_port must be >= 1024 and <= 65535");
      exit (1);
    }
  if (opts.cmd_port () == opts.data_port ())
    {
      error (0, 0, "cmd_port and data_port must not be equal");
      exit (1);
    }
#ifndef HAVE_LIBGNUTLS
  if (opts.use_tls ())
    {
      error (0, 0, "TLS not available");
      exit (1);
    }
#endif
  if (! opts.use_tls () && opts.auth_file ().length ())
    error (0, 0, "no TLS used, option 'auth_file' has no effect");
}

// For execv, we need to copy some const char* to non-const. We can
// use new without delete since this function is only called between
// fork() and exec().
static
char * copy_to_non_const (const char *str)
{
  char *ret = new char [strlen (str)];

  strcpy (ret, str);

  return ret;
}

int
main (int argc, char **argv)
{
  std::string fname ("octave-pserver");

  // don't run as root and try not to run with elevated privileges
  uid_t uid = getuid (), euid = geteuid ();
  if (uid == 0)
    {
      error (0, 0, "running this program as root is regarded as dangerous, aborting");
      exit (1);
    }
  if (uid < 0 || uid != euid)
    {
      error (0, 0, "The program seems to be run with elevated privileges. This is regarded as dangerous, aborting.");
      exit (1);
    }

  p_cmdline_options opts (argc, argv);

  int oct_fd; // octave_binary will be opened by check_options() to
              // avoid racing conditions
  check_options (opts, oct_fd);

  struct utsname utsname_buf;
  if (uname (&utsname_buf))
    {
      perror ("uname");
      exit (1);
    }
  std::string hostname (utsname_buf.nodename);
  std::string pidname ("/tmp/.octave-");
  pidname = pidname + hostname + ".pid";

  // initialize atexit function to write "exiting, hostname\n"
  stash_or_get_hostname (hostname);

  int tp;
  if ((tp = fork ()))
    {
      if (tp == -1)
        {
          error (0, 0, "could not fork");
          exit (1);
        }

      exit (0);
    }

  // register atexit function to write "exiting, hostname\n"
  if (atexit (print_hostname_atexit))
    {
      perror ("atexit");
      exit (1);
    }

  // block signals
  sigset_t blockedsigs;
  if (sigfillset (&blockedsigs)
      || pthread_sigmask (SIG_BLOCK, &blockedsigs, NULL))
    {
      error (0, 0, "could not block signals");
      exit (1);
    }

  /* write pidfile */
  if (check_and_write_pidfile (pidname.c_str (), getpid ()))
    {
      std::cerr << "octave-pserver: " << hostname.c_str ()
                << ": can't write pidfile, server possibly already running"
                << std::endl;
      exit (1);
    }

  // register atexit function which unlinks pidfile
  stash_or_get_pidfile (pidname);
  if (atexit (unlink_pidfile_atexit))
    {
      perror ("atexit");
      exit (1);
    }

  // install signal handling pthread which exits the whole process at
  // termination signals
  pthread_t parent_thread = pthread_self ();
  pthread_t sighandler_thread;
  if (pthread_create (&sighandler_thread, NULL, pull_signals, &parent_thread))
    {
      perror ("pthread_create");
      exit (1);
    }

  std::cout << pidname.c_str () << std::endl;

  /* Redirect stdin and stdout to /dev/null. */
  if (! freopen ("/dev/null", "r", stdin))
    {
      perror ("freopen");
      exit (1);
    }
  if (! freopen ("/dev/null", "w", stdout))
    {
      perror ("freopen");
      exit (1);
    }

  std::string errname ("/tmp/octave_error-");
  errname = errname + hostname.c_str () + ".log";
  struct stat s_stat;
  if (stat (errname.c_str (), &s_stat) == 0)
    {
      std::string bakname ("/tmp/octave_error-");
      bakname = bakname + hostname.c_str () + ".bak";
      if (rename (errname.c_str (), bakname.c_str ()))
        {
          perror ("rename");
          exit (1);
        }
    }
  if (! freopen (errname.c_str (), "w", stderr))
    {
      perror ("freopen");
      exit (1);
    } 

  struct addrinfo *ai, hints;

  memset ((void *) &hints, 0, sizeof (hints));
  hints.ai_family = AF_INET;
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_protocol = 0;
  hints.ai_flags = AI_PASSIVE;
  if (getaddrinfo (NULL, opts.cmd_port_string ().c_str (), &hints, &ai))
    {
      perror ("getaddrinfo");
      exit (1);
    }

  // SOCK_CLOEXEC: this file descriptor is only needed here, not in
  // the Octave server, which will use a file descriptor returned by
  // accept()
  int sock = socket (PF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
  if (sock == -1)
    {
      perror ("socket");
      exit (1);
    }

  if (bind (sock, ai->ai_addr, ai->ai_addrlen))
    {
      perror ("bind");
      exit (1);
    }


  if (listen (sock, SOMAXCONN))
    {
      perror ("listen");
      exit (1);
    }


  if (getaddrinfo (NULL, opts.data_port_string ().c_str (), &hints, &ai))
    {
      perror ("getaddrinfo");
      exit (1);
    }

  // _don't_ specify SOCK_CLOEXEC, since the Octave server will call
  // accept() on this file descriptor
  int dsock = socket (PF_INET, SOCK_STREAM, 0);
  if (dsock == -1)
    {
      perror ("socket");
      exit (1);
    }

  if (bind (dsock, ai->ai_addr, ai->ai_addrlen))
    {
      perror ("bind");
      exit (1);
    }

  if (listen (dsock, SOMAXCONN))
    {
      perror ("listen");
      exit (1);
    }

  freeaddrinfo (ai);

  int server_no = -1; // for debugging
  for(;;)
    {
      server_no++;
      dsprintf ("server_no: %i, trying to accept command stream at port %s\n",
                server_no, opts.cmd_port_string ().c_str ());
      int asock = accept (sock, 0, 0);
      if (asock == -1)
        {
          perror ("accept");
          exit (1);
        }
      dsprintf ("%i: accepted command stream\n", server_no);
      /* Normal production daemon.  Fork, and have the child process
         the connection.  The parent continues listening. */

      int pid;
      if ((pid = fork ()) == -1)
        {
          perror ("fork");
          exit (1);
        }
      else if (pid == 0) 
        {
          // From here on, use only _exit(), not exit(). The signal
          // handling pthread isn't present in the current process so
          // it can't cause an exit().

          int tp;
          if ((tp = fork ()))
            {
              if (tp == -1)
                {
                  perror ("fork");
                  _exit (1);
                }
              else
                _exit (0);
            }

          dsprintf ("%i: child after fork\n", server_no);

          // empty signal mask
          sigset_t nosigs;
          if (sigemptyset (&nosigs)
              || pthread_sigmask (SIG_SETMASK, &nosigs, NULL))
            {
              error (0, 0, "could not empty signal mask");
              _exit (1);
            }

          // Arguments for exec()ing Octave should include the values
          // of the current variables asock and dsock, hostname, also
          // some representation of use_gnutls and cafile. Also
          // server_no.
          const int nargs = 2; // made to grow, currently these are
                               // the '--eval' option and its argument
          char * args [nargs + 2];
          args[0] = copy_to_non_const (opts.octave_binary ().c_str ());
          args[1] = copy_to_non_const ("--eval");
          std::stringstream eval_arg;
          eval_arg << "pkg load parallel; __octave_server__ ("
                   << asock << ", "
                   << dsock << ", "
                   << (opts.use_tls () ? "true" : "false") << ", "
                   << "'" << opts.auth_file () << "'" << ", "
                   << "'" << hostname << "'" << ", "
                   << server_no
                   << ")";
          std::string eval_arg_str (eval_arg.str ());
          args[2] = copy_to_non_const (eval_arg_str.c_str ());
          args[3] = NULL;
          
          // exec() Octave
          dsprintf ("%i: exec()ing Octave\n", server_no);
          if (execv (args[0], args))
            {
              perror ("execv");
              _exit (1);
            }

          // not reached
        }

      // parent

      waitpid (pid, NULL, 0); // child with pid has forked another child

      close (asock);
    } // unconditional loop

  // This code is currently not reached since the server parent
  // process can only be killed by a signal.
  close (sock);
  close (dsock);
  exit (1);
  // Silence compiler warning.
  exit (0);
}
