/* npth-sigev.c - signal handling interface
   Copyright (C) 2011 g10 Code GmbH

   This file is part of NPTH.

   NPTH is free software; you can redistribute it and/or modify it
   under the terms of either

   - the GNU Lesser General Public License as published by the Free
   Software Foundation; either version 3 of the License, or (at
   your option) any later version.

   or

   - 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.

   or both in parallel, as here.

   NPTH 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 copies of the GNU General Public License
   and the GNU Lesser General Public License along with this program;
   if not, see <http://www.gnu.org/licenses/>.  */

/* This is a support interface to make it easier to handle signals.

   The interfaces here support one (and only one) thread (here called
   "main thread") in the application to monitor several signals while
   selecting on filedescriptors.

   First, the main thread should call npth_sigev_init.  This
   initializes some global data structures used to record interesting
   and pending signals.

   Then, the main thread should call npth_sigev_add for every signal
   it is interested in observing, and finally npth_sigev_fini.  This
   will block the signal in the main threads sigmask.  Note that these
   signals should also be blocked in all other threads.  Since they
   are blocked in the main thread after calling npth_sigev_add, it is
   recommended to call npth_sigev_add in the main thread before
   creating any threads.

   The function npth_sigev_sigmask is a convenient function that
   returns the sigmask of the thread at time of npth_sigev_init, but
   with all registered signals unblocked.  It is recommended to do all
   other changes to the main thread's sigmask before calling
   npth_sigev_init, so that the return value of npth_sigev_sigmask can
   be used in the npth_pselect invocation.

   In any case, the main thread should invoke npth_pselect with a
   sigmask that has all signals that should be monitored unblocked.

   After npth_pselect returns, npth_sigev_get_pending can be called in
   a loop until it returns 0 to iterate over the list of pending
   signals.  Each time a signal is returned by that function, its
   status is reset to non-pending.  */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <signal.h>
#include <assert.h>

#include "npth.h"

/* Record events that have been noticed.  */
static sigset_t sigev_pending;

/* The signal mask during normal operation.  */
static sigset_t sigev_block;

/* The signal mask during pselect.  */
static sigset_t sigev_unblock;

/* Registered signal numbers.  Needed to iterate over sigset_t.
   Bah.  */
#define SIGEV_MAX 32
static int sigev_signum[SIGEV_MAX];
static int sigev_signum_cnt;

/* The internal handler which just sets a global flag.  */
static void
_sigev_handler (int signum)
{
  sigaddset (&sigev_pending, signum);
}


/* Start setting up signal event handling.  */
void
npth_sigev_init (void)
{
  sigemptyset (&sigev_pending);
  pthread_sigmask (SIG_SETMASK, NULL, &sigev_block);
  pthread_sigmask (SIG_SETMASK, NULL, &sigev_unblock);
}


/* Add signal SIGNUM to the list of watched signals.  */
void
npth_sigev_add (int signum)
{
  struct sigaction sa;
  sigset_t ss;

  sigemptyset(&ss);

  assert (sigev_signum_cnt < SIGEV_MAX);
  sigev_signum[sigev_signum_cnt++] = signum;

  /* Make sure we can receive it.  */
  sigdelset (&sigev_unblock, signum);
  sigaddset (&sigev_block, signum);

  sa.sa_handler = _sigev_handler;
  sa.sa_mask = ss;
  sa.sa_flags = 0; /* NOT setting SA_RESTART! */

  sigaction (signum, &sa, NULL);
}


#ifdef HAVE_PTHREAD_ATFORK
/* There is non-POSIX operating system where fork is not available to
   applications.  There, we have no pthread_atfork either.  In such a
   case, we don't call pthread_atfork.  */
static void
restore_sigmask_for_child_process (void)
{
  pthread_sigmask (SIG_SETMASK, &sigev_unblock, NULL);
}
#endif

/* Finish the list of watched signals.  This starts to block them,
   too.  */
void
npth_sigev_fini (void)
{
  /* Block the interesting signals.  */
  pthread_sigmask (SIG_SETMASK, &sigev_block, NULL);
#ifdef HAVE_PTHREAD_ATFORK
  pthread_atfork (NULL, NULL, restore_sigmask_for_child_process);
#endif
}


/* Get the sigmask as needed for pselect.  */
sigset_t *
npth_sigev_sigmask (void)
{
  return &sigev_unblock;
}


/* Return the next signal event that occured.  Returns if none are
   left, 1 on success.  */
int
npth_sigev_get_pending (int *r_signum)
{
  int i;
  for (i = 0; i < sigev_signum_cnt; i++)
    {
      int signum = sigev_signum[i];
      if (sigismember (&sigev_pending, signum))
	{
	  sigdelset (&sigev_pending, signum);
	  *r_signum = signum;
	  return 1;
	}
    }
  return 0;
}

