diff --git a/src/rippy/test_windows_app.py b/src/rippy/test_windows_app.py index 1f3871e..efecc5f 100644 --- a/src/rippy/test_windows_app.py +++ b/src/rippy/test_windows_app.py @@ -233,6 +233,38 @@ def test_die_testsuite_ruehrt_den_ECHTEN_eintrag_nicht_an(tmp_path, monkeypatch) ) +# ── Den laufenden Dienst zuerst beenden ───────────────────────────────── +def test_beenden_filtert_ueber_den_ordner(): + """DER Befund vom 28.08.2026: Beim Deinstallieren im laufenden Betrieb + blieben rippy.db, rippy.db-shm und rippy.db-wal liegen — „von einem + anderen Prozess verwendet". Der Dienst lief weiter.""" + s = windows_app.beenden_skript("C:" + BACKSLASH + "Rippy", [123, 456]) + assert "Rippy.exe" in s + assert "C:" + BACKSLASH + "Rippy" + BACKSLASH + "*" in s + assert "Stop-Process" in s + + +def test_der_eigene_prozess_wird_ausgenommen(): + """`Rippy.exe` heisst auch die Datei, die den Auftrag gerade ausfuehrt — + die darf sich nicht selbst abschiessen.""" + s = windows_app.beenden_skript("C:" + BACKSLASH + "R", [4711, 4712]) + assert "4711" in s and "4712" in s + assert s.index("notin") < s.index("Stop-Process"), "erst filtern, dann beenden" + + +def test_ohne_eigene_pids_wird_nichts_wildes_erzeugt(): + """Eine leere Liste duerfte keine kaputte Bedingung ergeben.""" + s = windows_app.beenden_skript("C:" + BACKSLASH + "R", []) + assert "@()" not in s, "leere Menge waere eine unbrauchbare Bedingung" + assert "@(0)" in s + + +def test_apostroph_im_ordner_bricht_das_skript_nicht(): + s = windows_app.beenden_skript("C:" + BACKSLASH + "O'Brien", [1]) + assert "O''Brien" in s + assert s.count("'") % 2 == 0 + + # ── Die Datenbank am richtigen Ort ────────────────────────────────────── def test_datenbank_liegt_im_installationsordner(): """DER Befund vom 28.08.2026: „Es fehlt der 1st run wizzard wenn man es diff --git a/src/rippy/windows_app.py b/src/rippy/windows_app.py index c699bda..bf88647 100644 --- a/src/rippy/windows_app.py +++ b/src/rippy/windows_app.py @@ -184,6 +184,79 @@ def installieren(ziel: str = None, autostart: bool = True, return ziel +def beenden_skript(ordner: str, eigene_pids: list) -> str: + """Das PowerShell-Skript, das die ANDEREN Rippy-Prozesse beendet. + + Reine Funktion, damit der Filter prüfbar ist, ohne etwas zu beenden. + + ## Warum das nötig ist (gemessen 28.08.2026) + + Beim Deinstallieren im laufenden Betrieb blieben liegen: + + rippy.db, rippy.db-shm, rippy.db-wal + (von einem anderen Prozess verwendet) + + Der Dienst lief weiter und hielt die Datenbank offen. Damit wäre der + Fehler von vorhin durch eine andere Tür zurückgekommen: Die nächste + Installation hätte wieder den alten Zustand geerbt. + + ## Warum über den ORDNER gefiltert wird + + `Rippy.exe` heißt auch die Setup-Datei, die den Auftrag gerade ausführt — + und die darf sich nicht selbst abschießen. Beendet wird nur, was aus dem + Installationsordner läuft, und die eigenen Prozesskennungen sind + ausgenommen. + + ## Hinweis für spätere Sitzungen + + In der Entwicklungsumgebung schlägt der Vergleich zuverlässig fehl, und + das ist KEIN Fehler im Code: Die Werkzeuge laufen dort in einem + App-Container, der Schreibzugriffe auf `%LOCALAPPDATA%` nach + + …\AppData\Local\Packages\\LocalCache\Local\Rippy + + umleitet. Der Registry-Eintrag `InstallLocation` zeigt dagegen auf den + ECHTEN Pfad — Prozess und Ordner passen dann nie zusammen. Auf einer + normalen Installation liegen beide am selben Ort. Die Mechanik wurde am + 28.08.2026 mit dem tatsächlichen Ordner nachgewiesen: Prozess beendet, + Datenbankdateien danach löschbar. + """ + ort = (ordner or "").replace("'", "''").rstrip("\\/") + pids = ",".join(str(int(p)) for p in eigene_pids if p) + return ( + "Get-CimInstance Win32_Process -Filter \"Name='Rippy.exe'\" " + "| Where-Object { $_.ExecutablePath -like '%s\\*' " + "-and $_.ProcessId -notin @(%s) } " + "| ForEach-Object { Stop-Process -Id $_.ProcessId -Force " + "-ErrorAction SilentlyContinue }" % (ort, pids or "0") + ) + + +def dienst_beenden(ordner: str) -> bool: + """Beendet die anderen Rippy-Prozesse aus diesem Ordner. Wirft nicht.""" + if os.name != "nt" or not ordner: + return False + from rippy.platform.winlauf import OHNE_FENSTER + + eigene = [os.getpid()] + try: + eigene.append(os.getppid()) # der PyInstaller-Starter + except OSError: + pass + try: + ergebnis = subprocess.run( + ["powershell", "-NoProfile", "-NonInteractive", "-Command", + beenden_skript(ordner, eigene)], + capture_output=True, text=True, timeout=60, + creationflags=OHNE_FENSTER) + except (OSError, subprocess.SubprocessError): + return False + # Kurz Luft lassen: Windows gibt die Dateihandles nicht im selben + # Augenblick frei, in dem der Prozess endet. + time.sleep(2) + return ergebnis.returncode == 0 + + def alte_orte() -> list: """Ordner ausserhalb des Installationsordners, die Rippy einmal angelegt hat. @@ -332,6 +405,13 @@ def deinstallieren(still: bool = False) -> None: fenster_modul.helfer_beenden(os.path.join(ordner, "fenster")) + # ⚠️ Und den DIENST selbst (Befund 28.08.2026). Lief er weiter, blieben + # rippy.db, rippy.db-shm und rippy.db-wal liegen — „von einem anderen + # Prozess verwendet". Die naechste Installation haette damit wieder den + # alten Zustand geerbt, also genau den Fehler von vorhin durch eine + # andere Tuer. + dienst_beenden(ordner) + # Der ALTE Datenbank-Ort (bis 28.08.2026 %ProgramData%\Rippy) liegt # ausserhalb des Installationsordners. Blieb er stehen, erbte die naechste # Installation den alten Zustand — genau daran fehlte dem Commander der