1/* A front-end using readline to "cook" input lines.
2 *
3 * Copyright (C) 2004, 1999  Per Bothner
4 *
5 * This front-end program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License as published
7 * by the Free Software Foundation; either version 2, or (at your option)
8 * any later version.
9 *
10 * Some code from Johnson & Troan: "Linux Application Development"
11 * (Addison-Wesley, 1998) was used directly or for inspiration.
12 *
13 * 2003-11-07 Wolfgang Taeuber <wolfgang_taeuber@agilent.com>
14 * Specify a history file and the size of the history file with command
15 * line options; use EDITOR/VISUAL to set vi/emacs preference.
16 */
17
18/* PROBLEMS/TODO:
19 *
20 * Only tested under GNU/Linux and Mac OS 10.x;  needs to be ported.
21 *
22 * Switching between line-editing-mode vs raw-char-mode depending on
23 * what tcgetattr returns is inherently not robust, plus it doesn't
24 * work when ssh/telnetting in.  A better solution is possible if the
25 * tty system can send in-line escape sequences indicating the current
26 * mode, echo'd input, etc.  That would also allow a user preference
27 * to set different colors for prompt, input, stdout, and stderr.
28 *
29 * When running mc -c under the Linux console, mc does not recognize
30 * mouse clicks, which mc does when not running under rlfe.
31 *
32 * Pasting selected text containing tabs is like hitting the tab character,
33 * which invokes readline completion.  We don't want this.  I don't know
34 * if this is fixable without integrating rlfe into a terminal emulator.
35 *
36 * Echo suppression is a kludge, but can only be avoided with better kernel
37 * support: We need a tty mode to disable "real" echoing, while still
38 * letting the inferior think its tty driver to doing echoing.
39 * Stevens's book claims SCR$ and BSD4.3+ have TIOCREMOTE.
40 *
41 * The latest readline may have some hooks we can use to avoid having
42 * to back up the prompt. (See HAVE_ALREADY_PROMPTED.)
43 *
44 * Desirable readline feature:  When in cooked no-echo mode (e.g. password),
45 * echo characters are they are types with '*', but remove them when done.
46 *
47 * Asynchronous output while we're editing an input line should be
48 * inserted in the output view *before* the input line, so that the
49 * lines being edited (with the prompt) float at the end of the input.
50 *
51 * A "page mode" option to emulate more/less behavior:  At each page of
52 * output, pause for a user command.  This required parsing the output
53 * to keep track of line lengths.  It also requires remembering the
54 * output, if we want an option to scroll back, which suggests that
55 * this should be integrated with a terminal emulator like xterm.
56 */
57
58#include <stdio.h>
59#include <fcntl.h>
60#include <sys/types.h>
61#include <sys/socket.h>
62#include <netinet/in.h>
63#include <arpa/inet.h>
64#include <signal.h>
65#include <netdb.h>
66#include <stdlib.h>
67#include <errno.h>
68#include <grp.h>
69#include <string.h>
70#include <sys/stat.h>
71#include <unistd.h>
72#include <sys/ioctl.h>
73#include <termios.h>
74
75#include "config.h"
76
77#ifdef READLINE_LIBRARY
78#  include "readline.h"
79#  include "history.h"
80#else
81#  include <readline/readline.h>
82#  include <readline/history.h>
83#endif
84
85#ifndef COMMAND
86#define COMMAND "/bin/bash"
87#endif
88#ifndef COMMAND_ARGS
89#define COMMAND_ARGS COMMAND
90#endif
91
92#ifndef ALT_COMMAND
93#define ALT_COMMAND "/bin/sh"
94#endif
95#ifndef ALT_COMMAND_ARGS
96#define ALT_COMMAND_ARGS ALT_COMMAND
97#endif
98
99#ifndef HAVE_MEMMOVE
100#  if __GNUC__ > 1
101#    define memmove(d, s, n)	__builtin_memcpy(d, s, n)
102#  else
103#    define memmove(d, s, n)	memcpy(d, s, n)
104#  endif
105#else
106#  define memmove(d, s, n)	memcpy(d, s, n)
107#endif
108
109#define APPLICATION_NAME "rlfe"
110
111static int in_from_inferior_fd;
112static int out_to_inferior_fd;
113static void set_edit_mode ();
114static void usage_exit ();
115static char *hist_file = 0;
116static int  hist_size = 0;
117
118/* Unfortunately, we cannot safely display echo from the inferior process.
119   The reason is that the echo bit in the pty is "owned" by the inferior,
120   and if we try to turn it off, we could confuse the inferior.
121   Thus, when echoing, we get echo twice:  First readline echoes while
122   we're actually editing. Then we send the line to the inferior, and the
123   terminal driver send back an extra echo.
124   The work-around is to remember the input lines, and when we see that
125   line come back, we supress the output.
126   A better solution (supposedly available on SVR4) would be a smarter
127   terminal driver, with more flags ... */
128#define ECHO_SUPPRESS_MAX 1024
129char echo_suppress_buffer[ECHO_SUPPRESS_MAX];
130int echo_suppress_start = 0;
131int echo_suppress_limit = 0;
132
133/*#define DEBUG*/
134
135#ifdef DEBUG
136FILE *logfile = NULL;
137#define DPRINT0(FMT) (fprintf(logfile, FMT), fflush(logfile))
138#define DPRINT1(FMT, V1) (fprintf(logfile, FMT, V1), fflush(logfile))
139#define DPRINT2(FMT, V1, V2) (fprintf(logfile, FMT, V1, V2), fflush(logfile))
140#else
141#define DPRINT0(FMT) ((void) 0) /* Do nothing */
142#define DPRINT1(FMT, V1) ((void) 0) /* Do nothing */
143#define DPRINT2(FMT, V1, V2) ((void) 0) /* Do nothing */
144#endif
145
146struct termios orig_term;
147
148/* Pid of child process. */
149static pid_t child = -1;
150
151static void
152sig_child (int signo)
153{
154  int status;
155  wait (&status);
156  if (hist_file != 0)
157    {
158      write_history (hist_file);
159      if (hist_size)
160	history_truncate_file (hist_file, hist_size);
161    }
162  DPRINT0 ("(Child process died.)\n");
163  tcsetattr(STDIN_FILENO, TCSANOW, &orig_term);
164  exit (0);
165}
166
167volatile int propagate_sigwinch = 0;
168
169/* sigwinch_handler
170 * propagate window size changes from input file descriptor to
171 * master side of pty.
172 */
173void sigwinch_handler(int signal) {
174   propagate_sigwinch = 1;
175}
176
177
178/* get_slave_pty() returns an integer file descriptor.
179 * If it returns < 0, an error has occurred.
180 * Otherwise, it has returned the slave file descriptor.
181 */
182
183int get_slave_pty(char *name) {
184   struct group *gptr;
185   gid_t gid;
186   int slave = -1;
187
188   /* chown/chmod the corresponding pty, if possible.
189    * This will only work if the process has root permissions.
190    * Alternatively, write and exec a small setuid program that
191    * does just this.
192    */
193   if ((gptr = getgrnam("tty")) != 0) {
194      gid = gptr->gr_gid;
195   } else {
196      /* if the tty group does not exist, don't change the
197       * group on the slave pty, only the owner
198       */
199      gid = -1;
200   }
201
202   /* Note that we do not check for errors here.  If this is code
203    * where these actions are critical, check for errors!
204    */
205   chown(name, getuid(), gid);
206   /* This code only makes the slave read/writeable for the user.
207    * If this is for an interactive shell that will want to
208    * receive "write" and "wall" messages, OR S_IWGRP into the
209    * second argument below.
210    */
211   chmod(name, S_IRUSR|S_IWUSR);
212
213   /* open the corresponding slave pty */
214   slave = open(name, O_RDWR);
215   return (slave);
216}
217
218/* Certain special characters, such as ctrl/C, we want to pass directly
219   to the inferior, rather than letting readline handle them. */
220
221static char special_chars[20];
222static int special_chars_count;
223
224static void
225add_special_char(int ch)
226{
227  if (ch != 0)
228    special_chars[special_chars_count++] = ch;
229}
230
231static int eof_char;
232
233static int
234is_special_char(int ch)
235{
236  int i;
237#if 0
238  if (ch == eof_char && rl_point == rl_end)
239    return 1;
240#endif
241  for (i = special_chars_count;  --i >= 0; )
242    if (special_chars[i] == ch)
243      return 1;
244  return 0;
245}
246
247static char buf[1024];
248/* buf[0 .. buf_count-1] is the what has been emitted on the current line.
249   It is used as the readline prompt. */
250static int buf_count = 0;
251
252int do_emphasize_input = 1;
253int current_emphasize_input;
254
255char *start_input_mode = "\033[1m";
256char *end_input_mode = "\033[0m";
257
258int num_keys = 0;
259
260static void maybe_emphasize_input (int on)
261{
262  if (on == current_emphasize_input
263      || (on && ! do_emphasize_input))
264    return;
265  fprintf (rl_outstream, on ? start_input_mode : end_input_mode);
266  fflush (rl_outstream);
267  current_emphasize_input = on;
268}
269
270static void
271null_prep_terminal (int meta)
272{
273}
274
275static void
276null_deprep_terminal ()
277{
278  maybe_emphasize_input (0);
279}
280
281static int
282pre_input_change_mode (void)
283{
284  return 0;
285}
286
287char pending_special_char;
288
289static void
290line_handler (char *line)
291{
292  if (line == NULL)
293    {
294      char buf[1];
295      DPRINT0("saw eof!\n");
296      buf[0] = '\004'; /* ctrl/d */
297      write (out_to_inferior_fd, buf, 1);
298    }
299  else
300    {
301      static char enter[] = "\r";
302      /*  Send line to inferior: */
303      int length = strlen (line);
304      if (length > ECHO_SUPPRESS_MAX-2)
305	{
306	  echo_suppress_start = 0;
307	  echo_suppress_limit = 0;
308	}
309      else
310	{
311	  if (echo_suppress_limit + length > ECHO_SUPPRESS_MAX - 2)
312	    {
313	      if (echo_suppress_limit - echo_suppress_start + length
314		  <= ECHO_SUPPRESS_MAX - 2)
315		{
316		  memmove (echo_suppress_buffer,
317			   echo_suppress_buffer + echo_suppress_start,
318			   echo_suppress_limit - echo_suppress_start);
319		  echo_suppress_limit -= echo_suppress_start;
320		  echo_suppress_start = 0;
321		}
322	      else
323		{
324		  echo_suppress_limit = 0;
325		}
326	      echo_suppress_start = 0;
327	    }
328	  memcpy (echo_suppress_buffer + echo_suppress_limit,
329		  line, length);
330	  echo_suppress_limit += length;
331	  echo_suppress_buffer[echo_suppress_limit++] = '\r';
332	  echo_suppress_buffer[echo_suppress_limit++] = '\n';
333	}
334      write (out_to_inferior_fd, line, length);
335      if (pending_special_char == 0)
336        {
337          write (out_to_inferior_fd, enter, sizeof(enter)-1);
338          if (*line)
339            add_history (line);
340        }
341      free (line);
342    }
343  rl_callback_handler_remove ();
344  buf_count = 0;
345  num_keys = 0;
346  if (pending_special_char != 0)
347    {
348      write (out_to_inferior_fd, &pending_special_char, 1);
349      pending_special_char = 0;
350    }
351}
352
353/* Value of rl_getc_function.
354   Use this because readline should read from stdin, not rl_instream,
355   points to the pty (so readline has monitor its terminal modes). */
356
357int
358my_rl_getc (FILE *dummy)
359{
360  int ch = rl_getc (stdin);
361  if (is_special_char (ch))
362    {
363      pending_special_char = ch;
364      return '\r';
365    }
366  return ch;
367}
368
369int
370main(int argc, char** argv)
371{
372  char *path;
373  int i;
374  int master;
375  char *name;
376  int in_from_tty_fd;
377  struct sigaction act;
378  struct winsize ws;
379  struct termios t;
380  int maxfd;
381  fd_set in_set;
382  static char empty_string[1] = "";
383  char *prompt = empty_string;
384  int ioctl_err = 0;
385  int arg_base = 1;
386
387#ifdef DEBUG
388  logfile = fopen("/tmp/rlfe.log", "w");
389#endif
390
391  while (arg_base<argc)
392    {
393      if (argv[arg_base][0] != '-')
394	break;
395      if (arg_base+1 >= argc )
396	usage_exit();
397      switch(argv[arg_base][1])
398	{
399	case 'h':
400	  arg_base++;
401	  hist_file = argv[arg_base];
402	  break;
403	case 's':
404	  arg_base++;
405	  hist_size = atoi(argv[arg_base]);
406	  if (hist_size<0)
407	    usage_exit();
408	  break;
409	default:
410	  usage_exit();
411	}
412      arg_base++;
413    }
414  if (hist_file)
415    read_history (hist_file);
416
417  set_edit_mode ();
418
419  rl_readline_name = APPLICATION_NAME;
420
421  if ((master = OpenPTY (&name)) < 0)
422    {
423      perror("ptypair: could not open master pty");
424      exit(1);
425    }
426
427  DPRINT1("pty name: '%s'\n", name);
428
429  /* set up SIGWINCH handler */
430  act.sa_handler = sigwinch_handler;
431  sigemptyset(&(act.sa_mask));
432  act.sa_flags = 0;
433  if (sigaction(SIGWINCH, &act, NULL) < 0)
434    {
435      perror("ptypair: could not handle SIGWINCH ");
436      exit(1);
437    }
438
439  if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) < 0)
440    {
441      perror("ptypair: could not get window size");
442      exit(1);
443    }
444
445  if ((child = fork()) < 0)
446    {
447      perror("cannot fork");
448      exit(1);
449    }
450
451  if (child == 0)
452    {
453      int slave;  /* file descriptor for slave pty */
454
455      /* We are in the child process */
456      close(master);
457
458#ifdef TIOCSCTTY
459      if ((slave = get_slave_pty(name)) < 0)
460	{
461	  perror("ptypair: could not open slave pty");
462	  exit(1);
463	}
464#endif
465
466      /* We need to make this process a session group leader, because
467       * it is on a new PTY, and things like job control simply will
468       * not work correctly unless there is a session group leader
469       * and process group leader (which a session group leader
470       * automatically is). This also disassociates us from our old
471       * controlling tty.
472       */
473      if (setsid() < 0)
474	{
475	  perror("could not set session leader");
476	}
477
478      /* Tie us to our new controlling tty. */
479#ifdef TIOCSCTTY
480      if (ioctl(slave, TIOCSCTTY, NULL))
481	{
482	  perror("could not set new controlling tty");
483	}
484#else
485      if ((slave = get_slave_pty(name)) < 0)
486	{
487	  perror("ptypair: could not open slave pty");
488	  exit(1);
489	}
490#endif
491
492      /* make slave pty be standard in, out, and error */
493      dup2(slave, STDIN_FILENO);
494      dup2(slave, STDOUT_FILENO);
495      dup2(slave, STDERR_FILENO);
496
497      /* at this point the slave pty should be standard input */
498      if (slave > 2)
499	{
500	  close(slave);
501	}
502
503      /* Try to restore window size; failure isn't critical */
504      if (ioctl(STDOUT_FILENO, TIOCSWINSZ, &ws) < 0)
505	{
506	  perror("could not restore window size");
507	}
508
509      /* now start the shell */
510      {
511	static char* command_args[] = { COMMAND_ARGS, NULL };
512	static char* alt_command_args[] = { ALT_COMMAND_ARGS, NULL };
513	if (argc <= 1)
514	  {
515	    execvp (COMMAND, command_args);
516	    execvp (ALT_COMMAND, alt_command_args);
517	  }
518	else
519	  execvp (argv[arg_base], &argv[arg_base]);
520      }
521
522      /* should never be reached */
523      exit(1);
524    }
525
526  /* parent */
527  signal (SIGCHLD, sig_child);
528
529  /* Note that we only set termios settings for standard input;
530   * the master side of a pty is NOT a tty.
531   */
532  tcgetattr(STDIN_FILENO, &orig_term);
533
534  t = orig_term;
535  eof_char = t.c_cc[VEOF];
536  /*  add_special_char(t.c_cc[VEOF]);*/
537  add_special_char(t.c_cc[VINTR]);
538  add_special_char(t.c_cc[VQUIT]);
539  add_special_char(t.c_cc[VSUSP]);
540#if defined (VDISCARD)
541  add_special_char(t.c_cc[VDISCARD]);
542#endif
543
544  t.c_lflag &= ~(ICANON | ISIG | ECHO | ECHOCTL | ECHOE | \
545		 ECHOK | ECHOKE | ECHONL | ECHOPRT );
546  t.c_iflag &= ~ICRNL;
547  t.c_iflag |= IGNBRK;
548  t.c_cc[VMIN] = 1;
549  t.c_cc[VTIME] = 0;
550  tcsetattr(STDIN_FILENO, TCSANOW, &t);
551  in_from_inferior_fd = master;
552  out_to_inferior_fd = master;
553  rl_instream = fdopen (master, "r");
554  rl_getc_function = my_rl_getc;
555
556  rl_prep_term_function = null_prep_terminal;
557  rl_deprep_term_function = null_deprep_terminal;
558  rl_pre_input_hook = pre_input_change_mode;
559  rl_callback_handler_install (prompt, line_handler);
560
561  in_from_tty_fd = STDIN_FILENO;
562  FD_ZERO (&in_set);
563  maxfd = in_from_inferior_fd > in_from_tty_fd ? in_from_inferior_fd
564    : in_from_tty_fd;
565  for (;;)
566    {
567      int num;
568      FD_SET (in_from_inferior_fd, &in_set);
569      FD_SET (in_from_tty_fd, &in_set);
570
571      num = select(maxfd+1, &in_set, NULL, NULL, NULL);
572
573      if (propagate_sigwinch)
574	{
575	  struct winsize ws;
576	  if (ioctl (STDIN_FILENO, TIOCGWINSZ, &ws) >= 0)
577	    {
578	      ioctl (master, TIOCSWINSZ, &ws);
579	    }
580	  propagate_sigwinch = 0;
581	  continue;
582	}
583
584      if (num <= 0)
585	{
586	  perror ("select");
587	  exit (-1);
588	}
589      if (FD_ISSET (in_from_tty_fd, &in_set))
590	{
591	  extern int readline_echoing_p;
592	  struct termios term_master;
593	  int do_canon = 1;
594	  int do_icrnl = 1;
595	  int ioctl_ret;
596
597	  DPRINT1("[tty avail num_keys:%d]\n", num_keys);
598
599	  /* If we can't get tty modes for the master side of the pty, we
600	     can't handle non-canonical-mode programs.  Always assume the
601	     master is in canonical echo mode if we can't tell. */
602	  ioctl_ret = tcgetattr(master, &term_master);
603
604	  if (ioctl_ret >= 0)
605	    {
606	      do_canon = (term_master.c_lflag & ICANON) != 0;
607	      do_icrnl = (term_master.c_lflag & ICRNL) != 0;
608	      readline_echoing_p = (term_master.c_lflag & ECHO) != 0;
609	      DPRINT1 ("echo,canon,crnl:%03d\n",
610		       100 * readline_echoing_p
611		       + 10 * do_canon
612		       + 1 * do_icrnl);
613	    }
614	  else
615	    {
616	      if (ioctl_err == 0)
617		DPRINT1("tcgetattr on master fd failed: errno = %d\n", errno);
618	      ioctl_err = 1;
619	    }
620
621	  if (do_canon == 0 && num_keys == 0)
622	    {
623	      char ch[10];
624	      int count = read (STDIN_FILENO, ch, sizeof(ch));
625	      DPRINT1("[read %d chars from stdin: ", count);
626	      DPRINT2(" \"%.*s\"]\n", count, ch);
627	      if (do_icrnl)
628		{
629		  int i = count;
630		  while (--i >= 0)
631		    {
632		      if (ch[i] == '\r')
633			ch[i] = '\n';
634		    }
635		}
636	      maybe_emphasize_input (1);
637	      write (out_to_inferior_fd, ch, count);
638	    }
639	  else
640	    {
641	      if (num_keys == 0)
642		{
643		  int i;
644		  /* Re-install callback handler for new prompt. */
645		  if (prompt != empty_string)
646		    free (prompt);
647		  if (prompt == NULL)
648		    {
649		      DPRINT0("New empty prompt\n");
650		      prompt = empty_string;
651		    }
652		  else
653		    {
654		      if (do_emphasize_input && buf_count > 0)
655			{
656			  prompt = malloc (buf_count + strlen (end_input_mode)
657					   + strlen (start_input_mode) + 5);
658			  sprintf (prompt, "\001%s\002%.*s\001%s\002",
659				   end_input_mode,
660				   buf_count, buf,
661				   start_input_mode);
662			}
663		      else
664			{
665			  prompt = malloc (buf_count + 1);
666			  memcpy (prompt, buf, buf_count);
667			  prompt[buf_count] = '\0';
668			}
669		      DPRINT1("New prompt '%s'\n", prompt);
670#if 0 /* ifdef HAVE_RL_ALREADY_PROMPTED */
671		      /* Doesn't quite work when do_emphasize_input is 1. */
672		      rl_already_prompted = buf_count > 0;
673#else
674		      if (buf_count > 0)
675			write (1, "\r", 1);
676#endif
677		    }
678
679		  rl_callback_handler_install (prompt, line_handler);
680		}
681	      num_keys++;
682	      maybe_emphasize_input (1);
683	      rl_callback_read_char ();
684	    }
685	}
686      else /* output from inferior. */
687	{
688	  int i;
689	  int count;
690	  int old_count;
691	  if (buf_count > (sizeof(buf) >> 2))
692	    buf_count = 0;
693	  count = read (in_from_inferior_fd, buf+buf_count,
694			sizeof(buf) - buf_count);
695          DPRINT2("read %d from inferior, buf_count=%d", count, buf_count);
696	  DPRINT2(": \"%.*s\"", count, buf+buf_count);
697	  maybe_emphasize_input (0);
698	  if (count <= 0)
699	    {
700	      DPRINT0 ("(Connection closed by foreign host.)\n");
701	      tcsetattr(STDIN_FILENO, TCSANOW, &orig_term);
702	      exit (0);
703	    }
704	  old_count = buf_count;
705
706          /* Look for any pending echo that we need to suppress. */
707	  while (echo_suppress_start < echo_suppress_limit
708		 && count > 0
709		 && buf[buf_count] == echo_suppress_buffer[echo_suppress_start])
710	    {
711	      count--;
712	      buf_count++;
713	      echo_suppress_start++;
714	    }
715	  DPRINT1("suppressed %d characters of echo.\n", buf_count-old_count);
716
717          /* Write to the terminal anything that was not suppressed. */
718          if (count > 0)
719            write (1, buf + buf_count, count);
720
721          /* Finally, look for a prompt candidate.
722           * When we get around to going input (from the keyboard),
723           * we will consider the prompt to be anything since the last
724           * line terminator.  So we need to save that text in the
725           * initial part of buf.  However, anything before the
726           * most recent end-of-line is not interesting. */
727	  buf_count += count;
728#if 1
729	  for (i = buf_count;  --i >= old_count; )
730#else
731	  for (i = buf_count - 1;  i-- >= buf_count - count; )
732#endif
733	    {
734	      if (buf[i] == '\n' || buf[i] == '\r')
735		{
736		  i++;
737		  memmove (buf, buf+i, buf_count - i);
738		  buf_count -= i;
739		  break;
740		}
741	    }
742	  DPRINT2("-> i: %d, buf_count: %d\n", i, buf_count);
743	}
744    }
745}
746
747static void set_edit_mode ()
748{
749  int vi = 0;
750  char *shellopts;
751
752  shellopts = getenv ("SHELLOPTS");
753  while (shellopts != 0)
754    {
755      if (strncmp ("vi", shellopts, 2) == 0)
756	{
757	  vi = 1;
758	  break;
759	}
760      shellopts = index (shellopts + 1, ':');
761    }
762
763  if (!vi)
764    {
765      if (getenv ("EDITOR") != 0)
766	vi |= strcmp (getenv ("EDITOR"), "vi") == 0;
767    }
768
769  if (vi)
770    rl_variable_bind ("editing-mode", "vi");
771  else
772    rl_variable_bind ("editing-mode", "emacs");
773}
774
775
776static void usage_exit ()
777{
778  fprintf (stderr, "Usage: rlfe [-h histfile] [-s size] cmd [arg1] [arg2] ...\n\n");
779  exit (1);
780}
781