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, or set-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 sets remain-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 its remain-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 inelegant sleep 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 execing 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 same channel. […]

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 waits. Exactly one wait foo can defeat a race condition. More than one wait foo at a time creates yet another race condition.