ftp://ftp.uni-heidelberg.de/pub/X11/contrib/applications/xscreensaver-1.25.tar.Z
[xscreensaver] / driver / subprocs.c
1 /* xscreensaver, Copyright (c) 1991-1993 Jamie Zawinski <jwz@mcom.com>
2  *
3  * Permission to use, copy, modify, distribute, and sell this software and its
4  * documentation for any purpose is hereby granted without fee, provided that
5  * the above copyright notice appear in all copies and that both that
6  * copyright notice and this permission notice appear in supporting
7  * documentation.  No representations are made about the suitability of this
8  * software for any purpose.  It is provided "as is" without express or 
9  * implied warranty.
10  */
11
12 /* I would really like some error messages to show up on the screensaver window
13    itself when subprocs die, or when we can't launch them.  If the process
14    produces output, but does not actually die, I would like that output to go
15    to the appropriate stdout/stderr as they do now.  X and Unix conspire to
16    make this incredibly difficult.
17
18    - Not all systems have SIGIO, so we can't necessarily be signalled when a
19      process dies, so we'd have to poll it with wait() or something awful like
20      that, which would mean the main thread waking up more often than it does
21      now.
22
23    - We can't tell the difference between a process dying, and a process not
24      being launched correctly (for example, not being on $PATH) partly because
25      of the contortions we need to go through with /bin/sh in order to launch
26      it.
27
28    - We can't do X stuff from signal handlers, so we'd need to set a flag, 
29      save the error message, and notice that flag in the main thread.  The
30      problem is that the main thread is probably sleeping, waiting for the 
31      next X event, so to do this we'd have to register a pipe FD or something,
32      and write to it when something loses.
33
34    - We could assume that any output produced by a subproc indicates an error,
35      and blast that across the screen.  This means we'd need to use popen()
36      instead of forking and execing /bin/sh to run it for us.  Possibly this
37      would work, but see comment in exec_screenhack() about getting pids.
38      I think we could do the "exec " trick with popen() but would SIGIO get
39      delivered correctly?  Who knows.  (We could register the pipe-FD with
40      Xt, and handle output on it with a callback.)
41
42    - For the simple case of the programs not being on $PATH, we could just 
43      search $PATH before launching the shell, but that seems hardly worth the
44      effort...  And it's broken!!  Why should we have to duplicate half the
45      work of the shell?  (Because it's Unix, that's why!  Bend over.)
46  */
47
48 #if __STDC__
49 #include <stdlib.h>
50 #include <unistd.h>
51 #include <string.h>
52 #endif
53
54 #include <stdio.h>
55
56 #include <X11/Xlib.h>           /* not used for much... */
57
58 #ifndef ESRCH
59 #include <errno.h>
60 #endif
61
62 #include <sys/time.h>           /* sys/resource.h needs this for timeval */
63 #include <sys/resource.h>       /* for setpriority() and PRIO_PROCESS */
64 #include <sys/wait.h>           /* for waitpid() and associated macros */
65 #include <signal.h>             /* for the signal names */
66
67 extern char **environ;          /* why isn't this in some header file? */
68
69 #ifndef NO_SETUID
70 #include <pwd.h>                /* for getpwnam() and struct passwd */
71 #include <grp.h>                /* for getgrgid() and struct group */
72 #endif /* NO_SETUID */
73
74 #if !defined(SIGCHLD) && defined(SIGCLD)
75 #define SIGCHLD SIGCLD
76 #endif
77
78 #if __STDC__
79 extern int putenv (/* const char * */); /* getenv() is in stdlib.h... */
80 extern int kill (pid_t, int);           /* signal() is in sys/signal.h... */
81 #endif
82
83 # ifndef random
84 #  if defined(SVR4) || defined(SYSV)
85 #   define random() rand()
86 #  else /* !totally-losing-SYSV */
87     extern long random();               /* rand() is in stdlib.h... */
88 #  endif /* !totally-losing-SYSV */
89 # endif /* random defined */
90
91 #include "xscreensaver.h"
92
93 /* this must be `sh', not whatever $SHELL happens to be. */
94 char *shell;
95 static pid_t pid = 0;
96 char **screenhacks;
97 int screenhacks_count;
98 int current_hack = -1;
99 char *demo_hack;
100 int next_mode_p = 0;
101 Bool locking_disabled_p = False;
102 char *nolock_reason = 0;
103 int nice_inferior = 0;
104
105 extern Bool demo_mode_p;
106
107 static void
108 #if __STDC__
109 exec_screenhack (char *command)
110 #else
111 exec_screenhack (command)
112      char *command;
113 #endif
114 {
115   char *tmp;
116   char buf [512];
117   char *av [5];
118   int ac = 0;
119
120   /* Close this fork's version of the display's fd.  It will open its own. */
121   close (ConnectionNumber (dpy));
122   
123   /* I don't believe what a sorry excuse for an operating system UNIX is!
124
125      - I want to spawn a process.
126      - I want to know it's pid so that I can kill it.
127      - I would like to receive a message when it dies of natural causes.
128      - I want the spawned process to have user-specified arguments.
129
130      The *only way* to parse arguments the way the shells do is to run a
131      shell (or duplicate what they do, which would be a *lot* of code.)
132
133      The *only way* to know the pid of the process is to fork() and exec()
134      it in the spawned side of the fork.
135
136      But if you're running a shell to parse your arguments, this gives you
137      the pid of the SHELL, not the pid of the PROCESS that you're actually
138      interested in, which is an *inferior* of the shell.  This also means
139      that the SIGCHLD you get applies to the shell, not its inferior.
140
141      So, the only solution other than implementing an argument parser here
142      is to force the shell to exec() its inferior.  What a fucking hack!
143      We prepend "exec " to the command string.
144
145      (Actually, Clint Wong <clint@jts.com> points out that process groups
146      might be used to take care of this problem; this may be worth considering
147      some day, except that, 1: this code works now, so why fix it, and 2: from
148      what I've seen in Emacs, dealing with process groups isn't especially
149      portable.)
150    */
151   tmp = command;
152   command = (char *) malloc (strlen (tmp) + 6);
153   memcpy (command, "exec ", 5);
154   memcpy (command + 5, tmp, strlen (tmp) + 1);
155
156   /* Invoke the shell as "/bin/sh -c 'exec prog -arg -arg ...'" */
157   av [ac++] = shell;
158   av [ac++] = "-c";
159   av [ac++] = command;
160   av [ac++] = 0;
161   
162   if (verbose_p)
163     printf ("%s: spawning \"%s\" in pid %d.\n", progname, command, getpid ());
164
165 #if defined(SYSV) || defined(SVR4) || defined(__hpux)
166   {
167     int old_nice = nice (0);
168     int n = nice_inferior - old_nice;
169     errno = 0;
170     if (nice (n) == -1 && errno != 0)
171       {
172         sprintf (buf, "%s: %snice(%d) failed", progname,
173                  (verbose_p ? "## " : ""), n);
174         perror (buf);
175       }
176   }
177 #else /* !SYSV */
178 #ifdef PRIO_PROCESS
179   if (setpriority (PRIO_PROCESS, getpid(), nice_inferior) != 0)
180     {
181       sprintf (buf, "%s: %ssetpriority(PRIO_PROCESS, %d, %d) failed",
182                progname, (verbose_p ? "## " : ""), getpid(), nice_inferior);
183       perror (buf);
184     }
185 #else /* !PRIO_PROCESS */
186   if (nice_inferior != 0)
187     fprintf (stderr,
188            "%s: %sdon't know how to change process priority on this system.\n",
189              progname, (verbose_p ? "## " : ""));
190 #endif /* !PRIO_PROCESS */
191 #endif /* !SYSV */
192
193   /* Now overlay the current process with /bin/sh running the command.
194      If this returns, it's an error.
195    */
196   execve (av [0], av, environ);
197
198   sprintf (buf, "%s: %sexecve() failed", progname, (verbose_p ? "## " : ""));
199   perror (buf);
200   exit (1);     /* Note that this only exits a child fork.  */
201 }
202
203 /* to avoid a race between the main thread and the SIGCHLD handler */
204 static int killing = 0;
205 static Bool suspending = False;
206
207 static char *current_hack_name P((void));
208
209 static void
210 #if __STDC__
211 await_child_death (Bool killed)
212 #else
213 await_child_death (killed)
214      Bool killed;
215 #endif
216 {
217   Bool suspended_p = False;
218   int status;
219   pid_t kid;
220   killing = 1;
221   if (! pid)
222     return;
223
224   do
225     {
226       kid = waitpid (pid, &status, WUNTRACED);
227     }
228   while (kid == -1 && errno == EINTR);
229
230   if (kid == pid)
231     {
232       if (WIFEXITED (status))
233         {
234           int exit_status = WEXITSTATUS (status);
235           if (exit_status & 0x80)
236             exit_status |= ~0xFF;
237           /* One might assume that exiting with non-0 means something went
238              wrong.  But that loser xswarm exits with the code that it was
239              killed with, so it *always* exits abnormally.  Treat abnormal
240              exits as "normal" (don't mention them) if we've just killed
241              the subprocess.  But mention them if they happen on their own.
242            */
243           if (exit_status != 0 && (verbose_p || (! killed)))
244             fprintf (stderr,
245                      "%s: %schild pid %d (%s) exited abnormally (code %d).\n",
246                     progname, (verbose_p ? "## " : ""),
247                      pid, current_hack_name (), exit_status);
248           else if (verbose_p)
249             printf ("%s: child pid %d (%s) exited normally.\n",
250                     progname, pid, current_hack_name ());
251         }
252       else if (WIFSIGNALED (status))
253         {
254           if (!killed || WTERMSIG (status) != SIGTERM)
255             fprintf (stderr,
256                      "%s: %schild pid %d (%s) terminated with signal %d!\n",
257                      progname, (verbose_p ? "## " : ""),
258                      pid, current_hack_name (), WTERMSIG (status));
259           else if (verbose_p)
260             printf ("%s: child pid %d (%s) terminated with SIGTERM.\n",
261                     progname, pid, current_hack_name ());
262         }
263       else if (suspending)
264         {
265           suspended_p = True;
266           suspending = False; /* complain if it happens twice */
267         }
268       else if (WIFSTOPPED (status))
269         {
270           suspended_p = True;
271           fprintf (stderr, "%s: %schild pid %d (%s) stopped with signal %d!\n",
272                    progname, (verbose_p ? "## " : ""), pid,
273                    current_hack_name (), WSTOPSIG (status));
274         }
275       else
276         fprintf (stderr, "%s: %schild pid %d (%s) died in a mysterious way!",
277                  progname, (verbose_p ? "## " : ""), pid, current_hack_name());
278     }
279   else if (kid <= 0)
280     fprintf (stderr, "%s: %swaitpid(%d, ...) says there are no kids?  (%d)\n",
281              progname, (verbose_p ? "## " : ""), pid, kid);
282   else
283     fprintf (stderr, "%s: %swaitpid(%d, ...) says proc %d died, not %d?\n",
284              progname, (verbose_p ? "## " : ""), pid, kid, pid);
285   killing = 0;
286   if (suspended_p != True)
287     pid = 0;
288 }
289
290 static char *
291 current_hack_name ()
292 {
293   static char chn [1024];
294   char *hack = (demo_mode_p ? demo_hack : screenhacks [current_hack]);
295   int i;
296   for (i = 0; hack [i] != 0 && hack [i] != ' ' && hack [i] != '\t'; i++)
297     chn [i] = hack [i];
298   chn [i] = 0;
299   return chn;
300 }
301
302 #ifdef SIGCHLD
303 static void
304 sigchld_handler (sig)
305      int sig;
306 {
307   if (killing)
308     return;
309   if (! pid)
310     abort ();
311   await_child_death (False);
312 }
313 #endif
314
315
316 void
317 init_sigchld ()
318 {
319 #ifdef SIGCHLD
320   if (((int) signal (SIGCHLD, sigchld_handler)) == -1)
321     {
322       char buf [255];
323       sprintf (buf, "%s: %scouldn't catch SIGCHLD", progname,
324                (verbose_p ? "## " : ""));
325       perror (buf);
326     }
327 #endif
328 }
329
330
331 extern void raise_window P((Bool inhibit_fade, Bool between_hacks_p));
332
333 void
334 spawn_screenhack (first_time_p)
335      Bool first_time_p;
336 {
337   raise_window (first_time_p, True);
338   XFlush (dpy);
339
340   if (screenhacks_count || demo_mode_p)
341     {
342       char *hack;
343       pid_t forked;
344       char buf [255];
345       int new_hack;
346       if (demo_mode_p)
347         {
348           hack = demo_hack;
349         }
350       else
351         {
352           if (screenhacks_count == 1)
353             new_hack = 0;
354           else if (next_mode_p == 1)
355             new_hack = (current_hack + 1) % screenhacks_count,
356             next_mode_p = 0;
357           else if (next_mode_p == 2)
358             {
359               new_hack = ((current_hack + screenhacks_count - 1)
360                           % screenhacks_count);
361               next_mode_p = 0;
362             }
363           else
364             while ((new_hack = random () % screenhacks_count) == current_hack)
365               ;
366           current_hack = new_hack;
367           hack = screenhacks[current_hack];
368         }
369
370       switch (forked = fork ())
371         {
372         case -1:
373           sprintf (buf, "%s: %scouldn't fork",
374                    progname, (verbose_p ? "## " : ""));
375           perror (buf);
376           restore_real_vroot ();
377           exit (1);
378         case 0:
379           exec_screenhack (hack); /* this does not return */
380           break;
381         default:
382           pid = forked;
383           break;
384         }
385     }
386 }
387
388 void
389 kill_screenhack ()
390 {
391   killing = 1;
392   if (! pid)
393     return;
394   if (kill (pid, SIGTERM) < 0)
395     {
396       if (errno == ESRCH)
397         {
398           /* Sometimes we don't get a SIGCHLD at all!  WTF?
399              It's a race condition.  It looks to me like what's happening is
400              something like: a subprocess dies of natural causes.  There is a
401              small window between when the process dies and when the SIGCHLD
402              is (would have been) delivered.  If we happen to try to kill()
403              the process during that time, the kill() fails, because the
404              process is already dead.  But! no SIGCHLD is delivered (perhaps
405              because the failed kill() has reset some state in the kernel?)
406              Anyway, if kill() says "No such process", then we have to wait()
407              for it anyway, because the process has already become a zombie.
408              I love Unix.
409            */
410           await_child_death (False);
411         }
412       else
413         {
414           char buf [255];
415           sprintf (buf, "%s: %scouldn't kill child process %d", progname,
416                    (verbose_p ? "## " : ""), pid);
417           perror (buf);
418         }
419     }
420   else
421     {
422       if (verbose_p)
423         printf ("%s: killing pid %d.\n", progname, pid);
424       await_child_death (True);
425     }
426 }
427
428
429 void
430 suspend_screenhack (suspend_p)
431      Bool suspend_p;
432 {
433   
434   suspending = suspend_p;
435   if (! pid)
436     ;
437   else if (kill (pid, (suspend_p ? SIGSTOP : SIGCONT)) < 0)
438     {
439       char buf [255];
440       sprintf (buf, "%s: %scouldn't %s child process %d", progname,
441                (verbose_p ? "## " : ""),
442                (suspend_p ? "suspend" : "resume"),
443                pid);
444       perror (buf);
445     }
446   else if (verbose_p)
447     printf ("%s: %s pid %d.\n", progname,
448             (suspend_p ? "suspending" : "resuming"), pid);
449 }
450
451 \f
452 /* Restarting the xscreensaver process from scratch. */
453
454 static char **saved_argv;
455
456 void
457 save_argv (argc, argv)
458      int argc;
459      char **argv;
460 {
461   saved_argv = (char **) malloc ((argc + 2) * sizeof (char *));
462   saved_argv [argc] = 0;
463   while (argc--)
464     {
465       int i = strlen (argv [argc]) + 1;
466       saved_argv [argc] = (char *) malloc (i);
467       memcpy (saved_argv [argc], argv [argc], i);
468     }
469 }
470
471 void
472 restart_process ()
473 {
474   XCloseDisplay (dpy);
475   fflush (stdout);
476   fflush (stderr);
477   execvp (saved_argv [0], saved_argv);
478   fprintf (stderr, "%s: %scould not restart process: %s (%d)\n",
479            progname, (verbose_p ? "## " : ""),
480            (errno == E2BIG ? "arglist too big" :
481             errno == EACCES ? "could not execute" :
482             errno == EFAULT ? "memory fault" :
483             errno == EIO ? "I/O error" :
484             errno == ENAMETOOLONG ? "name too long" :
485             errno == ELOOP ? "too many symbolic links" :
486             errno == ENOENT ? "file no longer exists" :
487             errno == ENOTDIR ? "directory no longer exists" :
488             errno == ENOEXEC ? "bad executable file" :
489             errno == ENOMEM ? "out of memory" :
490             "execvp() returned unknown error code"),
491            errno);
492   exit (1);
493 }
494
495 void
496 demo_mode_restart_process ()
497 {
498   int i;
499   for (i = 0; saved_argv [i]; i++);
500   /* add the -demo switch; save_argv() left room for this. */
501   saved_argv [i] = "-demo";
502   saved_argv [i+1] = 0;
503   restart_process ();
504 }
505
506 void
507 hack_environment ()
508 {
509   /* Store $DISPLAY into the environment, so that the $DISPLAY variable that
510      the spawned processes inherit is the same as the value of -display passed
511      in on our command line (which is not necessarily the same as what our
512      $DISPLAY variable is.)
513    */
514   char *s, buf [2048];
515   int i;
516   sprintf (buf, "DISPLAY=%s", DisplayString (dpy));
517   i = strlen (buf);
518   s = (char *) malloc (i+1);
519   strncpy (s, buf, i+1);
520   if (putenv (s))
521     abort ();
522 }
523
524 \f
525 /* Change the uid/gid of the screensaver process, so that it is safe for it
526    to run setuid root (which it needs to do on some systems to read the 
527    encrypted passwords from the passwd file.)
528
529    hack_uid() is run before opening the X connection, so that XAuth works.
530    hack_uid_warn() is called after the connection is opened and the command
531    line arguments are parsed, so that the messages from hack_uid() get 
532    printed after we know whether we're in `verbose' mode.
533  */
534
535 #ifndef NO_SETUID
536
537 static int hack_uid_errno;
538 static char hack_uid_buf [255], *hack_uid_error;
539
540 void
541 hack_uid ()
542 {
543   /* If we've been run as setuid or setgid to someone else (most likely root)
544      turn off the extra permissions so that random user-specified programs
545      don't get special privileges.  (On some systems it might be necessary
546      to install this as setuid root in order to read the passwd file to
547      implement lock-mode...)
548   */
549   setgid (getgid ());
550   setuid (getuid ());
551
552   hack_uid_errno = 0;
553   hack_uid_error = 0;
554
555   /* If we're being run as root (as from xdm) then switch the user id
556      to something safe. */
557   if (getuid () == 0)
558     {
559       struct passwd *p;
560       /* Locking can't work when running as root, because we have no way of
561          knowing what the user id of the logged in user is (so we don't know
562          whose password to prompt for.)
563        */
564       locking_disabled_p = True;
565       nolock_reason = "running as root";
566       p = getpwnam ("nobody");
567       if (! p) p = getpwnam ("daemon");
568       if (! p) p = getpwnam ("bin");
569       if (! p) p = getpwnam ("sys");
570       if (! p)
571         {
572           hack_uid_error = "couldn't find safe uid; running as root.";
573           hack_uid_errno = -1;
574         }
575       else
576         {
577           struct group *g = getgrgid (p->pw_gid);
578           hack_uid_error = hack_uid_buf;
579           sprintf (hack_uid_error, "changing uid/gid to %s/%s (%d/%d).",
580                    p->pw_name, (g ? g->gr_name : "???"), p->pw_uid, p->pw_gid);
581
582           /* Change the gid to be a safe one.  If we can't do that, then
583              print a warning.  We change the gid before the uid so that we
584              change the gid while still root. */
585           if (setgid (p->pw_gid) != 0)
586             {
587               hack_uid_errno = errno;
588               sprintf (hack_uid_error, "couldn't set gid to %s (%d)",
589                        (g ? g->gr_name : "???"), p->pw_gid);
590             }
591
592           /* Now change the uid to be a safe one. */
593           if (setuid (p->pw_uid) != 0)
594             {
595               hack_uid_errno = errno;
596               sprintf (hack_uid_error, "couldn't set uid to %s (%d)",
597                        p->pw_name, p->pw_uid);
598             }
599         }
600     }
601 #ifndef NO_LOCKING
602  else   /* disable locking if already being run as "someone else" */
603    {
604      struct passwd *p = getpwuid (getuid ());
605      if (!p ||
606          !strcmp (p->pw_name, "root") ||
607          !strcmp (p->pw_name, "nobody") ||
608          !strcmp (p->pw_name, "daemon") ||
609          !strcmp (p->pw_name, "bin") ||
610          !strcmp (p->pw_name, "sys"))
611        {
612          locking_disabled_p = True;
613          nolock_reason = hack_uid_buf;
614          sprintf (nolock_reason, "running as %s", p->pw_name);
615        }
616    }
617 #endif /* NO_LOCKING */
618 }
619
620 void
621 hack_uid_warn ()
622 {
623   if (! hack_uid_error)
624     ;
625   else if (hack_uid_errno == 0)
626     {
627       if (verbose_p)
628         printf ("%s: %s\n", progname, hack_uid_error);
629     }
630   else
631     {
632       char buf [255];
633       sprintf (buf, "%s: %s%s", progname, (verbose_p ? "## " : ""),
634                hack_uid_error);
635       if (hack_uid_errno == -1)
636         fprintf (stderr, "%s\n", buf);
637       else
638         {
639           errno = hack_uid_errno;
640           perror (buf);
641         }
642     }
643 }
644
645 #endif /* !NO_SETUID */