/* subprocs.c --- choosing, spawning, and killing screenhacks.
 * xscreensaver, Copyright © 1991-2024 Jamie Zawinski <jwz@jwz.org>
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation.  No representations are made about the suitability of this
 * software for any purpose.  It is provided "as is" without express or 
 * implied warranty.
 */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <ctype.h>
#include <stdio.h>
#include <string.h>

#include <X11/Xlib.h>		/* not used for much... */
#include <X11/Xatom.h>
#include <X11/Intrinsic.h>	/* For XtAppAddSignal */

#ifndef ESRCH
# include <errno.h>
#endif

#include <sys/time.h>		/* sys/resource.h needs this for timeval */
#include <sys/param.h>		/* for PATH_MAX */

#ifdef HAVE_SYS_WAIT_H
# include <sys/wait.h>		/* for waitpid() and associated macros */
#endif

#ifdef HAVE_SETRLIMIT
# include <sys/resource.h>	/* for setrlimit() and RLIMIT_AS */
#endif

#include <signal.h>		/* for the signal names */
#include <time.h>

#ifdef ENABLE_NLS
# include <locale.h>
# include <libintl.h>
# define _(S) gettext(S)
#else
# define _(S) (S)
#endif

#include "xscreensaver.h"
#include "exec.h"
#include "yarandom.h"
#include "visual.h"		/* for id_to_visual() */
#include "atoms.h"
#include "screenshot.h"


enum job_status {
  job_running,	/* the process is still alive */
  job_stopped,	/* we have sent it a STOP signal */
  job_killed,	/* we have sent it a TERM signal */
  job_dead	/* we have wait()ed for it, and it's dead -- this state only
		   occurs so that we can avoid calling free() from a signal
		   handler.  Shortly after going into this state, the list
		   element will be removed. */
};

#define EXEC_FAILED_EXIT_STATUS -33

struct screenhack_job {
  char *name;
  pid_t pid;
  int screen;
  enum job_status status;
  time_t launched, killed;
  struct screenhack_job *next;
};

static struct screenhack_job *jobs = 0;

static void clean_job_list (void);
static void await_dying_children (saver_info *si);
static void describe_dead_child (saver_info *, pid_t, int wait_status,
                                 struct rusage);

static XtSignalId xt_sigterm_id = 0;
static int sigterm_received = 0;

static XtSignalId xt_sigchld_id = 0;
static int sigchld_received = 0;


const char *
signal_name (int signal)
{
  /* sys_signame[], sys_siglist[], strsignal() and sigabbrev_np()
     are an unportable mess. */
  switch (signal) {
  case SIGHUP:	  return "SIGHUP";
  case SIGINT:	  return "SIGINT";
  case SIGQUIT:	  return "SIGQUIT";
  case SIGILL:	  return "SIGILL";
  case SIGTRAP:	  return "SIGTRAP";
#ifdef SIGABRT
  case SIGABRT:	  return "SIGABRT";
#endif
  case SIGFPE:	  return "SIGFPE";
  case SIGKILL:	  return "SIGKILL";
  case SIGBUS:	  return "SIGBUS";
  case SIGSEGV:	  return "SIGSEGV";
  case SIGPIPE:	  return "SIGPIPE";
  case SIGALRM:	  return "SIGALRM";
  case SIGTERM:	  return "SIGTERM";
#ifdef SIGSTOP
  case SIGSTOP:	  return "SIGSTOP";
#endif
#ifdef SIGCONT
  case SIGCONT:	  return "SIGCONT";
#endif
#ifdef SIGUSR1
  case SIGUSR1:	  return "SIGUSR1";
#endif
#ifdef SIGUSR2
  case SIGUSR2:	  return "SIGUSR2";
#endif
#ifdef SIGEMT
  case SIGEMT:	  return "SIGEMT";
#endif
#ifdef SIGSYS
  case SIGSYS:	  return "SIGSYS";
#endif
  case SIGCHLD:	  return "SIGCHLD";
#ifdef SIGPWR
  case SIGPWR:	  return "SIGPWR";
#endif
#ifdef SIGWINCH
  case SIGWINCH:  return "SIGWINCH";
#endif
#ifdef SIGURG
  case SIGURG:	  return "SIGURG";
#endif
#ifdef SIGIO
  case SIGIO:	  return "SIGIO";
#endif
#ifdef SIGVTALRM
  case SIGVTALRM: return "SIGVTALRM";
#endif
#ifdef SIGXCPU
  case SIGXCPU:	  return "SIGXCPU";
#endif
#ifdef SIGXFSZ
  case SIGXFSZ:	  return "SIGXFSZ";
#endif
#ifdef SIGDANGER
  case SIGDANGER: return "SIGDANGER";
#endif
  default:
    {
      static char buf[50];
      sprintf(buf, "signal %d\n", signal);
      return buf;
    }
  }
}


/* Management of child processes, and de-zombification.
 */

static char *
timestring (time_t when)
{
  static char buf[255] = { 0 };
  struct tm tm;
  localtime_r (&when, &tm);
  sprintf (buf, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec);
  return buf;
}

#ifdef DEBUG
void show_job_list (void);
void
show_job_list (void)
{
  struct screenhack_job *job;
  fprintf(stderr, "%s: job list:\n", blurb());
  for (job = jobs; job; job = job->next)
    {
      fprintf (stderr, "  %5ld: %2d: (%s) %s %s\n",
               (long) job->pid,
               job->screen,
               (job->status == job_running ? "running" :
                job->status == job_stopped ? "stopped" :
                job->status == job_killed  ? " killed" :
                job->status == job_dead    ? "   dead" : "    ???"),
               (job->killed   ? timestring (job->killed) :
                job->launched ? timestring (job->launched) :
                "??:??:??"),
               job->name);
    }
  fprintf (stderr, "\n");
}
#endif /* DEBUG */


static void
make_job (pid_t pid, int screen, const char *cmd)
{
  struct screenhack_job *job = (struct screenhack_job *) malloc (sizeof(*job));

  static char name [1024];
  const char *in = cmd;
  char *out = name;
  int got_eq = 0;

  clean_job_list();

 AGAIN:
  while (*in && isspace(*in)) in++;		/* skip whitespace */
  while (*in && !isspace(*in) && *in != ':') {
    if (*in == '=') got_eq = 1;
    *out++ = *in++;			/* snarf first token */
  }

  if (got_eq)				/* if the first token was FOO=bar */
    {					/* then get the next token instead. */
      got_eq = 0;
      out = name;
      goto AGAIN;
    }

  while (*in && isspace(*in)) in++;		/* skip whitespace */
  *out = 0;

  job->name = strdup(name);
  job->pid = pid;
  job->screen = screen;
  job->status = job_running;
  job->launched = time ((time_t *) 0);
  job->killed = 0;
  job->next = jobs;
  jobs = job;
}


static void
free_job (struct screenhack_job *job)
{
  if (!job)
    return;
  else if (job == jobs)
    jobs = jobs->next;
  else
    {
      struct screenhack_job *job2, *prev;
      for (prev = 0, job2 = jobs;
	   job2;
	   prev = job2, job2 = job2->next)
	if (job2 == job)
	  {
	    prev->next = job->next;
	    break;
	  }
    }
  free(job->name);
  free(job);
}


/* Cleans out dead jobs from the jobs list.
 */
static void
clean_job_list (void)
{
  struct screenhack_job *job, *prev, *next;
  time_t now = time ((time_t *) 0);
  static time_t last_warn = 0;
  Bool warnedp = False;

  for (prev = 0, job = jobs, next = (job ? job->next : 0);
       job;
       prev = job, job = next, next = (job ? job->next : 0))
    {
      if (job->status == job_dead)
	{
	  if (prev)
	    prev->next = next;
	  free_job (job);
	  job = prev;
	}
      else if (job->status == job_killed &&
               now - job->killed > 10 &&
               now - last_warn   > 10)
        {
          fprintf (stderr,
                   "%s: WARNING: pid %ld (%s) sent SIGTERM %ld seconds ago"
                   " and did not die!\n",
                   blurb(),
                   (long) job->pid,
                   job->name,
                   (long) (now - job->killed));
          warnedp = True;
        }
    }
  if (warnedp) last_warn = now;
}


static struct screenhack_job *
find_job (pid_t pid)
{
  struct screenhack_job *job;
  for (job = jobs; job; job = job->next)
    if (job->pid == pid)
      return job;
  return 0;
}


static int
kill_job (saver_info *si, pid_t pid, int signal)
{
  saver_preferences *p = &si->prefs;
  struct screenhack_job *job;
  int status = -1;

  clean_job_list();

  job = find_job (pid);
  if (!job ||
      !job->pid ||
      job->status == job_killed)
    {
      if (p->verbose_p)
	fprintf (stderr, "%s: no child %ld to signal!\n",
		 blurb(), (long) pid);
      goto DONE;
    }

  switch (signal) {
  case SIGTERM:
    job->status = job_killed;
    job->killed = time ((time_t *) 0);
    break;
#ifdef SIGSTOP
  case SIGSTOP: job->status = job_stopped; break;
  case SIGCONT: job->status = job_running; break;
#endif /* SIGSTOP */
  default: abort();
  }

  if (p->verbose_p)
    fprintf (stderr, "%s: %d: %s pid %lu (%s)\n",
             blurb(), job->screen,
             (job->status == job_killed  ? "killing" :
              job->status == job_stopped ? "suspending" : "resuming"),
             (unsigned long) job->pid,
             job->name);

  status = kill (job->pid, signal);

  if (p->verbose_p && status < 0)
    {
      if (errno == ESRCH)
	fprintf (stderr,
                 "%s: %d: child process %lu (%s) was already dead\n",
		 blurb(), job->screen, (unsigned long) job->pid, job->name);
      else
	{
	  char buf [1024];
	  sprintf (buf, "%s: %d: couldn't kill child process %lu (%s)",
		   blurb(), job->screen, (unsigned long) job->pid, job->name);
	  perror (buf);
	}
    }

  await_dying_children (si);

 DONE:
  clean_job_list();
  return status;
}


/* We use Xt-style signal handling.  A Unix signal fires, and we inform Xt of
   that.  Then after we return to the top-level command loop on the main
   stack, Xt runs our callback function for that signal.  Just like Xt timers.
 */
static void
catch_signal (int sig, RETSIGTYPE (*handler) (int))
{
# ifdef HAVE_SIGACTION
  struct sigaction a;
  a.sa_handler = handler;
  sigemptyset (&a.sa_mask);
  a.sa_flags = 0;
  if (sigaction (sig, &a, 0) < 0)
# else  /* !HAVE_SIGACTION */
  if (((long) signal (sig, handler)) == -1L)
# endif /* !HAVE_SIGACTION */
    {
      char buf [255];
      sprintf (buf, "%s: couldn't catch signal %s", blurb(),
               signal_name (sig));
      perror (buf);
      abort();
    }
}


/* Exiting gracefully.

   When xscreensaver sends a SIGTERM signal to xscreensaver-gfx, rather
   than exiting immediately, we want it to do two things:

     - Send a SIGTERM to each running screenhack.  They would *probably*
       die of a BadWindow X error once their window was deleted, but this
       is cleaner and more immediate.

     - Fade the screens in from black.  This might take several seconds.

   Should another signal come in while that is ongoing, we should just
   die immediately.
 */

static RETSIGTYPE
saver_sigterm_handler (int sig)
{
  /* This is the actual signal handler, running on the signal stack.
     After firing once, set this signal back to the default behavior. */
  sigterm_received = sig;
  catch_signal (sig, SIG_DFL);

  /* The first time a signal fires, inform Xt of that so that it will run
     xt_signal_handler().  "XtNoticeSignal is the only Intrinsics function
     that can safely be called from a signal handler". */
  if (xt_sigterm_id)
    XtNoticeSignal (xt_sigterm_id);
}


static void
xt_sigterm_handler (XtPointer data, XtSignalId *id)
{
  /* This runs from the Xt event loop on the main stack, some time after
     the signal fired. */
  saver_info *si = (saver_info *) data;
  saver_preferences *p = &si->prefs;
  static Bool hit_p = False;
  int i;

  if (xt_sigterm_id)
    XtRemoveSignal (xt_sigterm_id);
  xt_sigterm_id = 0;

  if (hit_p)
    fprintf (stderr, "%s: second signal: %s: exiting\n", blurb(), 
             signal_name (sigterm_received));
  else
    {
      hit_p = True;
      if (p->verbose_p)
        fprintf (stderr, "%s: %s: unblanking\n", blurb(), 
                 signal_name (sigterm_received));

       /* We are in the process of shutting down and are about to exit,
          so don't accidentally re-launch hacks. */
      si->terminating_p = True;

      if (si->watchdog_id)
        {
          XtRemoveTimeOut (si->watchdog_id);
          si->watchdog_id = 0;
        }

      /* Kill before unblanking, to stop drawing as soon as possible. */
      for (i = 0; i < si->nscreens; i++)
        {
          saver_screen_info *ssi = &si->screens[i];
          if (ssi->cycle_id)
            {
              XtRemoveTimeOut (ssi->cycle_id);
              ssi->cycle_id = 0;
              ssi->cycle_at = 0;
            }
          kill_screenhack (ssi);
        }
      unblank_screen (si);

      if (p->verbose_p)
        fprintf (stderr, "%s: %s: exiting\n", blurb(), 
                 signal_name (sigterm_received));
    }

  /* Exit with the original signal received. */
  kill (getpid(), sigterm_received);
  abort();
}


/* SIGCHLD handling.  Basically the same deal as SIGTERM.
 */

static RETSIGTYPE
sigchld_handler (int sig)
{
  /* This is the actual signal handler, running on the signal stack.
     After firing once, set this signal to fire again. */
  sigchld_received = sig;
# ifndef HAVE_SIGACTION
  catch_signal (SIGCHLD, sigchld_handler);
# endif

  if (xt_sigchld_id)
    XtNoticeSignal (xt_sigchld_id);
}


static void
xt_sigchld_handler (XtPointer data, XtSignalId *id)
{
  /* This runs from the Xt event loop on the main stack, some time after
     the signal fired. */
  saver_info *si = (saver_info *) data;

  if (si->prefs.debug_p)
    fprintf(stderr, "%s: got SIGCHLD\n", blurb());

  await_dying_children (si);		/* Their first album was better */
}


static void
await_dying_children (saver_info *si)
{
  while (1)
    {
      int wait_status = 0;
      pid_t kid;
      struct rusage rus;

      errno = 0;
      kid = wait4 (-1, &wait_status, WNOHANG|WUNTRACED, &rus);

      if (si->prefs.debug_p)
	{
	  if (kid < 0 && errno)
            fprintf (stderr, "%s: waitpid(-1) ==> %ld (%d)\n", blurb(),
                     (long) kid, errno);
          else
            fprintf (stderr, "%s: waitpid(-1) ==> %ld\n", blurb(),
                     (long) kid);
	}

      /* 0 means no more children to reap.
	 -1 means error -- except "interrupted system call" isn't a "real"
	 error, so if we get that, we should just try again. */
      if (kid == 0 ||
	  (kid < 0 && errno != EINTR))
	break;

      describe_dead_child (si, kid, wait_status, rus);
    }
}


static void
describe_dead_child (saver_info *si, pid_t kid, int wait_status,
                     struct rusage rus)
{
  int i;
  saver_preferences *p = &si->prefs;
  struct screenhack_job *job = find_job (kid);
  const char *name = job ? job->name : "<unknown>";
  int screen_no = job ? job->screen : 0;
  char msg[1024];
  *msg = 0;

  if (WIFEXITED (wait_status))
    {
      int exit_status = WEXITSTATUS (wait_status);
      /* Treat exit code as a signed 8-bit quantity. */
      if (exit_status & 0x80) exit_status |= ~0xFF;

      if (exit_status == EXEC_FAILED_EXIT_STATUS)
        strcpy (msg, _("is not installed"));
      else
        sprintf (msg, _("crashed with status %d"), exit_status);

      if (p->verbose_p)
        fprintf (stderr,
                 "%s: %d: child pid %lu (%s) exited abnormally"
                 " with status %d\n",
                 blurb(), screen_no, (unsigned long) kid, name, exit_status);
      if (job)
	job->status = job_dead;
    }
  else if (WIFSIGNALED (wait_status))
    {
      const char *sig = signal_name (WTERMSIG (wait_status));
      if (job &&
	  job->status == job_killed &&
	  WTERMSIG (wait_status) == SIGTERM)
        {
          /* Expected notification after we killed it. */
          sprintf (msg, _("exited normally with %.100s"), sig);
          if (p->verbose_p)
            fprintf (stderr, "%s: %d: child pid %lu (%s)"
                     " exited normally with %s\n",
                     blurb(), screen_no, (unsigned long) kid, name, sig);
        }
      else
        {
          /* Unexpected signal. */
          sprintf (msg, _("crashed with %.100s"), sig);
          if (p->verbose_p)
            fprintf (stderr, "%s: %d: child pid %lu (%s)"
                     " unexpectedly terminated with %s\n",
                     blurb(), screen_no, (unsigned long) kid, name, sig);
        }
      if (job)
	job->status = job_dead;
    }
  else if (WIFSTOPPED (wait_status))
    {
      if (p->verbose_p)
        fprintf (stderr, "%s: child pid %lu (%s) stopped with %s\n",
                 blurb(), (unsigned long) kid, name,
                 signal_name (WSTOPSIG (wait_status)));
      if (job)
	job->status = job_stopped;
    }
  else
    {
      /* Didn't exit, signal or stop; is this even possible? */
      sprintf (msg, _("crashed mysteriously"));
      fprintf (stderr, "%s: child pid %lu (%s) died in a mysterious way!",
	       blurb(), (unsigned long) kid, name);
      if (job)
	job->status = job_dead;
    }

# ifdef LOG_CPU_TIME
  if (p->verbose_p && job && job->status == job_dead)
    {
      long u = rus.ru_utime.tv_usec / 1000 + rus.ru_utime.tv_sec * 1000;
      long s = rus.ru_stime.tv_usec / 1000 + rus.ru_stime.tv_sec * 1000;
      fprintf (stderr, "%s: %d: CPU used: %.1fu, %.1fs\n",
               blurb(), screen_no, u / 1000.0, s / 1000.0);
    }
# endif /* LOG_CPU_TIME */

  /* Clear out the pid so that any_screenhacks_running_p() knows it's dead.
   */
  if (!job || job->status == job_dead)
    {
      for (i = 0; i < si->nscreens; i++)
        {
          saver_screen_info *ssi = &si->screens[i];
          if (kid == ssi->pid)
            {
              ssi->pid = 0;
              if (*msg)
                screenhack_obituary (ssi, name, msg);
            }
        }
    }
}


void
init_sigchld (saver_info *si)
{
  static Bool signals_initialized_p = 0;
  if (signals_initialized_p) return;
  signals_initialized_p = True;

  catch_signal (SIGTERM, saver_sigterm_handler);	/* kill     */
  catch_signal (SIGINT,  saver_sigterm_handler);	/* shell ^C */
  catch_signal (SIGQUIT, saver_sigterm_handler);	/* shell ^| */
  catch_signal (SIGCHLD, sigchld_handler);

  xt_sigchld_id = XtAppAddSignal (si->app, xt_sigchld_handler, si);
  xt_sigterm_id = XtAppAddSignal (si->app, xt_sigterm_handler, si);
}


static void
hack_subproc_environment (Screen *screen, Window saver_window)
{
  /* Store $DISPLAY into the environment, so that the $DISPLAY variable that
     the spawned processes inherit is correct.  First, it must be on the same
     host and display as the value of -display passed in on our command line
     (which is not necessarily the same as what our $DISPLAY variable is.)
     Second, the screen number in the $DISPLAY passed to the subprocess should
     be the screen on which this particular hack is running -- not the display
     specification which the driver itself is using, since the driver ignores
     its screen number and manages all existing screens.

     Likewise, store a window ID in $XSCREENSAVER_WINDOW -- this is necessary
     in a Xinerama or RANDR world where a single X11 'Screen' spans multiple
     monitors, and we want to run a hack on each piece of glass, not spanning
     them.  In that case, multiple hacks have the same $DISPLAY, screen and
     root window.
   */
  Display *dpy = DisplayOfScreen (screen);
  const char *odpy = DisplayString (dpy);
  char *ndpy = (char *) malloc (strlen(odpy) + 20);
  char *nssw = (char *) malloc (40);
  char *s, *c;

  strcpy (ndpy, "DISPLAY=");
  s = ndpy + strlen(ndpy);
  strcpy (s, odpy);

  /* We have to find the last colon since it is the boundary between
     hostname & screen - IPv6 numeric format addresses may have many
     colons before that point, and DECnet addresses always have two colons */
  c = strrchr(s,':');				/* skip to last colon */
  if (c != NULL) s = c+1;
  while (isdigit(*s)) s++;			/* skip over dpy number */
  while (*s == '.') s++;			/* skip over dot */
  if (s[-1] != '.') *s++ = '.';			/* put on a dot */
  sprintf(s, "%d", screen_number (screen));	/* put on screen number */

  sprintf (nssw, "XSCREENSAVER_WINDOW=0x%lX", (unsigned long) saver_window);

  if (putenv (ndpy))
    abort ();
  if (putenv (nssw))
    abort ();

  /* don't free ndpy/nssw -- some implementations of putenv (BSD 4.4,
     glibc 2.0) copy the argument, but some (libc4,5, glibc 2.1.2)
     do not.  So we must leak it (and/or the previous setting). Yay.
   */
}


#ifdef ABORT_TESTER  /* Shoot down processes after a bit, for debugging */
static void
abort_debug_timer (XtPointer closure, XtIntervalId *id)
{
  saver_screen_info *ssi = (saver_screen_info *) closure;
  if (ssi->pid)
    {
      fprintf (stderr, "%s: %d: %ld: born to ill\n", blurb(), ssi->number,
               (unsigned long) ssi->pid);
      kill (ssi->pid, SIGILL);
    }
}
#endif /* ABORT_TESTER */


/* Executes the command in another process.
   Command may be any single command acceptable to /bin/sh.
   It may include wildcards, but no semicolons.
   If successful, the pid of the other process is returned.
   Otherwise, -1 is returned and an error may have been
   printed to stderr.
 */
static pid_t
fork_and_exec (saver_screen_info *ssi, const char *command)
{
  saver_info *si = ssi->global;
  saver_preferences *p = &si->prefs;
  pid_t forked;

  switch ((int) (forked = fork ()))
    {
    case -1:
      {
        char buf [255];
        sprintf (buf, "%s: couldn't fork", blurb());
        perror (buf);
        break;
      }

    case 0:
      close (ConnectionNumber (si->dpy));	/* close display fd */
      if (ssi)
        hack_subproc_environment (ssi->screen, ssi->screensaver_window);

      exec_command (p->shell, command, p->nice_inferior);
      /* If that returned, we were unable to exec the subprocess. */
      exit (EXEC_FAILED_EXIT_STATUS);  /* exits child fork */
      break;

    default:	/* parent */
      make_job (forked, (ssi ? ssi->number : 0), command);
      if (p->verbose_p)
        fprintf (stderr, "%s: %d: forked \"%s\" in pid %lu"
                 " on window 0x%lx\n",
                 blurb(), (ssi ? ssi->number : 0), command,
                 (unsigned long) forked,
                 (unsigned long) ssi->screensaver_window);
      break;
    }

# ifdef ABORT_TESTER
  if (forked)
    XtAppAddTimeOut (si->app, 1000 * (5 + (5 * ssi->number)),
                     abort_debug_timer, (XtPointer) ssi);
# endif

  return forked;
}


static Bool
select_visual_of_hack (saver_screen_info *ssi, screenhack *hack)
{
  saver_info *si = ssi->global;
  saver_preferences *p = &si->prefs;
  Bool selected;

  if (hack->visual && *hack->visual)
    selected = select_visual(ssi, hack->visual);
  else
    selected = select_visual(ssi, 0);

  if (!selected && (p->verbose_p || si->demoing_p))
    fprintf (stderr,
             (si->demoing_p
              ? "%s: warning, no \"%s\" visual for \"%s\"\n"
              : "%s: no \"%s\" visual; skipping \"%s\"\n"),
             blurb(),
             (hack->visual && *hack->visual ? hack->visual : "???"),
             hack->command);

  return selected;
}


void
spawn_screenhack (saver_screen_info *ssi)
{
  saver_info *si = ssi->global;
  saver_preferences *p = &si->prefs;
  XFlush (si->dpy);

  if (!monitor_powered_on_p (si->dpy))
    {
      if (si->prefs.verbose_p)
        fprintf (stderr,
                 "%s: %d: X says monitor has powered down; "
                 "not launching a hack\n", blurb(), ssi->number);
      ssi->current_hack = -1;

      /* Hooray, this doesn't actually clear the window if it was OpenGL.
         And some X servers apparently ignore XClearWindow if the monitor is
         powered off, meaning when the monitor powers back on, the stale bits
         that we just tried to erase are still in the frame buffer.
       */
      XClearWindow (si->dpy, ssi->screensaver_window);

      /* Even though we aren't launching a hack, do launch the cycle timer,
         in case the monitor powers back up at some point without us having
         un-blanked. */
      goto DONE;
    }

  if (p->screenhacks_count)
    {
      screenhack *hack;
      pid_t forked;
      char buf [255];
      int new_hack = -1;
      int retry_count = 0;
      Bool force = False;

    AGAIN:

      if (p->screenhacks_count < 1)
        {
          /* No hacks at all */
          new_hack = -1;
        }
      else if (p->screenhacks_count == 1)
        {
          /* Exactly one hack in the list */
          new_hack = 0;
        }
      else if (si->selection_mode == -1)
        {
          /* Select the next hack, wrapping. */
          new_hack = (ssi->current_hack + 1) % p->screenhacks_count;
        }
      else if (si->selection_mode == -2)
        {
          /* Select the previous hack, wrapping. */
          if (ssi->current_hack < 0)
            new_hack = p->screenhacks_count - 1;
          else
            new_hack = ((ssi->current_hack + p->screenhacks_count - 1)
                        % p->screenhacks_count);
        }
      else if (si->selection_mode > 0)
	{
          /* Select a specific hack, by number (via the ACTIVATE command.) */
	  new_hack = ((si->selection_mode - 1) % p->screenhacks_count);
	  force = True;
	}
      else if (p->mode == ONE_HACK &&
               p->selected_hack >= 0)
	{
          /* Select a specific hack, by number (via "One Saver" mode.) */
          new_hack = p->selected_hack;
	  force = True;
	}
      else if (p->mode == BLANK_ONLY || p->mode == DONT_BLANK)
        {
          new_hack = -1;
        }
      else if (p->mode == RANDOM_HACKS_SAME &&
               ssi->number != 0)
	{
	  /* Use the same hack that's running on screen 0.
             (Assumes this function was called on screen 0 first.)
           */
          new_hack = si->screens[0].current_hack;
	}
      else  /* (p->mode == RANDOM_HACKS) */
	{
	  /* Select a random hack (but not the one we just ran.) */
	  while ((new_hack = random () % p->screenhacks_count)
		 == ssi->current_hack)
	    ;
	}

      if (new_hack < 0)   /* don't run a hack */
        {
          ssi->current_hack = -1;
          goto DONE;
        }

      ssi->current_hack = new_hack;
      hack = p->screenhacks[ssi->current_hack];

      /* If the hack is disabled, or there is no visual for this hack,
	 then try again (move forward, or backward, or re-randomize.)
	 Unless this hack was specified explicitly, in which case,
	 use it regardless.
       */
      if (force)
        select_visual_of_hack (ssi, hack);
        
      if (!force &&
	  (!hack->enabled_p ||
	   !on_path_p (hack->command) ||
	   !select_visual_of_hack (ssi, hack)))
	{
	  if (++retry_count > (p->screenhacks_count*4))
	    {
	      /* Uh, oops.  Odds are, there are no suitable visuals,
		 and we're looping.  Give up.  (This is totally lame,
		 what we should do is make a list of suitable hacks at
		 the beginning, then only loop over them.)
	      */
	      if (p->verbose_p)
		fprintf(stderr,
		      "%s: %d: no programs enabled, or no suitable visuals\n",
			blurb(), ssi->number);
	      return;
	    }
	  else
	    goto AGAIN;
	}

      /* Install screenshot property on window. Must be after
         select_visual_of_hack() which might replace the window. */
      if (ssi->screenshot)
        screenshot_save (si->dpy, ssi->screensaver_window, ssi->screenshot);

      if (getuid() == (uid_t) 0 || geteuid() == (uid_t) 0)
        /* Prior to XScreenSaver 6, if running as root, we would change the
           effective uid to the user "nobody" or "daemon" or "noaccess",
           but even that was just encouraging bad behavior.  Don't log in
           as root. */
        {
          fprintf (stderr, "%s: %d: running as root: not launching hacks.\n",
                   blurb(), ssi->number);
          screenhack_obituary (ssi, "", "XScreenSaver: Don't log in as root.");
          goto DONE;
        }

      if (! si->best_gl_visuals ||
          ! si->best_gl_visuals[ssi->real_screen_number])
        {
          /* Lots of things malfunction in mysterious ways if only *part* of
             the XScreenSaver application is installed.  That some distros
             still insist on dividing XScreenSaver into multiple
             bafflingly-named sub-packages that can be omitted willy nilly
             causes repeated, predicted, time-wasting and extremely irritating
             problems for everybody, while solving no problems whatsoever.

             Here's your car, but let's make it trivially easy for everyone to
             accidentally omit the seat belts and distributor cap, because
             some weirdo once wanted that in 2002.

             Install all of XScreenSaver or none.
           */
          screenhack_obituary (ssi, "",
            "No GL visuals: the xscreensaver-gl* packages are required.");
          goto DONE;
        }

      forked = fork_and_exec (ssi, hack->command);
      switch ((int) forked)
	{
	case -1: /* fork failed */
	case 0:  /* child fork (can't happen) */
	  sprintf (buf, "%s: couldn't fork", blurb());
	  perror (buf);
          exit (1);
	  break;

	default:
	  ssi->pid = forked;
	  break;
	}

      XChangeProperty (si->dpy, ssi->screensaver_window, XA_WM_COMMAND,
                       XA_STRING, 8, PropModeReplace,
                       (unsigned char *) hack->command,
                       strlen (hack->command));
      XChangeProperty (si->dpy, ssi->screensaver_window, XA_NET_WM_PID,
                       XA_CARDINAL, 32, PropModeReplace,
                       (unsigned char *) &ssi->pid, 1);
    }

 DONE:

  if (ssi->current_hack < 0)
    XDeleteProperty (si->dpy, ssi->screensaver_window, XA_WM_COMMAND);

  store_saver_status (si);  /* store current hack numbers */

  /* If there is no hack running, clear the window, in case there are
     stale bits left over because the server chose to ignore our earlier
     call to XClearWindow while the monitor was powered down.
   */
  if (!ssi->pid && !ssi->error_dialog)
    XClearWindow (si->dpy, ssi->screensaver_window);

  /* Now that the hack has launched, queue a timer to cycle it. */
  if (!si->demoing_p && p->cycle)
    {
      time_t now = time ((time_t *) 0);
      Time how_long = p->cycle;

      /* If we're in "SELECT n" mode, the cycle timer going off will just
         restart this same hack again.  There's not much point in doing this
         every 5 or 10 minutes, but on the other hand, leaving one hack
         running for days is probably not a great idea, since they tend to
         leak and/or crash.  So, restart the thing once an hour.
       */
      if (si->selection_mode > 0 && ssi->pid)
        how_long = 1000 * 60 * 60;

      /* If there are multiple screens, stagger the restart time of subsequent
         screens: they will all change every N minutes, but not at the same
         time.  But don't let that offset be more than about 5 minutes.

         I originally did this by just adding an offset to the very first
         cycle only, but after a few days, the cycles would synchronize again!
         Are Xt timers implemented with Huygens pendulums??  So compare this
         screen's target time against the previous screen's, and offset it as
         needed.
       */
      if (ssi->number > 0 &&
          p->mode != RANDOM_HACKS_SAME)
        {
          saver_screen_info *prev = &si->screens[ssi->number-1];
          time_t cycle_at = now + how_long / 1000;
          time_t prev_at  = prev->cycle_at;

          Time max = 1000 * 60 * 60 * 10;
          Time off = (how_long > max ? max : how_long) / si->nscreens;

          if (cycle_at < prev_at + off / 1000)
            {
              time_t old = cycle_at;
              cycle_at = prev_at + off / 1000;
              how_long = 1000 * (cycle_at - now);

              if (p->verbose_p && cycle_at - old > 2)
                fprintf (stderr, "%s: %d: offsetting cycle time by %ld sec\n",
                         blurb(), ssi->number,
                         cycle_at - old);
            }
        }

      if (p->debug_p)
        fprintf (stderr, "%s: %d: starting cycle_timer (%ld)\n",
                 blurb(), ssi->number, how_long);

      if (ssi->cycle_id)
        XtRemoveTimeOut (ssi->cycle_id);
      ssi->cycle_id =
        XtAppAddTimeOut (si->app, how_long, cycle_timer, (XtPointer) ssi);
      ssi->cycle_at = now + how_long / 1000;

      if (p->verbose_p)
        {
          time_t t = time((time_t *) 0) + how_long/1000;
          fprintf (stderr, "%s: %d: next cycle in %lu sec at %s\n",
                   blurb(), ssi->number, how_long/1000, timestring(t));
        }
    }
}


void
kill_screenhack (saver_screen_info *ssi)
{
  saver_info *si = ssi->global;
  if (ssi->pid)
    kill_job (si, ssi->pid, SIGTERM);
  ssi->pid = 0;

  /* Do not clear ssi->current_hack here, see watchdog_timer(). */

  /* Hooray, this doesn't actually clear the window if it was OpenGL.
     And some X servers apparently ignore XClearWindow if the monitor is
     powered off, meaning when the monitor powers back on, the stale bits
     that we just tried to erase are still in the frame buffer.
   */
  XClearWindow (si->dpy, ssi->screensaver_window);
}


Bool
any_screenhacks_running_p (saver_info *si)
{
  Bool any_running_p = False;
  int i;
  for (i = 0; i < si->nscreens; i++)
    {
      saver_screen_info *ssi = &si->screens[i];
      if (ssi->pid) any_running_p = True;
      /* Consider it running if an error dialog is posted, so that we
         don't prematurely clear the window. */
      if (ssi->error_dialog) any_running_p = True;
    }
  return any_running_p;
}


/* Fork "xscreensaver-gl-visual" and wait for it to print the IDs of
   the GL visual that should be used on this screen.
 */
Visual *
get_best_gl_visual (saver_info *si, Screen *screen)
{
  saver_preferences *p = &si->prefs;
  pid_t forked;
  int fds [2];
  int in, out;
  int errfds[2];
  int errin = -1, errout = -1;
  char buf[1024];

  char *av[10];
  int ac = 0;

  av[ac++] = "xscreensaver-gl-visual";
  av[ac] = 0;

  if (pipe (fds))
    {
      perror ("error creating pipe:");
      return 0;
    }

  in = fds [0];
  out = fds [1];

  if (!si->prefs.verbose_p)
    {
      if (pipe (errfds))
        {
          perror ("error creating pipe:");
          return 0;
        }

      errin = errfds [0];
      errout = errfds [1];
    }

  switch ((int) (forked = fork ()))
    {
    case -1:
      {
        sprintf (buf, "%s: couldn't fork", blurb());
        perror (buf);
        exit (1);
      }
    case 0:
      {
        close (in);  /* don't need this one */
        close (ConnectionNumber (si->dpy));	/* close display fd */

        if (dup2 (out, STDOUT_FILENO) < 0)	/* pipe stdout */
          {
            perror ("could not dup() a new stdout:");
            return 0;
          }

        if (! si->prefs.verbose_p)
          {
            close(errin);
            if (dup2 (errout, STDERR_FILENO) < 0)
              {
                perror ("could not dup() a new stderr:");
                return 0;
              }
          }

        hack_subproc_environment (screen, 0);	/* set $DISPLAY */

        execvp (av[0], av);			/* shouldn't return. */

        if (errno != ENOENT /* || si->prefs.verbose_p */ )
          {
            /* Ignore "no such file or directory" errors.
               Issue all other exec errors, though. */
            sprintf (buf, "%s: running %s", blurb(), av[0]);
            perror (buf);
          }
        exit (EXEC_FAILED_EXIT_STATUS);		/* exits fork */
        break;
      }
    default:
      {
        int result = 0;
        int wait_status = 0;
        int exit_status = EXEC_FAILED_EXIT_STATUS;
        pid_t pid = -1;
        FILE *f;
        unsigned long v = 0;
        char c;
        int i = 0;

        make_job (forked, 0, av[0]);  /* Bookkeeping for SIGCHLD */

        if (p->verbose_p)
          fprintf (stderr, "%s: %d: forked \"%s\" in pid %lu\n",
                   blurb(), 0, av[0], (unsigned long) forked);

        f = fdopen (in, "r");
        close (out);  /* don't need this one */

        *buf = 0;
        do {
          errno = 0;
          if (! fgets (buf, sizeof(buf)-1, f))
            *buf = 0;
        } while (errno == EINTR &&	/* fgets might fail due to SIGCHLD. */
                 i++ < 1000);		/* And just in case. */

        fclose (f);

        if (! si->prefs.verbose_p)
          {
            close (errout);
            close (errin);
          }

        /* Wait for the child to die - wait for this pid only, not others. */
        pid = waitpid (forked, &wait_status, 0);
        if (si->prefs.debug_p)
          fprintf (stderr, "%s: waitpid(%ld) => %ld\n", blurb(),
                   (long) forked, (long) pid);

        exit_status = WEXITSTATUS (wait_status);
        /* Treat exit code as a signed 8-bit quantity. */
        if (exit_status & 0x80) exit_status |= ~0xFF;

        if (exit_status == EXEC_FAILED_EXIT_STATUS)
          {
            fprintf (stderr, "%s: %s is not installed\n", blurb(), av[0]);
            return 0;
          }

        if (1 == sscanf (buf, "0x%lx %c", &v, &c))
          result = (int) v;

        if (result == 0)
          {
            if (si->prefs.verbose_p)
              {
                int L = strlen(buf);
                fprintf (stderr, "%s: %s did not report a GL visual!\n",
                         blurb(), av[0]);

                if (L && buf[L-1] == '\n')
                  buf[--L] = 0;
                if (*buf)
                  fprintf (stderr, "%s: %s said: \"%s\"\n",
                           blurb(), av[0], buf);
              }
            return 0;
          }
        else
          {
            Visual *v = id_to_visual (screen, result);
            if (si->prefs.verbose_p)
              fprintf (stderr, "%s: %d: %s: GL visual is 0x%X%s\n",
                       blurb(), screen_number (screen),
                       av[0], result,
                       (v == DefaultVisualOfScreen (screen)
                        ? " (default)" : ""));
            return v;
          }
      }
    }

  abort();
}
