4

Quite often I send nvim, ranger or other applications to background with Ctrl+z and then forget in which exactly pane it was open - brute-forcing tens of panes is quite tedious task.

Is there anything similar to ${pane_current_command}, but for background task?

#(echo $(jobs -l)) doesn't work probably because always attached to a different session.

chuwy
  • 141

1 Answers1

3

Is there anything similar to ${pane_current_command}, but for background task?

No. It's not as easy as you think.

#(echo $(jobs -l)) doesn't work probably because always attached to a different session.

jobs is a shell builtin. The command will show you jobs of the shell that runs it. In your case everything depends on how you run the snippet and how you quote it.

  • If you run tmux … that uses this snippet directly in a shell whose jobs you want to know and you don't single-quote $(), so it is expanded by the shell before tmux executable even runs, then it will kinda work ("kinda", because proper quoting is advised and echo is not the best tool here; consider printf '%s\n').

  • If you run tmux … that uses this snippet in a shell that is not the shell whose jobs you want to know and you don't single-quote $(), it will be expanded (i.e. jobs will be run) by the wrong shell.

  • If you

    • run tmux … that uses this snippet and you single-quote $()
    • or there is no outer shell that could expand $() before tmux executable runs
    • or you use the snippet directly in tmux (prefix: or via a key binding)

    then $() will be expanded by a shell spawned by tmux to process the inside of #(). Note in this case echo or printf is not needed or even harmful. It doesn't matter much because the shell that processes what's in #() is not the one you want anyway.

I'm not aware of any way (interface) in any shell that allows you to query the shell for its jobs from the outside.


Injecting a command (don't)

One cumbersome thing you can do is to inject jobs -lEnter into the actual shell running in a pane, as if you typed it; tmux can do this for sure (send-keys). Then you need to capture and isolate the output; tmux probably can do this. But:

  • the pane may be in copy mode or something;
  • the process in the pane may not be a shell;
  • or it may be a shell busy running something (keys will go to a child program);
  • or its command line may not be empty when you start injecting;
  • and even if injecting goes well, there can be other (background) processes that output to the console.

This approach is very unreliable. You can improve it by making the command itself request tmux to print something. Example:

tmux run-shell "printf '%s\n' '$(jobs -l)'"

If you run it in a shell whose jobs you want to know, double-quoted $() (it is double-quoted) will be expanded by this shell so jobs will return what you want. Finally printf will run in another shell that gets the output of jobs as a string (so this doesn't apply).

But before printf runs, tmux will interpret the string and the inner shell will interpret the string. If jobs outputs anything that can be expanded by tmux run-shell (e.g. #{pane_id}) then it will be expanded. If it outputs single-quotes then it will close our single-quote prematurely. Syntax error may occur but it's not the worst that can happen. Code injection may happen. Sanitizing output from jobs may help but it's not easy to do this properly.

To automate it you need to inject the command to a shell (in practice: many shells). As we saw, injecting by sending keys is troublesome. There is another way.


Shell trap (not really useful)

If you defined this function in every shell opened in tmux:

_tmux_show_jobs () { tmux run-shell -t "$TMUX_PANE" "printf '%s\n' '$(jobs -l)'"}

and then in every shell trap some signal so it triggers the function, and then make tmux iterate over available panes and send signals to #{pane_pid} – then you can make each shell pass the output of jobs -l to tmux, to be displayed in its pane. However:

  • Unwanted parsing and substituting can still occur. You can fix it by redirecting jobs -l to a temporary file (separate for each shell) and cating the file inside tmux run-shell.
  • Whatever signal you choose, you don't want to send it mindlessly to every pane. It's possible to create a pane that runs anything (I mean not necessarily a shell). It's possible to exec to anything from a shell. Arbitrary tools react to signals in their own way and you cannot assume any signal is safe. In particular SIGUSR1 and SIGUSR2 terminate the process by default. Other signals are meaningful and trapping them may trigger the function when you don't want it.
  • Even if you manage to make tmux send signals to shells only and trap the chosen signal in every single shell, in many circumstances shells won't react to signals immediately. I think this disqualifies the entire trap-based approach.

Without jobs (useful at last)

We can take advantage from the fact that you don't really want to know all jobs or background tasks. You want to find tasks that are stopped. These can be queried for from the outside. Consider this basic code:

#!/bin/sh

unset IFS
for p in $(
   tmux list-panes -F '#{pane_id}'
)
do
   tmux run-shell -t "$p" 'ps -o s,pid,args -g #{pane_pid} | sed -e "/^T/!d" -e "s/. //"'
done

Notes:

  • for p in $( … ); do … is wrong in general but here the format is well defined, the syntax will work with default IFS. Additionally if you run the code from within tmux then there will be at least one pane, so $() will never generate exactly zero words (which would lead to syntax error).
  • Instead of -g #{pane_pid} it could be --ppid #{pane_pid} but
    • it's not POSIX;
    • -g includes grandchildren, so if you run (not exec) a shell from within original shell, or sudo su -, and then run another process and stop it, the code will still find it; I think it's better.
  • list-panes by default lists all panes in the current window. If you'd like to iterate over all panes in the current session, then use list-panes -s.

There's a disadvantage though. The result (if not empty) for each pane will be displayed in the pane in copy mode. But if the pane is already in some non-default mode, the result may never be displayed or be appended to whatever is displayed. In particular if the results are visible and you run the code for the second time, duplicate lines will appear.

The only way I found to make sure the pane is cleared before printing the result and the result always appears is to quit whatever non-default mode the pane is in. The below code does it. It deliberately doesn't change modes if the result is empty.

#!/bin/sh

unset IFS
tmux list-panes -F '#{pane_id} #{pane_pid}' | while
   read -r id pid
do
   output="$(ps -o s,pid,args -g "$pid" | sed -e '/^T/!d' -e 's/. //')"
   [ "$output" ] && {
      [ "$(tmux display-message -p -t "$id" "#{pane_in_mode}")" -eq 1 ] && tmux send-keys -t "$id" q
      tmux run-shell -t "$id" "printf 'Stopped processes:\n \n%s\n' '$output'"
   }
done
exit 0

I saved it as _tmux-find-stopped in a directory where my $PATH points to. I added this line to my ~/.tmux.conf:

bind -T prefix j run-shell '_tmux-find-stopped'

After I prefix:source-file ~/.tmux.confEnter, I can find stopped processes with prefixj. New tmux servers will source the file by default.