#!/usr/bin/env python3
"""LKE-Sicherheit — single-file Linux security scanner UI.

The program only installs distribution packages from configured official
repositories.  Scan actions are read-only; it never deletes or quarantines
customer data automatically.
"""
import os
import platform
import queue
import math
import random
import re
import shutil
import subprocess
import sys
import threading
import time
from pathlib import Path


APP = "LKE-Sicherheit"
VERSION = "1.0"


def sh(cmd, timeout=None):
    """Run a command safely and return its combined text output."""
    try:
        run = subprocess.run(cmd, text=True, stdout=subprocess.PIPE,
                             stderr=subprocess.STDOUT, timeout=timeout)
        return run.returncode, run.stdout.strip()
    except subprocess.TimeoutExpired:
        return 124, "Zeitüberschreitung."
    except FileNotFoundError:
        return 127, f"Nicht gefunden: {cmd[0]}"
    except Exception as exc:
        return 1, str(exc)


def os_info():
    data = {}
    try:
        for line in Path("/etc/os-release").read_text().splitlines():
            if "=" in line:
                key, value = line.split("=", 1)
                data[key] = value.strip().strip('"')
    except OSError:
        pass
    return data


def package_manager():
    for command, family in (("apt-get", "apt"), ("dnf", "dnf"), ("yum", "yum"),
                            ("pacman", "pacman"), ("zypper", "zypper"), ("apk", "apk")):
        if shutil.which(command):
            return command, family
    return None, None


def package_command(family, packages):
    # Disable pseudo-terminals and optional recommendations: GUI logs stay clean
    # and packages such as postfix are not pulled in just for mail notifications.
    if family == "apt":
        return (["apt-get", "-o", "Dpkg::Use-Pty=0", "update"],
                ["apt-get", "-o", "Dpkg::Use-Pty=0", "install", "-y", "--no-install-recommends", *packages])
    if family == "dnf": return None, ["dnf", "install", "-y", *packages]
    if family == "yum": return None, ["yum", "install", "-y", *packages]
    if family == "pacman": return None, ["pacman", "-Sy", "--noconfirm", *packages]
    if family == "zypper": return None, ["zypper", "--non-interactive", "install", *packages]
    if family == "apk": return None, ["apk", "add", *packages]
    return None, None


try:
    import tkinter as tk
    from tkinter import filedialog, messagebox, ttk
except ImportError:
    # Bootstrapping Tk is intentional: this is the only external GUI component.
    mgr, family = package_manager()
    if not mgr:
        print("LKE-Sicherheit benötigt Python Tkinter. Kein Paketmanager erkannt.")
        sys.exit(1)
    packages = {"apt": ["python3-tk"], "dnf": ["python3-tkinter"], "yum": ["python3-tkinter"],
                "pacman": ["tk"], "zypper": ["python3-tk"], "apk": ["py3-tkinter"]}[family]
    _, install = package_command(family, packages)
    print("Installiere GUI-Voraussetzung (ggf. sudo-Passwort eingeben) …")
    subprocess.run(["sudo", *install], check=False)
    os.execv(sys.executable, [sys.executable, *sys.argv])


class LKESafe(tk.Tk):
    bg, panel, panel2 = "#07152d", "#0b2347", "#0e315f"
    cyan, blue, text, muted, danger = "#27c5ff", "#2476ff", "#eff8ff", "#9bb5d4", "#ff6685"

    def __init__(self):
        super().__init__()
        self.title(f"{APP} {VERSION}")
        self.geometry("1160x760")
        self.minsize(960, 650)
        self.configure(bg=self.bg)
        self.jobs = queue.Queue()
        self.running = False
        self.target = tk.StringVar(value="/")
        self.status = tk.StringVar(value="System wird geprüft …")
        self.score = tk.StringVar(value="…")
        self.score_detail = tk.StringVar(value="Werkzeuge werden erkannt")
        self.tool_state = tk.StringVar(value="Werkzeuge werden erkannt …")
        self.animation_tick = 0
        self.ambient_nodes = [(random.randint(10, 430), random.randint(12, 78),
                               random.choice((-0.55, -0.35, 0.35, 0.55)), random.randint(2, 4))
                              for _ in range(16)]
        self._style()
        self._layout()
        self.after(80, self._drain)
        self.after(200, self._startup_check)
        self.after(100, self._pulse)

    def _style(self):
        style = ttk.Style(self)
        style.theme_use("clam")
        style.configure("TProgressbar", troughcolor=self.panel, background=self.cyan,
                        bordercolor=self.panel, lightcolor=self.cyan, darkcolor=self.blue)

    def label(self, parent, text, size=11, color=None, weight="normal", **kw):
        options = dict(bg=parent.cget("bg"), fg=color or self.text,
                       font=("TkDefaultFont", size, weight), **kw)
        # Without textvariable Tk prints the internal name (for example PY_VAR2).
        if isinstance(text, tk.Variable):
            options["textvariable"] = text
        else:
            options["text"] = text
        return tk.Label(parent, **options)

    def button(self, parent, text, command, accent=False):
        return tk.Button(parent, text=text, command=command, relief="flat", bd=0,
                         cursor="hand2", padx=16, pady=10, font=("TkDefaultFont", 10, "bold"),
                         bg=self.cyan if accent else self.panel2,
                         fg=self.bg if accent else self.text,
                         activebackground="#75dcff" if accent else "#17477f",
                         activeforeground=self.bg if accent else self.text)

    def _layout(self):
        header = tk.Frame(self, bg="#061127", height=92)
        header.pack(fill="x")
        header.pack_propagate(False)
        brand = tk.Frame(header, bg="#061127")
        brand.pack(side="left", padx=30, pady=15)
        self.label(brand, "◈  LKE", 23, self.cyan, "bold").pack(side="left")
        self.label(brand, "-SICHERHEIT", 23, self.text, "bold").pack(side="left")
        self.label(brand, "  Linux Security Center", 10, self.muted).pack(side="left", pady=(8, 0))
        self.dot = tk.Label(header, text="●", bg="#061127", fg="#33e6a0", font=("TkDefaultFont", 18))
        self.dot.pack(side="right", padx=(0, 8))
        self.label(header, "SCHUTZ BEREIT", 9, self.muted, "bold").pack(side="right")
        self.header_fx = tk.Canvas(header, bg="#061127", highlightthickness=0, bd=0)
        self.header_fx.place(relx=.42, y=0, relwidth=.50, relheight=1)
        self.header_fx.tk.call("lower", self.header_fx._w)

        main = tk.Frame(self, bg=self.bg)
        main.pack(fill="both", expand=True, padx=24, pady=20)
        sidebar = tk.Frame(main, bg=self.panel, width=245)
        sidebar.pack(side="left", fill="y", padx=(0, 18))
        sidebar.pack_propagate(False)
        self.label(sidebar, "SICHERHEITSSTATUS", 9, self.muted, "bold").pack(anchor="w", padx=20, pady=(22, 6))
        self.label(sidebar, "Sicherheits-\nCockpit", 20, self.text, "bold", justify="left").pack(anchor="w", padx=20)
        scorebox = tk.Frame(sidebar, bg=self.panel2)
        scorebox.pack(fill="x", padx=18, pady=22)
        self.label(scorebox, "STATUS", 8, self.muted, "bold").pack(pady=(12, 0))
        self.label(scorebox, self.score, 26, self.cyan, "bold").pack(pady=(1, 0))
        self.label(scorebox, self.score_detail, 9, self.muted, wraplength=190).pack(pady=(0, 12))
        for title, info in (("◉  Live-Prüfung", "Pakete & Dienste"), ("◌  Datenschutz", "Keine Auto-Löschung"),
                            ("⌁  Plattform", platform.system())):
            block = tk.Frame(sidebar, bg=self.panel)
            block.pack(fill="x", padx=20, pady=8)
            self.label(block, title, 10, self.text, "bold").pack(anchor="w")
            self.label(block, info, 9, self.muted).pack(anchor="w", pady=(2, 0))
        self.label(sidebar, f"LKE-Sicherheit v{VERSION}", 8, self.muted).pack(side="bottom", pady=18)

        # The action area deliberately scrolls independently.  This keeps every
        # tool reachable on small laptop displays and with high DPI scaling.
        scroll_host = tk.Frame(main, bg=self.bg)
        scroll_host.pack(side="left", fill="both", expand=True)
        self.canvas = tk.Canvas(scroll_host, bg=self.bg, highlightthickness=0, bd=0)
        rail = tk.Frame(scroll_host, bg="#061630", width=16)
        rail.pack(side="right", fill="y", padx=(8, 0))
        rail.pack_propagate(False)
        scrollbar = tk.Scrollbar(rail, command=self.canvas.yview, relief="flat", bd=0,
                                 width=10, bg="#1c5d9e", activebackground=self.cyan,
                                 troughcolor="#061630", highlightthickness=0,
                                 elementborderwidth=0)
        self.canvas.configure(yscrollcommand=scrollbar.set)
        scrollbar.pack(fill="y", expand=True, padx=3, pady=8)
        self.canvas.pack(side="left", fill="both", expand=True)
        content = tk.Frame(self.canvas, bg=self.bg)
        self.canvas_window = self.canvas.create_window((0, 0), window=content, anchor="nw")
        content.bind("<Configure>", lambda _event: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
        self.canvas.bind("<Configure>", lambda event: self.canvas.itemconfigure(self.canvas_window, width=event.width))
        self.canvas.bind_all("<MouseWheel>", self._scroll)
        self.canvas.bind_all("<Button-4>", lambda _event: self.canvas.yview_scroll(-3, "units"))
        self.canvas.bind_all("<Button-5>", lambda _event: self.canvas.yview_scroll(3, "units"))
        top = tk.Frame(content, bg=self.bg)
        top.pack(fill="x")
        self.label(top, "Dein Sicherheitszentrum", 24, self.text, "bold").pack(anchor="w")
        self.label(top, "Scans starten, System prüfen und Schutzwerkzeuge zentral verwalten.", 10, self.muted).pack(anchor="w", pady=(4, 16))
        action = tk.Frame(content, bg=self.panel)
        action.pack(fill="x")
        self.label(action, "Scan-Ziel", 10, self.muted, "bold").grid(row=0, column=0, padx=(18, 10), pady=17)
        entry = tk.Entry(action, textvariable=self.target, bg="#061a36", fg=self.text, insertbackground=self.cyan,
                         relief="flat", font=("TkDefaultFont", 11), width=42)
        entry.grid(row=0, column=1, ipady=8, padx=(0, 10), pady=12, sticky="ew")
        self.button(action, "ORDNER WÄHLEN", self.choose_target).grid(row=0, column=2, padx=4)
        self.button(action, "SCHNELL-SCAN", lambda: self.start("quick"), True).grid(row=0, column=3, padx=(4, 14))
        action.grid_columnconfigure(1, weight=1)

        grid = tk.Frame(content, bg=self.bg)
        grid.pack(fill="x", pady=16)
        cards = [
            ("◉", "Malware-Scan", "ClamAV: Signaturen & Dateien", "clam"),
            ("◈", "Rootkit-Check", "RKHunter + Chkrootkit", "rootkit"),
            ("⌁", "System-Audit", "Lynis-Hardening-Prüfung", "lynis"),
            ("◫", "Dienste prüfen", "Offene Ports & aktive Services", "services"),
            ("◫", "Chroot / Offline-Check", "Eingehängtes System prüfen", "chroot"),
            ("↻", "Signaturen aktualisieren", "ClamAV-Datenbank", "update"),
            ("⚙", "Werkzeuge installieren", "Erkannte Distribution nutzen", "install"),
        ]
        for n, (icon, title, desc, action_id) in enumerate(cards):
            card = tk.Frame(grid, bg=self.panel, highlightthickness=1, highlightbackground="#16416f")
            card.grid(row=n // 3, column=n % 3, sticky="nsew", padx=6, pady=6)
            self.label(card, icon, 22, self.cyan, "bold").pack(anchor="w", padx=16, pady=(15, 2))
            self.label(card, title, 12, self.text, "bold").pack(anchor="w", padx=16)
            self.label(card, desc, 9, self.muted, wraplength=190, justify="left").pack(anchor="w", padx=16, pady=(3, 12))
            self.button(card, "ÖFFNEN  →", lambda a=action_id: self.start(a)).pack(anchor="w", padx=14, pady=(0, 14))
            card.bind("<Enter>", lambda _event, c=card: self._card_hover(c, True))
            card.bind("<Leave>", lambda _event, c=card: self._card_hover(c, False))
        for col in range(3): grid.grid_columnconfigure(col, weight=1, uniform="cards")

        console = tk.Frame(content, bg="#041126", highlightthickness=1, highlightbackground="#123864")
        console.pack(fill="both", expand=True, pady=(4, 0))
        bar = tk.Frame(console, bg="#081b38")
        bar.pack(fill="x")
        self.label(bar, "AKTIVITÄT", 9, self.muted, "bold").pack(side="left", padx=14, pady=9)
        self.label(bar, self.status, 9, self.cyan).pack(side="right", padx=14)
        self.output = tk.Text(console, height=11, bg="#041126", fg="#c7e4ff", insertbackground=self.cyan,
                              relief="flat", wrap="word", font=("TkFixedFont", 9), state="disabled")
        self.output.pack(fill="both", expand=True, padx=12, pady=(4, 10))
        self.log(f"{APP} bereit. Alle Scans sind standardmäßig nicht-destruktiv.")

    def choose_target(self):
        folder = filedialog.askdirectory(initialdir=self.target.get() if Path(self.target.get()).exists() else "/")
        if folder: self.target.set(folder)

    def _scroll(self, event):
        """Support mouse wheels on X11, Wayland and common touchpads."""
        if event.delta:
            self.canvas.yview_scroll(-int(event.delta / 120), "units")

    def _card_hover(self, card, active):
        """A restrained lift/glow effect for interactive action cards."""
        card.configure(bg="#103766" if active else self.panel,
                       highlightbackground=self.cyan if active else "#16416f")

    def log(self, text):
        self.output.configure(state="normal")
        self.output.insert("end", f"[{time.strftime('%H:%M:%S')}] {text}\n")
        self.output.see("end")
        self.output.configure(state="disabled")

    def _pulse(self):
        """Render the subtle live-security HUD; no external image assets needed."""
        self.animation_tick += 1
        phase = self.animation_tick / 8
        self.dot.configure(fg="#8affcf" if int(phase) % 2 else "#33e6a0")
        self._draw_header_fx(phase)
        self.after(45, self._pulse)

    def _draw_header_fx(self, phase):
        canvas = self.header_fx
        width, height = max(canvas.winfo_width(), 1), max(canvas.winfo_height(), 1)
        canvas.delete("all")
        # Layered aurora waves create a calm moving background rather than a UI bar.
        for layer, color in enumerate(("#08264b", "#0a315b", "#0b3b6c", "#104a7b")):
            points = []
            for x in range(-12, width + 13, 12):
                y = height * (.75 - layer * .12) + math.sin(x / 72 + phase * (.35 + layer * .07)) * (8 + layer * 2)
                points.extend((x, y))
            canvas.create_line(*points, fill=color, width=2 + layer, smooth=True)
        # A faint orbital halo anchors the artwork at the right of the title bar.
        orbit_x, orbit_y = width * .78, height * .52
        for radius, color in ((42, "#0e3d69"), (28, "#155584"), (15, "#1a6698")):
            canvas.create_oval(orbit_x-radius, orbit_y-radius, orbit_x+radius, orbit_y+radius,
                               outline=color, width=1)
        satellite_angle = phase * .65
        sat_x = orbit_x + math.cos(satellite_angle) * 42
        sat_y = orbit_y + math.sin(satellite_angle) * 42
        canvas.create_oval(sat_x-3, sat_y-3, sat_x+3, sat_y+3, fill=self.cyan, outline="")
        points = []
        for index, (x, y, velocity, radius) in enumerate(self.ambient_nodes):
            x += velocity
            if x < 0 or x > 440: velocity *= -1; x = max(0, min(440, x))
            self.ambient_nodes[index] = (x, y, velocity, radius)
            px, py = x / 440 * width, y / 92 * height
            points.append((px, py))
        for index, (px, py) in enumerate(points):
            other_x, other_y = points[(index + 3) % len(points)]
            canvas.create_line(px, py, other_x, other_y, fill="#0c3c68", width=1)
        for px, py in points:
            glow = 3 + int(2 * (1 + math.sin(phase + px / 70)))
            canvas.create_oval(px-glow, py-glow, px+glow, py+glow, fill="#0b3158", outline="")
            canvas.create_oval(px-1.5, py-1.5, px+1.5, py+1.5, fill=self.cyan, outline="")

    def _drain(self):
        try:
            while True:
                kind, data = self.jobs.get_nowait()
                if kind == "log": self.log(data)
                elif kind == "status": self.status.set(data)
                elif kind == "score":
                    score, detail = data
                    self.score.set(score)
                    self.score_detail.set(detail)
                elif kind == "auto_install": self.start("install", automatic=True)
                elif kind == "done": self.running = False; self.status.set("Bereit")
        except queue.Empty: pass
        self.after(80, self._drain)

    def _startup_check(self):
        def worker():
            info = os_info(); mgr, family = package_manager()
            found = [x for x in ("clamscan", "rkhunter", "chkrootkit", "lynis") if shutil.which(x)]
            self.jobs.put(("log", f"Erkannt: {info.get('PRETTY_NAME', platform.platform())}"))
            self.jobs.put(("log", f"Paketmanager: {mgr or 'nicht erkannt'} | Werkzeuge: {', '.join(found) or 'noch nicht installiert'}"))
            self.jobs.put(("status", "Bereit — Werkzeuge installieren oder Scan wählen"))
            detail = f"{len(found)} von 4 Schutzmodulen aktiv"
            if len(found) < 4:
                detail += f" · {4-len(found)} fehlen"
            self.jobs.put(("score", (f"{len(found)} / 4", detail)))
            if len(found) < 4 and family:
                self.jobs.put(("log", "Fehlende Schutzmodule werden automatisch installiert …"))
                self.jobs.put(("auto_install", None))
        threading.Thread(target=worker, daemon=True).start()

    def start(self, action, automatic=False):
        if self.running:
            if not automatic:
                messagebox.showinfo(APP, "Ein Vorgang läuft bereits.")
            return
        if action in ("clam", "quick", "chroot") and not Path(self.target.get()).is_dir():
            messagebox.showerror(APP, "Bitte wähle einen existierenden Scan-Ordner.")
            return
        if action == "install" and not automatic and not messagebox.askyesno(APP, "Scanner aus den offiziellen Paketquellen installieren?\nDafür kann ein Administrator-Passwort abgefragt werden."):
            return
        self.running = True
        title = "Automatische Installation wird vorbereitet …" if action == "install" and automatic else "Vorgang wird gestartet …"
        self.status.set(title)
        self.log(title)
        self.after(50, lambda: self.canvas.yview_moveto(1.0))
        threading.Thread(target=self._run, args=(action,), daemon=True).start()

    def command(self, cmd, label):
        self.jobs.put(("log", f"▶ {label}"))
        self.jobs.put(("status", label))
        try:
            environment = os.environ.copy()
            environment.update({"DEBIAN_FRONTEND": "noninteractive", "APT_LISTCHANGES_FRONTEND": "none"})
            process = subprocess.Popen(cmd, text=True, stdout=subprocess.PIPE,
                                       stderr=subprocess.STDOUT, bufsize=1, env=environment)
            assert process.stdout is not None
            compact_packages = "Paket" in label or "Sicherheitswerkzeuge" in label
            if compact_packages:
                self.jobs.put(("log", "Pakete werden verarbeitet – Details sind bewusst kompakt gehalten."))
            for line in process.stdout:
                line = self._clean_terminal_line(line)
                if line and (not compact_packages or self._important_package_line(line)):
                    self.jobs.put(("log", line))
            code = process.wait()
        except FileNotFoundError:
            code = 127
            self.jobs.put(("log", f"Nicht gefunden: {cmd[0]}"))
        except Exception as exc:
            code = 1
            self.jobs.put(("log", f"Fehler: {exc}"))
        if code == 0:
            self.jobs.put(("log", "✓ Erfolgreich abgeschlossen."))
        else:
            self.jobs.put(("log", f"⚠ Beendet mit Status {code}."))
        return code

    @staticmethod
    def _clean_terminal_line(line):
        """Remove ANSI/debconf control sequences which only make sense in a terminal."""
        line = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", line)
        return "".join(char for char in line if char in "\t\n" or ord(char) >= 32).strip()

    @staticmethod
    def _important_package_line(line):
        value = line.lower()
        markers = ("paketlisten", "abhängigkeits", "statusinformationen", "die folgenden",
                   "aktualisiert", "neu installiert", "müssen", "nach dieser operation",
                   "eingerichtet", "verarbeitet", "fehler", "warnung", "konnte nicht",
                   "nicht installiert", "wird ", "fertig")
        return any(marker in value for marker in markers)

    def root_command(self, cmd):
        """Use Polkit in desktop sessions so automatic setup shows a GUI prompt."""
        if shutil.which("pkexec") and (os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")):
            return ["pkexec", *cmd]
        return ["sudo", *cmd]

    def _run(self, action):
        try:
            target = self.target.get()
            if action == "install":
                mgr, family = package_manager()
                if not family: self.jobs.put(("log", "Kein unterstützter Paketmanager erkannt.")); return
                packages = {"apt": ["clamav", "clamav-freshclam", "rkhunter", "chkrootkit", "lynis"],
                            "dnf": ["clamav", "clamav-update", "rkhunter", "chkrootkit", "lynis"],
                            "yum": ["clamav", "rkhunter", "chkrootkit", "lynis"],
                            "pacman": ["clamav", "rkhunter", "chkrootkit", "lynis"],
                            "zypper": ["clamav", "rkhunter", "chkrootkit", "lynis"],
                            "apk": ["clamav", "rkhunter", "chkrootkit", "lynis"]}[family]
                update, install = package_command(family, packages)
                if update: self.command(self.root_command(update), "Paketquellen aktualisieren")
                code = self.command(self.root_command(install), "Sicherheitswerkzeuge installieren")
                if code == 0:
                    self.jobs.put(("log", "Installation abgeschlossen. ClamAV-Signaturen werden aktualisiert …"))
                    self._update_signatures()
                    found = [x for x in ("clamscan", "rkhunter", "chkrootkit", "lynis") if shutil.which(x)]
                    self.jobs.put(("score", (f"{len(found)} / 4", f"{len(found)} von 4 Schutzmodulen aktiv")))
            elif action == "quick":
                self._clam(target, quick=True); self._services()
            elif action == "clam": self._clam(target)
            elif action == "rootkit":
                self.command(self.root_command(["rkhunter", "--check", "--sk"]), "RKHunter Rootkit-Prüfung")
                self.command(self.root_command(["chkrootkit"]), "Chkrootkit-Prüfung")
            elif action == "chroot":
                if target == "/":
                    self.jobs.put(("log", "Für den Chroot-/Offline-Check bitte den Einhängepunkt eines anderen Systems wählen (nicht /)."))
                elif not shutil.which("rkhunter"):
                    self.jobs.put(("log", "RKHunter fehlt. Bitte zuerst »Werkzeuge installieren« wählen."))
                else:
                    self.jobs.put(("log", "Offline-Modus: Der gewählte Einhängepunkt wird nur gelesen; keine chroot-Shell wird geöffnet."))
                    self.command(self.root_command(["rkhunter", "--check", "--sk", "--rootdir", target]), f"RKHunter Offline-Check: {target}")
            elif action == "lynis": self.command(self.root_command(["lynis", "audit", "system", "--quick", "--no-colors"]), "Lynis System-Audit")
            elif action == "services": self._services()
            elif action == "update":
                self._update_signatures()
        finally:
            self.jobs.put(("done", None))

    def _clam(self, target, quick=False):
        if not shutil.which("clamscan"):
            self.jobs.put(("log", "ClamAV fehlt. Bitte zuerst »Werkzeuge installieren« wählen.")); return
        cmd = ["clamscan", "--recursive", "--infected", "--bell", "--no-summary"]
        if quick: cmd.extend(["--max-filesize=25M", "--max-scansize=100M"])
        cmd.append(target)
        self.command(cmd, f"ClamAV {'Schnellscan' if quick else 'Malware-Scan'}: {target}")
        self.jobs.put(("log", "Hinweis: Funde wurden nur gemeldet, nicht verändert oder gelöscht."))

    def _update_signatures(self):
        """Avoid FreshClam's log lock when the packaged background service runs."""
        fresh = shutil.which("freshclam")
        if not fresh:
            self.jobs.put(("log", "freshclam fehlt. Bitte zuerst Werkzeuge installieren."))
            return
        service = "clamav-freshclam.service"
        active = shutil.which("systemctl") and sh(["systemctl", "is-active", "--quiet", service])[0] == 0
        if active:
            self.jobs.put(("log", "Der Hintergrund-Updater wird kurz für das manuelle Update pausiert."))
            self.command(self.root_command(["systemctl", "stop", service]), "ClamAV-Updater pausieren")
        try:
            self.command(self.root_command([fresh]), "ClamAV-Signaturen aktualisieren")
        finally:
            if active:
                self.command(self.root_command(["systemctl", "start", service]), "ClamAV-Updater wieder starten")

    def _services(self):
        self.command(["ss", "-tulpn"], "Offene Netzwerkports prüfen")
        if shutil.which("systemctl"):
            self.command(["systemctl", "--no-pager", "--type=service", "--state=running"], "Aktive Dienste auflisten")


if __name__ == "__main__":
    LKESafe().mainloop()
