If I understood your description correctly, I daresay that the wrapper program is not designed to spawn children that are interactive, otherwise it would take care of stopping (SIGSTOP) its children before accessing the tty and then resuming them (SIGCONT) as soon as it has finished with the tty. Apparently that is not the case, if it expects to be allowed to access the tty arbitrarily.
It can be fairly easy to make a helper program to be put between your SHLVL=1 and wrapper thus acting as cushion layer between the two so that your first shell does not detect wrapper being stopped; then this helper program would detect when wrapper gets stopped, and on such event it would stop wrapper’s children, give the tty back to wrapper and resume it. But then it wouldn’t be as well as easy to detect when wrapper has finished with the tty without active cooperation (i.e. notification) by wrapper itself. In fact, given the described behavior, I suspect wrapper actually does not put itself in the background nor does anything more than just going to sleep onto some blocking system-call.
However, if it really puts itself in the background, then the best you can do is to have the helper program continuously poll the tty about the current foreground process, and when that changes back to being wrapper's child, the helper program resumes it (if wrapper doesn’t do so itself, as I suspect).
That is, in general, for resuming the child, I’m afraid you need some specific event (or sequence of events), detectable from outside of wrapper, with which you can correctly infer that wrapper has indeed finished with the tty, and on such event(s) resume wrapper’s child.
For a case where it can be reasonable to have a solution where you manually resume the wrapper's child, here is a sample Python program that should handle the specific case:
#!/usr/bin/python3
import os
import sys
import signal
def main():
if len(sys.argv) < 2:
sys.exit(0)
def _noop_handler(sig, frame):
"""signal handler that does nothing"""
pass
termination_signals = {signal.SIGHUP, signal.SIGINT, signal.SIGTERM}
management_signals = {signal.SIGCHLD, signal.SIGCONT, signal.SIGTTIN,
signal.SIGUSR1, signal.SIGUSR2}
signal.pthread_sigmask(
signal.SIG_BLOCK,
management_signals | termination_signals
)
child = os.fork()
if child == 0: # child process after fork
signal.sigwait({signal.SIGUSR1}) # wait go-ahead signal from parent
signal.pthread_sigmask(
signal.SIG_UNBLOCK,
management_signals | termination_signals
)
os.execvp(sys.argv[1], sys.argv[1:]) # run command
elif child > 0: # parent process after fork
# I want to manipulate tty ownership freely, so ignore SIGTTOU
signal.signal(signal.SIGTTOU, signal.SIG_IGN)
# A handler for SIGCHLD is required on some systems where semantics
# for ignored signals is to never deliver them even to sigwait(2)
signal.signal(signal.SIGCHLD, _noop_handler)
in_fd = sys.stdin.fileno()
my_pid = os.getpid()
ppid = os.getppid()
os.setpgid(child, child) # put child in its own process group
if os.tcgetpgrp(in_fd) == my_pid:
# if I have been given the tty, hand it over to child
# This is not the case when shell spawned me in "background" &
os.tcsetpgrp(in_fd, child)
os.kill(child, signal.SIGUSR1) # all set for child, make it go ahead
last_robbed_group = 0
# signals to care for child
io_wanted_signals = {signal.SIGTTIN, signal.SIGTTOU}
def _send_sig(_pgid, _sig, accept_myself=False) -> bool:
"""
send a signal to a process group if that is not my own or
if accept_myself kwarg is True, and ignore OSError exceptions
"""
if not accept_myself and _pgid == my_pid:
return True
try:
os.killpg(_pgid, _sig)
except OSError:
return False
return True
def _resume_child_if_appropriate():
"""
resume child unless that would steal tty from my own parent
"""
nonlocal last_robbed_group
fg_group = os.tcgetpgrp(in_fd)
if fg_group == os.getpgid(ppid):
# Minimal protection against stealing tty from parent shell.
# If this would be the case, rather stop myself too
_send_sig(my_pid, signal.SIGTTIN, accept_myself=True)
return
# Forcibly stop current tty owner
_send_sig(fg_group, signal.SIGSTOP)
if fg_group not in {os.getpgid(child), my_pid}:
# remember who you stole tty from
last_robbed_group = fg_group
# Resume child
os.tcsetpgrp(in_fd, os.getpgid(child))
_send_sig(os.getpgid(child), signal.SIGCONT)
waited_signals = termination_signals | management_signals
while True:
# Blocking loop over wait for signals
sig = signal.sigwait(waited_signals)
if sig in termination_signals:
# Propagate termination signal and then exit
_send_sig(os.getpgid(child), sig)
os.wait()
sys.exit(128 + sig)
elif sig == signal.SIGCONT:
# CONT received, presumably from parent shell, propagate it
_resume_child_if_appropriate()
elif sig == signal.SIGTTIN:
# TTIN received, presumably from myself
prev_fg = os.tcgetpgrp(in_fd)
# Stop current tty owner if not my own parent
if prev_fg != os.getpgid(ppid):
_send_sig(prev_fg, signal.SIGSTOP)
try:
# Give tty back to my own parent and stop myself
os.tcsetpgrp(in_fd, os.getpgid(ppid))
_send_sig(my_pid, signal.SIGSTOP, accept_myself=True)
except OSError:
try:
# ugh, parent unreachable, restore things
os.tcsetpgrp(in_fd, prev_fg)
_send_sig(prev_fg, signal.SIGCONT)
except OSError:
# Non-restorable situation ? let's idle then
os.tcsetpgrp(in_fd, my_pid)
elif sig == signal.SIGCHLD:
# Event related to child, let's investigate it
pid, status = os.waitpid(child, os.WNOHANG | os.WUNTRACED)
if pid > 0:
if os.WIFSIGNALED(status):
# Child terminated by signal, let's propagate this
sys.exit(128 + os.WTERMSIG(status))
elif os.WIFEXITED(status):
# Child exited normally, let's propagate this
sys.exit(os.WEXITSTATUS(status))
elif os.WIFSTOPPED(status) and \
os.WSTOPSIG(status) in io_wanted_signals:
# Child got stopped trying to access the tty, resume it
_resume_child_if_appropriate()
elif sig in {signal.SIGUSR1, signal.SIGUSR2} \
and last_robbed_group:
# Management signals to resume robbed process
if sig == signal.SIGUSR2:
# Forcibly stop child, whatever it is doing or not doing
_send_sig(os.getpgid(child), signal.SIGSTOP)
try:
# resume robbed process
os.tcsetpgrp(in_fd, last_robbed_group)
os.killpg(last_robbed_group, signal.SIGCONT)
except OSError:
# Robbed process no longer exists ? oh well..
last_robbed_group = 0
try:
# resume child then
os.tcsetpgrp(in_fd, os.getpgid(child))
os.killpg(os.getpgid(child), signal.SIGCONT)
except OSError:
pass
if __name__ == '__main__':
main()
It requires at least Python v3.3.
It is not well engineered: it is made of one main function with a couple of sub-functions, but the purpose is to be as more readable and understandable as possible while still providing the basic amount of necessary features.
Also, it could be expanded to e.g. play nicely with shells that are not its direct parent, or with possible recursive invocations of the same program, or also with possible race conditions when querying the current foreground process and later changing it, and probably other corner cases.
The above program automatically stops the current tty owner and resumes wrapper if it gets stopped due to it accessing the tty while not allowed to. In order to manually resume the previous tty owner I made provision for two choices:
- a soft resume: the preferable way when you are sure that
wrapper finished with the tty; send a SIGUSR1 to the helper-program and it will just resume the previous tty owner
- a hard resume: the way to use when you want to stop
wrapper regardless; send SIGUSR2 to the helper-program and it will SIGSTOP wrapper before resuming the previous tty owner
You may also send SIGCONT to the helper-program: it will forcibly stop the current tty owner and resume wrapper regardless.
With this setup, in general you should better avoid sending STOP/CONT signals directly to either wrapper or any of its children or sub-children.
In all cases, always keep in mind that you’re playing with what is usually a delicate interaction between "foreign" programs and their controlled jobs, especially when you invoke interactive shells within interactive shells. These often do not like to be SIGSTOP-ed and SIGCONT-ed arbitrarily. Therefore you often need to carefully apply the correct sequence of operations in order to not have them react by exiting abruptly or messing the terminal window.
openvtas I said in the post, and also triedchvt. Neither worked. Ctrl-Alt-F is not mapped to anything on my keyboard.