summaryrefslogblamecommitdiffstats
path: root/driver/subprocs.c
blob: 2b3453d069b5c3721fbef45ef13a33f8c83c1d4a (plain) (tree)
1
2
                                                              
                                                                    


















                                                                              

                                                        















                                                                           


                                                          





                        

      


                         

                                                        
 





















                                                                            






















































































                                                                      
   
 











                                                                    






                                              










                                                                    


                         
                  

 
           











                                                                               

                                                                     










                                                                              
                                                                     









                                      


























                                          
                                           















































                                                                           
 
          







                                                
















                                                         



















                                                                      
                                                                     












                                                                              




                   





                                                                              
 








                                            
     




                                                            
     



                      
 














































                                                                          
     





















                                                                        

     




















                                                                    
 

 












                                                                          







                                     
                        

                
                                                              



                               

                                                                       
              

                                                                  








                                                                          
                                                      




           

                                                                





                                                   

                 



                                                  


                                                       





                                                                             




                                     



                                                             
         














                                                                         
         





                                    


                                                                    




                                  

                                                               

                                                                          



                               










                                                                           

                                      
     









                                                     
     

 

    
                             
 


                                        
 



                                                                      
 

                                                                   


 










                                                                              
 










                                                                             
 


                            
 








                                                                             
 
                                                                            
 








                                                                     


 
                                                                          
           
                                                       
 

                                                         
     


                                                                          

     
                         








                                                           
            

                                                           
                               














                                                                      

                                                                        
 
                                                         
                                                                    



                                       






                                                           


            





                                                             



                
























                                                                    






                                         
                                      



                                                            









                                                                            








































































                                                                             
                    


























                                                                      
                                                                              






                                              






                                                      
                   





                            







                                                                       

     







































































                                                                              









                                        
 

                                                                        


 
    
                                          






                                               


                                                                     



                       
 

                                                                    
   


                                                   
                                    









                              
                                      






















                                          





                                                    
                 








































                                                                       
                


                            






                                                                   















                                                                             

                                                                























                                                                        
                                                                   










                                                           
/* subprocs.c --- choosing, spawning, and killing screenhacks.
 * xscreensaver, Copyright © 1991-2021 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"


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. */
};

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));

      /* 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;

      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 (1);  /* 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. */
      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;
	}

      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 */

  /* 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;

  /* Hooray, this doesn't actually clear the window if it was OpenGL. */
  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 (1);                               /* exits fork */
        break;
      }
    default:
      {
        int result = 0;
        int wait_status = 0;
        pid_t pid = -1;
        FILE *f;
        unsigned long v = 0;
        char c;

        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;
        if (! fgets (buf, sizeof(buf)-1, f))
          *buf = 0;
        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);

        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();
}