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

Reply via email to