diff options
Diffstat (limited to 'cmd-queue.c')
-rw-r--r-- | cmd-queue.c | 903 |
1 files changed, 903 insertions, 0 deletions
diff --git a/cmd-queue.c b/cmd-queue.c new file mode 100644 index 0000000..8325e2e --- /dev/null +++ b/cmd-queue.c @@ -0,0 +1,903 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2013 Nicholas Marriott <nicholas.marriott@gmail.com> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> + +#include <ctype.h> +#include <pwd.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "tmux.h" + +/* Command queue flags. */ +#define CMDQ_FIRED 0x1 +#define CMDQ_WAITING 0x2 + +/* Command queue item type. */ +enum cmdq_type { + CMDQ_COMMAND, + CMDQ_CALLBACK, +}; + +/* Command queue item. */ +struct cmdq_item { + char *name; + struct cmdq_list *queue; + struct cmdq_item *next; + + struct client *client; + struct client *target_client; + + enum cmdq_type type; + u_int group; + + u_int number; + time_t time; + + int flags; + + struct cmdq_state *state; + struct cmd_find_state source; + struct cmd_find_state target; + + struct cmd_list *cmdlist; + struct cmd *cmd; + + cmdq_cb cb; + void *data; + + TAILQ_ENTRY(cmdq_item) entry; +}; +TAILQ_HEAD(cmdq_item_list, cmdq_item); + +/* + * Command queue state. This is the context for commands on the command queue. + * It holds information about how the commands were fired (the key and flags), + * any additional formats for the commands, and the current default target. + * Multiple commands can share the same state and a command may update the + * default target. + */ +struct cmdq_state { + int references; + int flags; + + struct format_tree *formats; + + struct key_event event; + struct cmd_find_state current; +}; + +/* Command queue. */ +struct cmdq_list { + struct cmdq_item *item; + struct cmdq_item_list list; +}; + +/* Get command queue name. */ +static const char * +cmdq_name(struct client *c) +{ + static char s[256]; + + if (c == NULL) + return ("<global>"); + if (c->name != NULL) + xsnprintf(s, sizeof s, "<%s>", c->name); + else + xsnprintf(s, sizeof s, "<%p>", c); + return (s); +} + +/* Get command queue from client. */ +static struct cmdq_list * +cmdq_get(struct client *c) +{ + static struct cmdq_list *global_queue; + + if (c == NULL) { + if (global_queue == NULL) + global_queue = cmdq_new(); + return (global_queue); + } + return (c->queue); +} + +/* Create a queue. */ +struct cmdq_list * +cmdq_new(void) +{ + struct cmdq_list *queue; + + queue = xcalloc(1, sizeof *queue); + TAILQ_INIT (&queue->list); + return (queue); +} + +/* Free a queue. */ +void +cmdq_free(struct cmdq_list *queue) +{ + if (!TAILQ_EMPTY(&queue->list)) + fatalx("queue not empty"); + free(queue); +} + +/* Get item name. */ +const char * +cmdq_get_name(struct cmdq_item *item) +{ + return (item->name); +} + +/* Get item client. */ +struct client * +cmdq_get_client(struct cmdq_item *item) +{ + return (item->client); +} + +/* Get item target client. */ +struct client * +cmdq_get_target_client(struct cmdq_item *item) +{ + return (item->target_client); +} + +/* Get item state. */ +struct cmdq_state * +cmdq_get_state(struct cmdq_item *item) +{ + return (item->state); +} + +/* Get item target. */ +struct cmd_find_state * +cmdq_get_target(struct cmdq_item *item) +{ + return (&item->target); +} + +/* Get item source. */ +struct cmd_find_state * +cmdq_get_source(struct cmdq_item *item) +{ + return (&item->source); +} + +/* Get state event. */ +struct key_event * +cmdq_get_event(struct cmdq_item *item) +{ + return (&item->state->event); +} + +/* Get state current target. */ +struct cmd_find_state * +cmdq_get_current(struct cmdq_item *item) +{ + return (&item->state->current); +} + +/* Get state flags. */ +int +cmdq_get_flags(struct cmdq_item *item) +{ + return (item->state->flags); +} + +/* Create a new state. */ +struct cmdq_state * +cmdq_new_state(struct cmd_find_state *current, struct key_event *event, + int flags) +{ + struct cmdq_state *state; + + state = xcalloc(1, sizeof *state); + state->references = 1; + state->flags = flags; + + if (event != NULL) + memcpy(&state->event, event, sizeof state->event); + else + state->event.key = KEYC_NONE; + if (current != NULL && cmd_find_valid_state(current)) + cmd_find_copy_state(&state->current, current); + else + cmd_find_clear_state(&state->current, 0); + + return (state); +} + +/* Add a reference to a state. */ +struct cmdq_state * +cmdq_link_state(struct cmdq_state *state) +{ + state->references++; + return (state); +} + +/* Make a copy of a state. */ +struct cmdq_state * +cmdq_copy_state(struct cmdq_state *state) +{ + return (cmdq_new_state(&state->current, &state->event, state->flags)); +} + +/* Free a state. */ +void +cmdq_free_state(struct cmdq_state *state) +{ + if (--state->references != 0) + return; + + if (state->formats != NULL) + format_free(state->formats); + free(state); +} + +/* Add a format to command queue. */ +void +cmdq_add_format(struct cmdq_state *state, const char *key, const char *fmt, ...) +{ + va_list ap; + char *value; + + va_start(ap, fmt); + xvasprintf(&value, fmt, ap); + va_end(ap); + + if (state->formats == NULL) + state->formats = format_create(NULL, NULL, FORMAT_NONE, 0); + format_add(state->formats, key, "%s", value); + + free(value); +} + +/* Add formats to command queue. */ +void +cmdq_add_formats(struct cmdq_state *state, struct format_tree *ft) +{ + if (state->formats == NULL) + state->formats = format_create(NULL, NULL, FORMAT_NONE, 0); + format_merge(state->formats, ft); +} + +/* Merge formats from item. */ +void +cmdq_merge_formats(struct cmdq_item *item, struct format_tree *ft) +{ + const struct cmd_entry *entry; + + if (item->cmd != NULL) { + entry = cmd_get_entry(item->cmd); + format_add(ft, "command", "%s", entry->name); + } + if (item->state->formats != NULL) + format_merge(ft, item->state->formats); +} + +/* Append an item. */ +struct cmdq_item * +cmdq_append(struct client *c, struct cmdq_item *item) +{ + struct cmdq_list *queue = cmdq_get(c); + struct cmdq_item *next; + + do { + next = item->next; + item->next = NULL; + + if (c != NULL) + c->references++; + item->client = c; + + item->queue = queue; + TAILQ_INSERT_TAIL(&queue->list, item, entry); + log_debug("%s %s: %s", __func__, cmdq_name(c), item->name); + + item = next; + } while (item != NULL); + return (TAILQ_LAST(&queue->list, cmdq_item_list)); +} + +/* Insert an item. */ +struct cmdq_item * +cmdq_insert_after(struct cmdq_item *after, struct cmdq_item *item) +{ + struct client *c = after->client; + struct cmdq_list *queue = after->queue; + struct cmdq_item *next; + + do { + next = item->next; + item->next = after->next; + after->next = item; + + if (c != NULL) + c->references++; + item->client = c; + + item->queue = queue; + TAILQ_INSERT_AFTER(&queue->list, after, item, entry); + log_debug("%s %s: %s after %s", __func__, cmdq_name(c), + item->name, after->name); + + after = item; + item = next; + } while (item != NULL); + return (after); +} + +/* Insert a hook. */ +void +cmdq_insert_hook(struct session *s, struct cmdq_item *item, + struct cmd_find_state *current, const char *fmt, ...) +{ + struct cmdq_state *state = item->state; + struct cmd *cmd = item->cmd; + struct args *args = cmd_get_args(cmd); + struct args_entry *ae; + struct args_value *av; + struct options *oo; + va_list ap; + char *name, tmp[32], flag, *arguments; + u_int i; + const char *value; + struct cmdq_item *new_item; + struct cmdq_state *new_state; + struct options_entry *o; + struct options_array_item *a; + struct cmd_list *cmdlist; + + if (item->state->flags & CMDQ_STATE_NOHOOKS) + return; + if (s == NULL) + oo = global_s_options; + else + oo = s->options; + + va_start(ap, fmt); + xvasprintf(&name, fmt, ap); + va_end(ap); + + o = options_get(oo, name); + if (o == NULL) { + free(name); + return; + } + log_debug("running hook %s (parent %p)", name, item); + + /* + * The hooks get a new state because they should not update the current + * target or formats for any subsequent commands. + */ + new_state = cmdq_new_state(current, &state->event, CMDQ_STATE_NOHOOKS); + cmdq_add_format(new_state, "hook", "%s", name); + + arguments = args_print(args); + cmdq_add_format(new_state, "hook_arguments", "%s", arguments); + free(arguments); + + for (i = 0; i < args_count(args); i++) { + xsnprintf(tmp, sizeof tmp, "hook_argument_%d", i); + cmdq_add_format(new_state, tmp, "%s", args_string(args, i)); + } + flag = args_first(args, &ae); + while (flag != 0) { + value = args_get(args, flag); + if (value == NULL) { + xsnprintf(tmp, sizeof tmp, "hook_flag_%c", flag); + cmdq_add_format(new_state, tmp, "1"); + } else { + xsnprintf(tmp, sizeof tmp, "hook_flag_%c", flag); + cmdq_add_format(new_state, tmp, "%s", value); + } + + i = 0; + av = args_first_value(args, flag); + while (av != NULL) { + xsnprintf(tmp, sizeof tmp, "hook_flag_%c_%d", flag, i); + cmdq_add_format(new_state, tmp, "%s", av->string); + i++; + av = args_next_value(av); + } + + flag = args_next(&ae); + } + + a = options_array_first(o); + while (a != NULL) { + cmdlist = options_array_item_value(a)->cmdlist; + if (cmdlist != NULL) { + new_item = cmdq_get_command(cmdlist, new_state); + if (item != NULL) + item = cmdq_insert_after(item, new_item); + else + item = cmdq_append(NULL, new_item); + } + a = options_array_next(a); + } + + cmdq_free_state(new_state); + free(name); +} + +/* Continue processing command queue. */ +void +cmdq_continue(struct cmdq_item *item) +{ + item->flags &= ~CMDQ_WAITING; +} + +/* Remove an item. */ +static void +cmdq_remove(struct cmdq_item *item) +{ + if (item->client != NULL) + server_client_unref(item->client); + if (item->cmdlist != NULL) + cmd_list_free(item->cmdlist); + cmdq_free_state(item->state); + + TAILQ_REMOVE(&item->queue->list, item, entry); + + free(item->name); + free(item); +} + +/* Remove all subsequent items that match this item's group. */ +static void +cmdq_remove_group(struct cmdq_item *item) +{ + struct cmdq_item *this, *next; + + if (item->group == 0) + return; + this = TAILQ_NEXT(item, entry); + while (this != NULL) { + next = TAILQ_NEXT(this, entry); + if (this->group == item->group) + cmdq_remove(this); + this = next; + } +} + +/* Empty command callback. */ +static enum cmd_retval +cmdq_empty_command(__unused struct cmdq_item *item, __unused void *data) +{ + return (CMD_RETURN_NORMAL); +} + +/* Get a command for the command queue. */ +struct cmdq_item * +cmdq_get_command(struct cmd_list *cmdlist, struct cmdq_state *state) +{ + struct cmdq_item *item, *first = NULL, *last = NULL; + struct cmd *cmd; + const struct cmd_entry *entry; + int created = 0; + + if ((cmd = cmd_list_first(cmdlist)) == NULL) + return (cmdq_get_callback(cmdq_empty_command, NULL)); + + if (state == NULL) { + state = cmdq_new_state(NULL, NULL, 0); + created = 1; + } + + while (cmd != NULL) { + entry = cmd_get_entry(cmd); + + item = xcalloc(1, sizeof *item); + xasprintf(&item->name, "[%s/%p]", entry->name, item); + item->type = CMDQ_COMMAND; + + item->group = cmd_get_group(cmd); + item->state = cmdq_link_state(state); + + item->cmdlist = cmdlist; + item->cmd = cmd; + + cmdlist->references++; + log_debug("%s: %s group %u", __func__, item->name, item->group); + + if (first == NULL) + first = item; + if (last != NULL) + last->next = item; + last = item; + + cmd = cmd_list_next(cmd); + } + + if (created) + cmdq_free_state(state); + return (first); +} + +/* Fill in flag for a command. */ +static enum cmd_retval +cmdq_find_flag(struct cmdq_item *item, struct cmd_find_state *fs, + const struct cmd_entry_flag *flag) +{ + const char *value; + + if (flag->flag == 0) { + cmd_find_from_client(fs, item->target_client, 0); + return (CMD_RETURN_NORMAL); + } + + value = args_get(cmd_get_args(item->cmd), flag->flag); + if (cmd_find_target(fs, item, value, flag->type, flag->flags) != 0) { + cmd_find_clear_state(fs, 0); + return (CMD_RETURN_ERROR); + } + return (CMD_RETURN_NORMAL); +} + +/* Add message with command. */ +static void +cmdq_add_message(struct cmdq_item *item) +{ + struct client *c = item->client; + struct cmdq_state *state = item->state; + const char *key; + char *tmp; + uid_t uid; + struct passwd *pw; + char *user = NULL; + + tmp = cmd_print(item->cmd); + if (c != NULL) { + uid = proc_get_peer_uid(c->peer); + if (uid != (uid_t)-1 && uid != getuid()) { + if ((pw = getpwuid(uid)) != NULL) + xasprintf(&user, "[%s]", pw->pw_name); + else + user = xstrdup("[unknown]"); + } else + user = xstrdup(""); + if (c->session != NULL && state->event.key != KEYC_NONE) { + key = key_string_lookup_key(state->event.key, 0); + server_add_message("%s%s key %s: %s", c->name, user, + key, tmp); + } else { + server_add_message("%s%s command: %s", c->name, user, + tmp); + } + free(user); + } else + server_add_message("command: %s", tmp); + free(tmp); +} + +/* Fire command on command queue. */ +static enum cmd_retval +cmdq_fire_command(struct cmdq_item *item) +{ + const char *name = cmdq_name(item->client); + struct cmdq_state *state = item->state; + struct cmd *cmd = item->cmd; + struct args *args = cmd_get_args(cmd); + const struct cmd_entry *entry = cmd_get_entry(cmd); + struct client *tc, *saved = item->client; + enum cmd_retval retval; + struct cmd_find_state *fsp, fs; + int flags, quiet = 0; + char *tmp; + + if (cfg_finished) + cmdq_add_message(item); + if (log_get_level() > 1) { + tmp = cmd_print(cmd); + log_debug("%s %s: (%u) %s", __func__, name, item->group, tmp); + free(tmp); + } + + flags = !!(state->flags & CMDQ_STATE_CONTROL); + cmdq_guard(item, "begin", flags); + + if (item->client == NULL) + item->client = cmd_find_client(item, NULL, 1); + + if (entry->flags & CMD_CLIENT_CANFAIL) + quiet = 1; + if (entry->flags & CMD_CLIENT_CFLAG) { + tc = cmd_find_client(item, args_get(args, 'c'), quiet); + if (tc == NULL && !quiet) { + retval = CMD_RETURN_ERROR; + goto out; + } + } else if (entry->flags & CMD_CLIENT_TFLAG) { + tc = cmd_find_client(item, args_get(args, 't'), quiet); + if (tc == NULL && !quiet) { + retval = CMD_RETURN_ERROR; + goto out; + } + } else + tc = cmd_find_client(item, NULL, 1); + item->target_client = tc; + + retval = cmdq_find_flag(item, &item->source, &entry->source); + if (retval == CMD_RETURN_ERROR) + goto out; + retval = cmdq_find_flag(item, &item->target, &entry->target); + if (retval == CMD_RETURN_ERROR) + goto out; + + retval = entry->exec(cmd, item); + if (retval == CMD_RETURN_ERROR) + goto out; + + if (entry->flags & CMD_AFTERHOOK) { + if (cmd_find_valid_state(&item->target)) + fsp = &item->target; + else if (cmd_find_valid_state(&item->state->current)) + fsp = &item->state->current; + else if (cmd_find_from_client(&fs, item->client, 0) == 0) + fsp = &fs; + else + goto out; + cmdq_insert_hook(fsp->s, item, fsp, "after-%s", entry->name); + } + +out: + item->client = saved; + if (retval == CMD_RETURN_ERROR) + cmdq_guard(item, "error", flags); + else + cmdq_guard(item, "end", flags); + return (retval); +} + +/* Get a callback for the command queue. */ +struct cmdq_item * +cmdq_get_callback1(const char *name, cmdq_cb cb, void *data) +{ + struct cmdq_item *item; + + item = xcalloc(1, sizeof *item); + xasprintf(&item->name, "[%s/%p]", name, item); + item->type = CMDQ_CALLBACK; + + item->group = 0; + item->state = cmdq_new_state(NULL, NULL, 0); + + item->cb = cb; + item->data = data; + + return (item); +} + +/* Generic error callback. */ +static enum cmd_retval +cmdq_error_callback(struct cmdq_item *item, void *data) +{ + char *error = data; + + cmdq_error(item, "%s", error); + free(error); + + return (CMD_RETURN_NORMAL); +} + +/* Get an error callback for the command queue. */ +struct cmdq_item * +cmdq_get_error(const char *error) +{ + return (cmdq_get_callback(cmdq_error_callback, xstrdup(error))); +} + +/* Fire callback on callback queue. */ +static enum cmd_retval +cmdq_fire_callback(struct cmdq_item *item) +{ + return (item->cb(item, item->data)); +} + +/* Process next item on command queue. */ +u_int +cmdq_next(struct client *c) +{ + struct cmdq_list *queue = cmdq_get(c); + const char *name = cmdq_name(c); + struct cmdq_item *item; + enum cmd_retval retval; + u_int items = 0; + static u_int number; + + if (TAILQ_EMPTY(&queue->list)) { + log_debug("%s %s: empty", __func__, name); + return (0); + } + if (TAILQ_FIRST(&queue->list)->flags & CMDQ_WAITING) { + log_debug("%s %s: waiting", __func__, name); + return (0); + } + + log_debug("%s %s: enter", __func__, name); + for (;;) { + item = queue->item = TAILQ_FIRST(&queue->list); + if (item == NULL) + break; + log_debug("%s %s: %s (%d), flags %x", __func__, name, + item->name, item->type, item->flags); + + /* + * Any item with the waiting flag set waits until an external + * event clears the flag (for example, a job - look at + * run-shell). + */ + if (item->flags & CMDQ_WAITING) + goto waiting; + + /* + * Items are only fired once, once the fired flag is set, a + * waiting flag can only be cleared by an external event. + */ + if (~item->flags & CMDQ_FIRED) { + item->time = time(NULL); + item->number = ++number; + + switch (item->type) { + case CMDQ_COMMAND: + retval = cmdq_fire_command(item); + + /* + * If a command returns an error, remove any + * subsequent commands in the same group. + */ + if (retval == CMD_RETURN_ERROR) + cmdq_remove_group(item); + break; + case CMDQ_CALLBACK: + retval = cmdq_fire_callback(item); + break; + default: + retval = CMD_RETURN_ERROR; + break; + } + item->flags |= CMDQ_FIRED; + + if (retval == CMD_RETURN_WAIT) { + item->flags |= CMDQ_WAITING; + goto waiting; + } + items++; + } + cmdq_remove(item); + } + queue->item = NULL; + + log_debug("%s %s: exit (empty)", __func__, name); + return (items); + +waiting: + log_debug("%s %s: exit (wait)", __func__, name); + return (items); +} + +/* Get running item if any. */ +struct cmdq_item * +cmdq_running(struct client *c) +{ + struct cmdq_list *queue = cmdq_get(c); + + if (queue->item == NULL) + return (NULL); + if (queue->item->flags & CMDQ_WAITING) + return (NULL); + return (queue->item); +} + +/* Print a guard line. */ +void +cmdq_guard(struct cmdq_item *item, const char *guard, int flags) +{ + struct client *c = item->client; + long t = item->time; + u_int number = item->number; + + if (c != NULL && (c->flags & CLIENT_CONTROL)) + control_write(c, "%%%s %ld %u %d", guard, t, number, flags); +} + +/* Show message from command. */ +void +cmdq_print(struct cmdq_item *item, const char *fmt, ...) +{ + struct client *c = item->client; + struct window_pane *wp; + struct window_mode_entry *wme; + va_list ap; + char *tmp, *msg; + + va_start(ap, fmt); + xvasprintf(&msg, fmt, ap); + va_end(ap); + + log_debug("%s: %s", __func__, msg); + + if (c == NULL) + /* nothing */; + else if (c->session == NULL || (c->flags & CLIENT_CONTROL)) { + if (~c->flags & CLIENT_UTF8) { + tmp = msg; + msg = utf8_sanitize(tmp); + free(tmp); + } + if (c->flags & CLIENT_CONTROL) + control_write(c, "%s", msg); + else + file_print(c, "%s\n", msg); + } else { + wp = server_client_get_pane(c); + wme = TAILQ_FIRST(&wp->modes); + if (wme == NULL || wme->mode != &window_view_mode) { + window_pane_set_mode(wp, NULL, &window_view_mode, NULL, + NULL); + } + window_copy_add(wp, 0, "%s", msg); + } + + free(msg); +} + +/* Show error from command. */ +void +cmdq_error(struct cmdq_item *item, const char *fmt, ...) +{ + struct client *c = item->client; + struct cmd *cmd = item->cmd; + va_list ap; + char *msg, *tmp; + const char *file; + u_int line; + + va_start(ap, fmt); + xvasprintf(&msg, fmt, ap); + va_end(ap); + + log_debug("%s: %s", __func__, msg); + + if (c == NULL) { + cmd_get_source(cmd, &file, &line); + cfg_add_cause("%s:%u: %s", file, line, msg); + } else if (c->session == NULL || (c->flags & CLIENT_CONTROL)) { + server_add_message("%s message: %s", c->name, msg); + if (~c->flags & CLIENT_UTF8) { + tmp = msg; + msg = utf8_sanitize(tmp); + free(tmp); + } + if (c->flags & CLIENT_CONTROL) + control_write(c, "%s", msg); + else + file_error(c, "%s\n", msg); + c->retval = 1; + } else { + *msg = toupper((u_char) *msg); + status_message_set(c, -1, 1, 0, "%s", msg); + } + + free(msg); +} |