diff --git a/.gitattributes b/.gitattributes index 5aeb5d5..8510755 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ # Shell-Skripte brauchen LF — mit CRLF scheitert der Interpreter im Container # ("/bin/sh^M: bad interpreter"). Wir entwickeln auf Windows, gebaut wird auf Linux. *.sh text eol=lf +*.exe binary +*.ico binary diff --git a/deploy/worker-windows/RippyWorkerSetup.exe b/deploy/worker-windows/RippyWorkerSetup.exe new file mode 100644 index 0000000..e35558a Binary files /dev/null and b/deploy/worker-windows/RippyWorkerSetup.exe differ diff --git a/deploy/worker-windows/build-exe.ps1 b/deploy/worker-windows/build-exe.ps1 new file mode 100644 index 0000000..34e5eeb --- /dev/null +++ b/deploy/worker-windows/build-exe.ps1 @@ -0,0 +1,59 @@ +# Baut rippy.ico + RippyWorkerSetup.exe aus install-gui.ps1 (ps2exe). +# +# WANN NEU BAUEN: nur wenn sich install-gui.ps1 (die Installer-Oberflaeche) +# aendert. NICHT bei MakeMKV-/HandBrake-Updates — die .exe holt HandBrake zur +# Laufzeit (GitHub latest) und den Worker-Code live von Rippy, friert also +# keine Versionen ein. +# AUSFUEHREN AUF WINDOWS (eine Windows-.exe kann nicht von Linux gebaut werden): +# powershell -ExecutionPolicy Bypass -File build-exe.ps1 +# Danach RippyWorkerSetup.exe committen (die API serviert sie). +$ErrorActionPreference = "Stop" +Add-Type -AssemblyName System.Drawing +Set-Location $PSScriptRoot + +$guiSrc = Join-Path $PSScriptRoot "install-gui.ps1" + +# 1. Rippy-Disc-Icon 256x256 zeichnen -> PNG -> ICO-Container +$bmp = New-Object System.Drawing.Bitmap 256, 256 +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.SmoothingMode = "AntiAlias" +$g.Clear([System.Drawing.Color]::Transparent) +$g.FillEllipse((New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(99, 102, 241))), 8, 8, 240, 240) +$g.FillEllipse((New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(147, 51, 234))), 40, 40, 176, 176) +$g.FillEllipse([System.Drawing.Brushes]::White, 104, 104, 48, 48) +$ms = New-Object System.IO.MemoryStream +$bmp.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png) +$png = $ms.ToArray() + +$icoStream = New-Object System.IO.MemoryStream +$bw = New-Object System.IO.BinaryWriter $icoStream +$bw.Write([UInt16]0); $bw.Write([UInt16]1); $bw.Write([UInt16]1) # ICONDIR +$bw.Write([Byte]0); $bw.Write([Byte]0); $bw.Write([Byte]0); $bw.Write([Byte]0) # 256x256, colors, res +$bw.Write([UInt16]1); $bw.Write([UInt16]32) # planes, bpp +$bw.Write([UInt32]$png.Length); $bw.Write([UInt32]22) # size, offset +$bw.Write($png); $bw.Flush() +[System.IO.File]::WriteAllBytes("$PSScriptRoot\rippy.ico", $icoStream.ToArray()) +Write-Host "rippy.ico erzeugt ($($png.Length) Bytes PNG)" + +# 2. ps2exe sicherstellen — TLS 1.2 ist der Fix fuer den NuGet-Bootstrap-Fehler +[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072 +if (-not (Get-Module -ListAvailable ps2exe)) { + Write-Host "Bootstrappe NuGet + PSGallery ..." + try { Get-PackageProvider -Name NuGet -ErrorAction Stop | Out-Null } + catch { Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser | Out-Null } + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction SilentlyContinue + Write-Host "Installiere ps2exe ..." + Install-Module ps2exe -Scope CurrentUser -Force -AllowClobber +} +Import-Module ps2exe + +# 3. Kompilieren (WinForms -> noConsole + STA, Icon eingebettet) +Invoke-ps2exe -inputFile $guiSrc -outputFile "$PSScriptRoot\RippyWorkerSetup.exe" ` + -iconFile "$PSScriptRoot\rippy.ico" -noConsole -STA ` + -title "Rippy Worker Setup" -product "Rippy" -company "Rippy" -version "1.0.0.0" + +if (Test-Path "$PSScriptRoot\RippyWorkerSetup.exe") { + Write-Host "OK: RippyWorkerSetup.exe = $((Get-Item "$PSScriptRoot\RippyWorkerSetup.exe").Length) Bytes" +} else { + Write-Host "FEHLER: keine .exe erzeugt" +} diff --git a/deploy/worker-windows/rippy.ico b/deploy/worker-windows/rippy.ico new file mode 100644 index 0000000..9e748d2 Binary files /dev/null and b/deploy/worker-windows/rippy.ico differ diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index a1e80e9..5f7155f 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -22,5 +22,9 @@ COPY docker/api/ . # die Zielmaschine braucht weder git noch Docker. COPY docker/worker/*.py docker/worker/requirements.txt worker_dist/ COPY deploy/worker-windows/install.ps1 deploy/worker-windows/install-gui.ps1 worker_dist/ +# Vorgebaute .exe (Rippy-Icon, kein Konsolenfenster) — auf Windows via +# deploy/worker-windows/build-exe.ps1 erzeugt (Windows-.exe geht nicht von +# Linux). Nur bei GUI-Aenderungen neu bauen, nicht bei Tool-Updates. +COPY deploy/worker-windows/RippyWorkerSetup.exe worker_dist/ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--app-dir", "/app"] diff --git a/docker/api/main.py b/docker/api/main.py index f0fd9d5..932d25b 100644 --- a/docker/api/main.py +++ b/docker/api/main.py @@ -1091,14 +1091,30 @@ async def worker_setup_windows(): @app.get("/worker-setup/windows-gui") async def worker_setup_windows_gui(): - """Grafischer Windows-Installer (WinForms) — vom .bat-Launcher geladen und - gestartet, damit der Nutzer ein Fenster statt CLI bekommt.""" + """Grafischer Windows-Installer (WinForms, PowerShell-Quelle) — Rückfall + für Fortgeschrittene; der Normalweg ist die .exe unten.""" pfad = "worker_dist/install-gui.ps1" if not os.path.isfile(pfad): raise HTTPException(status_code=404, detail="GUI-Installer nicht im Image — API neu bauen") return FileResponse(pfad, media_type="text/plain", filename="rippy-worker-gui.ps1") +@app.get("/worker-setup/windows-exe") +async def worker_setup_windows_exe(): + """Fertige Windows-Installer-.exe (Rippy-Icon, kein Konsolenfenster). + + Vorgebaut auf Windows (deploy/worker-windows/build-exe.ps1) — eine + Windows-.exe lässt sich nicht auf Linux bauen. Generisch: die GUI fragt + die Rippy-Adresse selbst ab, HandBrake + Worker-Code kommen zur Laufzeit.""" + pfad = "worker_dist/RippyWorkerSetup.exe" + if not os.path.isfile(pfad): + raise HTTPException(status_code=404, detail="Installer-.exe nicht im Image — API neu bauen") + return FileResponse( + pfad, media_type="application/vnd.microsoft.portable-executable", + filename="RippyWorkerSetup.exe", + ) + + @app.get("/worker-setup/paket") async def worker_setup_paket(): """Worker-Quellcode als Zip — der Windows-Installer lädt ihn von hier. diff --git a/docker/ui/src/components/WorkerVerwaltung.tsx b/docker/ui/src/components/WorkerVerwaltung.tsx index d1c9e27..2cf85bc 100644 --- a/docker/ui/src/components/WorkerVerwaltung.tsx +++ b/docker/ui/src/components/WorkerVerwaltung.tsx @@ -91,28 +91,14 @@ export default function WorkerVerwaltung() { }) } - // Grafischer Installer: erzeugt eine .vbs CLIENT-seitig mit der eingetragenen - // LAN-IP (der Browser kennt sie, die API nicht sicher). Doppelklick startet - // via wscript PowerShell KOMPLETT VERSTECKT (kein Konsolenfenster) und lädt - // den WinForms-Installer von Rippy — nur das Installer-Fenster erscheint. - const guiInstallerHerunterladen = () => { - const host = rippyHost.trim() - if (!host) { toast('error', 'Erst die LAN-IP der Rippy-Maschine eintragen'); return } - const ps = `try { Invoke-WebRequest 'http://${host}/api/worker-setup/windows-gui' -OutFile ($env:TEMP + '\\rippy-gui.ps1') -UseBasicParsing; & ($env:TEMP + '\\rippy-gui.ps1') -RippyHost '${host}' } catch { Add-Type -AssemblyName System.Windows.Forms; [void][System.Windows.Forms.MessageBox]::Show($_.Exception.Message, 'Rippy Installer') }` - // VBScript verdoppelt innere Anführungszeichen; Fensterstil 0 = versteckt - const psEscaped = ps.replace(/"/g, '""') - const vbs = [ - "' Rippy Encoding-Worker - grafische Installation (ohne Konsolenfenster)", - 'Set sh = CreateObject("WScript.Shell")', - `sh.Run "powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -STA -Command ""${psEscaped}""", 0, False`, - ].join('\r\n') - const blob = new Blob([vbs], { type: 'application/octet-stream' }) + // Fertige Installer-.exe (Rippy-Icon, kein Konsolenfenster) direkt laden. + // Generisch — die GUI fragt die Rippy-Adresse selbst ab. + const exeHerunterladen = () => { const a = document.createElement('a') - a.href = URL.createObjectURL(blob) - a.download = 'rippy-worker-setup.vbs' + a.href = '/api/worker-setup/windows-exe' + a.download = 'RippyWorkerSetup.exe' document.body.appendChild(a); a.click(); a.remove() - URL.revokeObjectURL(a.href) - toast('success', 'rippy-worker-setup.vbs heruntergeladen — doppelklicken zum Installieren') + toast('success', 'RippyWorkerSetup.exe wird heruntergeladen — doppelklicken zum Installieren') } return ( @@ -224,20 +210,24 @@ export default function WorkerVerwaltung() { ))} - {/* Windows: grafischer Installer als Haupt-Weg (Doppelklick statt CLI) */} + {/* Windows: fertige .exe als Haupt-Weg (Doppelklick, Icon, kein Konsolenfenster) */} {variante === 'windows' && (

- Einfachster Weg: grafischen Installer herunterladen, doppelklicken, - Fenster ausfüllen, „Installieren". Braucht nur Python 3.10+ auf dem PC — kein Docker, kein git. + Einfachster Weg: Installer (.exe) herunterladen, doppelklicken, + im Fenster Adresse + Name eintragen, „Installieren". Kein Konsolenfenster. + Braucht nur Python 3.10+ auf dem PC — kein Docker, kein git.

- +

+ Im Installer-Fenster als Rippy-Adresse eintragen: {rippyHost.trim() || ''} +

- Öffnet direkt das Installer-Fenster — kein Konsolenfenster. Windows warnt beim Doppelklick evtl. - („Windows hat Ihren PC geschützt") → „Weitere Informationen" → „Trotzdem ausführen". Die Datei - lädt den Installer nur von deiner Rippy-Maschine. + Windows warnt beim ersten Start evtl. („Windows hat Ihren PC geschützt", unbekannter Herausgeber) + → „Weitere Informationen" → „Trotzdem ausführen". Die .exe holt Worker-Code und HandBrake nur von + deiner Rippy-Maschine bzw. dem offiziellen HandBrake-Release.

)}