#!/usr/bin/env python3
#
# Copyright (c) 2025 openmediavault plugin developers
#
# This file is licensed under the terms of the GNU General Public
# License version 2. This program is licensed "as is" without any
# warranty of any kind, whether express or implied.
#
# version: 0.0.7
#
import os, sys, subprocess, time, re, shutil
import pwd, grp
import atexit, signal
import fcntl
from pathlib import Path

try:
    import yaml  # type: ignore
except Exception:
    yaml = None

SCRIPT_VERSION = "0.0.7"
CONFIG = Path("/etc/omv-writecache/config.yaml")
TMPFS_MOUNT = Path("/run/omv-writecache")
LOG_FILE = "/var/log/omv-writecache.log"

LOCK_FILE = None
ARRAY_LOCK = None

def log(msg: str, level: str = "INFO"):
    ts = time.strftime("%Y-%m-%d %H:%M:%S")
    print(f"{ts} - {msg}")
    try:
        os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
        with open(LOG_FILE, "a") as f:
            f.write(f"{ts} [{level}] {msg}\n")
    except Exception:
        pass

# locking
def get_lock_filename(lockname):
    return f"/run/{lockname}.lock"

def check_and_create_lock(lockname):
    global LOCK_FILE
    global ARRAY_LOCK
    global _LOCK_FD
    LOCK_FILE = get_lock_filename(lockname)
    ARRAY_LOCK = lockname

    # Open (create if missing) and keep fd for the whole process
    _LOCK_FD = os.open(LOCK_FILE, os.O_WRONLY | os.O_CREAT, 0o644)
    try:
        fcntl.flock(_LOCK_FD, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except BlockingIOError:
        # Someone else holds it; read PID for diagnostics if possible
        try:
            # Use pread to avoid seek races
            owner_pid = os.read(_LOCK_FD, 32).decode('ascii').strip() or "unknown"
        except Exception:
            owner_pid = "unknown"
        os.close(_LOCK_FD)
        _LOCK_FD = None
        log(f"Instance with lock name {lockname} and PID {owner_pid} is already running!", "WARN")
        sys.exit(1)

    # We own the lock now; write our PID
    try:
        os.ftruncate(_LOCK_FD, 0)
        os.lseek(_LOCK_FD, 0, os.SEEK_SET)
        os.write(_LOCK_FD, str(os.getpid()).encode("ascii"))
        os.fsync(_LOCK_FD)
    except Exception as e:
        log(f"Failed to write PID to lock file: {e}", "WARN")

    atexit.register(release_lock)  # Ensure lock is removed on normal exit
    log(f"Lock file {LOCK_FILE} created and locked.")

def release_lock():
    """Release lock; only unlink if the PID matches ours."""
    global _LOCK_FD
    if not LOCK_FILE:
        return
    try:
        if _LOCK_FD is not None:
            try:
                fcntl.flock(_LOCK_FD, fcntl.LOCK_UN)
            finally:
                os.close(_LOCK_FD)
                _LOCK_FD = None
        # Unlink only if we still own it
        try:
            with open(LOCK_FILE, "r") as f:
                existing_pid = (f.read() or "").strip()
            if existing_pid == str(os.getpid()):
                os.remove(LOCK_FILE)
                log(f"Lock file {LOCK_FILE} released.")
            elif existing_pid:
                log(f"Lock file {LOCK_FILE} not removed (owned by PID {existing_pid}).")
        except FileNotFoundError:
            pass
    except Exception as e:
        log(f"Lock release error: {e}", "WARN")
def handle_signal(signum, frame):
    # Handle termination signals to release the lock properly
    print(f"Received signal {signum}, cleaning up...")
    sys.exit(1)


def lower_bind_for(name: str) -> Path:
    return TMPFS_MOUNT / name / "lower"

def is_mountpoint(path: Path) -> bool:
    try:
        with open("/proc/mounts") as f:
            for line in f:
                parts = line.split()
                if len(parts) >= 2 and parts[1] == str(path):
                    return True
    except Exception:
        pass
    return False

def wait_for_unmount(path: Path, timeout: int = 10) -> bool:
    """Wait for a path to be unmounted, with timeout"""
    for _ in range(timeout * 2):
        if not is_mountpoint(path) and not is_overlay_mounted(path):
            return True
        time.sleep(0.5)
    return False

def force_unmount_with_retry(path: Path, max_retries: int = 3) -> bool:
    """Forcefully unmount a path with retries and fuser killing if needed"""
    for attempt in range(max_retries):
        if not is_mountpoint(path) and not is_overlay_mounted(path):
            return True

        # Try lazy unmount first
        result = run(["umount", "-l", str(path)], check=False)
        if result.returncode == 0:
            if wait_for_unmount(path, 5):
                return True

        # If that fails, try to kill processes using the mountpoint
        if attempt < max_retries - 1:  # Don't do this on the last attempt
            log(f"FORCE_UNMOUNT_RETRY {attempt + 1}: Killing processes using {path}")
            run(["fuser", "-km", str(path)], check=False)

            # Try regular unmount after killing processes
            result = run(["umount", str(path)], check=False)
            if result.returncode == 0:
                if wait_for_unmount(path, 5):
                    return True

    return False

def ensure_lower_bind(real_path: Path, name: str) -> Path:
    lower = lower_bind_for(name)
    lower.mkdir(parents=True, exist_ok=True)
    if not is_mountpoint(lower):
        run(["mount", "--bind", str(real_path), str(lower)], check=False)
        run(["mount", "--make-private", str(lower)], check=False)
    # Always force RO (even if it was already mounted)
    remount_lower(lower, rw=False)
    return lower

def remount_lower(lower: Path, rw: bool):
    run(["mount", "-o", f"remount,{'rw' if rw else 'ro'},bind", str(lower)], check=False)

def run(cmd, check=True, quiet=False):
    cmd_str = ' '.join(cmd)
    log(f"CMD: {cmd_str}", "DEBUG")
    res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    if res.returncode != 0:
        log(f"CMD_FAILED: {cmd_str} (exit {res.returncode})", "ERROR")
        if res.stdout.strip() and not quiet:
            log(f"OUTPUT: {res.stdout.strip()}", "ERROR")
        if check:
            raise SystemExit(res.returncode)
    else:
        log(f"CMD_OK: {cmd_str}", "DEBUG")
        if res.stdout.strip() and not quiet:
            log(f"OUTPUT: {res.stdout.strip()}", "DEBUG")
    return res

def ensure_persistent_journal():
    conf_path = "/etc/systemd/journald.conf.d/10-writecache.conf"
    journal_dir = "/var/log/journal"

    # Check if Storage=persistent is set
    if os.path.exists(conf_path):
        with open(conf_path, "r") as f:
            for line in f:
                line = line.strip()
                if line.lower().startswith("storage=") and line.split("=", 1)[1].strip().lower() == "persistent":
                    # Ensure directory exists
                    if not os.path.isdir(journal_dir):
                        log(f"JRNDIR: {journal_dir} not found. Creating", "DEBUG")
                        os.makedirs(journal_dir, exist_ok=True)
                        # Set owner to root:systemd-journal and permissions to 2755
                        try:
                            gid = grp.getgrnam("systemd-journal").gr_gid
                        except KeyError:
                            gid = 0  # fallback to root if group missing
                        os.chown(journal_dir, 0, gid)
                        os.chmod(journal_dir, 0o2755)
                        # ensure machine id subdirectory exists
                        try:
                            mid = Path("/etc/machine-id").read_text().strip()
                        except FileNotFoundError:
                            mid = ""
                        if mid:
                            mid_dir = os.path.join(journal_dir, mid)
                            if not os.path.isdir(journal_dir):
                                log(f"MIDDIR: {mid_dir} not found. Creating", "DEBUG")
                                os.makedirs(mid_dir, exist_ok=True)
                            os.chown(journal_dir, 0, gid)
                            os.chmod(journal_dir, 0o2755)
                    break

def journalctl_rotate_sync():
    log("ROTATE_START: Beginning journalctl rotate and sync operation")
    try:
        run(["journalctl", "--rotate"], check=False)
        run(["journalctl", "--sync"], check=False)
    except Exception as e:
        log(f"journalctl rotate/sync failed: {e}", "WARN")

def load_config():
    defaults = {
        "enable": True,
        "tmpfs_size": "25%",
        "journald_storage": "volatile",
        "flush_on_shutdown": True,
        "flush_daily": False,
        "paths": "/var/log = drop\n/var/tmp = drop\n/var/cache/apt/archives = drop\n/var/lib/apt/lists = drop\n/var/lib/dpkg/updates = flush\n",
    }
    if not CONFIG.exists() or yaml is None:
        log("CONFIG: Using defaults", "DEBUG")
        return defaults
    try:
        with open(CONFIG) as f:
            data = yaml.safe_load(f) or {}
        defaults.update(data)
        log(f"CONFIG: Loaded from {CONFIG}", "DEBUG")
    except Exception as e:
        log(f"CONFIG_ERROR: {e}", "ERROR")
    return defaults

def parse_paths(paths_text):
    items = []
    for line in paths_text.splitlines():
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        if "=" in line:
            k, v = line.split("=", 1)
            p = k.strip()
            mode = v.strip()
        else:
            p = line.strip()
            mode = "drop"
        items.append((p, mode))
    # dedupe preserving order
    seen = set(); res = []
    for p, mode in items:
        if p not in seen:
            res.append((p, mode))
            seen.add(p)
    return res

def ensure_tmpfs(size):
    TMPFS_MOUNT.mkdir(parents=True, exist_ok=True)
    with open("/proc/mounts") as f:
        mounts = f.read()
    if f" {TMPFS_MOUNT} tmpfs " in mounts:
        log(f"TMPFS_EXISTS: {TMPFS_MOUNT}")
        return
    log(f"TMPFS_MOUNT: {TMPFS_MOUNT} size={size}")
    run(["mount", "-t", "tmpfs", "-o", f"size={size},mode=0755", "tmpfs", str(TMPFS_MOUNT)])

def sanitize_name(p):
    return re.sub(r"[^A-Za-z0-9._-]+", "_", p.strip("/"))

def is_overlay_mounted(path: Path):
    with open("/proc/mounts") as f:
        for line in f:
            parts = line.split()
            if len(parts) >= 3 and parts[1] == str(path):
                return parts[2] == "overlay"
    return False

def find_bad_specials(upper_dir: Path):
    bad = []
    for root, dirs, files in os.walk(upper_dir, topdown=True):
        for name in files:
            # skip proper overlay whiteouts
            if name.startswith(".wh."):
                continue
            p = Path(root) / name
            try:
                st = os.lstat(p)
            except FileNotFoundError:
                continue
            # stat.S_ISCHR -> character device
            if (st.st_mode & 0o170000) == 0o020000:  # chr dev
                bad.append(str(p))
    return bad

def scrub_bad_specials(upper_dir: Path):
    bad = find_bad_specials(upper_dir)
    if not bad:
        return False, []
    for p in bad:
        try:
            os.remove(p)
        except IsADirectoryError:
            shutil.rmtree(p, ignore_errors=True)
        except FileNotFoundError:
            pass
        except Exception as e:
            log(f"SCRUB_ERROR: {p}: {e}", "ERROR")
    return True, bad

def clear_overlay_opq_and_whiteouts(upper_dir: Path) -> int:
    """Remove .wh..wh..opq and any .wh.* entries under upper_dir (depth 1)."""
    removed = 0
    try:
        opq = upper_dir / ".wh..wh..opq"
        if opq.exists():
            opq.unlink(missing_ok=True)
            removed += 1
        # Remove xattr if set
        try:
            import os
            os.setxattr(str(upper_dir), b"trusted.overlay.opaque", b"")
            # if above doesn't clear on your Python, fallback to setfattr via run()
        except Exception:
            run(["setfattr", "-x", "trusted.overlay.opaque", str(upper_dir)], check=False)

        # Remove per-file whiteouts directly under this dir
        for p in upper_dir.glob(".wh*"):
            try:
                p.unlink()
                removed += 1
            except Exception:
                pass
    except Exception:
        pass
    return removed

def mount_overlays(cfg):
    if not cfg.get("enable", True):
        log("DISABLED: WriteCache disabled", "WARN")
        return
    ensure_tmpfs(cfg.get("tmpfs_size", "25%"))
    for path, mode in parse_paths(cfg.get("paths", "")):
        p = Path(path).resolve()
        # Do not allow overlay targets inside our workspace (e.g., lower/upper/work)
        if str(p).startswith(str(TMPFS_MOUNT) + "/") or str(p) == str(TMPFS_MOUNT):
            log(f"SKIP_WORKSPACE_PATH: refusing to overlay {p}", "ERROR")
            continue
        if not p.exists():
            log(f"SKIP_MISSING: {p}", "WARN")
            continue
        name = sanitize_name(str(p))
        upper = TMPFS_MOUNT / name / "upper"
        work = TMPFS_MOUNT / name / "work"
        upper.mkdir(parents=True, exist_ok=True)
        work.mkdir(parents=True, exist_ok=True)
        changed, bad = scrub_bad_specials(upper)
        if changed:
            log(f"SCRUBBED_BAD_SPECIALS: {len(bad)} removed under {upper}")
        if is_overlay_mounted(p):
            log(f"ALREADY_MOUNTED: {p}")
            continue
        # Use a bind of the real path as the overlay's lowerdir
        lower = ensure_lower_bind(p, name)
        # If something mounted overlay on the lower bind path, tear it down
        if is_overlay_mounted(lower):
            log(f"FIXUP: overlay was mounted on lower bind {lower}; unmounting", "ERROR")
            force_unmount_with_retry(lower)
        opts = f"lowerdir={lower},upperdir={upper},workdir={work},metacopy=off,redirect_dir=off,index=off"
        log(f"OVERLAY_MOUNT: {p} policy={mode} lowerdir={lower}")
        res = run(["mount", "-t", "overlay", "overlay", "-o", opts, str(p)], check=False)
        if res.returncode == 0:
            log(f"MOUNTED: {p} policy={mode}")
        else:
            log(f"MOUNT_FAILED: {p}", "ERROR")

def apply_whiteouts(upper_dir, lower_dir):
    """
    Apply overlayfs whiteouts from upper -> lower.
    - .wh.<name> deletes <name> in lower
    - .wh..wh..opq makes directory opaque: remove lower entries not present in upper
    """
    upper_dir = os.path.abspath(upper_dir)
    lower_dir = os.path.abspath(lower_dir)
    log(f"Applying whiteouts from {upper_dir} to {lower_dir}")

    for root, dirs, files in os.walk(upper_dir, topdown=True):
        rel = os.path.relpath(root, upper_dir)
        lower_root = os.path.join(lower_dir, rel) if rel != "." else lower_dir

        # Opaque marker handling
        if ".wh..wh..opq" in files:
            try:
                os.makedirs(lower_root, exist_ok=True)
                # live names present in upper at this level (excluding whiteouts)
                upper_live = set(n for n in (files + dirs)
                                 if not n.startswith(".wh.") and n != ".wh..wh..opq")
                try:
                    lower_entries = os.listdir(lower_root)
                except FileNotFoundError:
                    lower_entries = []
                for name in lower_entries:
                    if name not in upper_live:
                        target = os.path.join(lower_root, name)
                        if os.path.isdir(target) and not os.path.islink(target):
                            shutil.rmtree(target, ignore_errors=True)
                        else:
                            try:
                                os.remove(target)
                            except FileNotFoundError:
                                pass
                log(f"Applied opaque whiteout at {lower_root}")
            except Exception as e:
                log(f"Opaque cleanup failed at {lower_root}: {e}")

        # Regular whiteouts
        for f in files:
            if f.startswith(".wh.") and f != ".wh..wh..opq":
                target = os.path.join(lower_root, f[4:])
                if os.path.isdir(target) and not os.path.islink(target):
                    shutil.rmtree(target, ignore_errors=True)
                else:
                    try:
                        os.remove(target)
                    except FileNotFoundError:
                        pass
                log(f"Applied whiteout for {target}")

def rsync_upper_to_lower(upper_dir, lower_dir):
    """
    Sync overlayfs upper -> lower.
    - Apply whiteouts before rsync.
    - Preserve perms, times, ACLs, hardlinks (no xattrs).
    - Avoid specials/devices so we don't mknod transient files.
    - Treat rsync 23/24 as non-fatal (vanished files etc.).
    - Report number of files transferred.
    """
    os.makedirs(lower_dir, exist_ok=True)
    upper_dir = os.path.abspath(upper_dir)
    lower_dir = os.path.abspath(lower_dir)

    log(f"RSYNC: {upper_dir} -> {lower_dir}", "DEBUG")
    apply_whiteouts(upper_dir, lower_dir)

    args = [
        "rsync", "-aHA", "--inplace",
        "--no-specials", "--no-devices",
        "--exclude", ".wh.*",
        "--stats", "--human-readable",
        upper_dir.rstrip("/") + "/",
        lower_dir.rstrip("/") + "/",
    ]
    proc = run(args, check=False, quiet=True)
    output = proc.stdout or ""
    error = proc.stderr or ""
    transferred = 0
    total_bytes = None

    for line in output.splitlines():
        if "Number of regular files transferred" in line:
            try:
                transferred = int(line.split(":")[1].strip())
            except (IndexError, ValueError):
                pass
        elif "Total transferred file size" in line:
            try:
                total_bytes = line.split(":")[1].strip()
            except IndexError:
                pass

    if transferred > 0 or total_bytes:
        info = []
        if transferred:
            info.append(f"{transferred} files")
        if total_bytes:
            info.append(f"{total_bytes} transferred")
        log(f"RSYNC_STATS: {upper_dir} -> {lower_dir} :: {'; '.join(info)}", "INFO")

    if proc.returncode == 0:
        log(f"RSYNC_OK: {upper_dir} -> {lower_dir}", "DEBUG")
        return True
    if proc.returncode in (23, 24):
        # Partial/vanished files — OK for log dirs and ephemeral paths
        log(f"RSYNC_WARN({proc.returncode}): {upper_dir} -> {lower_dir} (partial/vanished files)", "WARN")
        return True
    log(f"RSYNC_FAILED: exit {proc.returncode} ({error.strip()})", "ERROR")
    return False


def clear_upper_dir(upper_dir: str):
    """Remove all entries from the upper dir (including .wh.* and subdirs)."""
    for entry in os.listdir(upper_dir):
        if entry in ('.', '..'):
            continue
        p = os.path.join(upper_dir, entry)
        try:
            if os.path.isdir(p) and not os.path.islink(p):
                shutil.rmtree(p, ignore_errors=True)
            else:
                os.remove(p)
        except FileNotFoundError:
            pass
        except Exception as e:
            log(f"CLEAR_UPPER_WARN: failed to remove {p}: {e}", "WARN")

def flush(cfg):
    log("FLUSH_START: Beginning flush operation")
    errors = 0
    for path, mode in parse_paths(cfg.get("paths", "")):
        if mode not in ("flush", "persist", "writeback"):
            log(f"SKIP_MODE: {path} mode={mode}", "DEBUG")
            continue

        p = Path(path).resolve()
        name = sanitize_name(str(p))
        upper = TMPFS_MOUNT / name / "upper"
        work = TMPFS_MOUNT / name / "work"
        if not upper.exists():
            log(f"NO_UPPER: {p}", "DEBUG")
            continue

        lower = lower_bind_for(name)
        if not is_mountpoint(lower):
            lower = ensure_lower_bind(p, name)

        # Unmount overlay so lower changes are seen on remount
        if is_overlay_mounted(p):
            log(f"PRE-FLUSH_UNMOUNT: {p}")
            if not force_unmount_with_retry(p):
                log(f"PRE-FLUSH_UNMOUNT_FAILED: {p}", "ERROR")
                errors += 1
                continue

        ok = False
        try:
            # RW for rsync
            remount_lower(lower, rw=True)

            # scrub before syncing (whiteouts/opaque markers)
            changed, bad = scrub_bad_specials(str(upper))
            opq_removed = 0
            try:
                opq_removed = clear_overlay_opq_and_whiteouts(upper)
            except Exception:
                pass
            if changed or opq_removed:
                log(f"SCRUBBED_UPPER: specials={len(bad)} opq_removed={opq_removed} under {upper}")

            # actual flush
            ok = rsync_upper_to_lower(str(upper), str(lower))

        except Exception as e:
            errors += 1
            log(f"FLUSH_ERROR: {p} - {e}", "ERROR")
        finally:
            # ALWAYS go back to RO
            try:
                remount_lower(lower, rw=False)
            except Exception as e2:
                errors += 1
                log(f"LOWER_RO_RESTORE_FAILED: {lower} - {e2}", "ERROR")

        if ok:
            # post-sync cleanup of upper
            try:
                try:
                    clear_overlay_opq_and_whiteouts(upper)
                except Exception:
                    pass
                clear_upper_dir(str(upper))
                log(f"FLUSHED: {p}")
            except Exception as e:
                errors += 1
                log(f"CLEAR_UPPER_FAILED: {upper} - {e}", "ERROR")
        else:
            log(f"FLUSH_FAILED: {p}", "ERROR")

        # Completely recreate both upper and work directories to avoid reuse conflicts
        try:
            # Remove and recreate upper directory
            if upper.exists():
                shutil.rmtree(upper, ignore_errors=True)
            upper.mkdir(parents=True, exist_ok=True)
            log(f"RECREATED_UPPERDIR: {upper}")

            # Remove and recreate work directory
            if work.exists():
                shutil.rmtree(work, ignore_errors=True)
            work.mkdir(parents=True, exist_ok=True)
            log(f"RECREATED_WORKDIR: {work}")
        except Exception as e:
            log(f"DIR_RECREATE_FAILED: {e}", "ERROR")
            errors += 1

        # Remount overlay (always) so merged view sees the new lower state
        opts = f"lowerdir={lower},upperdir={upper},workdir={work},metacopy=off,redirect_dir=off,index=off"
        log(f"POST-FLUSH_MOUNT: {p} lowerdir={lower}")
        mount_result = run(["mount", "-t", "overlay", "overlay", "-o", opts, str(p)], check=False)

        if mount_result.returncode != 0:
            log(f"POST-FLUSH_MOUNT_FAILED: {p} - exit {mount_result.returncode}", "ERROR")
            errors += 1

    log("FLUSH_COMPLETE: Success" if errors == 0 else f"FLUSH_COMPLETE: {errors} errors",
        "ERROR" if errors else "INFO")
    return errors == 0

def unmount(cfg):
    log("UNMOUNT_START: Beginning unmount operation")
    items = list(parse_paths(cfg.get("paths","")))
    # 1) remove any overlay mounted ON the lower bind
    for path, _ in items:
        name = sanitize_name(str(Path(path).resolve()))
        lower = lower_bind_for(name)
        if is_overlay_mounted(lower):
            log(f"LOWER_OVERLAY_UNMOUNT: {lower}")
            force_unmount_with_retry(lower)

    # 2) unmount main overlays deepest-first
    paths = [Path(p).resolve() for p, _ in items]
    for p in sorted(paths, key=lambda x: len(str(x)), reverse=True):
        name = sanitize_name(str(p))
        lower = lower_bind_for(name)
        if is_overlay_mounted(p):
            log(f"UNMOUNT: {p}")
            force_unmount_with_retry(p)
        else:
            log(f"NOT_MOUNTED: {p}", "DEBUG")
        if is_mountpoint(lower):
            log(f"LOWER_UNMOUNT: {lower}")
            force_unmount_with_retry(lower)

    # 3) drop tmpfs if nothing is using it
    with open("/proc/mounts") as f:
        mounts = f.read()
    if f" {TMPFS_MOUNT} tmpfs " in mounts:
        log(f"TMPFS_UNMOUNT: {TMPFS_MOUNT}")
        force_unmount_with_retry(TMPFS_MOUNT)
    else:
        log(f"TMPFS_NOT_MOUNTED: {TMPFS_MOUNT}", "DEBUG")
    log("UNMOUNT_COMPLETE: Finished")

def cmd_mount():
    log("MOUNT_CMD: Starting mount command")
    cfg = load_config()
    if cfg.get("enable", True):
        mount_overlays(cfg)
    else:
        log("DISABLED: Not mounting", "WARN")

def cmd_flush():
    log("FLUSH_CMD: Starting flush command")
    cfg = load_config()
    ok = flush(cfg)
    sys.exit(0 if ok else 1)

def cmd_unmount():
    log("UNMOUNT_CMD: Starting unmount command")
    cfg = load_config()
    if cfg.get("flush_on_shutdown", True):
        log("PRE_UNMOUNT_FLUSH: Flushing before unmount")
        flush(cfg)
    unmount(cfg)

def main():
    if len(sys.argv) < 2:
        print("Usage: omv-writecache [mount|rotateflush|flush|unmount|remount|status]")
        sys.exit(2)

    cmd = sys.argv[1]

    if cmd in ["-v", "--version", "version"]:
        print(SCRIPT_VERSION)
        sys.exit(0)

    if os.geteuid() != 0:
        print("This tool must be run as root", file=sys.stderr)
        sys.exit(1)

    check_and_create_lock("omv-writecache")
    if cmd in ["mount","rotateflush","flush","unmount","remount"]:
        ensure_persistent_journal()

    log(f"SCRIPT_START: omv-writecache {cmd}")

    if cmd == "mount":
        cmd_mount()
    elif cmd == "rotateflush":
        journalctl_rotate_sync()
        cmd_flush()
    elif cmd == "flush":
        cmd_flush()
    elif cmd == "unmount":
        cmd_unmount()
    elif cmd == "remount":
        cmd_unmount()
        cmd_mount()
    elif cmd == "status":
        log("STATUS_CMD: Starting status command")
        lines = []
        with open("/proc/mounts") as f:
            for line in f:
                parts = line.split()
                if len(parts) < 4:
                    continue
                device, mnt, fstype, opts = parts[0], parts[1], parts[2], parts[3]
                # Base tmpfs for writecache
                if fstype == "tmpfs" and mnt == "/run/omv-writecache":
                    lines.append(line.strip())
                    continue
                # Overlay mounts created by writecache
                if fstype == "overlay" and "upperdir=/run/omv-writecache/" in opts:
                    lines.append(line.strip())
        if lines:
            print("\n".join(lines))
            log(f"STATUS: Found {len(lines)} writecache mounts")
        else:
            print("No writecache mounts found")
            log("STATUS: No writecache mounts found")
    else:
        log(f"UNKNOWN_CMD: {cmd}", "ERROR")
        print("Unknown command")
        sys.exit(2)

    log(f"SCRIPT_END: omv-writecache {cmd}")

    if cmd in ["mount","rotateflush","flush","unmount","remount"]:
        # release lock
        release_lock()


# Handle termination signals (Ctrl+C, kill command)
signal.signal(signal.SIGINT, handle_signal)   # Handle Ctrl+C
signal.signal(signal.SIGTERM, handle_signal)  # Handle termination (kill command)

if __name__ == "__main__":
    try:
        main()
    finally:
        release_lock()
