/* $OpenBSD: cmd-queue.c,v 1.116 2024/04/23 13:34:51 jsg Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott * * 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 #include #include #include #include #include #include #include #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 (""); 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, struct cmd_find_state *current) { if (current != NULL) return (cmdq_new_state(current, &state->event, state->flags)); 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_data(struct cmdq_item *item, int parse, struct evbuffer *evb) { server_client_print(item->client, parse, evb); } /* Show message from command. */ void cmdq_print(struct cmdq_item *item, const char *fmt, ...) { va_list ap; struct evbuffer *evb; evb = evbuffer_new(); if (evb == NULL) fatalx("out of memory"); va_start(ap, fmt); evbuffer_add_vprintf(evb, fmt, ap); va_end(ap); cmdq_print_data(item, 0, evb); evbuffer_free(evb); } /* 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); }