Skip to content

Instantly share code, notes, and snippets.

@TBog
Last active March 7, 2024 00:04
Show Gist options
  • Save TBog/9b8b8dc4d77f535c3ebf7bbdc9389cfe to your computer and use it in GitHub Desktop.
Save TBog/9b8b8dc4d77f535c3ebf7bbdc9389cfe to your computer and use it in GitHub Desktop.

Revisions

  1. TBog revised this gist Apr 28, 2021. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions !readme.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,5 @@
    # Arch linux zram
    https://github.com/Nefelim4ag/systemd-swap

    ## Changes
    - only one zram device
  2. TBog created this gist Apr 28, 2021.
    11 changes: 11 additions & 0 deletions !readme.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,11 @@
    # Arch linux zram

    ## Changes
    - only one zram device
    - predict zram size based on compression algorithm
    - set mem_limit for zram

    ### Paths
    * /usr/bin/systemd-swap
    * /etc/systemd/swap.conf
    * /usr/share/systemd-swap/swap-default.conf
    61 changes: 61 additions & 0 deletions swap-default.conf
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,61 @@
    ################################################################################
    # Defaults are optimized for general usage
    ################################################################################

    ################################################################################
    # You can override any settings with files in:
    # /etc/systemd/swap.conf.d/*.conf
    ################################################################################

    ################################################################################
    # Zswap
    #
    # Kernel >= 3.11
    # Zswap create compress cache between swap and memory to reduce IO
    # https://www.kernel.org/doc/Documentation/vm/zswap.txt

    zswap_enabled=1
    zswap_compressor=zstd # lzo lz4 zstd lzo-rle lz4hc
    zswap_max_pool_percent=25 # 1-99
    zswap_zpool=z3fold # zbud z3fold (note z3fold requires kernel 4.8+)

    ################################################################################
    # ZRam
    #
    # Kernel >= 3.15
    # Zram compression streams count for additional information see:
    # https://www.kernel.org/doc/Documentation/blockdev/zram.txt

    zram_enabled=0
    zram_size=$(( RAM_SIZE / 4 )) # This is 1/4 of ram size by default.
    zram_count=${NCPU} # Device count
    zram_streams=${NCPU} # Compress streams
    zram_alg=zstd # See $zswap_compressor
    zram_prio=32767 # 1 - 32767

    ################################################################################
    # Swap File Chunked
    # Allocate swap files dynamically
    # For btrfs fallback to swapfile + loop will be used
    # ex. Min swap size 256M, Max 32*256M
    swapfc_enabled=0
    swapfc_force_use_loop=0 # Force usage of swapfile + loop
    swapfc_frequency=1 # How often to check free swap space in seconds
    swapfc_chunk_size=256M # Size of swap chunk
    swapfc_max_count=32 # Note: 32 is a kernel maximum
    swapfc_min_count=0 # Minimum amount of chunks to preallocate
    swapfc_free_ram_perc=35 # Add first chunk if free ram < 35%
    swapfc_free_swap_perc=15 # Add new chunk if free swap < 15%
    swapfc_remove_free_swap_perc=55 # Remove chunk if free swap > 55% && chunk count > 2
    swapfc_priority=50 # Priority of swapfiles (decreasing by one for each swapfile).
    swapfc_path=/var/lib/systemd-swap/swapfc/
    # Only for swapfile + loop
    swapfc_nocow=1 # Disable CoW on swapfile
    swapfc_directio=1 # Use directio for loop dev
    swapfc_force_preallocated=0 # Will preallocate created files

    ################################################################################
    # Swap devices
    # Find and auto swapon all available swap devices
    swapd_auto_swapon=1
    swapd_prio=1024
    38 changes: 38 additions & 0 deletions swap.conf
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,38 @@
    # This file is part of systemd-swap.
    #
    # Entries in this file show the systemd-swap defaults as
    # specified in /usr/share/systemd-swap/swap-default.conf
    # You can change settings by editing this file.
    # Defaults can be restored by simply deleting this file.
    #
    # See swap.conf(5) and /usr/share/systemd-swap/swap-default.conf for details.

    zswap_enabled=0
    #zswap_compressor=zstd
    #zswap_max_pool_percent=25
    #zswap_zpool=z3fold

    zram_enabled=1
    zram_size=$(( RAM_SIZE / 3 ))
    zram_count=1
    zram_streams=${NCPU}
    zram_alg=lzo-rle
    zram_prio=75

    swapfc_enabled=0
    #swapfc_force_use_loop=0
    #swapfc_frequency=1
    #swapfc_chunk_size=256M
    #swapfc_max_count=32
    #swapfc_min_count=0
    #swapfc_free_ram_perc=35
    #swapfc_free_swap_perc=15
    #swapfc_remove_free_swap_perc=55
    #swapfc_priority=50
    #swapfc_path=/var/lib/systemd-swap/swapfc/
    #swapfc_nocow=1
    #swapfc_directio=1
    #swapfc_force_preallocated=0

    swapd_auto_swapon=0
    #swapd_prio=1024
    913 changes: 913 additions & 0 deletions systemd-swap
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,913 @@
    #!/usr/bin/python3 -u

    # Copyright 2020, Timofey Titovets and the systemd-swap contributors
    # SPDX-License-Identifier: GPL-3.0-or-later

    from __future__ import annotations

    import argparse
    import glob
    import os
    import pickle
    import re
    import shutil
    import signal
    import stat
    import subprocess
    import sys
    import threading
    import time
    import types
    from typing import * # pylint: disable=unused-wildcard-import

    import systemd.daemon
    import sysv_ipc


    def get_mem_stats(fields: List[str]) -> Dict[str, int]:
    stats = {}
    with open("/proc/meminfo") as meminfo:
    for line in meminfo:
    items = line.split()
    key = items[0][:-1]
    if items[2] == "kB" and key in fields:
    fields.remove(key)
    stats[key] = int(items[1]) * 1024
    if len(fields) == 0:
    break
    assert len(fields) == 0
    return stats


    # Global variables.
    # NCPU and RAM_SIZE are referenced inside of `swap-default.conf`.
    NCPU = os.cpu_count() or 1
    RUN_SYSD = "/run/systemd"
    ETC_SYSD = "/etc/systemd"
    VEN_SYSD = "/usr/lib/systemd"
    DEF_CONFIG = "/usr/share/systemd-swap/swap-default.conf"
    ETC_CONFIG = f"{ETC_SYSD}/swap.conf"
    RAM_SIZE = get_mem_stats(["MemTotal"])["MemTotal"]
    PAGE_SIZE = int(
    subprocess.run(
    ["getconf", "PAGESIZE"], check=True, text=True, stdout=subprocess.PIPE
    ).stdout
    )
    WORK_DIR = "/run/systemd/swap"
    LOCK_STARTED = f"{WORK_DIR}/.started"
    ZSWAP_M = "/sys/module/zswap"
    ZSWAP_M_P = "/sys/module/zswap/parameters"
    KMAJOR, KMINOR = [int(v) for v in os.uname().release.split(".")[0:2]]
    IS_DEBUG = False
    sigterm_event = threading.Event()


    class Config:
    def __init__(self):
    os.environ["NCPU"] = str(NCPU)
    os.environ["RAM_SIZE"] = str(RAM_SIZE)
    self.config = {}
    # Load default values.
    if os.path.isfile(DEF_CONFIG):
    try:
    self.config.update(Config.parse_config(DEF_CONFIG))
    except:
    error(f"Error loading {DEF_CONFIG}")
    # Config precedence follows systemd scheme:
    # etc > run > lib for all fragments > /etc/systemd/swap.conf
    if os.path.isfile(ETC_CONFIG):
    try:
    self.config.update(Config.parse_config(ETC_CONFIG))
    except:
    warn(f"Could not load {DEF_CONFIG}")
    config_files = {}
    for path in [VEN_SYSD, RUN_SYSD, ETC_SYSD]:
    path += "/swap.conf.d"
    for file_path in glob.glob(f"{path}/*.conf"):
    if not os.access(file_path, os.R_OK) or os.path.isdir(file_path):
    if os.path.isfile(file_path):
    warn(f"Permission denied reading: {file_path}")
    continue
    config_files[os.path.basename(file_path)] = file_path
    debug(f"Found {file_path}")
    debug(f"Selected configuration artifacts: {list(config_files.values())}")
    # Sort lexicographically.
    config_files = dict(sorted(config_files.items()))
    for config_file in config_files.values():
    info(f"Load: {config_file}")
    self.config.update(Config.parse_config(config_file))

    def get(self, key: str, as_type: Type = str) -> as_type:
    if as_type is bool:
    return self.config[key].lower() in ["yes", "y", "1", "true"]
    return as_type(self.config[key])

    @staticmethod
    def parse_config(file: str) -> Dict[str, str]:
    config = {}
    lines = None
    with open(file) as f:
    lines = f.read().splitlines()
    for line in lines:
    line = line.strip()
    if line.startswith("#") or "=" not in line:
    continue
    key, value = line.split("=", 1)
    config[key] = subprocess.run(
    [f"echo {value}"],
    shell=True,
    check=True,
    text=True,
    stdout=subprocess.PIPE,
    ).stdout.rstrip()
    return config


    class DestroyInfo:
    pickle_path = f"{WORK_DIR}/destroy_info.pickle"

    def __init__(
    self, zswap_parameters: Dict[str, str], zram_already_set: Optional[bool]
    ):
    self.zswap_parameters = zswap_parameters
    self.zram_already_set = zram_already_set

    def get_zswap_parameters(self) -> Dict[str, str]:
    return self.zswap_parameters

    def get_zram_already_set(self) -> bool:
    return self.zram_already_set

    def save(self) -> None:
    with open(self.pickle_path, "wb") as f:
    pickle.dump(self, f)

    @classmethod
    def load(cls) -> Optional[cls]:
    try:
    with open(cls.pickle_path, "rb") as f:
    return pickle.load(f)
    except:
    return None


    class SwapFc:
    def __init__(self, config: Config, sem: sysv_ipc.Semaphore):
    self.assign_config(config)
    self.sem = sem
    # Validate swapfc_frequency due to possible issues caused if set incorrectly.
    if not 1 <= self.swapfc_frequency <= 24 * 60 * 60:
    warn(
    "swapfc_frequency must be in range of 1..86400: "
    f"{self.swapfc_frequency} - set to 1"
    )
    self.swapfc_frequency = 1
    self.polling_rate = self.swapfc_frequency
    systemd.daemon.notify("STATUS=Monitoring memory status...")
    # Create parent directories for swapfc_path.
    makedirs(os.path.dirname(self.swapfc_path))
    self.fs_type, subvolume = self.get_fs_type()
    if self.fs_type == "btrfs":
    if not subvolume:
    subprocess.run(
    ["btrfs", "subvolume", "create", self.swapfc_path],
    check=True,
    stdout=subprocess.DEVNULL,
    stderr=subprocess.DEVNULL,
    )
    else:
    makedirs(self.swapfc_path)
    self.chunk_size = int(
    subprocess.run(
    ["numfmt", "--to=none", "--from=iec", self.swapfc_chunk_size],
    check=True,
    text=True,
    stdout=subprocess.PIPE,
    ).stdout
    )
    self.block_size = os.statvfs(self.swapfc_path).f_bsize
    if self.fs_type == "btrfs":
    # If btrfs supports regular swap files (kernel version 5+), force disable
    # COW to avoid data corruption. If it doesn't, use the old swap-through-loop
    # workaround.
    if KMAJOR >= 5:
    self.swapfc_nocow = True
    else:
    self.swapfc_force_use_loop = True
    if not 1 <= self.swapfc_max_count <= 32:
    warn("swapfc_max_count must be in range 1..32, reset to 1")
    self.swapfc_max_count = 1
    makedirs(f"{WORK_DIR}/swapfc")
    self.allocated = 0
    for _ in range(self.swapfc_min_count):
    self.create_swapfile("swapFC: allocate chunk: ")

    def run(self) -> None:
    systemd.daemon.notify("READY=1")
    if self.allocated == 0:
    memory_usage = round(
    RAM_SIZE * (100 - self.swapfc_free_ram_perc) / (1024 * 1024 * 100)
    )
    info(
    f"swapFC: on-demand swap activation at >{memory_usage} MiB memory usage"
    )
    signal.signal(signal.SIGTERM, sigterm_handler)
    while True:
    self.sem.release()
    sigterm_event.wait(self.polling_rate)
    if sigterm_event.is_set():
    break
    try:
    self.sem.acquire(0)
    except sysv_ipc.BusyError:
    break
    if self.allocated == 0:
    curr_free_ram_perc = self.get_free_ram_perc()
    if curr_free_ram_perc < self.swapfc_free_ram_perc:
    self.create_swapfile(
    f"swapFC: free ram: {curr_free_ram_perc} < "
    f"{self.swapfc_free_ram_perc} - allocate chunk: "
    )
    continue
    curr_free_swap_perc = self.get_free_swap_perc()
    if (
    curr_free_swap_perc < self.swapfc_free_swap_perc
    and self.allocated < self.swapfc_max_count
    ):
    self.create_swapfile(
    f"swapFC: free swap: {curr_free_swap_perc} < "
    f"{self.swapfc_free_swap_perc} - allocate chunk: "
    )
    continue
    if self.allocated <= max(self.swapfc_min_count, 2):
    continue
    if curr_free_swap_perc > self.swapfc_remove_free_swap_perc:
    self.destroy_swapfile(
    f"swapFC: free swap: {curr_free_swap_perc} > "
    f"{self.swapfc_remove_free_swap_perc} - free up chunk: "
    + str(self.allocated)
    )

    def get_fs_type(self) -> Tuple[str, bool]:
    subvolume = False
    path = None
    if os.path.isdir(self.swapfc_path):
    path = self.swapfc_path
    elif os.path.isdir(os.path.dirname(self.swapfc_path)):
    path = os.path.dirname(self.swapfc_path)
    else:
    error("swapfc_path is invalid")
    output = subprocess.run(
    ["df", path, "--output=fstype"],
    check=True,
    text=True,
    stdout=subprocess.PIPE,
    ).stdout
    fs_type = output.splitlines()[1]
    if fs_type == "-":
    ret_code = subprocess.run(
    ["btrfs", "subvolume", "show", path],
    stdout=subprocess.DEVNULL,
    stderr=subprocess.DEVNULL,
    ).returncode
    if ret_code == 0:
    fs_type = "btrfs"
    if path == self.swapfc_path:
    subvolume = True
    else:
    error("swapfc_path is located on an unknown filesystem")
    return fs_type, subvolume

    def assign_config(self, config: Config) -> None:
    yn = lambda x: config.get(x, bool)
    self.swapfc_chunk_size = config.get("swapfc_chunk_size")
    self.swapfc_directio = yn("swapfc_directio")
    self.swapfc_force_preallocated = yn("swapfc_force_preallocated")
    self.swapfc_force_use_loop = yn("swapfc_force_use_loop")
    self.swapfc_free_ram_perc = config.get("swapfc_free_ram_perc", int)
    self.swapfc_free_swap_perc = config.get("swapfc_free_swap_perc", int)
    self.swapfc_frequency = config.get("swapfc_frequency", int)
    self.swapfc_max_count = config.get("swapfc_max_count", int)
    self.swapfc_min_count = config.get("swapfc_min_count", int)
    self.swapfc_nocow = yn("swapfc_nocow")
    self.swapfc_path = config.get("swapfc_path").rstrip("/")
    self.swapfc_priority = config.get("swapfc_priority", int)
    self.swapfc_remove_free_swap_perc = config.get(
    "swapfc_remove_free_swap_perc", int
    )

    def create_swapfile(self, msg: str) -> None:
    if not self.has_enough_space(self.swapfc_path):
    warn("swapFC: ENOSPC")
    # Prevent spamming the journal.
    self.double_polling_rate()
    systemd.daemon.notify("STATUS=Not enough space for allocating chunk")
    return
    # In case we have adjusted the polling rate, reset it.
    self.reset_polling_rate()
    systemd.daemon.notify("STATUS=Allocating swap file...")
    self.allocated += 1
    info(f"{msg} {self.allocated}")
    swapfile = self.prepare_swapfile(
    os.path.join(self.swapfc_path, str(self.allocated))
    )
    subprocess.run(
    ["mkswap", "-L", f"SWAP_{self.fs_type}_{self.allocated}", swapfile],
    check=True,
    stdout=subprocess.DEVNULL,
    stderr=subprocess.DEVNULL,
    )
    options = "discard" if not self.swapfc_force_preallocated else None
    unit_name = gen_swap_unit(
    what=swapfile,
    priority=self.swapfc_priority,
    options=options,
    tag=f"swapfc_{self.allocated}",
    )
    self.swapfc_priority -= 1
    subprocess.run(["systemctl", "daemon-reload"], check=True)
    subprocess.run(["systemctl", "start", unit_name], check=True)
    mode = os.stat(swapfile).st_mode
    if stat.S_ISBLK(mode):
    subprocess.run(["losetup", "-d", swapfile])
    systemd.daemon.notify("STATUS=Monitoring memory status...")

    def has_enough_space(self, path: str) -> bool:
    # Check free space to avoid problems on swap IO + ENOSPC.
    free_blocks = os.statvfs(path).f_bavail
    free_bytes = free_blocks * self.block_size
    # Also try leaving some free space.
    free_bytes -= self.chunk_size
    return free_bytes >= self.chunk_size

    def double_polling_rate(self) -> None:
    new_rate = self.polling_rate * 2
    # Do not double, interval is long enough.
    if new_rate > 86400 or new_rate > self.swapfc_frequency * 1000:
    return
    self.polling_rate = new_rate
    warn(f"swapFC: polling rate doubled to {self.polling_rate}s")

    def reset_polling_rate(self) -> None:
    if self.polling_rate > self.swapfc_frequency:
    self.polling_rate = self.swapfc_frequency
    info(f"swapFC: polling rate reset to {self.polling_rate}s")

    def prepare_swapfile(self, path: str) -> str:
    # Delete file if it already exists.
    force_remove(path)
    os.mknod(path)
    if self.fs_type == "btrfs" and self.swapfc_nocow:
    subprocess.run(["chattr", "+C", path], check=True)
    zeros = b"\x00" * 1024 * 1024
    with open(path, "wb") as swapfile:
    for _ in range(round(self.chunk_size / (1024 * 1024))):
    swapfile.write(zeros)
    swapfile.flush()
    return path if not self.swapfc_force_use_loop else self.losetup_w(path)

    def losetup_w(self, path: str) -> str:
    directio = "on" if self.swapfc_directio else "off"
    file = subprocess.run(
    ["losetup", "-f", "--show", f"--direct-io={directio}", path],
    check=True,
    text=True,
    stdout=subprocess.PIPE,
    ).stdout.rstrip()
    # Loop uses a file descriptor - if the file still exists, but does not have a
    # path like O_TMPFILE. When loop detaches a file, the file will be deleted.
    os.remove(path)
    return file

    def destroy_swapfile(self, msg: str) -> None:
    systemd.daemon.notify("STATUS=Deallocating swap file...")
    info(msg)
    for unit_path in find_swap_units():
    content = None
    with open(unit_path) as f:
    content = f.read()
    if f"swapfc_{self.allocated}" in content:
    dev = get_what_from_swap_unit(unit_path)
    unit_name = os.path.basename(unit_path)
    ret_code = subprocess.run(["systemctl", "stop", unit_name]).returncode
    if ret_code != 0:
    subprocess.run(["swapoff", dev], check=True)
    force_remove(unit_path, verbose=True)
    if os.path.isfile(dev):
    force_remove(dev)
    break
    self.allocated -= 1
    systemd.daemon.notify("STATUS=Monitoring memory status...")

    @staticmethod
    def get_free_ram_perc() -> int:
    ram_stats = get_mem_stats(["MemTotal", "MemFree"])
    return round((ram_stats["MemFree"] * 100) / ram_stats["MemTotal"])

    @staticmethod
    def get_free_swap_perc() -> int:
    swap_stats = get_mem_stats(["SwapTotal", "SwapFree"])
    # Minimum for total is 1 to prevent divide by zero.
    return round((swap_stats["SwapFree"] * 100) / max(swap_stats["SwapTotal"], 1))


    def debug(msg: str) -> None:
    if IS_DEBUG:
    print("DEBUG:", msg, file=sys.stderr)


    def info(msg: str) -> None:
    print("INFO:", msg)


    def warn(msg: str) -> None:
    print("WARN:", msg, file=sys.stderr)


    def error(msg: str) -> NoReturn:
    print("ERRO:", msg, file=sys.stderr)
    sys.exit(1)


    def force_remove(file: str, verbose: bool = False) -> None:
    try:
    os.remove(file)
    if verbose:
    info(f"Removed {file}")
    except OSError:
    if verbose:
    warn(f"Cannot remove {file}")


    def relative_symlink(target: str, link_name: str) -> None:
    if os.path.lexists(link_name):
    force_remove(link_name)
    os.symlink(os.path.relpath(target, os.path.dirname(link_name)), link_name)


    def write(data: str, file: str) -> None:
    with open(file, "w") as f:
    f.write(data)


    def read(file: str) -> str:
    with open(file) as f:
    return f.read()


    def am_i_root(exit_on_error: bool = True) -> bool:
    if os.getuid() == 0:
    return True
    if exit_on_error:
    error("Script must be run as root!")
    else:
    return False


    def find_swap_units() -> List[str]:
    swap_units = []
    for path in ["/run/systemd/system", "/run/systemd/generator"]:
    for file_path in glob.glob(f"{path}/**/*.swap", recursive=True):
    if os.path.isfile(file_path) and not os.path.islink(file_path):
    swap_units.append(file_path)
    return swap_units


    def get_what_from_swap_unit(file: str) -> str:
    with open(file) as file:
    for line in file.read().splitlines():
    if line.startswith("What="):
    return line[len("What=") :]


    def gen_swap_unit(
    what: str, tag: str, priority: Optional[int] = None, options: Optional[str] = None
    ) -> str:
    what = os.path.realpath(what)
    # Assume it's a file by default.
    _type = "File"
    mode = os.stat(what).st_mode
    if stat.S_ISBLK(mode):
    _type = "Block/Partition"
    if "loop" in what:
    _type = "File"
    unit_name = subprocess.run(
    ["systemd-escape", "-p", "--suffix=swap", what],
    check=True,
    text=True,
    stdout=subprocess.PIPE,
    ).stdout.rstrip()
    unit_path = f"{RUN_SYSD}/system/{unit_name}"
    content = (
    "[Unit]\n"
    f"Description=Swap {_type}\n"
    "Documentation=https://github.com/Nefelim4ag/systemd-swap\n"
    "\n"
    "# Generated by systemd-swap\n"
    f"# Tag={tag}\n"
    "\n"
    "[Swap]\n"
    f"What={what}\n"
    "TimeoutSec=1h\n"
    )
    if priority:
    content += f"Priority={priority}\n"
    if options:
    content += f"Options={options}\n"
    write(content, unit_path)
    relative_symlink(unit_path, f"{RUN_SYSD}/system/swap.target.wants/{unit_name}")
    if _type == "File":
    relative_symlink(
    unit_path, f"{RUN_SYSD}/system/local-fs.target.wants/{unit_name}"
    )
    return unit_name


    def swapoff(unit_path: str, subsystem: str) -> None:
    dev = get_what_from_swap_unit(unit_path)
    info(f"{subsystem}: swapoff {dev}")
    subprocess.run(["swapoff", dev])
    force_remove(unit_path, verbose=True)
    if subsystem == "swapFC":
    if os.path.isfile(dev):
    force_remove(dev, verbose=True)
    elif subsystem == "Zram":
    subprocess.run(["zramctl", "-r", dev])


    def makedirs(path: str) -> None:
    os.makedirs(path, exist_ok=True)


    def sigterm_handler(signum: int, frame: Optional[types.FrameType]) -> None:
    sigterm_event.set()


    def get_sem_id() -> int:
    sysv_id = sysv_ipc.ftok(__file__, 1, silence_warning=True)
    debug(f"ftok() returned this ID: {sysv_id}")
    return sysv_id


    def init_directories() -> None:
    makedirs(WORK_DIR)
    makedirs(f"{RUN_SYSD}/system/local-fs.target.wants")
    makedirs(f"{RUN_SYSD}/system/swap.target.wants")


    def start() -> None:
    am_i_root()
    # Clean up in case a previous instance did not exit cleanly.
    stop(on_init=True)
    init_directories()
    sem = None
    try:
    # Semaphore guarding against running more than one instance and signalling if
    # cleanup can start.
    sem = sysv_ipc.Semaphore(get_sem_id(), flags=sysv_ipc.IPC_CREX)
    except sysv_ipc.ExistentialError:
    error(f"{sys.argv[0]} already started")
    config = Config()
    yn = lambda x: config.get(x, bool)
    if yn("zram_enabled") and (
    yn("zswap_enabled") or yn("swapfc_enabled") or yn("swapd_auto_swapon")
    ):
    warn(
    "Combining zram with zswap/swapfc/swapd_auto_swapon can lead to LRU "
    "inversion and is strongly recommended against"
    )
    zswap_parameters = {}
    if yn("zswap_enabled"):
    systemd.daemon.notify("STATUS=Setting up Zswap...")
    if not os.path.isdir(ZSWAP_M):
    error("Zswap - not supported on current kernel")
    info("Zswap: backup current configuration: start")
    makedirs(f"{WORK_DIR}/zswap")
    for file in os.listdir(ZSWAP_M_P):
    file_path = os.path.join(ZSWAP_M_P, file)
    zswap_parameters[file_path] = read(file_path)
    info("Zswap: backup current configuration: complete")
    info("Zswap: set new parameters: start")
    info(
    f'Zswap: Enable: {config.get("zswap_enabled")}, Comp: '
    f'{config.get("zswap_compressor")}, Max pool %: '
    f'{config.get("zswap_max_pool_percent")}, Zpool: '
    f'{config.get("zswap_zpool")}'
    )
    write(config.get("zswap_enabled"), f"{ZSWAP_M_P}/enabled")
    write(config.get("zswap_compressor"), f"{ZSWAP_M_P}/compressor")
    write(config.get("zswap_max_pool_percent"), f"{ZSWAP_M_P}/max_pool_percent")
    write(config.get("zswap_zpool"), f"{ZSWAP_M_P}/zpool")
    info("Zswap: set new parameters: complete")
    zram_already_set = None
    if yn("zram_enabled"):
    systemd.daemon.notify("STATUS=Setting up Zram...")
    info("Zram: check availability")
    if not os.path.isdir("/sys/module/zram"):
    zram_already_set = False
    info("Zram: not part of kernel, trying to find zram module")
    ret_code = subprocess.run(["modprobe", "-n", "zram"]).returncode
    if ret_code != 0:
    error("Zram: can't find zram module!")
    zram_initialized = False
    for _ in range(10):
    ret_code = subprocess.run(["modprobe", "zram"]).returncode
    if ret_code == 0:
    zram_initialized = True
    info("Zram: module successfully loaded")
    break
    time.sleep(1)
    if not zram_initialized:
    error("Zram: can't load zram module")
    else:
    zram_already_set = True
    info("Zram: module already loaded")
    subprocess.run(["systemctl", "daemon-reload"], check=True)
    if config.get("zram_alg").startswith("lzo") or "zstd" == config.get("zram_alg"):
    compression_factor = 3
    elif "lz4" == config.get("zram_alg"):
    compression_factor = 2.5
    else:
    compression_factor = 2
    zram_size = round(config.get("zram_size", int) / config.get("zram_count", int))
    for _ in range(config.get("zram_count", int)):
    info("Zram: trying to initialize free device")
    # zramctl is an external program -> return path to first free device.
    output = subprocess.run(
    [
    "zramctl",
    "-f",
    "-a",
    config.get("zram_alg"),
    "-t",
    config.get("zram_streams"),
    "-s",
    str(zram_size),
    ],
    text=True,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    ).stdout.rstrip()
    zram_dev = None
    if "failed to reset: Device or resource busy" in output:
    time.sleep(1)
    elif "zramctl: no free zram device found" in output:
    warn("Zram: zramctl can't find free device")
    info("Zram: using workaround hook for hot add")
    if not os.path.isfile("/sys/class/zram-control/hot_add"):
    error(
    "Zram: this kernel does not support hot adding zram devices, "
    "please use a 4.2+ kernel or see 'modinfo zram´ and create a "
    "modprobe rule"
    )
    new_zram = read("/sys/class/zram-control/hot_add").rstrip()
    info(f"Zram: success: new device /dev/zram{new_zram}")
    elif "/dev/zram" in output:
    mode = os.stat(output).st_mode
    if not stat.S_ISBLK(mode):
    continue
    zram_dev = output
    else:
    error(f"Zram: unexpected output from zramctl: {output}")
    mode = os.stat(zram_dev).st_mode
    if stat.S_ISBLK(mode):
    new_zram = zram_dev[zram_dev.rfind('/')+1:];
    mem_limit = round(zram_size / compression_factor)
    write(str(mem_limit), f"/sys/block/{new_zram}/mem_limit")
    info(f"Zram: initialized: {zram_dev} size: {zram_size/1024/1024/1024:.2f}GiB limit: {mem_limit/1024/1024/1024:.2f}GiB")
    ret_code = subprocess.run(
    ["mkswap", zram_dev],
    stdout=subprocess.DEVNULL,
    stderr=subprocess.DEVNULL,
    ).returncode
    if ret_code == 0:
    unit_name = gen_swap_unit(
    what=zram_dev,
    options="discard",
    priority=config.get("zram_prio"),
    tag="zram",
    )
    subprocess.run(["systemctl", "daemon-reload"], check=True)
    subprocess.run(["systemctl", "start", unit_name], check=True)
    else:
    warn("Zram: can't get free zram device")
    systemd.daemon.notify("STATUS=Zram setup finished")
    info("Writing destroy info...")
    DestroyInfo(zswap_parameters, zram_already_set).save()
    if yn("swapd_auto_swapon"):
    systemd.daemon.notify("STATUS=Activating swap units...")
    info("swapD: pick up devices from systemd-gpt-auto-generator")
    for unit_path in find_swap_units():
    if "systemd-gpt-auto-generator" in read(unit_path):
    dev = get_what_from_swap_unit(unit_path)
    subprocess.run(["swapoff", dev], check=True)
    force_remove(unit_path, verbose=True)
    info("swapD: searching swap devices")
    makedirs(f"{WORK_DIR}/swapd")
    swapd_prio = config.get("swapd_prio", int)
    # blkid returns 2 if nothing was found.
    devices = subprocess.run(
    ["blkid", "-t", "TYPE=swap", "-o", "device"],
    text=True,
    stdout=subprocess.PIPE,
    ).stdout.splitlines()
    for device in devices:
    if "zram" in device or "loop" in device:
    continue
    used_devices = subprocess.run(
    ["swapon", "--show=NAME", "--noheadings"],
    check=True,
    text=True,
    stdout=subprocess.PIPE,
    ).stdout.splitlines()
    for used_device in used_devices:
    if device == used_device:
    device = None
    if device is None:
    continue
    mode = os.stat(device).st_mode
    if not stat.S_ISBLK(mode):
    continue
    unit_name = gen_swap_unit(
    what=device, options="discard", priority=swapd_prio, tag="swapd"
    )
    subprocess.run(["systemctl", "daemon-reload"], check=True)
    ret_code = subprocess.run(["systemctl", "start", unit_name]).returncode
    if ret_code != 0:
    continue
    info(f"swapD: enabled device: {device}")
    swapd_prio -= 1
    systemd.daemon.notify("STATUS=Swap unit activation finished")
    if yn("swapfc_enabled"):
    swap_fc = SwapFc(config, sem)
    swap_fc.run()
    else:
    systemd.daemon.notify("READY=1")
    # Done setting up. Allow cleanup to take place.
    sem.release()


    def stop(on_init: bool = False) -> None:
    am_i_root()
    config = Config()
    sem = None
    sem_id = get_sem_id()
    try:
    sem = sysv_ipc.Semaphore(sem_id)
    if not on_init:
    try:
    sem.acquire(60)
    except sysv_ipc.BusyError:
    warn("Could not acquire semaphore, commencing stop action anyway...")
    systemd.daemon.notify("STOPPING=1")
    except sysv_ipc.ExistentialError:
    # Prevent systemd-swap from starting/stopping while cleaning up.
    sem = sysv_ipc.Semaphore(sem_id, flags=sysv_ipc.IPC_CREX)
    if not on_init:
    warn(f"{sys.argv[0]} might not be running")
    destroy_info = DestroyInfo.load()
    swap_units = find_swap_units()
    for unit_path in filter(lambda u: "swapd" in read(u), swap_units):
    swapoff(unit_path, "swapD")
    for unit_path in filter(lambda u: "swapfc" in read(u), swap_units):
    swapoff(unit_path, "swapFC")
    for unit_path in filter(lambda u: "zram" in read(u), swap_units):
    swapoff(unit_path, "Zram")
    if destroy_info:
    if destroy_info.zram_already_set == False:
    info("Zram: unloading kernel module...")
    subprocess.run(["modprobe", "-r", "zram"])
    if os.path.isdir(f"{WORK_DIR}/zswap"):
    info("Zswap: restore configuration: start")
    for zswap_parameter, value in destroy_info.zswap_parameters.items():
    write(value, zswap_parameter)
    info("Zswap: restore configuration: complete")
    info("Removing working directory...")
    shutil.rmtree(WORK_DIR, ignore_errors=True)
    swapfc_path = config.get("swapfc_path")
    info(f"Removing files in {swapfc_path}...")
    try:
    for file in os.listdir(swapfc_path):
    force_remove(os.path.join(swapfc_path, file), verbose=True)
    except OSError:
    pass
    sem.remove()


    def status() -> None:
    if not am_i_root(exit_on_error=False):
    warn("Not root! Some output might be missing.")
    swap_stats = get_mem_stats(["SwapTotal", "SwapFree"])
    swap_used = swap_stats["SwapTotal"] - swap_stats["SwapFree"]
    try:
    if os.path.isdir("/sys/module/zswap"):
    used_bytes = int(read("/sys/kernel/debug/zswap/pool_total_size"))
    used_pages = used_bytes / PAGE_SIZE
    stored_pages = int(read("/sys/kernel/debug/zswap/stored_pages"))
    stored_bytes = stored_pages * PAGE_SIZE
    ratio = 0
    if stored_pages > 0:
    ratio = used_pages * 100 / stored_pages
    zswap_info = ""
    for file in sorted(os.listdir("/sys/module/zswap/parameters")):
    zswap_info += (
    f'. {file} {read(f"/sys/module/zswap/parameters/{file}")}\n'
    )
    subprocess.run(["column", "-t"], input=zswap_info, text=True)
    zswap_info = ""
    for file in sorted(os.listdir("/sys/kernel/debug/zswap")):
    zswap_info += f'. . {file} {read(f"/sys/kernel/debug/zswap/{file}")}\n'
    zswap_info += f". . compress_ratio {round(ratio)}%\n"
    if swap_used > 0:
    zswap_info += (
    f". . zswap_store/swap_store {stored_bytes}/{swap_used} "
    f"{round(stored_bytes * 100 / swap_used)}%\n"
    )
    print("Zswap:")
    subprocess.run(["column", "-t"], input=zswap_info, text=True)
    except:
    warn("Zswap info inaccesible")
    zramctl = subprocess.run(
    ["zramctl"], check=True, text=True, stdout=subprocess.PIPE
    ).stdout
    if "[SWAP]" in zramctl: # pylint: disable=unsupported-membership-test
    zramctl = zramctl.splitlines()
    zram_info = ""
    for line in zramctl:
    if line.startswith("NAME") or "[SWAP]" in line:
    if line.endswith("MOUNTPOINT"):
    line = line[: -len("MOUNTPOINT")]
    elif line.endswith("[SWAP]"):
    line = line[: -len("[SWAP]")]
    zram_info += f". {line}\n"
    print("Zram:")
    subprocess.run(["column -t | uniq"], input=zram_info, text=True, shell=True)
    if os.path.isdir(f"{WORK_DIR}/swapd"):
    swapon = subprocess.run(
    ["swapon", "--raw"], check=True, text=True, stdout=subprocess.PIPE
    ).stdout.splitlines()
    swapd_info = ""
    for line in swapon:
    if not re.search("zram|file|loop", line): # pylint: disable=no-member
    swapd_info += f". {line}\n"
    print("swapD:")
    subprocess.run(["column", "-t"], input=swapd_info, text=True)
    if os.path.isdir(f"{WORK_DIR}/swapfc"):
    swapon = subprocess.run(
    ["swapon", "--raw"], check=True, text=True, stdout=subprocess.PIPE
    ).stdout.splitlines()
    swapfc_info = ""
    for line in swapon:
    if re.search("NAME|file|loop", line): # pylint: disable=no-member
    swapfc_info += f". {line}\n"
    print("swapFC:")
    subprocess.run(["column", "-t"], input=swapfc_info, text=True)


    def compression() -> None:
    proc_crypto = None
    with open("/proc/crypto") as f:
    proc_crypto = f.read()
    matches = re.finditer( # pylint: disable=no-member
    r"name\s*:\s*(\S*).*?type\s*:\s*(\S*)",
    proc_crypto,
    re.DOTALL, # pylint: disable=no-member
    )
    print("Found loaded compression algorithms: ", end="")
    first = True
    for match in matches:
    algo, _type = match.groups()
    if _type == "compression":
    if first:
    first = False
    else:
    print(", ", end="")
    print(algo, end="")
    print()


    def main() -> None:
    argparser = argparse.ArgumentParser()
    argparser.add_argument(
    "command",
    choices=["start", "stop", "status", "compression"],
    default="status",
    nargs="?",
    help="`start' the daemon, `stop' it, show some swap `status' info, or display "
    "the loaded `compression' algorithms",
    )
    args = argparser.parse_args()
    if args.command == "start":
    start()
    elif args.command == "stop":
    stop()
    elif args.command == "status":
    status()
    elif args.command == "compression":
    compression()
    else:
    raise RuntimeError


    if __name__ == "__main__":
    main()