From 74c08c979c9c60cbda0ec70e52ba3ebb28519589 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Mon, 27 Jul 2020 19:39:16 +0100 Subject: [PATCH] Delay KeyboardInterrupt() handling when running a subprocess Because we weren't giving subprocess time to gracefully exit, we'd often get "Device or resource is busy" errors when CTRL+C'ing mkosi as we we'd try to unmount directories while a subprocess was still running in it. By delaying handling of SIGINT while a subprocess is running, the subprocess can exit gracefully which prevents EBUSY errors from occurring when running cleanup code. To allow interrupting hanging subprocesses, we only delay the first SIGINT we receive. If a user presses CTRL+C again, we exit immediately as before. --- mkosi | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/mkosi b/mkosi index 1d03f3437..9dc73ece0 100755 --- a/mkosi +++ b/mkosi @@ -21,6 +21,7 @@ import platform import re import shlex import shutil +import signal import stat import string import subprocess @@ -50,6 +51,7 @@ from typing import ( cast, TYPE_CHECKING, ) +from types import FrameType __version__ = '5' @@ -95,6 +97,31 @@ def print_error(text: str) -> None: sys.stderr.write(f"‣ \033[31;1m{text}\033[0m\n") +@contextlib.contextmanager +def delay_interrupt() -> Generator[None, None, None]: + # CTRL+C is sent to the entire process group. We delay its handling in mkosi itself so the subprocess can + # exit cleanly before doing mkosi's cleanup. If we don't do this, we get device or resource is busy + # errors when unmounting stuff later on during cleanup. We only delay a single CTRL+C interrupt so that a + # user can always exit mkosi even if a subprocess hangs by pressing CTRL+C twice. + interrupted = False + def handler(signal: int, frame: FrameType) -> None: + nonlocal interrupted + if interrupted: + raise KeyboardInterrupt() + else: + interrupted = True + + s = signal.signal(signal.SIGINT, handler) + + try: + yield + finally: + signal.signal(signal.SIGINT, s) + + if interrupted: + die("Interrupted") + + def run(cmdline: List[str], execvp: bool = False, **kwargs: Any) -> CompletedProcess: if 'run' in arg_debug: sys.stderr.write('+ ' + ' '.join(shlex.quote(x) for x in cmdline) + '\n') @@ -103,7 +130,8 @@ def run(cmdline: List[str], execvp: bool = False, **kwargs: Any) -> CompletedPro assert not kwargs os.execvp(cmdline[0], cmdline) else: - return subprocess.run(cmdline, **kwargs) + with delay_interrupt(): + return subprocess.run(cmdline, **kwargs) except FileNotFoundError as e: die(f"{cmdline[0]} not found in PATH.") -- 2.47.2