Files
tmux/monitor.c
2026-07-06 12:10:18 +00:00

659 lines
15 KiB
C

/* $OpenBSD$ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicm@users.sourceforge.net>
*
* 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);
}