From http://www.jwz.org/xscreensaver/xscreensaver-5.36.tar.gz
[xscreensaver] / utils / textclient.c
1 /* xscreensaver, Copyright (c) 2012-2016 Jamie Zawinski <jwz@jwz.org>
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  * Running programs under a pipe or pty and returning bytes from them.
12  * Uses these X resources:
13  * 
14  * program:             What to run.  Usually "xscreensaver-text".
15  * relaunchDelay: secs  How long after the command dies before restarting.
16  * usePty: bool         Whether to run the command interactively.
17  * metaSendsESC: bool   Whether to send Alt-x as ESC x in pty-mode.
18  * swapBSDEL: bool      Swap Backspace and Delete in pty-mode.
19  *
20  * On iOS and Android, textclient-mobile.c is used instead.
21  */
22
23 #include "utils.h"
24
25 #if !defined(USE_IPHONE) && !defined(HAVE_ANDROID)  /* whole file */
26
27 #include "textclient.h"
28 #include "resources.h"
29
30 #ifndef HAVE_COCOA
31 # define XK_MISCELLANY
32 # include <X11/keysymdef.h>
33 # include <X11/Xatom.h>
34 # include <X11/Intrinsic.h>
35 #endif
36
37 #include <stdio.h>
38
39 #include <signal.h>
40 #include <sys/wait.h>
41
42 #ifdef HAVE_UNISTD_H
43 # include <unistd.h>
44 # include <fcntl.h>  /* for O_RDWR */
45 #endif
46
47 #ifdef HAVE_FORKPTY
48 # include <sys/ioctl.h>
49 # ifdef HAVE_PTY_H
50 #  include <pty.h>
51 # endif
52 # ifdef HAVE_UTIL_H
53 #  include <util.h>
54 # endif
55 # ifdef HAVE_SYS_TERMIOS_H
56 #  include <sys/termios.h>
57 # endif
58 #endif /* HAVE_FORKPTY */
59
60 #undef DEBUG
61
62 extern const char *progname;
63
64 struct text_data {
65   Display *dpy;
66   char *program;
67   int pix_w, pix_h, char_w, char_h;
68   int max_lines;
69
70   Bool pty_p;
71   XtIntervalId pipe_timer;
72   FILE *pipe;
73   pid_t pid;
74   XtInputId pipe_id;
75   Bool input_available_p;
76   Time subproc_relaunch_delay;
77   XComposeStatus compose;
78
79   Bool meta_sends_esc_p;
80   Bool swap_bs_del_p;
81   Bool meta_done_once;
82   unsigned int meta_mask;
83
84   const char *out_buffer;
85   int out_column;
86 };
87
88
89 static void
90 subproc_cb (XtPointer closure, int *source, XtInputId *id)
91 {
92   text_data *d = (text_data *) closure;
93 # ifdef DEBUG
94   if (! d->input_available_p)
95     fprintf (stderr, "%s: textclient: input available\n", progname);
96 # endif
97   d->input_available_p = True;
98 }
99
100
101 # define BACKSLASH(c) \
102   (! ((c >= 'a' && c <= 'z') || \
103       (c >= 'A' && c <= 'Z') || \
104       (c >= '0' && c <= '9') || \
105       c == '.' || c == '_' || c == '-' || c == '+' || c == '/'))
106
107 #ifdef HAVE_COCOA
108 static char *
109 escape_str (char *s, const char *src)
110 {
111   while (*src) {
112     char c = *src++;
113     if (BACKSLASH(c)) *s++ = '\\';
114     *s++ = c;
115   }
116   return s;
117 }
118 #endif
119
120 static void
121 launch_text_generator (text_data *d)
122 {
123   XtAppContext app = XtDisplayToApplicationContext (d->dpy);
124   char buf[255];
125   const char *oprogram = d->program;
126   char *s;
127
128   size_t oprogram_size = strlen(oprogram);
129   size_t len;
130
131 # ifdef HAVE_COCOA
132   /* /bin/sh on OS X 10.10 wipes out the PATH. */
133   const char *path = getenv("PATH");
134   size_t cmd_capacity = (oprogram_size + strlen(path)) * 2 + 100;
135   char *cmd = s = malloc (cmd_capacity);
136   strcpy (s, "export PATH=");
137   s += strlen (s);
138   s = escape_str (s, path);
139   strcpy (s, "; ");
140   s += strlen (s);
141 # else
142   char *cmd = s = malloc ((strlen(oprogram)) * 2 + 100);
143 # endif
144
145   strcpy (s, "( ");
146   strcat (s, oprogram);
147   s += strlen (s);
148
149   /* Kludge!  Special-case "xscreensaver-text" to tell it how wide
150      the screen is.  We used to do this by just always feeding
151      `program' through sprintf() and setting the default value to
152      "xscreensaver-text --cols %d", but that makes things blow up
153      if someone ever uses a --program that includes a % anywhere.
154    */
155   len = 17; /* strlen("xscreensaver-text") */
156   if (oprogram_size >= len &&
157     !memcmp (oprogram, "xscreensaver-text", len) &&
158     (oprogram[len] == ' ' || !oprogram[len]))
159     {
160       /* strstr is sloppy here. Technically, we should be parsing the command
161          line to identify flags and their arguments. This will blow up if one
162          of those pesky end users could set .textLiteral to "--cols".
163        */
164       if (d->char_w && !strstr (oprogram, "--cols "))
165         sprintf (s, " --cols %d", d->char_w);
166       if (d->max_lines && !strstr (oprogram, "--lines "))
167         sprintf (s, " --lines %d", d->max_lines);
168       s += strlen(s);
169
170 # ifdef HAVE_COCOA
171       /* Also special-case "xscreensaver-text" to specify the text content on
172          the command line. defaults(1) on macOS doesn't know about the default
173          screenhack resources that don't make it into the
174          ~/Library/Preferences/ByHost/org.jwz.xscreensaver.*.plist.
175        */
176
177       char *text_mode_flag = " --date";
178       char *value_res = NULL;
179       char *text_mode = get_string_resource (d->dpy, "textMode", "String");
180
181       if (text_mode)
182         {
183           if (!strcmp (text_mode, "1") || !strcmp (text_mode, "literal"))
184             {
185               text_mode_flag = " --text";
186               value_res = "textLiteral";
187             }
188           else if (!strcmp (text_mode, "2") || !strcmp (text_mode, "file"))
189             {
190               text_mode_flag = " --file";
191               value_res = "textFile";
192             }
193           else if (!strcmp (text_mode, "3") || !strcmp (text_mode, "url"))
194             {
195               text_mode_flag = " --url";
196               value_res = "textURL";
197             }
198           else if (!strcmp (text_mode, "4") || !strcmp (text_mode, "program"))
199             {
200               text_mode_flag = " --program";
201               value_res = "textProgram";
202             }
203
204           free (text_mode);
205         }
206
207       strcpy (s, text_mode_flag);
208       s += strlen (s);
209
210       if (value_res)
211         {
212           size_t old_s = s - cmd;
213           char *value = get_string_resource (d->dpy, value_res, "");
214           if (!value)
215             value = strdup("");
216           cmd = realloc(cmd, cmd_capacity + strlen(value) * 2);
217           s = cmd + old_s;
218           *s = ' ';
219           ++s;
220           s = escape_str(s, value);
221           free(value);
222         }
223 # endif /* HAVE_COCOA */
224     }
225
226   strcpy (s, " ) 2>&1");
227
228 # ifdef DEBUG
229   fprintf (stderr, "%s: textclient: launch %s: %s\n", progname, 
230            (d->pty_p ? "pty" : "pipe"), cmd);
231 # endif
232
233 #ifdef HAVE_FORKPTY
234   if (d->pty_p)
235     {
236       int fd;
237       struct winsize ws;
238       
239       ws.ws_col = d->char_w;
240       ws.ws_row = d->char_h;
241       ws.ws_xpixel = d->pix_w;
242       ws.ws_ypixel = d->pix_h;
243       
244       d->pipe = 0;
245
246 # ifdef HAVE_COCOA
247       if (getenv ("MallocScribble"))
248         /* This is here to stop me from wasting my time trying to answer
249            this question the next time I forget about it. */
250         fprintf (stderr, "%s: WARNING: forkpty hates 'Enable Guard Malloc'\n",
251                  progname);
252 # endif
253
254       if ((d->pid = forkpty(&fd, NULL, NULL, &ws)) < 0)
255         {
256           /* Unable to fork */
257           sprintf (buf, "%.100s: forkpty", progname);
258           perror (buf);
259         }
260       else if (!d->pid)
261         {
262           /* This is the child fork. */
263           char *av[10];
264           int i = 0;
265           if (putenv ("TERM=vt100"))
266             abort();
267           av[i++] = "/bin/sh";
268           av[i++] = "-c";
269           av[i++] = cmd;
270           av[i] = 0;
271 # ifdef DEBUG
272           {
273             int j;
274             fprintf (stderr, "%s: textclient: execvp:", progname);
275             for (j = 0; j < i; j++)
276               fprintf (stderr, " %s", av[j]);
277             fprintf (stderr, "\n");
278           }
279 # endif
280           execvp (av[0], av);
281           sprintf (buf, "%.100s: %.100s", progname, oprogram);
282           perror (buf);
283           exit (1);
284         }
285       else
286         {
287           /* This is the parent fork. */
288           if (d->pipe) abort();
289           d->pipe = fdopen (fd, "r+");
290           if (d->pipe_id) abort();
291           d->pipe_id =
292             XtAppAddInput (app, fileno (d->pipe),
293                            (XtPointer) (XtInputReadMask | XtInputExceptMask),
294                            subproc_cb, (XtPointer) d);
295 # ifdef DEBUG
296           fprintf (stderr, "%s: textclient: pid = %d\n", progname, d->pid);
297 # endif
298         }
299     }
300   else
301 #endif /* HAVE_FORKPTY */
302     {
303       /* don't mess up controlling terminal on "-pipe -program tcsh". */
304       static int protected_stdin_p = 0;
305       if (! protected_stdin_p) {
306         fclose (stdin);
307         open ("/dev/null", O_RDWR); /* re-allocate fd 0 */
308         protected_stdin_p = 1;
309       }
310
311       if (d->pipe) abort();
312       if ((d->pipe = popen (cmd, "r")))
313         {
314           if (d->pipe_id) abort();
315           d->pipe_id =
316             XtAppAddInput (app, fileno (d->pipe),
317                            (XtPointer) (XtInputReadMask | XtInputExceptMask),
318                            subproc_cb, (XtPointer) d);
319 # ifdef DEBUG
320           fprintf (stderr, "%s: textclient: popen\n", progname);
321 # endif
322         }
323       else
324         {
325           sprintf (buf, "%.100s: %.100s", progname, cmd);
326           perror (buf);
327         }
328     }
329
330   free (cmd);
331 }
332
333
334 static void
335 relaunch_generator_timer (XtPointer closure, XtIntervalId *id)
336 {
337   text_data *d = (text_data *) closure;
338   /* if (!d->pipe_timer) abort(); */
339   d->pipe_timer = 0;
340 # ifdef DEBUG
341   fprintf (stderr, "%s: textclient: launch timer fired\n", progname);
342 # endif
343   launch_text_generator (d);
344 }
345
346
347 static void
348 start_timer (text_data *d)
349 {
350   XtAppContext app = XtDisplayToApplicationContext (d->dpy);
351
352 # ifdef DEBUG
353   fprintf (stderr, "%s: textclient: relaunching in %d\n", progname, 
354            (int) d->subproc_relaunch_delay);
355 # endif
356   if (d->pipe_timer)
357     XtRemoveTimeOut (d->pipe_timer);
358   d->pipe_timer =
359     XtAppAddTimeOut (app, d->subproc_relaunch_delay,
360                      relaunch_generator_timer,
361                      (XtPointer) d);
362 }
363
364
365 static void
366 close_pipe (text_data *d)
367 {
368   if (d->pid)
369     {
370 # ifdef DEBUG
371       fprintf (stderr, "%s: textclient: kill %d\n", progname, d->pid);
372 # endif
373       kill (d->pid, SIGTERM);
374     }
375   d->pid = 0;
376
377   if (d->pipe_id)
378     XtRemoveInput (d->pipe_id);
379   d->pipe_id = 0;
380
381   if (d->pipe)
382     {
383 # ifdef DEBUG
384       fprintf (stderr, "%s: textclient: pclose\n", progname);
385 # endif
386       pclose (d->pipe);
387     }
388   d->pipe = 0;
389
390
391 }
392
393
394 void
395 textclient_reshape (text_data *d,
396                     int pix_w, int pix_h,
397                     int char_w, int char_h,
398                     int max_lines)
399 {
400 # if defined(HAVE_FORKPTY) && defined(TIOCSWINSZ)
401
402   d->pix_w  = pix_w;
403   d->pix_h  = pix_h;
404   d->char_w = char_w;
405   d->char_h = char_h;
406   d->max_lines = max_lines;
407
408 # ifdef DEBUG
409   fprintf (stderr, "%s: textclient: reshape: %dx%d, %dx%d\n", progname,
410            pix_w, pix_h, char_w, char_h);
411 # endif
412
413   if (d->pid && d->pipe)
414     {
415       /* Tell the sub-process that the screen size has changed. */
416       struct winsize ws;
417       ws.ws_col = char_w;
418       ws.ws_row = char_h;
419       ws.ws_xpixel = pix_w;
420       ws.ws_ypixel = pix_h;
421       ioctl (fileno (d->pipe), TIOCSWINSZ, &ws);
422       kill (d->pid, SIGWINCH);
423 #  ifdef DEBUG
424       fprintf (stderr, "%s: textclient: SIGWINCH\n", progname);
425 #  endif
426     }
427 # endif /* HAVE_FORKPTY && TIOCSWINSZ */
428
429
430   /* If we're running xscreensaver-text, then kill and restart it any
431      time the window is resized so that it gets an updated --cols arg
432      right away.  But if we're running something else, leave it alone.
433    */
434   if (!strcmp (d->program, "xscreensaver-text"))
435     {
436 # ifdef DEBUG
437       fprintf (stderr, "%s: textclient: reshape relaunch\n", progname);
438 # endif
439       close_pipe (d);
440       d->input_available_p = False;
441       start_timer (d);
442     }
443 }
444
445
446 text_data *
447 textclient_open (Display *dpy)
448 {
449   text_data *d = (text_data *) calloc (1, sizeof (*d));
450
451 # ifdef DEBUG
452   fprintf (stderr, "%s: textclient: init\n", progname);
453 # endif
454
455   d->dpy = dpy;
456
457   if (get_boolean_resource (dpy, "usePty", "UsePty"))
458     {
459 # ifdef HAVE_FORKPTY
460       d->pty_p = True;
461 # else
462       fprintf (stderr,
463                "%s: no pty support on this system; using a pipe instead.\n",
464                progname);
465 # endif
466     }
467
468   d->subproc_relaunch_delay =
469     get_integer_resource (dpy, "relaunchDelay", "Time");
470   if (d->subproc_relaunch_delay < 1)
471     d->subproc_relaunch_delay = 1;
472   d->subproc_relaunch_delay *= 1000;
473
474
475   d->meta_sends_esc_p = get_boolean_resource (dpy, "metaSendsESC", "Boolean");
476   d->swap_bs_del_p    = get_boolean_resource (dpy, "swapBSDEL",    "Boolean");
477
478   d->program = get_string_resource (dpy, "program", "Program");
479
480
481 # ifdef HAVE_FORKPTY
482   /* Kludge for MacOS standalone mode: see OSX/SaverRunner.m. */
483   {
484     const char *s = getenv ("XSCREENSAVER_STANDALONE");
485     if (s && *s && strcmp(s, "0"))
486       {
487         d->pty_p = 1;
488         d->program = strdup (getenv ("SHELL"));
489 #  ifdef DEBUG
490         fprintf (stderr, "%s: textclient: standalone: %s\n",
491                  progname, d->program);
492 #  endif
493       }
494   }
495 # endif
496
497   start_timer (d);
498
499   return d;
500 }
501
502
503 void
504 textclient_close (text_data *d)
505 {
506 # ifdef DEBUG
507   fprintf (stderr, "%s: textclient: free\n", progname);
508 # endif
509
510   close_pipe (d);
511   if (d->program)
512     free (d->program);
513   if (d->pipe_timer)
514     XtRemoveTimeOut (d->pipe_timer);
515   d->pipe_timer = 0;
516   memset (d, 0, sizeof (*d));
517   free (d);
518 }
519
520 int
521 textclient_getc (text_data *d)
522 {
523   XtAppContext app = XtDisplayToApplicationContext (d->dpy);
524   int ret = -1;
525
526   if (XtAppPending (app) & (XtIMTimer|XtIMAlternateInput))
527     XtAppProcessEvent (app, XtIMTimer|XtIMAlternateInput);
528
529   if (d->out_buffer && *d->out_buffer)
530     {
531       ret = *d->out_buffer;
532       d->out_buffer++;
533     }
534   else if (d->input_available_p && d->pipe)
535     {
536       unsigned char s[2];
537       int n = read (fileno (d->pipe), (void *) s, 1);
538       if (n > 0)
539         ret = s[0];
540       else              /* EOF */
541         {
542           if (d->pid)
543             {
544 # ifdef DEBUG
545               fprintf (stderr, "%s: textclient: waitpid %d\n",
546                        progname, d->pid);
547 # endif
548               waitpid (d->pid, NULL, 0);
549               d->pid = 0;
550             }
551
552           close_pipe (d);
553
554           if (d->out_column > 0)
555             {
556 # ifdef DEBUG
557               fprintf (stderr, "%s: textclient: adding blank line at EOF\n",
558                        progname);
559 # endif
560               d->out_buffer = "\r\n\r\n";
561             }
562
563           start_timer (d);
564         }
565       d->input_available_p = False;
566     }
567
568   if (ret == '\r' || ret == '\n')
569     d->out_column = 0;
570   else if (ret > 0)
571     d->out_column++;
572
573 # ifdef DEBUG
574   if (ret <= 0)
575     fprintf (stderr, "%s: textclient: getc: %d\n", progname, ret);
576   else if (ret < ' ')
577     fprintf (stderr, "%s: textclient: getc: %03o\n", progname, ret);
578   else
579     fprintf (stderr, "%s: textclient: getc: '%c'\n", progname, (char) ret);
580 # endif
581
582   return ret;
583 }
584
585
586 /* The interpretation of the ModN modifiers is dependent on what keys
587    are bound to them: Mod1 does not necessarily mean "meta".  It only
588    means "meta" if Meta_L or Meta_R are bound to it.  If Meta_L is on
589    Mod5, then Mod5 is the one that means Meta.  Oh, and Meta and Alt
590    aren't necessarily the same thing.  Icepicks in my forehead!
591  */
592 static unsigned int
593 do_icccm_meta_key_stupidity (Display *dpy)
594 {
595   unsigned int modbits = 0;
596 # ifndef HAVE_COCOA
597   int i, j, k;
598   XModifierKeymap *modmap = XGetModifierMapping (dpy);
599   for (i = 3; i < 8; i++)
600     for (j = 0; j < modmap->max_keypermod; j++)
601       {
602         int code = modmap->modifiermap[i * modmap->max_keypermod + j];
603         KeySym *syms;
604         int nsyms = 0;
605         if (code == 0) continue;
606         syms = XGetKeyboardMapping (dpy, code, 1, &nsyms);
607         for (k = 0; k < nsyms; k++)
608           if (syms[k] == XK_Meta_L || syms[k] == XK_Meta_R ||
609               syms[k] == XK_Alt_L  || syms[k] == XK_Alt_R)
610             modbits |= (1 << i);
611         XFree (syms);
612       }
613   XFreeModifiermap (modmap);
614 # endif /* HAVE_COCOA */
615   return modbits;
616 }
617
618
619 /* Returns a mask of the bit or bits of a KeyPress event that mean "meta". 
620  */
621 static unsigned int
622 meta_modifier (text_data *d)
623 {
624   if (!d->meta_done_once)
625     {
626       /* Really, we are supposed to recompute this if a KeymapNotify
627          event comes in, but fuck it. */
628       d->meta_done_once = True;
629       d->meta_mask = do_icccm_meta_key_stupidity (d->dpy);
630 # ifdef DEBUG
631       fprintf (stderr, "%s: textclient: ICCCM Meta is 0x%08X\n",
632                progname, d->meta_mask);
633 # endif
634     }
635   return d->meta_mask;
636 }
637
638
639 Bool
640 textclient_putc (text_data *d, XKeyEvent *k)
641 {
642   KeySym keysym;
643   unsigned char c = 0;
644   XLookupString (k, (char *) &c, 1, &keysym, &d->compose);
645   if (c != 0 && d->pipe)
646     {
647       if (!d->swap_bs_del_p) ;
648       else if (c == 127) c = 8;
649       else if (c == 8)   c = 127;
650
651       /* If meta was held down, send ESC, or turn on the high bit. */
652       if (k->state & meta_modifier (d))
653         {
654           if (d->meta_sends_esc_p)
655             fputc ('\033', d->pipe);
656           else
657             c |= 0x80;
658         }
659
660       fputc (c, d->pipe);
661       fflush (d->pipe);
662       k->type = 0;  /* don't interpret this event defaultly. */
663
664 # ifdef DEBUG
665       fprintf (stderr, "%s: textclient: putc '%c'\n", progname, (char) c);
666 # endif
667
668       return True;
669     }
670   return False;
671 }
672
673 #endif /* !USE_IPHONE -- whole file */