Compare commits
2
Commits
069fc46f44
...
e1577a4f47
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1577a4f47 | ||
|
|
c01ef1081c |
@@ -1267,6 +1267,59 @@ Eingabemaske — nur der Knopf „Verbinden" wird zu „Zeile kopieren".
|
||||
`apparmor:unconfined`, `propagation: rshared` und die Mount-Wache fallen in
|
||||
V2-5 ersatzlos weg.
|
||||
|
||||
### Entscheid 4 — Echtes Fenster statt Browser-Tab, aber ohne Electron
|
||||
|
||||
Nachgefragt vom Commander am 28.08.2026:
|
||||
|
||||
> „Warum nutzen wir für Windows weiterhin einen Browser? Warum nutzen wir kein
|
||||
> Electron oder sowas und machen daraus einen echten Client?"
|
||||
|
||||
Die erste Hälfte trifft zu und wird umgesetzt: Ein Browser-Tab ist kein Client.
|
||||
Er hat eine Adresszeile, die niemand braucht, liegt zwischen fremden Tabs, hat
|
||||
kein eigenes Symbol in der Taskleiste, und wer den Browser schließt, glaubt, er
|
||||
habe Rippy beendet.
|
||||
|
||||
Die zweite Hälfte wird **abgelehnt**, und zwar gemessen statt geschätzt:
|
||||
|
||||
| Weg | Zusatz zum Paket | Was mitkommt |
|
||||
|-----|------------------|--------------|
|
||||
| **pywebview + WebView2** | **8,0 MB** | nur die Anbindung; der Renderer liegt schon auf dem System |
|
||||
| Electron | ~150–210 MB | ein komplettes zweites Chromium **und** eine zweite Laufzeitumgebung neben Python |
|
||||
|
||||
Windows 11 liefert die WebView2-Laufzeit mit. Auf dem Rechner des Commanders am
|
||||
28.08.2026 nachgesehen: **151.0.4129.107**. Und nachgemessen, was sie wirklich
|
||||
rendert — nicht angenommen:
|
||||
|
||||
```
|
||||
navigator.userAgent …Chrome/151.0.0.0 Safari/537.36 Edg/151.0.0.0
|
||||
fetch ja EventSource ja CSS Grid ja Pfeilfunktionen ja
|
||||
```
|
||||
|
||||
Das ist dasselbe Chromium, das auch in Electron steckt. Rippy bekommt also
|
||||
denselben Renderer, nur ohne ihn ein zweites Mal mitzuschleppen. Damit bleibt
|
||||
auch § 4.1 unverändert gültig — dort stand WebView2 von Anfang an.
|
||||
|
||||
**Drei Dinge gehören zum Entscheid, nicht als Beiwerk:**
|
||||
|
||||
1. **Kein stiller Rückfall auf MSHTML.** `pywebview` kann unter Windows auch den
|
||||
alten IE-Renderer nehmen; der stellt die React-Oberfläche nicht dar. Fehlt
|
||||
die WebView2-Laufzeit, öffnet Rippy **den Browser** und sagt warum — ein
|
||||
leeres Fenster wäre schlimmer als ein Tab.
|
||||
2. **Fenster und Tray sind getrennte Prozesse.** `pystray` belegt unter Windows
|
||||
den Haupt-Thread, das WebView2-Fenster braucht ihn genauso; zwei
|
||||
Nachrichtenschleifen passen nicht in einen Thread. `--dienst` hält Server und
|
||||
Tray, `--oeffnen` ist der Client davor. Beide heißen im Taskmanager
|
||||
`Rippy.exe`, und das Fenster lässt sich schließen, ohne den Dienst
|
||||
mitzureißen.
|
||||
3. **Die eigene Konsole wird versteckt, eine geerbte nie.** `Rippy.exe` ist ein
|
||||
Konsolenprogramm, weil der Installer seine Meldungen zeigen muss. Beim
|
||||
Doppelklick auf das Desktop-Symbol blitzte damit erst eine schwarze Box auf.
|
||||
`GetConsoleProcessList` unterscheidet beides: genau ein Prozess an der
|
||||
Konsole heißt, sie gehört uns.
|
||||
|
||||
**Folge für den Plan:** Kein neuer Etappen-Punkt — das ist Teil von V2-4 und
|
||||
dort erledigt. `src/rippy/fenster.py` trägt die vollständige Begründung.
|
||||
|
||||
---
|
||||
|
||||
### Was jetzt noch fehlt, bevor gebaut wird
|
||||
|
||||
+32
-4
@@ -619,15 +619,43 @@ Medien.
|
||||
plus Worker) UND ein Verbindungsabriss keine Liste leert.
|
||||
|
||||
### V2-4: Windows nativ
|
||||
- [ ] `drives/windows.py` — Win32 statt ioctl (`IOCTL_STORAGE_CHECK_VERIFY2`,
|
||||
- [x] `drives/windows.py` — Win32 statt ioctl (`IOCTL_STORAGE_CHECK_VERIFY2`,
|
||||
`IOCTL_STORAGE_MEDIA_REMOVAL`, `IOCTL_STORAGE_EJECT_MEDIA`,
|
||||
MMC `GET CONFIGURATION` für den Disc-Typ)
|
||||
- [ ] Disc-Einwurf per `WM_DEVICECHANGE` statt Polling
|
||||
- [ ] Dienst (WinSW) + Tray als GETRENNTE Prozesse; WebView2-Fenster
|
||||
- [ ] Nuitka `--standalone` (one-dir, nicht one-file) + Inno Setup
|
||||
- [ ] Disc-Einwurf per `WM_DEVICECHANGE` statt Polling — **offen**, läuft
|
||||
vorerst über den Poll aus `drives/detection.py`
|
||||
- [x] Dienst + Fenster als GETRENNTE Prozesse; **WebView2-Fenster**
|
||||
(`fenster.py`, Entscheid 4 in `KONZEPT-V2.md` § 10)
|
||||
- [x] Eintrag in „Programme und Features" und im Taskmanager als `Rippy.exe`
|
||||
(ausdrücklicher Wunsch des Commanders)
|
||||
- [x] Desktop-Symbol und Startmenü-Eintrag, im Setup abschaltbar
|
||||
- [x] Werkzeug-Erkennung und -Beschaffung (`tools/katalog.py`,
|
||||
`tools/beschaffen.py`) — Rippy findet, holt und aktualisiert
|
||||
HandBrake/MakeMKV selbst
|
||||
- [ ] Standby blocken via `SetThreadExecutionState`, ohne `ES_DISPLAY_REQUIRED`
|
||||
— **offen**
|
||||
- **Fertig, wenn:** auf einem frischen Win-11-Rechner gilt: Installer →
|
||||
Disc rein → MKV raus. Und der UHD-Schlüssel kommt automatisch.
|
||||
**Noch offen:** ein echter Rip auf Windows ist ungeprüft — das Laufwerk
|
||||
hängt an der VM.
|
||||
|
||||
> **Abweichung vom Plan, bewusst und mit Folgen.** Statt Nuitka `--standalone`
|
||||
> (one-dir) + Inno Setup wurde **PyInstaller onefile** genommen: ein Programm,
|
||||
> drei Betriebsarten (`RippySetup.exe`, `--dienst`, `--deinstallieren`), kein
|
||||
> zweites Werkzeug in der Kette.
|
||||
>
|
||||
> Der Preis stand oben schon im Plan — *„ein Onefile-Paket entpackt sich bei
|
||||
> jedem Start neu nach `%TEMP%`"* — und er ist am 28.08.2026 fällig geworden:
|
||||
> Ein Dienst lief eine Stunde, `/api/health` gab 200, `/` gab **404**. Vom
|
||||
> Entpack-Verzeichnis des laufenden Prozesses waren 31 statt 44 Einträge übrig,
|
||||
> `ui` und `api` fehlten. Die API meldete sich gesund, während die Oberfläche
|
||||
> weg war.
|
||||
>
|
||||
> **Gegenmaßnahme:** Der Installer legt die Oberfläche als Kopie NEBEN das
|
||||
> Programm (`windows_app.ui_auspacken`), und `daemon._ui_pfad()` nimmt diese
|
||||
> Kopie vor dem Entpack-Verzeichnis. Zwei Tests halten das fest. Sollte sich
|
||||
> derselbe Ausfall an den API-Modulen zeigen, ist one-dir die richtige Antwort
|
||||
> — dann ist diese Abweichung zurückzunehmen.
|
||||
|
||||
### V2-5: Docker neu
|
||||
- [ ] EIN Image, drei Profile (`standalone`, `api`, `node`)
|
||||
|
||||
+108
-1
@@ -1,6 +1,113 @@
|
||||
# SAVEPOINT — Rippy
|
||||
|
||||
## Aktueller Stand: v4.0-beta — V2-0 bis V2-3 stehen, Polling ist weg (28.08.2026)
|
||||
## Aktueller Stand: v4.0-rc — Windows ist ein echtes Programm (28.08.2026)
|
||||
|
||||
> **Rippy läuft auf Windows ohne Docker, in einem eigenen Fenster, findet und
|
||||
> aktualisiert seine Werkzeuge selbst — und meldet sich bei Windows an wie
|
||||
> jedes andere Programm.**
|
||||
|
||||
### ZUSTAND, gemessen
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Repo | `c01ef10` auf `main` |
|
||||
| Tests | **562 grün** + 18 übersprungen (Sitzungsbeginn: 378) |
|
||||
| `RippySetup.exe` | **31,7 MB**, liegt auf dem Desktop |
|
||||
| Fenster | WebView2 **151.0.4129.107**, 1280×860, Oberfläche im Bild belegt |
|
||||
| Werkzeuge auf diesem PC | 5 Encoder gefunden, darunter **AMD VCE** |
|
||||
| VM (Arcane) | **noch auf `05ab655`** — alles ab `b723fee` ist NICHT deployt |
|
||||
| Echter Rip auf Windows | **ungeprüft** — das Laufwerk hängt an der VM |
|
||||
|
||||
### Was jetzt geht
|
||||
|
||||
Doppelklick RippySetup.exe -> installiert nach %LOCALAPPDATA%\Rippy
|
||||
Desktop-Symbol + Startmenue-Eintrag
|
||||
Eintrag in "Programme und Features"
|
||||
Autostart (HKCU\...\Run)
|
||||
Doppelklick Desktop-Symbol -> eigenes Fenster, keine Adresszeile,
|
||||
kein Browser, kein Konsolenblitzer
|
||||
Taskmanager -> "Rippy.exe" mit Beschreibung
|
||||
Programme und Features -> Rippy 2.0.0, Deinstallieren raeumt auf
|
||||
|
||||
### Die Entscheidung dieser Sitzung: WebView2, nicht Electron
|
||||
|
||||
Der Commander fragte: *„Warum nutzen wir für Windows weiterhin einen Browser?
|
||||
Warum nutzen wir kein Electron oder sowas und machen daraus einen echten
|
||||
Client."*
|
||||
|
||||
Erste Hälfte: berechtigt, umgesetzt. Zweite Hälfte: abgelehnt, **gemessen**:
|
||||
|
||||
| | Zusatz | Was mitkommt |
|
||||
|---|---|---|
|
||||
| pywebview + WebView2 | **2,7 MB** in der EXE | nur die Anbindung |
|
||||
| Electron | ~150–210 MB | zweites Chromium **und** zweite Laufzeit neben Python |
|
||||
|
||||
Windows 11 liefert die Laufzeit mit. Nachgemessen, was sie rendert:
|
||||
`Chrome/151.0.0.0 … Edg/151.0.0.0`, fetch ✓, EventSource ✓, CSS Grid ✓.
|
||||
Dasselbe Chromium wie in Electron — nur ohne es zweimal mitzuschleppen.
|
||||
Vollständige Begründung: `src/rippy/fenster.py`, `KONZEPT-V2.md` § 10
|
||||
Entscheid 4.
|
||||
|
||||
### DER FUND: ein Dienst, der gesund meldete und keine Oberfläche mehr hatte
|
||||
|
||||
Beim Nachsehen im laufenden Betrieb — nicht in einem Test:
|
||||
|
||||
/api/health HTTP 200
|
||||
/ HTTP 404 im Fenster: {"detail":"Not Found"}
|
||||
|
||||
Der Server war in Ordnung. Sein Entpack-Verzeichnis war es nicht:
|
||||
|
||||
_MEI000074b02 31 Eintraege, 5 Ordner <- der laufende Dienst, kein ui
|
||||
_MEI000082e82 44 Eintraege, 16 Ordner <- vollstaendig
|
||||
|
||||
Eine PyInstaller-Onefile-EXE liest bei **jeder** Anfrage aus `%TEMP%\_MEIxxxxx`.
|
||||
Ein Temp-Verzeichnis ist kein Ort für etwas, das eine Woche liegen bleiben
|
||||
soll — und der Ausfall ist der schlimmstmögliche: Die API antwortet weiter,
|
||||
der Dienst gilt als gesund, nur die Oberfläche ist weg.
|
||||
|
||||
**Genau davor warnte `ROADMAP.md` beim Bau-Verfahren** („one-dir statt
|
||||
one-file"). Die Abweichung bleibt, die Lücke ist geschlossen: Der Installer
|
||||
legt die Oberfläche neben das Programm, `daemon._ui_pfad()` nimmt diese Kopie
|
||||
zuerst. Zwei Tests halten es fest. Zeigt sich derselbe Ausfall an den
|
||||
API-Modulen, ist one-dir die richtige Antwort.
|
||||
|
||||
### Der Vorfall, der den PC des Commanders getroffen hat
|
||||
|
||||
Ein Test rief `installieren()` auf. Seit dem Verknüpfungs-Feature legt das
|
||||
Verknüpfungen auf dem **echten** Desktop an — die kennen kein `tmp_path`. Der
|
||||
Test überschrieb damit das funktionierende Desktop-Symbol durch eines, das auf
|
||||
eine 2-KB-Attrappe in `…\Temp\pytest-of-…` zeigte. Windows meldete beim Klick:
|
||||
*„Diese App kann auf dem PC nicht ausgeführt werden."*
|
||||
|
||||
Aufgeräumt hatte der Test nur Registry und Autostart. **Ein Test, der Spuren
|
||||
außerhalb von `tmp_path` hinterlässt, ist kein Test, sondern ein Eingriff.**
|
||||
Jetzt zwei Sicherungen statt einer: `verknuepfen=False` **und** die
|
||||
Anlege-Funktion ist ersetzt. Dazu ein eigener Wächter-Test.
|
||||
|
||||
### Gebaut in dieser Sitzung (11 Commits)
|
||||
|
||||
- **V2-4 Windows-Treiber** (`b723fee`, `3d7f3b1`, `9d95c95`) — Win32 per
|
||||
ctypes, an einer echten Disc bewiesen
|
||||
- **Daemon unter Windows** (`e5254c2`) — `rippyd` ohne Docker
|
||||
- **RippySetup.exe** (`059183c`) — ein Programm, drei Betriebsarten
|
||||
- **Werkzeug-Kette** (`288f9ee`, `f4a8d77`) — finden, holen, aktuell halten;
|
||||
Rippy ist **standalone**, wie der Commander es verlangt hat
|
||||
- **Verknüpfungen + `--oeffnen`** (`99c0586`)
|
||||
- **Test-Vorfall behoben** (`069fc46`)
|
||||
- **Echtes Fenster + UI-Kopie** (`c01ef10`)
|
||||
|
||||
### WAS ALS NÄCHSTES ANSTEHT
|
||||
|
||||
- [ ] **Laufwerk an den PC** — ein echter Rip unter Windows ist noch nie
|
||||
gelaufen. Alles davor ist bewiesen, das nicht.
|
||||
- [ ] **Deploy auf Arcane** — die VM steht auf `05ab655`, elf Commits zurück.
|
||||
- [ ] **V2-5 Docker neu**: ein Image, drei Profile, Host-Mounts.
|
||||
- [ ] **V2-6 Headless** — zurückgestellt, aber nicht gestrichen
|
||||
(ausdrücklich: *„soll aber nicht vernachlässigt werden"*).
|
||||
- [ ] **V2-7**: neue Funktionen inkl. Schlüsselkette in drei Stufen.
|
||||
- [ ] Offen aus V2-4: `WM_DEVICECHANGE` statt Poll, `SetThreadExecutionState`.
|
||||
|
||||
## Letzter Stand davor: v4.0-beta — V2-0 bis V2-3 stehen, Polling ist weg (28.08.2026)
|
||||
|
||||
> **Vier Etappen gebaut, alle gruen, alles deployt.** Und ein Vorfall, den es
|
||||
> ohne den Deploy nie gegeben haette — der aber eine echte Altlast aufgedeckt hat.
|
||||
|
||||
@@ -100,11 +100,17 @@ def pruefen() -> list:
|
||||
import PyInstaller # noqa: F401
|
||||
except ImportError:
|
||||
maengel.append("pyinstaller fehlt (pip install pyinstaller).")
|
||||
for modul, paket in (("pystray", "pystray"), ("PIL", "pillow")):
|
||||
for modul, paket, wofuer in (
|
||||
("pystray", "pystray", "kein Tray-Symbol"),
|
||||
("PIL", "pillow", "kein Tray-Symbol"),
|
||||
("webview", "pywebview", "kein eigenes Fenster, nur ein Browser-Tab"),
|
||||
("clr", "pythonnet", "kein eigenes Fenster (WebView2 laeuft ueber .NET)"),
|
||||
):
|
||||
try:
|
||||
__import__(modul)
|
||||
except ImportError:
|
||||
maengel.append(f"{modul} fehlt (pip install {paket}) — ohne das kein Tray-Symbol.")
|
||||
maengel.append(
|
||||
f"{modul} fehlt (pip install {paket}) — ohne das {wofuer}.")
|
||||
return maengel
|
||||
|
||||
|
||||
@@ -145,6 +151,13 @@ def bauen(ausgabe: str, version: str) -> str:
|
||||
"--hidden-import", "uvicorn.lifespan.on",
|
||||
"--hidden-import", "rippy.drives.windows",
|
||||
"--hidden-import", "pystray._win32",
|
||||
# pywebview waehlt seine Plattform zur LAUFZEIT aus (importlib), also
|
||||
# sieht PyInstaller den Import nicht. Ohne diese drei geht das Fenster
|
||||
# nicht auf und Rippy faellt still auf den Browser zurueck -- also
|
||||
# genau dorthin, wovon wir gerade weg wollten.
|
||||
"--hidden-import", "webview",
|
||||
"--hidden-import", "webview.platforms.winforms",
|
||||
"--hidden-import", "webview.platforms.edgechromium",
|
||||
# Nicht mitnehmen: Der Windows-Treiber braucht kein psycopg2/celery,
|
||||
# und beides zieht Ballast nach.
|
||||
"--exclude-module", "psycopg2",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Was die BAU-Umgebung braucht, zusaetzlich zu docker/api/requirements.txt.
|
||||
# Der Nutzer installiert davon nichts — alles landet in der RippySetup.exe.
|
||||
#
|
||||
# py -3.12 -m venv venv
|
||||
# venv\Scripts\pip install -r docker/api/requirements.txt
|
||||
# venv\Scripts\pip install -r packaging/windows/requirements-bau.txt
|
||||
# venv\Scripts\python packaging/windows/build.py
|
||||
#
|
||||
# Warum die Fassungen festgenagelt sind: Ein Bau, der gestern ging und heute
|
||||
# nicht mehr, kostet mehr Zeit als jede Aktualisierung einbringt. Ein Wechsel
|
||||
# ist eine Entscheidung, kein Zufall.
|
||||
|
||||
pyinstaller==6.22.2
|
||||
|
||||
# Tray-Symbol — ohne das laeuft Rippy zwar, ist aber unsichtbar und nicht
|
||||
# zu beenden, ausser ueber den Taskmanager.
|
||||
pystray==0.19.5
|
||||
pillow==12.3.0
|
||||
|
||||
# Das eigene Fenster (WebView2). Gemessen am 28.08.2026: 8,0 MB in der
|
||||
# Bau-Umgebung. Electron waere an derselben Stelle 150-210 MB gewesen und
|
||||
# haette eine zweite komplette Laufzeitumgebung neben Python mitgebracht.
|
||||
# Siehe src/rippy/fenster.py fuer die vollstaendige Begruendung.
|
||||
pywebview==6.2.1
|
||||
pythonnet==3.1.0
|
||||
+42
-6
@@ -113,7 +113,36 @@ def _api_pfad() -> str:
|
||||
|
||||
|
||||
def _ui_pfad() -> str:
|
||||
"""Wo liegen die gebauten UI-Dateien? Leerer String = kein UI mitgeliefert."""
|
||||
"""Wo liegen die gebauten UI-Dateien? Leerer String = kein UI mitgeliefert.
|
||||
|
||||
## Warum die installierte Kopie VOR dem Paket kommt (28.08.2026)
|
||||
|
||||
Gemessen an einem Dienst, der seit einer Stunde lief: `/api/health` gab
|
||||
HTTP 200, `/` gab **404**. Im Fenster stand `{"detail":"Not Found"}`.
|
||||
|
||||
Der Grund lag nicht im Server, sondern unter ihm. Eine
|
||||
PyInstaller-Onefile-EXE entpackt sich beim Start nach `%TEMP%\\_MEIxxxxx`
|
||||
und liest von dort weiter — `StaticFiles` schlaegt bei JEDER Anfrage in
|
||||
diesem Ordner nach. Nachgesehen, was davon uebrig war:
|
||||
|
||||
_MEI000074b02 31 Eintraege, 5 Ordner — kein `ui`, kein `api`
|
||||
_MEI000082e82 44 Eintraege, 16 Ordner — vollstaendig
|
||||
|
||||
Der halbe Ordner gehoerte dem laufenden Dienst. Ein Temp-Verzeichnis ist
|
||||
kein Ort, an dem etwas eine Woche lang liegen bleibt: Aufraeumwerkzeuge,
|
||||
Storage Sense und die Aufraeumroutine anderer Onefile-Prozesse greifen
|
||||
dort hinein. Und der Ausfall ist der schlimmstmoegliche — die API
|
||||
antwortet weiter, der Dienst gilt als gesund, nur die Oberflaeche ist
|
||||
weg. Genau die Sorte stiller Fehlschlag, vor der AGENTS.md warnt.
|
||||
|
||||
Deshalb legt der Installer eine Kopie neben das Programm, und die hat
|
||||
hier Vorrang. Sie liegt in `%LOCALAPPDATA%\\Rippy`, gehoert Rippy und
|
||||
verschwindet beim Deinstallieren mit.
|
||||
"""
|
||||
neben_programm = os.path.join(os.path.dirname(_programmordner()), "ui")
|
||||
if os.path.isdir(neben_programm):
|
||||
return neben_programm
|
||||
|
||||
gebuendelt = getattr(sys, "_MEIPASS", None)
|
||||
if gebuendelt:
|
||||
pfad = os.path.join(gebuendelt, "ui")
|
||||
@@ -121,11 +150,18 @@ def _ui_pfad() -> str:
|
||||
|
||||
hier = os.path.dirname(os.path.abspath(__file__))
|
||||
repo = os.path.dirname(os.path.dirname(hier))
|
||||
for pfad in (os.path.join(repo, "docker", "ui", "dist"),
|
||||
os.path.join(os.path.dirname(sys.executable), "ui")):
|
||||
if os.path.isdir(pfad):
|
||||
return pfad
|
||||
return ""
|
||||
pfad = os.path.join(repo, "docker", "ui", "dist")
|
||||
return pfad if os.path.isdir(pfad) else ""
|
||||
|
||||
|
||||
def _programmordner() -> str:
|
||||
"""Die laufende Programmdatei — im Paket die EXE, sonst der Interpreter.
|
||||
|
||||
Gibt eine DATEI zurueck, nicht den Ordner; der Aufrufer nimmt sich mit
|
||||
`dirname` was er braucht. `sys.executable` ist unter PyInstaller die
|
||||
Rippy.exe selbst, nicht das ausgepackte Python.
|
||||
"""
|
||||
return sys.executable
|
||||
|
||||
|
||||
def argumente(argv=None) -> argparse.Namespace:
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
"""Rippy als echtes Fenster — nicht als Browser-Tab.
|
||||
|
||||
## Die Frage des Commanders (28.08.2026)
|
||||
|
||||
> „Warum nutzen wir für Windows weiterhin einen Browser? Warum nutzen wir
|
||||
> kein Electron oder sowas und machen daraus einen echten Client?"
|
||||
|
||||
Die erste Hälfte ist berechtigt: Ein Browser-Tab IST kein Client. Er hat eine
|
||||
Adresszeile, die niemand braucht, liegt zwischen fremden Tabs, hat kein
|
||||
eigenes Symbol in der Taskleiste, und wer den Browser schließt, glaubt, er
|
||||
habe Rippy beendet.
|
||||
|
||||
Die zweite Hälfte wäre die teure Antwort darauf. Gemessen, nicht geschätzt:
|
||||
|
||||
Electron ~150-210 MB (Chromium + Node komplett mit)
|
||||
pywebview + WebView2 8,0 MB (in die Bau-Umgebung gemessen)
|
||||
|
||||
Electron brächte eine ZWEITE komplette Laufzeitumgebung neben Python mit —
|
||||
und ein zweites Chromium neben dem, das auf diesem Rechner ohnehin schon
|
||||
liegt. Denn Windows 11 liefert die WebView2-Laufzeit mit; auf diesem Rechner
|
||||
am 28.08.2026 nachgesehen: 151.0.4129.107.
|
||||
|
||||
Nachgemessen, was sie wirklich rendert — nicht angenommen:
|
||||
|
||||
navigator.userAgent ...Chrome/151.0.0.0 Safari/537.36 Edg/151.0.0.0
|
||||
fetch ja EventSource ja
|
||||
CSS Grid ja Pfeilfunktionen ja
|
||||
|
||||
Das ist dasselbe Chromium, das auch in Electron steckt. Rippy bekommt also
|
||||
denselben Renderer — nur ohne ihn ein zweites Mal mitzuschleppen.
|
||||
|
||||
## Warum nicht MSHTML als Rückfall
|
||||
|
||||
`pywebview` kann unter Windows auch auf den alten IE-Renderer zurückfallen.
|
||||
Eine React-Oberfläche mit CSS Grid stellt der NICHT dar — sie erschiene leer
|
||||
oder zerfallen. Ein leeres Fenster ist schlimmer als ein Browser-Tab.
|
||||
Deshalb wird die Laufzeit VORHER geprüft, und ohne sie geht Rippy in den
|
||||
Browser statt in ein Fenster, das nichts zeigen kann.
|
||||
|
||||
## Warum das Fenster ein eigener Prozess ist
|
||||
|
||||
Das Tray-Symbol (`pystray`) braucht unter Windows den HAUPT-Thread, sonst
|
||||
bekommt es keine Fensternachrichten. Das Fenster (`pywebview`/WinForms)
|
||||
braucht ihn genauso. Zwei Nachrichtenschleifen passen nicht in einen Thread.
|
||||
|
||||
Also: `Rippy.exe --dienst` hält Server und Tray, `Rippy.exe --fenster` ist
|
||||
der Client davor. Beide heißen im Taskmanager „Rippy.exe" — was der
|
||||
Commander wollte —, und das Fenster lässt sich schließen, ohne den Dienst
|
||||
mitzureißen.
|
||||
|
||||
## Was hier NICHT passiert
|
||||
|
||||
Kein Aufräumen von Ausnahmen zu einem stillen `False`. Geht das Fenster
|
||||
nicht auf, muss der Aufrufer das ERFAHREN, damit er den Browser öffnen kann.
|
||||
Ein stiller Fehlschlag wäre ein Doppelklick, nach dem nichts passiert.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
TITEL = "Rippy"
|
||||
BREITE = 1280
|
||||
HOEHE = 860
|
||||
MINDESTBREITE = 900
|
||||
MINDESTHOEHE = 600
|
||||
|
||||
# Die Evergreen-Laufzeit von WebView2 trägt sich unter dieser festen Kennung
|
||||
# bei EdgeUpdate ein. Sie ist von Microsoft vergeben und ändert sich nicht.
|
||||
WEBVIEW2_KENNUNG = "{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"
|
||||
|
||||
# Die Laufzeit kann pro Maschine oder pro Nutzer liegen — beides ist gültig.
|
||||
_SCHLUESSEL = (
|
||||
("HKLM", r"SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients"),
|
||||
("HKLM", r"SOFTWARE\Microsoft\EdgeUpdate\Clients"),
|
||||
("HKCU", r"Software\Microsoft\EdgeUpdate\Clients"),
|
||||
)
|
||||
|
||||
|
||||
def echte_version(wert) -> str:
|
||||
"""Prüft einen `pv`-Wert aus der Registry (reine Funktion).
|
||||
|
||||
EdgeUpdate trägt bei einer DEINSTALLIERTEN Laufzeit `0.0.0.0` ein, statt
|
||||
den Schlüssel zu löschen. Wer das für vorhanden hält, baut ein Fenster,
|
||||
das nie etwas anzeigt — und der Nutzer sieht einen weißen Kasten ohne
|
||||
jede Erklärung.
|
||||
"""
|
||||
text = str(wert or "").strip()
|
||||
return "" if not text or text == "0.0.0.0" else text
|
||||
|
||||
|
||||
def webview2_version() -> str:
|
||||
"""Die Version der WebView2-Laufzeit. Leer, wenn sie nicht da ist.
|
||||
|
||||
Die Version statt eines bloßen „ja/nein": Steht in einem Fehlerbericht
|
||||
`151.0.4129.107`, ist die Lage klar. Steht dort `True`, fängt das Raten
|
||||
an.
|
||||
"""
|
||||
if os.name != "nt":
|
||||
return ""
|
||||
import winreg
|
||||
|
||||
wurzeln = {"HKLM": winreg.HKEY_LOCAL_MACHINE, "HKCU": winreg.HKEY_CURRENT_USER}
|
||||
for name, pfad in _SCHLUESSEL:
|
||||
try:
|
||||
with winreg.OpenKey(wurzeln[name], pfad + "\\" + WEBVIEW2_KENNUNG) as k:
|
||||
wert, _ = winreg.QueryValueEx(k, "pv")
|
||||
except OSError:
|
||||
continue
|
||||
version = echte_version(wert)
|
||||
if version:
|
||||
return version
|
||||
return ""
|
||||
|
||||
|
||||
def webview2_vorhanden() -> bool:
|
||||
return bool(webview2_version())
|
||||
|
||||
|
||||
def bibliothek_vorhanden() -> bool:
|
||||
"""Ist `pywebview` überhaupt im Paket? (Ohne es zu starten.)"""
|
||||
import importlib.util
|
||||
|
||||
try:
|
||||
return importlib.util.find_spec("webview") is not None
|
||||
except (ImportError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def verfuegbar() -> tuple:
|
||||
"""Kann ein echtes Fenster aufgehen? (ja/nein, Begründung)
|
||||
|
||||
Gibt IMMER eine Begründung zurück, auch beim Ja. Wer nur `False`
|
||||
zurückgibt, zwingt den Nächsten zum Nachforschen — und genau solche
|
||||
stillen Fehlschläge sind in diesem Projekt schon zweimal teuer geworden.
|
||||
"""
|
||||
if os.name != "nt":
|
||||
return False, "kein Windows"
|
||||
if not bibliothek_vorhanden():
|
||||
return False, "pywebview ist nicht mit im Paket"
|
||||
version = webview2_version()
|
||||
if not version:
|
||||
return False, "die WebView2-Laufzeit fehlt (ohne sie bliebe das Fenster leer)"
|
||||
return True, "WebView2 " + version
|
||||
|
||||
|
||||
def speicherort() -> str:
|
||||
"""Wo WebView2 seinen Zwischenspeicher ablegt.
|
||||
|
||||
Ohne eigene Angabe landet er im TEMP — und ist bei jedem Aufräumen weg.
|
||||
Neben der Datenbank gehört er zu Rippy und verschwindet beim
|
||||
Deinstallieren mit.
|
||||
"""
|
||||
basis = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
|
||||
ordner = os.path.join(basis, "Rippy", "fenster")
|
||||
try:
|
||||
os.makedirs(ordner, exist_ok=True)
|
||||
except OSError:
|
||||
return ""
|
||||
return ordner
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Wirft, wenn es nicht geht — der Aufrufer entscheidet dann über den
|
||||
Browser. Diese Funktion trifft die Entscheidung NICHT selbst, weil sie
|
||||
sie nicht mitteilen könnte: Sie läuft, bis das Fenster zu ist.
|
||||
"""
|
||||
import webview
|
||||
|
||||
webview.create_window(
|
||||
titel, adresse,
|
||||
width=breite, height=hoehe,
|
||||
min_size=(MINDESTBREITE, MINDESTHOEHE),
|
||||
text_select=True, # sonst ließe sich kein Log-Text kopieren
|
||||
confirm_close=False, # der Dienst läuft weiter; nichts geht verloren
|
||||
)
|
||||
# gui="edgechromium" erzwingt WebView2 und verbietet den Rückfall auf
|
||||
# MSHTML. Lieber ein Fehler, den der Aufrufer sieht, als ein Fenster,
|
||||
# das die Oberfläche nicht darstellen kann.
|
||||
webview.start(gui="edgechromium", private_mode=False,
|
||||
storage_path=speicherort())
|
||||
|
||||
|
||||
def oeffnen_oder_browser(adresse: str, titel: str = TITEL) -> str:
|
||||
"""Fenster, sonst Browser. Gibt zurück, was WIRKLICH passiert ist.
|
||||
|
||||
Der Rückgabewert ist `"fenster"`, `"browser"` oder `"nichts"` — keine
|
||||
Erfolgsmeldung. Der Aufrufer soll dem Nutzer sagen können, was er sieht,
|
||||
statt zu behaupten, es sei alles gut gegangen.
|
||||
"""
|
||||
geht, grund = verfuegbar()
|
||||
if geht:
|
||||
try:
|
||||
oeffnen(adresse, titel)
|
||||
return "fenster"
|
||||
except Exception as e: # noqa: BLE001
|
||||
# Bewusst breit: Was WinForms und die CLR beim Start werfen
|
||||
# können, ist nicht abschließend bekannt — und keine dieser
|
||||
# Ausnahmen darf den Doppelklick ins Leere laufen lassen. Sie
|
||||
# wird gemeldet, nicht verschluckt.
|
||||
melden("Das Fenster ging nicht auf (%s: %s) — es öffnet der Browser."
|
||||
% (type(e).__name__, e))
|
||||
else:
|
||||
melden("Fenster nicht möglich (%s) — es öffnet der Browser." % grund)
|
||||
|
||||
import webbrowser
|
||||
|
||||
return "browser" if webbrowser.open(adresse) else "nichts"
|
||||
|
||||
|
||||
def melden(text: str) -> None:
|
||||
"""Ausgabe, die auch ohne Konsole nicht abstürzt (siehe windows_app)."""
|
||||
try:
|
||||
sys.stdout.write(text + "\n")
|
||||
sys.stdout.flush()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Kind-Prozesse ohne Fenster — und die eigene Konsole.
|
||||
|
||||
Ein Konsolenfenster laesst sich im Test nicht ansehen. Geprueft wird deshalb
|
||||
die ENTSCHEIDUNG: Gehoert die Konsole uns, oder haben wir sie geerbt? Genau
|
||||
daran haengt, ob `konsole_verbergen()` ein Fremdkoerper ausblendet oder dem
|
||||
Nutzer sein Terminal wegnimmt.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from rippy.platform import winlauf
|
||||
|
||||
nur_windows = pytest.mark.skipif(os.name != "nt", reason="nur unter Windows")
|
||||
|
||||
|
||||
def test_flag_ist_auf_windows_gesetzt_und_sonst_null():
|
||||
"""Auf Linux ist CREATE_NO_WINDOW nicht vorhanden; `creationflags=0` wird
|
||||
dort akzeptiert und ignoriert. So laeuft derselbe Code auf beiden Seiten."""
|
||||
if os.name == "nt":
|
||||
assert winlauf.OHNE_FENSTER == subprocess.CREATE_NO_WINDOW
|
||||
else:
|
||||
assert winlauf.OHNE_FENSTER == 0
|
||||
|
||||
|
||||
def test_ohne_windows_wird_nichts_verborgen():
|
||||
"""Der Kern laeuft auch unter Linux. Ein ctypes.WinDLL waere dort ein
|
||||
harter Fehler mitten im Start."""
|
||||
if os.name == "nt":
|
||||
pytest.skip("prueft das Verhalten auf Nicht-Windows")
|
||||
assert winlauf.konsole_verbergen() is False
|
||||
assert winlauf.konsole_gehoert_uns() is False
|
||||
|
||||
|
||||
@nur_windows
|
||||
def test_geerbte_konsole_wird_in_ruhe_gelassen():
|
||||
"""DER Fallstrick. `Rippy.exe --status` in einem Terminal wuerde bei
|
||||
einer naiven Umsetzung das Terminal des Nutzers verschwinden lassen.
|
||||
|
||||
Dieser Test laeuft SELBST in einem Terminal (pytest wurde von einer Shell
|
||||
gestartet) — an dieser Konsole haengt also mehr als ein Prozess. Genau
|
||||
der Fall, in dem nichts passieren darf.
|
||||
"""
|
||||
if winlauf.konsole_gehoert_uns():
|
||||
pytest.skip("Dieser Lauf hat eine eigene Konsole — kein Erbfall")
|
||||
assert winlauf.konsole_verbergen() is False
|
||||
|
||||
|
||||
@nur_windows
|
||||
def test_eigene_konsole_wird_erkannt():
|
||||
"""Ein frisch gestarteter Prozess MIT eigener Konsole muss sich als
|
||||
Eigentuemer erkennen — sonst waere die Konsole nie zu verbergen und die
|
||||
schwarze Box bliebe bei jedem Doppelklick stehen."""
|
||||
programm = (
|
||||
"import sys; sys.path.insert(0, %r);"
|
||||
"from rippy.platform import winlauf;"
|
||||
"print(winlauf.konsole_gehoert_uns())"
|
||||
% os.path.join(os.path.dirname(os.path.dirname(
|
||||
os.path.dirname(os.path.abspath(__file__)))))
|
||||
)
|
||||
import sys
|
||||
|
||||
ergebnis = subprocess.run(
|
||||
[sys.executable, "-c", programm],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
# NEUE Konsole: Damit hat der Kindprozess eine eigene, an der sonst
|
||||
# niemand haengt. Ohne dieses Flag erbte er unsere.
|
||||
creationflags=subprocess.CREATE_NEW_CONSOLE,
|
||||
)
|
||||
assert ergebnis.stdout.strip() == "True", ergebnis.stderr
|
||||
@@ -23,6 +23,20 @@ ripping.py und schluessel.py; jeder hätte das Flag einzeln vergessen können, u
|
||||
genau so ist es passiert. Auf Linux ist `CREATE_NO_WINDOW` nicht vorhanden und
|
||||
`creationflags=0` eine Nulloperation — im Worker-Container gegengeprüft, damit
|
||||
derselbe Code auf beiden Seiten läuft.
|
||||
|
||||
## Die eigene Konsole (28.08.2026)
|
||||
|
||||
Dieselbe Frage noch einmal, nur andersherum: Rippy.exe ist als
|
||||
Konsolenprogramm gebaut — der Installer MUSS seine Meldungen zeigen können.
|
||||
Ein Doppelklick auf das Desktop-Symbol öffnet damit aber erst eine schwarze
|
||||
Box und dann das Fenster. Für ein Programm, das ein „echter Client" sein
|
||||
soll, ist das ein Fremdkörper.
|
||||
|
||||
`konsole_verbergen()` blendet sie aus — aber nur die EIGENE. Wer
|
||||
`Rippy.exe --oeffnen` in einem Terminal eintippt, dessen Konsole gehört ihm,
|
||||
nicht Rippy; sie zu verstecken würde ihm sein Fenster wegnehmen. Woran man
|
||||
das unterscheidet: `GetConsoleProcessList` sagt, wie viele Prozesse an dieser
|
||||
Konsole hängen. Genau einer — dann ist es die eigene.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
@@ -30,3 +44,47 @@ import subprocess
|
||||
# Auf Windows das Flag, auf Linux 0 (dort wird creationflags=0 akzeptiert und
|
||||
# ignoriert — am 26.07.2026 im Worker-Container gemessen).
|
||||
OHNE_FENSTER = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||||
|
||||
SW_HIDE = 0
|
||||
|
||||
|
||||
def konsole_gehoert_uns() -> bool:
|
||||
"""Hängt NUR dieser Prozess an der Konsole?
|
||||
|
||||
False heißt: Wir haben sie geerbt (Terminal, `cmd`, PowerShell). Dann ist
|
||||
sie fremdes Eigentum — Hände weg. Ohne diese Unterscheidung würde ein
|
||||
`Rippy.exe --status` im Terminal das Terminal des Nutzers verschwinden
|
||||
lassen.
|
||||
"""
|
||||
import ctypes
|
||||
|
||||
try:
|
||||
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
puffer = (ctypes.c_uint * 8)()
|
||||
anzahl = kernel32.GetConsoleProcessList(puffer, 8)
|
||||
except (AttributeError, OSError):
|
||||
return False
|
||||
return anzahl == 1
|
||||
|
||||
|
||||
def konsole_verbergen() -> bool:
|
||||
"""Blendet die eigene Konsole aus. True, wenn es wirklich passiert ist.
|
||||
|
||||
Kein `FreeConsole()`: Das schließt auch die Standardausgabe, und ein
|
||||
späteres `print` würde dann werfen. Verstecken reicht — das Fenster ist
|
||||
weg, die Ströme bleiben heil.
|
||||
"""
|
||||
import ctypes
|
||||
import os
|
||||
|
||||
if os.name != "nt" or not konsole_gehoert_uns():
|
||||
return False
|
||||
try:
|
||||
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
fenster = kernel32.GetConsoleWindow()
|
||||
if not fenster:
|
||||
return False
|
||||
return bool(ctypes.WinDLL("user32", use_last_error=True)
|
||||
.ShowWindow(fenster, SW_HIDE))
|
||||
except (AttributeError, OSError):
|
||||
return False
|
||||
|
||||
@@ -10,6 +10,7 @@ sitzen die Fehler, die man spaeter nicht mehr findet:
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from rippy import config, daemon
|
||||
|
||||
@@ -66,6 +67,45 @@ def test_ui_pfad_ist_entweder_da_oder_ehrlich_leer():
|
||||
assert pfad == "" or os.path.isdir(pfad)
|
||||
|
||||
|
||||
def test_kopie_neben_dem_programm_geht_vor_dem_paket(tmp_path, monkeypatch):
|
||||
"""DER Befund vom 28.08.2026. Ein Dienst lief eine Stunde:
|
||||
`/api/health` gab 200, `/` gab 404, im Fenster stand
|
||||
`{"detail":"Not Found"}`.
|
||||
|
||||
Der Server war in Ordnung — sein Entpack-Verzeichnis unter `%TEMP%` war
|
||||
es nicht: 31 Eintraege statt 44, kein `ui`. `StaticFiles` schlaegt bei
|
||||
JEDER Anfrage dort nach, also wurde aus einer laufenden Oberflaeche eine
|
||||
404, waehrend die API weiter gesund meldete.
|
||||
|
||||
Die installierte Kopie hat deshalb Vorrang. Faellt dieser Test aus, ist
|
||||
der Dienst wieder darauf angewiesen, dass niemand das Temp aufraeumt.
|
||||
"""
|
||||
installiert = tmp_path / "installiert"
|
||||
(installiert / "ui").mkdir(parents=True)
|
||||
(installiert / "ui" / "index.html").write_text("<!DOCTYPE html>",
|
||||
encoding="utf-8")
|
||||
paket = tmp_path / "temp" / "_MEI4711"
|
||||
(paket / "ui").mkdir(parents=True)
|
||||
|
||||
monkeypatch.setattr(daemon, "_programmordner",
|
||||
lambda: str(installiert / "Rippy.exe"))
|
||||
monkeypatch.setattr(sys, "_MEIPASS", str(paket), raising=False)
|
||||
|
||||
assert daemon._ui_pfad() == str(installiert / "ui")
|
||||
|
||||
|
||||
def test_ohne_kopie_bleibt_das_paket_der_rueckfall(tmp_path, monkeypatch):
|
||||
"""Wer die Setup-Datei direkt startet, hat keinen Installationsordner.
|
||||
Dann ist das Entpack-Verzeichnis richtig — nur eben nicht auf Dauer."""
|
||||
paket = tmp_path / "_MEI4711"
|
||||
(paket / "ui").mkdir(parents=True)
|
||||
monkeypatch.setattr(daemon, "_programmordner",
|
||||
lambda: str(tmp_path / "leer" / "RippySetup.exe"))
|
||||
monkeypatch.setattr(sys, "_MEIPASS", str(paket), raising=False)
|
||||
|
||||
assert daemon._ui_pfad() == str(paket / "ui")
|
||||
|
||||
|
||||
def test_standard_konfigpfad_passt_zur_plattform():
|
||||
pfad = daemon.standard_konfigpfad()
|
||||
if os.name == "nt":
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
"""Das native Fenster: Verfuegbarkeit, Rueckfall, Speicherort.
|
||||
|
||||
Ein Fenster laesst sich in einem Test nicht oeffnen — es wuerde auf einen
|
||||
Doppelklick warten, den niemand macht. Geprueft wird deshalb alles DAVOR und
|
||||
DANEBEN: die Entscheidung, ob ein Fenster moeglich ist, und der Rueckfall auf
|
||||
den Browser. Genau dort sassen die Fehler, die dieses Projekt schon zweimal
|
||||
gekostet hat.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from rippy import fenster
|
||||
|
||||
nur_windows = pytest.mark.skipif(os.name != "nt", reason="nur unter Windows")
|
||||
|
||||
|
||||
# ── Der Registry-Wert ───────────────────────────────────────────────────
|
||||
def test_null_version_gilt_als_nicht_vorhanden():
|
||||
"""DER Fallstrick bei WebView2: EdgeUpdate LOESCHT den Schluessel bei
|
||||
einer Deinstallation nicht, sondern setzt `pv` auf `0.0.0.0`. Wer nur
|
||||
auf die Existenz des Schluessels prueft, oeffnet ein Fenster, das nichts
|
||||
anzeigen kann — und der Nutzer sieht einen weissen Kasten."""
|
||||
assert fenster.echte_version("0.0.0.0") == ""
|
||||
|
||||
|
||||
def test_echte_version_kommt_durch():
|
||||
assert fenster.echte_version("151.0.4129.107") == "151.0.4129.107"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("wert", ["", None, " "])
|
||||
def test_leere_werte_gelten_nicht(wert):
|
||||
assert fenster.echte_version(wert) == ""
|
||||
|
||||
|
||||
# ── Die Entscheidung ────────────────────────────────────────────────────
|
||||
def test_verfuegbar_gibt_immer_eine_begruendung():
|
||||
"""Auch beim Ja. Ein blosses False zwingt den Naechsten zum Nachforschen
|
||||
— und ein Fehlschlag ohne Begruendung ist in diesem Projekt schon
|
||||
zweimal teuer geworden."""
|
||||
geht, grund = fenster.verfuegbar()
|
||||
assert isinstance(geht, bool)
|
||||
assert grund, "auch ein Ja braucht eine Begruendung"
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="prueft das Verhalten auf Nicht-Windows")
|
||||
def test_ohne_windows_kein_fenster():
|
||||
geht, grund = fenster.verfuegbar()
|
||||
assert geht is False
|
||||
assert "Windows" in grund
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="winreg gibt es nur unter Windows")
|
||||
def test_version_ohne_windows_ist_leer_statt_zu_werfen():
|
||||
"""Der Kern laeuft auch unter Linux (Docker, headless). Ein ImportError
|
||||
auf winreg wuerde dort jeden Aufruf sprengen."""
|
||||
assert fenster.webview2_version() == ""
|
||||
|
||||
|
||||
def test_fehlende_bibliothek_verhindert_das_fenster(monkeypatch):
|
||||
monkeypatch.setattr(os, "name", "nt")
|
||||
monkeypatch.setattr(fenster, "bibliothek_vorhanden", lambda: False)
|
||||
geht, grund = fenster.verfuegbar()
|
||||
assert geht is False
|
||||
assert "pywebview" in grund
|
||||
|
||||
|
||||
def test_fehlende_laufzeit_verhindert_das_fenster(monkeypatch):
|
||||
"""Lieber Browser als ein leeres Fenster: Ohne WebView2 faellt pywebview
|
||||
auf MSHTML zurueck, und der stellt die React-Oberflaeche nicht dar."""
|
||||
monkeypatch.setattr(os, "name", "nt")
|
||||
monkeypatch.setattr(fenster, "bibliothek_vorhanden", lambda: True)
|
||||
monkeypatch.setattr(fenster, "webview2_version", lambda: "")
|
||||
geht, grund = fenster.verfuegbar()
|
||||
assert geht is False
|
||||
assert "WebView2" in grund
|
||||
|
||||
|
||||
# ── Der Rueckfall ───────────────────────────────────────────────────────
|
||||
def test_ohne_fenster_geht_der_browser_auf(monkeypatch):
|
||||
gerufen = []
|
||||
monkeypatch.setattr(fenster, "verfuegbar", lambda: (False, "kein Windows"))
|
||||
monkeypatch.setattr("webbrowser.open", lambda a: gerufen.append(a) or True)
|
||||
|
||||
assert fenster.oeffnen_oder_browser("http://127.0.0.1:7788") == "browser"
|
||||
assert gerufen == ["http://127.0.0.1:7788"]
|
||||
|
||||
|
||||
def test_geplatztes_fenster_faellt_auf_den_browser_zurueck(monkeypatch, capsys):
|
||||
"""Der wichtigste Test hier. Was WinForms und die CLR beim Start werfen
|
||||
koennen, ist nicht abschliessend bekannt. Faellt das nicht zurueck, ist
|
||||
der Doppelklick auf das Desktop-Symbol wirkungslos — und der Nutzer
|
||||
haelt Rippy fuer kaputt."""
|
||||
def platzt(*a, **k):
|
||||
raise RuntimeError("edgechromium nicht gefunden")
|
||||
|
||||
gerufen = []
|
||||
monkeypatch.setattr(fenster, "verfuegbar", lambda: (True, "WebView2 151"))
|
||||
monkeypatch.setattr(fenster, "oeffnen", platzt)
|
||||
monkeypatch.setattr("webbrowser.open", lambda a: gerufen.append(a) or True)
|
||||
|
||||
assert fenster.oeffnen_oder_browser("http://127.0.0.1:7788") == "browser"
|
||||
assert gerufen == ["http://127.0.0.1:7788"]
|
||||
# Und der Grund muss SICHTBAR sein, nicht verschluckt.
|
||||
assert "edgechromium nicht gefunden" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_wenn_gar_nichts_geht_wird_das_auch_gesagt(monkeypatch):
|
||||
"""`webbrowser.open` gibt False zurueck, wenn kein Browser gefunden wird.
|
||||
Ein `return "browser"` waere dann eine Luege."""
|
||||
monkeypatch.setattr(fenster, "verfuegbar", lambda: (False, "kein Windows"))
|
||||
monkeypatch.setattr("webbrowser.open", lambda a: False)
|
||||
assert fenster.oeffnen_oder_browser("http://127.0.0.1:7788") == "nichts"
|
||||
|
||||
|
||||
def test_erfolgreiches_fenster_oeffnet_keinen_browser(monkeypatch):
|
||||
"""Sonst haette der Nutzer BEIDES auf — Fenster und Tab."""
|
||||
gerufen = []
|
||||
monkeypatch.setattr(fenster, "verfuegbar", lambda: (True, "WebView2 151"))
|
||||
monkeypatch.setattr(fenster, "oeffnen", lambda *a, **k: None)
|
||||
monkeypatch.setattr("webbrowser.open", lambda a: gerufen.append(a) or True)
|
||||
|
||||
assert fenster.oeffnen_oder_browser("http://127.0.0.1:7788") == "fenster"
|
||||
assert gerufen == []
|
||||
|
||||
|
||||
# ── Der Speicherort ─────────────────────────────────────────────────────
|
||||
def test_speicherort_liegt_bei_rippy(monkeypatch, tmp_path):
|
||||
"""Im TEMP waere er bei jedem Aufraeumen weg — und beim Deinstallieren
|
||||
bliebe er liegen."""
|
||||
monkeypatch.setenv("LOCALAPPDATA", str(tmp_path))
|
||||
ordner = fenster.speicherort()
|
||||
assert ordner.startswith(str(tmp_path))
|
||||
assert os.path.isdir(ordner)
|
||||
|
||||
|
||||
def test_unbeschreibbarer_ort_wirft_nicht(monkeypatch):
|
||||
"""Ein fehlender Zwischenspeicher ist aergerlich, kein Grund, das
|
||||
Fenster gar nicht erst zu oeffnen."""
|
||||
def geht_nicht(*a, **k):
|
||||
raise OSError("kein Platz")
|
||||
|
||||
monkeypatch.setattr(os, "makedirs", geht_nicht)
|
||||
assert fenster.speicherort() == ""
|
||||
|
||||
|
||||
# ── Auf dieser Maschine wirklich nachsehen ──────────────────────────────
|
||||
@nur_windows
|
||||
def test_webview2_ist_auf_diesem_rechner_da():
|
||||
"""Kein Muss-Test, sondern ein Messpunkt: Faellt er aus, laeuft Rippy im
|
||||
Browser statt im Fenster — und man weiss auch gleich, warum."""
|
||||
version = fenster.webview2_version()
|
||||
if not version:
|
||||
pytest.skip("Keine WebView2-Laufzeit auf dieser Maschine")
|
||||
assert version.count(".") >= 2, version
|
||||
@@ -154,3 +154,139 @@ def test_installation_ruehrt_die_echten_verknuepfungen_nicht_an(tmp_path, monkey
|
||||
finally:
|
||||
reg.austragen()
|
||||
assert aufrufe == []
|
||||
|
||||
|
||||
# ── Die Oberflaeche neben dem Programm ──────────────────────────────────
|
||||
def test_ui_wird_neben_das_programm_gelegt(tmp_path, monkeypatch):
|
||||
"""DER Befund vom 28.08.2026: Ein Dienst lief eine Stunde, `/api/health`
|
||||
gab 200, `/` gab 404. Die Onefile-EXE liest ihre Oberflaeche aus
|
||||
`%TEMP%\\_MEIxxxxx`, und von diesem Ordner waren 31 statt 44 Eintraege
|
||||
uebrig — kein `ui`. Die API antwortete weiter, der Dienst galt als
|
||||
gesund, nur die Oberflaeche war weg."""
|
||||
paket = tmp_path / "paket" / "ui"
|
||||
(paket / "assets").mkdir(parents=True)
|
||||
(paket / "index.html").write_text("<!DOCTYPE html>", encoding="utf-8")
|
||||
(paket / "assets" / "haupt.js").write_text("//", encoding="utf-8")
|
||||
monkeypatch.setattr(windows_app, "_mitgeliefert",
|
||||
lambda name: str(tmp_path / "paket" / name))
|
||||
|
||||
ziel = tmp_path / "installiert"
|
||||
ziel.mkdir()
|
||||
ordner = windows_app.ui_auspacken(str(ziel))
|
||||
|
||||
assert ordner == str(ziel / "ui")
|
||||
assert os.path.isfile(os.path.join(ordner, "index.html"))
|
||||
assert os.path.isfile(os.path.join(ordner, "assets", "haupt.js"))
|
||||
|
||||
|
||||
def test_alter_stand_wird_beim_update_nicht_vermischt(tmp_path, monkeypatch):
|
||||
"""Ein Update mit weniger Dateien liesse sonst Reste liegen — die
|
||||
Oberflaeche waere ein Gemisch aus zwei Fassungen, und der Fehler saehe
|
||||
aus wie ein Zufall."""
|
||||
paket = tmp_path / "paket" / "ui"
|
||||
paket.mkdir(parents=True)
|
||||
(paket / "index.html").write_text("neu", encoding="utf-8")
|
||||
monkeypatch.setattr(windows_app, "_mitgeliefert",
|
||||
lambda name: str(tmp_path / "paket" / name))
|
||||
|
||||
ziel = tmp_path / "installiert"
|
||||
(ziel / "ui").mkdir(parents=True)
|
||||
(ziel / "ui" / "veraltet.js").write_text("alt", encoding="utf-8")
|
||||
|
||||
windows_app.ui_auspacken(str(ziel))
|
||||
assert not os.path.exists(str(ziel / "ui" / "veraltet.js"))
|
||||
|
||||
|
||||
def test_ohne_gebuendeltes_ui_passiert_nichts(tmp_path, monkeypatch):
|
||||
"""Aus dem Repo gestartet liegt das UI schon an fester Stelle."""
|
||||
monkeypatch.setattr(windows_app, "_mitgeliefert",
|
||||
lambda name: str(tmp_path / "gibt-es-nicht"))
|
||||
assert windows_app.ui_auspacken(str(tmp_path)) == ""
|
||||
|
||||
|
||||
def test_fehlschlag_beim_ablegen_bricht_die_installation_nicht_ab(tmp_path, monkeypatch, capsys):
|
||||
"""Ohne Kopie laeuft Rippy aus dem Temp weiter — schlechter, aber nicht
|
||||
kaputt. Gesagt werden muss es trotzdem, sonst sucht spaeter jemand den
|
||||
404 im Server."""
|
||||
paket = tmp_path / "paket" / "ui"
|
||||
paket.mkdir(parents=True)
|
||||
(paket / "index.html").write_text("x", encoding="utf-8")
|
||||
monkeypatch.setattr(windows_app, "_mitgeliefert",
|
||||
lambda name: str(tmp_path / "paket" / name))
|
||||
|
||||
def geht_nicht(*a, **k):
|
||||
raise OSError("Zugriff verweigert")
|
||||
|
||||
monkeypatch.setattr(windows_app.shutil, "copytree", geht_nicht)
|
||||
assert windows_app.ui_auspacken(str(tmp_path / "ziel")) == ""
|
||||
assert "Oberflaeche konnte nicht abgelegt werden" in capsys.readouterr().out
|
||||
|
||||
|
||||
# ── Oeffnen: Fenster statt Browser-Tab ──────────────────────────────────
|
||||
def test_oeffnen_nimmt_das_fenster_nicht_den_browser(monkeypatch):
|
||||
"""Die Antwort auf die Frage des Commanders vom 28.08.2026: „Warum
|
||||
nutzen wir fuer Windows weiterhin einen Browser?" — tun wir nicht mehr.
|
||||
Ein Doppelklick fuehrt in ein eigenes Fenster mit eigenem Symbol in der
|
||||
Taskleiste, nicht in einen Tab zwischen fremden Tabs."""
|
||||
from rippy import fenster
|
||||
|
||||
gerufen = []
|
||||
monkeypatch.setattr(windows_app, "bereitstellen", lambda port=0: True)
|
||||
monkeypatch.setattr(fenster, "oeffnen_oder_browser",
|
||||
lambda a, t="Rippy": gerufen.append(a) or "fenster")
|
||||
monkeypatch.setattr("webbrowser.open",
|
||||
lambda a: pytest.fail("Der Browser darf hier nicht aufgehen"))
|
||||
|
||||
assert windows_app.oeffnen(7788) == 0
|
||||
assert gerufen == ["http://127.0.0.1:7788"]
|
||||
|
||||
|
||||
def test_im_browser_geht_wirklich_in_den_browser(monkeypatch):
|
||||
"""Der Schalter muss die Bibliothek gar nicht erst anfassen — sonst
|
||||
haette man auf einem Rechner ohne WebView2 keinen Ausweg."""
|
||||
from rippy import fenster
|
||||
|
||||
gerufen = []
|
||||
monkeypatch.setattr(windows_app, "bereitstellen", lambda port=0: True)
|
||||
monkeypatch.setattr(fenster, "oeffnen_oder_browser",
|
||||
lambda *a, **k: pytest.fail("kein Fenster erwartet"))
|
||||
monkeypatch.setattr("webbrowser.open", lambda a: gerufen.append(a) or True)
|
||||
|
||||
assert windows_app.oeffnen(7788, als_fenster=False) == 0
|
||||
assert gerufen == ["http://127.0.0.1:7788"]
|
||||
|
||||
|
||||
def test_ohne_antwortenden_dienst_geht_gar_nichts_auf(monkeypatch):
|
||||
"""Ein Fenster auf eine tote Adresse zeigt einen Verbindungsfehler —
|
||||
und der sieht aus, als sei Rippy kaputt. Lieber nichts oeffnen und einen
|
||||
Fehlercode zurueckgeben."""
|
||||
from rippy import fenster
|
||||
|
||||
monkeypatch.setattr(windows_app, "bereitstellen", lambda port=0: False)
|
||||
monkeypatch.setattr(fenster, "oeffnen_oder_browser",
|
||||
lambda *a, **k: pytest.fail("nichts darf aufgehen"))
|
||||
monkeypatch.setattr("webbrowser.open",
|
||||
lambda a: pytest.fail("nichts darf aufgehen"))
|
||||
|
||||
assert windows_app.oeffnen(7788) == 1
|
||||
|
||||
|
||||
def test_wenn_weder_fenster_noch_browser_geht_ist_das_ein_fehler(monkeypatch):
|
||||
"""`oeffnen_oder_browser` gibt „nichts" zurueck, wenn beide Wege
|
||||
scheitern. Ein Rueckgabewert 0 waere dann eine Luege."""
|
||||
from rippy import fenster
|
||||
|
||||
monkeypatch.setattr(windows_app, "bereitstellen", lambda port=0: True)
|
||||
monkeypatch.setattr(fenster, "oeffnen_oder_browser", lambda *a, **k: "nichts")
|
||||
assert windows_app.oeffnen(7788) == 1
|
||||
|
||||
|
||||
def test_laufender_dienst_wird_nicht_noch_einmal_gestartet(monkeypatch):
|
||||
"""Sonst startete jeder Klick einen ZWEITEN Server, der am belegten Port
|
||||
scheitert — genau die Falle, wegen der die Verknuepfungen ueberhaupt auf
|
||||
--oeffnen zeigen."""
|
||||
monkeypatch.setattr(windows_app.Dienst, "erreichbar",
|
||||
lambda self, timeout=1.0: True)
|
||||
monkeypatch.setattr(windows_app, "starte_hintergrund",
|
||||
lambda p: pytest.fail("es laeuft doch schon einer"))
|
||||
assert windows_app.bereitstellen(7788) is True
|
||||
|
||||
+123
-22
@@ -122,6 +122,8 @@ def installieren(ziel: str = None, autostart: bool = True,
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
ui_auspacken(ziel)
|
||||
|
||||
# Der Eintrag in „Programme und Features" — ohne ihn ist Rippy ein
|
||||
# Programm, das man nur von Hand wieder loswird.
|
||||
reg.eintragen(ziel, programm, VERSION,
|
||||
@@ -150,6 +152,50 @@ def installieren(ziel: str = None, autostart: bool = True,
|
||||
return ziel
|
||||
|
||||
|
||||
def ui_auspacken(ziel: str) -> str:
|
||||
r"""Legt die Weboberfläche NEBEN das Programm. Gibt den Ordner zurück.
|
||||
|
||||
## Warum das sein muss (gemessen 28.08.2026)
|
||||
|
||||
Ein Dienst lief eine Stunde. `/api/health` gab HTTP 200, `/` gab **404**
|
||||
— im Fenster stand `{"detail":"Not Found"}`. Der Server war in Ordnung;
|
||||
was fehlte, lag unter ihm.
|
||||
|
||||
Eine PyInstaller-Onefile-EXE entpackt sich beim Start nach
|
||||
`%TEMP%\_MEIxxxxx` und liest von dort weiter. `StaticFiles` schlägt bei
|
||||
JEDER Anfrage dort nach. Nachgesehen, was vom Ordner des laufenden
|
||||
Dienstes übrig war: 31 Einträge statt 44, kein `ui`, kein `api`.
|
||||
|
||||
Ein Temp-Verzeichnis ist kein Ort für etwas, das eine Woche liegen
|
||||
bleiben soll. Und der Ausfall ist der schlimmstmögliche: Die API
|
||||
antwortet weiter, der Dienst gilt als gesund, nur die Oberfläche ist weg.
|
||||
|
||||
Die Kopie hier gehört Rippy, liegt im Installationsordner und wird beim
|
||||
Deinstallieren mit entfernt (`deinstallieren()` räumt den ganzen Ordner).
|
||||
|
||||
Leerer Rückgabewert heißt: keine Kopie angelegt — dann greift der
|
||||
Rückfall auf `sys._MEIPASS` in `daemon._ui_pfad()`.
|
||||
"""
|
||||
quelle = _mitgeliefert("ui")
|
||||
if not os.path.isdir(quelle):
|
||||
# Aus dem Repo gestartet: Da liegt das UI schon an fester Stelle.
|
||||
return ""
|
||||
|
||||
ordner = os.path.join(ziel, "ui")
|
||||
try:
|
||||
# Erst weg, dann neu: Ein Update mit weniger Dateien ließe sonst
|
||||
# Reste des alten Standes liegen, und die Oberfläche wäre ein
|
||||
# Gemisch aus zwei Fassungen.
|
||||
shutil.rmtree(ordner, ignore_errors=True)
|
||||
shutil.copytree(quelle, ordner)
|
||||
except OSError as e:
|
||||
# Nicht werfen: Ohne Kopie läuft Rippy aus dem Temp weiter — schlechter,
|
||||
# aber nicht kaputt. Gesagt wird es trotzdem.
|
||||
melden(" Oberflaeche konnte nicht abgelegt werden (%s)." % e)
|
||||
return ""
|
||||
return ordner
|
||||
|
||||
|
||||
def starte_hintergrund(programm: str) -> None:
|
||||
"""Startet Rippy als Hintergrundprozess — ohne aufblitzendes Fenster."""
|
||||
from rippy.platform.winlauf import OHNE_FENSTER
|
||||
@@ -158,6 +204,24 @@ def starte_hintergrund(programm: str) -> None:
|
||||
close_fds=True)
|
||||
|
||||
|
||||
def starte_fensterprozess(programm: str = None) -> None:
|
||||
"""Startet die Oberflaeche als EIGENEN Prozess.
|
||||
|
||||
Warum nicht einfach hier ein Fenster oeffnen: Das Tray-Symbol (pystray)
|
||||
belegt unter Windows den Haupt-Thread — es braucht ihn fuer die
|
||||
Fensternachrichten. Das WebView2-Fenster braucht ihn genauso. Zwei
|
||||
Nachrichtenschleifen passen nicht in einen Thread.
|
||||
|
||||
Zwei Prozesse loesen das sauber: Der Dienst haelt Server und Tray, das
|
||||
Fenster ist der Client davor. Beide heissen im Taskmanager „Rippy.exe",
|
||||
und das Fenster laesst sich schliessen, ohne den Dienst mitzureissen.
|
||||
"""
|
||||
from rippy.platform.winlauf import OHNE_FENSTER
|
||||
|
||||
subprocess.Popen([programm or _eigene_datei(), "--oeffnen"],
|
||||
creationflags=OHNE_FENSTER, close_fds=True)
|
||||
|
||||
|
||||
def deinstallieren(still: bool = False) -> None:
|
||||
"""Nimmt alle Spuren zurück: Autostart, Registry-Eintrag, Dateien."""
|
||||
# WOHIN wurde installiert? Das steht in der Registry, nicht im Code.
|
||||
@@ -355,8 +419,30 @@ def dienst_starten(port: int = STANDARD_PORT) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def oeffnen(port: int = STANDARD_PORT) -> int:
|
||||
"""Rippy oeffnen — Dienst starten, falls noetig, dann den Browser.
|
||||
def bereitstellen(port: int = STANDARD_PORT) -> bool:
|
||||
"""Sorgt dafuer, dass der Dienst ANTWORTET. Startet ihn notfalls.
|
||||
|
||||
Getrennt von `oeffnen()`, weil beide Wege dorthin — Fenster und Browser —
|
||||
genau dieselbe Vorbedingung haben: Ohne antwortenden Server zeigt das
|
||||
eine wie das andere nur einen Verbindungsfehler.
|
||||
"""
|
||||
dienst = Dienst(port)
|
||||
if dienst.erreichbar(timeout=1.5):
|
||||
return True
|
||||
|
||||
melden("Rippy wird gestartet …")
|
||||
starte_hintergrund(_eigene_datei())
|
||||
# Warten, bis er ANTWORTET — nicht bis der Prozess existiert.
|
||||
for _ in range(60):
|
||||
if dienst.erreichbar(timeout=1.0):
|
||||
return True
|
||||
time.sleep(0.5)
|
||||
melden("Rippy antwortet nicht. Laeuft schon etwas auf Port %d?" % port)
|
||||
return False
|
||||
|
||||
|
||||
def oeffnen(port: int = STANDARD_PORT, als_fenster: bool = True) -> int:
|
||||
"""Rippy oeffnen — Dienst starten, falls noetig, dann die Oberflaeche.
|
||||
|
||||
## Warum die Verknuepfungen hierher zeigen und nicht auf --dienst
|
||||
|
||||
@@ -369,26 +455,28 @@ def oeffnen(port: int = STANDARD_PORT) -> int:
|
||||
das beim zweiten Klick nichts tut, ist schlimmer als keins.
|
||||
|
||||
Deshalb: erst NACHSEHEN, ob jemand antwortet. Nur wenn nicht, starten —
|
||||
und dann warten, bis er wirklich da ist, statt sofort einen Browser auf
|
||||
und dann warten, bis er wirklich da ist, statt sofort ein Fenster auf
|
||||
eine tote Adresse zu schicken.
|
||||
|
||||
## Warum hier ein Fenster und kein Browser-Tab
|
||||
|
||||
Siehe `rippy/fenster.py`. Kurz: Ein Tab hat eine Adresszeile, die
|
||||
niemand braucht, kein eigenes Symbol in der Taskleiste, und wer den
|
||||
Browser schliesst, glaubt, er habe Rippy beendet. Geht das Fenster nicht
|
||||
auf, faellt Rippy auf den Browser zurueck und SAGT, warum.
|
||||
"""
|
||||
dienst = Dienst(port)
|
||||
if not bereitstellen(port):
|
||||
return 1
|
||||
|
||||
if not dienst.erreichbar(timeout=1.5):
|
||||
melden("Rippy wird gestartet …")
|
||||
programm = _eigene_datei()
|
||||
starte_hintergrund(programm)
|
||||
# Warten, bis er ANTWORTET — nicht bis der Prozess existiert.
|
||||
for _ in range(60):
|
||||
if dienst.erreichbar(timeout=1.0):
|
||||
break
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
melden("Rippy antwortet nicht. Laeuft schon etwas auf Port %d?" % port)
|
||||
return 1
|
||||
adresse = Dienst(port).adresse
|
||||
if not als_fenster:
|
||||
return 0 if webbrowser.open(adresse) else 1
|
||||
|
||||
webbrowser.open(dienst.adresse)
|
||||
return 0
|
||||
from rippy import fenster as fenster_modul
|
||||
|
||||
# Der Rueckgabewert sagt, was WIRKLICH passiert ist — nicht, dass es
|
||||
# geklappt hat. „nichts" heisst: weder Fenster noch Browser.
|
||||
return 0 if fenster_modul.oeffnen_oder_browser(adresse) != "nichts" else 1
|
||||
|
||||
|
||||
def tray_bauen(dienst):
|
||||
@@ -413,7 +501,9 @@ def tray_bauen(dienst):
|
||||
zeichner.ellipse((26, 26, 38, 38), fill=(245, 158, 11))
|
||||
|
||||
def oeffnen(_=None, __=None):
|
||||
webbrowser.open(dienst.adresse)
|
||||
# Eigener Prozess: pystray haelt hier den Haupt-Thread, das Fenster
|
||||
# braucht ihn aber selbst (siehe starte_fensterprozess).
|
||||
starte_fensterprozess()
|
||||
|
||||
def beenden(symbol, _=None):
|
||||
dienst.stoppen()
|
||||
@@ -445,6 +535,7 @@ Optionen:
|
||||
--ziel PFAD Installationsordner
|
||||
--kein-autostart
|
||||
--keine-verknuepfungen ohne Desktop-Symbol und Startmenue-Eintrag
|
||||
--im-browser im Browser statt im eigenen Fenster
|
||||
--still ohne Rückmeldungen
|
||||
"""
|
||||
|
||||
@@ -482,7 +573,15 @@ def main(argv=None) -> int:
|
||||
deinstallieren(still="--still" in argv)
|
||||
return 0
|
||||
if "--oeffnen" in argv:
|
||||
return oeffnen(int(wert("--port", STANDARD_PORT)))
|
||||
# Rippy.exe ist ein Konsolenprogramm (der Installer MUSS seine
|
||||
# Meldungen zeigen koennen). Beim Doppelklick auf das Desktop-Symbol
|
||||
# blitzte damit erst eine schwarze Box auf und dann das Fenster --
|
||||
# ein Fremdkoerper in einem Programm, das ein echter Client sein soll.
|
||||
# Verborgen wird nur die EIGENE Konsole, nie eine geerbte.
|
||||
from rippy.platform.winlauf import konsole_verbergen
|
||||
konsole_verbergen()
|
||||
return oeffnen(int(wert("--port", STANDARD_PORT)),
|
||||
als_fenster="--im-browser" not in argv)
|
||||
if "--dienst" in argv:
|
||||
return dienst_starten(int(wert("--port", STANDARD_PORT)))
|
||||
|
||||
@@ -494,10 +593,12 @@ def main(argv=None) -> int:
|
||||
verknuepfen="--keine-verknuepfungen" not in argv,
|
||||
)
|
||||
melden("Rippy wurde nach %s installiert." % ziel)
|
||||
melden("Die Oberfläche öffnet sich gleich im Browser.")
|
||||
melden("Die Oberfläche öffnet sich gleich in einem eigenen Fenster.")
|
||||
if "--nicht-starten" not in argv:
|
||||
# Als eigener Prozess und aus dem INSTALLIERTEN Programm heraus:
|
||||
# Das Setup soll sich beenden koennen, ohne das Fenster mitzunehmen.
|
||||
time.sleep(3)
|
||||
webbrowser.open("http://127.0.0.1:%d" % int(wert("--port", STANDARD_PORT)))
|
||||
starte_fensterprozess(os.path.join(ziel, "Rippy.exe"))
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user