Command that provides features for all synchronization needs in tmux scripts.
I added an example script that serves a temporary documentation and
shows the features provides by displaying a simple 'animation' which
integrates multiple panes.
I can see two issues with the current implementation:
1- When unlocking waiting panes, I guess it should be done in the
event loop, however I was getting random crashes when doing that, so
now everything is done synchronously inside the command.
2- It must take action when a pane that is holding the monitor exits,
but I couldn't figure out how to do it
There must be more problems, but I need feedback to fix.
---
Makefile.am | 1 +
cmd-monitor.c | 311 ++++++++++++++++++++++++++++++++++++++++++++++++++++
cmd.c | 1 +
examples/monitor.sh | 120 ++++++++++++++++++++
tmux.h | 1 +
5 files changed, 434 insertions(+)
create mode 100644 cmd-monitor.c
create mode 100755 examples/monitor.sh
diff --git a/Makefile.am b/Makefile.am
index 5caa498..98f11bc 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -100,6 +100,7 @@ dist_tmux_SOURCES = \
cmd-load-buffer.c \
cmd-lock-server.c \
cmd-move-window.c \
+ cmd-monitor.c \
cmd-new-session.c \
cmd-new-window.c \
cmd-paste-buffer.c \
diff --git a/cmd-monitor.c b/cmd-monitor.c
new file mode 100644
index 0000000..0b7cf8e
--- /dev/null
+++ b/cmd-monitor.c
@@ -0,0 +1,311 @@
+/* $Id$ */
+
+/*
+ * Copyright (c) 2013 Thiago de Arruda<tpadilh...@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.
+ */
+
+/*
+ * This command adds features that allow scripts running in different panes
+ * to easily synchronize tasks. It exposes 'monitor' objects to tmux panes,
+ * which are similar to the monitor concept used in concurrent programming.
+ *
+ * The major difference is that this one manages/synchronizes tmux panes
+ * instead of threads.
+ *
+ * See http://en.wikipedia.org/wiki/Monitor_(synchronization) for more
+ * information about monitors
+*/
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "tmux.h"
+
+enum cmd_retval cmd_monitor_exec(struct cmd *, struct cmd_q *);
+
+const struct cmd_entry cmd_monitor_entry = {
+ "monitor", NULL,
+ "luws", 1, 1,
+ "[-l | -u | -w | -s] name",
+ 0,
+ NULL,
+ NULL,
+ cmd_monitor_exec
+};
+
+enum monitor_options {
+ MONITOR_ENQUEUE = 1,
+ // if this is set, as soon as the pane acquires the lock, it will
+ // temporarily give up the lock rights and be put on the wait stack until
+ // a signal is received, at which point it will be put back in front
+ // of the lock queue again
+ MONITOR_WAIT = 2,
+ MONITOR_SIGNAL = 4,
+ // if the pane is not holding the lock when it starts waiting for a signal,
+ // then this flag is set, which means that as soon as the the signal
+ // arrives, it will leave the wait stack and release the lock
+ MONITOR_RELEASE = 8
+};
+
+// queue of panes, used for tracking panes waiting to lock the monitor, or to
+// receive a signal from another pane
+struct cmdq_node {
+
+ struct cmd_q *cmdq;
+
+ int pane_id;
+
+ enum monitor_options options;
+
+ SIMPLEQ_ENTRY(cmdq_node) node;
+};
+
+// search tree that keeps track of monitors. each monitor is identified by a
+// name, and the monitor node only exists while there's some pane using it
+struct monitor_node {
+
+ // monitor name
+ char *name;
+
+ // client/pane id
+ int pane_id;
+
+ // number of times the pane has locked the monitor.
+ int count;
+
+ SIMPLEQ_HEAD(, cmdq_node) lock_queue; // panes waiting to lock the monitor
+ SIMPLEQ_HEAD(, cmdq_node) wait_stack; // panes waiting for signal
+
+ RB_ENTRY(monitor_node) node;
+};
+
+static inline int monitors_cmp(struct monitor_node *m1, struct monitor_node *m2)
+{
+ return (strcmp(m1->name, m2->name));
+}
+
+RB_HEAD(monitors, monitor_node) monitors_head = RB_INITIALIZER(&monitors_head);
+RB_GENERATE_STATIC(monitors, monitor_node, node, monitors_cmp);
+
+static inline int get_pane_id(struct cmd_q *cmdq)
+{
+ struct window_pane *wp;
+ cmd_find_pane(cmdq, NULL, NULL, &wp);
+ return (int)wp->id;
+}
+
+static struct monitor_node *monitor_find(const char *name)
+{
+ struct monitor_node m_name;
+ m_name.name = (char *)name;
+ return RB_FIND(monitors, &monitors_head, &m_name);
+}
+
+static struct monitor_node *monitor_find_or_create(const char *name)
+{
+ struct monitor_node *m;
+
+ m = monitor_find(name);
+
+ if (m == NULL) {
+ // creates/insert a node for representing the monitor state
+ m = xmalloc(sizeof *m);
+ m->name = xstrdup(name);
+ m->count = 0;
+ m->pane_id = -1;
+ SIMPLEQ_INIT(&m->lock_queue);
+ SIMPLEQ_INIT(&m->wait_stack);
+ RB_INSERT(monitors, &monitors_head, m);
+ }
+
+ return m;
+}
+
+static void monitor_shift(struct monitor_node *m);
+
+static void monitor_signal_move_cmdq(struct monitor_node *m)
+{
+ struct cmdq_node *q;
+ // move all cmdq objects from the wait stack to the
+ // front of the lock queue in a way that the object
+ // in the bottom of the stack will be in the head
+ // of the queue(in other words, the first pane to
+ // enter the wait stack will be the first to awake)
+ while ((q = SIMPLEQ_FIRST(&m->wait_stack)) != NULL) {
+ SIMPLEQ_REMOVE_HEAD(&m->wait_stack, node);
+ SIMPLEQ_INSERT_HEAD(&m->lock_queue, q, node);
+ }
+ if (m->pane_id == -1)
+ // the monitor is unlocked, so start shifting
+ monitor_shift(m);
+}
+
+// entry point to the monitor which enforces the mutial exclusion property.
+// all monitor actions, have to go through here. the only exeception is
+// monitor_unlock since the pane needs to already be occupying the monitor
+static int monitor_enter(struct monitor_node *m, struct cmd_q *cmdq, int pane_id, enum monitor_options options)
+{
+ struct cmdq_node *q;
+
+ if (m->pane_id == -1 || m->pane_id == pane_id) {
+ if (options & MONITOR_WAIT) {
+ cmdq->references++;
+ q = xmalloc(sizeof *q);
+ q->cmdq = cmdq;
+ q->pane_id = pane_id;
+ // since this pane will enter the wait stack,
+ // we can remove the wait flag now
+ q->options = options & ~MONITOR_WAIT;
+ // if the lock was acquired as result of a wait command(meaning the pane
+ // wasn't holding the lock), then release as soon as it receives signal
+ if (m->pane_id != pane_id)
+ q->options |= MONITOR_RELEASE;
+ SIMPLEQ_INSERT_HEAD(&m->wait_stack, q, node);
+ // release the pane lock
+ m->pane_id = -1;
+ // waiting resets the number of locks acquired on the monitor
+ m->count = 0;
+ if (!SIMPLEQ_EMPTY(&m->wait_stack))
+ monitor_shift(m);
+ } else if (options & MONITOR_SIGNAL) {
+ monitor_signal_move_cmdq(m);
+ } else {
+ // lock the monitor
+ m->pane_id = pane_id;
+ // this implementation is reentrant, so the pane can lock the same monitor
+ // multiple times, as long as it also releases it the same number of times
+ m->count++;
+ }
+ return 1;
+ }
+
+ if (options & MONITOR_ENQUEUE) {
+ // push to the lock queue
+ cmdq->references++;
+ q = xmalloc(sizeof *q);
+ q->cmdq = cmdq;
+ q->pane_id = pane_id;
+ if ((options & MONITOR_WAIT) || (options & MONITOR_SIGNAL))
+ // if the monitor is already locked, and this came from a wait/signal command, then
+ // also release the lock as soon as it is acquired
+ options |= MONITOR_RELEASE;
+ q->options = options;
+ SIMPLEQ_INSERT_TAIL(&m->lock_queue, q, node);
+ }
+
+ return 0;
+}
+
+static void monitor_shift(struct monitor_node *m)
+{
+ struct cmdq_node *q;
+
+ // release the lock
+ m->pane_id = -1;
+
+ // process all panes that don't need to hold the lock(signaling/waiting panes)
+ while ((q = SIMPLEQ_FIRST(&m->lock_queue)) != NULL && (q->options & MONITOR_RELEASE)) {
+ SIMPLEQ_REMOVE_HEAD(&m->lock_queue, node);
+ if (q->options & MONITOR_SIGNAL)
+ monitor_signal_move_cmdq(m);
+ if (!cmdq_free(q->cmdq))
+ cmdq_continue(q->cmdq);
+ free(q);
+ }
+
+ // process a pane waiting to acquire the monitor lock
+ if (q != NULL) {
+ SIMPLEQ_REMOVE_HEAD(&m->lock_queue, node);
+ if (!monitor_enter(m, q->cmdq, q->pane_id, (q->options & ~MONITOR_ENQUEUE)))
+ fatal("Failed to pass the monitor lock to the next pane");
+ if (!cmdq_free(q->cmdq))
+ cmdq_continue(q->cmdq);
+ free(q);
+ } else if (SIMPLEQ_EMPTY(&m->wait_stack)) {
+ // monitor is unlocked and no pane is in the wait stack
+ // so now remove and free the monitor node
+ RB_REMOVE(monitors, &monitors_head, m);
+ free(m->name);
+ free(m);
+ }
+}
+
+// Entry point
+
+static inline enum cmd_retval monitor_lock(const char *name, struct cmd_q *cmdq)
+{
+ if (monitor_enter(monitor_find_or_create(name), cmdq, get_pane_id(cmdq), MONITOR_ENQUEUE))
+ return (CMD_RETURN_NORMAL);
+
+ return (CMD_RETURN_WAIT);
+}
+
+static inline enum cmd_retval monitor_unlock(const char *name, struct cmd_q *cmdq)
+{
+ struct monitor_node *m;
+
+ m = monitor_find(name);
+
+ if (m == NULL || m->pane_id != get_pane_id(cmdq)) {
+ cmdq_error(cmdq, "Not holding the lock to '%s'", name);
+ return (CMD_RETURN_ERROR);
+ }
+
+ m->count--;
+
+ if (m->count == 0)
+ monitor_shift(m);
+
+ return (CMD_RETURN_NORMAL);
+}
+
+static inline enum cmd_retval monitor_wait(const char *name, struct cmd_q *cmdq)
+{
+ monitor_enter(monitor_find_or_create(name), cmdq, get_pane_id(cmdq), MONITOR_ENQUEUE | MONITOR_WAIT);
+ return (CMD_RETURN_WAIT);
+}
+
+static inline enum cmd_retval monitor_signal(const char *name, struct cmd_q *cmdq)
+{
+ struct monitor_node *m;
+
+ m = monitor_find(name);
+
+ if (m != NULL)
+ if (!monitor_enter(m, cmdq, get_pane_id(cmdq), MONITOR_ENQUEUE | MONITOR_SIGNAL))
+ return (CMD_RETURN_WAIT);
+
+ return (CMD_RETURN_NORMAL);
+}
+
+enum cmd_retval cmd_monitor_exec(struct cmd *self, struct cmd_q *cmdq)
+{
+ struct args *args = self->args;
+ const char *name = args->argv[0];
+
+ if (args_has(args, 'l'))
+ return monitor_lock(name, cmdq);
+ else if (args_has(args, 'u'))
+ return monitor_unlock(name, cmdq);
+ else if (args_has(args, 'w'))
+ return monitor_wait(name, cmdq);
+ else if (args_has(args, 's'))
+ return monitor_signal(name, cmdq);
+
+ cmdq_error(cmdq, "Must specify an action to be executed on the monitor");
+
+ return (CMD_RETURN_ERROR);
+}
diff --git a/cmd.c b/cmd.c
index f3326df..c4386dc 100644
--- a/cmd.c
+++ b/cmd.c
@@ -71,6 +71,7 @@ const struct cmd_entry *cmd_table[] = {
&cmd_lock_session_entry,
&cmd_move_pane_entry,
&cmd_move_window_entry,
+ &cmd_monitor_entry,
&cmd_new_session_entry,
&cmd_new_window_entry,
&cmd_next_layout_entry,
diff --git a/examples/monitor.sh b/examples/monitor.sh
new file mode 100755
index 0000000..9157e45
--- /dev/null
+++ b/examples/monitor.sh
@@ -0,0 +1,120 @@
+#!/bin/bash
+
+# Shows how one can make panes synchronize work using the 'monitor' command
+#
+# The monitor object allows four basic operations which are executed
+# with mutual exclusion(meaning the monitor lock needs to be acquired
+# before executing)
+#
+# l: lock the monitor (primitive operation indirectly called by others)
+# u: releases the monitor lock
+# w: enter the monitor 'wait stack' and blocks until a signal is received
+# s: signal all panes waiting in the monitor stack. if the monitor
+# is locked, then it will block
+
+if [ -z $TMUX ]; then
+ echo "Start tmux first" >&2
+ exit 1
+fi
+
+kill_child_panes() {
+ tmux kill-pane -t $pane1
+ tmux kill-pane -t $pane2
+ tmux kill-pane -t $pane3
+ exit
+}
+abspath=$(cd ${0%/*} && echo $PWD/${0##*/})
+
+case $1 in
+ pane1)
+ tmux setw -q @pane1id $TMUX_PANE
+ tmux monitor -s pane1
+ tmux monitor -w pane1
+ tmux split-window -d -h "$abspath pane3"
+ tmux split-window -d -h "$abspath pane2"
+ tmux monitor -w pane1
+ columns=$(tput cols)
+ n=1
+ while true; do
+ for i in $(seq 1 $columns); do
+ sleep "0.1"
+ echo -n $n
+ done
+ echo
+ tmux monitor -s pane2
+ tmux monitor -w pane1
+ n=$(( ($n + 3) % 9 ))
+ done
+ ;;
+ pane2)
+ tmux setw -q @pane2id $TMUX_PANE
+ tmux monitor -s pane2
+ tmux monitor -w pane2
+ columns=$(tput cols)
+ n=2
+ while true; do
+ for i in $(seq 1 $columns); do
+ sleep "0.1"
+ echo -n $n
+ done
+ echo
+ tmux monitor -s pane3
+ tmux monitor -w pane2
+ n=$(( ($n + 3) % 9 ))
+ done
+ ;;
+ pane3)
+ tmux setw -q @pane3id $TMUX_PANE
+ tmux monitor -s pane3
+ tmux monitor -w pane3
+ columns=$(tput cols)
+ n=3
+ while true; do
+ for i in $(seq 1 $columns); do
+ sleep "0.1"
+ echo -n $n
+ done
+ echo
+ tmux monitor -s pane1
+ tmux monitor -w pane3
+ n=$(( ($n + 3) % 9 ))
+ done
+ ;;
+ *)
+ columns=$(tput cols)
+ trap kill_child_panes SIGINT SIGTERM
+ clear
+ echo "This is a simple script that shows how tmux can synchronize"
+ echo "code running in different panes."
+ echo
+ echo "Besides recursively spliting panes, it will run a simple animation"
+ echo "demonstrating the coordination between panes"
+ echo
+ sleep 1
+ echo "First split horizontally"
+ tmux split-window "$abspath pane1"
+ tmux monitor -w pane1
+ pane1=`tmux showw -v @pane1id`
+ sleep 1
+ echo "Now we split the child pane 2 times"
+ tmux monitor -s pane1
+ tmux monitor -w pane3
+ tmux monitor -w pane2
+ pane2=`tmux showw -v @pane2id`
+ pane3=`tmux showw -v @pane3id`
+ column_width=$(($columns / 3))
+ sleep 1
+ echo "Resize equally"
+ tmux resize-pane -t $pane1 -x $column_width
+ tmux resize-pane -t $pane2 -x $column_width
+ tmux resize-pane -t $pane3 -x $column_width
+ sleep 1
+ echo "Start animation"
+ tmux monitor -s pane1
+ tmux select-pane -t $TMUX_PANE
+ while true; do
+ sleep 1000
+ done
+ ;;
+esac
+
diff --git a/tmux.h b/tmux.h
index 0df1f4d..21b44f5 100644
--- a/tmux.h
+++ b/tmux.h
@@ -1793,6 +1793,7 @@ extern const struct cmd_entry cmd_lock_server_entry;
extern const struct cmd_entry cmd_lock_session_entry;
extern const struct cmd_entry cmd_move_pane_entry;
extern const struct cmd_entry cmd_move_window_entry;
+extern const struct cmd_entry cmd_monitor_entry;
extern const struct cmd_entry cmd_new_session_entry;
extern const struct cmd_entry cmd_new_window_entry;
extern const struct cmd_entry cmd_next_layout_entry;
------------------------------------------------------------------------------
Everyone hates slow websites. So do we.
Make your web apps faster with AppDynamics
Download AppDynamics Lite for free today:
http://p.sf.net/sfu/appdyn_d2d_feb
_______________________________________________
tmux-users mailing list
tmux-users@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/tmux-users