From 367daa451bd837c3e267c8385bbc92898f06ecca Mon Sep 17 00:00:00 2001 From: Andre Noll Date: Mon, 5 Sep 2011 15:47:34 +0200 Subject: [PATCH] client: Implement interactive mode. This makes para_client enter an interactive session when started with no command. Command line history and command completion are available in interactive sessions. This populates the previously empty files interactive.h and interactive.c which contain the readline/interactive specific part. Everything in these files is independent of para_client. Conversely, client.c and client_common.c are independent of readline. The public API defined in interactive.h was designed to be reused from other applications. In fact, a subsequent commit changes para_audioc to offer interactive sessions as well. --- client.c | 535 +++++++++++++++++++++++++++++++++++- error.h | 5 +- ggo/client.m4 | 3 + ggo/complete.m4 | 12 + ggo/history_file.m4 | 12 + ggo/makefile | 2 +- interactive.c | 640 ++++++++++++++++++++++++++++++++++++++++++++ interactive.h | 88 ++++++ 8 files changed, 1283 insertions(+), 14 deletions(-) create mode 100644 ggo/complete.m4 create mode 100644 ggo/history_file.m4 diff --git a/client.c b/client.c index 038fc013..c6fde490 100644 --- a/client.c +++ b/client.c @@ -8,6 +8,7 @@ #include #include +#include #include "para.h" #include "list.h" @@ -20,6 +21,7 @@ #include "client.h" #include "buffer_tree.h" #include "error.h" +#include "version.h" INIT_CLIENT_ERRLISTS; @@ -28,6 +30,513 @@ static struct client_task *ct; static struct stdin_task sit; static struct stdout_task sot; +static int client_loglevel = LL_ERROR; +DEFINE_STDERR_LOGGER(stderr_log, client_loglevel); +__printf_2_3 void (*para_log)(int, const char*, ...) = stderr_log; + +#ifdef HAVE_READLINE +#include "interactive.h" +#include "server_completion.h" +#include "afs_completion.h" + +struct exec_task { + struct task task; + struct btr_node *btrn; + char *result_buf; + size_t result_size; +}; + +static void exec_pre_select(struct sched *s, struct task *t) +{ + struct exec_task *et = container_of(t, struct exec_task, task); + int ret = btr_node_status(et->btrn, 0, BTR_NT_LEAF); + + if (ret != 0) + sched_min_delay(s); +} + +static void exec_post_select(__a_unused struct sched *s, struct task *t) +{ + struct exec_task *et = container_of(t, struct exec_task, task); + struct btr_node *btrn = et->btrn; + char *buf; + size_t sz; + int ret; + + ret = btr_node_status(btrn, 0, BTR_NT_LEAF); + if (ret <= 0) { + t->error = ret; + return; + } + sz = btr_next_buffer(btrn, &buf); + if (sz <= 1) + goto out; + et->result_buf = para_realloc(et->result_buf, et->result_size + sz - 1); + memcpy(et->result_buf + et->result_size - 1, buf, sz - 1); + et->result_size += sz - 1; + et->result_buf[et->result_size - 1] = '\0'; +out: + btr_consume(btrn, sz); +} + +static int make_client_argv(const char *line) +{ + int ret; + + free_argv(ct->conf.inputs); + ret = create_argv(line, " ", &ct->conf.inputs); + if (ret >= 0) + ct->conf.inputs_num = ret; + return ret; +} + +static int execute_client_command(const char *cmd, char **result) +{ + int ret; + struct sched command_sched = {.default_timeout = {.tv_sec = 1}}; + struct exec_task exec_task = { + .task = { + .pre_select = exec_pre_select, + .post_select = exec_post_select, + .status = "client exec task", + }, + .result_buf = para_strdup(""), + .result_size = 1, + }; + *result = NULL; + ret = make_client_argv(cmd); + if (ret < 0) + goto out; + exec_task.btrn = btr_new_node(&(struct btr_node_description) + EMBRACE(.name = "exec_collect")); + register_task(&command_sched, &exec_task.task); + ret = client_connect(ct, &command_sched, NULL, exec_task.btrn); + if (ret < 0) + goto out; + schedule(&command_sched); + *result = exec_task.result_buf; + btr_remove_node(exec_task.btrn); + client_disconnect(ct); + ret = 1; +out: + btr_free_node(exec_task.btrn); + if (ret < 0) + free(exec_task.result_buf); + return ret; +} + +static int extract_matches_from_command(const char *word, char *cmd, + char ***matches) +{ + char *buf, **sl; + int ret; + + ret = execute_client_command(cmd, &buf); + if (ret < 0) + return ret; + ret = create_argv(buf, "\n", &sl); + free(buf); + if (ret < 0) + return ret; + ret = i9e_extract_completions(word, sl, matches); + free_argv(sl); + return ret; +} + +static int complete_attributes(const char *word, char ***matches) +{ + return extract_matches_from_command(word, "lsatt", matches); +} + +static void complete_addblob(__a_unused const char *blob_type, + __a_unused struct i9e_completion_info *ci, + __a_unused struct i9e_completion_result *cr) +{ + cr->filename_completion_desired = true; +} + +static void generic_blob_complete(const char *blob_type, + struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + char cmd[20]; + sprintf(cmd, "ls%s", blob_type); + extract_matches_from_command(ci->word, cmd, &cr->matches); +} + +static void complete_catblob(const char *blob_type, + struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + generic_blob_complete(blob_type, ci, cr); +} + +static void complete_lsblob(const char *blob_type, + struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + char *opts[] = {"-i", "-l", "-r", NULL}; + + if (ci->word[0] == '-') + return i9e_complete_option(opts, ci, cr); + generic_blob_complete(blob_type, ci, cr); +} + +static void complete_rmblob(const char *blob_type, + struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + generic_blob_complete(blob_type, ci, cr); +} + +static void complete_mvblob(const char *blob_type, + struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + generic_blob_complete(blob_type, ci, cr); +} + +/* these don't need any completions */ +I9E_DUMMY_COMPLETER(ff); +I9E_DUMMY_COMPLETER(hup); +I9E_DUMMY_COMPLETER(jmp); +I9E_DUMMY_COMPLETER(next); +I9E_DUMMY_COMPLETER(nomore); +I9E_DUMMY_COMPLETER(pause); +I9E_DUMMY_COMPLETER(play); +I9E_DUMMY_COMPLETER(si); +I9E_DUMMY_COMPLETER(term); +I9E_DUMMY_COMPLETER(version); +I9E_DUMMY_COMPLETER(stop); +I9E_DUMMY_COMPLETER(addatt); +I9E_DUMMY_COMPLETER(init); + +static struct i9e_completer completers[]; + +static void help_completer(struct i9e_completion_info *ci, + struct i9e_completion_result *result) +{ + result->matches = i9e_complete_commands(ci->word, completers); +} + +static void stat_completer(struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + char *opts[] = {"-n=", "-p", NULL}; + //PARA_CRIT_LOG("word: %s\n", ci->word); + i9e_complete_option(opts, ci, cr); +} + +static void sender_completer(struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + char *senders[] = {"http", "dccp", "udp", NULL}; + char *http_cmds[] = {"on", "off", "allow", "deny", "help", NULL}; + char *dccp_cmds[] = {"on", "off", "allow", "deny", "help", NULL}; + char *udp_cmds[] ={"on", "off", "add", "delete", "help", NULL}; + char *sender; + char **cmds; + + //PARA_CRIT_LOG("wn: %d\n", ci->word_num); + if (ci->word_num == 0 || ci->word_num > 3) + return; + if (ci->word_num == 1 || (ci->word_num == 2 && *ci->word != '\0')) { + i9e_extract_completions(ci->word, senders, &cr->matches); + return; + } + sender = ci->argv[1]; + //PARA_CRIT_LOG("sender: %s\n", sender); + if (strcmp(sender, "http") == 0) + cmds = http_cmds; + else if (strcmp(sender, "dccp") == 0) + cmds = dccp_cmds; + else if (strcmp(sender, "udp") == 0) + cmds = udp_cmds; + else + return; + i9e_extract_completions(ci->word, cmds, &cr->matches); +} + +static void add_completer(struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + char *opts[] = {"-a", "-l", "-f", "-v", "--", NULL}; + + if (ci->word[0] == '-') + i9e_complete_option(opts, ci, cr); + cr->filename_completion_desired = true; +} + +static void ls_completer(struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + char *opts[] = { + "--", "-l", "-ls", "-ll", "-lv", "-lp", "-lm", "-lc", "-p", + "-a", "-r", "-d", "-sp", "-sl", "-ss", "-sn", "-sf", "-sc", + "-si", "-sy", "-sb", "-sd", "-sa", NULL + }; + if (ci->word[0] == '-') + i9e_complete_option(opts, ci, cr); + cr->filename_completion_desired = true; +} + +static void setatt_completer(struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + char *buf, **sl; + int i, ret, num_atts; + + if (ci->word_num == 0) + return; + + if (*ci->word == '/' || *ci->word == '\0') + cr->filename_completion_desired = true; + if (*ci->word == '/') + return; + ret = execute_client_command("lsatt", &buf); + if (ret < 0) + return; + ret = create_argv(buf, "\n", &sl); + if (ret < 0) + goto out; + num_atts = ret; + sl = para_realloc(sl, (2 * num_atts + 1) * sizeof(char *)); + for (i = 0; i < num_atts; i++) { + char *orig = sl[i]; + sl[i] = make_message("%s+", orig); + sl[num_atts + i] = make_message("%s-", orig); + free(orig); + } + sl[2 * num_atts] = NULL; + ret = i9e_extract_completions(ci->word, sl, &cr->matches); +out: + free(buf); + free_argv(sl); +} + +static void lsatt_completer(struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + char *opts[] = {"-i", "-l", "-r", NULL}; + + if (ci->word[0] == '-') + i9e_complete_option(opts, ci, cr); + else + complete_attributes(ci->word, &cr->matches); +} + +static void mvatt_completer(struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + complete_attributes(ci->word, &cr->matches); +} + +static void rmatt_completer(struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + complete_attributes(ci->word, &cr->matches); +} + +static void check_completer(struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + char *opts[] = {"-a", "-m", "-p", NULL}; + i9e_complete_option(opts, ci, cr); +} + +static void rm_completer(struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + char *opts[] = {"-v", "-f", "-p", NULL}; + + if (ci->word[0] == '-') { + i9e_complete_option(opts, ci, cr); + return; + } + cr->filename_completion_desired = true; +} + +static void touch_completer(struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + char *opts[] = {"-n=", "-l=", "-y=", "-i=", "-a=", "-v", "-p", NULL}; + + if (ci->word[0] == '-') + i9e_complete_option(opts, ci, cr); + cr->filename_completion_desired = true; +} + +static void cpsi_completer(struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + char *opts[] = {"-a", "-y", "-i", "-l", "-n", "-v", NULL}; + + if (ci->word[0] == '-') + i9e_complete_option(opts, ci, cr); + cr->filename_completion_desired = true; +} + +static void select_completer(struct i9e_completion_info *ci, + struct i9e_completion_result *cr) +{ + char *mood_buf, *pl_buf, **moods, **playlists, **mops; + int num_moods, num_pl, i, n, ret; + + ret = execute_client_command("lsmood", &mood_buf); + if (ret < 0) + return; + ret = execute_client_command("lspl", &pl_buf); + if (ret < 0) + goto free_mood_buf; + + ret = create_argv(mood_buf, "\n", &moods); + if (ret < 0) + goto free_pl_buf; + num_moods = ret; + ret = create_argv(pl_buf, "\n", &playlists); + if (ret < 0) + goto free_moods; + num_pl = ret; + n = num_moods + num_pl; + mops = para_malloc((n + 1) * sizeof(char *)); + for (i = 0; i < num_moods; i++) + mops[i] = make_message("m/%s", moods[i]); + for (i = 0; i < num_pl; i++) + mops[num_moods + i] = make_message("p/%s", playlists[i]); + mops[n] = NULL; + i9e_extract_completions(ci->word, mops, &cr->matches); + free_argv(mops); + free_argv(playlists); +free_moods: + free_argv(moods); +free_pl_buf: + free(pl_buf); +free_mood_buf: + free(mood_buf); +} + +#define DEFINE_BLOB_COMPLETER(cmd, blob_type) \ + static void cmd ## blob_type ## _completer( \ + struct i9e_completion_info *ci, \ + struct i9e_completion_result *cr) \ + {complete_ ## cmd ## blob(#blob_type, ci, cr);} + +DEFINE_BLOB_COMPLETER(add, mood) +DEFINE_BLOB_COMPLETER(add, lyr) +DEFINE_BLOB_COMPLETER(add, img) +DEFINE_BLOB_COMPLETER(add, pl) +DEFINE_BLOB_COMPLETER(cat, mood) +DEFINE_BLOB_COMPLETER(cat, lyr) +DEFINE_BLOB_COMPLETER(cat, img) +DEFINE_BLOB_COMPLETER(cat, pl) +DEFINE_BLOB_COMPLETER(ls, mood) +DEFINE_BLOB_COMPLETER(ls, lyr) +DEFINE_BLOB_COMPLETER(ls, img) +DEFINE_BLOB_COMPLETER(ls, pl) +DEFINE_BLOB_COMPLETER(rm, mood) +DEFINE_BLOB_COMPLETER(rm, lyr) +DEFINE_BLOB_COMPLETER(rm, img) +DEFINE_BLOB_COMPLETER(rm, pl) +DEFINE_BLOB_COMPLETER(mv, mood) +DEFINE_BLOB_COMPLETER(mv, lyr) +DEFINE_BLOB_COMPLETER(mv, img) +DEFINE_BLOB_COMPLETER(mv, pl) + +static int client_i9e_line_handler(char *line) +{ + int ret; + + client_disconnect(ct); + if (!line || !*line) + return 0; + PARA_DEBUG_LOG("line handler: %s\n", line); + ret = make_client_argv(line); + if (ret < 0) + return ret; + ret = client_connect(ct, &sched, NULL, NULL); + if (ret < 0) + return ret; + i9e_attach_to_stdout(ct->btrn); + return 1; +} + +static void client_sighandler(int s) +{ + i9e_signal_dispatch(s); +} + +static struct i9e_completer completers[] = { + SERVER_COMPLETERS + AFS_COMPLETERS + {.name = NULL} +}; + +__noreturn static void interactive_session(void) +{ + int ret; + char *history_file; + struct sigaction act; + struct i9e_client_info ici = { + .fds = {0, 1, 2}, + .prompt = "para_client> ", + .line_handler = client_i9e_line_handler, + .loglevel = client_loglevel, + .completers = completers, + }; + + PARA_NOTICE_LOG("\n%s\n", VERSION_TEXT("client")); + if (ct->conf.history_file_given) + history_file = para_strdup(ct->conf.history_file_arg); + else { + char *home = para_homedir(); + history_file = make_message("%s/.paraslash/client.history", + home); + free(home); + } + ici.history_file = history_file; + + act.sa_handler = client_sighandler; + sigemptyset(&act.sa_mask); + act.sa_flags = 0; + sigaction(SIGINT, &act, NULL); + sched.select_function = i9e_select; + + ret = i9e_open(&ici, &sched); + if (ret < 0) + goto out; + para_log = i9e_log; + ret = schedule(&sched); + i9e_close(); + para_log = stderr_log; +out: + if (ret < 0) + PARA_ERROR_LOG("%s\n", para_strerror(-ret)); + exit(ret < 0? EXIT_FAILURE : EXIT_SUCCESS); + //client_close(ct); +} + +__noreturn static void print_completions(void) +{ + int ret = i9e_print_completions(completers); + exit(ret <= 0? EXIT_FAILURE : EXIT_SUCCESS); +} + +#else /* HAVE_READLINE */ + +__noreturn static void interactive_session(void) +{ + PARA_EMERG_LOG("interactive sessions not available\n"); + exit(EXIT_FAILURE); +} + +__noreturn static void print_completions(void) +{ + PARA_EMERG_LOG("command completion not available\n"); + exit(EXIT_FAILURE); +} + +#endif /* HAVE_READLINE */ + static void supervisor_post_select(struct sched *s, struct task *t) { if (ct->task.error < 0) { @@ -43,8 +552,7 @@ static void supervisor_post_select(struct sched *s, struct task *t) if (ct->status == CL_RECEIVING) { stdout_set_defaults(&sot); register_task(s, &sot.task); - t->error = -E_TASK_STARTED; - return; + t->error = -E_TASK_STARTED; return; } } @@ -53,18 +561,16 @@ static struct task svt = { .status = "supervisor task" }; -static int client_loglevel = LL_ERROR; /* loglevel */ -INIT_STDERR_LOGGING(client_loglevel); - /** * The client program to connect to para_server. * * \param argc Usual argument count. * \param argv Usual argument vector. * - * It registers two tasks: The client task that communicates with para_server - * and the supervisor task that minitors whether the client task intends to - * read from stdin or write to stdout. + * When called without a paraslash command, an interactive session is started. + * Otherwise, the client task and the supervisor task are started. The former + * communicates with para_server while the latter monitors whether the client + * task intends to read from stdin or write to stdout. * * Once it has been determined whether the client command corresponds to a * stdin command (addmood, addimg, ..), either the stdin task or the stdout @@ -76,13 +582,18 @@ INIT_STDERR_LOGGING(client_loglevel); */ int main(int argc, char *argv[]) { - int ret; - static struct sched s; init_random_seed_or_die(); - s.default_timeout.tv_sec = 1; - s.default_timeout.tv_usec = 0; + sched.default_timeout.tv_sec = 1; + + ret = client_parse_config(argc, argv, &ct, &client_loglevel); + if (ret < 0) + goto out; + if (ct->conf.complete_given) + print_completions(); + if (ret == 0) + interactive_session(); /* does not return */ /* * We add buffer tree nodes for stdin and stdout even though diff --git a/error.h b/error.h index ab7aed25..8a0a9c2e 100644 --- a/error.h +++ b/error.h @@ -440,7 +440,10 @@ extern const char **para_errlist[]; #define CHUNK_QUEUE_ERRORS \ PARA_ERROR(QUEUE, "packet queue overrun"), \ -#define INTERACTIVE_ERRORS + +#define INTERACTIVE_ERRORS \ + PARA_ERROR(I9E_EOF, "end of input"), \ + PARA_ERROR(I9E_SETUPTERM, "failed to set up terminal"), \ /** \endcond errors */ diff --git a/ggo/client.m4 b/ggo/client.m4 index 5770ff0a..8ade5089 100644 --- a/ggo/client.m4 +++ b/ggo/client.m4 @@ -1,6 +1,7 @@ include(header.m4) define(CURRENT_PROGRAM,para_client) define(DEFAULT_CONFIG_FILE,~/.paraslash/client.conf) +define(DEFAULT_HISTORY_FILE,~/.paraslash/client.history) args "--no-handle-error" option "hostname" i "ip or host to connect" string typestr="host" default="localhost" optional @@ -11,3 +12,5 @@ option "key_file" k "(default='~/.paraslash/key.')" string typestr="filena include(loglevel.m4) include(config_file.m4) +include(history_file.m4) +include(complete.m4) diff --git a/ggo/complete.m4 b/ggo/complete.m4 new file mode 100644 index 00000000..14e737cf --- /dev/null +++ b/ggo/complete.m4 @@ -0,0 +1,12 @@ + +option "complete" - +#~~~~~~~~~~~~~~~~~~ +"print possible command line completions" + flag off + details = " + If this flag is given, CURRENT_PROGRAM reads the environment + variables COMP_LINE and COMP_POINT to obtain the current command line + and the cursor position respectively, prints possible completions + to stdout and exits. +" + diff --git a/ggo/history_file.m4 b/ggo/history_file.m4 new file mode 100644 index 00000000..932d88bf --- /dev/null +++ b/ggo/history_file.m4 @@ -0,0 +1,12 @@ + +option "history_file" - +#~~~~~~~~~~~~~~~~~~~~~~ +"(default='DEFAULT_HISTORY_FILE')" +string typestr = "filename" +optional +details = " + If CURRENT_PROGRAM runs in interactive mode, it reads the history + file on startup. Upon exit, the in-memory history is appended + to the history file. +" + diff --git a/ggo/makefile b/ggo/makefile index bcc172b2..367bd377 100644 --- a/ggo/makefile +++ b/ggo/makefile @@ -60,7 +60,7 @@ $(ggo_dir)/fsck.ggo: $(ggo_dir)/loglevel.m4 $(ggo_dir)/gui.ggo: $(ggo_dir)/loglevel.m4 $(ggo_dir)/recv.ggo: $(ggo_dir)/loglevel.m4 $(ggo_dir)/write.ggo: $(ggo_dir)/loglevel.m4 -$(ggo_dir)/client.ggo: $(ggo_dir)/loglevel.m4 $(ggo_dir)/config_file.m4 +$(ggo_dir)/client.ggo: $(ggo_dir)/loglevel.m4 $(ggo_dir)/config_file.m4 $(ggo_dir)/history_file.m4 $(ggo_dir)/complete.m4 $(ggo_dir)/%.ggo: $(ggo_dir)/%.m4 $(ggo_dir)/header.m4 @[ -z "$(Q)" ] || echo 'M4 $<' diff --git a/interactive.c b/interactive.c index e69de29b..32047755 100644 --- a/interactive.c +++ b/interactive.c @@ -0,0 +1,640 @@ +/* + * Copyright (C) 2011 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; +} diff --git a/interactive.h b/interactive.h index e69de29b..be42c91f 100644 --- a/interactive.h +++ b/interactive.h @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2011 Andre Noll + * + * Licensed under the GPL v2. For licencing details see COPYING. + */ + +/** \file interactive.h Public API for interactive sessions. */ + +/* Interactive is hard to spell, lets' write i9e. */ + +/** Structure passed as input to the completers. */ +struct i9e_completion_info { + char *buffer; /**< The full line. */ + char *word; /**< The word the cursor is in. */ + int point; /**< Cursor position. */ + char **argv; /**< Vector of words in \a buffer. */ + int argc; /**< Number of elements(words) in argv. */ + int word_num; /**< The cursor is on this word. */ +}; + +/** Completion information returned by the completers. */ +struct i9e_completion_result { + /** NULL-terminated array of possible completions. */ + char **matches; + /** Whether standard filename completion should be performed. */ + bool filename_completion_desired; + /** Suppress adding a space character after the completed word. */ + bool dont_append_space; +}; + +/** + * Define a completer which does nothing. + * + * \param name Determines the name of the function to be defined. + */ +#define I9E_DUMMY_COMPLETER(name) void name ## _completer( \ + __a_unused struct i9e_completion_info *ciname, \ + struct i9e_completion_result *result) {result->matches = NULL;} + +/** + * A completer is simply a function pointer and name of the command for which + * it performs completion. + */ +struct i9e_completer { + /** The command for which this completer provides completion. */ + const char *name; + /** The completer returns all possible completions via the second parameter. */ + void (*completer)(struct i9e_completion_info *, struct i9e_completion_result *); +}; + +/** + * The i9e configuration settings of the client. + * + * A structure of this type must be allocated and filled in by the client + * before it is passed to the i9e subsystem via \ref i9e_open(). + */ +struct i9e_client_info { + /** Threshold for i9e_log(). */ + int loglevel; + /** Complete input lines are passed to this callback function. */ + int (*line_handler)(char *line); + /** File descriptors to use for input/output/log. */ + int fds[3]; + /** Text of the current prompt. */ + char *prompt; + /** Where to store the readline history. */ + char *history_file; + /** + * The array of completers, one per command. This is used for + * completing the first word (the command) and for calling the right + * completer if the cursor is not on the first word. + */ + struct i9e_completer *completers; +}; + +int i9e_open(struct i9e_client_info *ici, struct sched *s); +void i9e_attach_to_stdout(struct btr_node *producer); +void i9e_close(void); +void i9e_signal_dispatch(int sig_num); +__printf_2_3 void i9e_log(int ll, const char* fmt,...); +int i9e_select(int n, fd_set *readfds, fd_set *writefds, + struct timeval *timeout_tv); +int i9e_extract_completions(const char *word, char **string_list, + char ***result); +char **i9e_complete_commands(const char *word, struct i9e_completer *completers); +void i9e_complete_option(char **opts, struct i9e_completion_info *ci, + struct i9e_completion_result *cr); +int i9e_print_completions(struct i9e_completer *completers); -- 2.30.2