mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-01 00:27:20 -05:00
fix: ignore Ctrl-Z when no shell can resume
Started with no job-control shell, a Ctrl-Z stopped the screen and its launcher for good. The key now stops it only when its process group is a shell's job, and the bar says why otherwise.
This commit is contained in:
1 parent
0fa4552dae
commit
2ab01d3eec
6 files changed
+112
-35
No files matched your search
+1
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
$defaultInstallSha256 = "00ad7b4c93b5d69e17943e904121485da924ee681c2796a6bbd45aae6052e2b5"
|
||||
$defaultInstallSha256 = "3e97159b3ab76eb1f3cf748b1b47de23b1acf091899eca1dff1be1d2d9406dd4"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+20
-2
@@ -17138,6 +17138,24 @@ def _closing_lines(state: TuiState) -> list[str]:
|
||||
return ["RetroBIOS: stopped. Nothing was installed."]
|
||||
|
||||
|
||||
CTRL_Z_NO_SHELL = ("Ctrl-Z is ignored: no shell started this installer, and none could "
|
||||
"bring it back.")
|
||||
|
||||
|
||||
def tui_ctrl_z(state: TuiState) -> bool:
|
||||
"""Whether a Ctrl-Z typed on the screen stops it: only where a shell
|
||||
with job control can resume it, the installer's process group being one
|
||||
of its jobs, not its session's own. Started from a terminal app's -e,
|
||||
a desktop entry or a launcher's sh -c, nothing ever sent SIGCONT, and
|
||||
the screen and its launcher stayed stopped for good (code_audit_18
|
||||
P3-1). Otherwise the key is ignored and the bar says why."""
|
||||
if (hasattr(signal, "SIGSTOP") and hasattr(os, "getsid")
|
||||
and os.getpgrp() != os.getsid(0)):
|
||||
return True
|
||||
state.status, state.status_more = CTRL_Z_NO_SHELL, ""
|
||||
return False
|
||||
|
||||
|
||||
def tui_suspend(runs: list[_EngineRun], whole: bool) -> None:
|
||||
"""Stop the screen and the runs it started until fg continues them.
|
||||
|
||||
@@ -17309,8 +17327,8 @@ def run_tui(args: argparse.Namespace) -> int:
|
||||
for key in terminal.keys(TUI_FRAME):
|
||||
if key.name == "ctrl-z":
|
||||
# ISIG is off, so Ctrl-Z comes as a key: it stops what
|
||||
# the terminal's would, where the system has job control.
|
||||
if hasattr(signal, "SIGSTOP"):
|
||||
# the terminal's would, where a shell can resume it.
|
||||
if tui_ctrl_z(state):
|
||||
flags["suspend"] = "key"
|
||||
break
|
||||
continue
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ esac
|
||||
TEMP_INSTALLER=""
|
||||
TEMP_DIRECTORY=""
|
||||
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
DEFAULT_INSTALL_SHA256="00ad7b4c93b5d69e17943e904121485da924ee681c2796a6bbd45aae6052e2b5"
|
||||
DEFAULT_INSTALL_SHA256="3e97159b3ab76eb1f3cf748b1b47de23b1acf091899eca1dff1be1d2d9406dd4"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{
|
||||
"dest": "roms/ports/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 794767,
|
||||
"sha256": "00ad7b4c93b5d69e17943e904121485da924ee681c2796a6bbd45aae6052e2b5",
|
||||
"size": 795560,
|
||||
"sha256": "3e97159b3ab76eb1f3cf748b1b47de23b1acf091899eca1dff1be1d2d9406dd4",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -71,8 +71,8 @@
|
||||
"muos": {
|
||||
"archive": {
|
||||
"dest": "ARCHIVE/RetroBIOS.muxapp",
|
||||
"size": 847316,
|
||||
"sha256": "56cac10af6b7afaac0d1b3c0964d54caeabda46e75d35580dcedda264e2daac8"
|
||||
"size": 848109,
|
||||
"sha256": "0a210d563bdb3a6f7ca87eaeb11d48a8ac07f1a1c580011f227fcccc5bba546e"
|
||||
},
|
||||
"members": [
|
||||
{
|
||||
@@ -92,8 +92,8 @@
|
||||
{
|
||||
"name": "RetroBIOS/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 794767,
|
||||
"sha256": "00ad7b4c93b5d69e17943e904121485da924ee681c2796a6bbd45aae6052e2b5",
|
||||
"size": 795560,
|
||||
"sha256": "3e97159b3ab76eb1f3cf748b1b47de23b1acf091899eca1dff1be1d2d9406dd4",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -166,8 +166,8 @@
|
||||
{
|
||||
"dest": ".config/modules/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 794767,
|
||||
"sha256": "00ad7b4c93b5d69e17943e904121485da924ee681c2796a6bbd45aae6052e2b5",
|
||||
"size": 795560,
|
||||
"sha256": "3e97159b3ab76eb1f3cf748b1b47de23b1acf091899eca1dff1be1d2d9406dd4",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
|
||||
+78
-22
@@ -5159,25 +5159,38 @@ class TheTerminalComesBack(unittest.TestCase):
|
||||
self.assertGivenBack(term)
|
||||
self.assertModesBack(term.modes)
|
||||
|
||||
def shell(self) -> Pty:
|
||||
"""An interactive bash on a pseudo-terminal, a shell with job control
|
||||
as a person's terminal runs one."""
|
||||
self.repo()
|
||||
env = {**self.env, "PS1": "PROMPT$ ",
|
||||
"PATH": f"{Path(sys.executable).parent}{os.pathsep}{self.env.get('PATH', '')}"}
|
||||
term = Pty(["--norc", "--noprofile", "-i"], env, program=["/bin/bash"])
|
||||
self.addCleanup(lambda: term.status is None and os.killpg(term.pid, 9))
|
||||
term.wait("PROMPT$")
|
||||
return term
|
||||
|
||||
def typed(self, term: Pty, line: str) -> None:
|
||||
os.write(term.fd, line.encode() + b"\r")
|
||||
|
||||
@unittest.skipUnless(Path("/bin/bash").exists(), "a shell with job control")
|
||||
def test_ctrl_z_then_fg(self):
|
||||
"""ISIG is off on the screen, so Ctrl-Z reached it as a byte and did
|
||||
nothing: the shell never got its terminal back."""
|
||||
import signal
|
||||
|
||||
term = self.ended(lambda term: None)
|
||||
nothing: the shell never got its terminal back. From a shell with
|
||||
job control it stops, and fg brings the screen back."""
|
||||
term = self.shell()
|
||||
self.typed(term, f"{shlex.quote(sys.executable)} {REPO_ROOT / 'install.py'} --tui")
|
||||
term.wait("Where do you play?")
|
||||
mark = len(term.raw)
|
||||
term.send("\x1a")
|
||||
end, stopped = time.monotonic() + 10, False
|
||||
while time.monotonic() < end and not stopped:
|
||||
end = time.monotonic() + 10
|
||||
while b"PROMPT$" not in term.raw[mark:] and time.monotonic() < end:
|
||||
term._pump(0.05)
|
||||
pid, status = os.waitpid(term.pid, os.WNOHANG | os.WUNTRACED)
|
||||
stopped = bool(pid) and os.WIFSTOPPED(status)
|
||||
self.assertTrue(stopped, term.text())
|
||||
self.assertIn(b"Stopped", term.raw[mark:], term.raw[mark:][-300:])
|
||||
self.assertGivenBack(term, mark)
|
||||
self.assertModesBack(termios.tcgetattr(term.fd))
|
||||
# fg: the shell's SIGCONT, and the screen is back, raw again.
|
||||
mark = len(term.raw)
|
||||
os.kill(term.pid, signal.SIGCONT)
|
||||
self.typed(term, "fg")
|
||||
end = time.monotonic() + 5
|
||||
while b"\x1b[?1049h" not in term.raw[mark:] and time.monotonic() < end:
|
||||
term._pump(0.05)
|
||||
@@ -5185,9 +5198,43 @@ class TheTerminalComesBack(unittest.TestCase):
|
||||
term.wait("Where do you play?")
|
||||
self.assertFalse(termios.tcgetattr(term.fd)[3] & termios.ECHO)
|
||||
term.send("q")
|
||||
term.wait("PROMPT$")
|
||||
self.typed(term, "exit")
|
||||
self.assertEqual(term.close(), 130)
|
||||
self.assertGivenBack(term)
|
||||
self.assertModesBack(term.modes)
|
||||
|
||||
def test_ctrl_z_with_no_shell_to_resume_it_is_ignored(self):
|
||||
"""code_audit_18 P3-1: started straight on a terminal (a terminal
|
||||
app's -e, a desktop entry) or under a launcher's sh -c, nothing ever
|
||||
resumed the screen a Ctrl-Z stopped. It now goes on, and says why."""
|
||||
self.repo()
|
||||
for program in (None, ["/bin/sh", "-c", f"{shlex.quote(sys.executable)} "
|
||||
f"{REPO_ROOT / 'install.py'} \"$@\"", "sh"]):
|
||||
with self.subTest(program=program):
|
||||
term = Pty(["--tui"], self.env, program=program)
|
||||
self.addCleanup(lambda term=term: term.status is None
|
||||
and os.killpg(term.pid, 9))
|
||||
term.wait("Where do you play?")
|
||||
term.send("\x1a")
|
||||
screen = term.wait("Ctrl-Z is ignored")
|
||||
self.assertIn("no shell started this installer", " ".join(screen.split()))
|
||||
pid, status = os.waitpid(term.pid, os.WNOHANG | os.WUNTRACED)
|
||||
self.assertEqual(pid, 0, "stopped")
|
||||
term.send("q")
|
||||
self.assertEqual(term.close(), 130)
|
||||
self.assertGivenBack(term)
|
||||
|
||||
def test_ctrl_z_needs_a_shell_that_can_resume(self):
|
||||
state = install.TuiState(os_type="linux", home=HOME)
|
||||
with mock.patch.object(install.os, "getpgrp", return_value=10), \
|
||||
mock.patch.object(install.os, "getsid", return_value=10):
|
||||
self.assertFalse(install.tui_ctrl_z(state))
|
||||
self.assertEqual(state.status, install.CTRL_Z_NO_SHELL)
|
||||
state.status = ""
|
||||
with mock.patch.object(install.os, "getpgrp", return_value=12), \
|
||||
mock.patch.object(install.os, "getsid", return_value=10):
|
||||
self.assertTrue(install.tui_ctrl_z(state))
|
||||
self.assertEqual(state.status, "")
|
||||
|
||||
@staticmethod
|
||||
def proc_state(pid: int) -> str:
|
||||
@@ -5217,27 +5264,34 @@ class TheTerminalComesBack(unittest.TestCase):
|
||||
import signal
|
||||
|
||||
repo = self.repo(GameDataRepository, slow=True)
|
||||
self.env["PATH"] = f"{Path(sys.executable).parent}{os.pathsep}{self.env['PATH']}"
|
||||
term = Pty(["--jobs", "1"], self.env, program=["/bin/sh", str(REPO_ROOT / "install.sh")])
|
||||
env = {**self.env, "PS1": "PROMPT$ ",
|
||||
"PATH": f"{Path(sys.executable).parent}{os.pathsep}{self.env['PATH']}"}
|
||||
# Typed at an interactive bash, as a person runs it: install.sh is a
|
||||
# job of a shell that can resume it (code_audit_18 P3-1).
|
||||
term = Pty(["--norc", "--noprofile", "-i"], env, program=["/bin/bash"])
|
||||
self.addCleanup(lambda: term.status is None and os.killpg(term.pid, 9))
|
||||
term.wait("PROMPT$")
|
||||
self.typed(term, f"sh {REPO_ROOT / 'install.sh'} --jobs 1")
|
||||
term.wait("Where do you play?")
|
||||
term.send("\r")
|
||||
term.wait("to download")
|
||||
term.send("\r")
|
||||
term.wait("Installing")
|
||||
time.sleep(0.8)
|
||||
installer = self.children(term.pid)
|
||||
wrapper = self.children(term.pid)
|
||||
self.assertEqual(len(wrapper), 1, wrapper)
|
||||
installer = self.children(wrapper[0])
|
||||
self.assertEqual(len(installer), 1, installer)
|
||||
runs = self.children(installer[0])
|
||||
self.assertTrue(runs, "no install run")
|
||||
mark = len(term.raw)
|
||||
term.send("\x1a")
|
||||
end, stopped = time.monotonic() + 10, False
|
||||
while time.monotonic() < end and not stopped:
|
||||
end = time.monotonic() + 10
|
||||
while b"PROMPT$" not in term.raw[mark:] and time.monotonic() < end:
|
||||
term._pump(0.05)
|
||||
pid, status = os.waitpid(term.pid, os.WNOHANG | os.WUNTRACED)
|
||||
stopped = bool(pid) and os.WIFSTOPPED(status)
|
||||
# The wrapper is stopped: a shell's job of it is reported stopped.
|
||||
self.assertTrue(stopped, term.text())
|
||||
# The wrapper is stopped: the shell reports its job stopped.
|
||||
self.assertIn(b"Stopped", term.raw[mark:], term.raw[mark:][-300:])
|
||||
self.assertEqual(self.proc_state(wrapper[0]), "T")
|
||||
self.assertEqual(self.proc_state(installer[0]), "T")
|
||||
self.assertEqual([self.proc_state(run) for run in runs], ["T"] * len(runs))
|
||||
written = sorted(repo.dest.rglob("*"))
|
||||
@@ -5246,13 +5300,15 @@ class TheTerminalComesBack(unittest.TestCase):
|
||||
# fg continues the job's group: the screen comes back and the install
|
||||
# carries on to its end.
|
||||
mark = len(term.raw)
|
||||
os.killpg(term.pid, signal.SIGCONT)
|
||||
self.typed(term, "fg")
|
||||
end = time.monotonic() + 5
|
||||
while b"\x1b[?1049h" not in term.raw[mark:] and time.monotonic() < end:
|
||||
term._pump(0.05)
|
||||
self.assertIn(b"\x1b[?1049h", term.raw[mark:])
|
||||
term.wait("files installed", timeout=30)
|
||||
term.send("\r")
|
||||
term.wait("PROMPT$")
|
||||
self.typed(term, "exit")
|
||||
self.assertEqual(term.close(), 0)
|
||||
|
||||
|
||||
|
||||
+4
-1
@@ -217,7 +217,10 @@ However the screen ends (Enter on the result, `q`, Ctrl+C, Ctrl+D, SIGTERM, a
|
||||
hangup), the terminal is given back as the screen found it: the cursor shown,
|
||||
the main screen back, echo and line editing on. Ctrl+Z, or SIGTSTP from
|
||||
outside, gives it back the same way and stops the run, and `fg` redraws the
|
||||
screen where it was.
|
||||
screen where it was. Ctrl+Z stops it only when a shell with job control
|
||||
started it, the one thing that can resume it: started from a terminal app's
|
||||
`-e`, a desktop entry or a launcher's `sh -c`, the key is ignored and the bar
|
||||
says why.
|
||||
|
||||
- `--tui` asks for the full screen, and names the reason when it cannot open
|
||||
(code 2).
|
||||
|
||||
Reference in new issue
Block a user