summaryrefslogtreecommitdiffstats
path: root/control.c
diff options
context:
space:
mode:
Diffstat (limited to 'control.c')
-rw-r--r--control.c1107
1 files changed, 1107 insertions, 0 deletions
diff --git a/control.c b/control.c
new file mode 100644
index 0000000..73286e0
--- /dev/null
+++ b/control.c
@@ -0,0 +1,1107 @@
+/* $OpenBSD$ */
+
+/*
+ * Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com>
+ * Copyright (c) 2012 George Nachman <tmux@georgester.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 <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "tmux.h"
+
+/*
+ * Block of data to output. Each client has one "all" queue of blocks and
+ * another queue for each pane (in struct client_offset). %output blocks are
+ * added to both queues and other output lines (notifications) added only to
+ * the client queue.
+ *
+ * When a client becomes writeable, data from blocks on the pane queue are sent
+ * up to the maximum size (CLIENT_BUFFER_HIGH). If a block is entirely written,
+ * it is removed from both pane and client queues and if this means non-%output
+ * blocks are now at the head of the client queue, they are written.
+ *
+ * This means a %output block holds up any subsequent non-%output blocks until
+ * it is written which enforces ordering even if the client cannot accept the
+ * entire block in one go.
+ */
+struct control_block {
+ size_t size;
+ char *line;
+ uint64_t t;
+
+ TAILQ_ENTRY(control_block) entry;
+ TAILQ_ENTRY(control_block) all_entry;
+};
+
+/* Control client pane. */
+struct control_pane {
+ u_int pane;
+
+ /*
+ * Offsets into the pane data. The first (offset) is the data we have
+ * written; the second (queued) the data we have queued (pointed to by
+ * a block).
+ */
+ struct window_pane_offset offset;
+ struct window_pane_offset queued;
+
+ int flags;
+#define CONTROL_PANE_OFF 0x1
+#define CONTROL_PANE_PAUSED 0x2
+
+ int pending_flag;
+ TAILQ_ENTRY(control_pane) pending_entry;
+
+ TAILQ_HEAD(, control_block) blocks;
+
+ RB_ENTRY(control_pane) entry;
+};
+RB_HEAD(control_panes, control_pane);
+
+/* Subscription pane. */
+struct control_sub_pane {
+ u_int pane;
+ u_int idx;
+ char *last;
+
+ RB_ENTRY(control_sub_pane) entry;
+};
+RB_HEAD(control_sub_panes, control_sub_pane);
+
+/* Subscription window. */
+struct control_sub_window {
+ u_int window;
+ u_int idx;
+ char *last;
+
+ RB_ENTRY(control_sub_window) entry;
+};
+RB_HEAD(control_sub_windows, control_sub_window);
+
+/* Control client subscription. */
+struct control_sub {
+ char *name;
+ char *format;
+
+ enum control_sub_type type;
+ u_int id;
+
+ char *last;
+ struct control_sub_panes panes;
+ struct control_sub_windows windows;
+
+ RB_ENTRY(control_sub) entry;
+};
+RB_HEAD(control_subs, control_sub);
+
+/* Control client state. */
+struct control_state {
+ struct control_panes panes;
+
+ TAILQ_HEAD(, control_pane) pending_list;
+ u_int pending_count;
+
+ TAILQ_HEAD(, control_block) all_blocks;
+
+ struct bufferevent *read_event;
+ struct bufferevent *write_event;
+
+ struct control_subs subs;
+ struct event subs_timer;
+};
+
+/* Low and high watermarks. */
+#define CONTROL_BUFFER_LOW 512
+#define CONTROL_BUFFER_HIGH 8192
+
+/* Minimum to write to each client. */
+#define CONTROL_WRITE_MINIMUM 32
+
+/* Maximum age for clients that are not using pause mode. */
+#define CONTROL_MAXIMUM_AGE 300000
+
+/* Flags to ignore client. */
+#define CONTROL_IGNORE_FLAGS \
+ (CLIENT_CONTROL_NOOUTPUT| \
+ CLIENT_UNATTACHEDFLAGS)
+
+/* Compare client panes. */
+static int
+control_pane_cmp(struct control_pane *cp1, struct control_pane *cp2)
+{
+ if (cp1->pane < cp2->pane)
+ return (-1);
+ if (cp1->pane > cp2->pane)
+ return (1);
+ return (0);
+}
+RB_GENERATE_STATIC(control_panes, control_pane, entry, control_pane_cmp);
+
+/* Compare client subs. */
+static int
+control_sub_cmp(struct control_sub *csub1, struct control_sub *csub2)
+{
+ return (strcmp(csub1->name, csub2->name));
+}
+RB_GENERATE_STATIC(control_subs, control_sub, entry, control_sub_cmp);
+
+/* Compare client subscription panes. */
+static int
+control_sub_pane_cmp(struct control_sub_pane *csp1,
+ struct control_sub_pane *csp2)
+{
+ if (csp1->pane < csp2->pane)
+ return (-1);
+ if (csp1->pane > csp2->pane)
+ return (1);
+ if (csp1->idx < csp2->idx)
+ return (-1);
+ if (csp1->idx > csp2->idx)
+ return (1);
+ return (0);
+}
+RB_GENERATE_STATIC(control_sub_panes, control_sub_pane, entry,
+ control_sub_pane_cmp);
+
+/* Compare client subscription windows. */
+static int
+control_sub_window_cmp(struct control_sub_window *csw1,
+ struct control_sub_window *csw2)
+{
+ if (csw1->window < csw2->window)
+ return (-1);
+ if (csw1->window > csw2->window)
+ return (1);
+ if (csw1->idx < csw2->idx)
+ return (-1);
+ if (csw1->idx > csw2->idx)
+ return (1);
+ return (0);
+}
+RB_GENERATE_STATIC(control_sub_windows, control_sub_window, entry,
+ control_sub_window_cmp);
+
+/* Free a subscription. */
+static void
+control_free_sub(struct control_state *cs, struct control_sub *csub)
+{
+ struct control_sub_pane *csp, *csp1;
+ struct control_sub_window *csw, *csw1;
+
+ RB_FOREACH_SAFE(csp, control_sub_panes, &csub->panes, csp1) {
+ RB_REMOVE(control_sub_panes, &csub->panes, csp);
+ free(csp);
+ }
+ RB_FOREACH_SAFE(csw, control_sub_windows, &csub->windows, csw1) {
+ RB_REMOVE(control_sub_windows, &csub->windows, csw);
+ free(csw);
+ }
+ free(csub->last);
+
+ RB_REMOVE(control_subs, &cs->subs, csub);
+ free(csub->name);
+ free(csub->format);
+ free(csub);
+}
+
+/* Free a block. */
+static void
+control_free_block(struct control_state *cs, struct control_block *cb)
+{
+ free(cb->line);
+ TAILQ_REMOVE(&cs->all_blocks, cb, all_entry);
+ free(cb);
+}
+
+/* Get pane offsets for this client. */
+static struct control_pane *
+control_get_pane(struct client *c, struct window_pane *wp)
+{
+ struct control_state *cs = c->control_state;
+ struct control_pane cp = { .pane = wp->id };
+
+ return (RB_FIND(control_panes, &cs->panes, &cp));
+}
+
+/* Add pane offsets for this client. */
+static struct control_pane *
+control_add_pane(struct client *c, struct window_pane *wp)
+{
+ struct control_state *cs = c->control_state;
+ struct control_pane *cp;
+
+ cp = control_get_pane(c, wp);
+ if (cp != NULL)
+ return (cp);
+
+ cp = xcalloc(1, sizeof *cp);
+ cp->pane = wp->id;
+ RB_INSERT(control_panes, &cs->panes, cp);
+
+ memcpy(&cp->offset, &wp->offset, sizeof cp->offset);
+ memcpy(&cp->queued, &wp->offset, sizeof cp->queued);
+ TAILQ_INIT(&cp->blocks);
+
+ return (cp);
+}
+
+/* Discard output for a pane. */
+static void
+control_discard_pane(struct client *c, struct control_pane *cp)
+{
+ struct control_state *cs = c->control_state;
+ struct control_block *cb, *cb1;
+
+ TAILQ_FOREACH_SAFE(cb, &cp->blocks, entry, cb1) {
+ TAILQ_REMOVE(&cp->blocks, cb, entry);
+ control_free_block(cs, cb);
+ }
+}
+
+/* Get actual pane for this client. */
+static struct window_pane *
+control_window_pane(struct client *c, u_int pane)
+{
+ struct window_pane *wp;
+
+ if (c->session == NULL)
+ return (NULL);
+ if ((wp = window_pane_find_by_id(pane)) == NULL)
+ return (NULL);
+ if (winlink_find_by_window(&c->session->windows, wp->window) == NULL)
+ return (NULL);
+ return (wp);
+}
+
+/* Reset control offsets. */
+void
+control_reset_offsets(struct client *c)
+{
+ struct control_state *cs = c->control_state;
+ struct control_pane *cp, *cp1;
+
+ RB_FOREACH_SAFE(cp, control_panes, &cs->panes, cp1) {
+ RB_REMOVE(control_panes, &cs->panes, cp);
+ free(cp);
+ }
+
+ TAILQ_INIT(&cs->pending_list);
+ cs->pending_count = 0;
+}
+
+/* Get offsets for client. */
+struct window_pane_offset *
+control_pane_offset(struct client *c, struct window_pane *wp, int *off)
+{
+ struct control_state *cs = c->control_state;
+ struct control_pane *cp;
+
+ if (c->flags & CLIENT_CONTROL_NOOUTPUT) {
+ *off = 0;
+ return (NULL);
+ }
+
+ cp = control_get_pane(c, wp);
+ if (cp == NULL || (cp->flags & CONTROL_PANE_PAUSED)) {
+ *off = 0;
+ return (NULL);
+ }
+ if (cp->flags & CONTROL_PANE_OFF) {
+ *off = 1;
+ return (NULL);
+ }
+ *off = (EVBUFFER_LENGTH(cs->write_event->output) >= CONTROL_BUFFER_LOW);
+ return (&cp->offset);
+}
+
+/* Set pane as on. */
+void
+control_set_pane_on(struct client *c, struct window_pane *wp)
+{
+ struct control_pane *cp;
+
+ cp = control_get_pane(c, wp);
+ if (cp != NULL && (cp->flags & CONTROL_PANE_OFF)) {
+ cp->flags &= ~CONTROL_PANE_OFF;
+ memcpy(&cp->offset, &wp->offset, sizeof cp->offset);
+ memcpy(&cp->queued, &wp->offset, sizeof cp->queued);
+ }
+}
+
+/* Set pane as off. */
+void
+control_set_pane_off(struct client *c, struct window_pane *wp)
+{
+ struct control_pane *cp;
+
+ cp = control_add_pane(c, wp);
+ cp->flags |= CONTROL_PANE_OFF;
+}
+
+/* Continue a paused pane. */
+void
+control_continue_pane(struct client *c, struct window_pane *wp)
+{
+ struct control_pane *cp;
+
+ cp = control_get_pane(c, wp);
+ if (cp != NULL && (cp->flags & CONTROL_PANE_PAUSED)) {
+ cp->flags &= ~CONTROL_PANE_PAUSED;
+ memcpy(&cp->offset, &wp->offset, sizeof cp->offset);
+ memcpy(&cp->queued, &wp->offset, sizeof cp->queued);
+ control_write(c, "%%continue %%%u", wp->id);
+ }
+}
+
+/* Pause a pane. */
+void
+control_pause_pane(struct client *c, struct window_pane *wp)
+{
+ struct control_pane *cp;
+
+ cp = control_add_pane(c, wp);
+ if (~cp->flags & CONTROL_PANE_PAUSED) {
+ cp->flags |= CONTROL_PANE_PAUSED;
+ control_discard_pane(c, cp);
+ control_write(c, "%%pause %%%u", wp->id);
+ }
+}
+
+/* Write a line. */
+static void printflike(2, 0)
+control_vwrite(struct client *c, const char *fmt, va_list ap)
+{
+ struct control_state *cs = c->control_state;
+ char *s;
+
+ xvasprintf(&s, fmt, ap);
+ log_debug("%s: %s: writing line: %s", __func__, c->name, s);
+
+ bufferevent_write(cs->write_event, s, strlen(s));
+ bufferevent_write(cs->write_event, "\n", 1);
+
+ bufferevent_enable(cs->write_event, EV_WRITE);
+ free(s);
+}
+
+/* Write a line. */
+void
+control_write(struct client *c, const char *fmt, ...)
+{
+ struct control_state *cs = c->control_state;
+ struct control_block *cb;
+ va_list ap;
+
+ va_start(ap, fmt);
+
+ if (TAILQ_EMPTY(&cs->all_blocks)) {
+ control_vwrite(c, fmt, ap);
+ va_end(ap);
+ return;
+ }
+
+ cb = xcalloc(1, sizeof *cb);
+ xvasprintf(&cb->line, fmt, ap);
+ TAILQ_INSERT_TAIL(&cs->all_blocks, cb, all_entry);
+ cb->t = get_timer();
+
+ log_debug("%s: %s: storing line: %s", __func__, c->name, cb->line);
+ bufferevent_enable(cs->write_event, EV_WRITE);
+
+ va_end(ap);
+}
+
+/* Check age for this pane. */
+static int
+control_check_age(struct client *c, struct window_pane *wp,
+ struct control_pane *cp)
+{
+ struct control_block *cb;
+ uint64_t t, age;
+
+ cb = TAILQ_FIRST(&cp->blocks);
+ if (cb == NULL)
+ return (0);
+ t = get_timer();
+ if (cb->t >= t)
+ return (0);
+
+ age = t - cb->t;
+ log_debug("%s: %s: %%%u is %llu behind", __func__, c->name, wp->id,
+ (unsigned long long)age);
+
+ if (c->flags & CLIENT_CONTROL_PAUSEAFTER) {
+ if (age < c->pause_age)
+ return (0);
+ cp->flags |= CONTROL_PANE_PAUSED;
+ control_discard_pane(c, cp);
+ control_write(c, "%%pause %%%u", wp->id);
+ } else {
+ if (age < CONTROL_MAXIMUM_AGE)
+ return (0);
+ c->exit_message = xstrdup("too far behind");
+ c->flags |= CLIENT_EXIT;
+ control_discard(c);
+ }
+ return (1);
+}
+
+/* Write output from a pane. */
+void
+control_write_output(struct client *c, struct window_pane *wp)
+{
+ struct control_state *cs = c->control_state;
+ struct control_pane *cp;
+ struct control_block *cb;
+ size_t new_size;
+
+ if (winlink_find_by_window(&c->session->windows, wp->window) == NULL)
+ return;
+
+ if (c->flags & CONTROL_IGNORE_FLAGS) {
+ cp = control_get_pane(c, wp);
+ if (cp != NULL)
+ goto ignore;
+ return;
+ }
+ cp = control_add_pane(c, wp);
+ if (cp->flags & (CONTROL_PANE_OFF|CONTROL_PANE_PAUSED))
+ goto ignore;
+ if (control_check_age(c, wp, cp))
+ return;
+
+ window_pane_get_new_data(wp, &cp->queued, &new_size);
+ if (new_size == 0)
+ return;
+ window_pane_update_used_data(wp, &cp->queued, new_size);
+
+ cb = xcalloc(1, sizeof *cb);
+ cb->size = new_size;
+ TAILQ_INSERT_TAIL(&cs->all_blocks, cb, all_entry);
+ cb->t = get_timer();
+
+ TAILQ_INSERT_TAIL(&cp->blocks, cb, entry);
+ log_debug("%s: %s: new output block of %zu for %%%u", __func__, c->name,
+ cb->size, wp->id);
+
+ if (!cp->pending_flag) {
+ log_debug("%s: %s: %%%u now pending", __func__, c->name,
+ wp->id);
+ TAILQ_INSERT_TAIL(&cs->pending_list, cp, pending_entry);
+ cp->pending_flag = 1;
+ cs->pending_count++;
+ }
+ bufferevent_enable(cs->write_event, EV_WRITE);
+ return;
+
+ignore:
+ log_debug("%s: %s: ignoring pane %%%u", __func__, c->name, wp->id);
+ window_pane_update_used_data(wp, &cp->offset, SIZE_MAX);
+ window_pane_update_used_data(wp, &cp->queued, SIZE_MAX);
+}
+
+/* Control client error callback. */
+static enum cmd_retval
+control_error(struct cmdq_item *item, void *data)
+{
+ struct client *c = cmdq_get_client(item);
+ char *error = data;
+
+ cmdq_guard(item, "begin", 1);
+ control_write(c, "parse error: %s", error);
+ cmdq_guard(item, "error", 1);
+
+ free(error);
+ return (CMD_RETURN_NORMAL);
+}
+
+/* Control client error callback. */
+static void
+control_error_callback(__unused struct bufferevent *bufev,
+ __unused short what, void *data)
+{
+ struct client *c = data;
+
+ c->flags |= CLIENT_EXIT;
+}
+
+/* Control client input callback. Read lines and fire commands. */
+static void
+control_read_callback(__unused struct bufferevent *bufev, void *data)
+{
+ struct client *c = data;
+ struct control_state *cs = c->control_state;
+ struct evbuffer *buffer = cs->read_event->input;
+ char *line, *error;
+ struct cmdq_state *state;
+ enum cmd_parse_status status;
+
+ for (;;) {
+ line = evbuffer_readln(buffer, NULL, EVBUFFER_EOL_LF);
+ if (line == NULL)
+ break;
+ log_debug("%s: %s: %s", __func__, c->name, line);
+ if (*line == '\0') { /* empty line detach */
+ free(line);
+ c->flags |= CLIENT_EXIT;
+ break;
+ }
+
+ state = cmdq_new_state(NULL, NULL, CMDQ_STATE_CONTROL);
+ status = cmd_parse_and_append(line, NULL, c, state, &error);
+ if (status == CMD_PARSE_ERROR)
+ cmdq_append(c, cmdq_get_callback(control_error, error));
+ cmdq_free_state(state);
+
+ free(line);
+ }
+}
+
+/* Does this control client have outstanding data to write? */
+int
+control_all_done(struct client *c)
+{
+ struct control_state *cs = c->control_state;
+
+ if (!TAILQ_EMPTY(&cs->all_blocks))
+ return (0);
+ return (EVBUFFER_LENGTH(cs->write_event->output) == 0);
+}
+
+/* Flush all blocks until output. */
+static void
+control_flush_all_blocks(struct client *c)
+{
+ struct control_state *cs = c->control_state;
+ struct control_block *cb, *cb1;
+
+ TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1) {
+ if (cb->size != 0)
+ break;
+ log_debug("%s: %s: flushing line: %s", __func__, c->name,
+ cb->line);
+
+ bufferevent_write(cs->write_event, cb->line, strlen(cb->line));
+ bufferevent_write(cs->write_event, "\n", 1);
+ control_free_block(cs, cb);
+ }
+}
+
+/* Append data to buffer. */
+static struct evbuffer *
+control_append_data(struct client *c, struct control_pane *cp, uint64_t age,
+ struct evbuffer *message, struct window_pane *wp, size_t size)
+{
+ u_char *new_data;
+ size_t new_size;
+ u_int i;
+
+ if (message == NULL) {
+ message = evbuffer_new();
+ if (message == NULL)
+ fatalx("out of memory");
+ if (c->flags & CLIENT_CONTROL_PAUSEAFTER) {
+ evbuffer_add_printf(message,
+ "%%extended-output %%%u %llu : ", wp->id,
+ (unsigned long long)age);
+ } else
+ evbuffer_add_printf(message, "%%output %%%u ", wp->id);
+ }
+
+ new_data = window_pane_get_new_data(wp, &cp->offset, &new_size);
+ if (new_size < size)
+ fatalx("not enough data: %zu < %zu", new_size, size);
+ for (i = 0; i < size; i++) {
+ if (new_data[i] < ' ' || new_data[i] == '\\')
+ evbuffer_add_printf(message, "\\%03o", new_data[i]);
+ else
+ evbuffer_add_printf(message, "%c", new_data[i]);
+ }
+ window_pane_update_used_data(wp, &cp->offset, size);
+ return (message);
+}
+
+/* Write buffer. */
+static void
+control_write_data(struct client *c, struct evbuffer *message)
+{
+ struct control_state *cs = c->control_state;
+
+ log_debug("%s: %s: %.*s", __func__, c->name,
+ (int)EVBUFFER_LENGTH(message), EVBUFFER_DATA(message));
+
+ evbuffer_add(message, "\n", 1);
+ bufferevent_write_buffer(cs->write_event, message);
+ evbuffer_free(message);
+}
+
+/* Write output to client. */
+static int
+control_write_pending(struct client *c, struct control_pane *cp, size_t limit)
+{
+ struct control_state *cs = c->control_state;
+ struct window_pane *wp = NULL;
+ struct evbuffer *message = NULL;
+ size_t used = 0, size;
+ struct control_block *cb, *cb1;
+ uint64_t age, t = get_timer();
+
+ wp = control_window_pane(c, cp->pane);
+ if (wp == NULL || wp->fd == -1) {
+ TAILQ_FOREACH_SAFE(cb, &cp->blocks, entry, cb1) {
+ TAILQ_REMOVE(&cp->blocks, cb, entry);
+ control_free_block(cs, cb);
+ }
+ control_flush_all_blocks(c);
+ return (0);
+ }
+
+ while (used != limit && !TAILQ_EMPTY(&cp->blocks)) {
+ if (control_check_age(c, wp, cp)) {
+ if (message != NULL)
+ evbuffer_free(message);
+ message = NULL;
+ break;
+ }
+
+ cb = TAILQ_FIRST(&cp->blocks);
+ if (cb->t < t)
+ age = t - cb->t;
+ else
+ age = 0;
+ log_debug("%s: %s: output block %zu (age %llu) for %%%u "
+ "(used %zu/%zu)", __func__, c->name, cb->size,
+ (unsigned long long)age, cp->pane, used, limit);
+
+ size = cb->size;
+ if (size > limit - used)
+ size = limit - used;
+ used += size;
+
+ message = control_append_data(c, cp, age, message, wp, size);
+
+ cb->size -= size;
+ if (cb->size == 0) {
+ TAILQ_REMOVE(&cp->blocks, cb, entry);
+ control_free_block(cs, cb);
+
+ cb = TAILQ_FIRST(&cs->all_blocks);
+ if (cb != NULL && cb->size == 0) {
+ if (wp != NULL && message != NULL) {
+ control_write_data(c, message);
+ message = NULL;
+ }
+ control_flush_all_blocks(c);
+ }
+ }
+ }
+ if (message != NULL)
+ control_write_data(c, message);
+ return (!TAILQ_EMPTY(&cp->blocks));
+}
+
+/* Control client write callback. */
+static void
+control_write_callback(__unused struct bufferevent *bufev, void *data)
+{
+ struct client *c = data;
+ struct control_state *cs = c->control_state;
+ struct control_pane *cp, *cp1;
+ struct evbuffer *evb = cs->write_event->output;
+ size_t space, limit;
+
+ control_flush_all_blocks(c);
+
+ while (EVBUFFER_LENGTH(evb) < CONTROL_BUFFER_HIGH) {
+ if (cs->pending_count == 0)
+ break;
+ space = CONTROL_BUFFER_HIGH - EVBUFFER_LENGTH(evb);
+ log_debug("%s: %s: %zu bytes available, %u panes", __func__,
+ c->name, space, cs->pending_count);
+
+ limit = (space / cs->pending_count / 3); /* 3 bytes for \xxx */
+ if (limit < CONTROL_WRITE_MINIMUM)
+ limit = CONTROL_WRITE_MINIMUM;
+
+ TAILQ_FOREACH_SAFE(cp, &cs->pending_list, pending_entry, cp1) {
+ if (EVBUFFER_LENGTH(evb) >= CONTROL_BUFFER_HIGH)
+ break;
+ if (control_write_pending(c, cp, limit))
+ continue;
+ TAILQ_REMOVE(&cs->pending_list, cp, pending_entry);
+ cp->pending_flag = 0;
+ cs->pending_count--;
+ }
+ }
+ if (EVBUFFER_LENGTH(evb) == 0)
+ bufferevent_disable(cs->write_event, EV_WRITE);
+}
+
+/* Initialize for control mode. */
+void
+control_start(struct client *c)
+{
+ struct control_state *cs;
+
+ if (c->flags & CLIENT_CONTROLCONTROL) {
+ close(c->out_fd);
+ c->out_fd = -1;
+ } else
+ setblocking(c->out_fd, 0);
+ setblocking(c->fd, 0);
+
+ cs = c->control_state = xcalloc(1, sizeof *cs);
+ RB_INIT(&cs->panes);
+ TAILQ_INIT(&cs->pending_list);
+ TAILQ_INIT(&cs->all_blocks);
+ RB_INIT(&cs->subs);
+
+ cs->read_event = bufferevent_new(c->fd, control_read_callback,
+ control_write_callback, control_error_callback, c);
+ bufferevent_enable(cs->read_event, EV_READ);
+
+ if (c->flags & CLIENT_CONTROLCONTROL)
+ cs->write_event = cs->read_event;
+ else {
+ cs->write_event = bufferevent_new(c->out_fd, NULL,
+ control_write_callback, control_error_callback, c);
+ }
+ bufferevent_setwatermark(cs->write_event, EV_WRITE, CONTROL_BUFFER_LOW,
+ 0);
+
+ if (c->flags & CLIENT_CONTROLCONTROL) {
+ bufferevent_write(cs->write_event, "\033P1000p", 7);
+ bufferevent_enable(cs->write_event, EV_WRITE);
+ }
+}
+
+/* Discard all output for a client. */
+void
+control_discard(struct client *c)
+{
+ struct control_state *cs = c->control_state;
+ struct control_pane *cp;
+
+ RB_FOREACH(cp, control_panes, &cs->panes)
+ control_discard_pane(c, cp);
+ bufferevent_disable(cs->read_event, EV_READ);
+}
+
+/* Stop control mode. */
+void
+control_stop(struct client *c)
+{
+ struct control_state *cs = c->control_state;
+ struct control_block *cb, *cb1;
+ struct control_sub *csub, *csub1;
+
+ if (~c->flags & CLIENT_CONTROLCONTROL)
+ bufferevent_free(cs->write_event);
+ bufferevent_free(cs->read_event);
+
+ RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1)
+ control_free_sub(cs, csub);
+ if (evtimer_initialized(&cs->subs_timer))
+ evtimer_del(&cs->subs_timer);
+
+ TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1)
+ control_free_block(cs, cb);
+ control_reset_offsets(c);
+
+ free(cs);
+}
+
+/* Check session subscription. */
+static void
+control_check_subs_session(struct client *c, struct control_sub *csub)
+{
+ struct session *s = c->session;
+ struct format_tree *ft;
+ char *value;
+
+ ft = format_create_defaults(NULL, c, s, NULL, NULL);
+ value = format_expand(ft, csub->format);
+ format_free(ft);
+
+ if (csub->last != NULL && strcmp(value, csub->last) == 0) {
+ free(value);
+ return;
+ }
+ control_write(c,
+ "%%subscription-changed %s $%u - - - : %s",
+ csub->name, s->id, value);
+ free(csub->last);
+ csub->last = value;
+}
+
+/* Check pane subscription. */
+static void
+control_check_subs_pane(struct client *c, struct control_sub *csub)
+{
+ struct session *s = c->session;
+ struct window_pane *wp;
+ struct window *w;
+ struct winlink *wl;
+ struct format_tree *ft;
+ char *value;
+ struct control_sub_pane *csp, find;
+
+ wp = window_pane_find_by_id(csub->id);
+ if (wp == NULL || wp->fd == -1)
+ return;
+ w = wp->window;
+
+ TAILQ_FOREACH(wl, &w->winlinks, wentry) {
+ if (wl->session != s)
+ continue;
+
+ ft = format_create_defaults(NULL, c, s, wl, wp);
+ value = format_expand(ft, csub->format);
+ format_free(ft);
+
+ find.pane = wp->id;
+ find.idx = wl->idx;
+
+ csp = RB_FIND(control_sub_panes, &csub->panes, &find);
+ if (csp == NULL) {
+ csp = xcalloc(1, sizeof *csp);
+ csp->pane = wp->id;
+ csp->idx = wl->idx;
+ RB_INSERT(control_sub_panes, &csub->panes, csp);
+ }
+
+ if (csp->last != NULL && strcmp(value, csp->last) == 0) {
+ free(value);
+ continue;
+ }
+ control_write(c,
+ "%%subscription-changed %s $%u @%u %u %%%u : %s",
+ csub->name, s->id, w->id, wl->idx, wp->id, value);
+ free(csp->last);
+ csp->last = value;
+ }
+}
+
+/* Check all panes subscription. */
+static void
+control_check_subs_all_panes(struct client *c, struct control_sub *csub)
+{
+ struct session *s = c->session;
+ struct window_pane *wp;
+ struct window *w;
+ struct winlink *wl;
+ struct format_tree *ft;
+ char *value;
+ struct control_sub_pane *csp, find;
+
+ RB_FOREACH(wl, winlinks, &s->windows) {
+ w = wl->window;
+ TAILQ_FOREACH(wp, &w->panes, entry) {
+ ft = format_create_defaults(NULL, c, s, wl, wp);
+ value = format_expand(ft, csub->format);
+ format_free(ft);
+
+ find.pane = wp->id;
+ find.idx = wl->idx;
+
+ csp = RB_FIND(control_sub_panes, &csub->panes, &find);
+ if (csp == NULL) {
+ csp = xcalloc(1, sizeof *csp);
+ csp->pane = wp->id;
+ csp->idx = wl->idx;
+ RB_INSERT(control_sub_panes, &csub->panes, csp);
+ }
+
+ if (csp->last != NULL &&
+ strcmp(value, csp->last) == 0) {
+ free(value);
+ continue;
+ }
+ control_write(c,
+ "%%subscription-changed %s $%u @%u %u %%%u : %s",
+ csub->name, s->id, w->id, wl->idx, wp->id, value);
+ free(csp->last);
+ csp->last = value;
+ }
+ }
+}
+
+/* Check window subscription. */
+static void
+control_check_subs_window(struct client *c, struct control_sub *csub)
+{
+ struct session *s = c->session;
+ struct window *w;
+ struct winlink *wl;
+ struct format_tree *ft;
+ char *value;
+ struct control_sub_window *csw, find;
+
+ w = window_find_by_id(csub->id);
+ if (w == NULL)
+ return;
+
+ TAILQ_FOREACH(wl, &w->winlinks, wentry) {
+ if (wl->session != s)
+ continue;
+
+ ft = format_create_defaults(NULL, c, s, wl, NULL);
+ value = format_expand(ft, csub->format);
+ format_free(ft);
+
+ find.window = w->id;
+ find.idx = wl->idx;
+
+ csw = RB_FIND(control_sub_windows, &csub->windows, &find);
+ if (csw == NULL) {
+ csw = xcalloc(1, sizeof *csw);
+ csw->window = w->id;
+ csw->idx = wl->idx;
+ RB_INSERT(control_sub_windows, &csub->windows, csw);
+ }
+
+ if (csw->last != NULL && strcmp(value, csw->last) == 0) {
+ free(value);
+ continue;
+ }
+ control_write(c,
+ "%%subscription-changed %s $%u @%u %u - : %s",
+ csub->name, s->id, w->id, wl->idx, value);
+ free(csw->last);
+ csw->last = value;
+ }
+}
+
+/* Check all windows subscription. */
+static void
+control_check_subs_all_windows(struct client *c, struct control_sub *csub)
+{
+ struct session *s = c->session;
+ struct window *w;
+ struct winlink *wl;
+ struct format_tree *ft;
+ char *value;
+ struct control_sub_window *csw, find;
+
+ RB_FOREACH(wl, winlinks, &s->windows) {
+ w = wl->window;
+
+ ft = format_create_defaults(NULL, c, s, wl, NULL);
+ value = format_expand(ft, csub->format);
+ format_free(ft);
+
+ find.window = w->id;
+ find.idx = wl->idx;
+
+ csw = RB_FIND(control_sub_windows, &csub->windows, &find);
+ if (csw == NULL) {
+ csw = xcalloc(1, sizeof *csw);
+ csw->window = w->id;
+ csw->idx = wl->idx;
+ RB_INSERT(control_sub_windows, &csub->windows, csw);
+ }
+
+ if (csw->last != NULL && strcmp(value, csw->last) == 0) {
+ free(value);
+ continue;
+ }
+ control_write(c,
+ "%%subscription-changed %s $%u @%u %u - : %s",
+ csub->name, s->id, w->id, wl->idx, value);
+ free(csw->last);
+ csw->last = value;
+ }
+}
+
+/* Check subscriptions timer. */
+static void
+control_check_subs_timer(__unused int fd, __unused short events, void *data)
+{
+ struct client *c = data;
+ struct control_state *cs = c->control_state;
+ struct control_sub *csub, *csub1;
+ struct timeval tv = { .tv_sec = 1 };
+
+ log_debug("%s: timer fired", __func__);
+ evtimer_add(&cs->subs_timer, &tv);
+
+ RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1) {
+ switch (csub->type) {
+ case CONTROL_SUB_SESSION:
+ control_check_subs_session(c, csub);
+ break;
+ case CONTROL_SUB_PANE:
+ control_check_subs_pane(c, csub);
+ break;
+ case CONTROL_SUB_ALL_PANES:
+ control_check_subs_all_panes(c, csub);
+ break;
+ case CONTROL_SUB_WINDOW:
+ control_check_subs_window(c, csub);
+ break;
+ case CONTROL_SUB_ALL_WINDOWS:
+ control_check_subs_all_windows(c, csub);
+ break;
+ }
+ }
+}
+
+/* Add a subscription. */
+void
+control_add_sub(struct client *c, const char *name, enum control_sub_type type,
+ int id, const char *format)
+{
+ struct control_state *cs = c->control_state;
+ struct control_sub *csub, find;
+ struct timeval tv = { .tv_sec = 1 };
+
+ find.name = (char *)name;
+ if ((csub = RB_FIND(control_subs, &cs->subs, &find)) != NULL)
+ control_free_sub(cs, csub);
+
+ csub = xcalloc(1, sizeof *csub);
+ csub->name = xstrdup(name);
+ csub->type = type;
+ csub->id = id;
+ csub->format = xstrdup(format);
+ RB_INSERT(control_subs, &cs->subs, csub);
+
+ RB_INIT(&csub->panes);
+ RB_INIT(&csub->windows);
+
+ if (!evtimer_initialized(&cs->subs_timer))
+ evtimer_set(&cs->subs_timer, control_check_subs_timer, c);
+ if (!evtimer_pending(&cs->subs_timer, NULL))
+ evtimer_add(&cs->subs_timer, &tv);
+}
+
+/* Remove a subscription. */
+void
+control_remove_sub(struct client *c, const char *name)
+{
+ struct control_state *cs = c->control_state;
+ struct control_sub *csub, find;
+
+ find.name = (char *)name;
+ if ((csub = RB_FIND(control_subs, &cs->subs, &find)) != NULL)
+ control_free_sub(cs, csub);
+ if (RB_EMPTY(&cs->subs))
+ evtimer_del(&cs->subs_timer);
+}