Files
tmux/monitor.c
nicm d29aa12117 Replace the notification system with events. Events can carry a payload of
additional payload (to reduce problems with lifetime of objects) and are
delivered to one or more event sinks. This is more powerful and reduces the
complex dependencies between control mode and hooks. Events are now used for
hooks, control mode notifications and for monitors (set-hook -B).

wait-for can now wait for an event to fire (-E flag, with -F to for filter),
with -v to print the payload, as well as listing (-l) waiting clients on an
event and forcing one to wake (-w).

A few additional hooks are also now available (pane-created, pane-resized, etc)
and some of the existing ones have additional format variables available.
2026-07-10 13:38:45 +00:00

659 lines
15 KiB
C

/* $OpenBSD: monitor.c,v 1.5 2026/07/10 13:38:45 nicm Exp $ */
/*
* Copyright (c) 2026 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 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 <event.h>
#include <stdlib.h>
#include <string.h>
#include "tmux.h"
/* Subscription pane. */
struct monitor_pane {
u_int pane;
u_int idx;
char *last;
u_int generation;
RB_ENTRY(monitor_pane) entry;
};
RB_HEAD(monitor_panes, monitor_pane);
/* Subscription window. */
struct monitor_window {
u_int window;
u_int idx;
char *last;
u_int generation;
RB_ENTRY(monitor_window) entry;
};
RB_HEAD(monitor_windows, monitor_window);
/* Subscription. */
struct monitor_item {
char *name;
char *format;
enum monitor_type type;
u_int id;
u_int flags;
char *last;
struct monitor_panes panes;
struct monitor_windows windows;
RB_ENTRY(monitor_item) entry;
};
RB_HEAD(monitor_items, monitor_item);
/* Monitored subscription set. */
struct monitor_set {
struct client *client;
struct session *session;
monitor_cb cb;
void *data;
struct monitor_items items;
struct event timer;
u_int generation;
};
static void monitor_timer(__unused int, __unused short, void *);
/* Get the session for this monitor set. */
static struct session *
monitor_get_session(struct monitor_set *ms)
{
struct session *s;
if (ms->client != NULL)
return (ms->client->session);
s = ms->session;
if (s == NULL)
return (RB_MIN(sessions, &sessions));
if (session_find_by_id(s->id) != s)
return (NULL);
return (s);
}
/* Create a format tree for a subscription. */
static struct format_tree *
monitor_create_formats(struct client *c, struct session *s, struct winlink *wl,
struct window_pane *wp)
{
struct format_tree *ft;
ft = format_create(NULL, NULL, 0, FORMAT_NOJOBS);
format_defaults(ft, c, s, wl, wp);
return (ft);
}
/* Compare subscriptions. */
static int
monitor_item_cmp(struct monitor_item *m1, struct monitor_item *m2)
{
return (strcmp(m1->name, m2->name));
}
RB_GENERATE_STATIC(monitor_items, monitor_item, entry, monitor_item_cmp);
/* Compare subscription panes. */
static int
monitor_pane_cmp(struct monitor_pane *mp1, struct monitor_pane *mp2)
{
if (mp1->pane < mp2->pane)
return (-1);
if (mp1->pane > mp2->pane)
return (1);
if (mp1->idx < mp2->idx)
return (-1);
if (mp1->idx > mp2->idx)
return (1);
return (0);
}
RB_GENERATE_STATIC(monitor_panes, monitor_pane, entry, monitor_pane_cmp);
/* Compare subscription windows. */
static int
monitor_window_cmp(struct monitor_window *mw1, struct monitor_window *mw2)
{
if (mw1->window < mw2->window)
return (-1);
if (mw1->window > mw2->window)
return (1);
if (mw1->idx < mw2->idx)
return (-1);
if (mw1->idx > mw2->idx)
return (1);
return (0);
}
RB_GENERATE_STATIC(monitor_windows, monitor_window, entry, monitor_window_cmp);
/* Free a subscription. */
static void
monitor_free_item(struct monitor_set *ms, struct monitor_item *me)
{
struct monitor_pane *mp, *mp1;
struct monitor_window *mw, *mw1;
RB_FOREACH_SAFE(mp, monitor_panes, &me->panes, mp1) {
RB_REMOVE(monitor_panes, &me->panes, mp);
free(mp->last);
free(mp);
}
RB_FOREACH_SAFE(mw, monitor_windows, &me->windows, mw1) {
RB_REMOVE(monitor_windows, &me->windows, mw);
free(mw->last);
free(mw);
}
free(me->last);
RB_REMOVE(monitor_items, &ms->items, me);
free(me->name);
free(me->format);
free(me);
}
/* Report a changed value. */
static void
monitor_report(struct monitor_set *ms, struct monitor_item *me,
struct session *s, struct winlink *wl, struct window_pane *wp,
const char *value, const char *last)
{
struct monitor_change change = { 0 };
log_debug("%s: %s changed to %s", __func__, me->name, value);
change.name = me->name;
change.value = value;
change.last = last;
change.c = ms->client;
change.s = s;
change.wl = wl;
change.wp = wp;
ms->cb(&change, ms->data);
}
/* Check a value against its last value and report if changed. */
static void
monitor_check_value(struct monitor_set *ms, struct monitor_item *me,
struct session *s, struct winlink *wl, struct window_pane *wp,
char *value, char **last)
{
if (*last == NULL) {
*last = value;
if (me->flags & MONITOR_NOTIFY_INITIAL)
monitor_report(ms, me, s, wl, wp, value, NULL);
return;
}
if (strcmp(value, *last) == 0) {
free(value);
return;
}
monitor_report(ms, me, s, wl, wp, value, *last);
free(*last);
*last = value;
}
/* Check session subscription. */
static void
monitor_check_session(struct monitor_set *ms, struct monitor_item *me,
struct format_tree *ft)
{
struct session *s = monitor_get_session(ms);
char *value;
value = format_expand(ft, me->format);
monitor_check_value(ms, me, s, NULL, NULL, value, &me->last);
}
/* Check pane subscription. */
static void
monitor_check_pane(struct monitor_set *ms, struct monitor_item *me)
{
struct client *c = ms->client;
struct session *s = monitor_get_session(ms);
struct window_pane *wp;
struct window *w;
struct winlink *wl;
struct format_tree *ft;
char *value;
struct monitor_pane *mp, find;
wp = window_pane_find_by_id(me->id);
if (wp == NULL || wp->fd == -1)
return;
w = wp->window;
TAILQ_FOREACH(wl, &w->winlinks, wentry) {
if (wl->session != s)
continue;
ft = monitor_create_formats(c, s, wl, wp);
value = format_expand(ft, me->format);
format_free(ft);
find.pane = wp->id;
find.idx = wl->idx;
mp = RB_FIND(monitor_panes, &me->panes, &find);
if (mp == NULL) {
mp = xcalloc(1, sizeof *mp);
mp->pane = wp->id;
mp->idx = wl->idx;
RB_INSERT(monitor_panes, &me->panes, mp);
}
monitor_check_value(ms, me, s, wl, wp, value, &mp->last);
}
}
/* Check one all-panes subscription. */
static void
monitor_check_all_panes_one(struct monitor_set *ms, struct monitor_item *me,
struct format_tree *ft, struct winlink *wl, struct window_pane *wp)
{
struct session *s = monitor_get_session(ms);
char *value;
struct monitor_pane *mp, find;
value = format_expand(ft, me->format);
find.pane = wp->id;
find.idx = wl->idx;
mp = RB_FIND(monitor_panes, &me->panes, &find);
if (mp == NULL) {
mp = xcalloc(1, sizeof *mp);
mp->pane = wp->id;
mp->idx = wl->idx;
RB_INSERT(monitor_panes, &me->panes, mp);
}
mp->generation = ms->generation;
monitor_check_value(ms, me, s, wl, wp, value, &mp->last);
}
/* Remove all-panes entries not seen during the current scan. */
static void
monitor_sweep_all_panes(struct monitor_item *me, u_int generation)
{
struct monitor_pane *mp, *mp1;
RB_FOREACH_SAFE(mp, monitor_panes, &me->panes, mp1) {
if (mp->generation == generation)
continue;
RB_REMOVE(monitor_panes, &me->panes, mp);
free(mp->last);
free(mp);
}
}
/* Check window subscription. */
static void
monitor_check_window(struct monitor_set *ms, struct monitor_item *me)
{
struct client *c = ms->client;
struct session *s = monitor_get_session(ms);
struct window *w;
struct winlink *wl;
struct format_tree *ft;
char *value;
struct monitor_window *mw, find;
w = window_find_by_id(me->id);
if (w == NULL)
return;
TAILQ_FOREACH(wl, &w->winlinks, wentry) {
if (wl->session != s)
continue;
ft = monitor_create_formats(c, s, wl, NULL);
value = format_expand(ft, me->format);
format_free(ft);
find.window = w->id;
find.idx = wl->idx;
mw = RB_FIND(monitor_windows, &me->windows, &find);
if (mw == NULL) {
mw = xcalloc(1, sizeof *mw);
mw->window = w->id;
mw->idx = wl->idx;
RB_INSERT(monitor_windows, &me->windows, mw);
}
monitor_check_value(ms, me, s, wl, NULL, value, &mw->last);
}
}
/* Check one all-windows subscription. */
static void
monitor_check_all_windows_one(struct monitor_set *ms, struct monitor_item *me,
struct format_tree *ft, struct winlink *wl)
{
struct session *s = monitor_get_session(ms);
struct window *w = wl->window;
char *value;
struct monitor_window *mw, find;
value = format_expand(ft, me->format);
find.window = w->id;
find.idx = wl->idx;
mw = RB_FIND(monitor_windows, &me->windows, &find);
if (mw == NULL) {
mw = xcalloc(1, sizeof *mw);
mw->window = w->id;
mw->idx = wl->idx;
RB_INSERT(monitor_windows, &me->windows, mw);
}
mw->generation = ms->generation;
monitor_check_value(ms, me, s, wl, NULL, value, &mw->last);
}
/* Remove all-windows entries not seen during the current scan. */
static void
monitor_sweep_all_windows(struct monitor_item *me, u_int generation)
{
struct monitor_window *mw, *mw1;
RB_FOREACH_SAFE(mw, monitor_windows, &me->windows, mw1) {
if (mw->generation == generation)
continue;
RB_REMOVE(monitor_windows, &me->windows, mw);
free(mw->last);
free(mw);
}
}
/* Check session subscriptions. */
static void
monitor_check_sessions(struct monitor_set *ms)
{
struct client *c = ms->client;
struct session *s = monitor_get_session(ms);
struct monitor_item *me, *me1;
struct format_tree *ft;
ft = monitor_create_formats(c, s, NULL, NULL);
RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) {
if (me->type == MONITOR_SESSION)
monitor_check_session(ms, me, ft);
}
format_free(ft);
}
/* Check pane and window subscriptions. */
static void
monitor_check_panes_windows(struct monitor_set *ms)
{
struct monitor_item *me, *me1;
RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) {
switch (me->type) {
case MONITOR_PANE:
monitor_check_pane(ms, me);
break;
case MONITOR_WINDOW:
monitor_check_window(ms, me);
break;
case MONITOR_SESSION:
case MONITOR_ALL_PANES:
case MONITOR_ALL_WINDOWS:
break;
}
}
}
/* Check all-panes subscriptions. */
static void
monitor_check_all_panes(struct monitor_set *ms)
{
struct client *c = ms->client;
struct session *s = monitor_get_session(ms);
struct monitor_item *me, *me1;
struct window_pane *wp;
struct format_tree *ft;
struct winlink *wl;
if (++ms->generation == 0)
ms->generation = 1;
RB_FOREACH(wl, winlinks, &s->windows) {
TAILQ_FOREACH(wp, &wl->window->panes, entry) {
ft = monitor_create_formats(c, s, wl, wp);
RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) {
if (me->type != MONITOR_ALL_PANES)
continue;
monitor_check_all_panes_one(ms, me, ft, wl, wp);
}
format_free(ft);
}
}
RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) {
if (me->type == MONITOR_ALL_PANES)
monitor_sweep_all_panes(me, ms->generation);
}
}
/* Check all-windows subscriptions. */
static void
monitor_check_all_windows(struct monitor_set *ms)
{
struct client *c = ms->client;
struct session *s = monitor_get_session(ms);
struct monitor_item *me, *me1;
struct format_tree *ft;
struct winlink *wl;
if (++ms->generation == 0)
ms->generation = 1;
RB_FOREACH(wl, winlinks, &s->windows) {
ft = monitor_create_formats(c, s, wl, NULL);
RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) {
if (me->type != MONITOR_ALL_WINDOWS)
continue;
monitor_check_all_windows_one(ms, me, ft, wl);
}
format_free(ft);
}
RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) {
if (me->type == MONITOR_ALL_WINDOWS)
monitor_sweep_all_windows(me, ms->generation);
}
}
/* Check subscriptions. */
static void
monitor_timer(__unused int fd, __unused short events, void *data)
{
struct monitor_set *ms = data;
struct monitor_item *me;
struct timeval tv = { .tv_sec = 1 };
int have_session = 0, have_all_panes = 0;
int have_all_windows = 0;
log_debug("%s: timer fired", __func__);
evtimer_add(&ms->timer, &tv);
if (monitor_get_session(ms) == NULL)
return;
RB_FOREACH(me, monitor_items, &ms->items) {
switch (me->type) {
case MONITOR_SESSION:
have_session = 1;
break;
case MONITOR_ALL_PANES:
have_all_panes = 1;
break;
case MONITOR_ALL_WINDOWS:
have_all_windows = 1;
break;
case MONITOR_PANE:
case MONITOR_WINDOW:
break;
}
}
if (have_session)
monitor_check_sessions(ms);
monitor_check_panes_windows(ms);
if (have_all_panes)
monitor_check_all_panes(ms);
if (have_all_windows)
monitor_check_all_windows(ms);
}
/* Create a monitor set. */
static struct monitor_set *
monitor_create(monitor_cb cb, void *data)
{
struct monitor_set *ms;
ms = xcalloc(1, sizeof *ms);
ms->cb = cb;
ms->data = data;
RB_INIT(&ms->items);
return (ms);
}
/* Create a client monitor set. */
struct monitor_set *
monitor_create_client(struct client *c, monitor_cb cb, void *data)
{
struct monitor_set *ms;
ms = monitor_create(cb, data);
ms->client = c;
return (ms);
}
/* Create a monitor set for a session. */
struct monitor_set *
monitor_create_session(struct session *s, monitor_cb cb, void *data)
{
struct monitor_set *ms;
ms = monitor_create(cb, data);
ms->session = s;
if (s != NULL)
session_add_ref(s, __func__);
return (ms);
}
/* Destroy a monitor set. */
void
monitor_destroy(struct monitor_set *ms)
{
struct monitor_item *me, *me1;
if (ms != NULL) {
if (evtimer_initialized(&ms->timer))
evtimer_del(&ms->timer);
RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1)
monitor_free_item(ms, me);
if (ms->session != NULL)
session_remove_ref(ms->session, __func__);
free(ms);
}
}
/* Parse a subscription. */
int
monitor_parse(const char *value, char **name, enum monitor_type *type, int *id,
char **format)
{
char *copy, *what, *split;
copy = xstrdup(value);
*id = -1;
what = strchr(copy, ':');
if (what == NULL)
goto fail;
*what++ = '\0';
split = strchr(what, ':');
if (split == NULL)
goto fail;
*split++ = '\0';
if (strcmp(what, "%*") == 0)
*type = MONITOR_ALL_PANES;
else if (sscanf(what, "%%%d", id) == 1 && *id >= 0)
*type = MONITOR_PANE;
else if (strcmp(what, "@*") == 0)
*type = MONITOR_ALL_WINDOWS;
else if (sscanf(what, "@%d", id) == 1 && *id >= 0)
*type = MONITOR_WINDOW;
else
*type = MONITOR_SESSION;
*name = xstrdup(copy);
*format = xstrdup(split);
free(copy);
return (0);
fail:
free(copy);
return (-1);
}
/* Add a subscription. */
void
monitor_add(struct monitor_set *ms, const char *name, enum monitor_type type,
int id, const char *format, u_int flags)
{
struct monitor_item *me, find = { .name = (char *)name };
struct timeval tv = { .tv_sec = 1 };
if ((me = RB_FIND(monitor_items, &ms->items, &find)) != NULL)
monitor_free_item(ms, me);
me = xcalloc(1, sizeof *me);
me->name = xstrdup(name);
me->format = xstrdup(format);
me->type = type;
me->id = id;
me->flags = flags;
RB_INIT(&me->panes);
RB_INIT(&me->windows);
RB_INSERT(monitor_items, &ms->items, me);
if (!evtimer_initialized(&ms->timer))
evtimer_set(&ms->timer, monitor_timer, ms);
if (!evtimer_pending(&ms->timer, NULL))
evtimer_add(&ms->timer, &tv);
}
/* Remove a subscription. */
void
monitor_remove(struct monitor_set *ms, const char *name)
{
struct monitor_item *me, find = { .name = (char *)name };
if ((me = RB_FIND(monitor_items, &ms->items, &find)) != NULL)
monitor_free_item(ms, me);
if (RB_EMPTY(&ms->items) && evtimer_initialized(&ms->timer))
evtimer_del(&ms->timer);
}