From d10141ef07644fc6a39211d60e811a868d91c575 Mon Sep 17 00:00:00 2001 From: Tarek BOCHKATI Date: Thu, 11 Mar 2021 03:38:54 +0100 Subject: [PATCH] telnet: auto-completion of "registered" commands auto-completion behavior: - if there is only one matched command complete the user-command - else if multiple matches add the common part then in second step list all matched commands - sub-commands are handled in the same way - auto-completion restarts after these characters ';', '[', '{' Change-Id: I1b81dd19191a5785e68d0bb5cd244e01a4dd0587 Signed-off-by: Tarek BOCHKATI Reviewed-on: http://openocd.zylin.com/6095 Tested-by: jenkins Reviewed-by: Antonio Borneo --- src/server/telnet_server.c | 248 +++++++++++++++++++++++++++++++++---- 1 file changed, 223 insertions(+), 25 deletions(-) diff --git a/src/server/telnet_server.c b/src/server/telnet_server.c index e9de4f033..d5e0353c8 100644 --- a/src/server/telnet_server.c +++ b/src/server/telnet_server.c @@ -29,6 +29,7 @@ #include "telnet_server.h" #include #include +#include static char *telnet_port; @@ -58,6 +59,13 @@ static int telnet_write(struct connection *connection, const void *data, return ERROR_SERVER_REMOTE_CLOSED; } +/* output an audible bell */ +static int telnet_bell(struct connection *connection) +{ + /* ("\a" does not work, at least on windows) */ + return telnet_write(connection, "\x07", 1); +} + static int telnet_prompt(struct connection *connection) { struct telnet_connection *t_con = connection->priv; @@ -366,6 +374,217 @@ static void telnet_move_cursor(struct connection *connection, size_t pos) tc->line_cursor = pos; } +/* check buffer size leaving one spare character for string null termination */ +static inline bool telnet_can_insert(struct connection *connection, size_t len) +{ + struct telnet_connection *t_con = connection->priv; + + return t_con->line_size + len < TELNET_LINE_MAX_SIZE; +} + +/* write to telnet console, and update the telnet_connection members + * this function is capable of inserting in the middle of a line + * please ensure that data does not contain special characters (\n, \r, \t, \b ...) + * + * returns false when it fails to insert the requested data + */ +static bool telnet_insert(struct connection *connection, const void *data, size_t len) +{ + struct telnet_connection *t_con = connection->priv; + + if (!telnet_can_insert(connection, len)) { + telnet_bell(connection); + return false; + } + + if (t_con->line_cursor < t_con->line_size) { + /* we have some content after the cursor */ + memmove(t_con->line + t_con->line_cursor + len, + t_con->line + t_con->line_cursor, + t_con->line_size - t_con->line_cursor); + } + + strncpy(t_con->line + t_con->line_cursor, data, len); + + telnet_write(connection, + t_con->line + t_con->line_cursor, + t_con->line_size + len - t_con->line_cursor); + + t_con->line_size += len; + t_con->line_cursor += len; + + for (size_t i = t_con->line_cursor; i < t_con->line_size; i++) + telnet_write(connection, "\b", 1); + + return true; +} + +static void telnet_auto_complete(struct connection *connection) +{ + struct telnet_connection *t_con = connection->priv; + struct command_context *command_context = connection->cmd_ctx; + + struct cmd_match { + char *cmd; + struct list_head lh; + }; + + LIST_HEAD(matches); + + /* user command sequence, either at line beginning + * or we start over after these characters ';', '[', '{' */ + size_t seq_start = (t_con->line_cursor == 0) ? 0 : (t_con->line_cursor - 1); + while (seq_start > 0) { + char c = t_con->line[seq_start]; + if (c == ';' || c == '[' || c == '{') { + seq_start++; + break; + } + + seq_start--; + } + + /* user command position in the line, ignore leading spaces */ + size_t usr_cmd_pos = seq_start; + while ((usr_cmd_pos < t_con->line_cursor) && isspace(t_con->line[usr_cmd_pos])) + usr_cmd_pos++; + + /* user command length */ + size_t usr_cmd_len = t_con->line_cursor - usr_cmd_pos; + + /* optimize multiple spaces in the user command, + * because info commands does not tolerate multiple spaces */ + size_t optimized_spaces = 0; + char query[usr_cmd_len + 1]; + for (size_t i = 0; i < usr_cmd_len; i++) { + if ((i < usr_cmd_len - 1) && isspace(t_con->line[usr_cmd_pos + i]) + && isspace(t_con->line[usr_cmd_pos + i + 1])) { + optimized_spaces++; + continue; + } + + query[i - optimized_spaces] = t_con->line[usr_cmd_pos + i]; + } + + usr_cmd_len -= optimized_spaces; + query[usr_cmd_len] = '\0'; + + /* filter commands */ + char *query_cmd = alloc_printf("lsort [info commands {%s*}]", query); + + if (!query_cmd) { + LOG_ERROR("Out of memory"); + return; + } + + int retval = Jim_EvalSource(command_context->interp, __FILE__, __LINE__, query_cmd); + free(query_cmd); + if (retval != JIM_OK) + return; + + Jim_Obj *list = Jim_GetResult(command_context->interp); + Jim_IncrRefCount(list); + + /* common prefix length of the matched commands */ + size_t common_len = 0; + char *first_match = NULL; /* used to compute the common prefix length */ + + int len = Jim_ListLength(command_context->interp, list); + for (int i = 0; i < len; i++) { + Jim_Obj *elem = Jim_ListGetIndex(command_context->interp, list, i); + Jim_IncrRefCount(elem); + + char *name = (char *)Jim_GetString(elem, NULL); + + /* validate the command */ + bool ignore_cmd = false; + Jim_Cmd *jim_cmd = Jim_GetCommand(command_context->interp, elem, JIM_NONE); + + if (!jim_cmd) + ignore_cmd = true; + else { + if (!jim_cmd->isproc) { + /* ignore commands without handler + * and those with COMMAND_CONFIG mode */ + /* FIXME it's better to use jimcmd_is_ocd_command(jim_cmd) + * or command_find_from_name(command_context->interp, name) */ + struct command *cmd = jim_cmd->u.native.privData; + if (!cmd) + ignore_cmd = true; + /* make Valgrind happy by checking that cmd is not NULL */ + else if (cmd != NULL && !cmd->handler && !cmd->jim_handler) + ignore_cmd = true; + else if (cmd != NULL && cmd->mode == COMMAND_CONFIG) + ignore_cmd = true; + } + } + + /* save the command in the prediction list */ + if (!ignore_cmd) { + struct cmd_match *match = calloc(1, sizeof(struct cmd_match)); + if (!match) { + LOG_ERROR("Out of memory"); + Jim_DecrRefCount(command_context->interp, elem); + break; /* break the for loop */ + } + + if (list_empty(&matches)) { + common_len = strlen(name); + first_match = name; + } else { + size_t new_common_len = usr_cmd_len; /* save some loops */ + + while (new_common_len < common_len && first_match[new_common_len] == name[new_common_len]) + new_common_len++; + + common_len = new_common_len; + } + + match->cmd = name; + list_add_tail(&match->lh, &matches); + } + + Jim_DecrRefCount(command_context->interp, elem); + } + /* end of command filtering */ + + /* proceed with auto-completion */ + if (list_empty(&matches)) + telnet_bell(connection); + else if (common_len == usr_cmd_len && list_is_singular(&matches) && t_con->line_cursor == t_con->line_size) + telnet_insert(connection, " ", 1); + else if (common_len > usr_cmd_len) { + int completion_size = common_len - usr_cmd_len; + if (telnet_insert(connection, first_match + usr_cmd_len, completion_size)) { + /* in bash this extra space is only added when the cursor in at the end of line */ + if (list_is_singular(&matches) && t_con->line_cursor == t_con->line_size) + telnet_insert(connection, " ", 1); + } + } else if (!list_is_singular(&matches)) { + telnet_write(connection, "\n\r", 2); + + struct cmd_match *match; + list_for_each_entry(match, &matches, lh) { + telnet_write(connection, match->cmd, strlen(match->cmd)); + telnet_write(connection, "\n\r", 2); + } + + telnet_prompt(connection); + telnet_write(connection, t_con->line, t_con->line_size); + + /* restore the terminal visible cursor location */ + for (size_t i = t_con->line_cursor; i < t_con->line_size; i++) + telnet_write(connection, "\b", 1); + } + + /* destroy the command_list */ + struct cmd_match *tmp, *match; + list_for_each_entry_safe(match, tmp, &matches, lh) + free(match); + + Jim_DecrRefCount(command_context->interp, list); +} + static int telnet_input(struct connection *connection) { int bytes_read; @@ -391,30 +610,7 @@ static int telnet_input(struct connection *connection) t_con->state = TELNET_STATE_IAC; else { if (isprint(*buf_p)) { /* printable character */ - /* watch buffer size leaving one spare character for - * string null termination */ - if (t_con->line_size == TELNET_LINE_MAX_SIZE-1) { - /* output audible bell if buffer is full - * "\a" does not work, at least on windows */ - telnet_write(connection, "\x07", 1); - } else if (t_con->line_cursor == t_con->line_size) { - telnet_write(connection, buf_p, 1); - t_con->line[t_con->line_size++] = *buf_p; - t_con->line_cursor++; - } else { - size_t i; - memmove(t_con->line + t_con->line_cursor + 1, - t_con->line + t_con->line_cursor, - t_con->line_size - t_con->line_cursor); - t_con->line[t_con->line_cursor] = *buf_p; - t_con->line_size++; - telnet_write(connection, - t_con->line + t_con->line_cursor, - t_con->line_size - t_con->line_cursor); - t_con->line_cursor++; - for (i = t_con->line_cursor; i < t_con->line_size; i++) - telnet_write(connection, "\b", 1); - } + telnet_insert(connection, buf_p, 1); } else { /* non-printable */ if (*buf_p == 0x1b) { /* escape */ t_con->state = TELNET_STATE_ESCAPE; @@ -548,7 +744,9 @@ static int telnet_input(struct connection *connection) t_con->line[t_con->line_cursor] = '\0'; t_con->line_size = t_con->line_cursor; } - } else + } else if (*buf_p == '\t') + telnet_auto_complete(connection); + else LOG_DEBUG("unhandled nonprintable: %2.2x", *buf_p); } } -- 2.30.2