less stops my script; why is that and how to avoid?

I have this Bash script named as s in current directory:

#!/bin/bash
pipe_test() {
    ( set -m; (
        $1
    ); set +m ) | 
    (
        $2
    )
}
pipe_test "$1" "$2"

If I call e.g.

./s yes less

the script gets stopped. (Similar thing happens if I use any other pager I tried instead of less, i.e. more and most.) I can continue it by fg builtin, though.

I want to have job control (enabled by set -m) for the subshell to have a distinct process group ID for the processes of the subshell.

Information about my system:

$ bashbug
...
Machine: x86_64
OS: linux-gnu
Compiler: gcc
Compilation CFLAGS: -g -O2 -fdebug-prefix-map=/build/bash-cP61jF/bash-5.0=. -fstack-protector-strong -Wformat -Werror=format->
uname output: Linux jarnos-OptiPlex-745 5.4.0-29-generic #33-Ubuntu SMP Wed Apr 29 14:32:27 UTC 2020 x86_64 x86_64 x86_64 GNU>
Machine Type: x86_64-pc-linux-gnu

Bash Version: 5.0
Patch Level: 16
Release Status: release
$ less --version
less version: 551

Here is Solutions:

We have many solutions to this problem, But we recommend you to use the first solution because it is tested & true solution that will 100% work for you.

Solution 1

The reason why that happens is because enabling job control (set -m) brings along not just process grouping, but also the machinery for handling “foreground” and “background” jobs. This “machinery” implies that each command run in turn while job control is enabled becomes the foreground process group.

Therefore, in short, when that sub-shell (the left part of your pipeline) enables job control it literally steals the terminal from the entire pipeline, which had it until then and which, in your example, includes the less process, thus making it become background and, as such, not allowed to use the terminal any more. It therefore gets stopped because less does keep accessing the terminal.

By issuing fg you give the terminal back to the entire pipeline, hence to less, and all ends well. Unless you run additional commands within the job-controlling sub-shell, because in such case each additional command would steal the terminal again.

One way around it is to simply run your job-controlled sub-sub-shell in background:

( set -m; (
        $1
    ) & set +m ) | 
    (
        $2
    )

You will have the command expressed by $1 run in its distinct process group as you wish, while the backgrounded mode prevents stealing the terminal, thus leaving it to the pipeline and hence to $2.

Naturally this requires that the command in $1 does not want to read the terminal itself, otherwise it will be the one to get stopped as soon as it attempts to do it.

Also, likewise to as I said above, any additional job-controlled sub-sub-shell you might like to add would require the same “backgrounding” treatment, all along until you set +m, otherwise each additional job-controlled sub-sub-shell would steal the terminal again.

That said, if all you need process grouping for is to kill processes, you might consider using pkill to target them. For instance pkill -P will send a signal to the processes whose parent is the indicated PID. This way you can target all children (but not grand-children) of your sub-process by just knowing the sub-process’s PID.

Solution 2

Removing the set -m solves the problem (what is that to do anyway?).

Three processes are stopped by the kernel via SIGTTOU:

  • the script process
  • a subshell
  • less

But not yes. Its process is put into a separate process group; probably by the set -m. So the kernel tries to hit all processes in that pipeline but misses one. This missing is not the reason for the “stopped” message, though.

Usually SIGTTOU is caused by a background process trying to write to the terminal. But that is not the only possible reason:

int SIGTTOU
This is similar to SIGTTIN, but is generated when a process in a background job attempts to write to the terminal or set its modes. Again, the default action is to stop the process. SIGTTOU is only generated for an attempt to write to the terminal if the TOSTOP output mode is set; see Output Modes.

See https://www.gnu.org/software/libc/manual/html_node/Job-Control-Signals.html

The last syscall before the is (by less):

ioctl(3, SNDCTL_TMR_STOP or TCSETSW, {B38400 opost isig -icanon -echo ...}) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)

So my assessment is that for some strange reason (i.e. set -m) the pipeline is put in the background. There are several syscalls like

ioctl(255, TIOCSPGRP, [23715]

by different processes. The last one is by the subshell

ioctl(2, TIOCSPGRP, [23718]) = 0

making yes the foreground process group after making it the leader of its own process group (with no other members) by

setpgid(23718, 23718 <unfinished ...>

Note: Use and implement solution 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply