diff --git a/package/batocera/core/batocera-configgen/configgen/configgen/emulatorlauncher.py b/package/batocera/core/batocera-configgen/configgen/configgen/emulatorlauncher.py index bb2cad1c5b6..b6e4bf80887 100644 --- a/package/batocera/core/batocera-configgen/configgen/configgen/emulatorlauncher.py +++ b/package/batocera/core/batocera-configgen/configgen/configgen/emulatorlauncher.py @@ -184,15 +184,17 @@ def start_rom(args: argparse.Namespace, maxnbplayers: int, rom: Path, original_r # run the emulator _evmapy_instance = evmapy(systemName, system.config.emulator, effectiveCore, original_rom, player_controllers, guns) + # change directory if wanted. it is restored on the way out: a rom on a + # squashfs is unmounted below, and a process sitting in a mount, even + # this one, keeps it busy + executionDirectory = generator.executionDirectory(system.config, rom) + with ( _evmapy_instance, - set_hotkeygen_context(generator, system) + set_hotkeygen_context(generator, system), + contextlib.chdir(executionDirectory) if executionDirectory is not None + else contextlib.nullcontext() ): - # change directory if wanted - executionDirectory = generator.executionDirectory(system.config, rom) - if executionDirectory is not None: - os.chdir(executionDirectory) - # Initialize overlay process-tracking and check for active MangoHUD bezel_proc = None bezel_log_file = None diff --git a/package/batocera/core/batocera-configgen/configgen/configgen/utils/overlayfs.py b/package/batocera/core/batocera-configgen/configgen/configgen/utils/overlayfs.py index f51dcd17f70..4f7f4afe32e 100644 --- a/package/batocera/core/batocera-configgen/configgen/configgen/utils/overlayfs.py +++ b/package/batocera/core/batocera-configgen/configgen/configgen/utils/overlayfs.py @@ -9,6 +9,7 @@ from ..batoceraPaths import mkdir_if_not_exists from ..exceptions import BatoceraException +from .squashfs import unmount if TYPE_CHECKING: from collections.abc import Generator @@ -19,15 +20,12 @@ def _unmount_and_remove(mount_point: Path): - if mount_point.is_mount(): - result = subprocess.run(["umount", str(mount_point)], capture_output=True, text=True) - if result.returncode != 0: - _logger.error("failed unmounting '%s' (rc=%d) because %s", - mount_point, result.returncode, result.stderr.strip()) - - # Skip the follow-on removal if the umount failed because it might still - # be connected to the ROM's save area (system crashing or bad state?). - return + if mount_point.is_mount() and not unmount(mount_point): + _logger.error("failed unmounting '%s'", mount_point) + + # Skip the follow-on removal if the umount failed because it might still + # be connected to the ROM's save area (system crashing or bad state?). + return shutil.rmtree(mount_point, ignore_errors=True) diff --git a/package/batocera/core/batocera-configgen/configgen/configgen/utils/squashfs.py b/package/batocera/core/batocera-configgen/configgen/configgen/utils/squashfs.py index d79c93f4c28..a4731878f29 100644 --- a/package/batocera/core/batocera-configgen/configgen/configgen/utils/squashfs.py +++ b/package/batocera/core/batocera-configgen/configgen/configgen/utils/squashfs.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import logging import subprocess from contextlib import contextmanager @@ -17,6 +18,21 @@ _SQUASHFS_DIR: Final = Path("/var/run/squashfs/") +def unmount(mount_point: Path, /) -> bool: + result = subprocess.run(["umount", str(mount_point)], capture_output=True, text=True) + + if result.returncode == 0: + return True + + # a game that was killed rather than closed can leave a process holding the mount + # while it dies: detach it anyway, the kernel drops it once the last one lets go, + # leaving it mounted would keep the rom busy until the next reboot + _logger.warning("'%s' is busy, detaching it lazily because %s", + mount_point, result.stderr.strip()) + + return subprocess.run(["umount", "-l", str(mount_point)], capture_output=True, text=True).returncode == 0 + + @contextmanager def mount_squashfs(rom: Path, /) -> Generator[Path]: _logger.debug("mount_squashfs(%s)", rom) @@ -27,14 +43,22 @@ def mount_squashfs(rom: Path, /) -> Generator[Path]: # first, try to clean an empty remaining directory (for example because of a crash) if mount_point.exists() and mount_point.is_dir(): _logger.debug("squashfs_rom: %s already exists", mount_point) - # try to remove an empty directory, else, run the directory, ignoring the .squashfs + + # a previous run may have left the rom mounted here, after a crash or an unmount + # that was refused: take it down rather than running the game off a stale mount, + # which isn't necessarily even the same rom + if mount_point.is_mount(): + _logger.debug("squashfs_rom: %s is still mounted, unmounting it first", mount_point) + unmount(mount_point) + + # whatever is left is not ours to run the game off: it isn't necessarily even + # the same rom, and it would never be unmounted either try: mount_point.rmdir() - except (FileNotFoundError, OSError): - _logger.debug("squashfs_rom: failed to rmdir %s", mount_point) - yield mount_point - # No cleanup is necessary - return + except FileNotFoundError: + pass + except OSError as e: + raise BatoceraException(f"Unable to clean the mount point {mount_point}") from e # ok, the base directory doesn't exist, let's create it and mount the squashfs on it mount_point.mkdir() @@ -66,10 +90,10 @@ def mount_squashfs(rom: Path, /) -> Generator[Path]: _logger.debug("mount_squashfs: cleaning up %s", mount_point) # unmount - return_code = subprocess.call(["umount", mount_point]) - if return_code != 0: + if not unmount(mount_point): _logger.debug("mount_squashfs: unmounting %s failed", mount_point) raise BatoceraException(f"Unable to unmount the file {mount_point}") - # cleaning the empty directory - mount_point.rmdir() + # cleaning the empty directory, a lazily detached mount may still hold it + with contextlib.suppress(OSError): + mount_point.rmdir()