From b15f2d81ff0d4edb086b84926da7a921dffd9f1c Mon Sep 17 00:00:00 2001 From: Selby Mashiki Date: Sun, 25 Oct 2026 20:55:10 +0200 Subject: [PATCH] fix: disable active swap partitions in umount_all_existing() When an existing swap partition is active on a target disk, lsblk reports '[SWAP]' in its mountpoints array. Previously, DeviceHandler.umount_all_existing() called umount() on LINUX_SWAP partitions, causing umount() to execute 'umount -R [SWAP]' (which fails with exit code 32) and leaving swap active on the device so subsequent partition table modifications fail with 'Device or resource busy'. Add a swapoff() helper in archinstall.lib.disk.utils, call it for LINUX_SWAP partitions in umount_all_existing(), and filter out '[SWAP]' in umount(). --- archinstall/lib/disk/device_handler.py | 3 ++ archinstall/lib/disk/utils.py | 19 +++++-- tests/test_swapoff_existing.py | 73 ++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 tests/test_swapoff_existing.py diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index be6290bb76..66b2fc5f05 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -13,6 +13,7 @@ get_lsblk_info, linux_root_guid, mount, + swapoff, udev_sync, umount, ) @@ -513,6 +514,8 @@ def umount_all_existing(self, device_path: Path) -> None: # un-mount for existing encrypted partitions if partition.fs_type == FilesystemType.CRYPTO_LUKS: Luks2(partition.path).lock() + elif partition.fs_type == FilesystemType.LINUX_SWAP: + swapoff(partition.path) else: umount(partition.path, recursive=True) diff --git a/archinstall/lib/disk/utils.py b/archinstall/lib/disk/utils.py index efa1f16515..a2702306bf 100644 --- a/archinstall/lib/disk/utils.py +++ b/archinstall/lib/disk/utils.py @@ -176,17 +176,18 @@ def mount( def umount(mountpoint: Path | str, recursive: bool = False) -> None: lsblk_info = get_lsblk_info(mountpoint) - if not lsblk_info.mountpoints: + mountpoints = [p for p in lsblk_info.mountpoints if p != Path('[SWAP]')] + if not mountpoints: return - debug(f'Partition {mountpoint} is currently mounted at: {[str(m) for m in lsblk_info.mountpoints]}') + debug(f'Partition {mountpoint} is currently mounted at: {[str(m) for m in mountpoints]}') cmd = ['umount'] if recursive: cmd.append('-R') - for path in lsblk_info.mountpoints: + for path in mountpoints: debug(f'Unmounting mountpoint: {path}') SysCommand([*cmd, str(path)]) @@ -198,6 +199,18 @@ def swapon(path: Path) -> None: raise DiskError(f'Could not enable swap {path}:\n{err.message}') +def swapoff(path: Path) -> None: + lsblk_info = get_lsblk_info(path) + if Path('[SWAP]') not in lsblk_info.mountpoints: + return + + debug(f'Disabling swap on: {path}') + try: + SysCommand(['swapoff', str(path)]) + except SysCallError as err: + raise DiskError(f'Could not disable swap {path}:\n{err.message}') + + def linux_root_guid(arch: str | None) -> PartitionGUID: if arch == 'aarch64': return PartitionGUID.LINUX_ROOT_AARCH64 diff --git a/tests/test_swapoff_existing.py b/tests/test_swapoff_existing.py new file mode 100644 index 0000000000..abf695ac2c --- /dev/null +++ b/tests/test_swapoff_existing.py @@ -0,0 +1,73 @@ +from pathlib import Path +from unittest.mock import MagicMock + +from pytest import MonkeyPatch + +from archinstall.lib.disk.device_handler import DeviceHandler +from archinstall.lib.disk.utils import swapoff, umount +from archinstall.lib.models.device import FilesystemType + + +def test_swapoff_runs_only_when_swap_active(monkeypatch: MonkeyPatch) -> None: + cmds: list[list[str]] = [] + + monkeypatch.setattr( + 'archinstall.lib.disk.utils.SysCommand', + lambda cmd: cmds.append(list(cmd)), + ) + + active_lsblk = MagicMock(mountpoints=[Path('[SWAP]')]) + inactive_lsblk = MagicMock(mountpoints=[]) + + monkeypatch.setattr( + 'archinstall.lib.disk.utils.get_lsblk_info', + lambda path: active_lsblk if path == Path('/dev/sda2') else inactive_lsblk, + ) + + swapoff(Path('/dev/sda2')) + swapoff(Path('/dev/sda3')) + + assert cmds == [['swapoff', '/dev/sda2']] + + +def test_umount_ignores_swap_pseudo_mountpoint(monkeypatch: MonkeyPatch) -> None: + cmds: list[list[str]] = [] + + monkeypatch.setattr( + 'archinstall.lib.disk.utils.SysCommand', + lambda cmd: cmds.append(list(cmd)), + ) + monkeypatch.setattr( + 'archinstall.lib.disk.utils.get_lsblk_info', + lambda _path: MagicMock(mountpoints=[Path('[SWAP]')]), + ) + + umount(Path('/dev/sda2'), recursive=True) + + assert cmds == [] + + +def test_umount_all_existing_disables_swap_and_unmounts_filesystems(monkeypatch: MonkeyPatch) -> None: + handler = object.__new__(DeviceHandler) + + swap_part = MagicMock(path=Path('/dev/sda2'), fs_type=FilesystemType.LINUX_SWAP) + ext4_part = MagicMock(path=Path('/dev/sda3'), fs_type=FilesystemType.EXT4) + mock_device = MagicMock(partition_infos=[swap_part, ext4_part]) + handler._devices = {Path('/dev/sda'): mock_device} + + swapoff_paths: list[Path] = [] + umount_paths: list[Path | str] = [] + + monkeypatch.setattr( + 'archinstall.lib.disk.device_handler.swapoff', + lambda path: swapoff_paths.append(path), + ) + monkeypatch.setattr( + 'archinstall.lib.disk.device_handler.umount', + lambda path, **_kwargs: umount_paths.append(path), + ) + + handler.umount_all_existing(Path('/dev/sda')) + + assert swapoff_paths == [Path('/dev/sda2')] + assert umount_paths == [Path('/dev/sda3')]