/* cygserver_process.cc

   Copyright 2001, 2002 Red Hat Inc.

   Written by Robert Collins <rbtcollins@hotmail.com>

   This file is part of Cygwin.

   This software is a copyrighted work licensed under the terms of the
   Cygwin license.  Please consult the file "CYGWIN_LICENSE" for
   details. */

#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <windows.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include "wincap.h"
#include <pthread.h>
#include <threaded_queue.h>
#include <cygwin/cygserver_process.h>

#define debug_printf if (DEBUG) printf
#define DEBUG 1

/* the cache structures and classes are designed for one cache per server process.
 * To make multiple process caches, a redesign will be needed
 */

/* process cache */
process_cache::process_cache (unsigned int num_initial_workers):
head (NULL)
{
  /* there can only be one */
  InitializeCriticalSection (&cache_write_access);
  if ((cache_add_trigger = CreateEvent (NULL, FALSE, FALSE, NULL)) == NULL)
    {
      printf ("Failed to create cache add trigger (%lu), terminating\n",
	      GetLastError ());
      exit (1);
    }
  initial_workers = num_initial_workers;
}

process_cache::~process_cache ()
{
}

class process *
process_cache::process (long pid)
{
  class process *entry = head;
  /* TODO: make this more granular, so a search doesn't involve the write lock */
  EnterCriticalSection (&cache_write_access);
  if (!entry)
    {
      entry = new class process (pid);
      entry->next =
	(class process *) InterlockedExchangePointer (&head, entry);
      PulseEvent (cache_add_trigger);
    }
  else
    {
      while (entry->winpid != pid && entry->next)
	entry = entry->next;
      if (entry->winpid != pid)
	{
	  class process *new_entry = new class process (pid);
	  new_entry->next =
	    (class process *) InterlockedExchangePointer (&entry->next,
							  new_entry);
	  entry = new_entry;
	  PulseEvent (cache_add_trigger);
	}
    }
  LeaveCriticalSection (&cache_write_access);
  return entry;
}

static DWORD WINAPI
request_loop (LPVOID LpParam)
{
  class process_process_param *params = (process_process_param *) LpParam;
  return params->request_loop ();
}

void
process_cache::process_requests ()
{
  class process_process_param *params = new process_process_param;
  threaded_queue::process_requests (params, request_loop);
}

void
process_cache::add_task (class process * theprocess)
{
  /* safe to not "Try" because workers don't hog this, they wait on the event
   */
  /* every derived ::add must enter the section! */
  EnterCriticalSection (&queuelock);
  queue_request *listrequest = new process_cleanup (theprocess);
  threaded_queue::add (listrequest);
  LeaveCriticalSection (&queuelock);
}

/* NOT fully MT SAFE: must be called by only one thread in a program */
void
process_cache::remove_process (class process *theprocess)
{
  class process *entry = head;
  /* unlink */
  EnterCriticalSection (&cache_write_access);
  if (entry == theprocess)
    {
      entry = (class process *) InterlockedExchangePointer (&head, theprocess->next);
      if (entry != theprocess)
	{
	  printf ("Bug encountered, process cache corrupted\n");
	  exit (1);
	}
    }
  else
    {
      while (entry->next && entry->next != theprocess)
	entry = entry->next;
      class process *temp = (class process *) InterlockedExchangePointer (&entry->next, theprocess->next);
      if (temp != theprocess)
	{
	  printf ("Bug encountered, process cache corrupted\n");
	  exit (1);
	}
    }
  LeaveCriticalSection (&cache_write_access);
  /* Process any cleanup tasks */
  add_task (theprocess);
}

/* copy <= max_copy HANDLEs to dest[], starting at an offset into _our list_ of
 * begin_at. (Ie begin_at = 5, the first copied handle is still written to dest[0]
 * NOTE: Thread safe, but not thread guaranteed - a newly added process may be missed.
 * Who cares - It'll get caught the next time.
 */
int
process_cache::handle_snapshot (HANDLE * hdest, class process ** edest,
				ssize_t max_copy, int begin_at)
{
  /* TODO:? grab a delete-lock, to prevent deletes during this process ? */
  class process *entry = head;
  int count = begin_at;
  /* skip begin_at entries */
  while (entry && count)
    {
      if (entry->exit_code () == STILL_ACTIVE)
	count--;
      entry = entry->next;
    }
  /* hit the end of the list within begin_at entries */
  if (count)
    return 0;
  HANDLE *hto = hdest;
  class process **eto = edest;
  while (entry && count < max_copy)
    {
      /* hack */
      if (entry->exit_code () == STILL_ACTIVE)
	{
	  *hto = entry->handle ();
	  *eto = entry;
	  count++;
	  hto++;
	  eto++;
	}
      entry = entry->next;
    }
  return count;
}

/* process's */
/* global process crit section */
static CRITICAL_SECTION process_access;
static pthread_once_t process_init;

void
do_process_init (void)
{
  InitializeCriticalSection (&process_access);
  /* we don't have a cache shutdown capability today */
}

process::process (long pid):
winpid (pid), next (NULL), cleaning_up (0), head (NULL), _exit_status (STILL_ACTIVE)
{
  pthread_once (&process_init, do_process_init);
  EnterCriticalSection (&process_access);
  thehandle = OpenProcess (PROCESS_ALL_ACCESS, FALSE, pid);
  if (!thehandle)
    {
      printf ("unable to obtain handle for new cache process %ld\n", pid);
      thehandle = INVALID_HANDLE_VALUE;
    }
  debug_printf ("Got handle %p for new cache process %ld\n", thehandle, pid);
  InitializeCriticalSection (&access);
  LeaveCriticalSection (&process_access);
}

process::~process ()
{
  DeleteCriticalSection (&access);
}

HANDLE
process::handle ()
{
//  DWORD exitstate = exit_code ();
//  if (exitstate == STILL_ACTIVE)
  return thehandle;

  /* FIXME: call the cleanup list ? */

//  CloseHandle (thehandle);
//  debug_printf ("Process id %ld has terminated, attempting to open a new handle\n",
//       winpid);
//  thehandle = OpenProcess (PROCESS_ALL_ACCESS, FALSE, winpid);
//  debug_printf ("Got handle %p when refreshing cache process %ld\n", thehandle, winpid);
//  /* FIXME: what if OpenProcess fails ? */
//  if (thehandle)
//    {
//      _exit_status = STILL_ACTIVE;
//      exit_code ();
//    }
//  else
//    thehandle = INVALID_HANDLE_VALUE;
//  return thehandle;
}

DWORD process::exit_code ()
{
  if (_exit_status != STILL_ACTIVE)
    return _exit_status;
  bool
    err = GetExitCodeProcess (thehandle, &_exit_status);
  if (!err)
    {
      debug_printf ("Failed to retrieve exit code (%ld)\n", GetLastError ());
      thehandle = INVALID_HANDLE_VALUE;
      return _exit_status;
    }
  else if (_exit_status == STILL_ACTIVE)
    return _exit_status;
  /* add new cleanup task etc etc ? */
  return _exit_status;
}

/* this is single threaded. It's called after the process is removed from the cache,
 * but inserts may be attemped by worker threads that have a pointer to it */
void
process::cleanup ()
{
  /* Serialize this */
  EnterCriticalSection (&access);
  InterlockedIncrement (&(long)cleaning_up);
  class cleanup_routine *entry = head;
  while (entry)
    {
      class cleanup_routine *temp;
      entry->cleanup (winpid);
      temp = entry->next;
      delete entry;
      entry = temp;
    }
  LeaveCriticalSection (&access);
}

bool
process::add_cleanup_routine (class cleanup_routine *new_cleanup)
{
  if (cleaning_up)
    return false;
  EnterCriticalSection (&access);
  /* check that we didn't block with ::cleanup ()
   * This rigmarole is to get around win9x's glaring missing TryEnterCriticalSection call
   * which would be a whole lot easier
   */
  if (cleaning_up)
    {
      LeaveCriticalSection (&access);
      return false;
    }
  new_cleanup->next = head;
  head = new_cleanup;
  LeaveCriticalSection (&access);
  return true;
}

/* process_cleanup */
void
process_cleanup::process ()
{
  theprocess->cleanup ();
  delete theprocess;
}

/* process_process_param */
DWORD
process_process_param::request_loop ()
{
  process_cache *cache = (process_cache *) queue;
  /* always malloc one, so there is no special case in the loop */
  ssize_t HandlesSize = 2;
  HANDLE *Handles = (HANDLE *) malloc (sizeof (HANDLE) * HandlesSize);
  process **Entries = (process **) malloc (sizeof (LPVOID) * HandlesSize);
  /* TODO: put [1] at the end as it will also get done if a process dies? */
  Handles[0] = interrupt;
  Handles[1] = cache->cache_add_trigger;
  while (cache->active && !shutdown)
    {
      int copied;
      copied = -1;
      int offset;
      offset = 1;
      int count;
      count = 2;
      while ((copied == HandlesSize - 2 - offset) || copied < 0)
	{
	  /* we need more storage to cope with all the HANDLES */
	  if (copied == HandlesSize - 2 - offset)
	    {
	      HANDLE *temp = (HANDLE *) realloc (Handles,
						 sizeof (HANDLE) *
						 HandlesSize + 10);
	      if (!temp)
		{
		  printf
		    ("cannot allocate more storage for the handle array!\n");
		  exit (1);
		}
	      Handles = temp;
	      process **ptemp = (process **) realloc (Entries,
						      sizeof (LPVOID) *
						      HandlesSize + 10);
	      if (!ptemp)
		{
		  printf
		    ("cannot allocate more storage for the handle array!\n");
		  exit (1);
		}
	      Entries = ptemp;
	      HandlesSize += 10;
	    }
	  offset += copied;
	  copied =
	    cache->handle_snapshot (&Handles[2], &Entries[2],
				    HandlesSize - 2 - offset, offset);
	  count += copied;
	}
      debug_printf ("waiting on %u objects\n", count);
      DWORD rc = WaitForMultipleObjects (count, Handles, FALSE, INFINITE);
      if (rc == WAIT_FAILED)
	{
	  printf ("Could not wait on the process handles (%ld)!\n",
		  GetLastError ());
	  exit (1);
	}
      int objindex = rc - WAIT_OBJECT_0;
      if (objindex > 1 && objindex < count)
	{
	  debug_printf ("Process %ld has left the building\n",
			Entries[objindex]->winpid);
	  /* fire off the termination routines */
	  cache->remove_process (Entries[objindex]);
	}
      else if (objindex >= 0 && objindex < 2)
	{
	  /* 0 is shutdown - do nothing */
	  /* 1 is a cache add event - just rebuild the object list */
	}
      else
	{
	  printf
	    ("unexpected return code from WaitForMultiple objects in process_process_param::request_loop\n");
	}
    }
  running = false;
  return 0;
}