/* bsd_mutex.cc

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. */
#ifdef __OUTSIDE_CYGWIN__
#include "woutsup.h"
#include <errno.h>
#define _KERNEL 1
#define __BSD_VISIBLE 1
#include <sys/smallprint.h>
#include <limits.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <sys/sem.h>

#include "process.h"
#include "cygserver_ipc.h"

/* A BSD kernel global mutex. */
struct mtx Giant;

void
mtx_init (mtx *m, const char *name, const void *, int)
{
  m->name = name;
  m->owner = 0;
  m->cnt = 0;
  /* Can't use Windows Mutexes here since Windows Mutexes are only
     unlockable by the lock owner. */
  m->h = CreateSemaphore (NULL, 1, 1, NULL);
  if (!m->h)
    panic ("couldn't allocate %s mutex, %u\n", name, GetLastError ());
}

void
_mtx_lock (mtx *m, DWORD winpid, const char *file, int line)
{
  _debug (file, line, "Try locking mutex %s (%u) (hold: %u)",
	m->name, winpid, m->owner);
  if (WaitForSingleObject (m->h, INFINITE) != WAIT_OBJECT_0)
    _panic (file, line, "wait for %s in %d failed, %u", m->name, winpid,
	    GetLastError ());
  m->owner = winpid;
  _debug (file, line, "Locked      mutex %s/%u (owner: %u)",
	m->name, ++m->cnt, winpid);
}

int
mtx_owned (mtx *m, DWORD winpid)
{
  return m->owner == winpid;
}

void
_mtx_assert (mtx *m, int what, DWORD winpid, const char *file, int line)
{
  switch (what)
    {
      case MA_OWNED:
        if (!mtx_owned (m, winpid))
	  _panic (file, line, "Mutex %s not owned", m->name);
	break;
      case MA_NOTOWNED:
        if (mtx_owned (m, winpid))
	  _panic (file, line, "Mutex %s is owned", m->name);
        break;
      default:
        break;
    }
}

void
_mtx_unlock (mtx *m, const char *file, int line)
{
  DWORD owner = m->owner;
  unsigned long cnt = m->cnt;
  m->owner = 0;
  /* Cautiously check if mtx_destroy has been called (shutdown).
     In that case, m->h is NULL. */
  if (m->h && !ReleaseSemaphore (m->h, 1, NULL))
    {
      /* Check if the semaphore was already on it's max value. */
      if (GetLastError () != ERROR_TOO_MANY_POSTS)
	_panic (file, line, "release of mutex %s failed, %u", m->name,
		GetLastError ());
    }
  _debug (file, line, "Unlocked    mutex %s/%u (owner: %u)",
  	m->name, cnt, owner);
}

void
mtx_destroy (mtx *m)
{
  HANDLE tmp = m->h;
  m->h = NULL;
  if (tmp)
    CloseHandle (tmp);
}

/*
 * Helper functions for msleep/wakeup.
 */

static int
win_priority (int priority)
{
  int p = (int)((priority) & PRIO_MASK) - PZERO;
  /* Generating a valid priority value is a bit tricky.  The only valid
     values on NT4 are -15, -2, -1, 0, 1, 2, 15. */
  switch (p)
    {
      case -15: case -14: case -13: case -12: case -11:
        return THREAD_PRIORITY_IDLE;
      case -10: case -9: case -8: case -7: case -6:
        return THREAD_PRIORITY_LOWEST;
      case -5: case -4: case -3: case -2: case -1:
        return THREAD_PRIORITY_BELOW_NORMAL;
      case 0:
        return THREAD_PRIORITY_NORMAL;
      case 1: case 2: case 3: case 4: case 5:
        return THREAD_PRIORITY_ABOVE_NORMAL;
      case 6: case 7: case 8: case 9: case 10:
      	return THREAD_PRIORITY_HIGHEST;
      case 11: case 12: case 13: case 14: case 15:
        return THREAD_PRIORITY_TIME_CRITICAL;
    }
  return THREAD_PRIORITY_NORMAL;
}

/*
 * Sets the thread priority, returns the old priority.
 */
static int
set_priority (int priority)
{
  int old_prio = GetThreadPriority (GetCurrentThread ());
  if (!SetThreadPriority (GetCurrentThread (), win_priority (priority)))
    log (LOG_WARNING,
    	  "Warning: Setting thread priority to %d failed with error %u\n",
	  win_priority (priority), GetLastError ());
  return old_prio;
}

/*
 * Original description from BSD code:
 *
 * General sleep call.  Suspends the current process until a wakeup is
 * performed on the specified identifier.  The process will then be made
 * runnable with the specified priority.  Sleeps at most timo/hz seconds
 * (0 means no timeout).  If pri includes PCATCH flag, signals are checked
 * before and after sleeping, else signals are not checked.  Returns 0 if
 * awakened, EWOULDBLOCK if the timeout expires.  If PCATCH is set and a
 * signal needs to be delivered, ERESTART is returned if the current system
 * call should be restarted if possible, and EINTR is returned if the system
 * call should be interrupted by the signal (return EINTR).
 *
 * The mutex argument is exited before the caller is suspended, and
 * entered before msleep returns.  If priority includes the PDROP
 * flag the mutex is not entered before returning.
 */
static HANDLE msleep_glob_evt;

class msleep_sync_array
{
  enum msleep_action {
    MSLEEP_ENTER = 0,
    MSLEEP_LEAVE,
    MSLEEP_WAKEUP
  };

  CRITICAL_SECTION cs;
  long cnt;
  long max_cnt;
  struct msleep_record {
    void *ident;
    HANDLE wakeup_evt;
    LONG threads;
  } *a;

  int find_ident (void *ident, msleep_action action)
  {
    int i;
    for (i = 0; i < cnt; ++i)
      if (a[i].ident == ident)
	return i;
    if (i >= max_cnt)
      panic ("ident %x not found and run out of slots.", ident);
    if (i >= cnt && action == MSLEEP_LEAVE)
      panic ("ident %x not found (%d).", ident, action);
    return i;
  }

  HANDLE first_entry (int i, void *ident)
  {
    debug ("New ident %x, index %d", ident, i);
    a[i].ident = ident;
    a[i].wakeup_evt = CreateEvent (NULL, TRUE, FALSE, NULL);
    if (!a[i].wakeup_evt)
      panic ("CreateEvent failed: %u", GetLastError ());
    debug ("i = %d, CreateEvent: %x", i, a[i].wakeup_evt);
    a[i].threads = 1;
    ++cnt;
    return a[i].wakeup_evt;
  }

  HANDLE next_entry (int i)
  {
    if (a[i].ident && WaitForSingleObject (a[i].wakeup_evt, 0) != WAIT_OBJECT_0)
      {
        ++a[i].threads;
	return a[i].wakeup_evt;
      }
    return NULL;
  }

public:

  msleep_sync_array (int count) : cnt (0), max_cnt (count)
  {
    InitializeCriticalSection (&cs);
    if (!(a = new msleep_record[count]))
      panic ("Allocating msleep records failed: %d", errno);
  }

  ~msleep_sync_array () { delete a; }

  HANDLE enter (void *ident)
  {
    HANDLE evt = NULL;
    while (!evt)
      {
        EnterCriticalSection (&cs);
	int i = find_ident (ident, MSLEEP_ENTER);
	if (i >= cnt)
	  evt = first_entry (i, ident);
	else if (!(evt = next_entry (i)))
	  {
	    /* wakeup has been called, so sleep to wait until all
	       formerly waiting threads have left and retry. */
	    LeaveCriticalSection (&cs);
	    Sleep (1L);
	  }
      }
    LeaveCriticalSection (&cs);
    return evt;
  }

  void leave (void *ident)
  {
    EnterCriticalSection (&cs);
    int i = find_ident (ident, MSLEEP_LEAVE);
    if (--a[i].threads == 0)
      {
	debug ("i = %d, CloseEvent: %x", i, a[i].wakeup_evt);
	CloseHandle (a[i].wakeup_evt);
	a[i].ident = NULL;
	--cnt;
	if (i < cnt)
	  a[i] = a[cnt];
      }
    LeaveCriticalSection (&cs);
  }

  void wakeup (void *ident)
  {
    EnterCriticalSection (&cs);
    int i = find_ident (ident, MSLEEP_WAKEUP);
    if (i < cnt && a[i].ident)
      SetEvent (a[i].wakeup_evt);
    LeaveCriticalSection (&cs);
  }
};

static msleep_sync_array *msleep_sync;
extern struct msginfo msginfo;
extern struct seminfo seminfo;

void
msleep_init (void)
{
  msleep_glob_evt = CreateEvent (NULL, TRUE, FALSE, NULL);
  if (!msleep_glob_evt)
    panic ("CreateEvent in msleep_init failed: %u", GetLastError ());
  int32_t msgmni = support_msgqueues ? msginfo.msgmni : 0;
  int32_t semmni = support_semaphores ? seminfo.semmni : 0;
  TUNABLE_INT_FETCH ("kern.ipc.msgmni", &msgmni);
  TUNABLE_INT_FETCH ("kern.ipc.semmni", &semmni);
  debug ("Try allocating msgmni (%d) + semmni (%d) msleep records",
  	 msgmni, semmni);
  msleep_sync = new msleep_sync_array (msgmni + semmni);
  if (!msleep_sync)
    panic ("Allocating msleep records in msleep_init failed: %d", errno);
}

int
_msleep (void *ident, struct mtx *mtx, int priority,
	const char *wmesg, int timo, struct thread *td)
{
  int ret = -1;

  HANDLE evt = msleep_sync->enter (ident);

  if (mtx)
    mtx_unlock (mtx);
  int old_priority = set_priority (priority);
  HANDLE obj[4] =
    {
      evt,
      msleep_glob_evt,
      td->client->handle (),
      td->ipcblk->signal_arrived
    };
  /* PCATCH handling.  If PCATCH is given and signal_arrived is a valid
     handle, then it's used in the WaitFor call and EINTR is returned. */
  int obj_cnt = 3;
  if ((priority & PCATCH) && obj[3])
    obj_cnt = 4;
  switch (WaitForMultipleObjects (obj_cnt, obj, FALSE, timo ?: INFINITE))
    {
      case WAIT_OBJECT_0:	/* wakeup() has been called. */
	ret = 0;
	debug ("msleep wakeup called for %d", td->td_proc->winpid);
        break;
      case WAIT_OBJECT_0 + 1:	/* Shutdown event (triggered by wakeup_all). */
        priority |= PDROP;
	fallthrough;
      case WAIT_OBJECT_0 + 2:	/* The dependent process has exited. */
	debug ("msleep process exit or shutdown for %d", td->td_proc->winpid);
	ret = EIDRM;
        break;
      case WAIT_OBJECT_0 + 3:	/* Signal for calling process arrived. */
	debug ("msleep process got signal for %d", td->td_proc->winpid);
        ret = EINTR;
	break;
      case WAIT_TIMEOUT:
        ret = EWOULDBLOCK;
        break;
      default:
	/* There's a chance that a process has been terminated before
	   WaitForMultipleObjects has been called.  In this case the handles
	   might be invalid.  The error code returned is ERROR_INVALID_HANDLE.
	   Since we can trust the values of these handles otherwise, we
	   treat an ERROR_INVALID_HANDLE as a normal process termination and
	   hope for the best. */
	if (GetLastError () != ERROR_INVALID_HANDLE)
	  panic ("wait in msleep (%s) failed, %u", wmesg, GetLastError ());
	debug ("wait in msleep (%s) failed for %d, %u", wmesg,
	       td->td_proc->winpid, GetLastError ());
	ret = EIDRM;
	break;
    }

  msleep_sync->leave (ident);

  set_priority (old_priority);

  if (mtx && !(priority & PDROP))
    mtx_lock (mtx);
  return ret;
}

/*
 * Make all threads sleeping on the specified identifier runnable.
 */
int
wakeup (void *ident)
{
  msleep_sync->wakeup (ident);
  return 0;
}

/*
 * Wakeup all sleeping threads.  Only called in the context of cygserver
 * shutdown.
 */
void
wakeup_all (void)
{
  SetEvent (msleep_glob_evt);
}
#endif /* __OUTSIDE_CYGWIN__ */
