bash : Executing blocking script in Tmux
Im trying to run 2 scripts in 2 different TMUX panes from a bash file. The problem is that they are all blocking, so once I execute a process from one pane, I can't move to the other pane to execute the other job.
How can I overcome this issue ?
I'll post the code sample.
#! /bin/bash
tmux split-window -v
tmux select-pane -t 0
./blocking_script_1
tmux select-pane -t 1 #doesnt happen
./blocking-script_2 #doesnt happen
Thanks
Liam
Tmux does not magically alter the flow of your script
I'm trying to run 2 scripts in 2 different TMUX panes from a bash file. The problem is that they are all blocking, so once I execute a process from one pane, I can't move to the other pane to execute the other job.
You seem to think that after this line
tmux select-pane -t 1
the next line will execute in the selected pane. This is not true. Even if your main script runs both scripts in the background, they will still run where the main script runs.
Sending keys to run something in tmux is hardly ever the right way
The other answer advises send-keys
to inject commands to other panes. This is cumbersome and not really reliable (e.g. you need to make sure there's an idle shell there, with empty command line).
The right way to run something in tmux
Run your scripts in newly created panes. Example:
# create a new window and run the first script
tmux new-window -n my-scripts -c "$PWD" ./blocking_script_1
# split the newly created window and run the second script in a new pane
tmux split-window -v -c "$PWD" -t :my-scripts ./blocking-script_2
Or if you run the main script in a pane, you can run one of the target scripts normally as a part of the main script after you start the other in another pane:
# split the current window and run the second script in a new pane
tmux split-window -v -c "$PWD" ./blocking-script_2
# run the first script normally
./blocking_script_1
If you want to utilize an existing pane then you need respawn-pane
, possibly with -k
to kill whatever runs in the target pane at the moment (in this case do not target the pane your main script runs in, unless it's the last thing the main script has to do).
You may find the -d
option of new-window
and split-window
useful. It makes the newly created window/pane not become current. From now on in this answer if I need to make any new pane current, I will create all panes with -d
and only finally select-window
or select-pane
once. This is to avoid mishaps when you paste any snippet in an interactive shell in tmux and tmux selects another pane before the whole snippet gets to the shell.
Examining results
Note any of the above commands that run ./blocking-script_2
actually runs a POSIX shell (sh
) in a pane. The shell interprets the command (./blocking-script_2
). After the script exits, the shell exits. Some implementations of sh
may be smart enough to detect they can exec
to ./blocking-script_2
in the first place. One way in another after the script exits there is no process in the pane, so normally the pane gets destroyed.
There are few options to see the output from ./blocking-script_2
after it exits:
- log the output and don't mind the pane will die;
-
instead of sole
./blocking-script_2
run the script and any command that doesn't exit by itself and doesn't generate (much) output, in sequence; the extra command will keep the pane alive; examples:# it can be an interactive shell tmux … './blocking-script_2; exec bash -i' # or simply a loop like this tmux … './blocking-script_2; while sleep 3600; do :; done'
use the
remain-on-exit
option for the window; in theory this seems the most elegant.
Problems with setting remain-on-exit on
Note this snippet is flawed:
# flawed
tmux new-window -dn my-scripts -c "$PWD" ./blocking_script_1
tmux set-window-option -t :my-scripts remain-on-exit on
tmux split-window -dvc "$PWD" -t :my-scripts ./blocking-script_2
tmux select-window -t :my-scripts
If ./blocking_script_1
exits soon enough, the window may be destroyed before other commands run; then they will not find the window and fail. Compare race condition. Few ideas:
-
Use
set remain-on-exit
to set a session option, orset-window-option -g remain-on-exit
to set a global window option. Example:# flawed tmux set-window-option -g remain-on-exit on tmux new-window -dn my-scripts -c "$PWD" ./blocking_script_1 tmux split-window -dvc "$PWD" -t :my-scripts ./blocking-script_2 tmux select-window -t :my-scripts
And later:
tmux set-window-option -gu remain-on-exit
All windows that don't have the option explicitly set will use the session option or the global window option. Such inheritance is somewhat complicated and I won't explain it here (see
man 1 tmux
). The option is checked when a pane dies, so you cannot change it back until both your scripts exit. This approach can affect many windows for a while. If some other pane process exits, the pane will not be destroyed, while normally it would be. -
Use
set set-remain-on-exit
to set a session option that setsremain-on-exit
for new windows at the moment they are created:# flawed tmux set set-remain-on-exit on tmux new-window -dn my-scripts -c "$PWD" ./blocking_script_1 tmux set -u set-remain-on-exit tmux split-window -dvc "$PWD" -t :my-scripts ./blocking-script_2 tmux select-window -t :my-scripts
This does not affect existing windows at all. Still if yet another window is created (by another script or by you manually) and the timing is just right,
set-remain-on-exit
will set itsremain-on-exit
as well. -
Create a window with a dummy original pane that doesn't exit by itself; set the window up; spawn at least one pane that should survive; destroy the dummy pane:
# flawed (because of a bug) tmux new-window -dn my-scripts 'while sleep 100; do :; done' tmux set-window-option -t :my-scripts remain-on-exit on tmux split-window -dvc "$PWD" -t :my-scripts ./blocking-script_1 sleep 2 tmux kill-pane -t :my-scripts.0 tmux split-window -dvc "$PWD" -t :my-scripts ./blocking-script_2 tmux select-window -t :my-scripts
I discovered if I
kill-pane
the dummy pane too soon after creating it then my tmux server will crash. This shouldn't happen, it looks like a bug. The inelegantsleep 2
will probably work in practice; in theory however any delay may not be long enough in some circumstances. -
Let each pane in the new window set the right option before
exec
ing to a target script:# not flawed (AFAIK), cumbersome tmux new-window -dn my-scripts -c "$PWD" 'tmux set-window-option -t :my-scripts remain-on-exit on; exec ./blocking_script_1' tmux split-window -dvc "$PWD" -t :my-scripts 'tmux set-window-option -t :my-scripts remain-on-exit on; exec ./blocking-script_2' tmux select-window -t :my-scripts
"Each pane" because in theory it's possible the second pane dies before the first shell runs
tmux set-window-option …
. If this happens and only the first shell tries to change the option, the pane that held the second script will be destroyed.The more scripts you want to run, the more cumbersome this approach becomes.
Dealing with race conditions
As you can see the original flawed snippet suffers from possible race condition. When we try to fix it, new race conditions appear. This is common when working with tmux this way. tmux new-window …
is a command for the tmux server, tmux
here is just a client. After the client exits successfully, you can be sure a new window has been created; but you cannot really know what happens in it, on what stage, or if the window has not been already destroyed.
In your particular case running the main script in a shell inside tmux and targeting its window can guarantee the window exists. The first thing the script does should be setting remain-on-exit on
.
Still the right general way to deal with race conditions in tmux is wait-for
:
wait-for [-L | -S | -U] channel
(alias:wait
)When used without options, prevents the client from exiting until woken using
wait-for -S
with the samechannel
. […]
My tests indicate that tmux wait -S foo
never waits. When it's invoked, all waiting tmux wait foo
are woken, they exit. But if there is no tmux wait foo
waiting then the "waking power" is postponed and the next (future) tmux wait foo
will not wait. This means both commands may be invoked in any order. If tmux wait foo
exits then you can be sure tmux wait -S foo
has occurred (just or in the past).
The original snippet fixed:
# robust (AFAIK)
tmux new-window -dn my-scripts -c "$PWD" 'tmux wait baz; exec ./blocking_script_1'
tmux set-window-option -t :my-scripts remain-on-exit on
tmux wait -S baz
tmux split-window -dvc "$PWD" -t :my-scripts ./blocking-script_2
tmux select-window -t :my-scripts
The shell in the first pane will get blocked with tmux wait
until after the main script configures the option. Then it doesn't matter how soon the first script exits.
An approach where both shells wait until everything is ready appeared aesthetically pleasing to me:
# flawed though
tmux new-window -dn my-scripts -c "$PWD" 'tmux wait baz; exec ./blocking_script_1'
tmux split-window -dvc "$PWD" -t :my-scripts 'tmux wait baz; exec ./blocking-script_2'
tmux set-window-option -t :my-scripts remain-on-exit on
tmux wait -S baz
tmux select-window -t :my-scripts
Then I realized although I can run wait
and wait -S
in any order, this is not true for 2xwait
and wait -S
:
- the order
wait
,wait
,wait -S
will unblock both shells; - any other order will unblock just one shell.
This can be generalized to more wait
s. Exactly one wait foo
can defeat a race condition. More than one wait foo
at a time creates yet another race condition.