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