Added missing newline in NEDsim error message.
[screensavers] / screenhack / textclient.c
CommitLineData
3144ee8a
AT
1/* xscreensaver, Copyright (c) 2012-2020 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(HAVE_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
62extern const char *progname;
63
64struct 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
89static void
90subproc_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
108static char *
109escape_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
121/* Let's see if we're able to fork and exec at all. Thanks, macOS.
122 */
123static Bool
124selftest (void)
125{
126 static Bool done = False;
127 pid_t pid;
128 char buf [255];
129 if (done) return True;
130
131 pid = fork ();
132 switch ((int) pid)
133 {
134 case -1:
135 sprintf (buf, "%s: textclient: selftest: couldn't fork", progname);
136 perror (buf);
137 return False;
138
139 case 0: /* child */
140 {
141 char * const av[] = { "/bin/sh", "-c", "true", 0 };
142 execvp (av[0], av);
143 exit (1); /* exits child fork */
144 break;
145 }
146
147 default: /* parent */
148 {
149 int status = -1;
150 int i;
151 /* Busy-loops are bad mmmmkayyyy */
152 struct timeval tv;
153 tv.tv_sec = 0;
154 tv.tv_usec = 100000L; /* 0.1 sec */
155 for (i = 0; i < 50; i++) { /* 5 sec max */
156 pid_t pid2 = waitpid (pid, &status, 0);
157 if (pid == pid2) break;
158 (void) select (0, 0, 0, 0, &tv);
159 }
160
161 if (status != 0)
162 {
163# ifdef DEBUG
164 fprintf (stderr, "%s: selftest: textclient: status %d\n",
165 progname, status);
166# endif
167 return False;
168 }
169 else
170 {
171# ifdef DEBUG
172 fprintf (stderr, "%s: textclient: selftest ok\n", progname);
173# endif
174 done = True;
175 }
176 break;
177 }
178 }
179
180 return True;
181}
182
183
184static void start_timer (text_data *d);
185
186static void
187launch_text_generator (text_data *d)
188{
189 XtAppContext app = XtDisplayToApplicationContext (d->dpy);
190 char buf[255];
191 const char *oprogram = d->program;
192 char *s;
193
194 size_t oprogram_size = strlen(oprogram);
195 size_t len;
196
197# ifdef HAVE_COCOA
198 /* /bin/sh on OS X 10.10 wipes out the PATH. */
199 const char *path = getenv("PATH");
200 size_t cmd_capacity = (oprogram_size + strlen(path)) * 2 + 100;
201 char *cmd = s = malloc (cmd_capacity);
202 strcpy (s, "export PATH=");
203 s += strlen (s);
204 s = escape_str (s, path);
205 strcpy (s, "; ");
206 s += strlen (s);
207# else
208 char *cmd = s = malloc ((strlen(oprogram)) * 2 + 100);
209# endif
210
211 if (!selftest())
212 {
213 if (!d->out_buffer || !*d->out_buffer)
214 d->out_buffer = "Can't exec; Gatekeeper problem?\r\n\r\n";
215 start_timer (d);
216 return;
217 }
218
219 strcpy (s, "( ");
220 strcat (s, oprogram);
221 s += strlen (s);
222
223 /* Kludge! Special-case "xscreensaver-text" to tell it how wide
224 the screen is. We used to do this by just always feeding
225 `program' through sprintf() and setting the default value to
226 "xscreensaver-text --cols %d", but that makes things blow up
227 if someone ever uses a --program that includes a % anywhere.
228 */
229 len = 17; /* strlen("xscreensaver-text") */
230 if (oprogram_size >= len &&
231 !memcmp (oprogram, "xscreensaver-text", len) &&
232 (oprogram[len] == ' ' || !oprogram[len]))
233 {
234 /* strstr is sloppy here. Technically, we should be parsing the command
235 line to identify flags and their arguments. This will blow up if one
236 of those pesky end users could set .textLiteral to "--cols".
237 */
238 if (d->char_w && !strstr (oprogram, "--cols "))
239 sprintf (s, " --cols %d", d->char_w);
240 if (d->max_lines && !strstr (oprogram, "--lines "))
241 sprintf (s, " --lines %d", d->max_lines);
242 s += strlen(s);
243
244# ifdef HAVE_COCOA
245 /* Also special-case "xscreensaver-text" to specify the text content on
246 the command line. defaults(1) on macOS doesn't know about the default
247 screenhack resources that don't make it into the
248 ~/Library/Preferences/ByHost/org.jwz.xscreensaver.*.plist.
249 */
250
251 char *text_mode_flag = " --date";
252 char *value_res = NULL;
253 char *text_mode = get_string_resource (d->dpy, "textMode", "String");
254
255 if (text_mode)
256 {
257 if (!strcmp (text_mode, "1") || !strcmp (text_mode, "literal"))
258 {
259 text_mode_flag = " --text";
260 value_res = "textLiteral";
261 }
262 else if (!strcmp (text_mode, "2") || !strcmp (text_mode, "file"))
263 {
264 text_mode_flag = " --file";
265 value_res = "textFile";
266 }
267 else if (!strcmp (text_mode, "3") || !strcmp (text_mode, "url"))
268 {
269 text_mode_flag = " --url";
270 value_res = "textURL";
271 }
272 else if (!strcmp (text_mode, "4") || !strcmp (text_mode, "program"))
273 {
274 text_mode_flag = " --program";
275 value_res = "textProgram";
276 }
277
278 free (text_mode);
279 }
280
281 strcpy (s, text_mode_flag);
282 s += strlen (s);
283
284 if (value_res)
285 {
286 size_t old_s = s - cmd;
287 char *value = get_string_resource (d->dpy, value_res, "");
288 if (!value)
289 value = strdup("");
290 cmd = realloc(cmd, cmd_capacity + strlen(value) * 2);
291 s = cmd + old_s;
292 *s = ' ';
293 ++s;
294 s = escape_str(s, value);
295 free(value);
296 }
297# endif /* HAVE_COCOA */
298 }
299
300 strcpy (s, " ) 2>&1");
301
302# ifdef DEBUG
303 fprintf (stderr, "%s: textclient: launch %s: %s\n", progname,
304 (d->pty_p ? "pty" : "pipe"), cmd);
305# endif
306
307#ifdef HAVE_FORKPTY
308 if (d->pty_p)
309 {
310 int fd;
311 struct winsize ws;
312
313 ws.ws_col = d->char_w;
314 ws.ws_row = d->char_h;
315 ws.ws_xpixel = d->pix_w;
316 ws.ws_ypixel = d->pix_h;
317
318 d->pipe = 0;
319
320# ifdef HAVE_COCOA
321 if (getenv ("MallocScribble"))
322 /* This is here to stop me from wasting my time trying to answer
323 this question the next time I forget about it. */
324 fprintf (stderr, "%s: WARNING: forkpty hates 'Enable Guard Malloc'\n",
325 progname);
326# endif
327
328 if ((d->pid = forkpty(&fd, NULL, NULL, &ws)) < 0)
329 {
330 /* Unable to fork */
331 sprintf (buf, "%.100s: forkpty", progname);
332 perror (buf);
333 }
334 else if (!d->pid)
335 {
336 /* This is the child fork. */
337 char *av[10];
338 int i = 0;
339 if (putenv ("TERM=vt100"))
340 abort();
341 av[i++] = "/bin/sh";
342 av[i++] = "-c";
343 av[i++] = cmd;
344 av[i] = 0;
345# ifdef DEBUG
346 {
347 int j;
348 fprintf (stderr, "%s: textclient: execvp:", progname);
349 for (j = 0; j < i; j++)
350 fprintf (stderr, " %s", av[j]);
351 fprintf (stderr, "\n");
352 }
353# endif
354 execvp (av[0], av);
355 sprintf (buf, "%.100s: %.100s", progname, oprogram);
356 perror (buf);
357 exit (1);
358 }
359 else
360 {
361 /* This is the parent fork. */
362 if (d->pipe) abort();
363 d->pipe = fdopen (fd, "r+");
364 if (d->pipe_id) abort();
365 d->pipe_id =
366 XtAppAddInput (app, fileno (d->pipe),
367 (XtPointer) (XtInputReadMask | XtInputExceptMask),
368 subproc_cb, (XtPointer) d);
369# ifdef DEBUG
370 fprintf (stderr, "%s: textclient: pid = %d\n", progname, d->pid);
371# endif
372 }
373 }
374 else
375#endif /* HAVE_FORKPTY */
376 {
377 /* don't mess up controlling terminal on "-pipe -program tcsh". */
378 static int protected_stdin_p = 0;
379 if (! protected_stdin_p) {
380 fclose (stdin);
381 open ("/dev/null", O_RDWR); /* re-allocate fd 0 */
382 protected_stdin_p = 1;
383 }
384
385 if (d->pipe) abort();
386 if ((d->pipe = popen (cmd, "r")))
387 {
388 if (d->pipe_id) abort();
389 d->pipe_id =
390 XtAppAddInput (app, fileno (d->pipe),
391 (XtPointer) (XtInputReadMask | XtInputExceptMask),
392 subproc_cb, (XtPointer) d);
393# ifdef DEBUG
394 fprintf (stderr, "%s: textclient: popen\n", progname);
395# endif
396 }
397 else
398 {
399 sprintf (buf, "%.100s: %.100s", progname, cmd);
400 perror (buf);
401 }
402 }
403
404 free (cmd);
405}
406
407
408static void
409relaunch_generator_timer (XtPointer closure, XtIntervalId *id)
410{
411 text_data *d = (text_data *) closure;
412 /* if (!d->pipe_timer) abort(); */
413 d->pipe_timer = 0;
414# ifdef DEBUG
415 fprintf (stderr, "%s: textclient: launch timer fired\n", progname);
416# endif
417 launch_text_generator (d);
418}
419
420
421static void
422start_timer (text_data *d)
423{
424 XtAppContext app = XtDisplayToApplicationContext (d->dpy);
425
426# ifdef DEBUG
427 fprintf (stderr, "%s: textclient: relaunching in %d\n", progname,
428 (int) d->subproc_relaunch_delay);
429# endif
430 if (d->pipe_timer)
431 XtRemoveTimeOut (d->pipe_timer);
432 d->pipe_timer =
433 XtAppAddTimeOut (app, d->subproc_relaunch_delay,
434 relaunch_generator_timer,
435 (XtPointer) d);
436}
437
438
439static void
440close_pipe (text_data *d)
441{
442 if (d->pid)
443 {
444# ifdef DEBUG
445 fprintf (stderr, "%s: textclient: kill %d\n", progname, d->pid);
446# endif
447 kill (d->pid, SIGTERM);
448 }
449 d->pid = 0;
450
451 if (d->pipe_id)
452 XtRemoveInput (d->pipe_id);
453 d->pipe_id = 0;
454
455 if (d->pipe)
456 {
457# ifdef DEBUG
458 fprintf (stderr, "%s: textclient: pclose\n", progname);
459# endif
460 pclose (d->pipe);
461 }
462 d->pipe = 0;
463
464
465}
466
467
468void
469textclient_reshape (text_data *d,
470 int pix_w, int pix_h,
471 int char_w, int char_h,
472 int max_lines)
473{
474# if defined(HAVE_FORKPTY) && defined(TIOCSWINSZ)
475
476 d->pix_w = pix_w;
477 d->pix_h = pix_h;
478 d->char_w = char_w;
479 d->char_h = char_h;
480 d->max_lines = max_lines;
481
482# ifdef DEBUG
483 fprintf (stderr, "%s: textclient: reshape: %dx%d, %dx%d\n", progname,
484 pix_w, pix_h, char_w, char_h);
485# endif
486
487 if (d->pid && d->pipe)
488 {
489 /* Tell the sub-process that the screen size has changed. */
490 struct winsize ws;
491 ws.ws_col = char_w;
492 ws.ws_row = char_h;
493 ws.ws_xpixel = pix_w;
494 ws.ws_ypixel = pix_h;
495 ioctl (fileno (d->pipe), TIOCSWINSZ, &ws);
496 kill (d->pid, SIGWINCH);
497# ifdef DEBUG
498 fprintf (stderr, "%s: textclient: SIGWINCH\n", progname);
499# endif
500 }
501# endif /* HAVE_FORKPTY && TIOCSWINSZ */
502
503
504 /* If we're running xscreensaver-text, then kill and restart it any
505 time the window is resized so that it gets an updated --cols arg
506 right away. But if we're running something else, leave it alone.
507 */
508 if (!strcmp (d->program, "xscreensaver-text"))
509 {
510# ifdef DEBUG
511 fprintf (stderr, "%s: textclient: reshape relaunch\n", progname);
512# endif
513 close_pipe (d);
514 d->input_available_p = False;
515 start_timer (d);
516 }
517}
518
519
520text_data *
521textclient_open (Display *dpy)
522{
523 text_data *d = (text_data *) calloc (1, sizeof (*d));
524
525# ifdef DEBUG
526 fprintf (stderr, "%s: textclient: init\n", progname);
527# endif
528
529 d->dpy = dpy;
530
531 if (get_boolean_resource (dpy, "usePty", "UsePty"))
532 {
533# ifdef HAVE_FORKPTY
534 d->pty_p = True;
535# else
536 fprintf (stderr,
537 "%s: no pty support on this system; using a pipe instead.\n",
538 progname);
539# endif
540 }
541
542 d->subproc_relaunch_delay =
543 get_integer_resource (dpy, "relaunchDelay", "Time");
544 if (d->subproc_relaunch_delay < 1)
545 d->subproc_relaunch_delay = 1;
546 d->subproc_relaunch_delay *= 1000;
547
548
549 d->meta_sends_esc_p = get_boolean_resource (dpy, "metaSendsESC", "Boolean");
550 d->swap_bs_del_p = get_boolean_resource (dpy, "swapBSDEL", "Boolean");
551
552 d->program = get_string_resource (dpy, "program", "Program");
553
554
555# ifdef HAVE_FORKPTY
556 /* Kludge for MacOS standalone mode: see OSX/SaverRunner.m. */
557 {
558 const char *s = getenv ("XSCREENSAVER_STANDALONE");
559 if (s && *s && strcmp(s, "0"))
560 {
561 d->pty_p = 1;
562 d->program = strdup (getenv ("SHELL"));
563# ifdef DEBUG
564 fprintf (stderr, "%s: textclient: standalone: %s\n",
565 progname, d->program);
566# endif
567 }
568 }
569# endif
570
571 start_timer (d);
572
573 return d;
574}
575
576
577void
578textclient_close (text_data *d)
579{
580# ifdef DEBUG
581 fprintf (stderr, "%s: textclient: free\n", progname);
582# endif
583
584 close_pipe (d);
585 if (d->program)
586 free (d->program);
587 if (d->pipe_timer)
588 XtRemoveTimeOut (d->pipe_timer);
589 d->pipe_timer = 0;
590 memset (d, 0, sizeof (*d));
591 free (d);
592}
593
594int
595textclient_getc (text_data *d)
596{
597 XtAppContext app = XtDisplayToApplicationContext (d->dpy);
598 int ret = -1;
599
600 if (XtAppPending (app) & (XtIMTimer|XtIMAlternateInput))
601 XtAppProcessEvent (app, XtIMTimer|XtIMAlternateInput);
602
603 if (d->out_buffer && *d->out_buffer)
604 {
605 ret = *d->out_buffer;
606 d->out_buffer++;
607 }
608 else if (d->input_available_p && d->pipe)
609 {
610 unsigned char s[2];
611 int n = read (fileno (d->pipe), (void *) s, 1);
612 if (n > 0)
613 ret = s[0];
614 else /* EOF */
615 {
616 if (d->pid)
617 {
618# ifdef DEBUG
619 fprintf (stderr, "%s: textclient: waitpid %d\n",
620 progname, d->pid);
621# endif
622 waitpid (d->pid, NULL, 0);
623 d->pid = 0;
624 }
625
626 close_pipe (d);
627
628 if (d->out_column > 0)
629 {
630# ifdef DEBUG
631 fprintf (stderr, "%s: textclient: adding blank line at EOF\n",
632 progname);
633# endif
634 d->out_buffer = "\r\n\r\n";
635 }
636
637 start_timer (d);
638 }
639 d->input_available_p = False;
640 }
641
642 if (ret == '\r' || ret == '\n')
643 d->out_column = 0;
644 else if (ret > 0)
645 d->out_column++;
646
647# ifdef DEBUG
648 if (ret <= 0)
649 fprintf (stderr, "%s: textclient: getc: %d\n", progname, ret);
650 else if (ret < ' ')
651 fprintf (stderr, "%s: textclient: getc: %03o\n", progname, ret);
652 else
653 fprintf (stderr, "%s: textclient: getc: '%c'\n", progname, (char) ret);
654# endif
655
656 return ret;
657}
658
659
660/* The interpretation of the ModN modifiers is dependent on what keys
661 are bound to them: Mod1 does not necessarily mean "meta". It only
662 means "meta" if Meta_L or Meta_R are bound to it. If Meta_L is on
663 Mod5, then Mod5 is the one that means Meta. Oh, and Meta and Alt
664 aren't necessarily the same thing. Icepicks in my forehead!
665 */
666static unsigned int
667do_icccm_meta_key_stupidity (Display *dpy)
668{
669 unsigned int modbits = 0;
670# ifndef HAVE_COCOA
671 int i, j, k;
672 XModifierKeymap *modmap = XGetModifierMapping (dpy);
673 for (i = 3; i < 8; i++)
674 for (j = 0; j < modmap->max_keypermod; j++)
675 {
676 int code = modmap->modifiermap[i * modmap->max_keypermod + j];
677 KeySym *syms;
678 int nsyms = 0;
679 if (code == 0) continue;
680 syms = XGetKeyboardMapping (dpy, code, 1, &nsyms);
681 for (k = 0; k < nsyms; k++)
682 if (syms[k] == XK_Meta_L || syms[k] == XK_Meta_R ||
683 syms[k] == XK_Alt_L || syms[k] == XK_Alt_R)
684 modbits |= (1 << i);
685 XFree (syms);
686 }
687 XFreeModifiermap (modmap);
688# endif /* HAVE_COCOA */
689 return modbits;
690}
691
692
693/* Returns a mask of the bit or bits of a KeyPress event that mean "meta".
694 */
695static unsigned int
696meta_modifier (text_data *d)
697{
698 if (!d->meta_done_once)
699 {
700 /* Really, we are supposed to recompute this if a KeymapNotify
701 event comes in, but fuck it. */
702 d->meta_done_once = True;
703 d->meta_mask = do_icccm_meta_key_stupidity (d->dpy);
704# ifdef DEBUG
705 fprintf (stderr, "%s: textclient: ICCCM Meta is 0x%08X\n",
706 progname, d->meta_mask);
707# endif
708 }
709 return d->meta_mask;
710}
711
712
713Bool
714textclient_putc (text_data *d, XKeyEvent *k)
715{
716 KeySym keysym;
717 unsigned char c = 0;
718 XLookupString (k, (char *) &c, 1, &keysym, &d->compose);
719 if (c != 0 && d->pipe)
720 {
721 if (!d->swap_bs_del_p) ;
722 else if (c == 127) c = 8;
723 else if (c == 8) c = 127;
724
725 /* If meta was held down, send ESC, or turn on the high bit. */
726 if (k->state & meta_modifier (d))
727 {
728 if (d->meta_sends_esc_p)
729 fputc ('\033', d->pipe);
730 else
731 c |= 0x80;
732 }
733
734 fputc (c, d->pipe);
735 fflush (d->pipe);
736 k->type = 0; /* don't interpret this event defaultly. */
737
738# ifdef DEBUG
739 fprintf (stderr, "%s: textclient: putc '%c'\n", progname, (char) c);
740# endif
741
742 return True;
743 }
744 return False;
745}
746
747#endif /* !HAVE_IPHONE -- whole file */