From a5e64f8e56ff7a8291f07ea82e31119fc96f1d60 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Fri, 28 Aug 2026 13:15:58 +0200 Subject: [PATCH] fix(windows): der Deinstallierer liess 148 Dateien liegen und meldete Erfolg Beim ersten echten Deinstallieren auf dem Rechner des Commanders gemessen: Uninstall-Eintrag weg Autostart weg Verknuepfungen weg Dateien 149 NOCH DA (davon 148 in fenster\EBWebView\) Gemeldet wurde: "Rippy wurde entfernt." Zwei Ursachen, beide behoben: ## 1. WebView2 ueberlebt Rippy WebView2 startet eigene Prozesse (msedgewebview2.exe: Renderer, GPU, Netz, Crashpad). Stirbt Rippy, laufen sie als Waisen weiter und halten den Zwischenspeicher offen -- acht davon liefen noch, als ich nachsah. shutil.rmtree scheiterte an jeder gesperrten Datei. fenster.helfer_beenden() beendet sie jetzt VOR dem Loeschen. Gefiltert wird ueber den --user-data-dir in der Befehlszeile: msedgewebview2.exe benutzen auch andere Programme, und die alle zu beenden waere ein Uebergriff -- der Nutzer verloere die Fenster fremder Anwendungen. Ein Test haelt fest, dass erst gefiltert und dann beendet wird. ## 2. Der Fehler wurde verschluckt `except OSError: pass` in der Loeschschleife. Ein "Rippy wurde entfernt" ueber einem halb geleerten Ordner ist eine Falschaussage -- genau die Sorte stiller Fehlschlag, vor der AGENTS.md warnt. Jetzt wird gesammelt, was nicht ging, und mit dem Grund genannt. Dazu im Aufraeum-Skript: `rmdir /s /q` statt `rmdir`. Ein blosses rmdir scheitert an JEDER verbliebenen Datei und laesst den ganzen Ordner stehen -- selbst wenn spaeter nur noch eine Sperrdatei uebrig ist. Ampel lokal: 612 gruen, ruff sauber. Co-Authored-By: Claude Opus 5 --- src/rippy/fenster.py | 62 +++++++++++++++++++++++++++++++++++ src/rippy/test_fenster.py | 42 ++++++++++++++++++++++++ src/rippy/test_windows_app.py | 11 ++++++- src/rippy/windows_app.py | 34 ++++++++++++++++--- 4 files changed, 144 insertions(+), 5 deletions(-) diff --git a/src/rippy/fenster.py b/src/rippy/fenster.py index dd5dc90..d541323 100644 --- a/src/rippy/fenster.py +++ b/src/rippy/fenster.py @@ -159,6 +159,68 @@ def speicherort() -> str: return ordner +def helfer_skript(speicherort: str) -> str: + """Das PowerShell-Skript, das NUR unsere WebView2-Helfer beendet. + + Reine Funktion, damit der Filter prüfbar ist, ohne etwas zu beenden. + + ## Warum das nötig ist (gemessen 28.08.2026) + + Beim Deinstallieren blieben **148 Dateien** liegen, alle unter + `fenster\\EBWebView\\`. Der Grund: WebView2 startet eigene Prozesse + (`msedgewebview2.exe` — Renderer, GPU, Netz, Crashpad). Stirbt Rippy, + laufen sie als Waisen weiter und halten den Zwischenspeicher offen. + `shutil.rmtree` scheiterte, der Fehler wurde verschluckt, und der + Deinstallierer meldete trotzdem „Rippy wurde entfernt." + + ## Warum über den Speicherort gefiltert wird + + `msedgewebview2.exe` benutzen auch andere Programme. Sie alle zu beenden, + wäre ein Übergriff — der Nutzer verlöre die Fenster fremder Anwendungen. + Jeder Helfer trägt seinen `--user-data-dir` in der Befehlszeile; nur wer + auf UNSEREN Ordner zeigt, gehört uns. + """ + ort = (speicherort or "").replace("'", "''") + return ( + "Get-CimInstance Win32_Process -Filter \"Name='msedgewebview2.exe'\" " + "| Where-Object { $_.CommandLine -like '*%s*' } " + "| ForEach-Object { Stop-Process -Id $_.ProcessId -Force " + "-ErrorAction SilentlyContinue }" % ort + ) + + +def helfer_beenden(speicherort: str = None) -> bool: + """Beendet die WebView2-Helfer, die zu UNSEREM Speicherort gehören. + + True, wenn der Versuch durchlief. Wirft nicht — ein Deinstallieren darf + daran nicht scheitern. + """ + if os.name != "nt": + return False + import subprocess + + ort = speicherort or speicherort_lesen() + if not ort: + return False + from rippy.platform.winlauf import OHNE_FENSTER + + try: + ergebnis = subprocess.run( + ["powershell", "-NoProfile", "-NonInteractive", "-Command", + helfer_skript(ort)], + capture_output=True, text=True, timeout=60, + creationflags=OHNE_FENSTER) + except (OSError, subprocess.SubprocessError): + return False + return ergebnis.returncode == 0 + + +def speicherort_lesen() -> str: + """Der Speicherort OHNE ihn anzulegen — fürs Aufräumen.""" + basis = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~") + return os.path.join(basis, "Rippy", "fenster") + + def oeffnen(adresse: str, titel: str = TITEL, breite: int = BREITE, hoehe: int = HOEHE) -> None: """Öffnet das Fenster und kehrt erst zurück, wenn es geschlossen wird. diff --git a/src/rippy/test_fenster.py b/src/rippy/test_fenster.py index 0d49de0..8fcb797 100644 --- a/src/rippy/test_fenster.py +++ b/src/rippy/test_fenster.py @@ -125,6 +125,48 @@ def test_erfolgreiches_fenster_oeffnet_keinen_browser(monkeypatch): assert gerufen == [] +# ── Die WebView2-Helfer ───────────────────────────────────────────────── +def test_helfer_werden_ueber_den_speicherort_gefiltert(): + """DER Befund vom 28.08.2026: Nach dem Deinstallieren blieben 148 Dateien + liegen, alle unter `fenster\\EBWebView\\`. WebView2 startet eigene + Prozesse (Renderer, GPU, Netz, Crashpad); stirbt Rippy, laufen sie als + Waisen weiter und halten den Zwischenspeicher offen.""" + s = fenster.helfer_skript(r"C:\Users\X\AppData\Local\Rippy\fenster") + assert "msedgewebview2.exe" in s + assert r"C:\Users\X\AppData\Local\Rippy\fenster" in s + assert "Stop-Process" in s + + +def test_der_filter_trifft_NUR_unsere_helfer(): + """`msedgewebview2.exe` benutzen auch andere Programme. Sie alle zu + beenden waere ein Uebergriff — der Nutzer verloere die Fenster fremder + Anwendungen. Ohne `Where-Object` auf den Speicherort passiert genau das.""" + s = fenster.helfer_skript(r"C:\Rippy\fenster") + assert "Where-Object" in s, "ohne Filter wuerden FREMDE Fenster sterben" + assert s.index("Where-Object") < s.index("Stop-Process"), \ + "erst filtern, dann beenden" + + +def test_apostroph_im_pfad_bricht_das_skript_nicht(): + """In PowerShell wird ein Apostroph durch VERDOPPELN maskiert. Ohne das + bricht ein Pfad wie C:\\Users\\O'Brien\\… das Skript mitten entzwei.""" + s = fenster.helfer_skript(r"C:\Users\O'Brien\Rippy\fenster") + assert "O''Brien" in s + assert s.count("'") % 2 == 0 + + +def test_ohne_windows_werden_keine_helfer_beendet(): + if os.name == "nt": + pytest.skip("prueft das Verhalten auf Nicht-Windows") + assert fenster.helfer_beenden("/tmp/x") is False + + +def test_leerer_speicherort_beendet_nichts(monkeypatch): + """Ein leerer Filter wuerde auf ALLE msedgewebview2-Prozesse passen.""" + monkeypatch.setattr(fenster, "speicherort_lesen", lambda: "") + assert fenster.helfer_beenden("") is False + + # ── Der Speicherort ───────────────────────────────────────────────────── def test_speicherort_liegt_bei_rippy(monkeypatch, tmp_path): """Im TEMP waere er bei jedem Aufraeumen weg — und beim Deinstallieren diff --git a/src/rippy/test_windows_app.py b/src/rippy/test_windows_app.py index 6a97e8c..c98057d 100644 --- a/src/rippy/test_windows_app.py +++ b/src/rippy/test_windows_app.py @@ -55,7 +55,16 @@ def test_skript_loescht_sich_selbst(): def test_ordner_ohne_abschliessenden_trenner(): """rmdir will den Ordner OHNE Schluss-Backslash — mit scheitert es.""" s = windows_app.aufraeum_skript(r"C:\R\Rippy.exe", "C:" + BACKSLASH + "R" + BACKSLASH) - assert 'rmdir "' + r"C:\R" + '"' in s + assert 'rmdir /s /q "' + r"C:\R" + '"' in s + + +def test_skript_raeumt_den_ordner_MIT_inhalt(): + """DER Befund vom 28.08.2026: Nach dem Deinstallieren blieben 148 Dateien + liegen (WebView2-Zwischenspeicher, dessen Prozesse Rippy ueberlebt + hatten). Ein blosses `rmdir` scheitert an JEDER verbliebenen Datei und + laesst den ganzen Ordner stehen.""" + s = windows_app.aufraeum_skript(r"C:\R\Rippy.exe", r"C:\R") + assert "rmdir /s /q" in s, "ohne /s /q bleibt jeder nicht leere Ordner stehen" # ── Installation (echt, in einem Testordner) ──────────────────────────── diff --git a/src/rippy/windows_app.py b/src/rippy/windows_app.py index f0507ee..35adea1 100644 --- a/src/rippy/windows_app.py +++ b/src/rippy/windows_app.py @@ -269,25 +269,47 @@ def deinstallieren(still: bool = False) -> None: reg.autostart_entfernen() reg.austragen() + + # ⚠️ ZUERST die WebView2-Helfer beenden (Befund 28.08.2026). + # + # Beim ersten echten Deinstallieren blieben 148 Dateien liegen, alle + # unter `fenster\EBWebView\`. WebView2 startet eigene Prozesse + # (msedgewebview2.exe: Renderer, GPU, Netz, Crashpad); stirbt Rippy, + # laufen sie als Waisen weiter und halten den Zwischenspeicher offen. + # Danach ging `shutil.rmtree` nicht mehr — und der Fehler wurde + # verschluckt, waehrend „Rippy wurde entfernt" gemeldet wurde. + from rippy import fenster as fenster_modul + + fenster_modul.helfer_beenden(os.path.join(ordner, "fenster")) + laeuft_von_hier = os.path.abspath(_eigene_datei()).lower().startswith( os.path.abspath(ordner).lower()) # Die eigene Datei kann sich nicht selbst loeschen, solange sie laeuft. # Also: alles andere jetzt, die EXE per Aufraeum-Befehl danach. + geblieben = [] for name in os.listdir(ordner) if os.path.isdir(ordner) else []: pfad = os.path.join(ordner, name) if laeuft_von_hier and os.path.abspath(pfad).lower() == os.path.abspath(_eigene_datei()).lower(): continue try: shutil.rmtree(pfad) if os.path.isdir(pfad) else os.remove(pfad) - except OSError: - pass + except OSError as e: + # NICHT verschlucken. Ein „Rippy wurde entfernt" ueber einem + # halb geleerten Ordner ist eine Falschaussage. + geblieben.append("%s (%s)" % (name, e.strerror or e)) if laeuft_von_hier: selbst_loeschen(_eigene_datei(), ordner) if not still: - melden("Rippy wurde entfernt.") + if geblieben: + melden("Rippy wurde entfernt — diese Reste waren gesperrt und " + "werden gleich nachgeholt:") + for eintrag in geblieben: + melden(" " + eintrag) + else: + melden("Rippy wurde entfernt.") AUFRAEUM_SKRIPT = """@echo off @@ -299,7 +321,11 @@ for /l %%n in (1,1,15) do ( ) goto ende :weg -rmdir "{ordner}" >nul 2>&1 +rem /s /q, nicht nur rmdir: Ein blosses rmdir scheitert an JEDER +rem verbliebenen Datei. Am 28.08.2026 blieben 148 Dateien im +rem WebView2-Zwischenspeicher liegen, weil deren Prozesse Rippy ueberlebt +rem hatten -- der Ordner blieb dann samt Inhalt stehen. +rmdir /s /q "{ordner}" >nul 2>&1 :ende del /q "%~f0" >nul 2>&1 """