X-Git-Url: http://git.tuebingen.mpg.de/?p=paraslash.git;a=blobdiff_plain;f=interactive.c;h=64ee9202fc56309769609c3869738a3e8fd5ec65;hp=e69de29bb2d1d6434b8b29ae775ad8c2e48c5391;hb=88c5dc601a5d1b194da3102566a3fb97947a6aee;hpb=408458ad84180244b6a2ac327d13685bfb2b0867 diff --git a/interactive.c b/interactive.c index e69de29b..64ee9202 100644 --- a/interactive.c +++ b/interactive.c @@ -0,0 +1,640 @@ +/* + * Copyright (C) 2011-2012 Andre Noll + * + * Licensed under the GPL v2. For licencing details see COPYING. + */ + +/** \file interactive.c Readline abstraction for interactive sessions. */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "para.h" +#include "fd.h" +#include "buffer_tree.h" +#include "list.h" +#include "sched.h" +#include "interactive.h" +#include "string.h" +#include "error.h" + +struct i9e_private { + struct i9e_client_info *ici; + FILE *stderr_stream; + char empty_line[1000]; + struct task task; + struct btr_node *stdout_btrn; + bool line_handler_running; + bool input_eof; + bool caught_sigint; +}; +static struct i9e_private i9e_private, *i9ep = &i9e_private; + +static bool is_prefix(const char *partial, const char *full, size_t len) +{ + if (len == 0) + len = strlen(partial); + return !strncmp(partial, full, len); +} + +/* + * Generator function for command completion. STATE lets us know whether + * to start from scratch; without any state (i.e. STATE == 0), then we + * start at the top of the list. + */ +static char *command_generator(const char *text, int state) +{ + static int list_index, len; + const char *name; + struct i9e_client_info *ici = i9ep->ici; + + rl_attempted_completion_over = 1; /* disable filename completion */ + /* + * If this is a new word to complete, initialize now. This includes + * saving the length of TEXT for efficiency, and initializing the index + * variable to 0. + */ + if (state == 0) { + list_index = 0; + len = strlen(text); + } + /* Return the next name which partially matches from the command list. */ + while ((name = ici->completers[list_index].name)) { + list_index++; + if (is_prefix(text, name, len)) + return para_strdup(name); + } + return NULL; /* no names matched */ +} + +static void reset_completion_result(struct i9e_completion_result *cr) +{ + cr->dont_append_space = false; + cr->filename_completion_desired = false; + cr->matches = NULL; +} + +static void create_matches(struct i9e_completion_info *ci, + struct i9e_completer *completers, + struct i9e_completion_result *cr) +{ + int i, ret; + + reset_completion_result(cr); + + ret = create_argv(ci->buffer, " ", &ci->argv); + if (ret < 0 || !ci->argv[0]) + return; + + ci->argc = ret; + ci->word_num = compute_word_num(ci->buffer, " ", ci->point); + for (i = 0; completers[i].name; i++) { + if (strcmp(completers[i].name, ci->argv[0]) != 0) + continue; + completers[i].completer(ci, cr); + break; + } + PARA_DEBUG_LOG("current word: %d (%s)\n", ci->word_num, + ci->argv[ci->word_num]); + if (cr->matches) + for (i = 0; cr->matches[i]; i++) + PARA_DEBUG_LOG("match %d: %s\n", i, cr->matches[i]); +} + +static char *completion_generator(const char *word, int state) +{ + static int list_index; + static char **argv, **matches; + struct i9e_completer *completers = i9ep->ici->completers; + struct i9e_completion_info ci = { + .word = (char *)word, + .point = rl_point, + .buffer = rl_line_buffer, + }; + struct i9e_completion_result cr = {.matches = NULL}; + + if (state != 0) + goto out; + /* clean up previous matches and set defaults */ + free(matches); + matches = NULL; + free_argv(argv); + argv = NULL; + list_index = 0; + rl_completion_append_character = ' '; + rl_completion_suppress_append = false; + rl_attempted_completion_over = true; + + create_matches(&ci, completers, &cr); + + matches = cr.matches; + argv = ci.argv; + rl_completion_suppress_append = cr.dont_append_space; + rl_attempted_completion_over = !cr.filename_completion_desired; +out: + if (!matches) + return NULL; + return matches[list_index++]; +} + +/* + * Attempt to complete on the contents of TEXT. START and END bound the + * region of rl_line_buffer that contains the word to complete. TEXT is + * the word to complete. We can use the entire contents of rl_line_buffer + * in case we want to do some simple parsing. Return the array of matches, + * or NULL if there aren't any. + */ +static char **i9e_completer(const char *text, int start, __a_unused int end) +{ + struct i9e_client_info *ici = i9ep->ici; + + if (!ici->completers) + return NULL; + /* Complete on command names if this is the first word in the line. */ + if (start == 0) + return rl_completion_matches(text, command_generator); + return rl_completion_matches(text, completion_generator); +} + +/** + * Prepare writing to stdout. + * + * \param producer The buffer tree node which produces output. + * + * The i9e subsystem maintains a buffer tree node which may be attached to + * another node which generates output (a "producer"). When attached, the i9e + * buffer tree node copies the buffers generated by the producer to stdout. + * + * This function attaches the i9e input queue to an output queue of \a + * producer. + * + * \return Standard. + */ +void i9e_attach_to_stdout(struct btr_node *producer) +{ + assert(!i9ep->stdout_btrn); + i9ep->stdout_btrn = btr_new_node(&(struct btr_node_description) + EMBRACE(.name = "interactive_stdout", .parent = producer)); +} + +/** + * Reset the terminal and save the in-memory command line history. + * + * This should be called before the caller exits. + */ +void i9e_close(void) +{ + char *hf = i9ep->ici->history_file; + + rl_deprep_terminal(); + fprintf(i9ep->stderr_stream, "\n"); + if (hf) + write_history(hf); + fclose(i9ep->stderr_stream); +} + +static void wipe_bottom_line(void) +{ + fprintf(i9ep->stderr_stream, "\r%s\r", i9ep->empty_line); +} + +static void clear_bottom_line(void) +{ + int point; + char *text; + + if (rl_point == 0 && rl_end == 0) + return wipe_bottom_line(); + /* + * We might have a multi-line input that needs to be wiped here, so the + * simple printf("\r\r") is insufficient. To workaround this, we + * remove the whole line, redisplay and restore the killed text. + */ + point = rl_point; + text = rl_copy_text(0, rl_end); + rl_kill_full_line(0, 0); + rl_redisplay(); + wipe_bottom_line(); /* wipe out the prompt */ + rl_insert_text(text); + rl_point = point; +} + +static bool input_available(void) +{ + fd_set rfds; + struct timeval tv = {0, 0}; + int ret; + + FD_ZERO(&rfds); + FD_SET(i9ep->ici->fds[0], &rfds); + ret = para_select(1, &rfds, NULL, &tv); + return ret > 0; +} + +static void i9e_line_handler(char *line) +{ + int ret; + + i9ep->line_handler_running = true; + ret = i9ep->ici->line_handler(line); + i9ep->line_handler_running = false; + if (ret < 0) + PARA_WARNING_LOG("%s\n", para_strerror(-ret)); + rl_set_prompt(""); + if (line) { + if (!*line) + rl_set_prompt(i9ep->ici->prompt); + else + add_history(line); + free(line); + } else { + rl_set_prompt(""); + i9ep->input_eof = true; + } +} + +static void i9e_input(void) +{ + do { + rl_callback_read_char(); + } while (input_available()); +} + +static void i9e_post_select(struct sched *s, struct task *t) +{ + int ret; + struct btr_node *btrn = i9ep->stdout_btrn; + struct i9e_client_info *ici = i9ep->ici; + char *buf; + size_t sz; + + if (i9ep->input_eof) { + t->error = -E_I9E_EOF; + return; + } + if (!btrn) { + i9ep->caught_sigint = false; + if (FD_ISSET(ici->fds[0], &s->rfds)) + i9e_input(); + return; + } + if (i9ep->caught_sigint) + goto rm_btrn; + ret = btr_node_status(i9ep->stdout_btrn, 0, BTR_NT_LEAF); + if (ret < 0) + goto rm_btrn; + sz = btr_next_buffer(btrn, &buf); + if (sz == 0) + goto out; + ret = write_nonblock(ici->fds[1], buf, sz); + if (ret < 0) + goto rm_btrn; + btr_consume(btrn, ret); + goto out; +rm_btrn: + btr_remove_node(btrn); + btr_free_node(btrn); + i9ep->stdout_btrn = NULL; + rl_set_prompt(i9ep->ici->prompt); + rl_forced_update_display(); +out: + t->error = 0; +} + +static void i9e_pre_select(struct sched *s, __a_unused struct task *t) +{ + int ret; + + if (i9ep->input_eof || i9ep->caught_sigint) { + sched_min_delay(s); + return; + } + if (i9ep->stdout_btrn) { + ret = btr_node_status(i9ep->stdout_btrn, 0, BTR_NT_LEAF); + if (ret < 0) { + sched_min_delay(s); + return; + } + if (ret > 0) + para_fd_set(i9ep->ici->fds[1], &s->wfds, &s->max_fileno); + } + /* + * fd[0] might have been reset to blocking mode if our job was moved to + * the background due to CTRL-Z or SIGSTOP, so set the fd back to + * nonblocking mode. + */ + ret = mark_fd_nonblocking(i9ep->ici->fds[0]); + if (ret < 0) + PARA_WARNING_LOG("set to nonblock failed: (fd0 %d, %s)\n", + i9ep->ici->fds[0], para_strerror(-ret)); + para_fd_set(i9ep->ici->fds[0], &s->rfds, &s->max_fileno); + return; +} + +static void update_winsize(void) +{ + struct winsize w; + int ret = ioctl(i9ep->ici->fds[2], TIOCGWINSZ, (char *)&w); + int num_columns = 80; + + if (ret >= 0) { + assert(w.ws_col < sizeof(i9ep->empty_line)); + num_columns = w.ws_col; + } + memset(i9ep->empty_line, ' ', num_columns); + i9ep->empty_line[num_columns] = '\0'; +} + +/** + * Register the i9e task and initialize readline. + * + * \param ici The i9e configuration parameters set by the caller. + * \param s The scheduler instance to add the i9e task to. + * + * The caller must allocate and initialize the structure \a ici points to. + * + * \return Standard. + * \sa \ref register_task(). + */ +int i9e_open(struct i9e_client_info *ici, struct sched *s) +{ + int ret; + + if (!isatty(ici->fds[0])) + return -E_I9E_SETUPTERM; + ret = mark_fd_nonblocking(ici->fds[0]); + if (ret < 0) + return ret; + ret = mark_fd_nonblocking(ici->fds[1]); + if (ret < 0) + return ret; + i9ep->task.pre_select = i9e_pre_select; + i9ep->task.post_select = i9e_post_select; + register_task(s, &i9ep->task); + rl_readline_name = "para_i9e"; + rl_basic_word_break_characters = " "; + rl_attempted_completion_function = i9e_completer; + i9ep->ici = ici; + i9ep->stderr_stream = fdopen(ici->fds[2], "w"); + + if (ici->history_file) + read_history(ici->history_file); + update_winsize(); + rl_callback_handler_install(i9ep->ici->prompt, i9e_line_handler); + return 1; +} + +static void reset_line_state(void) +{ + if (i9ep->line_handler_running) + return; + rl_on_new_line(); + rl_reset_line_state(); + rl_forced_update_display(); +} + +/** + * The log function of the i9e subsystem. + * + * \param ll Severity log level. + * \param fmt Printf-like format string. + * + * This clears the bottom line of the terminal if necessary and writes the + * string given by \a fmt to fd[2], where fd[] is the array provided earlier in + * \ref i9e_open(). + */ +__printf_2_3 void i9e_log(int ll, const char* fmt,...) +{ + va_list argp; + + if (ll < i9ep->ici->loglevel) + return; + if (i9ep->line_handler_running == false) + clear_bottom_line(); + va_start(argp, fmt); + vfprintf(i9ep->stderr_stream, fmt, argp); + va_end(argp); + reset_line_state(); +} + +/** + * Tell i9e that the caller received a signal. + * + * \param sig_num The number of the signal received. + * + * Currently the function only cares about \p SIGINT, but this may change. + */ +void i9e_signal_dispatch(int sig_num) +{ + if (sig_num == SIGINT) { + fprintf(i9ep->stderr_stream, "\n"); + rl_replace_line ("", false /* clear_undo */); + reset_line_state(); + i9ep->caught_sigint = true; + } +} + +/** + * Wrapper for select(2) which does not restart on interrupts. + * + * \param n \sa \ref para_select(). + * \param readfds \sa \ref para_select(). + * \param writefds \sa \ref para_select(). + * \param timeout_tv \sa \ref para_select(). + * + * \return \sa \ref para_select(). + * + * The only difference between this function and \ref para_select() is that + * \ref i9e_select() returns zero if the select call returned \p EINTR. + */ +int i9e_select(int n, fd_set *readfds, fd_set *writefds, + struct timeval *timeout_tv) +{ + int ret = select(n, readfds, writefds, NULL, timeout_tv); + + if (ret < 0) { + if (errno == EINTR) + ret = 0; + else + ret = -ERRNO_TO_PARA_ERROR(errno); + } + return ret; +} + +/** + * Return the possible completions for a given word. + * + * \param word The word to complete. + * \param string_list All possible words in this context. + * \param result String list is returned here. + * + * This function never fails. If no completion was found, a string list of + * length zero is returned. In any case, the result must be freed by the caller + * using \ref free_argv(). + * + * This function is independent of readline and may be called before + * i9e_open(). + * + * return The number of possible completions. + */ +int i9e_extract_completions(const char *word, char **string_list, + char ***result) +{ + char **matches = para_malloc(sizeof(char *)); + int match_count = 0, matches_len = 1; + char **p; + int len = strlen(word); + + for (p = string_list; *p; p++) { + if (!is_prefix(word, *p, len)) + continue; + match_count++; + if (match_count >= matches_len) { + matches_len *= 2; + matches = para_realloc(matches, + matches_len * sizeof(char *)); + } + matches[match_count - 1] = para_strdup(*p); + } + matches[match_count] = NULL; + *result = matches; + return match_count; +} + +/** + * Return the list of partially matching words. + * + * \param word The command to complete. + * \param completers The array containing all command names. + * + * This is similar to \ref i9e_extract_completions(), but completes on the + * command names in \a completers. + * + * \return See \ref i9e_extract_completions(). + */ +char **i9e_complete_commands(const char *word, struct i9e_completer *completers) +{ + char **matches; + const char *cmd; + int i, match_count, len = strlen(word); + + /* + * In contrast to completing against an arbitrary string list, here we + * know all possible completions and expect that there will not be many + * of them. So it should be OK to iterate twice over all commands which + * simplifies the code a bit. + */ + for (i = 0, match_count = 0; (cmd = completers[i].name); i++) { + if (is_prefix(word, cmd, len)) + match_count++; + } + matches = para_malloc((match_count + 1) * sizeof(*matches)); + for (i = 0, match_count = 0; (cmd = completers[i].name); i++) + if (is_prefix(word, cmd, len)) + matches[match_count++] = para_strdup(cmd); + matches[match_count] = NULL; + return matches; +} + +/** + * Complete according to the given options. + * + * \param opts All available options. + * \param ci Information which was passed to the completer. + * \param cr Result pointer. + * + * This convenience helper can be used to complete an option. The array of all + * possible options is passed as the first argument. Flags, i.e. options + * without an argument, are expected to be listed as strings of type "-X" in \a + * opts while options which require an argument should be passed with a + * trailing "=" character like "-X=". + * + * If the word can be uniquely completed to a flag option, an additional space + * character is appended to the output. For non-flag options no space character + * is appended. + */ +void i9e_complete_option(char **opts, struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + int num_matches; + + num_matches = i9e_extract_completions(ci->word, opts, &cr->matches); + if (num_matches == 1) { + char *opt = cr->matches[0]; + char c = opt[strlen(opt) - 1]; + if (c == '=') + cr->dont_append_space = true; + } +} + +/** + * Print possible completions to stdout. + * + * \param completers The array of completion functions. + * + * At the end of the output a line starting with "-o=", followed by the + * (possibly empty) list of completion options is printed. Currently, the only + * two completion options are "nospace" and "filenames". The former indicates + * that no space should be appended even for a unique match while the latter + * indicates that usual filename completion should be performed in addition to + * the previously printed options. + * + * \return Standard. + */ +int i9e_print_completions(struct i9e_completer *completers) +{ + struct i9e_completion_result cr; + struct i9e_completion_info ci; + char *buf; + const char *end, *p; + int i, n, ret; + + reset_completion_result(&cr); + buf = getenv("COMP_POINT"); + ci.point = buf? atoi(buf) : 0; + ci.buffer = para_strdup(getenv("COMP_LINE")); + + ci.argc = create_argv(ci.buffer, " ", &ci.argv); + ci.word_num = compute_word_num(ci.buffer, " ", ci.point); + + end = ci.buffer + ci.point; + for (p = end; p > ci.buffer && *p != ' '; p--) + ; /* nothing */ + if (*p == ' ') + p++; + + n = end - p + 1; + ci.word = para_malloc(n + 1); + strncpy(ci.word, p, n); + ci.word[n] = '\0'; + + PARA_DEBUG_LOG("line: %s, point: %d (%c), wordnum: %d, word: %s\n", + ci.buffer, ci.point, ci.buffer[ci.point], ci.word_num, ci.word); + if (ci.word_num == 0) + cr.matches = i9e_complete_commands(ci.word, completers); + else + create_matches(&ci, completers, &cr); + ret = 0; + if (cr.matches && cr.matches[0]) { + for (i = 0; cr.matches[i]; i++) + printf("%s\n", cr.matches[i]); + ret = 1; + } + printf("-o="); + if (cr.dont_append_space) + printf("nospace"); + if (cr.filename_completion_desired) + printf(",filenames"); + printf("\n"); + free_argv(cr.matches); + free_argv(ci.argv); + free(ci.buffer); + free(ci.word); + return ret; +}