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