This looks good to me On Wed, Mar 6, 2013 at 6:37 AM, Nicholas Marriott <nicholas.marri...@gmail.com> wrote: > Ok this looks pretty good. I've made a few changes but mainly style/layout > nits: > > - I don't think we need find and find_and_create functions, the find > will only be done once and the create twice (when there is lock too). > > - A single cmdq can only be waiting once so no need to have a wrapper > struct, just put the TAILQ_ENTRY in struct cmd_q itself. > > - channel_node -> wait_channel and similar renaming. > > - We always include sys/types.h, needed or not. Also errors always start > with lowercase. > > - Move things about to sort of vaguely fit where they go in other > commands. > > - Add to man page. > > I considered putting structs in tmux.h but I can't see how they would be > needed outside this file so let's leave them local anyway. tmux.h is too > big already. > > Please take a look and make sure there isn't anything stupid and then I > will commit and we can modify to add locking. > > > diff --git a/Makefile.am b/Makefile.am > index 5caa498..19220d8 100644 > --- a/Makefile.am > +++ b/Makefile.am > @@ -135,6 +135,7 @@ dist_tmux_SOURCES = \ > cmd-switch-client.c \ > cmd-unbind-key.c \ > cmd-unlink-window.c \ > + cmd-wait-for.c \ > cmd.c \ > colour.c \ > control.c \ > diff --git a/cmd-wait-for.c b/cmd-wait-for.c > new file mode 100644 > index 0000000..6313358 > --- /dev/null > +++ b/cmd-wait-for.c > @@ -0,0 +1,124 @@ > +/* $Id$ */ > + > +/* > + * Copyright (c) 2013 Nicholas Marriott <n...@users.sourceforge.net> > + * 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. > + */ > + > +#include <sys/types.h> > + > +#include <stdlib.h> > +#include <string.h> > + > +#include "tmux.h" > + > +/* > + * Block or wake a client on a named wait channel. > + */ > + > +enum cmd_retval cmd_wait_for_exec(struct cmd *, struct cmd_q *); > + > +const struct cmd_entry cmd_wait_for_entry = { > + "wait-for", "wait", > + "S", 1, 1, > + "[-S] channel", > + 0, > + NULL, > + NULL, > + cmd_wait_for_exec > +}; > + > +struct wait_channel { > + const char *name; > + TAILQ_HEAD(, cmd_q) waiters; > + > + RB_ENTRY(wait_channel) entry; > +}; > +RB_HEAD(wait_channels, wait_channel); > +struct wait_channels wait_channels = RB_INITIALIZER(wait_channels); > + > +int wait_channel_cmp(struct wait_channel *, struct wait_channel *); > +RB_PROTOTYPE(wait_channels, wait_channel, entry, wait_channel_cmp); > +RB_GENERATE(wait_channels, wait_channel, entry, wait_channel_cmp); > + > +int > +wait_channel_cmp(struct wait_channel *wc1, struct wait_channel *wc2) > +{ > + return (strcmp(wc1->name, wc2->name)); > +} > + > +enum cmd_retval cmd_wait_for_signal(struct cmd_q *, const char *, > + struct wait_channel *); > +enum cmd_retval cmd_wait_for_wait(struct cmd_q *, const char *, > + struct wait_channel *); > + > +enum cmd_retval > +cmd_wait_for_exec(struct cmd *self, struct cmd_q *cmdq) > +{ > + struct args *args = self->args; > + const char *name = args->argv[0]; > + struct wait_channel *wc, wc0; > + > + wc0.name = name; > + wc = RB_FIND(wait_channels, &wait_channels, &wc0); > + > + if (args_has(args, 'S')) > + return (cmd_wait_for_signal(cmdq, name, wc)); > + return (cmd_wait_for_wait(cmdq, name, wc)); > +} > + > +enum cmd_retval > +cmd_wait_for_signal(struct cmd_q *cmdq, const char *name, > + struct wait_channel *wc) > +{ > + struct cmd_q *wq, *wq1; > + > + if (wc == NULL || TAILQ_EMPTY(&wc->waiters)) { > + cmdq_error(cmdq, "no waiting clients on %s", name); > + return (CMD_RETURN_ERROR); > + } > + > + TAILQ_FOREACH_SAFE(wq, &wc->waiters, waitentry, wq1) { > + TAILQ_REMOVE(&wc->waiters, wq, waitentry); > + if (!cmdq_free(wq)) > + cmdq_continue(wq); > + } > + RB_REMOVE(wait_channels, &wait_channels, wc); > + free((void*) wc->name); > + free(wc); > + > + return (CMD_RETURN_NORMAL); > +} > + > +enum cmd_retval > +cmd_wait_for_wait(struct cmd_q *cmdq, const char *name, > + struct wait_channel *wc) > +{ > + if (cmdq->client == NULL || cmdq->client->session != NULL) { > + cmdq_error(cmdq, "not able to wait"); > + return (CMD_RETURN_ERROR); > + } > + > + if (wc == NULL) { > + wc = xmalloc(sizeof *wc); > + wc->name = xstrdup(name); > + TAILQ_INIT(&wc->waiters); > + RB_INSERT(wait_channels, &wait_channels, wc); > + } > + TAILQ_INSERT_TAIL(&wc->waiters, cmdq, waitentry); > + cmdq->references++; > + > + return (CMD_RETURN_WAIT); > +} > diff --git a/cmd.c b/cmd.c > index 0d6a85f..20484ed 100644 > --- a/cmd.c > +++ b/cmd.c > @@ -112,6 +112,7 @@ const struct cmd_entry *cmd_table[] = { > &cmd_switch_client_entry, > &cmd_unbind_key_entry, > &cmd_unlink_window_entry, > + &cmd_wait_for_entry, > NULL > }; > > diff --git a/tmux.1 b/tmux.1 > index 1a9c058..8df7975 100644 > --- a/tmux.1 > +++ b/tmux.1 > @@ -3553,6 +3553,19 @@ If the command doesn't return success, the exit status > is also displayed. > .It Ic server-info > .D1 (alias: Ic info ) > Show server information and terminal details. > +.It Xo Ic wait-for > +.Fl S > +.Ar channel > +.Xc > +.D1 (alias: Ic wait ) > +When used without > +.Fl S , > +prevents the client from exiting until woken using > +.Ic wait-for > +.Fl S > +with the same channel. > +This command only works from outside > +.Nm . > .El > .Sh TERMINFO EXTENSIONS > .Nm > diff --git a/tmux.h b/tmux.h > index c1ad662..e58c1de 100644 > --- a/tmux.h > +++ b/tmux.h > @@ -1416,6 +1416,8 @@ struct cmd_q { > void *data; > > struct msg_command_data *msgdata; > + > + TAILQ_ENTRY(cmd_q) waitentry; > }; > > /* Command definition. */ > @@ -1835,6 +1837,7 @@ extern const struct cmd_entry cmd_switch_client_entry; > extern const struct cmd_entry cmd_unbind_key_entry; > extern const struct cmd_entry cmd_unlink_window_entry; > extern const struct cmd_entry cmd_up_pane_entry; > +extern const struct cmd_entry cmd_wait_for_entry; > > /* cmd-attach-session.c */ > enum cmd_retval cmd_attach_session(struct cmd_q *, const char*, int, > int); > > > > > On Tue, Mar 05, 2013 at 10:55:05PM -0300, Thiago Padilha wrote: >> Here it goes: >> >> The first patch implements wait/signal, the second extends it with >> lock/unlock. My goal this time was to make the code small and simple >> as possible, let me know if you think anything needs to be refactored. >> --- >> Makefile.am | 1 + >> cmd-wait-for.c | 145 >> ++++++++++++++++++++++++++++++++++++++++++++++ >> cmd.c | 1 + >> examples/tmux-wait-for.sh | 109 ++++++++++++++++++++++++++++++++++ >> tmux.h | 1 + >> 5 files changed, 257 insertions(+) >> create mode 100644 cmd-wait-for.c >> create mode 100755 examples/tmux-wait-for.sh > >> diff --git a/Makefile.am b/Makefile.am >> index 5caa498..19220d8 100644 >> --- a/Makefile.am >> +++ b/Makefile.am >> @@ -135,6 +135,7 @@ dist_tmux_SOURCES = \ >> cmd-switch-client.c \ >> cmd-unbind-key.c \ >> cmd-unlink-window.c \ >> + cmd-wait-for.c \ >> cmd.c \ >> colour.c \ >> control.c \ >> diff --git a/cmd-wait-for.c b/cmd-wait-for.c >> new file mode 100644 >> index 0000000..658109b >> --- /dev/null >> +++ b/cmd-wait-for.c >> @@ -0,0 +1,145 @@ >> +/* $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. >> + */ >> + >> +#include <stdlib.h> >> +#include <string.h> >> + >> +#include "tmux.h" >> + >> +struct cmdq_node { >> + struct cmd_q *cmdq; >> + TAILQ_ENTRY(cmdq_node) node; >> +}; >> + >> +struct channel_node { >> + char *name; >> + TAILQ_HEAD(, cmdq_node) waiting_cmdqs; >> + RB_ENTRY(channel_node) node; >> +}; >> +RB_HEAD(channels, channel_node) channels_head = >> RB_INITIALIZER(&channels_head); >> + >> +enum cmd_retval cmd_wait_for_exec(struct cmd *, struct cmd_q *); >> +enum cmd_retval channel_wait(const char *, struct cmd_q *); >> +enum cmd_retval channel_signal(const char *, struct cmd_q *); >> +struct channel_node *channels_find(const char *); >> +struct channel_node *channels_find_or_create(const char *); >> +int channel_cmp(struct channel_node *, struct channel_node *); >> +RB_PROTOTYPE(channels, channel_node, node, channel_cmp); >> + >> +const struct cmd_entry cmd_wait_for_entry = { >> + "wait-for", "wait", >> + "S", 1, 1, >> + "[-S] channel", >> + 0, >> + NULL, >> + NULL, >> + cmd_wait_for_exec >> +}; >> + >> +enum cmd_retval >> +cmd_wait_for_exec(struct cmd *self, struct cmd_q *cmdq) >> +{ >> + struct args *args = self->args; >> + const char *name = args->argv[0]; >> + >> + if (args_has(args, 'S')) >> + return channel_signal(name, cmdq); >> + >> + return channel_wait(name, cmdq); >> +} >> + >> +RB_GENERATE(channels, channel_node, node, channel_cmp); >> + >> +int >> +channel_cmp(struct channel_node *c1, struct channel_node *c2) >> +{ >> + return (strcmp(c1->name, c2->name)); >> +} >> + >> +struct channel_node * >> +channels_find(const char *name) >> +{ >> + struct channel_node c_name; >> + c_name.name = (char *)name; >> + return RB_FIND(channels, &channels_head, &c_name); >> +} >> + >> +struct channel_node * >> +channels_find_or_create(const char *name) >> +{ >> + struct channel_node *c; >> + >> + c = channels_find(name); >> + >> + if (c == NULL) { >> + c = xmalloc(sizeof *c); >> + c->name = xstrdup(name); >> + TAILQ_INIT(&c->waiting_cmdqs); >> + RB_INSERT(channels, &channels_head, c); >> + } >> + >> + return c; >> +} >> + >> +enum cmd_retval >> +channel_wait(const char *name, struct cmd_q *cmdq) >> +{ >> + struct cmdq_node *wq; >> + struct channel_node *c; >> + >> + if (cmdq->client == NULL || cmdq->client->session != NULL) { >> + cmdq_error(cmdq, "Not able to wait"); >> + return (CMD_RETURN_ERROR); >> + } >> + >> + c = channels_find_or_create(name); >> + wq = xmalloc(sizeof *wq); >> + wq->cmdq = cmdq; >> + TAILQ_INSERT_HEAD(&c->waiting_cmdqs, wq, node); >> + cmdq->references++; >> + >> + return (CMD_RETURN_WAIT); >> +} >> + >> +enum cmd_retval >> +channel_signal(const char *name, struct cmd_q *cmdq) >> +{ >> + struct cmdq_node *wq; >> + struct channel_node *c; >> + >> + c = channels_find(name); >> + >> + if (c == NULL || TAILQ_EMPTY(&c->waiting_cmdqs)) { >> + cmdq_error(cmdq, "No waiting clients"); >> + return (CMD_RETURN_ERROR); >> + } >> + >> + while ((wq = TAILQ_FIRST(&c->waiting_cmdqs)) != NULL) { >> + TAILQ_REMOVE(&c->waiting_cmdqs, wq, node); >> + if (!cmdq_free(wq->cmdq)) >> + cmdq_continue(wq->cmdq); >> + free(wq); >> + } >> + >> + RB_REMOVE(channels, &channels_head, c); >> + free(c->name); >> + free(c); >> + >> + return (CMD_RETURN_NORMAL); >> +} >> diff --git a/cmd.c b/cmd.c >> index 0d6a85f..20484ed 100644 >> --- a/cmd.c >> +++ b/cmd.c >> @@ -112,6 +112,7 @@ const struct cmd_entry *cmd_table[] = { >> &cmd_switch_client_entry, >> &cmd_unbind_key_entry, >> &cmd_unlink_window_entry, >> + &cmd_wait_for_entry, >> NULL >> }; >> >> diff --git a/examples/tmux-wait-for.sh b/examples/tmux-wait-for.sh >> new file mode 100755 >> index 0000000..94baa90 >> --- /dev/null >> +++ b/examples/tmux-wait-for.sh >> @@ -0,0 +1,109 @@ >> +#!/bin/bash >> + >> +# Shows how one can synchronize work using the 'wait' command >> + >> +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 wait -S pane1 >> + tmux wait pane1 >> + tmux split-window -d -h "$abspath pane3" >> + tmux split-window -d -h "$abspath pane2" >> + tmux wait 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 wait -S pane2 >> + tmux wait pane1 >> + n=$(( ($n + 3) % 9 )) >> + done >> + ;; >> + pane2) >> + tmux setw -q @pane2id $TMUX_PANE >> + tmux wait -S pane2 >> + tmux wait 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 wait -S pane3 >> + tmux wait pane2 >> + n=$(( ($n + 3) % 9 )) >> + done >> + ;; >> + pane3) >> + tmux setw -q @pane3id $TMUX_PANE >> + tmux wait -S pane3 >> + tmux wait 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 wait -S pane1 >> + tmux wait 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 wait pane1 >> + pane1=`tmux showw -v @pane1id` >> + sleep 1 >> + echo "Now we split the child pane 2 times" >> + tmux wait -S pane1 >> + tmux wait pane3 >> + tmux wait 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 wait -S pane1 >> + tmux select-pane -t $TMUX_PANE >> + while true; do >> + sleep 1000 >> + done >> + ;; >> +esac >> diff --git a/tmux.h b/tmux.h >> index c1ad662..95dd97c 100644 >> --- a/tmux.h >> +++ b/tmux.h >> @@ -1835,6 +1835,7 @@ extern const struct cmd_entry cmd_switch_client_entry; >> extern const struct cmd_entry cmd_unbind_key_entry; >> extern const struct cmd_entry cmd_unlink_window_entry; >> extern const struct cmd_entry cmd_up_pane_entry; >> +extern const struct cmd_entry cmd_wait_for_entry; >> >> /* cmd-attach-session.c */ >> enum cmd_retval cmd_attach_session(struct cmd_q *, const char*, >> int, int); > >> diff --git a/cmd-wait-for.c b/cmd-wait-for.c >> index 658109b..58e53c6 100644 >> --- a/cmd-wait-for.c >> +++ b/cmd-wait-for.c >> @@ -29,7 +29,9 @@ struct cmdq_node { >> >> struct channel_node { >> char *name; >> + int locked; >> TAILQ_HEAD(, cmdq_node) waiting_cmdqs; >> + TAILQ_HEAD(, cmdq_node) locking_cmdqs; >> RB_ENTRY(channel_node) node; >> }; >> RB_HEAD(channels, channel_node) channels_head = >> RB_INITIALIZER(&channels_head); >> @@ -37,6 +39,8 @@ RB_HEAD(channels, channel_node) channels_head = >> RB_INITIALIZER(&channels_head); >> enum cmd_retval cmd_wait_for_exec(struct cmd *, struct cmd_q *); >> enum cmd_retval channel_wait(const char *, struct cmd_q *); >> enum cmd_retval channel_signal(const char *, struct cmd_q *); >> +enum cmd_retval channel_lock(const char *, struct cmd_q *); >> +enum cmd_retval channel_unlock(const char *, struct cmd_q *); >> struct channel_node *channels_find(const char *); >> struct channel_node *channels_find_or_create(const char *); >> int channel_cmp(struct channel_node *, struct channel_node *); >> @@ -44,8 +48,8 @@ RB_PROTOTYPE(channels, channel_node, node, channel_cmp); >> >> const struct cmd_entry cmd_wait_for_entry = { >> "wait-for", "wait", >> - "S", 1, 1, >> - "[-S] channel", >> + "LSU", 1, 1, >> + "[-L | -S | -U] channel", >> 0, >> NULL, >> NULL, >> @@ -61,6 +65,12 @@ cmd_wait_for_exec(struct cmd *self, struct cmd_q *cmdq) >> if (args_has(args, 'S')) >> return channel_signal(name, cmdq); >> >> + if (args_has(args, 'L')) >> + return channel_lock(name, cmdq); >> + >> + if (args_has(args, 'U')) >> + return channel_unlock(name, cmdq); >> + >> return channel_wait(name, cmdq); >> } >> >> @@ -90,7 +100,9 @@ channels_find_or_create(const char *name) >> if (c == NULL) { >> c = xmalloc(sizeof *c); >> c->name = xstrdup(name); >> + c->locked = 0; >> TAILQ_INIT(&c->waiting_cmdqs); >> + TAILQ_INIT(&c->locking_cmdqs); >> RB_INSERT(channels, &channels_head, c); >> } >> >> @@ -137,9 +149,67 @@ channel_signal(const char *name, struct cmd_q *cmdq) >> free(wq); >> } >> >> - RB_REMOVE(channels, &channels_head, c); >> - free(c->name); >> - free(c); >> + if (!c->locked) { >> + RB_REMOVE(channels, &channels_head, c); >> + free(c->name); >> + free(c); >> + } >> >> return (CMD_RETURN_NORMAL); >> } >> + >> +enum cmd_retval >> +channel_lock(const char *name, struct cmd_q *cmdq) >> +{ >> + struct cmdq_node *lq; >> + struct channel_node *c; >> + >> + if (cmdq->client == NULL || cmdq->client->session != NULL) { >> + cmdq_error(cmdq, "Not able to lock"); >> + return (CMD_RETURN_ERROR); >> + } >> + >> + c = channels_find_or_create(name); >> + >> + if (c->locked) { >> + lq = xmalloc(sizeof *lq); >> + lq->cmdq = cmdq; >> + TAILQ_INSERT_TAIL(&c->locking_cmdqs, lq, node); >> + cmdq->references++; >> + return (CMD_RETURN_WAIT); >> + } >> + >> + c->locked = 1; >> + return (CMD_RETURN_NORMAL); >> +} >> + >> +enum cmd_retval >> +channel_unlock(const char *name, struct cmd_q *cmdq) >> +{ >> + struct cmdq_node *lq; >> + struct channel_node *c; >> + >> + c = channels_find(name); >> + >> + if (c == NULL || !c->locked) { >> + cmdq_error(cmdq, "Channel not locked"); >> + return (CMD_RETURN_ERROR); >> + } >> + >> + if ((lq = TAILQ_FIRST(&c->locking_cmdqs)) != NULL) { >> + TAILQ_REMOVE(&c->locking_cmdqs, lq, node); >> + if (!cmdq_free(lq->cmdq)) >> + cmdq_continue(lq->cmdq); >> + free(lq); >> + } else { >> + c->locked = 0; >> + if (TAILQ_EMPTY(&c->waiting_cmdqs)) { >> + RB_REMOVE(channels, &channels_head, c); >> + free(c->name); >> + free(c); >> + } >> + } >> + >> + return (CMD_RETURN_NORMAL); >> +} >> + > >> ------------------------------------------------------------------------------ >> Symantec Endpoint Protection 12 positioned as A LEADER in The Forrester >> Wave(TM): Endpoint Security, Q1 2013 and "remains a good choice" in the >> endpoint security space. For insight on selecting the right partner to >> tackle endpoint security challenges, access the full report. >> http://p.sf.net/sfu/symantec-dev2dev > >> _______________________________________________ >> tmux-users mailing list >> tmux-users@lists.sourceforge.net >> https://lists.sourceforge.net/lists/listinfo/tmux-users >
------------------------------------------------------------------------------ Symantec Endpoint Protection 12 positioned as A LEADER in The Forrester Wave(TM): Endpoint Security, Q1 2013 and "remains a good choice" in the endpoint security space. For insight on selecting the right partner to tackle endpoint security challenges, access the full report. http://p.sf.net/sfu/symantec-dev2dev _______________________________________________ tmux-users mailing list tmux-users@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/tmux-users