/* emxload.c (emx+gcc) */

/* This program keeps OS/2 programs in memory.

Copyright (c) 1993 Eberhard Mattes

This file is part of emxload.

emxload 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, or (at your option)
any later version.

emxload 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 emxload; see the file COPYING.  If not, write to
the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.

As a special exception, emxload can be distributed with emx.
Moreover, the emxload.exe binary can be distributed without source in
the emxrt.zip package unless emxload has been changed or recompiled.
If you modify the source code of emxload, the exceptions listed in
this paragraph no longer applies and you must remove this paragraph. */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <time.h>
#include <sys/emxload.h>
#define INCL_DOSPROCESS
#define INCL_DOSNMPIPES
#define INCL_DOSSEMAPHORES
#define INCL_DOSERRORS
#include <os2.h>
#include "emxload2.h"

#define VERSION "0.8h"

/* ------------------------------ SERVER ------------------------------ */

/* This is the maximum number of processes that can be managed by the
   emxload server. */

#define MAX_PROCS 256

/* A process table entry.  PID is the process ID, STOP is the time at
   which the program will be unloaded (0 means never unload), NAME is
   the path name of the executable.  If NAME is NULL, the slot is
   empty. */

typedef struct
{
  int pid;
  time_t stop;
  char *name;
} proc;

/* The name of the pipe. */
static char const pipe_name[] = _EMXLOAD_PIPENAME;

/* The table of the processes. */
static proc table[MAX_PROCS];

/* This many entries of the process table are initialized. */
static int procs = 0;

/* Mutex semaphore for protecting the process table. */
static HMTX hmtxTable;

/* This event semaphore is posted when the process table has been
   changed. */
static HEV hevReady;

/* The server pipe handle. */
static HPIPE hp;

/* This flag is used for testing.  Use -test instead of -server on the
   command line to start the server manually in test mode.  If `test'
   is non-zero, the server will display informational messages and
   error message.  Usually, the server is started detached and doesn't
   display anything. */
static int test = FALSE;

/* Prototypes. */

static void s_unload_all (void);


/* Load the program NAME into memory and create a process table entry.
   The program will be unloaded at the time indicated by STOP.  If
   STOP is 0, the program won't be unloaded automatically.  This
   function is called only while the process table is locked. */

static void s_load (const char *name, time_t stop)
{
  int i;
  RESULTCODES result;
  CHAR objbuf[16];
  PCHAR args;
  size_t len;
  APIRET rc;

  /* Find an unused slot in the process table. */

  for (i = 0; i < procs; ++i)
    if (table[i].name == NULL)
      break;
  if (i >= procs)
    {
      if (procs >= MAX_PROCS)
        {
          if (test)
            fputs ("Too many programs\n", stderr);
          return;
        }
      i = procs++;
      table[i].pid = -1;
      table[i].name = NULL;
      table[i].stop = 0;
    }

  /* Build the command line. */

  len = strlen (name);
  args = alloca (len + 3);
  memcpy (args, name, len);
  args[len+0] = 0;
  args[len+1] = 0;
  args[len+2] = 0;

  /* Load the program. */

  rc = DosExecPgm (objbuf, sizeof (objbuf), EXEC_LOAD, args, NULL,
                   &result, name);
  if (rc == 0)
    {

      /* Fill-in the process table entry. */

      table[i].stop = stop;
      table[i].name = strdup (name);
      if (table[i].name == NULL)
        {
          if (test)
            fputs ("Out of memory\n", stderr);
          s_unload_all ();
          exit (2);
        }
      table[i].pid = result.codeTerminate;

      /* Notify the other thread. */

      DosPostEventSem (hevReady);
    }
  else if (test)
    fprintf (stderr, "Cannot load %s, rc=%lu\n", name, rc);
}


/* Unload the program in process table entry I.  The process table
   entry may be unused, see unload_all().  After calling unload(), the
   process table entry will be marked unused. */

static void s_unload (int i)
{
  if (table[i].name != NULL)
    {
      if (DosKillProcess (DKP_PROCESS, table[i].pid) != 0 && test)
        fprintf (stderr, "Cannot unload %s\n", table[i].name);
      free (table[i].name);
      table[i].name = NULL;
      table[i].pid = -1;
    }
}


/* Unload all programs. */

static void s_unload_all (void)
{
  int i;

  for (i = 0; i < procs; ++i)
    s_unload (i);
}


/* Automatically unload programs.  This function never returns. */

static void auto_unload (void)
{
  int i, next_set, more;
  ULONG timeout, post_count, rc;
  time_t now, next;

  for (;;)
    {

      /* Request exclusive access to the process table. */

      DosRequestMutexSem (hmtxTable, SEM_INDEFINITE_WAIT);
      do
        {

          /* Scan the table, unloading programs which have expired.
             Compute the time to sleep until the next program will
             expire.  To improve accuracy of the timing, we repeat
             this loop until no program has been unloaded. */

          time (&now);
          next = 0; next_set = FALSE; more = FALSE;
          for (i = 0; i < procs; ++i)
            if (table[i].name != NULL && table[i].stop != 0)
              {
                if (now >= table[i].stop)
                  {
                    if (test)
                      printf ("Unloading %s\n", table[i].name);
                    s_unload (i);
                    more = TRUE;
                  }
                else if (!next_set)
                  {
                    next = table[i].stop;
                    next_set = TRUE;
                  }
                else if (table[i].stop < next)
                  next = table[i].stop;
              }
        } while (more);
      DosReleaseMutexSem (hmtxTable);

      if (test)
        {
          if (next_set)
            printf ("Waiting for %u seconds\n", (unsigned)(next - now));
          else
            printf ("Waiting forever\n");
        }

      /* Wait until next program expires or until a thread changes the
         process table. */

      timeout = (next_set ? (next - now) * 1000 : SEM_INDEFINITE_WAIT);
      rc = DosWaitEventSem (hevReady, timeout);
      rc = DosResetEventSem (hevReady, &post_count);
    }
}


/* Send answer to client.  Return 0 on success, -1 on failure. */

static int s_answer (const answer *ans)
{
  ULONG rc, cb;

  rc = DosWrite (hp, ans, sizeof (answer), &cb);
  return ((rc == 0) ? 0 : -1);
}


/* Send acknowledge to client. */

static void s_ack (void)
{
  answer ans;

  ans.ans_code = 0;
  s_answer (&ans);
}


/* Load or unload a program.  This function is called by the thread
   which reads the pipe. */

static void s_load_unload (const request *req)
{
  int i;
  time_t now, stop;
  
  stop = 0;

  /* Compute the time when the program will be unloaded. */

  if (req->req_code == _EMXLOAD_LOAD && req->seconds != _EMXLOAD_INDEFINITE)
    {
      time (&now);
      stop = now + req->seconds;
    }

  /* Request exclusive access to the process table. */

  DosRequestMutexSem (hmtxTable, SEM_INDEFINITE_WAIT);

  /* Scan table for a matching entry.  If the program is already in
     the table, update the timeout or unload the program. */

  for (i = 0; i < procs; ++i)
    if (table[i].name != NULL && _fncmp (table[i].name, req->name) == 0)
      {
        if (req->req_code != _EMXLOAD_LOAD)
          s_unload (i);
        else
          {
            table[i].stop = stop;
            DosPostEventSem (hevReady);
          }
        break;
      }

  /* Load the program if the program is not in the table. */

  if (req->req_code == _EMXLOAD_LOAD && i >= procs)
    s_load (req->name, stop);
  if (req->req_code == _EMXLOAD_UNLOAD_WAIT)
    s_ack ();
  DosReleaseMutexSem (hmtxTable);
}


/* Send a list of preloaded programs to the client. */

static void s_list (void)
{
  int i;
  time_t now;
  answer ans;

  /* Request exclusive access to the process table. */

  DosRequestMutexSem (hmtxTable, SEM_INDEFINITE_WAIT);

  /* Get the current time. */

  time (&now);

  /* List all the process table entries. */

  for (i = 0; i < procs; ++i)
    if (table[i].name != NULL)
      {
        if (table[i].stop == 0)
          ans.seconds = _EMXLOAD_INDEFINITE;
        else
          {
            ans.seconds = table[i].stop - now;
            if (ans.seconds < 0)
              ans.seconds = 0;
          }
        _strncpy (ans.name, table[i].name, sizeof (ans.name));
        ans.ans_code = 0;
        if (s_answer (&ans) != 0)
          break;
      }

  ans.ans_code = 1;
  s_answer (&ans);
  DosReleaseMutexSem (hmtxTable);
}


static void read_pipe (void)
{
  ULONG rc, len;
  request req;

  for (;;)
    {
      rc = DosRead (hp, &req, sizeof (req), &len);
      if (rc == ERROR_BROKEN_PIPE)
        break;
      if (rc != 0)
        {
          if (test)
            fprintf (stderr, "Error code %lu\n", rc);
          s_unload_all ();
          exit (2);
        }
      if (len == 0)
        break;
      if (len != sizeof (request))
        {
          if (test)
            fprintf (stderr, "Invalid record length: %lu\n", len);
        }
      else
        switch (req.req_code)
          {
          case _EMXLOAD_LOAD:
          case _EMXLOAD_UNLOAD:
          case _EMXLOAD_UNLOAD_WAIT:
            s_load_unload (&req);
            break;
          case _EMXLOAD_STOP:
          case _EMXLOAD_STOP_WAIT:
            s_unload_all ();
            exit (0);
          case _EMXLOAD_LIST:
            s_list ();
            break;
          default:
            if (test)
              fprintf (stderr, "Unknown request code: %d\n", req.req_code);
            break;
          }
    }
}


/* Handle connections to the pipe.  This function never returns. */

static void connections (void)
{
  ULONG rc;

  for (;;)
    {
      rc = DosConnectNPipe (hp);
      if (test)
        puts ("Connected");
      read_pipe ();
      rc = DosDisConnectNPipe (hp);
      if (test)
        puts ("Disconnected");
    }
}


/* This is an additional thread.  It handles connections to the named
   pipe. */

static void thread (void *arg)
{
  connections ();
}


/* This is the main thread of the server. */

static void server (void)
{
  ULONG rc;

  /* First, close all the file handles (unless in test mode) as we
     inherit the file handles of the parent process.  If we didn't
     close the file handles, the parent couldn't delete, rename
     etc. the files.  This should be done before creating the pipe to
     avoid a timing window. */

  if (!test)
    {
      LONG req;
      ULONG i, cur;

      req = 0; cur = 0;
      if (DosSetRelMaxFH (&req, &cur) == 0)
        for (i = 0; i < cur; ++i)
          DosClose ((HFILE)i);
    }

  /* Create the named pipe. */

  rc = DosCreateNPipe (pipe_name, &hp, NP_NOINHERIT|NP_ACCESS_DUPLEX,
                       NP_WAIT|NP_TYPE_MESSAGE|NP_READMODE_MESSAGE|1,
                       0x1000, 0x1000, 0);
  if (rc == ERROR_PIPE_BUSY)
    {
      if (test)
        fputs ("An emxload server is already running\n", stderr);
      exit (1);
    }
  if (rc != 0)
    {
      if (test)
        fprintf (stderr, "Error code %lu\n", rc);
      exit (2);
    }

  /* Create the semaphores. */

  rc = DosCreateMutexSem (NULL, &hmtxTable, 0, FALSE);
  rc = DosCreateEventSem (NULL, &hevReady, 0, FALSE);

  /* Start the second thread. */

  if (_beginthread (thread, NULL, 0x8000, NULL) == -1)
    {
      if (test)
        fputs ("Cannot start thread\n", stderr);
      exit (2);
    }

  auto_unload ();
}


/* ------------------------------ CLIENT ------------------------------ */

/* This flag is set by the -u and -uw command line options.  All
   programs given on the command line will be unloaded instead of
   loaded. */
static int unload_flag = FALSE;

/* This flag is set by the -uw command line option.  Wait for
   completion. */
static int unload_wait = FALSE;

/* The number of seconds to keep the programs in memory.  May be
   _EMXLOAD_INDEFINITE. */
static int seconds;

/* Prototypes. */

static void usage (void);


/* Handle one request for loading or unloading (client side). */

static void c_load_unload (const char *name)
{
  if (unload_flag)
    {
      if (_emxload_unload (name, unload_wait) != 0)
        fprintf (stderr, "emxload: cannot unload %s\n", name);
    }
  else
    {
      if (_emxload_prog (name, seconds) != 0)
        fprintf (stderr, "emxload: cannot load %s\n", name);
    }
}


/* Parse the command line for the client. */

static void client (int argc, char **argv)
{
  int i, mul;
  int load_gcc, load_gpp, load_gobjc, load_omf;
  char *p, *q;

  /* Set the default values. */

  seconds = 10 * 60;
  load_gcc = FALSE; load_gpp = FALSE; load_gobjc = FALSE; load_omf = FALSE;
  i = 1;

  /* Parse the options. */

  while (i < argc && argv[i][0] == '-')
    {
      p = argv[i++];
      if (strcmp (p, "-e") == 0)
        seconds = _EMXLOAD_INDEFINITE;
      else if (strcmp (p, "-u") == 0)
        unload_flag = TRUE;
      else if (strcmp (p, "-uw") == 0)
        {
          unload_flag = TRUE;
          unload_wait = TRUE;
        }
      else if (strncmp (p, "-m", 2) == 0 || strncmp (p, "-s", 2) == 0)
        {
          mul = (p[1] == 'm' ? 60 : 1);
          p += 2;
          if (*p == 0)
            {
              if (i >= argc)
                usage ();
              p = argv[i++];
            }
          errno = 0;
          seconds = (int)strtol (p, &q, 10);
          if (seconds < 1 || errno != 0 || q == p || *q != 0)
            usage ();
          seconds *= mul;
        }
      else if (strcmp (p, "-g++") == 0)
        load_gpp = TRUE;
      else if (strcmp (p, "-gcc") == 0)
        load_gcc = TRUE;
      else if (strcmp (p, "-gobjc") == 0)
        load_gobjc = TRUE;
      else if (strcmp (p, "-omf") == 0)
        load_omf = TRUE;
      else
        usage ();
    }

  /* At least program must be given. */

  if (i >= argc && !(load_gcc || load_gpp || load_gobjc || load_omf))
    usage ();

  /* Connect to the server and keep the connection open. */

  if (!unload_flag && _emxload_connect () != 0)
    {
      fputs ("emxload: cannot start emxload server\n", stderr);
      exit (2);
    }

  /* Load or unload the programs. */

  if (load_gcc || load_gpp || load_gobjc)
    {
      c_load_unload ("gcc");
      c_load_unload ("cpp");
      c_load_unload ("ld");
      c_load_unload ("as");
      c_load_unload ("emxbind");
    }
  if (load_omf)
    {
      c_load_unload ("emxomf");
      c_load_unload ("emxomfld");
      c_load_unload ("link386");
    }
  if (load_gcc)
    c_load_unload ("cc1");
  if (load_gpp)
    c_load_unload ("cc1plus");
  if (load_gobjc)
    c_load_unload ("cc1obj");
  while (i < argc)
    c_load_unload (argv[i++]);
}


/* List preloaded programs (client side). */

static void c_list (void)
{
  char name[260], timeout[20], *p;
  int seconds, header;

  if (_emxload_list_start () != 0)
    return;

  header = FALSE;
  while (_emxload_list_get (name, sizeof (name), &seconds) == 0)
    {
      if (!header)
        {
          puts ("Timeout ³ Program");
          puts ("ÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ");
          header = TRUE;
        }
      if (seconds == _EMXLOAD_INDEFINITE)
        strcpy (timeout, "NONE");
      else
        _itoa (seconds, timeout, 10);
      for (p = name; *p != 0; ++p)
        if (*p == '/')
          *p = '\\';
      printf ("%7s ³ %s\n", timeout, name);
    }
}

/* ------------------------------- MAIN ------------------------------- */

/* How to call this program. */

static void usage (void)
{
  fputs ("emxload " VERSION " -- Copyright (c) 1993 by Eberhard Mattes\n\n"
         "Usage: emxload [-m <limit>] [-s <limit>] [-e] [-u[w]]\n"
         "               [-gcc] [-g++] [-gobjc] [-omf] <program>...\n"
         "       emxload -l\n"
         "       emxload -q\n"
         "       emxload -qw\n\n"
         "Options:\n\n"
         "-m <limit>   Unload programs after <limit> minutes (default: 10)\n"
         "-s <limit>   Unload programs after <limit> seconds (default: 600)\n"
         "-e           No limit\n"
         "-u           Unload programs (overrides -m, -s and -e)\n"
         "-uw          Like -u, but wait until completed\n"
         "-gcc         Load gcc, cpp, cc1, ld, as, emxbind\n"
         "-g++         Load gcc, cpp, cc1plus, ld, as, emxbind\n"
         "-gobjc       Load gcc, cpp, cc1obj, ld, as, emxbind\n"
         "-omf         Load emxomf, emxomfld, link386\n"
         "-l           List preloaded programs\n"
         "-q           Stop server\n"
         "-qw          Like -w, but wait until completed\n", stderr);
  exit (1);
}


/* Main line. */

int main (int argc, char **argv)
{
  if (_osmode == DOS_MODE)
    {
      fputs ("There's no point in using this program under DOS\n", stderr);
      return (1);
    }
  if (argc == 2 && strcmp (argv[1], "-server") == 0)
    server ();
  else if (argc == 2 && strcmp (argv[1], "-test") == 0)
    {
      test = TRUE;
      server ();
    }
  else if (argc == 2 && strcmp (argv[1], "-l") == 0)
    c_list ();
  else if (argc == 2 && strcmp (argv[1], "-q") == 0)
    _emxload_stop (FALSE);
  else if (argc == 2 && strcmp (argv[1], "-qw") == 0)
    _emxload_stop (TRUE);
  else
    client (argc, argv);
  return (0);
}
