Debug/Telnet CLI enhancements 69/869/3
authorChris Luke <chrisy@flirble.org>
Mon, 25 Apr 2016 17:48:54 +0000 (13:48 -0400)
committerDave Barach <openvpp@barachs.net>
Wed, 27 Apr 2016 13:32:48 +0000 (13:32 +0000)
A fairly comprehensive re-work of the built-in debug and telnet CLI
to add various command line editing features and to add command history
to the debug CLI.

This may seem like a large change but a good amount of it is merely
reworking the existing CLI code (which changed its indent level).

The features this patch enables include:

- Enable history in the debug CLI.
- Put both stdin and telnet connections in char-by-char mode.
- Echo from the server, not the client, for more control.
- Add a mostly no-op but fairly complete Telnet protocol processor.
- Perform control code parsing on the input byte stream to match strings
  of both control codes and ANSI/VT100 escape sequences.
- Up/down keys scroll through the history (like ^P/^N).
- Do CRLF output cooking (\n -> \r\n) for connections that need it.
- Left/right cursor movements, insert/erase at cursor.
- Home/end cursor jumps.
- Jump left/right word at a time (Ctrl-left/right).
- Negotiate the terminal type from Telnet clients. (well, the code doesn’t
  really negotiate, it demands it, but the client is led to believe it
  was a negotiation)
- Read terminal type from TERM variable for the local debug CLI.
- Delete from cursor to end of line (^K). Delete char-right (^D/Del).
- Clear screen (^L) and repaint prompt/current command (on non-ANSI
  terminals it just newlines and repaints the line).

Change-Id: Id274b56ccfd4cc8c19ddc0f478890f21f284262a
Signed-off-by: Chris Luke <chrisy@flirble.org>
vlib/vlib/unix/cli.c
vlib/vlib/unix/unix.h

index 2e6e059..809be73 100644 (file)
 
 #include <vlib/vlib.h>
 #include <vlib/unix/unix.h>
+#include <vppinfra/timer.h>
 
+#include <ctype.h>
 #include <fcntl.h>
 #include <sys/stat.h>
 #include <termios.h>
 #include <unistd.h>
 #include <arpa/telnet.h>
 
+/* ANSI Escape code. */
+#define ESC "\x1b"
+
+/* ANSI Control Sequence Introducer. */
+#define CSI ESC "["
+
+/* ANSI sequences. */
+#define ANSI_CLEAR  CSI "2J" CSI "1;1H"
+
+/** Maximum depth into a byte stream from which to compile a Telnet
+ * protocol message. This is a saftey measure. */
+#define UNIX_CLI_MAX_DEPTH_TELNET 16
+
+/** Default CLI history depth if not configured in startup.conf */
+#define UNIX_CLI_DEFAULT_HISTORY 50
+
+/** Unix standard in */
+#define UNIX_CLI_STDIN_FD 0
+
+
+/** Unix CLI session. */
 typedef struct {
   u32 unix_file_index;
 
@@ -64,6 +87,18 @@ typedef struct {
   u8 * search_key;
   int search_mode;
 
+  /* Position of the insert cursor on the current input line */
+  u32 cursor;
+
+  /* Set if the CRLF mode wants CR + LF */
+  u8 crlf_mode;
+
+  /* Can we do ANSI output? */
+  u8 ansi_capable;
+
+  /* Has the session started? */
+  u8 started;
+
   u32 process_node_index;
 } unix_cli_file_t;
 
@@ -74,6 +109,118 @@ unix_cli_file_free (unix_cli_file_t * f)
   vec_free (f->input_vector);
 }
 
+/* CLI actions */
+typedef enum {
+  UNIX_CLI_PARSE_ACTION_NOACTION = 0,
+  UNIX_CLI_PARSE_ACTION_CRLF,
+  UNIX_CLI_PARSE_ACTION_TAB,
+  UNIX_CLI_PARSE_ACTION_ERASE,
+  UNIX_CLI_PARSE_ACTION_ERASERIGHT,
+  UNIX_CLI_PARSE_ACTION_UP,
+  UNIX_CLI_PARSE_ACTION_DOWN,
+  UNIX_CLI_PARSE_ACTION_LEFT,
+  UNIX_CLI_PARSE_ACTION_RIGHT,
+  UNIX_CLI_PARSE_ACTION_HOME,
+  UNIX_CLI_PARSE_ACTION_END,
+  UNIX_CLI_PARSE_ACTION_WORDLEFT,
+  UNIX_CLI_PARSE_ACTION_WORDRIGHT,
+  UNIX_CLI_PARSE_ACTION_ERASELINELEFT,
+  UNIX_CLI_PARSE_ACTION_ERASELINERIGHT,
+  UNIX_CLI_PARSE_ACTION_CLEAR,
+  UNIX_CLI_PARSE_ACTION_REVSEARCH,
+  UNIX_CLI_PARSE_ACTION_FWDSEARCH,
+  UNIX_CLI_PARSE_ACTION_HISTORY,
+  UNIX_CLI_PARSE_ACTION_YANK,
+  UNIX_CLI_PARSE_ACTION_TELNETIAC,
+
+  UNIX_CLI_PARSE_ACTION_PARTIALMATCH,
+  UNIX_CLI_PARSE_ACTION_NOMATCH
+} unix_cli_parse_action_t;
+
+/** \brief Mapping of input buffer strings to action values.
+ * @note This won't work as a hash since we need to be able to do
+ *       partial matches on the string.
+ */
+typedef struct {
+  u8 *input;                        /**< Input string to match. */
+  u32 len;                          /**< Length of input without final NUL. */
+  unix_cli_parse_action_t action;   /**< Action to take when matched. */
+} unix_cli_parse_actions_t;
+
+/** \brief Given a capital ASCII letter character return a NUL terminated
+ * string with the control code for that letter.
+ * \example CTL('A') returns { 0x01, 0x00 } as a u8[].
+ */
+#define CTL(c) (u8[]){ (c) - '@', 0 }
+
+#define _(a,b) { .input = (u8 *)(a), .len = sizeof(a) - 1, .action = (b) }
+static unix_cli_parse_actions_t unix_cli_parse_strings[] = {
+ /* Line handling */
+ _( "\r\n",   UNIX_CLI_PARSE_ACTION_CRLF ),       /* Must be before '\r' */
+ _( "\n",     UNIX_CLI_PARSE_ACTION_CRLF ),
+ _( "\r\0",   UNIX_CLI_PARSE_ACTION_CRLF ),       /* Telnet does this */
+ _( "\r",     UNIX_CLI_PARSE_ACTION_CRLF ),
+
+ /* Unix shell control codes */
+ _( CTL('B'), UNIX_CLI_PARSE_ACTION_LEFT ),
+ _( CTL('F'), UNIX_CLI_PARSE_ACTION_RIGHT ),
+ _( CTL('P'), UNIX_CLI_PARSE_ACTION_UP ),
+ _( CTL('N'), UNIX_CLI_PARSE_ACTION_DOWN ),
+ _( CTL('A'), UNIX_CLI_PARSE_ACTION_HOME ),
+ _( CTL('E'), UNIX_CLI_PARSE_ACTION_END ),
+ _( CTL('D'), UNIX_CLI_PARSE_ACTION_ERASERIGHT ),
+ _( CTL('U'), UNIX_CLI_PARSE_ACTION_ERASELINELEFT ),
+ _( CTL('K'), UNIX_CLI_PARSE_ACTION_ERASELINERIGHT ),
+ _( CTL('Y'), UNIX_CLI_PARSE_ACTION_YANK ),
+ _( CTL('L'), UNIX_CLI_PARSE_ACTION_CLEAR ),
+ _( ESC "b",  UNIX_CLI_PARSE_ACTION_WORDLEFT ),   /* Alt-B */
+ _( ESC "f",  UNIX_CLI_PARSE_ACTION_WORDRIGHT ),  /* Alt-F */
+ _( "\b",     UNIX_CLI_PARSE_ACTION_ERASE ),      /* ^H */
+ _( "\x7f",   UNIX_CLI_PARSE_ACTION_ERASE ),      /* Backspace */
+ _( "\t",     UNIX_CLI_PARSE_ACTION_TAB ),        /* ^I */
+
+ /* VT100 Normal mode - Broadest support */
+ _( CSI "A",  UNIX_CLI_PARSE_ACTION_UP ),
+ _( CSI "B",  UNIX_CLI_PARSE_ACTION_DOWN ),
+ _( CSI "C",  UNIX_CLI_PARSE_ACTION_RIGHT ),
+ _( CSI "D",  UNIX_CLI_PARSE_ACTION_LEFT ),
+ _( CSI "H",  UNIX_CLI_PARSE_ACTION_HOME ),
+ _( CSI "F",  UNIX_CLI_PARSE_ACTION_END ),
+ _( CSI "3~", UNIX_CLI_PARSE_ACTION_ERASERIGHT ), /* Delete */
+ _( CSI "1;5D", UNIX_CLI_PARSE_ACTION_WORDLEFT ), /* C-Left */
+ _( CSI "1;5C", UNIX_CLI_PARSE_ACTION_WORDRIGHT ),/* C-Right */
+
+  /* VT100 Application mode - Some Gnome Terminal functions use these */
+ _( ESC "OA", UNIX_CLI_PARSE_ACTION_UP ),
+ _( ESC "OB", UNIX_CLI_PARSE_ACTION_DOWN ),
+ _( ESC "OC", UNIX_CLI_PARSE_ACTION_RIGHT ),
+ _( ESC "OD", UNIX_CLI_PARSE_ACTION_LEFT ),
+ _( ESC "OH", UNIX_CLI_PARSE_ACTION_HOME ),
+ _( ESC "OF", UNIX_CLI_PARSE_ACTION_END ),
+
+ /* ANSI X3.41-1974 - sent by Microsoft Telnet and PuTTY */
+ _( CSI "1~", UNIX_CLI_PARSE_ACTION_HOME ),
+ _( CSI "4~", UNIX_CLI_PARSE_ACTION_END ),
+
+ /* Emacs-ish history search */
+ _( CTL('S'), UNIX_CLI_PARSE_ACTION_FWDSEARCH ),
+ _( CTL('R'), UNIX_CLI_PARSE_ACTION_REVSEARCH ),
+
+ /* TODO: replace with 'history' command? */
+ _( "?",      UNIX_CLI_PARSE_ACTION_HISTORY ),
+
+ /* Other protocol things */
+ _( "\xff",   UNIX_CLI_PARSE_ACTION_TELNETIAC ),  /* IAC */
+ _( "\0",     UNIX_CLI_PARSE_ACTION_NOACTION ),   /* NUL */
+ _( NULL,     UNIX_CLI_PARSE_ACTION_NOMATCH )
+};
+#undef _
+
+typedef enum {
+  UNIX_CLI_PROCESS_EVENT_READ_READY,
+  UNIX_CLI_PROCESS_EVENT_QUIT,
+} unix_cli_process_event_type_t;
+
 typedef struct {
   /* Prompt string for CLI. */
   u8 * cli_prompt;
@@ -88,6 +235,64 @@ typedef struct {
 
 static unix_cli_main_t unix_cli_main;
 
+/**
+ * \brief Search for a byte sequence in the action list.
+ *
+ * Searches unix_cli_parse_actions[] for a match with the bytes in \c input
+ * of maximum length \c ilen . When a match is made \c *matched indicates how
+ * many bytes were matched. Returns a value from the enum
+ * \c unix_cli_parse_action_t to indicate whether no match was found, a
+ * partial match was found or a complete match was found and what action,
+ * if any, should be taken.
+ *
+ * @param input   String fragment to search for.
+ * @param ilen    Length of the string in 'input'.
+ * @param matched Pointer to an integer that will contain the number of
+ *                bytes matched when a complete match is found.
+ *
+ * @return Action from \v unix_cli_parse_action_t that the string fragment
+ *         matches.
+ *         \c UNIX_CLI_PARSE_ACTION_PARTIALMATCH is returned when the whole
+ *         input string matches the start of at least one action.
+ *         \c UNIX_CLI_PARSE_ACTION_NOMATCH is returned when there is no
+ *         match at all.
+ */
+static unix_cli_parse_action_t
+unix_cli_match_action(u8 *input, u32 ilen, i32 *matched)
+{
+  unix_cli_parse_actions_t *a = unix_cli_parse_strings;
+  u8 partial = 0;
+
+  while (a->input)
+    {
+        if (ilen >= a->len)
+          {
+            /* see if the start of the input buffer exactly matches the current
+             * action string. */
+            if (memcmp(input, a->input, a->len) == 0)
+              {
+                *matched = a->len;
+                return a->action;
+              }
+          }
+        else
+          {
+            /* if the first ilen characters match, flag this as a partial -
+             * meaning keep collecting bytes in case of a future match */
+            if (memcmp(input, a->input, ilen) == 0)
+                partial = 1;
+          }
+
+        /* check next action */
+        a ++;
+    }
+
+  return partial ?
+        UNIX_CLI_PARSE_ACTION_PARTIALMATCH :
+        UNIX_CLI_PARSE_ACTION_NOMATCH;
+}
+
+
 static void
 unix_cli_add_pending_output (unix_file_t * uf,
                             unix_cli_file_t * cf,
@@ -123,7 +328,113 @@ unix_cli_del_pending_output (unix_file_t * uf,
     }
 }
 
-/* VLIB cli output function. */
+/** \brief A bit like strchr with a buffer length limit.
+ * Search a buffer for the first instance of a character up to the limit of
+ * the buffer length. If found then return the position of that character.
+ *
+ * The key departure from strchr is that if the character is not found then
+ * return the buffer length.
+ *
+ * @param chr The byte value to search for.
+ * @param str The buffer in which to search for the value.
+ * @param len The depth into the buffer to search.
+ *
+ * @return The index of the first occurence of \c chr. If \c chr is not
+ *          found then \c len instead.
+ */
+always_inline word unix_vlib_findchr(u8 chr, u8 *str, word len)
+{
+    word i = 0;
+    for (i = 0; i < len; i++, str++)
+      {
+        if (*str == chr)
+          return i;
+      }
+    return len;
+}
+
+/** \brief Send a buffer to the CLI stream if possible, enqueue it otherwise.
+ * Attempts to write given buffer to the file descriptor of the given
+ * Unix CLI session. If that session already has data in the output buffer
+ * or if the write attempt tells us to try again later then the given buffer
+ * is appended to the pending output buffer instead.
+ *
+ * This is typically called only from \c unix_vlib_cli_output_cooked since
+ * that is where CRLF handling occurs or from places where we explicitly do
+ * not want cooked handling.
+ *
+ * @param cf Unix CLI session of the desired stream to write to.
+ * @param uf The Unix file structure of the desired stream to write to.
+ * @param buffer Pointer to the buffer that needs to be written.
+ * @param buffer_bytes The number of bytes from \c buffer to write.
+ */
+static void unix_vlib_cli_output_raw(unix_cli_file_t * cf,
+          unix_file_t * uf,
+          u8 * buffer,
+          uword buffer_bytes)
+{
+  int n = 0;
+
+  if (vec_len (cf->output_vector) == 0)
+      n = write (uf->file_descriptor, buffer, buffer_bytes);
+
+  if (n < 0 && errno != EAGAIN)
+    {
+      clib_unix_warning ("write");
+    }
+  else if ((word) n < (word) buffer_bytes)
+    {
+      /* We got EAGAIN or we already have stuff in the buffer;
+       * queue up whatever didn't get sent for later. */
+      if (n < 0) n = 0;
+      unix_cli_add_pending_output (uf, cf, buffer + n, buffer_bytes - n);
+    }
+}
+
+/** \brief Process a buffer for CRLF handling before outputting it to the CLI.
+ *
+ * @param cf Unix CLI session of the desired stream to write to.
+ * @param uf The Unix file structure of the desired stream to write to.
+ * @param buffer Pointer to the buffer that needs to be written.
+ * @param buffer_bytes The number of bytes from \c buffer to write.
+ */
+static void unix_vlib_cli_output_cooked(unix_cli_file_t * cf,
+          unix_file_t * uf,
+          u8 * buffer,
+          uword buffer_bytes)
+{
+  word end = 0, start = 0;
+
+  while (end < buffer_bytes)
+    {
+      if (cf->crlf_mode)
+        {
+          /* iterate the line on \n's so we can insert a \r before it */
+          end = unix_vlib_findchr('\n',
+                                  buffer + start,
+                                  buffer_bytes - start) + start;
+        }
+      else
+        {
+          /* otherwise just send the whole buffer */
+          end = buffer_bytes;
+        }
+
+      unix_vlib_cli_output_raw(cf, uf, buffer + start, end - start);
+
+      if (cf->crlf_mode)
+        {
+          if (end < buffer_bytes)
+            {
+              unix_vlib_cli_output_raw(cf, uf, (u8 *)"\r\n", 2);
+              end ++; /* skip the \n that we already sent */
+            }
+          start = end;
+        }
+    }
+}
+
+/** \brief VLIB CLI output function. */
 static void unix_vlib_cli_output (uword cli_file_index,
                                  u8 * buffer,
                                  uword buffer_bytes)
@@ -132,245 +443,721 @@ static void unix_vlib_cli_output (uword cli_file_index,
   unix_cli_main_t * cm = &unix_cli_main;
   unix_cli_file_t * cf;
   unix_file_t * uf;
-  int n;
+
   cf = pool_elt_at_index (cm->cli_file_pool, cli_file_index);
   uf = pool_elt_at_index (um->file_pool, cf->unix_file_index);
-  n = 0;
-  if (vec_len (cf->output_vector) == 0)
-    n = write (uf->file_descriptor, buffer, buffer_bytes);
-  if (n < 0 && errno != EAGAIN)
-    clib_unix_warning ("write");
 
-  else if ((word) n < (word) buffer_bytes)
-    {
-      if (n < 0) n = 0;
-      unix_cli_add_pending_output (uf, cf, buffer + n, buffer_bytes - n);
-    }
+  unix_vlib_cli_output_cooked(cf, uf, buffer, buffer_bytes);
 }
 
-static int unix_cli_line_edit (unix_main_t * um, unix_cli_file_t * cf)
+/** \brief Identify whether a terminal type is ANSI capable. */
+static u8 unix_cli_terminal_type(u8 * term, uword len)
 {
+  /* This may later be better done as a hash of some sort. */
+#define _(a) do { \
+    if (strncasecmp(a, (char *)term, (size_t)len) == 0) return 1; \
+  } while(0)
+
+  _("xterm");
+  _("xterm-color");
+  _("xterm-256color"); /* iTerm on Mac */
+  _("screen");
+  _("ansi"); /* Microsoft Telnet */
+#undef _
+
+  return 0;
+}
+
+/** \brief Emit initial prompt on a connection. */
+static void unix_cli_file_welcome(unix_cli_main_t * cm, unix_cli_file_t * cf)
+{
+  unix_main_t * um = &unix_main;
   unix_file_t * uf = pool_elt_at_index (um->file_pool, cf->unix_file_index);
-  u8 * prev;
-  int i, j, delta;
 
-  for (i = 0; i < vec_len (cf->input_vector); i++)
+  /*
+   * Put the first bytes directly into the buffer so that further output is
+   * queued until everything is ready. (oterwise initial prompt can appear
+   * mid way through VPP initialization)
+   */
+  unix_cli_add_pending_output (uf, cf,
+             cm->cli_prompt,
+             vec_len (cm->cli_prompt));
+
+  cf->started = 1;
+}
+
+/** \brief A failsafe triggered on a timer to ensure we send the prompt
+ * to telnet sessions that fail to negotiate the terminal type. */
+static void unix_cli_file_welcome_timer(any arg, f64 delay)
+{
+  unix_cli_main_t * cm = &unix_cli_main;
+  unix_cli_file_t * cf;
+  (void)delay;
+
+  /* Check the connection didn't close already */
+  if (pool_is_free_index (cm->cli_file_pool, (uword)arg))
+    return;
+
+  cf = pool_elt_at_index (cm->cli_file_pool, (uword)arg);
+
+  if (!cf->started)
+    unix_cli_file_welcome(cm, cf);
+}
+
+/** \brief A mostly no-op Telnet state machine.
+ * Process Telnet command bytes in a way that ensures we're mostly
+ * transparent to the Telnet protocol. That is, it's mostly a no-op.
+ *
+ * @return -1 if we need more bytes, otherwise a positive integer number of
+ *          bytes to consume from the input_vector, not including the initial
+ *          IAC byte.
+ */
+static i32 unix_cli_process_telnet(unix_main_t * um,
+        unix_cli_file_t * cf,
+        unix_file_t * uf,
+        u8 * input_vector,
+        uword len)
+{
+  /* Input_vector starts at IAC byte.
+   * See if we have a complete message; if not, return -1 so we wait for more.
+   * if we have a complete message, consume those bytes from the vector.
+   */
+  i32 consume = 0;
+
+  if (len == 1)
+    return -1; /* want more bytes */
+
+  switch (input_vector[1])
     {
-      switch (cf->input_vector[i])
+      case IAC:
+        /* two IAC's in a row means to pass through 0xff.
+         * since that makes no sense here, just consume it.
+         */
+        consume = 1;
+        break;
+
+      case WILL:
+      case WONT:
+      case DO:
+      case DONT:
+        /* Expect 3 bytes */
+        if (vec_len(input_vector) < 3)
+          return -1; /* want more bytes */
+
+        consume = 2;
+        break;
+
+      case SB:
         {
-        case 0:
-          continue;
-            
-        case '?':
-          /* Erase the current command (if any) plus ?*/
-          for (j = 0; j < (vec_len (cf->current_command)+1); j++)
-            unix_cli_add_pending_output (uf, cf, (u8 *) "\b \b", 3);
-          
-          unix_cli_add_pending_output (uf, cf, (u8 *) "\r\nHistory:\r\n", 12);
-
-          for (j = 0; j < vec_len (cf->command_history); j++)
+          /* Sub option - search ahead for IAC SE to end it */
+          i32 i;
+          for (i = 3; i < len && i < UNIX_CLI_MAX_DEPTH_TELNET; i++)
             {
-              unix_cli_add_pending_output (uf, cf, cf->command_history[j],
-                                           vec_len(cf->command_history[j]));
-              unix_cli_add_pending_output (uf, cf, (u8 *) "\r\n", 2);
+              if (input_vector[i - 1] == IAC && input_vector[i] == SE)
+                {
+                  /* We have a complete message; see if we care about it */
+                  switch (input_vector[2])
+                    {
+                      case TELOPT_TTYPE:
+                        if (input_vector[3] != 0)
+                          break;
+                        /* See if the terminal type is ANSI capable */
+                        cf->ansi_capable =
+                            unix_cli_terminal_type(input_vector + 4, i - 5);
+                        /* If session not started, we can release the pause */
+                        if (!cf->started)
+                          /* Send the welcome banner and initial prompt */
+                          unix_cli_file_welcome(&unix_cli_main, cf);
+                        break;
+
+                      default:
+                        break;
+                    }
+                  /* Consume it all */
+                  consume = i;
+                  break;
+                }
             }
-          goto crlf;
 
-          /* ^R - reverse search */
-        case 'R' - '@':
-        case 'S' - '@':
-          if (cf->search_mode == 0)
-            {
-              /* Erase the current command (if any) plus ^R */
-              for (j = 0; j < (vec_len (cf->current_command)+2); j++)
-                  unix_cli_add_pending_output (uf, cf, (u8 *) "\b \b", 3);
-              
-              vec_reset_length (cf->search_key);
-              vec_reset_length (cf->current_command);
-              if (cf->input_vector[i] == 'R' - '@')
-                  cf->search_mode = -1;
-              else
-                  cf->search_mode = 1;
-            }
-          else
-            {
-              if (cf->input_vector[i] == 'R' - '@')
-                cf->search_mode = -1;
-              else
-                cf->search_mode = 1;
+          if (i == UNIX_CLI_MAX_DEPTH_TELNET)
+            consume = 1; /* hit max search depth, advance one byte */
+
+          if (consume == 0)
+            return -1; /* want more bytes */
 
-              cf->excursion += cf->search_mode;
-              unix_cli_add_pending_output (uf, cf, (u8 *) "\b \b", 3);
-              goto search_again;
-            }
           break;
+        }
 
-          /* ^U - line-kill */
-        case 'U'-'@':
-          /* Erase the command, plus ^U */
-          for (j = 0; j < (vec_len (cf->current_command)+2); j++)
-            unix_cli_add_pending_output (uf, cf, (u8 *) "\b \b", 3);
-          vec_reset_length (cf->current_command);
-          cf->search_mode = 0;
-          continue;
+      case GA:
+      case EL:
+      case EC:
+      case AO:
+      case IP:
+      case BREAK:
+      case DM:
+      case NOP:
+      case SE:
+      case EOR:
+      case ABORT:
+      case SUSP:
+      case xEOF:
+        /* Simple one-byte messages */
+        consume = 1;
+        break;
+
+      case AYT:
+        /* Are You There - trigger a visible response */
+        consume = 1;
+        unix_vlib_cli_output_cooked (cf, uf, (u8 *) "fd.io VPP\n", 10);
+        break;
+
+      default:
+        /* Unknown command! Eat the IAC byte */
+        break;
+    }
 
-          /* ^P - previous, ^N - next */
-        case 'P' - '@':
-        case 'N' - '@':
-          cf->search_mode = 0;
-          /* Erase the command, plus ^P */
-          for (j = 0; j < (vec_len (cf->current_command)+2); j++)
-            unix_cli_add_pending_output (uf, cf, (u8 *) "\b \b", 3);
+    return consume;
+}
+
+/** \brief Process actionable input.
+ * Based on the \c action process the input; this typically involves
+ * searching the command history or editing the current command line.
+ */
+static int unix_cli_line_process_one(unix_cli_main_t * cm,
+        unix_main_t * um,
+        unix_cli_file_t * cf,
+        unix_file_t * uf,
+        u8 input,
+        unix_cli_parse_action_t action)
+{
+  u8 * prev;
+  int j, delta;
+
+  switch (action)
+    {
+    case UNIX_CLI_PARSE_ACTION_NOACTION:
+      break;
+
+    case UNIX_CLI_PARSE_ACTION_HISTORY:
+      /* Erase the current command (if any)*/
+      for (j = cf->cursor; j < (vec_len (cf->current_command)); j++)
+        unix_vlib_cli_output_cooked (cf, uf, (u8 *) " ", 1);
+      for (j = 0; j < (vec_len (cf->current_command)); j++)
+        unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b \b", 3);
+
+      unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\nHistory:\n", 10);
+
+      for (j = 0; j < vec_len (cf->command_history); j++)
+        {
+          unix_vlib_cli_output_cooked (cf, uf, cf->command_history[j],
+                                       vec_len(cf->command_history[j]));
+          unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\n", 1);
+        }
+      goto crlf;
+
+    case UNIX_CLI_PARSE_ACTION_REVSEARCH:
+    case UNIX_CLI_PARSE_ACTION_FWDSEARCH:
+      if (cf->search_mode == 0)
+        {
+          /* Erase the current command (if any) */
+          for (j = 0; j < (vec_len (cf->current_command)); j++)
+              unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b \b", 3);
+
+          vec_reset_length (cf->search_key);
           vec_reset_length (cf->current_command);
-          if (vec_len (cf->command_history))
-            {
-              if (cf->input_vector[i] == 'P' - '@')
-                delta = -1;
-              else
-                delta = 1;
+          if (action == UNIX_CLI_PARSE_ACTION_REVSEARCH)
+              cf->search_mode = -1;
+          else
+              cf->search_mode = 1;
+          cf->cursor = 0;
+        }
+      else
+        {
+          if (action == UNIX_CLI_PARSE_ACTION_REVSEARCH)
+            cf->search_mode = -1;
+          else
+            cf->search_mode = 1;
 
-              cf->excursion += delta;
+          cf->excursion += cf->search_mode;
+          goto search_again;
+        }
+      break;
+
+    case UNIX_CLI_PARSE_ACTION_ERASELINELEFT:
+      /* Erase the command from the cursor to the start */
+
+      /* Shimmy forwards to the new end of line position */
+      delta = vec_len (cf->current_command) - cf->cursor;
+      for (j = cf->cursor; j > delta; j--)
+        unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b", 1);
+      /* Zap from here to the end of what is currently displayed */
+      for (; j < (vec_len (cf->current_command)); j++)
+        unix_vlib_cli_output_cooked (cf, uf, (u8 *) " ", 1);
+      /* Get back to the start of the line */
+      for (j = 0; j < (vec_len (cf->current_command)); j++)
+        unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b", 1);
+
+      j = vec_len(cf->current_command) - cf->cursor;
+      memmove(cf->current_command,
+              cf->current_command + cf->cursor,
+              j);
+      _vec_len(cf->current_command) = j;
+
+      /* Print the new contents */
+      unix_vlib_cli_output_cooked (cf, uf, cf->current_command, j);
+      /* Shimmy back to the start */
+      for (j = 0; j < (vec_len (cf->current_command)); j++)
+        unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b", 1);
+      cf->cursor = 0;
+
+      cf->search_mode = 0;
+      break;
+
+    case UNIX_CLI_PARSE_ACTION_ERASELINERIGHT:
+      /* Erase the command from the cursor to the end */
+
+      /* Zap from cursor to end of what is currently displayed */
+      for (j = cf->cursor; j < (vec_len (cf->current_command)); j++)
+        unix_vlib_cli_output_cooked (cf, uf, (u8 *) " ", 1);
+      /* Get back to where we were */
+      for (j = cf->cursor; j < (vec_len (cf->current_command)); j++)
+        unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b", 1);
+
+      /* Truncate the line at the cursor */
+      _vec_len(cf->current_command) = cf->cursor;
+
+      cf->search_mode = 0;
+      break;
+
+    case UNIX_CLI_PARSE_ACTION_LEFT:
+      if (cf->cursor > 0)
+        {
+          unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b", 1);
+          cf->cursor --;
+        }
 
-              if (cf->excursion > (i32) vec_len (cf->command_history) -1)
-                cf->excursion = 0;
-              else if (cf->excursion < 0)
-                cf->excursion = vec_len (cf->command_history) -1;
+      cf->search_mode = 0;
+      break;
+
+    case UNIX_CLI_PARSE_ACTION_RIGHT:
+      if (cf->cursor < vec_len(cf->current_command))
+        {
+          /* have to emit the character under the cursor */
+          unix_vlib_cli_output_cooked (cf, uf, cf->current_command + cf->cursor, 1);
+          cf->cursor ++;
+        }
 
-              prev = cf->command_history [cf->excursion];
-              vec_validate (cf->current_command, vec_len(prev)-1);
+      cf->search_mode = 0;
+      break;
+
+    case UNIX_CLI_PARSE_ACTION_UP:
+    case UNIX_CLI_PARSE_ACTION_DOWN:
+      cf->search_mode = 0;
+      /* Erase the command */
+      for (j = cf->cursor; j < (vec_len (cf->current_command)); j++)
+        unix_vlib_cli_output_cooked (cf, uf, (u8 *) " ", 1);
+      for (j = 0; j < (vec_len (cf->current_command)); j++)
+        unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b \b", 3);
+      vec_reset_length (cf->current_command);
+      if (vec_len (cf->command_history))
+        {
+          if (action == UNIX_CLI_PARSE_ACTION_UP)
+            delta = -1;
+          else
+            delta = 1;
+
+          cf->excursion += delta;
+
+          if (cf->excursion > (i32) vec_len (cf->command_history) -1)
+            cf->excursion = 0;
+          else if (cf->excursion < 0)
+            cf->excursion = vec_len (cf->command_history) -1;
+
+          prev = cf->command_history [cf->excursion];
+          vec_validate (cf->current_command, vec_len(prev)-1);
+
+          clib_memcpy (cf->current_command, prev, vec_len(prev));
+          _vec_len (cf->current_command) = vec_len(prev);
+          unix_vlib_cli_output_cooked (cf, uf, cf->current_command,
+                                       vec_len (cf->current_command));
+          cf->cursor = vec_len(cf->current_command);
 
-              clib_memcpy (cf->current_command, prev, vec_len(prev));
-              _vec_len (cf->current_command) = vec_len(prev);
-              unix_cli_add_pending_output (uf, cf, cf->current_command,
-                                           vec_len (cf->current_command));
-              break;
-            }
           break;
+        }
+      break;
 
-        case 0x7f:
-        case 'H' - '@':
-          for (j = 0; j < 2; j++)
-            unix_cli_add_pending_output (uf, cf, (u8 *) "\b \b", 3);
-          if (vec_len (cf->current_command))
+    case UNIX_CLI_PARSE_ACTION_HOME:
+      if (vec_len (cf->current_command) && cf->cursor > 0)
+        {
+          while (cf->cursor)
             {
-              unix_cli_add_pending_output (uf, cf, (u8 *) "\b \b", 3);
-              _vec_len (cf->current_command)--;
+              unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b", 1);
+              cf->cursor --;
             }
-          cf->search_mode = 0;
-          cf->excursion = 0;
-          cf->search_mode = 0;
-          vec_reset_length (cf->search_key);
-          break;
+        }
+
+      cf->search_mode = 0;
+      break;
 
-        case '\r':
-        case '\n':
-        crlf:
-          vec_add1 (cf->current_command, '\r');
-          vec_add1 (cf->current_command, '\n');
-          unix_cli_add_pending_output (uf, cf, (u8 *) "\b\b  \b\b\r\n", 8);
+    case UNIX_CLI_PARSE_ACTION_END:
+      if (vec_len (cf->current_command) &&
+              cf->cursor < vec_len(cf->current_command))
+        {
+          unix_vlib_cli_output_cooked (cf, uf,
+                cf->current_command + cf->cursor,
+                vec_len(cf->current_command) - cf->cursor);
+          cf->cursor = vec_len(cf->current_command);
+        }
 
-          vec_validate (cf->input_vector, vec_len(cf->current_command)-1);
-          clib_memcpy (cf->input_vector, cf->current_command, 
-                  vec_len(cf->current_command));
-          _vec_len(cf->input_vector) = _vec_len (cf->current_command);
+      cf->search_mode = 0;
+      break;
 
-          if (vec_len(cf->command_history) >= cf->history_limit)
+    case UNIX_CLI_PARSE_ACTION_WORDLEFT:
+      if (vec_len (cf->current_command) && cf->cursor > 0)
+        {
+          j = cf->cursor;
+
+          unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b", 1);
+          j --;
+
+          while (j && isspace(cf->current_command[j]))
             {
-              vec_free (cf->command_history[0]);
-              vec_delete (cf->command_history, 1, 0);
+              unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b", 1);
+              j --;
             }
-          /* Don't add blank lines to the cmd history */
-          if (vec_len (cf->current_command) > 2)
+          while (j && !isspace(cf->current_command[j]))
             {
-              _vec_len (cf->current_command) -= 2;
-              vec_add1 (cf->command_history, cf->current_command);
-              cf->current_command = 0;
+              if (isspace(cf->current_command[j - 1]))
+                break;
+              unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b", 1);
+              j --;
             }
-          else
-            vec_reset_length (cf->current_command);
-          cf->excursion = 0;
-          cf->search_mode = 0;
-          vec_reset_length (cf->search_key);
-          return 0;
 
-          /* telnet "mode character" blort, echo but don't process. */
-        case 0xff:
-            unix_cli_add_pending_output (uf, cf, cf->input_vector + i, 
-                                         6);
-            i += 6;
-            continue;
+          cf->cursor = j;
+        }
 
-        default:
-          if (cf->search_mode)
-            {
-              int j, k, limit, offset;
-              u8 * item;
+      cf->search_mode = 0;
+      break;
 
-              vec_add1 (cf->search_key, cf->input_vector[i]);
+    case UNIX_CLI_PARSE_ACTION_WORDRIGHT:
+      if (vec_len (cf->current_command) &&
+              cf->cursor < vec_len(cf->current_command))
+        {
+          int e = vec_len(cf->current_command);
+          j = cf->cursor;
+          while (j < e && !isspace(cf->current_command[j]))
+            j ++;
+          while (j < e && isspace(cf->current_command[j]))
+            j ++;
+          unix_vlib_cli_output_cooked (cf, uf,
+                cf->current_command + cf->cursor,
+                j - cf->cursor);
+          cf->cursor = j;
+        }
+
+      cf->search_mode = 0;
+      break;
+
+
+    case UNIX_CLI_PARSE_ACTION_ERASE:
+      if (vec_len (cf->current_command))
+        {
+          if (cf->cursor == vec_len(cf->current_command))
+            {
+              unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b \b", 3);
+              _vec_len (cf->current_command)--;
+              cf->cursor --;
+            }
+          else if (cf->cursor > 0)
+            {
+              /* shift everything at & to the right of the cursor left by 1 */
+              j = vec_len (cf->current_command) - cf->cursor;
+              memmove (cf->current_command + cf->cursor - 1,
+                cf->current_command + cf->cursor,
+                j);
+              _vec_len (cf->current_command)--;
+              cf->cursor --;
+              /* redraw the rest of the line */
+              unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b", 1);
+              unix_vlib_cli_output_cooked (cf, uf,
+                    cf->current_command + cf->cursor, j);
+              unix_vlib_cli_output_cooked (cf, uf, (u8 *) " \b\b", 3);
+              /* and shift the terminal cursor back where it should be */
+              while (-- j)
+                unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b", 1);
+            }
+        }
+      cf->search_mode = 0;
+      cf->excursion = 0;
+      vec_reset_length (cf->search_key);
+      break;
 
-            search_again:
-              for (j = 0; j < vec_len(cf->command_history); j++)
+    case UNIX_CLI_PARSE_ACTION_ERASERIGHT:
+      if (vec_len (cf->current_command))
+        {
+          if (cf->cursor < vec_len(cf->current_command))
+            {
+              /* shift everything to the right of the cursor left by 1 */
+              j = vec_len (cf->current_command) - cf->cursor - 1;
+              memmove (cf->current_command + cf->cursor,
+                cf->current_command + cf->cursor + 1,
+                j);
+              _vec_len (cf->current_command)--;
+              /* redraw the rest of the line */
+              unix_vlib_cli_output_cooked (cf, uf,
+                    cf->current_command + cf->cursor, j);
+              unix_vlib_cli_output_cooked (cf, uf, (u8 *) " \b", 2);
+              /* and shift the terminal cursor back where it should be */
+              if (j)
                 {
-                  if (cf->excursion > (i32) vec_len (cf->command_history) -1)
-                    cf->excursion = 0;
-                  else if (cf->excursion < 0)
-                    cf->excursion = vec_len (cf->command_history) -1;
+                  unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b", 1);
+                  while (-- j)
+                    unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b", 1);
+                }
+            }
+        }
+      else if (input == 'D' - '@')
+        {
+          /* ^D with no command entered = quit */
+          unix_vlib_cli_output_cooked (cf, uf, (u8 *) "quit\n", 5);
+          vlib_process_signal_event (um->vlib_main,
+                vlib_current_process (um->vlib_main),
+                UNIX_CLI_PROCESS_EVENT_QUIT,
+                cf - cm->cli_file_pool);
+        }
+      cf->search_mode = 0;
+      cf->excursion = 0;
+      vec_reset_length (cf->search_key);
+      break;
+
+    case UNIX_CLI_PARSE_ACTION_CLEAR:
+      /* If we're in ANSI mode, clear the screen.
+       * Then redraw the prompt and any existing command input, then put
+       * the cursor back where it was in that line.
+       */
+      if (cf->ansi_capable)
+          unix_vlib_cli_output_cooked (cf, uf,
+                    (u8 *) ANSI_CLEAR,
+                    sizeof(ANSI_CLEAR)-1);
+      else
+          unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\n", 1);
+
+      unix_vlib_cli_output_raw (cf, uf,
+                cm->cli_prompt,
+                vec_len (cm->cli_prompt));
+      unix_vlib_cli_output_raw (cf, uf,
+                cf->current_command,
+                vec_len (cf->current_command));
+      for (j = cf->cursor; j < vec_len(cf->current_command); j++)
+         unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b", 1);
+
+      break;
+
+    case UNIX_CLI_PARSE_ACTION_CRLF:
+    crlf:
+      vec_add1 (cf->current_command, '\r');
+      vec_add1 (cf->current_command, '\n');
+      unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\n", 1);
+
+      vec_validate (cf->input_vector, vec_len(cf->current_command)-1);
+      clib_memcpy (cf->input_vector, cf->current_command,
+              vec_len(cf->current_command));
+      _vec_len(cf->input_vector) = _vec_len (cf->current_command);
+
+      if (vec_len(cf->command_history) >= cf->history_limit)
+        {
+          vec_free (cf->command_history[0]);
+          vec_delete (cf->command_history, 1, 0);
+        }
+      /* Don't add blank lines to the cmd history */
+      if (vec_len (cf->current_command) > 2)
+        {
+          _vec_len (cf->current_command) -= 2;
+          vec_add1 (cf->command_history, cf->current_command);
+          cf->current_command = 0;
+        }
+      else
+        vec_reset_length (cf->current_command);
+      cf->excursion = 0;
+      cf->search_mode = 0;
+      vec_reset_length (cf->search_key);
+      cf->cursor = 0;
+
+      return 0;
+
+
+    default:
+      if (cf->search_mode && isprint(input))
+        {
+          int k, limit, offset;
+          u8 * item;
+
+          vec_add1 (cf->search_key, input);
+
+        search_again:
+          for (j = 0; j < vec_len(cf->command_history); j++)
+            {
+              if (cf->excursion > (i32) vec_len (cf->command_history) -1)
+                cf->excursion = 0;
+              else if (cf->excursion < 0)
+                cf->excursion = vec_len (cf->command_history) -1;
 
-                  item = cf->command_history[cf->excursion];
+              item = cf->command_history[cf->excursion];
 
-                  limit = (vec_len(cf->search_key) > vec_len (item)) ?
-                    vec_len(item) : vec_len (cf->search_key);
+              limit = (vec_len(cf->search_key) > vec_len (item)) ?
+                vec_len(item) : vec_len (cf->search_key);
 
-                  for (offset = 0; offset <= vec_len(item) - limit; offset++)
+              for (offset = 0; offset <= vec_len(item) - limit; offset++)
+                {
+                  for (k = 0; k < limit; k++)
                     {
-                      for (k = 0; k < limit; k++)
-                        {
-                          if (item[k+offset] != cf->search_key[k])
-                            goto next_offset;
-                        }
-                      goto found_at_offset;
-
-                    next_offset:
-                      ;
+                      if (item[k+offset] != cf->search_key[k])
+                        goto next_offset;
                     }
-                  goto next;
+                  goto found_at_offset;
+
+                next_offset:
+                  ;
+                }
+              goto next;
+
+            found_at_offset:
+              for (j = 0; j < vec_len (cf->current_command); j++)
+                unix_vlib_cli_output_cooked (cf, uf, (u8 *) "\b \b", 3);
+
+              vec_validate (cf->current_command, vec_len(item)-1);
 
-                found_at_offset:
-                  for (j = 0; j < vec_len (cf->current_command)+1; j++)
-                    unix_cli_add_pending_output (uf, cf, (u8 *) "\b \b", 3);
+              clib_memcpy (cf->current_command, item, vec_len(item));
+              _vec_len (cf->current_command) = vec_len(item);
+              unix_vlib_cli_output_cooked (cf, uf, cf->current_command,
+                                           vec_len (cf->current_command));
+              cf->cursor = vec_len (cf->current_command);
+              goto found;
 
-                  vec_validate (cf->current_command, vec_len(item)-1);
+            next:
+              cf->excursion += cf->search_mode;
+            }
 
-                  clib_memcpy (cf->current_command, item, vec_len(item));
-                  _vec_len (cf->current_command) = vec_len(item);
-                  unix_cli_add_pending_output (uf, cf, cf->current_command,
-                                               vec_len (cf->current_command));
-                  goto found;
+          unix_vlib_cli_output_cooked (cf, uf, (u8 *)"\nNo match...", 12);
+          vec_reset_length (cf->search_key);
+          vec_reset_length (cf->current_command);
+          cf->search_mode = 0;
+          cf->cursor = 0;
+          goto crlf;
+        }
+      else
+        {
+          if (isprint(input)) /* skip any errant control codes */
+            {
+              if (cf->cursor == vec_len(cf->current_command))
+                {
+                  /* Append to end */
+                  vec_add1 (cf->current_command, input);
+                  cf->cursor ++;
 
-                next:
-                  cf->excursion += cf->search_mode;
+                  /* Echo the character back to the client */
+                  unix_vlib_cli_output_raw (cf, uf, &input, 1);
+                }
+              else
+                {
+                  /* Insert at cursor: resize +1 byte, move everything over */
+                  j = vec_len (cf->current_command) - cf->cursor;
+                  vec_add1 (cf->current_command, (u8)'A');
+                  memmove (cf->current_command + cf->cursor + 1,
+                    cf->current_command + cf->cursor,
+                    j);
+                  cf->current_command[cf->cursor] = input;
+                  /* Redraw the line */
+                  j ++;
+                  unix_vlib_cli_output_raw (cf, uf,
+                        cf->current_command + cf->cursor, j);
+                  /* Put terminal cursor back */
+                  while (-- j)
+                    unix_vlib_cli_output_raw (cf, uf, (u8 *)"\b", 1);
+                  cf->cursor ++;
                 }
-              
-              unix_cli_add_pending_output (uf, cf, (u8 *)"\r\nno match..", 12);
-              vec_reset_length (cf->search_key);
-              vec_reset_length (cf->current_command);
-              cf->search_mode = 0;
-              goto crlf;
             }
-          else
-            vec_add1 (cf->current_command, cf->input_vector[i]);
+        }
+
+    found:
+
+      break;
+    }
+    return 1;
+}
 
-        found:
+/** \brief Process input bytes on a stream to provide line editing and
+ * command history in the CLI. */
+static int unix_cli_line_edit (unix_cli_main_t * cm,
+                      unix_main_t * um,
+                      unix_cli_file_t * cf)
+{
+  unix_file_t * uf = pool_elt_at_index (um->file_pool, cf->unix_file_index);
+  int i;
 
+  for (i = 0; i < vec_len (cf->input_vector); i++)
+    {
+      unix_cli_parse_action_t action;
+      /* See if the input buffer is some sort of control code */
+      i32 matched = 0;
+
+      action = unix_cli_match_action(&cf->input_vector[i],
+        vec_len (cf->input_vector) - i, &matched);
+
+      switch (action)
+        {
+        case UNIX_CLI_PARSE_ACTION_PARTIALMATCH:
+          if (i)
+            {
+              /* There was a partial match which means we need more bytes
+               * than the input buffer currently has.
+               * Since the bytes before here have been processed, shift
+               * the current contents to the start of the input buffer.
+               */
+              int j = vec_len (cf->input_vector) - i;
+              memmove(cf->input_vector, cf->input_vector + i, j);
+              _vec_len(cf->input_vector) = j;
+            }
+          return 1; /* wait for more */
+
+        case UNIX_CLI_PARSE_ACTION_TELNETIAC:
+          /* process telnet options */
+          matched = unix_cli_process_telnet(um, cf, uf,
+                cf->input_vector + i, vec_len(cf->input_vector) - i);
+          if (matched < 0)
+            {
+              if (i)
+                {
+                  /* There was a partial match which means we need more bytes
+                   * than the input buffer currently has.
+                   * Since the bytes before here have been processed, shift
+                   * the current contents to the start of the input buffer.
+                   */
+                  int j = vec_len (cf->input_vector) - i;
+                  memmove(cf->input_vector, cf->input_vector + i, j);
+                  _vec_len(cf->input_vector) = j;
+                }
+              return 1; /* wait for more */
+            }
           break;
+
+        default:
+          /* process the action */
+          if (!unix_cli_line_process_one(cm, um, cf, uf,
+                cf->input_vector[i], action))
+            return 0; /* CRLF found */
         }
+
+      i += matched;
     }
+
   vec_reset_length(cf->input_vector);
   return 1;
 }
 
-static void unix_cli_process_input (unix_cli_main_t * cm, uword cli_file_index)
+/** \brief Process input to a CLI session. */
+static void unix_cli_process_input (unix_cli_main_t * cm,
+                        uword cli_file_index)
 {
   unix_main_t * um = &unix_main;
   unix_file_t * uf;
@@ -383,7 +1170,7 @@ static void unix_cli_process_input (unix_cli_main_t * cm, uword cli_file_index)
       goto done;
 
   /* Line edit, echo, etc. */
-  if (cf->has_history && unix_cli_line_edit (um, cf))
+  if (cf->has_history && unix_cli_line_edit (cm, um, cf))
     return;
 
   if (um->log_fd)
@@ -426,7 +1213,7 @@ done:
 
   /* Prompt. */
   uf = pool_elt_at_index (um->file_pool, cf->unix_file_index);
-  unix_cli_add_pending_output (uf, cf,
+  unix_vlib_cli_output_raw (cf, uf,
                               cm->cli_prompt,
                               vec_len (cm->cli_prompt));
 }
@@ -442,7 +1229,7 @@ static void unix_cli_kill (unix_cli_main_t * cm, uword cli_file_index)
   uf = pool_elt_at_index (um->file_pool, cf->unix_file_index);
 
   /* Quit/EOF on stdin means quit program. */
-  if (uf->file_descriptor == 0)
+  if (uf->file_descriptor == UNIX_CLI_STDIN_FD)
     clib_longjmp (&um->vlib_main->main_loop_exit, VLIB_MAIN_LOOP_EXIT_CLI);
 
   vec_free (cf->current_command);
@@ -459,11 +1246,6 @@ static void unix_cli_kill (unix_cli_main_t * cm, uword cli_file_index)
   pool_put (cm->cli_file_pool, cf);
 }
 
-typedef enum {
-  UNIX_CLI_PROCESS_EVENT_READ_READY,
-  UNIX_CLI_PROCESS_EVENT_QUIT,
-} unix_cli_process_event_type_t;
-
 static uword
 unix_cli_process (vlib_main_t * vm,
                  vlib_node_runtime_t * rt,
@@ -568,7 +1350,7 @@ static u32 unix_cli_file_add (unix_cli_main_t * cm, char * name, int fd)
 {
   unix_main_t * um = &unix_main;
   unix_cli_file_t * cf;
-  unix_file_t * uf, template = {0};
+  unix_file_t template = {0};
   vlib_main_t * vm = um->vlib_main;
   vlib_node_t * n;
 
@@ -615,12 +1397,6 @@ static u32 unix_cli_file_add (unix_cli_main_t * cm, char * name, int fd)
   cf->output_vector = 0;
   cf->input_vector = 0;
 
-  uf = pool_elt_at_index (um->file_pool, cf->unix_file_index);
-
-  /* Prompt. */
-  unix_cli_add_pending_output (uf, cf,
-                              cm->cli_prompt, vec_len (cm->cli_prompt));
-
   vlib_start_process (vm, n->runtime_index);
   return cf - cm->cli_file_pool;
 }
@@ -653,28 +1429,39 @@ static clib_error_t * unix_cli_listen_read_ready (unix_file_t * uf)
   /* if we're supposed to run telnet session in character mode (default) */
   if (um->cli_line_mode == 0)
     {
-      u8 charmode_option[6];
+      /*
+       * Set telnet client character mode, echo on, suppress "go-ahead".
+       * Technically these should be negotiated, but this works.
+       */
+      u8 charmode_option[] = {
+        IAC, WONT, TELOPT_LINEMODE, /* server will do char-by-char */
+        IAC, DONT, TELOPT_LINEMODE, /* client should do char-by-char */
+        IAC, WILL, TELOPT_SGA,      /* server willl supress GA */
+        IAC, DO,   TELOPT_SGA,      /* client should supress Go Ahead */
+        IAC, WILL, TELOPT_ECHO,     /* server will do echo */
+        IAC, DONT, TELOPT_ECHO,     /* client should not echo */
+        IAC, DO,   TELOPT_TTYPE,    /* client should tell us its term type */
+        IAC, SB,   TELOPT_TTYPE, 1, IAC, SE, /* now tell me ttype */
+      };
 
+      /* Enable history on this CLI */
       cf->has_history = 1;
-      cf->history_limit = um->cli_history_limit ? um->cli_history_limit : 50;
+      cf->history_limit = um->cli_history_limit ?
+                          um->cli_history_limit :
+                          UNIX_CLI_DEFAULT_HISTORY;
 
-      /* 
-       * Set telnet client character mode, echo on, suppress "go-ahead" 
-       * Empirically, this sequence works. YMMV.
-       */
+      /* We need CRLF */
+      cf->crlf_mode = 1;
 
-      /* Tell the client no linemode, echo */
-      charmode_option[0] = IAC;
-      charmode_option[1] = DONT;
-      charmode_option[2] = TELOPT_LINEMODE;
-      charmode_option[3] = IAC;
-      charmode_option[4] = DO;
-      charmode_option[5] = TELOPT_SGA;
-      
       uf = pool_elt_at_index (um->file_pool, cf->unix_file_index);
-      
-      unix_cli_add_pending_output (uf, cf, charmode_option, 
+
+      /* Send the telnet options */
+      unix_vlib_cli_output_raw (cf, uf, charmode_option,
                                    ARRAY_LEN(charmode_option));
+
+      /* In case the client doesn't negotiate terminal type, use
+       * a timer to kick off the initial prompt. */
+      timer_call (unix_cli_file_welcome_timer, cf_index, 1);
     }
 
   return error;
@@ -685,8 +1472,12 @@ unix_cli_config (vlib_main_t * vm, unformat_input_t * input)
 {
   unix_main_t * um = &unix_main;
   unix_cli_main_t * cm = &unix_cli_main;
-  int flags, standard_input_fd;
+  int flags;
   clib_error_t * error = 0;
+  unix_cli_file_t * cf;
+  u32 cf_index;
+  struct termios tio;
+  u8 * term;
 
   /* We depend on unix flags being set. */
   if ((error = vlib_call_config_function (vm, unix_config)))
@@ -694,14 +1485,44 @@ unix_cli_config (vlib_main_t * vm, unformat_input_t * input)
 
   if (um->flags & UNIX_FLAG_INTERACTIVE)
     {
-      standard_input_fd = 0;
-
       /* Set stdin to be non-blocking. */
-      if ((flags = fcntl (standard_input_fd, F_GETFL, 0)) < 0)
-       flags = 0;
-      fcntl (standard_input_fd, F_SETFL, flags | O_NONBLOCK);
+      if ((flags = fcntl (UNIX_CLI_STDIN_FD, F_GETFL, 0)) < 0)
+        flags = 0;
+      fcntl (UNIX_CLI_STDIN_FD, F_SETFL, flags | O_NONBLOCK);
+
+      cf_index = unix_cli_file_add (cm, "stdin", UNIX_CLI_STDIN_FD);
+      cf = pool_elt_at_index (cm->cli_file_pool, cf_index);
+
+      /* If stdin is a tty and we are using chacracter mode, enable
+       * history on the CLI and set the tty line discipline accordingly. */
+      if (isatty(UNIX_CLI_STDIN_FD) && um->cli_line_mode == 0)
+        {
+          cf->has_history = 1;
+          cf->history_limit = um->cli_history_limit ?
+                              um->cli_history_limit :
+                              UNIX_CLI_DEFAULT_HISTORY;
+
+          /* Save the original tty state so we can restore it later */
+          tcgetattr(UNIX_CLI_STDIN_FD, &um->tio_stdin);
+          um->tio_isset = 1;
+
+          /* Tweak the tty settings */
+          tio = um->tio_stdin;
+          /* echo off, canonical mode off, ext'd input processing off */
+          tio.c_lflag &= ~(ECHO | ICANON | IEXTEN);
+          tio.c_cc[VMIN] = 1; /* 1 byte at a time */
+          tio.c_cc[VTIME] = 0; /* no timer */
+          tcsetattr(UNIX_CLI_STDIN_FD, TCSAFLUSH, &tio);
+
+          /* See if we can do ANSI/VT100 output */
+          term = (u8 *)getenv("TERM");
+          if (term != NULL)
+            cf->ansi_capable = unix_cli_terminal_type(term,
+                        strlen((char *)term));
+        }
 
-      unix_cli_file_add (cm, "stdin", standard_input_fd);
+      /* Send banner and initial prompt */
+      unix_cli_file_welcome(cm, cf);
     }
 
   /* If we have socket config, LISTEN, otherwise, don't */
@@ -731,6 +1552,20 @@ unix_cli_config (vlib_main_t * vm, unformat_input_t * input)
 
 VLIB_CONFIG_FUNCTION (unix_cli_config, "unix-cli");
 
+static clib_error_t *
+unix_cli_exit (vlib_main_t * vm)
+{
+  unix_main_t * um = &unix_main;
+
+  /* If stdin is a tty and we saved the tty state, reset the tty state */
+  if (isatty(UNIX_CLI_STDIN_FD) && um->tio_isset)
+    tcsetattr(UNIX_CLI_STDIN_FD, TCSAFLUSH, &um->tio_stdin);
+
+  return 0;
+}
+
+VLIB_MAIN_LOOP_EXIT_FUNCTION (unix_cli_exit);
+
 void vlib_unix_cli_set_prompt (char * prompt)
 {
   char * fmt = (prompt[strlen(prompt)-1] == ' ') ? "%s" : "%s ";
index 44a8853..269b9df 100644 (file)
@@ -41,6 +41,7 @@
 #define included_unix_unix_h
 
 #include <vppinfra/socket.h>
+#include <termios.h>
 
 struct unix_file;
 typedef clib_error_t * (unix_file_function_t) (struct unix_file * f);
@@ -102,10 +103,15 @@ typedef struct {
   /* CLI log file. GIGO. */
   u8 *log_filename;
   int log_fd;
-  /* Don't put telnet connections into character mode */
+  /* Don't put CLI connections into character mode */
   int cli_line_mode;
+
+  /* Maximum amount of command line history to keep per session */
   u32 cli_history_limit;
-  
+
+  /* Store the original state of stdin when it's a tty */
+  struct termios tio_stdin;
+  int tio_isset;
 } unix_main_t;
 
 /* Global main structure. */