Minimalistische GTK‑Anwendung mit PyGObject zur Anzeige von systemd‑Diensten

Das folgende Python‑Programm erstellt eine einfache, übersichtliche grafische Oberfläche (GUI) unter Linux. Es zeigt alle aktuellen systemd‑Dienste an und bietet Schaltflächen zum Aktualisieren, Starten, Stoppen, Neustarten sowie zum Aktivieren/Deaktivieren von Diensten.

  • Die Liste der Dienste wird über den Befehl systemctl --type=service --all --no-legend --plain eingelesen und zuverlässig ausgewertet.
  • Jede Zeile enthält die Spalten: Unit, Load, Active, Sub und Description.
  • Aktionen wie Start, Stop, Restart, Enable oder Disable benötigen in der Regel Administratorrechte. Das Programm versucht dafür pkexec zu verwenden (grafischer Authentifizierungsdialog). Falls pkexec nicht vorhanden ist, wird auf sudo im Terminal zurückgegriffen.

Funktionsumfang der Oberfläche

  • Filterfeld: Dienste können nach Namen, Status oder Beschreibung durchsucht werden.
  • Aktualisieren‑Button: Lädt die Liste neu.
  • Dienstliste: Anzeige in einer Tabelle mit Spalten für Unit, Status und Beschreibung.
  • Aktionsbuttons: Start, Stop, Restart, Enable, Disable – je nach aktuellem Status aktiv oder deaktiviert.
  • Statuszeile: Zeigt Rückmeldungen zu ausgeführten Aktionen an.

Installation und Nutzung

  • Abhängigkeiten:
    • Python 3
    • PyGObject (sudo apt install python3-gi gir1.2-gtk-3.0 auf Debian/Ubuntu)
    • Optional: pkexec (PolicyKit, meist im Paket policykit-1 enthalten)
  • Starten:
    • Datei speichern als systemd_gui.py
    • Ausführen mit: python3 systemd_gui.py
#!/usr/bin/env python3
import subprocess
import shlex
import sys
from gi.repository import Gtk, GObject

# Hilfsfunktion: Shell-Befehl ausführen und Ausgabe zurückgeben
def run_cmd(cmd):
    try:
        out = subprocess.check_output(shlex.split(cmd), stderr=subprocess.STDOUT)
        return out.decode("utf-8", errors="replace")
    except subprocess.CalledProcessError as e:
        return e.output.decode("utf-8", errors="replace")

# Liste aller Dienste abrufen
def list_services():
    """
    Nutzt `systemctl --type=service --all --no-legend --plain`
    und zerlegt die Ausgabe in Spalten.
    """
    cmd = "systemctl --type=service --all --no-legend --plain"
    output = run_cmd(cmd)
    services = []
    for line in output.splitlines():
        # Zeilen enthalten: UNIT LOAD ACTIVE SUB DESCRIPTION
        parts = line.split(None, 4)
        if len(parts) < 5:
            parts += [""] * (5 - len(parts))
        unit, load, active, sub, description = parts
        services.append({
            "unit": unit,
            "load": load,
            "active": active,
            "sub": sub,
            "description": description
        })
    return services

# Prüfen, ob Dienst aktiviert ist
def service_enabled(unit):
    out = run_cmd(f"systemctl is-enabled {shlex.quote(unit)}").strip()
    return out == "enabled"

# Prüfen, ob Dienst überhaupt aktivierbar ist (nicht "static")
def service_has_install(unit):
    out = run_cmd(f"systemctl show {shlex.quote(unit)} -p CanInstall")
    return "CanInstall=yes" in out

# Aktion auf Dienst ausführen (start/stop/restart/enable/disable)
def run_action(unit, action):
    base = f"systemctl {action} {shlex.quote(unit)}"
    if action in {"start", "stop", "restart", "enable", "disable"}:
        # Für Aktionen mit Root-Rechten: pkexec bevorzugt, sonst sudo
        pkexec_path = subprocess.run(["which", "pkexec"], capture_output=True, text=True)
        if pkexec_path.returncode == 0:
            cmd = f"pkexec {base}"
        else:
            cmd = f"sudo {base}"
    else:
        cmd = base
    return run_cmd(cmd)

# Hauptfenster
class ServiceList(Gtk.Window):
    def __init__(self):
        super().__init__(title="systemd Dienste")
        self.set_default_size(1000, 600)

        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        self.add(vbox)

        # Suchfeld + Aktualisieren-Button
        toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        vbox.pack_start(toolbar, False, False, 0)

        self.search_entry = Gtk.SearchEntry()
        self.search_entry.set_placeholder_text("Filter (Unit, Beschreibung, Status)")
        self.search_entry.connect("search-changed", self.on_search_changed)
        toolbar.pack_start(self.search_entry, True, True, 0)

        refresh_btn = Gtk.Button(label="Aktualisieren")
        refresh_btn.connect("clicked", self.on_refresh_clicked)
        toolbar.pack_start(refresh_btn, False, False, 0)

        # Datenmodell für Dienste
        self.store = Gtk.ListStore(str, str, str, str, str, str)  
        # Spalten: unit, load, active, sub, enabled, description
        self.filtered = self.store.filter_new()
        self.filtered.set_visible_func(self.filter_func)

        # Tabellenansicht
        self.view = Gtk.TreeView(model=self.filtered)
        self.view.get_selection().set_mode(Gtk.SelectionMode.SINGLE)

        def add_col(title, idx, expand=False):
            renderer = Gtk.CellRendererText()
            column = Gtk.TreeViewColumn(title, renderer, text=idx)
            column.set_expand(expand)
            self.view.append_column(column)

        add_col("Unit", 0, True)
        add_col("Load", 1)
        add_col("Active", 2)
        add_col("Sub", 3)
        add_col("Enabled", 4)
        add_col("Beschreibung", 5, True)

        scroller = Gtk.ScrolledWindow()
        scroller.add(self.view)
        vbox.pack_start(scroller, True, True, 0)

        # Aktionsbuttons
        action_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        vbox.pack_start(action_box, False, False, 6)

        self.start_btn = Gtk.Button(label="Start")
        self.stop_btn = Gtk.Button(label="Stop")
        self.restart_btn = Gtk.Button(label="Restart")
        self.enable_btn = Gtk.Button(label="Enable")
        self.disable_btn = Gtk.Button(label="Disable")

        for btn in [self.start_btn, self.stop_btn, self.restart_btn, self.enable_btn, self.disable_btn]:
            action_box.pack_start(btn, False, False, 0)
            btn.set_sensitive(False)

        self.start_btn.connect("clicked", self.action_clicked, "start")
        self.stop_btn.connect("clicked", self.action_clicked, "stop")
        self.restart_btn.connect("clicked", self.action_clicked, "restart")
        self.enable_btn.connect("clicked", self.action_clicked, "enable")
        self.disable_btn.connect("clicked", self.action_clicked, "disable")

        # Auswahländerung überwachen
        self.view.get_selection().connect("changed", self.on_selection_changed)

        # Statuszeile
        self.status = Gtk.Label(xalign=0)
        vbox.pack_start(self.status, False, False, 6)

        self.populate()

    # Dienste laden
    def populate(self):
        self.store.clear()
        services = list_services()
        for svc in services:
            enabled = "n/a"
            try:
                if service_has_install(svc["unit"]):
                    enabled = "enabled" if service_enabled(svc["unit"]) else "disabled"
                else:
                    enabled = "static"
            except Exception:
                enabled = "unknown"
            self.store.append([
                svc["unit"],
                svc["load"],
                svc["active"],
                svc["sub"],
                enabled,
                svc["description"]
            ])
        self.status.set_text(f"{len(services)} Dienste geladen.")

    def on_refresh_clicked(self, _btn):
        self.populate()

    def on_search_changed(self, entry):
        self.filtered.refilter()

    # Filterfunktion für Suchfeld
    def filter_func(self, model, iter, _data=None):
        q = self.search_entry.get_text().strip().lower()
        if not q:
            return True
        fields = [model[iter][i] for i in range(6)]
        return any(q in (f or "").lower() for f in fields)

    # Ausgewählten Dienst ermitteln
    def get_selected_unit(self):
        sel = self.view.get_selection()
        model, treeiter = sel.get_selected()
        if treeiter:
            return model[treeiter][0], model[treeiter][4], model[treeiter][2]
        return None, None, None

    # Buttons je nach Status aktivieren/deaktivieren
    def on_selection_changed(self, _selection):
        unit, enabled, active = self.get_selected_unit()
        has_selection = unit is not None

        self.start_btn.set_sensitive(has_selection and active != "active")
        self.stop_btn.set_sensitive(has_selection and active == "active")
        self.restart_btn.set_sensitive(has_selection and active == "active")
        self.enable_btn.set_sensitive(has_selection and enabled in {"disabled", "unknown"})
        self.disable_btn.set_sensitive(has_selection and enabled == "enabled")

    # Aktion ausführen
    def action_clicked(self, _btn, action):
        unit, _, _ = self.get_selected_unit()
        if not unit:
            return
        self.status.set_text(f"{action} wird ausgeführt: {unit} …")
        GObject.idle_add(self.perform_action, unit, action)

    def perform_action(self, unit, action):
        output = run_action(unit, action)
        self.populate()
        tail = "\n".join(output.strip().splitlines()[-3:])
        if tail:
            self.status.set_text(f"{action} {unit}: {tail}")
        else:
            self.status.set_text(f"{action} {unit}: erledigt.")

# Einstiegspunkt
def main():
    try:
        app = ServiceList()
        app.connect("destroy", Gtk.main_quit)
        app.show_all()
        Gtk.main()
    except Exception as e:
        print(f"Fehler: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    main()

© 2025 MaDe-Online