fish-shell/tests/pexpects/exit_nohang.py
ridiculousfish ed51e2baac Prevent hanging when restoring the foreground process group at exit
When fish starts, it notices which pgroup owns the tty, and then it
restores that pgroup's tty ownership when it exits. However if fish does
not own the tty, then (on Mac at least) the tcsetpgrp call triggers a
SIGSTOP and fish will hang while trying to exit.

The first change is to ignore SIGTTOU instead of defaulting it. This
prevents the hang; however it risks re-introducing #7060.

The second change somewhat mitigates the risk of the first: only do the
restore if the initial pgroup is different than fish's pgroup. This
prevents some useless calls which might potentially steal the tty from
another process (e.g. in #7060).
2021-04-05 17:44:14 -07:00

82 lines
2.4 KiB
Python

#!/usr/bin/env python3
from pexpect_helper import SpawnedProc
import subprocess
import sys
import signal
import time
import os
sp = SpawnedProc()
send, sendline, sleep, expect_prompt, expect_re = (
sp.send,
sp.sendline,
sp.sleep,
sp.expect_prompt,
sp.expect_re,
)
# Helper to print an error and exit.
def error_and_exit(text):
keys = sp.colors()
print("{RED}Test failed: {NORMAL}{TEXT}".format(TEXT=text, **keys))
sys.exit(1)
fish_pid = sp.spawn.pid
# Launch fish_test_helper.
expect_prompt()
exe_path = os.environ.get("fish_test_helper")
sp.sendline(exe_path + " nohup_wait")
# We expect it to transfer tty ownership to fish_test_helper.
sleep(0.1)
tty_owner = os.tcgetpgrp(sp.spawn.child_fd)
if fish_pid == tty_owner:
os.kill(fish_pid, signal.SIGKILL)
error_and_exit(
"Test failed: outer fish %d did not transfer tty owner to fish_test_helper"
% (fish_pid)
)
# Now we are going to tell fish to exit.
# It must not hang. But it might hang when trying to restore the tty.
os.kill(fish_pid, signal.SIGTERM)
# Loop a bit until the process exits (correct) or stops (incorrrect).
# When it exits it should be due to the SIGTERM that we sent it.
for i in range(10):
pid, status = os.waitpid(fish_pid, os.WUNTRACED | os.WNOHANG)
if pid == 0:
# No process ready yet, loop again.
sleep(0.1)
elif pid != fish_pid:
# Some other process exited (??)
os.kill(fish_pid, signal.SIGKILL)
error_and_exit(
"unexpected pid returned from waitpid. Got %d, expected %d"
% (pid, fish_pid)
)
elif os.WIFSTOPPED(status):
# Our pid stopped instead of exiting.
# Probably it stopped because of its tcsetpgrp call during exit.
# Kill it and report a failure.
os.kill(fish_pid, signal.SIGKILL)
error_and_exit("pid %d stopped instead of exiting on SIGTERM" % pid)
elif not os.WIFSIGNALED(status):
error_and_exit("pid %d did not signal-exit" % pid)
elif os.WTERMSIG(status) != signal.SIGTERM:
error_and_exit(
"pid %d signal-exited with %d instead of %d (SIGTERM)"
% (os.WTERMSIG(status), signal.SIGTERM)
)
else:
# Success!
sys.exit(0)
else:
# Our loop completed without the process being returned.
error_and_exit("fish with pid %d hung after SIGTERM" % fish_pid)
# Should never get here.
error_and_exit("unknown, should be unreachable")