X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?p=xscreensaver;a=blobdiff_plain;f=driver%2Fsubprocs.c;h=8b6187d9a1b6732cfb17ee13c895e3c570df625c;hp=ab969b1326fee782e4741fe02a3cef4ce17863a0;hb=551b3de3f619c04c2dd1971ee9b3f02e270c28c9;hpb=6edc84f12f15860a71430c45e8392a5e4ef8203c diff --git a/driver/subprocs.c b/driver/subprocs.c index ab969b13..8b6187d9 100644 --- a/driver/subprocs.c +++ b/driver/subprocs.c @@ -1,4 +1,6 @@ -/* xscreensaver, Copyright (c) 1991-1993 Jamie Zawinski +/* subprocs.c --- choosing, spawning, and killing screenhacks. + * xscreensaver, Copyright (c) 1991, 1992, 1993, 1995, 1997, 1998 + * Jamie Zawinski * * Permission to use, copy, modify, distribute, and sell this software and its * documentation for any purpose is hereby granted without fee, provided that @@ -9,117 +11,203 @@ * implied warranty. */ -/* I would really like some error messages to show up on the screensaver window - itself when subprocs die, or when we can't launch them. If the process - produces output, but does not actually die, I would like that output to go - to the appropriate stdout/stderr as they do now. X and Unix conspire to - make this incredibly difficult. - - - Not all systems have SIGIO, so we can't necessarily be signalled when a - process dies, so we'd have to poll it with wait() or something awful like - that, which would mean the main thread waking up more often than it does - now. - - - We can't tell the difference between a process dying, and a process not - being launched correctly (for example, not being on $PATH) partly because - of the contortions we need to go through with /bin/sh in order to launch - it. - - - We can't do X stuff from signal handlers, so we'd need to set a flag, - save the error message, and notice that flag in the main thread. The - problem is that the main thread is probably sleeping, waiting for the - next X event, so to do this we'd have to register a pipe FD or something, - and write to it when something loses. - - - We could assume that any output produced by a subproc indicates an error, - and blast that across the screen. This means we'd need to use popen() - instead of forking and execing /bin/sh to run it for us. Possibly this - would work, but see comment in exec_screenhack() about getting pids. - I think we could do the "exec " trick with popen() but would SIGIO get - delivered correctly? Who knows. (We could register the pipe-FD with - Xt, and handle output on it with a callback.) - - - For the simple case of the programs not being on $PATH, we could just - search $PATH before launching the shell, but that seems hardly worth the - effort... And it's broken!! Why should we have to duplicate half the - work of the shell? (Because it's Unix, that's why! Bend over.) - */ - -#if __STDC__ -#include -#include -#include +#ifdef HAVE_CONFIG_H +# include "config.h" #endif +#include #include +#include #include /* not used for much... */ #ifndef ESRCH -#include +# include #endif #include /* sys/resource.h needs this for timeval */ -#include /* for setpriority() and PRIO_PROCESS */ -#include /* for waitpid() and associated macros */ -#include /* for the signal names */ -extern char **environ; /* why isn't this in some header file? */ +#ifdef HAVE_SYS_WAIT_H +# include /* for waitpid() and associated macros */ +#endif -#ifndef NO_SETUID -#include /* for getpwnam() and struct passwd */ -#include /* for getgrgid() and struct group */ -#endif /* NO_SETUID */ +#if defined(HAVE_SETPRIORITY) && defined(PRIO_PROCESS) +# include /* for setpriority() and PRIO_PROCESS */ +#endif + +#ifdef VMS +# include +# include /* for close */ +# include /* for getpid */ +# define pid_t int +# define fork vfork +#endif /* VMS */ + +#include /* for the signal names */ #if !defined(SIGCHLD) && defined(SIGCLD) -#define SIGCHLD SIGCLD +# define SIGCHLD SIGCLD #endif -#if __STDC__ +#if 0 /* putenv() is declared in stdlib.h on modern linux systems. */ +#ifdef HAVE_PUTENV extern int putenv (/* const char * */); /* getenv() is in stdlib.h... */ -extern int kill (pid_t, int); /* signal() is in sys/signal.h... */ #endif +#endif + +extern int kill (pid_t, int); /* signal() is in sys/signal.h... */ -# ifndef random -# if defined(SVR4) || defined(SYSV) -# define random() rand() -# else /* !totally-losing-SYSV */ - extern long random(); /* rand() is in stdlib.h... */ -# endif /* !totally-losing-SYSV */ -# endif /* random defined */ +/* This file doesn't need the Xt headers, so stub these types out... */ +#undef XtPointer +#define XtAppContext void* +#define XrmDatabase void* +#define XtIntervalId void* +#define XtPointer void* +#define Widget void* #include "xscreensaver.h" +#include "yarandom.h" -/* this must be `sh', not whatever $SHELL happens to be. */ -char *shell; -static pid_t pid = 0; -char **screenhacks; -int screenhacks_count; -int current_hack = -1; -char *demo_hack; -int next_mode_p = 0; -Bool locking_disabled_p = False; -char *nolock_reason = 0; -int nice_inferior = 0; -extern Bool demo_mode_p; +extern saver_info *global_si_kludge; /* I hate C so much... */ static void -#if __STDC__ -exec_screenhack (char *command) +nice_subproc (int nice_level) +{ + if (nice_level == 0) + return; + +#if defined(HAVE_NICE) + { + int old_nice = nice (0); + int n = nice_level - old_nice; + errno = 0; + if (nice (n) == -1 && errno != 0) + { + char buf [512]; + sprintf (buf, "%s: nice(%d) failed", blurb(), n); + perror (buf); + } + } +#elif defined(HAVE_SETPRIORITY) && defined(PRIO_PROCESS) + if (setpriority (PRIO_PROCESS, getpid(), nice_level) != 0) + { + char buf [512]; + sprintf (buf, "%s: setpriority(PRIO_PROCESS, %lu, %d) failed", + blurb(), (unsigned long) getpid(), nice_level); + perror (buf); + } #else -exec_screenhack (command) - char *command; + fprintf (stderr, + "%s: don't know how to change process priority on this system.\n", + blurb()); + #endif +} + + +#ifndef VMS + +static void +exec_simple_command (const char *command) { - char *tmp; - char buf [512]; - char *av [5]; + char *av[1024]; int ac = 0; + char *token = strtok (strdup(command), " \t"); + while (token) + { + av[ac++] = token; + token = strtok(0, " \t"); + } + av[ac] = 0; + + execvp (av[0], av); /* shouldn't return. */ + + { + char buf [512]; + sprintf (buf, "%s: could not execute \"%s\"", blurb(), av[0]); + perror (buf); - /* Close this fork's version of the display's fd. It will open its own. */ - close (ConnectionNumber (dpy)); - + if (errno == ENOENT && + (token = getenv("PATH"))) + { +# ifndef PATH_MAX +# ifdef MAXPATHLEN +# define PATH_MAX MAXPATHLEN +# else +# define PATH_MAX 2048 +# endif +# endif + char path[PATH_MAX]; + fprintf (stderr, "\n"); + *path = 0; +# if defined(HAVE_GETCWD) + getcwd (path, sizeof(path)); +# elif defined(HAVE_GETWD) + getwd (path); +# endif + if (*path) + fprintf (stderr, " Current directory is: %s\n", path); + fprintf (stderr, " PATH is:\n"); + token = strtok (strdup(token), ":"); + while (token) + { + fprintf (stderr, " %s\n", token); + token = strtok(0, ":"); + } + fprintf (stderr, "\n"); + } + } + fflush(stderr); + fflush(stdout); + exit (1); /* Note that this only exits a child fork. */ +} + + +static void +exec_complex_command (const char *shell, const char *command) +{ + char *av[5]; + int ac = 0; + char *command2 = (char *) malloc (strlen (command) + 6); + memcpy (command2, "exec ", 5); + memcpy (command2 + 5, command, strlen (command) + 1); + + /* Invoke the shell as "/bin/sh -c 'exec prog -arg -arg ...'" */ + av [ac++] = (char *) shell; + av [ac++] = "-c"; + av [ac++] = command2; + av [ac] = 0; + + execvp (av[0], av); /* shouldn't return. */ + + { + char buf [512]; + sprintf (buf, "%s: execvp(\"%s\") failed", blurb(), av[0]); + perror (buf); + fflush(stderr); + fflush(stdout); + exit (1); /* Note that this only exits a child fork. */ + } +} + +#else /* VMS */ + +static void +exec_vms_command (const char *command) +{ + system (command); + fflush (stderr); + fflush (stdout); + exit (1); /* Note that this only exits a child fork. */ +} + +#endif /* !VMS */ + + +static void +exec_screenhack (saver_info *si, const char *command) +{ /* I don't believe what a sorry excuse for an operating system UNIX is! - I want to spawn a process. @@ -127,20 +215,26 @@ exec_screenhack (command) - I would like to receive a message when it dies of natural causes. - I want the spawned process to have user-specified arguments. - The *only way* to parse arguments the way the shells do is to run a - shell (or duplicate what they do, which would be a *lot* of code.) + If shell metacharacters are present (wildcards, backquotes, etc), the + only way to parse those arguments is to run a shell to do the parsing + for you. - The *only way* to know the pid of the process is to fork() and exec() + And the only way to know the pid of the process is to fork() and exec() it in the spawned side of the fork. But if you're running a shell to parse your arguments, this gives you - the pid of the SHELL, not the pid of the PROCESS that you're actually - interested in, which is an *inferior* of the shell. This also means - that the SIGCHLD you get applies to the shell, not its inferior. - - So, the only solution other than implementing an argument parser here - is to force the shell to exec() its inferior. What a fucking hack! - We prepend "exec " to the command string. + the pid of the *shell*, not the pid of the *process* that you're + actually interested in, which is an *inferior* of the shell. This also + means that the SIGCHLD you get applies to the shell, not its inferior. + (Why isn't that sufficient? I don't remember any more, but it turns + out that it isn't.) + + So, the only solution, when metacharacters are present, is to force the + shell to exec() its inferior. What a fucking hack! We prepend "exec " + to the command string, and hope it doesn't contain unquoted semicolons + or ampersands (we don't search for them, because we don't want to + prohibit their use in quoted strings (messages, for example) and parsing + out the various quote characters is too much of a pain.) (Actually, Clint Wong points out that process groups might be used to take care of this problem; this may be worth considering @@ -148,498 +242,836 @@ exec_screenhack (command) what I've seen in Emacs, dealing with process groups isn't especially portable.) */ - tmp = command; - command = (char *) malloc (strlen (tmp) + 6); - memcpy (command, "exec ", 5); - memcpy (command + 5, tmp, strlen (tmp) + 1); + saver_preferences *p = &si->prefs; - /* Invoke the shell as "/bin/sh -c 'exec prog -arg -arg ...'" */ - av [ac++] = shell; - av [ac++] = "-c"; - av [ac++] = command; - av [ac++] = 0; - - if (verbose_p) - printf ("%s: spawning \"%s\" in pid %d.\n", progname, command, getpid ()); +#ifndef VMS + Bool hairy_p = !!strpbrk (command, "*?$&!<>[];`'\\\"="); + /* note: = is in the above because of the sh syntax "FOO=bar cmd". */ -#if defined(SYSV) || defined(SVR4) || defined(__hpux) - { - int old_nice = nice (0); - int n = nice_inferior - old_nice; - errno = 0; - if (nice (n) == -1 && errno != 0) - { - sprintf (buf, "%s: %snice(%d) failed", progname, - (verbose_p ? "## " : ""), n); - perror (buf); - } - } -#else /* !SYSV */ -#ifdef PRIO_PROCESS - if (setpriority (PRIO_PROCESS, getpid(), nice_inferior) != 0) + if (p->verbose_p) + fprintf (stderr, "%s: spawning \"%s\" in pid %lu%s.\n", + blurb(), command, (unsigned long) getpid (), + (hairy_p ? " (via shell)" : "")); + + if (hairy_p) + /* If it contains any shell metacharacters, do it the hard way, + and fork a shell to parse the arguments for us. */ + exec_complex_command (p->shell, command); + else + /* Otherwise, we can just exec the program directly. */ + exec_simple_command (command); + +#else /* VMS */ + if (p->verbose_p) + fprintf (stderr, "%s: spawning \"%s\" in pid %lu.\n", + blurb(), command, getpid()); + exec_vms_command (command); +#endif /* VMS */ + + abort(); /* that shouldn't have returned. */ +} + + + +/* Management of child processes, and de-zombification. + */ + +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; + enum job_status status; + struct screenhack_job *next; +}; + +static struct screenhack_job *jobs = 0; + +/* for debugging -- nothing calls this, but it's useful to invoke from gdb. */ +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: (%s) %s\n", + (long) job->pid, + (job->status == job_running ? "running" : + job->status == job_stopped ? "stopped" : + job->status == job_killed ? " killed" : + job->status == job_dead ? " dead" : " ???"), + job->name); + fprintf (stderr, "\n"); +} + + +static void clean_job_list (void); + +static struct screenhack_job * +make_job (pid_t pid, const char *cmd) +{ + struct screenhack_job *job = (struct screenhack_job *) malloc (sizeof(*job)); + + static char name [1024]; + const char *in = cmd; + char *out = name; + + clean_job_list(); + + while (isspace(*in)) in++; /* skip whitespace */ + while (!isspace(*in) && *in != ':') + *out++ = *in++; /* snarf first token */ + while (isspace(*in)) in++; /* skip whitespace */ + if (*in == ':') /* token was a visual name; skip it. */ { - sprintf (buf, "%s: %ssetpriority(PRIO_PROCESS, %d, %d) failed", - progname, (verbose_p ? "## " : ""), getpid(), nice_inferior); - perror (buf); + in++; + out = name; + while (isspace(*in)) in++; /* skip whitespace */ + while (!isspace(*in)) *out++ = *in++; /* snarf first token */ } -#else /* !PRIO_PROCESS */ - if (nice_inferior != 0) - fprintf (stderr, - "%s: %sdon't know how to change process priority on this system.\n", - progname, (verbose_p ? "## " : "")); -#endif /* !PRIO_PROCESS */ -#endif /* !SYSV */ - - /* Now overlay the current process with /bin/sh running the command. - If this returns, it's an error. - */ - execve (av [0], av, environ); + *out = 0; - sprintf (buf, "%s: %sexecve() failed", progname, (verbose_p ? "## " : "")); - perror (buf); - exit (1); /* Note that this only exits a child fork. */ -} + job->name = strdup(name); + job->pid = pid; + job->status = job_running; + job->next = jobs; + jobs = job; -/* to avoid a race between the main thread and the SIGCHLD handler */ -static int killing = 0; -static Bool suspending = False; + return jobs; +} -static char *current_hack_name P((void)); static void -#if __STDC__ -await_child_death (Bool killed) -#else -await_child_death (killed) - Bool killed; -#endif +free_job (struct screenhack_job *job) { - Bool suspended_p = False; - int status; - pid_t kid; - killing = 1; - if (! pid) + if (!job) return; - - do + else if (job == jobs) + jobs = jobs->next; + else { - kid = waitpid (pid, &status, WUNTRACED); + struct screenhack_job *job2, *prev; + for (prev = 0, job2 = jobs; + job2; + prev = job2, job2 = job2->next) + if (job2 == job) + { + prev->next = job->next; + break; + } } - while (kid == -1 && errno == EINTR); + free(job->name); + free(job); +} + - if (kid == pid) +/* Cleans out dead jobs from the jobs list -- this must only be called + from the main thread, not from a signal handler. + */ +static void +clean_job_list (void) +{ + struct screenhack_job *job, *prev, *next; + for (prev = 0, job = jobs, next = (job ? job->next : 0); + job; + prev = job, job = next, next = (job ? job->next : 0)) { - if (WIFEXITED (status)) + if (job->status == job_dead) { - int exit_status = WEXITSTATUS (status); - if (exit_status & 0x80) - exit_status |= ~0xFF; - /* One might assume that exiting with non-0 means something went - wrong. But that loser xswarm exits with the code that it was - killed with, so it *always* exits abnormally. Treat abnormal - exits as "normal" (don't mention them) if we've just killed - the subprocess. But mention them if they happen on their own. - */ - if (exit_status != 0 && (verbose_p || (! killed))) - fprintf (stderr, - "%s: %schild pid %d (%s) exited abnormally (code %d).\n", - progname, (verbose_p ? "## " : ""), - pid, current_hack_name (), exit_status); - else if (verbose_p) - printf ("%s: child pid %d (%s) exited normally.\n", - progname, pid, current_hack_name ()); + if (prev) + prev->next = next; + free_job (job); + job = prev; } - else if (WIFSIGNALED (status)) - { - if (!killed || WTERMSIG (status) != SIGTERM) - fprintf (stderr, - "%s: %schild pid %d (%s) terminated with signal %d!\n", - progname, (verbose_p ? "## " : ""), - pid, current_hack_name (), WTERMSIG (status)); - else if (verbose_p) - printf ("%s: child pid %d (%s) terminated with SIGTERM.\n", - progname, pid, current_hack_name ()); - } - else if (suspending) - { - suspended_p = True; - suspending = False; /* complain if it happens twice */ - } - else if (WIFSTOPPED (status)) + } +} + + +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 void await_dying_children (saver_info *si); +#ifndef VMS +static void describe_dead_child (saver_info *, pid_t, int wait_status); +#endif + + +/* Semaphore to temporarily turn the SIGCHLD handler into a no-op. + Don't alter this directly -- use block_sigchld() / unblock_sigchld(). + */ +static int block_sigchld_handler = 0; + + +static void +block_sigchld (void) +{ +#ifdef HAVE_SIGACTION + sigset_t child_set; + sigemptyset (&child_set); + sigaddset (&child_set, SIGCHLD); + sigprocmask (SIG_BLOCK, &child_set, 0); +#endif /* HAVE_SIGACTION */ + + block_sigchld_handler++; +} + +static void +unblock_sigchld (void) +{ +#ifdef HAVE_SIGACTION + sigset_t child_set; + sigemptyset(&child_set); + sigaddset(&child_set, SIGCHLD); + sigprocmask(SIG_UNBLOCK, &child_set, 0); +#endif /* HAVE_SIGACTION */ + + block_sigchld_handler--; +} + +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(); + + if (block_sigchld_handler) + /* This function should not be called from the signal handler. */ + abort(); + + block_sigchld(); /* we control the horizontal... */ + + 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; break; +#ifdef SIGSTOP + /* #### there must be a way to do this on VMS... */ + case SIGSTOP: job->status = job_stopped; break; + case SIGCONT: job->status = job_running; break; +#endif /* SIGSTOP */ + default: abort(); + } + +#ifdef SIGSTOP + if (p->verbose_p) + fprintf (stderr, "%s: %s pid %lu.\n", blurb(), + (signal == SIGTERM ? "killing" : + signal == SIGSTOP ? "suspending" : + signal == SIGCONT ? "resuming" : "signalling"), + (unsigned long) job->pid); +#else /* !SIGSTOP */ + if (p->verbose_p) + fprintf (stderr, "%s: %s pid %lu.\n", blurb(), "killing", + (unsigned long) job->pid); +#endif /* !SIGSTOP */ + + status = kill (job->pid, signal); + + if (p->verbose_p && status < 0) + { + if (errno == ESRCH) + fprintf (stderr, "%s: child process %lu (%s) was already dead.\n", + blurb(), job->pid, job->name); + else { - suspended_p = True; - fprintf (stderr, "%s: %schild pid %d (%s) stopped with signal %d!\n", - progname, (verbose_p ? "## " : ""), pid, - current_hack_name (), WSTOPSIG (status)); + char buf [1024]; + sprintf (buf, "%s: couldn't kill child process %lu (%s)", + blurb(), job->pid, job->name); + perror (buf); } - else - fprintf (stderr, "%s: %schild pid %d (%s) died in a mysterious way!", - progname, (verbose_p ? "## " : ""), pid, current_hack_name()); } - else if (kid <= 0) - fprintf (stderr, "%s: %swaitpid(%d, ...) says there are no kids? (%d)\n", - progname, (verbose_p ? "## " : ""), pid, kid); - else - fprintf (stderr, "%s: %swaitpid(%d, ...) says proc %d died, not %d?\n", - progname, (verbose_p ? "## " : ""), pid, kid, pid); - killing = 0; - if (suspended_p != True) - pid = 0; + + await_dying_children (si); + + DONE: + unblock_sigchld(); + if (block_sigchld_handler < 0) + abort(); + + clean_job_list(); + return status; } -static char * -current_hack_name () + +#ifdef SIGCHLD +static RETSIGTYPE +sigchld_handler (int sig) { - static char chn [1024]; - char *hack = (demo_mode_p ? demo_hack : screenhacks [current_hack]); - int i; - for (i = 0; hack [i] != 0 && hack [i] != ' ' && hack [i] != '\t'; i++) - chn [i] = hack [i]; - chn [i] = 0; - return chn; + saver_info *si = global_si_kludge; /* I hate C so much... */ + + if (si->prefs.debug_p) + fprintf(stderr, "%s: got SIGCHLD%s\n", blurb(), + (block_sigchld_handler ? " (blocked)" : "")); + + if (block_sigchld_handler < 0) + abort(); + else if (block_sigchld_handler == 0) + { + block_sigchld(); + await_dying_children (si); + unblock_sigchld(); + } + + init_sigchld(); } +#endif /* SIGCHLD */ -#ifdef SIGCHLD + +#ifndef VMS static void -sigchld_handler (sig) - int sig; +await_dying_children (saver_info *si) { - if (killing) - return; - if (! pid) - abort (); - await_child_death (False); + while (1) + { + int wait_status = 0; + pid_t kid; + + errno = 0; + kid = waitpid (-1, &wait_status, WNOHANG|WUNTRACED); + + 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); + } } -#endif + + +static void +describe_dead_child (saver_info *si, pid_t kid, int wait_status) +{ + int i; + saver_preferences *p = &si->prefs; + struct screenhack_job *job = find_job (kid); + const char *name = job ? job->name : ""; + + 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; + + /* One might assume that exiting with non-0 means something went wrong. + But that loser xswarm exits with the code that it was killed with, so + it *always* exits abnormally. Treat abnormal exits as "normal" (don't + mention them) if we've just killed the subprocess. But mention them + if they happen on their own. + */ + if (!job || + (exit_status != 0 && + (p->verbose_p || job->status != job_killed))) + fprintf (stderr, + "%s: child pid %lu (%s) exited abnormally (code %d).\n", + blurb(), (unsigned long) kid, name, exit_status); + else if (p->verbose_p) + fprintf (stderr, "%s: child pid %lu (%s) exited normally.\n", + blurb(), (unsigned long) kid, name); + + if (job) + job->status = job_dead; + } + else if (WIFSIGNALED (wait_status)) + { + if (p->verbose_p || + !job || + job->status != job_killed || + WTERMSIG (wait_status) != SIGTERM) + fprintf (stderr, "%s: child pid %lu (%s) terminated with %s.\n", + blurb(), (unsigned long) kid, name, + signal_name (WTERMSIG(wait_status))); + + 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 + { + fprintf (stderr, "%s: child pid %lu (%s) died in a mysterious way!", + blurb(), (unsigned long) kid, name); + if (job) + job->status = job_dead; + } + + /* Clear out the pid so that screenhack_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; + } +} + +#else /* VMS */ +static void await_dying_children (saver_info *si) { return; } +#endif /* VMS */ void -init_sigchld () +init_sigchld (void) { #ifdef SIGCHLD - if (((int) signal (SIGCHLD, sigchld_handler)) == -1) + +# ifdef HAVE_SIGACTION /* Thanks to Tom Kelly */ + + static Bool sigchld_initialized_p = 0; + if (!sigchld_initialized_p) + { + struct sigaction action, old; + + action.sa_handler = sigchld_handler; + sigemptyset(&action.sa_mask); + action.sa_flags = 0; + + if (sigaction(SIGCHLD, &action, &old) < 0) + { + char buf [255]; + sprintf (buf, "%s: couldn't catch SIGCHLD", blurb()); + perror (buf); + } + sigchld_initialized_p = True; + } + +# else /* !HAVE_SIGACTION */ + + if (((long) signal (SIGCHLD, sigchld_handler)) == -1L) { char buf [255]; - sprintf (buf, "%s: %scouldn't catch SIGCHLD", progname, - (verbose_p ? "## " : "")); + sprintf (buf, "%s: couldn't catch SIGCHLD", blurb()); perror (buf); } -#endif +# endif /* !HAVE_SIGACTION */ +#endif /* SIGCHLD */ } -extern void raise_window P((Bool inhibit_fade, Bool between_hacks_p)); -void -spawn_screenhack (first_time_p) - Bool first_time_p; + + +static Bool +hack_enabled_p (const char *hack) +{ + const char *s = hack; + while (isspace(*s)) s++; + return (*s != '-'); +} + +static Bool +select_visual_of_hack (saver_screen_info *ssi, const char *hack) +{ + saver_info *si = ssi->global; + saver_preferences *p = &si->prefs; + Bool selected; + static char vis [1024]; + const char *in = hack; + char *out = vis; + while (isspace(*in)) in++; /* skip whitespace */ + if (*in == '-') in++; /* skip optional "-" */ + while (isspace(*in)) in++; /* skip whitespace */ + + while (!isspace(*in) && *in != ':') + *out++ = *in++; /* snarf first token */ + while (isspace(*in)) in++; /* skip whitespace */ + *out = 0; + + if (*in == ':') + selected = select_visual(ssi, vis); + else + selected = select_visual(ssi, 0); + + if (!selected && (p->verbose_p || si->demoing_p)) + { + if (*in == ':') in++; + while (isspace(*in)) in++; + fprintf (stderr, + (si->demoing_p + ? "%s: warning, no \"%s\" visual for \"%s\".\n" + : "%s: no \"%s\" visual; skipping \"%s\".\n"), + blurb(), (*vis ? vis : "???"), in); + } + + return selected; +} + + +static void +spawn_screenhack_1 (saver_screen_info *ssi, Bool first_time_p) { - raise_window (first_time_p, True); - XFlush (dpy); + saver_info *si = ssi->global; + saver_preferences *p = &si->prefs; + raise_window (si, first_time_p, True, False); + XFlush (si->dpy); - if (screenhacks_count || demo_mode_p) + if (p->screenhacks_count) { char *hack; pid_t forked; char buf [255]; int new_hack; - if (demo_mode_p) + int retry_count = 0; + Bool force = False; + + AGAIN: + + if (p->screenhacks_count == 1) + /* If there is only one hack in the list, there is no choice. */ + 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. */ + new_hack = ((ssi->current_hack + p->screenhacks_count - 1) + % p->screenhacks_count); + + else if (si->selection_mode > 0) + /* Select a specific hack, by number. No negotiation. */ { - hack = demo_hack; + new_hack = ((si->selection_mode - 1) % p->screenhacks_count); + force = True; } else { - if (screenhacks_count == 1) - new_hack = 0; - else if (next_mode_p == 1) - new_hack = (current_hack + 1) % screenhacks_count, - next_mode_p = 0; - else if (next_mode_p == 2) + /* Select a random hack (but not the one we just ran.) */ + while ((new_hack = random () % p->screenhacks_count) + == ssi->current_hack) + ; + } + + 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 (hack) || + !select_visual_of_hack (ssi, hack))) + { + if (++retry_count > (p->screenhacks_count*4)) { - new_hack = ((current_hack + screenhacks_count - 1) - % screenhacks_count); - next_mode_p = 0; + /* 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: no suitable visuals for these programs.\n", + blurb()); + return; } else - while ((new_hack = random () % screenhacks_count) == current_hack) - ; - current_hack = new_hack; - hack = screenhacks[current_hack]; + goto AGAIN; } - switch (forked = fork ()) + /* Turn off "next" and "prev" modes now, but "demo" mode is only + turned off by explicit action. + */ + if (si->selection_mode < 0) + si->selection_mode = 0; + + + /* If there's a visual description on the front of the command, nuke it. + */ + { + char *in = hack; + while (isspace(*in)) in++; /* skip whitespace */ + if (*in == '-') in++; /* skip optional "-" */ + while (isspace(*in)) in++; /* skip whitespace */ + hack = in; + while (!isspace(*in) && *in != ':') in++; /* snarf first token */ + while (isspace(*in)) in++; /* skip whitespace */ + if (*in == ':') + { + in++; + while (isspace(*in)) in++; + hack = in; + } + } + + switch ((int) (forked = fork ())) { case -1: - sprintf (buf, "%s: %scouldn't fork", - progname, (verbose_p ? "## " : "")); + sprintf (buf, "%s: couldn't fork", blurb()); perror (buf); - restore_real_vroot (); - exit (1); + restore_real_vroot (si); + saver_exit (si, 1, 0); + case 0: - exec_screenhack (hack); /* this does not return */ + close (ConnectionNumber (si->dpy)); /* close display fd */ + nice_subproc (p->nice_inferior); /* change process priority */ + hack_subproc_environment (ssi); /* set $DISPLAY */ + exec_screenhack (si, hack); /* this does not return */ + abort(); break; + default: - pid = forked; + ssi->pid = forked; + (void) make_job (forked, hack); break; } } } + void -kill_screenhack () +spawn_screenhack (saver_info *si, Bool first_time_p) { - killing = 1; - if (! pid) - return; - if (kill (pid, SIGTERM) < 0) + int i; + + if (!monitor_powered_on_p (si)) { - if (errno == ESRCH) - { - /* Sometimes we don't get a SIGCHLD at all! WTF? - It's a race condition. It looks to me like what's happening is - something like: a subprocess dies of natural causes. There is a - small window between when the process dies and when the SIGCHLD - is (would have been) delivered. If we happen to try to kill() - the process during that time, the kill() fails, because the - process is already dead. But! no SIGCHLD is delivered (perhaps - because the failed kill() has reset some state in the kernel?) - Anyway, if kill() says "No such process", then we have to wait() - for it anyway, because the process has already become a zombie. - I love Unix. - */ - await_child_death (False); - } - else - { - char buf [255]; - sprintf (buf, "%s: %scouldn't kill child process %d", progname, - (verbose_p ? "## " : ""), pid); - perror (buf); - } + if (si->prefs.verbose_p) + fprintf (stderr, + "%s: server reports that monitor has powered down; " + "not launching a new hack.\n", blurb()); + return; } - else + + for (i = 0; i < si->nscreens; i++) { - if (verbose_p) - printf ("%s: killing pid %d.\n", progname, pid); - await_child_death (True); + saver_screen_info *ssi = &si->screens[i]; + spawn_screenhack_1 (ssi, first_time_p); } } void -suspend_screenhack (suspend_p) - Bool suspend_p; +kill_screenhack (saver_info *si) { - - suspending = suspend_p; - if (! pid) - ; - else if (kill (pid, (suspend_p ? SIGSTOP : SIGCONT)) < 0) + int i; + for (i = 0; i < si->nscreens; i++) { - char buf [255]; - sprintf (buf, "%s: %scouldn't %s child process %d", progname, - (verbose_p ? "## " : ""), - (suspend_p ? "suspend" : "resume"), - pid); - perror (buf); + saver_screen_info *ssi = &si->screens[i]; + if (ssi->pid) + kill_job (si, ssi->pid, SIGTERM); + ssi->pid = 0; } - else if (verbose_p) - printf ("%s: %s pid %d.\n", progname, - (suspend_p ? "suspending" : "resuming"), pid); } - -/* Restarting the xscreensaver process from scratch. */ - -static char **saved_argv; void -save_argv (argc, argv) - int argc; - char **argv; +suspend_screenhack (saver_info *si, Bool suspend_p) { - saved_argv = (char **) malloc ((argc + 2) * sizeof (char *)); - saved_argv [argc] = 0; - while (argc--) +#ifdef SIGSTOP /* older VMS doesn't have it... */ + int i; + for (i = 0; i < si->nscreens; i++) { - int i = strlen (argv [argc]) + 1; - saved_argv [argc] = (char *) malloc (i); - memcpy (saved_argv [argc], argv [argc], i); + saver_screen_info *ssi = &si->screens[i]; + if (ssi->pid) + kill_job (si, ssi->pid, (suspend_p ? SIGSTOP : SIGCONT)); } +#endif /* SIGSTOP */ } + +/* Called when we're exiting abnormally, to kill off the subproc. */ void -restart_process () +emergency_kill_subproc (saver_info *si) { - XCloseDisplay (dpy); - fflush (stdout); - fflush (stderr); - execvp (saved_argv [0], saved_argv); - fprintf (stderr, "%s: %scould not restart process: %s (%d)\n", - progname, (verbose_p ? "## " : ""), - (errno == E2BIG ? "arglist too big" : - errno == EACCES ? "could not execute" : - errno == EFAULT ? "memory fault" : - errno == EIO ? "I/O error" : - errno == ENAMETOOLONG ? "name too long" : - errno == ELOOP ? "too many symbolic links" : - errno == ENOENT ? "file no longer exists" : - errno == ENOTDIR ? "directory no longer exists" : - errno == ENOEXEC ? "bad executable file" : - errno == ENOMEM ? "out of memory" : - "execvp() returned unknown error code"), - errno); - exit (1); + int i; +#ifdef SIGCHLD + signal (SIGCHLD, SIG_IGN); +#endif /* SIGCHLD */ + + for (i = 0; i < si->nscreens; i++) + { + saver_screen_info *ssi = &si->screens[i]; + if (ssi->pid) + { + kill_job (si, ssi->pid, SIGTERM); + ssi->pid = 0; + } + } } -void -demo_mode_restart_process () +Bool +screenhack_running_p (saver_info *si) { + Bool result = True; int i; - for (i = 0; saved_argv [i]; i++); - /* add the -demo switch; save_argv() left room for this. */ - saved_argv [i] = "-demo"; - saved_argv [i+1] = 0; - restart_process (); + for (i = 0; i < si->nscreens; i++) + { + saver_screen_info *ssi = &si->screens[i]; + if (!ssi->pid) + result = False; + } + return result; } + +/* Environment variables. */ + + +/* Modifies $PATH in the current environment, so that if DEFAULT_PATH_PREFIX + is defined, the xscreensaver daemon will search that directory for hacks. + */ void -hack_environment () +hack_environment (saver_info *si) +{ +#if defined(HAVE_PUTENV) && defined(DEFAULT_PATH_PREFIX) + static const char *def_path = DEFAULT_PATH_PREFIX; + if (def_path && *def_path) + { + const char *opath = getenv("PATH"); + char *npath = (char *) malloc(strlen(def_path) + strlen(opath) + 20); + strcpy (npath, "PATH="); + strcat (npath, def_path); + strcat (npath, ":"); + strcat (npath, opath); + + if (putenv (npath)) + abort (); + } +#endif /* HAVE_PUTENV && DEFAULT_PATH_PREFIX */ +} + + +void +hack_subproc_environment (saver_screen_info *ssi) { /* Store $DISPLAY into the environment, so that the $DISPLAY variable that - the spawned processes inherit is the same as the value of -display passed - in on our command line (which is not necessarily the same as what our - $DISPLAY variable is.) + 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. */ - char *s, buf [2048]; - int i; - sprintf (buf, "DISPLAY=%s", DisplayString (dpy)); - i = strlen (buf); - s = (char *) malloc (i+1); - strncpy (s, buf, i+1); - if (putenv (s)) + saver_info *si = ssi->global; + const char *odpy = DisplayString (si->dpy); + char *ndpy = (char *) malloc(strlen(odpy) + 20); + int screen_number; + char *s; + + for (screen_number = 0; screen_number < si->nscreens; screen_number++) + if (ssi == &si->screens[screen_number]) + break; + + strcpy (ndpy, "DISPLAY="); + s = ndpy + strlen(ndpy); + strcpy (s, odpy); + + while (*s && *s != ':') s++; /* skip to colon */ + while (*s == ':') s++; /* skip over colons */ + 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); /* put on screen number */ + + /* Allegedly, BSD 4.3 didn't have putenv(), but nobody runs such systems + any more, right? It's not Posix, but everyone seems to have it. */ +#ifdef HAVE_PUTENV + if (putenv (ndpy)) abort (); +#endif /* HAVE_PUTENV */ } -/* Change the uid/gid of the screensaver process, so that it is safe for it - to run setuid root (which it needs to do on some systems to read the - encrypted passwords from the passwd file.) - - hack_uid() is run before opening the X connection, so that XAuth works. - hack_uid_warn() is called after the connection is opened and the command - line arguments are parsed, so that the messages from hack_uid() get - printed after we know whether we're in `verbose' mode. - */ - -#ifndef NO_SETUID +/* Restarting the xscreensaver process from scratch. */ -static int hack_uid_errno; -static char hack_uid_buf [255], *hack_uid_error; +static char **saved_argv; void -hack_uid () +save_argv (int argc, char **argv) { - /* If we've been run as setuid or setgid to someone else (most likely root) - turn off the extra permissions so that random user-specified programs - don't get special privileges. (On some systems it might be necessary - to install this as setuid root in order to read the passwd file to - implement lock-mode...) - */ - setgid (getgid ()); - setuid (getuid ()); - - hack_uid_errno = 0; - hack_uid_error = 0; - - /* If we're being run as root (as from xdm) then switch the user id - to something safe. */ - if (getuid () == 0) + saved_argv = (char **) calloc (argc+2, sizeof (char *)); + saved_argv [argc] = 0; + while (argc--) { - struct passwd *p; - /* Locking can't work when running as root, because we have no way of - knowing what the user id of the logged in user is (so we don't know - whose password to prompt for.) - */ - locking_disabled_p = True; - nolock_reason = "running as root"; - p = getpwnam ("nobody"); - if (! p) p = getpwnam ("daemon"); - if (! p) p = getpwnam ("bin"); - if (! p) p = getpwnam ("sys"); - if (! p) - { - hack_uid_error = "couldn't find safe uid; running as root."; - hack_uid_errno = -1; - } - else - { - struct group *g = getgrgid (p->pw_gid); - hack_uid_error = hack_uid_buf; - sprintf (hack_uid_error, "changing uid/gid to %s/%s (%d/%d).", - p->pw_name, (g ? g->gr_name : "???"), p->pw_uid, p->pw_gid); - - /* Change the gid to be a safe one. If we can't do that, then - print a warning. We change the gid before the uid so that we - change the gid while still root. */ - if (setgid (p->pw_gid) != 0) - { - hack_uid_errno = errno; - sprintf (hack_uid_error, "couldn't set gid to %s (%d)", - (g ? g->gr_name : "???"), p->pw_gid); - } - - /* Now change the uid to be a safe one. */ - if (setuid (p->pw_uid) != 0) - { - hack_uid_errno = errno; - sprintf (hack_uid_error, "couldn't set uid to %s (%d)", - p->pw_name, p->pw_uid); - } - } + int i = strlen (argv [argc]) + 1; + saved_argv [argc] = (char *) malloc (i); + memcpy (saved_argv [argc], argv [argc], i); } -#ifndef NO_LOCKING - else /* disable locking if already being run as "someone else" */ - { - struct passwd *p = getpwuid (getuid ()); - if (!p || - !strcmp (p->pw_name, "root") || - !strcmp (p->pw_name, "nobody") || - !strcmp (p->pw_name, "daemon") || - !strcmp (p->pw_name, "bin") || - !strcmp (p->pw_name, "sys")) - { - locking_disabled_p = True; - nolock_reason = hack_uid_buf; - sprintf (nolock_reason, "running as %s", p->pw_name); - } - } -#endif /* NO_LOCKING */ } + +/* Re-execs the process with the arguments in saved_argv. + Does not return unless there was an error. + */ void -hack_uid_warn () +restart_process (saver_info *si) { - if (! hack_uid_error) - ; - else if (hack_uid_errno == 0) + if (si->prefs.verbose_p) { - if (verbose_p) - printf ("%s: %s\n", progname, hack_uid_error); + int i; + fprintf (real_stderr, "%s: re-executing", blurb()); + for (i = 0; saved_argv[i]; i++) + fprintf (real_stderr, " %s", saved_argv[i]); + fprintf (real_stderr, "\n"); } - else - { - char buf [255]; - sprintf (buf, "%s: %s%s", progname, (verbose_p ? "## " : ""), - hack_uid_error); - if (hack_uid_errno == -1) - fprintf (stderr, "%s\n", buf); - else - { - errno = hack_uid_errno; - perror (buf); - } - } -} + describe_uids (si, real_stderr); + fprintf (real_stderr, "\n"); -#endif /* !NO_SETUID */ + fflush (real_stdout); + fflush (real_stderr); + execvp (saved_argv [0], saved_argv); /* shouldn't return */ + { + char buf [512]; + sprintf (buf, "%s: could not restart process", blurb()); + perror(buf); + fflush(stderr); + } + XBell(si->dpy, 0); +}