fix(windows): Drueberinstallieren, Linux-Reste im Windows-Betrieb, Ordner-Waehler
Ampel / ampel (push) Successful in 1m27s

Vier Fragen des Commanders vom 29.08.2026, drei davon mit Codefolge.

## 1. „Was passiert wenn man die Setup.exe einfach drueber installiert?"

Bis hierher: nicht zuverlaessig. Rippy startet mit Windows, laeuft also fast
immer — dann ist `Rippy.exe` gesperrt, `shutil.copy2` warf PermissionError,
und die neue Fassung landete als `Rippy.exe.neu` daneben. Dazu die Meldung
„wird beim naechsten Start uebernommen".

**Diese Zusage hat niemand eingeloest.** `.neu` kam im ganzen Projekt genau
einmal vor: an der Stelle, die es schrieb. Wer drueberinstallierte, behielt
still die alte Fassung, und das Setup meldete Erfolg.

Jetzt wird der laufende Rippy vorher beendet (`dienst_beenden` gibt es seit
der Deinstallation und wartet auch die zwei Sekunden ab, die Windows fuer die
Dateihandles braucht). Eine von einer aelteren Setup-Fassung liegengelassene
`.neu` wird dabei uebernommen. Bleibt die Datei DANN noch gesperrt, gibt es
einen klaren Fehler statt einer Zusage — Rippy im Infobereich beenden und das
Setup erneut starten.

Der Tausch laeuft bewusst im SETUP und nicht beim Dienststart: Windows sperrt
eine laufende .exe, und `Rippy.exe` waere genau die zu ersetzende Datei.

## 3. Linux-Reste im Windows-Betrieb (Docker/Headless unveraendert)

**`caps.py`: `os.path.isdir("/app")`.** Damit hielt sich der eigenstaendige
Windows-Rippy fuer einen FREMDEN Worker — und das UI warnte vor fehlender
Pfad-Uebersetzung auf einer Maschine ohne Container und ohne Freigabe.
„Extern" heisst jetzt, was es meint: Rippy laeuft woanders als dieser Worker.

**`caps.py`: `shutil.which("makemkvcon")` + `os.path.ismount(daten_dir)`.**
Beide unter Windows immer falsch (Programme liegen nicht im PATH, ein
normaler Ordner ist kein Mount). Die Schluessel-Auskunft blieb dauerhaft
„unbekannt", obwohl MakeMKV samt Datenverzeichnis da war. Der Mount-Test
bleibt fuer den Container, wo er einen Zweck hat.

**`rohdaten.py`: `/app/temp/raw` und `/app/media` fest.** Dieses Modul findet
die Rohdaten eines Jobs wieder — fuer den Wiederholen-Dialog und fuer
„Rohdaten mitloeschen". Unter Windows fand es NIE etwas: Der Dialog meldete
„keine Rohdaten", das Aufraeumen loeschte nichts, und die Bruchstuecke eines
abgebrochenen Rips blieben liegen (bei 4K-UHD bis 100 GB).

Sieben Tests wurden dabei rot, und zwar zu Recht: Sie pruefen Container-Regeln,
liefen aber unter Windows. Die Wurzeln sind jetzt einspritzbar — beide
Betriebsfaelle auf jedem Rechner pruefbar statt vom laufenden abhaengig.

## 4. „Der Durchsuchen button fehlt. Wie es der Installer auch macht"

Neu: `OrdnerWaehler` — Pfadfeld plus „Durchsuchen …", benutzt fuer Ablage und
Arbeitsverzeichnis. Es waere der DRITTE fest eingebaute Ordner-Browser
geworden (RipTargetModal, StorageMounts); dieser hier ist wiederverwendbar.
`/browse` weiss seit dem 28.08. selbst, in welchem Betrieb es laeuft.

⚠️ Beim Einbau fiel der Import unter den Tisch. `vite` pruefte das NICHT — das
Buendel blieb byte-gleich gross, und zur Laufzeit waere es der naechste leere
Bildschirm gewesen. Aufgefallen nur, weil die erwartete Anzahl Ersetzungen
nicht stimmte. Im Browser gegengeprueft: alle sieben Laufwerke, Navigation in
D:\, keine Konsolenfehler.

843 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-29 14:28:01 +02:00
co-authored by Claude Opus 5
parent 21b1757aa4
commit b2acddbdfa
9 changed files with 440 additions and 39 deletions
+45
View File
@@ -513,3 +513,48 @@ def test_laufender_dienst_wird_nicht_noch_einmal_gestartet(monkeypatch):
monkeypatch.setattr(windows_app, "starte_hintergrund",
lambda p: pytest.fail("es laeuft doch schon einer"))
assert windows_app.bereitstellen(7788) is True
# ── Drueberinstallieren (Befund 29.08.2026) ─────────────────────────────
#
# Commander: „Was passiert wenn man die Setup.exe einfach drüber installiert?
# Geht das, ist es dann die aktuellste version?"
#
# Bis hierher: nicht zuverlaessig. Rippy startet mit Windows, laeuft also fast
# immer — dann ist `Rippy.exe` gesperrt, und die neue Fassung landete als
# `Rippy.exe.neu` daneben, mit der Meldung „wird beim naechsten Start
# uebernommen". **Diese Zusage hat niemand eingeloest**: `.neu` kam im ganzen
# Projekt genau einmal vor, naemlich an der Stelle, die es schrieb.
def test_eine_liegengebliebene_fassung_wird_uebernommen(tmp_path):
from rippy import windows_app
(tmp_path / "Rippy.exe").write_bytes(b"alt")
(tmp_path / "Rippy.exe.neu").write_bytes(b"neu")
assert windows_app.ausstehende_fassung_uebernehmen(str(tmp_path))
assert (tmp_path / "Rippy.exe").read_bytes() == b"neu"
assert not (tmp_path / "Rippy.exe.neu").exists()
def test_ohne_liegengebliebene_fassung_passiert_nichts(tmp_path):
from rippy import windows_app
(tmp_path / "Rippy.exe").write_bytes(b"alt")
assert windows_app.ausstehende_fassung_uebernehmen(str(tmp_path)) == ""
assert (tmp_path / "Rippy.exe").read_bytes() == b"alt"
def test_ein_fehlschlag_beim_tausch_wirft_nicht(tmp_path):
"""Rippy laeuft dann mit der alten Fassung weiter — besser als gar nicht."""
from rippy import windows_app
(tmp_path / "Rippy.exe").write_bytes(b"alt")
(tmp_path / "Rippy.exe.neu").write_bytes(b"neu")
def gesperrt(a, b):
raise OSError(32, "in Benutzung")
assert windows_app.ausstehende_fassung_uebernehmen(str(tmp_path), gesperrt) == ""
assert (tmp_path / "Rippy.exe").read_bytes() == b"alt"
+75 -8
View File
@@ -127,17 +127,44 @@ def installieren(ziel: str = None, autostart: bool = True,
quelle = _eigene_datei()
programm = os.path.join(ziel, "Rippy.exe")
# Sich selbst kopieren. Läuft gerade eine ältere Fassung, ist die Datei
# gesperrt — dann daneben legen und beim nächsten Start tauschen.
# ⚠️ ERST den laufenden Rippy beenden (Befund 29.08.2026).
#
# Commander: „Was passiert wenn man die Setup.exe einfach drüber
# installiert? Geht das, ist es dann die aktuellste version?"
#
# Bis hierher: NEIN, nicht zuverlässig. Rippy startet mit Windows, läuft
# also fast immer — dann ist `Rippy.exe` gesperrt, `shutil.copy2` warf
# `PermissionError`, und die neue Fassung landete als `Rippy.exe.neu`
# daneben. Dazu die Meldung „wird beim nächsten Start übernommen".
#
# **Diese Zusage hat niemand eingelöst.** `Rippy.exe.neu` kam im ganzen
# Projekt genau einmal vor: hier. Wer drüberinstallierte, behielt still
# die alte Fassung — und das Setup meldete Erfolg.
#
# Jetzt wird der laufende Rippy vorher beendet. `dienst_beenden` gibt es
# seit der Deinstallation; es wartet auch die zwei Sekunden ab, die
# Windows braucht, um die Dateihandles freizugeben.
if os.path.isfile(programm) and \
os.path.abspath(quelle).lower() != os.path.abspath(programm).lower():
if dienst_beenden(ziel):
melden(" Laufender Rippy beendet — die Dateien sind jetzt frei.")
# Und aufräumen, was eine ÄLTERE Setup-Fassung liegen ließ.
ausstehende_fassung_uebernehmen(ziel)
# Sich selbst kopieren.
if os.path.abspath(quelle).lower() != os.path.abspath(programm).lower():
try:
shutil.copy2(quelle, programm)
except PermissionError:
ersatz = programm + ".neu"
shutil.copy2(quelle, ersatz)
melden("Rippy läuft gerade — die neue Fassung wird beim nächsten "
"Start übernommen.")
programm = ersatz
except PermissionError as e:
# Ehrlich scheitern statt eine Zusage zu geben, die niemand
# einlöst. Hier lag bis zum 29.08.2026 der `.neu`-Rückfall — mit
# der Meldung wird beim nächsten Start übernommen" und ohne
# jeden Code, der das getan hätte.
raise RuntimeError(
"Rippy.exe liess sich nicht ersetzen: %s\n"
"Rippy laeuft noch. Bitte im Infobereich rechts unten "
"beenden (Rechtsklick auf das Rippy-Symbol) und das Setup "
"erneut starten." % e) from e
icon = _icon_pfad()
if icon and os.path.isfile(icon):
@@ -587,6 +614,46 @@ class Dienst:
return False
def ausstehende_fassung_uebernehmen(ordner: str = None, ersetzen=None) -> str:
"""Eine beim Update danebengelegte `Rippy.exe.neu` jetzt übernehmen.
Gibt den übernommenen Pfad zurück, sonst "".
## Die uneingelöste Zusage (Befund 29.08.2026)
Beim Drüberinstallieren schrieb `installieren()` bei gesperrter Datei eine
`Rippy.exe.neu` daneben und meldete: „wird beim nächsten Start
übernommen." **Diese Zeile war das einzige Vorkommen von `.neu` im ganzen
Projekt** — es gab niemanden, der sie übernommen hätte. Wer über eine
laufende Installation installierte, behielt still die alte Fassung, und
das Setup sagte „fertig".
## Warum das hier läuft und NICHT beim Dienststart
Der naheliegende Ort wäre der Start von Rippy selbst. Er funktioniert
nicht: Windows sperrt eine laufende `.exe`, und `Rippy.exe` ist genau die
Datei, die ersetzt werden müsste. `os.replace` scheitert dort immer.
Das Setup ist ein ANDERES Programm (`RippySetup.exe`) und hat den
laufenden Rippy vorher beendet — hier ist die Datei frei. Aufgeräumt wird
damit auch, was eine ältere Setup-Fassung liegen gelassen hat.
"""
ordner = ordner or os.path.dirname(_eigene_datei())
ziel = os.path.join(ordner, "Rippy.exe")
neu = ziel + ".neu"
if not os.path.isfile(neu):
return ""
try:
(ersetzen or os.replace)(neu, ziel)
except OSError as e:
# Nicht werfen: Rippy laeuft mit der alten Fassung weiter, das ist
# besser als gar nicht zu starten. Gesagt wird es trotzdem.
melden(" Neue Fassung konnte nicht uebernommen werden (%s)." % e)
return ""
melden(" Neue Fassung uebernommen: %s" % ziel)
return ziel
def _reste_wegraeumen() -> None:
"""Zurueckgelassene Auspack-Ordner entfernen. Darf nie stoeren."""
try: