Files
lk/lib/console/console.c
Travis Geiselbrecht 89f9805277 [lib][console] move the state of the console into an object
This will allow in the future multiple instances of it to be active at
at a time. Place the current console in a new TLS slot per thread so
threads created as a side effect of console commands can properly run
commands.
2021-05-29 00:52:47 -07:00

907 lines
24 KiB
C

/*
* Copyright (c) 2008-2009 Travis Geiselbrecht
*
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT
*/
#include <lib/console.h>
#include <lk/debug.h>
#include <lk/trace.h>
#include <assert.h>
#include <lk/err.h>
#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <kernel/thread.h>
#include <kernel/mutex.h>
#if WITH_LIB_ENV
#include <lib/env.h>
#endif
#define LOCAL_TRACE 0
// Whether to enable command line history. Uses a nonzero
// amount of memory, probably shouldn't enable for memory constrained devices.
#ifndef CONSOLE_ENABLE_HISTORY
#define CONSOLE_ENABLE_HISTORY 1
#endif
// Whether to enable "repeat" command.
#ifndef CONSOLE_ENABLE_REPEAT
#define CONSOLE_ENABLE_REPEAT 1
#endif
#define LINE_LEN 128
#define PANIC_LINE_LEN 32
#define MAX_NUM_ARGS 16
#define LOCAL_TRACE 0
#define WHITESPACE " \t"
// a single console instance
typedef struct console {
/* command processor state */
mutex_t lock;
int lastresult;
bool abort_script;
/* debug buffer */
char *debug_buffer;
/* echo commands? */
bool echo; // = true;
#if CONSOLE_ENABLE_HISTORY
/* command history stuff */
#define HISTORY_LEN 16
char history[HISTORY_LEN * LINE_LEN];
size_t history_next; // = 0;
#endif // CONSOLE_ENABLE_HISTORY
} console_t;
#if CONSOLE_ENABLE_HISTORY
/* command history routines */
static void add_history(console_t *con, const char *line);
static uint start_history_cursor(console_t *con);
static const char *next_history(console_t *con, uint *cursor);
static const char *prev_history(console_t *con, uint *cursor);
static void dump_history(console_t *con);
#endif
/* a linear array of statically defined command blocks,
defined in the linker script.
*/
extern const console_cmd_block __start_commands __WEAK;
extern const console_cmd_block __stop_commands __WEAK;
static int cmd_help(int argc, const console_cmd_args *argv);
static int cmd_help_panic(int argc, const console_cmd_args *argv);
static int cmd_echo(int argc, const console_cmd_args *argv);
static int cmd_test(int argc, const console_cmd_args *argv);
#if CONSOLE_ENABLE_HISTORY
static int cmd_history(int argc, const console_cmd_args *argv);
#endif
#if CONSOLE_ENABLE_REPEAT
static int cmd_repeat(int argc, const console_cmd_args *argv);
#endif
STATIC_COMMAND_START
STATIC_COMMAND("help", "this list", &cmd_help)
STATIC_COMMAND_MASKED("help", "this list", &cmd_help_panic, CMD_AVAIL_PANIC)
STATIC_COMMAND("echo", NULL, &cmd_echo)
#if LK_DEBUGLEVEL > 1
STATIC_COMMAND("test", "test the command processor", &cmd_test)
#if CONSOLE_ENABLE_HISTORY
STATIC_COMMAND("history", "command history", &cmd_history)
#endif
#if CONSOLE_ENABLE_REPEAT
STATIC_COMMAND("repeat", "repeats command multiple times", &cmd_repeat)
#endif
#endif
STATIC_COMMAND_END(console);
#if CONSOLE_ENABLE_HISTORY
static int cmd_history(int argc, const console_cmd_args *argv) {
dump_history(console_get_current());
return 0;
}
static inline char *history_line(console_t *con, uint line) {
return con->history + line * LINE_LEN;
}
static inline uint ptrnext(uint ptr) {
return (ptr + 1) % HISTORY_LEN;
}
static inline uint ptrprev(uint ptr) {
return (ptr - 1) % HISTORY_LEN;
}
static void dump_history(console_t *con) {
printf("command history:\n");
uint ptr = ptrprev(con->history_next);
int i;
for (i=0; i < HISTORY_LEN; i++) {
if (history_line(con, ptr)[0] != 0)
printf("\t%s\n", history_line(con, ptr));
ptr = ptrprev(ptr);
}
}
static void add_history(console_t *con, const char *line) {
// reject some stuff
if (line[0] == 0)
return;
size_t last = ptrprev(con->history_next);
if (strcmp(line, history_line(con, last)) == 0)
return;
strlcpy(history_line(con, con->history_next), line, LINE_LEN);
con->history_next = ptrnext(con->history_next);
}
static uint start_history_cursor(console_t *con) {
return ptrprev(con->history_next);
}
static const char *next_history(console_t *con, uint *cursor) {
uint i = ptrnext(*cursor);
if (i == con->history_next)
return ""; // can't let the cursor hit the head
*cursor = i;
return history_line(con, i);
}
static const char *prev_history(console_t *con, uint *cursor) {
uint i;
const char *str = history_line(con, *cursor);
/* if we are already at head, stop here */
if (*cursor == con->history_next)
return str;
/* back up one */
i = ptrprev(*cursor);
/* if the next one is gonna be null */
if (history_line(con, i)[0] == '\0')
return str;
/* update the cursor */
*cursor = i;
return str;
}
#endif // CONSOLE_ENABLE_HISTORY
console_t *console_get_current(void) {
console_t *con = (console_t *)tls_get(TLS_ENTRY_CONSOLE);
DEBUG_ASSERT(con);
return con;
}
console_t *console_set_current(console_t *con) {
console_t *old = (console_t *)tls_get(TLS_ENTRY_CONSOLE);
tls_set(TLS_ENTRY_CONSOLE, (uintptr_t)con);
LTRACEF("setting new %p, old %p\n", con, old);
return old;
}
#if CONSOLE_ENABLE_REPEAT
static int cmd_repeat(int argc, const console_cmd_args *argv) {
if (argc < 4) goto usage;
int times = argv[1].i;
int delay = argv[2].i;
if (times <= 0) goto usage;
if (delay < 0) goto usage;
// Worst case line length with quoting.
char line[LINE_LEN + MAX_NUM_ARGS * 3];
// Paste together all arguments, and quote them.
int idx = 0;
for (int i = 3; i < argc; ++i) {
if (i != 3) {
// Add a space before all args but the first.
line[idx++] = ' ';
}
line[idx++] = '"';
for (const char *src = argv[i].str; *src != '\0'; src++) {
line[idx++] = *src;
}
line[idx++] = '"';
}
line[idx] = '\0';
for (int i = 0; i < times; ++i) {
printf("[%d/%d]\n", i + 1, times);
int result = console_run_script_locked(console_get_current(), line);
if (result != 0) {
printf("terminating repeat loop, command exited with status %d\n",
result);
return result;
}
thread_sleep(delay);
}
return NO_ERROR;
usage:
printf("Usage: repeat <times> <delay in ms> <cmd> [args..]\n");
return ERR_INVALID_ARGS;
}
#endif // CONSOLE_ENABLE_REPEAT
static const console_cmd *match_command(const char *command, const uint8_t availability_mask) {
for (const console_cmd_block *block = &__start_commands; block != &__stop_commands; block++) {
const console_cmd *curr_cmd = block->list;
for (size_t i = 0; i < block->count; i++) {
if ((availability_mask & curr_cmd[i].availability_mask) == 0) {
continue;
}
if (strcmp(command, curr_cmd[i].cmd_str) == 0) {
return &curr_cmd[i];
}
}
}
return NULL;
}
static int read_debug_line(const char **outbuffer, void *cookie) {
int pos = 0;
int escape_level = 0;
console_t *con = (console_t *)cookie;
#if CONSOLE_ENABLE_HISTORY
uint history_cursor = start_history_cursor(con);
#endif
char *buffer = con->debug_buffer;
for (;;) {
/* loop until we get a char */
int c;
if ((c = getchar()) < 0)
continue;
// TRACEF("c = 0x%hhx\n", c);
if (escape_level == 0) {
switch (c) {
case '\r':
case '\n':
if (con->echo)
putchar('\n');
goto done;
case 0x7f: // backspace or delete
case 0x8:
if (pos > 0) {
pos--;
fputs("\b \b", stdout); // wipe out a character
}
break;
case 0x1b: // escape
escape_level++;
break;
default:
buffer[pos++] = c;
if (con->echo)
putchar(c);
}
} else if (escape_level == 1) {
// inside an escape, look for '['
if (c == '[') {
escape_level++;
} else {
// we didn't get it, abort
escape_level = 0;
}
} else { // escape_level > 1
switch (c) {
case 67: // right arrow
buffer[pos++] = ' ';
if (con->echo)
putchar(' ');
break;
case 68: // left arrow
if (pos > 0) {
pos--;
if (con->echo) {
fputs("\b \b", stdout); // wipe out a character
}
}
break;
#if CONSOLE_ENABLE_HISTORY
case 65: // up arrow -- previous history
case 66: // down arrow -- next history
// wipe out the current line
while (pos > 0) {
pos--;
if (con->echo) {
fputs("\b \b", stdout); // wipe out a character
}
}
if (c == 65)
strlcpy(buffer, prev_history(con, &history_cursor), LINE_LEN);
else
strlcpy(buffer, next_history(con, &history_cursor), LINE_LEN);
pos = strlen(buffer);
if (con->echo)
fputs(buffer, stdout);
break;
#endif
default:
break;
}
escape_level = 0;
}
/* end of line. */
if (pos == (LINE_LEN - 1)) {
fputs("\nerror: line too long\n", stdout);
pos = 0;
goto done;
}
}
done:
// dprintf("returning pos %d\n", pos);
// null terminate
buffer[pos] = 0;
#if CONSOLE_ENABLE_HISTORY
// add to history
add_history(con, buffer);
#endif
// return a pointer to our buffer
*outbuffer = buffer;
return pos;
}
static int tokenize_command(const char *inbuffer, const char **continuebuffer, char *buffer, size_t buflen, console_cmd_args *args, int arg_count) {
int inpos;
int outpos;
int arg;
enum {
INITIAL = 0,
NEXT_FIELD,
SPACE,
IN_SPACE,
TOKEN,
IN_TOKEN,
QUOTED_TOKEN,
IN_QUOTED_TOKEN,
VAR,
IN_VAR,
COMMAND_SEP,
} state;
char varname[128];
int varnamepos;
inpos = 0;
outpos = 0;
arg = 0;
varnamepos = 0;
state = INITIAL;
*continuebuffer = NULL;
for (;;) {
char c = inbuffer[inpos];
// dprintf(SPEW, "c 0x%hhx state %d arg %d inpos %d pos %d\n", c, state, arg, inpos, outpos);
switch (state) {
case INITIAL:
case NEXT_FIELD:
if (c == '\0')
goto done;
if (isspace(c))
state = SPACE;
else if (c == ';')
state = COMMAND_SEP;
else
state = TOKEN;
break;
case SPACE:
state = IN_SPACE;
break;
case IN_SPACE:
if (c == '\0')
goto done;
if (c == ';') {
state = COMMAND_SEP;
} else if (!isspace(c)) {
state = TOKEN;
} else {
inpos++; // consume the space
}
break;
case TOKEN:
// start of a token
DEBUG_ASSERT(c != '\0');
if (c == '"') {
// start of a quoted token
state = QUOTED_TOKEN;
} else if (c == '$') {
// start of a variable
state = VAR;
} else {
// regular, unquoted token
state = IN_TOKEN;
args[arg].str = &buffer[outpos];
}
break;
case IN_TOKEN:
if (c == '\0') {
arg++;
goto done;
}
if (isspace(c) || c == ';') {
arg++;
buffer[outpos] = 0;
outpos++;
/* are we out of tokens? */
if (arg == arg_count)
goto done;
state = NEXT_FIELD;
} else {
buffer[outpos] = c;
outpos++;
inpos++;
}
break;
case QUOTED_TOKEN:
// start of a quoted token
DEBUG_ASSERT(c == '"');
state = IN_QUOTED_TOKEN;
args[arg].str = &buffer[outpos];
inpos++; // consume the quote
break;
case IN_QUOTED_TOKEN:
if (c == '\0') {
arg++;
goto done;
}
if (c == '"') {
arg++;
buffer[outpos] = 0;
outpos++;
/* are we out of tokens? */
if (arg == arg_count)
goto done;
state = NEXT_FIELD;
}
buffer[outpos] = c;
outpos++;
inpos++;
break;
case VAR:
DEBUG_ASSERT(c == '$');
state = IN_VAR;
args[arg].str = &buffer[outpos];
inpos++; // consume the dollar sign
// initialize the place to store the variable name
varnamepos = 0;
break;
case IN_VAR:
if (c == '\0' || isspace(c) || c == ';') {
// hit the end of variable, look it up and stick it inline
varname[varnamepos] = 0;
#if WITH_LIB_ENV
int rc = env_get(varname, &buffer[outpos], buflen - outpos);
#else
(void)varname[0]; // nuke a warning
int rc = -1;
#endif
if (rc < 0) {
buffer[outpos++] = '0';
buffer[outpos++] = 0;
} else {
outpos += strlen(&buffer[outpos]) + 1;
}
arg++;
/* are we out of tokens? */
if (arg == arg_count)
goto done;
state = NEXT_FIELD;
} else {
varname[varnamepos] = c;
varnamepos++;
inpos++;
}
break;
case COMMAND_SEP:
// we hit a ;, so terminate the command and pass the remainder of the command back in continuebuffer
DEBUG_ASSERT(c == ';');
inpos++; // consume the ';'
*continuebuffer = &inbuffer[inpos];
goto done;
}
}
done:
buffer[outpos] = 0;
return arg;
}
static void convert_args(int argc, console_cmd_args *argv) {
int i;
for (i = 0; i < argc; i++) {
unsigned long u = atoul(argv[i].str);
argv[i].u = u;
argv[i].p = (void *)u;
argv[i].i = atol(argv[i].str);
if (!strcmp(argv[i].str, "true") || !strcmp(argv[i].str, "on")) {
argv[i].b = true;
} else if (!strcmp(argv[i].str, "false") || !strcmp(argv[i].str, "off")) {
argv[i].b = false;
} else {
argv[i].b = (argv[i].u == 0) ? false : true;
}
}
}
static status_t command_loop(console_t *con, int (*get_line)(const char **, void *), void *get_line_cookie, bool showprompt, bool locked) {
bool exit;
#if WITH_LIB_ENV
bool report_result;
#endif
console_cmd_args *args = NULL;
const char *buffer;
const char *continuebuffer;
char *outbuf = NULL;
args = (console_cmd_args *) malloc (MAX_NUM_ARGS * sizeof(console_cmd_args));
if (unlikely(args == NULL)) {
goto no_mem_error;
}
const size_t outbuflen = 1024;
outbuf = malloc(outbuflen);
if (unlikely(outbuf == NULL)) {
goto no_mem_error;
}
exit = false;
continuebuffer = NULL;
while (!exit) {
// read a new line if it hadn't been split previously and passed back from tokenize_command
if (continuebuffer == NULL) {
if (showprompt)
fputs("] ", stdout);
int len = get_line(&buffer, get_line_cookie);
if (len < 0)
break;
if (len == 0)
continue;
} else {
buffer = continuebuffer;
}
// dprintf("line = '%s'\n", buffer);
/* tokenize the line */
int argc = tokenize_command(buffer, &continuebuffer, outbuf, outbuflen,
args, MAX_NUM_ARGS);
if (argc < 0) {
if (showprompt)
printf("syntax error\n");
continue;
} else if (argc == 0) {
continue;
}
// dprintf("after tokenize: argc %d\n", argc);
// for (int i = 0; i < argc; i++)
// dprintf("%d: '%s'\n", i, args[i].str);
/* convert the args */
convert_args(argc, args);
/* try to match the command */
const console_cmd *command = match_command(args[0].str, CMD_AVAIL_NORMAL);
if (!command) {
if (showprompt)
printf("command not found\n");
continue;
}
if (!locked)
mutex_acquire(&con->lock);
con->abort_script = false;
con->lastresult = command->cmd_callback(argc, args);
#if WITH_LIB_ENV
bool report_result;
env_get_bool("reportresult", &report_result, false);
if (report_result) {
if (con->lastresult < 0)
printf("FAIL %d\n", con->lastresult);
else
printf("PASS %d\n", con->lastresult);
}
#endif
#if WITH_LIB_ENV
// stuff the result in an environment var
env_set_int("?", con->lastresult, true);
#endif
// someone must have aborted the current script
if (con->abort_script)
exit = true;
con->abort_script = false;
if (!locked)
mutex_release(&con->lock);
}
free(outbuf);
free(args);
return NO_ERROR;
no_mem_error:
if (outbuf)
free(outbuf);
if (args)
free(args);
dprintf(INFO, "%s: not enough memory\n", __func__);
return ERR_NO_MEMORY;
}
void console_abort_script(console_t *con) {
if (!con) {
con = console_get_current();
}
con->abort_script = true;
}
console_t *console_create(bool with_history) {
console_t *con = calloc(1, sizeof(console_t));
if (!con) {
dprintf(INFO, "error allocating console object\n");
return NULL;
}
// initialize
mutex_init(&con->lock);
con->echo = true;
con->debug_buffer = malloc(LINE_LEN);
return con;
}
void console_start(console_t *con) {
dprintf(INFO, "entering main console loop\n");
console_set_current(con);
while (command_loop(con, &read_debug_line, con, true, false) == NO_ERROR)
;
console_set_current(NULL);
dprintf(INFO, "exiting main console loop\n");
}
struct line_read_struct {
const char *string;
int pos;
char *buffer;
size_t buflen;
};
static int fetch_next_line(const char **buffer, void *cookie) {
struct line_read_struct *lineread = (struct line_read_struct *)cookie;
// we're done
if (lineread->string[lineread->pos] == 0)
return -1;
size_t bufpos = 0;
while (lineread->string[lineread->pos] != 0) {
if (lineread->string[lineread->pos] == '\n') {
lineread->pos++;
break;
}
if (bufpos == (lineread->buflen - 1))
break;
lineread->buffer[bufpos] = lineread->string[lineread->pos];
lineread->pos++;
bufpos++;
}
lineread->buffer[bufpos] = 0;
*buffer = lineread->buffer;
return bufpos;
}
static int console_run_script_etc(console_t *con, const char *string, bool locked) {
struct line_read_struct lineread;
lineread.string = string;
lineread.pos = 0;
lineread.buffer = malloc(LINE_LEN);
lineread.buflen = LINE_LEN;
command_loop(con, &fetch_next_line, (void *)&lineread, false, locked);
free(lineread.buffer);
return con->lastresult;
}
int console_run_script(console_t *con, const char *string) {
if (!con) {
con = console_get_current();
}
return console_run_script_etc(con, string, false);
}
int console_run_script_locked(console_t *con, const char *string) {
if (!con) {
con = console_get_current();
}
return console_run_script_etc(con, string, true);
}
console_cmd_func console_get_command_handler(const char *commandstr) {
const console_cmd *command = match_command(commandstr, CMD_AVAIL_NORMAL);
if (command)
return command->cmd_callback;
else
return NULL;
}
static int cmd_help_impl(uint8_t availability_mask) {
printf("command list by block:\n");
for (const console_cmd_block *block = &__start_commands; block != &__stop_commands; block++) {
const console_cmd *curr_cmd = block->list;
printf(" [%s]\n", block->name);
for (size_t i = 0; i < block->count; i++) {
if ((availability_mask & curr_cmd[i].availability_mask) == 0) {
// Skip commands that aren't available in the current shell.
continue;
}
if (curr_cmd[i].help_str)
printf("\t%-16s: %s\n", curr_cmd[i].cmd_str, curr_cmd[i].help_str);
}
}
return 0;
}
static int cmd_help(int argc, const console_cmd_args *argv) {
return cmd_help_impl(CMD_AVAIL_NORMAL);
}
static int cmd_help_panic(int argc, const console_cmd_args *argv) {
return cmd_help_impl(CMD_AVAIL_PANIC);
}
static int cmd_echo(int argc, const console_cmd_args *argv) {
if (argc > 1)
console_get_current()->echo = argv[1].b;
return NO_ERROR;
}
static void read_line_panic(char *buffer, const size_t len, FILE *panic_fd) {
size_t pos = 0;
for (;;) {
int c;
if ((c = getc(panic_fd)) < 0) {
continue;
}
switch (c) {
case '\r':
case '\n':
fputc('\n', panic_fd);
goto done;
case 0x7f: // backspace or delete
case 0x8:
if (pos > 0) {
pos--;
fputs("\b \b", panic_fd); // wipe out a character
}
break;
default:
buffer[pos++] = c;
fputc(c, panic_fd);
}
if (pos == (len - 1)) {
fputs("\nerror: line too long\n", panic_fd);
pos = 0;
goto done;
}
}
done:
buffer[pos] = 0;
}
void panic_shell_start(void) {
dprintf(INFO, "entering panic shell loop\n");
char input_buffer[PANIC_LINE_LEN];
console_cmd_args args[MAX_NUM_ARGS];
// panic_fd allows us to do I/O using the polling drivers.
// These drivers function even if interrupts are disabled.
FILE *panic_fd = get_panic_fd();
if (!panic_fd)
return;
for (;;) {
fputs("! ", panic_fd);
read_line_panic(input_buffer, PANIC_LINE_LEN, panic_fd);
int argc;
char *tok = strtok(input_buffer, WHITESPACE);
for (argc = 0; argc < MAX_NUM_ARGS; argc++) {
if (tok == NULL) {
break;
}
args[argc].str = tok;
tok = strtok(NULL, WHITESPACE);
}
if (argc == 0) {
continue;
}
convert_args(argc, args);
const console_cmd *command = match_command(args[0].str, CMD_AVAIL_PANIC);
if (!command) {
fputs("command not found\n", panic_fd);
continue;
}
command->cmd_callback(argc, args);
}
}
#if LK_DEBUGLEVEL > 1
static int cmd_test(int argc, const console_cmd_args *argv) {
int i;
printf("argc %d, argv %p\n", argc, argv);
for (i = 0; i < argc; i++)
printf("\t%d: str '%s', i %ld, u %#lx, b %d\n", i, argv[i].str, argv[i].i, argv[i].u, argv[i].b);
return 0;
}
#endif