diff --git a/deploy/worker-windows/RippyWorkerSetup.exe b/deploy/worker-windows/RippyWorkerSetup.exe index 9ac3fba..ab72a3a 100644 Binary files a/deploy/worker-windows/RippyWorkerSetup.exe and b/deploy/worker-windows/RippyWorkerSetup.exe differ diff --git a/deploy/worker-windows/build-exe.ps1 b/deploy/worker-windows/build-exe.ps1 index 2e6da51..bd56370 100644 --- a/deploy/worker-windows/build-exe.ps1 +++ b/deploy/worker-windows/build-exe.ps1 @@ -48,9 +48,18 @@ if (-not (Get-Module -ListAvailable ps2exe)) { Import-Module ps2exe # 3. Kompilieren (WinForms -> noConsole + STA, Icon eingebettet) +# +# -requireAdmin (seit 25.07.2026): Der Worker wird standardmaessig nach +# "C:\Program Files\Rippy Worker" installiert, und dorthin darf nur ein +# Administrator schreiben. Mit dem Schalter tragt die .exe ein Manifest, das +# Windows beim Doppelklick EINMAL per UAC fragen laesst - danach stimmen die +# Rechte fuer alles Weitere. Ohne ihn liefe der Installer unerhoeht und +# scheiterte beim Anlegen des Ordners; wer das Ziel ins eigene Profil legt, +# braucht die Erhoehung nicht, wird aber trotzdem gefragt (normal fuer einen +# Installer und besser als ein Fehlschlag mitten im Lauf). 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" + -iconFile "$PSScriptRoot\rippy.ico" -noConsole -STA -requireAdmin ` + -title "Rippy Worker Setup" -product "Rippy" -company "Rippy" -version "1.0.1.0" if (Test-Path "$PSScriptRoot\RippyWorkerSetup.exe") { Write-Host "OK: RippyWorkerSetup.exe = $((Get-Item "$PSScriptRoot\RippyWorkerSetup.exe").Length) Bytes" diff --git a/deploy/worker-windows/install-gui.ps1 b/deploy/worker-windows/install-gui.ps1 index 17df0f9..30d799d 100644 --- a/deploy/worker-windows/install-gui.ps1 +++ b/deploy/worker-windows/install-gui.ps1 @@ -1,268 +1,364 @@ -# Rippy Windows-Worker - GRAFISCHER Installer (WinForms, keine externe Runtime). -# Wird ueber rippy-worker-setup.bat gestartet (Doppelklick). Braucht nur -# Python 3.10+ auf der Maschine - kein Docker, kein git. -# -# ASCII-only (PowerShell 5.1 liest .ps1 als ANSI - Sonderzeichen zerschiessen -# das Skript). - -param( - [string]$RippyHost = "", - [string]$WorkerName = $env:COMPUTERNAME.ToLower() -) - -$ErrorActionPreference = "Stop" -Add-Type -AssemblyName System.Windows.Forms -Add-Type -AssemblyName System.Drawing - -$InstallDir = Join-Path $env:USERPROFILE "rippy-worker" - -# --- Fenster --------------------------------------------------------------- -$form = New-Object System.Windows.Forms.Form -$form.Text = "Rippy Encoding-Worker - Installation" -$form.Size = New-Object System.Drawing.Size(560, 520) -$form.StartPosition = "CenterScreen" -$form.FormBorderStyle = "FixedDialog" -$form.MaximizeBox = $false -$form.BackColor = [System.Drawing.Color]::FromArgb(248, 250, 252) -$form.Font = New-Object System.Drawing.Font("Segoe UI", 9) - -# Fenster-Icon: Rippy-Disc zur Laufzeit zeichnen (Indigo-Kreis + weisses Loch) -# - kein externes .ico noetig. Erscheint in Titelleiste und Taskleiste. -try { - $ibmp = New-Object System.Drawing.Bitmap 32, 32 - $ig = [System.Drawing.Graphics]::FromImage($ibmp) - $ig.SmoothingMode = "AntiAlias" - $ig.FillEllipse((New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(79, 70, 229))), 1, 1, 30, 30) - $ig.FillEllipse([System.Drawing.Brushes]::White, 12, 12, 8, 8) - $form.Icon = [System.Drawing.Icon]::FromHandle($ibmp.GetHicon()) -} catch {} - -$titel = New-Object System.Windows.Forms.Label -$titel.Text = "Rippy Encoding-Worker einrichten" -$titel.Font = New-Object System.Drawing.Font("Segoe UI", 14, [System.Drawing.FontStyle]::Bold) -$titel.ForeColor = [System.Drawing.Color]::FromArgb(79, 70, 229) -$titel.Location = New-Object System.Drawing.Point(24, 20) -$titel.Size = New-Object System.Drawing.Size(500, 30) -$form.Controls.Add($titel) - -$info = New-Object System.Windows.Forms.Label -$info.Text = "Diese Maschine uebernimmt die Video-Kompression fuer Rippy. Gerippt wird weiter auf der Rippy-Hauptmaschine." -$info.Location = New-Object System.Drawing.Point(24, 52) -$info.Size = New-Object System.Drawing.Size(500, 34) -$info.ForeColor = [System.Drawing.Color]::FromArgb(100, 116, 139) -$form.Controls.Add($info) - -# Rippy-Adresse -$lblHost = New-Object System.Windows.Forms.Label -$lblHost.Text = "LAN-IP der Rippy-Maschine (Docker-Host) - NICHT dieser PC:" -$lblHost.Location = New-Object System.Drawing.Point(24, 96) -$lblHost.Size = New-Object System.Drawing.Size(500, 18) -$form.Controls.Add($lblHost) - -$txtHost = New-Object System.Windows.Forms.TextBox -$txtHost.Location = New-Object System.Drawing.Point(24, 116) -$txtHost.Size = New-Object System.Drawing.Size(300, 26) -$txtHost.Text = $RippyHost -$txtHost.Font = New-Object System.Drawing.Font("Consolas", 10) -$form.Controls.Add($txtHost) - -# Worker-Name -$lblName = New-Object System.Windows.Forms.Label -$lblName.Text = "Name dieses Workers (frei waehlbar):" -$lblName.Location = New-Object System.Drawing.Point(24, 152) -$lblName.Size = New-Object System.Drawing.Size(500, 18) -$form.Controls.Add($lblName) - -$txtName = New-Object System.Windows.Forms.TextBox -$txtName.Location = New-Object System.Drawing.Point(24, 172) -$txtName.Size = New-Object System.Drawing.Size(300, 26) -$txtName.Text = $WorkerName -$form.Controls.Add($txtName) - -# Autostart -$chkAuto = New-Object System.Windows.Forms.CheckBox -$chkAuto.Text = "Beim Anmelden automatisch starten (Tray-Symbol)" -$chkAuto.Location = New-Object System.Drawing.Point(24, 206) -$chkAuto.Size = New-Object System.Drawing.Size(400, 22) -$chkAuto.Checked = $true -$form.Controls.Add($chkAuto) - -# Log-Bereich -$log = New-Object System.Windows.Forms.TextBox -$log.Location = New-Object System.Drawing.Point(24, 240) -$log.Size = New-Object System.Drawing.Size(500, 170) -$log.Multiline = $true -$log.ReadOnly = $true -$log.ScrollBars = "Vertical" -$log.BackColor = [System.Drawing.Color]::FromArgb(15, 23, 42) -$log.ForeColor = [System.Drawing.Color]::FromArgb(226, 232, 240) -$log.Font = New-Object System.Drawing.Font("Consolas", 8.5) -$form.Controls.Add($log) - -# Knoepfe -$btnInstall = New-Object System.Windows.Forms.Button -$btnInstall.Text = "Installieren" -$btnInstall.Location = New-Object System.Drawing.Point(24, 424) -$btnInstall.Size = New-Object System.Drawing.Size(150, 40) -$btnInstall.BackColor = [System.Drawing.Color]::FromArgb(79, 70, 229) -$btnInstall.ForeColor = [System.Drawing.Color]::White -$btnInstall.FlatStyle = "Flat" -$btnInstall.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold) -$form.Controls.Add($btnInstall) - -$btnStart = New-Object System.Windows.Forms.Button -$btnStart.Text = "Worker starten" -$btnStart.Location = New-Object System.Drawing.Point(190, 424) -$btnStart.Size = New-Object System.Drawing.Size(150, 40) -$btnStart.BackColor = [System.Drawing.Color]::FromArgb(16, 185, 129) -$btnStart.ForeColor = [System.Drawing.Color]::White -$btnStart.FlatStyle = "Flat" -$btnStart.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold) -$btnStart.Visible = $false -$form.Controls.Add($btnStart) - -function Log($text) { - $log.AppendText($text + "`r`n") - [System.Windows.Forms.Application]::DoEvents() -} - -# --- Installations-Logik --------------------------------------------------- -function Do-Install { - $rHost = $txtHost.Text.Trim() - $wName = $txtName.Text.Trim() - if (-not $rHost) { [System.Windows.Forms.MessageBox]::Show("Bitte die LAN-IP der Rippy-Maschine eintragen.", "Fehlt"); return } - if (-not $wName) { $wName = $env:COMPUTERNAME.ToLower() } - - $btnInstall.Enabled = $false - $log.Clear() - - try { - # 1. Python finden - $python = $null - foreach ($k in @("py -3", "python")) { - try { - $v = Invoke-Expression "$k --version" 2>&1 - if ("$v" -match "Python 3\.(\d+)" -and [int]$Matches[1] -ge 10) { $python = $k; break } - } catch {} - } - if (-not $python) { - Log "FEHLER: Python 3.10+ nicht gefunden." - Log "Installieren mit: winget install Python.Python.3.12" - Log "Danach diesen Installer erneut starten." - $btnInstall.Enabled = $true - return - } - Log "Python gefunden: $python" - - # 2. Ordner + Worker-Code - New-Item -ItemType Directory -Force $InstallDir | Out-Null - Set-Location $InstallDir - $HandBrakeVersion = "1.11.2" - try { - $rel = Invoke-RestMethod "https://api.github.com/repos/HandBrake/HandBrake/releases/latest" -TimeoutSec 15 -Headers @{ "User-Agent" = "rippy-installer" } - if ($rel.tag_name) { $HandBrakeVersion = ([string]$rel.tag_name).TrimStart("v") } - } catch {} - - Log "Lade Worker-Code von $rHost ..." - Invoke-WebRequest "http://$rHost/api/worker-setup/paket" -OutFile worker.zip -UseBasicParsing - Expand-Archive worker.zip -DestinationPath . -Force - Remove-Item worker.zip - - # 3. venv + Abhaengigkeiten - if (-not (Test-Path "venv")) { Log "Lege Python-Umgebung an ..."; Invoke-Expression "$python -m venv venv" } - Log "Installiere Python-Abhaengigkeiten (kann 1-2 min dauern) ..." - & .\venv\Scripts\python.exe -m pip install --quiet --upgrade pip - & .\venv\Scripts\pip.exe install --quiet -r requirements.txt - & .\venv\Scripts\pip.exe install --quiet pystray pillow - Log "Abhaengigkeiten installiert." - - # 4. HandBrakeCLI - if (-not (Test-Path ".\HandBrakeCLI.exe") -and -not (Get-Command HandBrakeCLI.exe -ErrorAction SilentlyContinue)) { - Log "Lade HandBrakeCLI $HandBrakeVersion (offizielles Release) ..." - $url = "https://github.com/HandBrake/HandBrake/releases/download/$HandBrakeVersion/HandBrakeCLI-$HandBrakeVersion-win-x86_64.zip" - Invoke-WebRequest $url -OutFile hb.zip -UseBasicParsing - Expand-Archive hb.zip -DestinationPath . -Force - Remove-Item hb.zip - } - Log "HandBrake bereit." - - # 5. Start-Skripte + Deinstaller erzeugen - $trayBat = "@echo off`r`ncd /d `"%~dp0`"`r`nset REDIS_URL=redis://${rHost}:6379/0`r`nset DATABASE_URL=postgresql://rippy:rippy@${rHost}:5432/rippy`r`nset API_URL=http://${rHost}:8000`r`nset WORKER_NAME=$wName`r`nset RIPPY_TRAY_HOST=$rHost`r`nset PATH=%~dp0;%PATH%`r`nstart `"`" venv\Scripts\pythonw.exe tray.py" - Set-Content -Path "start-tray.bat" -Value $trayBat -Encoding ASCII - - $workBat = "@echo off`r`ncd /d `"%~dp0`"`r`nset REDIS_URL=redis://${rHost}:6379/0`r`nset DATABASE_URL=postgresql://rippy:rippy@${rHost}:5432/rippy`r`nset API_URL=http://${rHost}:8000`r`nset WORKER_NAME=$wName`r`nset PATH=%~dp0;%PATH%`r`nvenv\Scripts\celery.exe -A celery_app worker --loglevel=info -Q transcode --pool=solo -n ${wName}@%%h" - Set-Content -Path "start-worker.bat" -Value $workBat -Encoding ASCII - - $uninstall = @" -param([switch]`$Force) -if (-not `$Force) { if ([System.Windows.Forms.MessageBox]::Show("Rippy-Worker '$wName' deinstallieren?","Rippy",4) -ne "Yes") { exit } } -Add-Type -AssemblyName System.Windows.Forms -Get-CimInstance Win32_Process | Where-Object { `$_.ExecutablePath -like "`$PSScriptRoot*" } | ForEach-Object { Stop-Process -Id `$_.ProcessId -Force -ErrorAction SilentlyContinue } -Start-Sleep 2 -# Autostart-Verknuepfung entfernen (neuer Weg) ... -try { Remove-Item -Force (Join-Path ([Environment]::GetFolderPath("Startup")) "RippyWorker.lnk") -ErrorAction Stop } catch {} -# ... und die geplante Aufgabe aus aelteren Installationen, falls vorhanden. -schtasks /delete /tn "RippyWorker" /f 2>`$null | Out-Null -try { Invoke-RestMethod -Method Delete "http://$rHost/api/workers/$wName" -TimeoutSec 5 | Out-Null } catch {} -Set-Location (Split-Path `$PSScriptRoot -Parent) -Remove-Item -Recurse -Force `$PSScriptRoot -[System.Windows.Forms.MessageBox]::Show("Rippy-Worker deinstalliert.","Rippy") -"@ - Set-Content -Path "uninstall.ps1" -Value $uninstall -Encoding ASCII - - # 6. Autostart ueber den Autostart-ORDNER, nicht ueber schtasks. - # - # Befund 25.07.2026 (auf dem Commander-PC aufgetreten): schtasks /create - # mit /tn "RippyWorker" legt die Aufgabe im WURZELORDNER der - # Aufgabenplanung an, und das verlangt Administratorrechte. Der - # Installer laeuft normal ohne. schtasks schrieb deshalb - # "FEHLER: Zugriff verweigert." nach stderr, und weil oben - # $ErrorActionPreference = "Stop" steht, wurde daraus ein Abbruch der - # GESAMTEN Installation - obwohl alles ausser dem Autostart fertig war. - # Der Nutzer sah nur "FEHLER: FEHLER: Zugriff verweigert" samt dem - # irrefuehrenden Tipp, die IP koenne falsch sein. - # - # Der Autostart-Ordner braucht NIE Adminrechte, und der Nutzer kann die - # Verknuepfung dort selbst sehen und loeschen. Zwei Fliegen. - if ($chkAuto.Checked) { - try { - $autostartOrdner = [Environment]::GetFolderPath("Startup") - $verknuepfung = Join-Path $autostartOrdner "RippyWorker.lnk" - $wsh = New-Object -ComObject WScript.Shell - $lnk = $wsh.CreateShortcut($verknuepfung) - $lnk.TargetPath = (Join-Path $InstallDir "start-tray.bat") - $lnk.WorkingDirectory = $InstallDir - $lnk.Description = "Rippy Encoding-Worker" - $lnk.Save() - Log "Autostart eingerichtet (startet bei der Anmeldung)." - } catch { - # Nur eine Warnung: der Worker selbst ist fertig und startbar. - Log "HINWEIS: Autostart konnte nicht eingerichtet werden ($($_.Exception.Message))." - Log " Der Worker ist trotzdem fertig installiert und laeuft." - Log " Von Hand: start-tray.bat in den Autostart-Ordner verknuepfen" - Log " (Windows-Taste + R, dann shell:startup eingeben)." - } - } - - Log "" - Log "FERTIG! Ordner: $InstallDir" - Log "Klick 'Worker starten' - er erscheint dann in Rippy unter Einstellungen -> Worker." - $btnStart.Visible = $true - $btnInstall.Text = "Neu installieren" - $btnInstall.Enabled = $true - } catch { - Log "" - Log "FEHLER: $_" - Log "Tipp: Ist die IP korrekt und die Rippy-Maschine im Heimnetz erreichbar?" - $btnInstall.Enabled = $true - } -} - -$btnInstall.Add_Click({ Do-Install }) -$btnStart.Add_Click({ - Start-Process -FilePath (Join-Path $InstallDir "start-tray.bat") -WindowStyle Hidden - [System.Windows.Forms.MessageBox]::Show("Worker gestartet - Symbol erscheint unten rechts neben der Uhr.", "Rippy") - $form.Close() -}) - -[void]$form.ShowDialog() +# Rippy Windows-Worker - GRAFISCHER Installer (WinForms, keine externe Runtime). +# Wird ueber rippy-worker-setup.bat gestartet (Doppelklick). Braucht nur +# Python 3.10+ auf der Maschine - kein Docker, kein git. +# +# ASCII-only (PowerShell 5.1 liest .ps1 als ANSI - Sonderzeichen zerschiessen +# das Skript). + +param( + [string]$RippyHost = "", + [string]$WorkerName = $env:COMPUTERNAME.ToLower() +) + +$ErrorActionPreference = "Stop" +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing + +# Standard-Zielverzeichnis: wie jedes andere Programm unter "Programme" +# (Commander-Wunsch 25.07.2026 - vorher lag der Worker im Benutzerprofil, was +# fuer ein Programm unueblich ist). GetFolderPath liefert den ECHTEN Pfad; +# deutsche Windows-Versionen zeigen im Explorer "Programme" an, der Pfad heisst +# trotzdem "C:\Program Files". +# +# Schreiben dorthin braucht Administratorrechte - deshalb ist die .exe mit +# -requireAdmin gebaut (build-exe.ps1) und fragt beim Start einmal per UAC. +# Ohne das waere hier der naechste "Zugriff verweigert" programmiert. +$StandardZiel = Join-Path ([Environment]::GetFolderPath("ProgramFiles")) "Rippy Worker" +$InstallDir = $StandardZiel + +# --- Fenster --------------------------------------------------------------- +$form = New-Object System.Windows.Forms.Form +$form.Text = "Rippy Encoding-Worker - Installation" +$form.Size = New-Object System.Drawing.Size(560, 575) +$form.StartPosition = "CenterScreen" +$form.FormBorderStyle = "FixedDialog" +$form.MaximizeBox = $false +$form.BackColor = [System.Drawing.Color]::FromArgb(248, 250, 252) +$form.Font = New-Object System.Drawing.Font("Segoe UI", 9) + +# Fenster-Icon: Rippy-Disc zur Laufzeit zeichnen (Indigo-Kreis + weisses Loch) +# - kein externes .ico noetig. Erscheint in Titelleiste und Taskleiste. +try { + $ibmp = New-Object System.Drawing.Bitmap 32, 32 + $ig = [System.Drawing.Graphics]::FromImage($ibmp) + $ig.SmoothingMode = "AntiAlias" + $ig.FillEllipse((New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(79, 70, 229))), 1, 1, 30, 30) + $ig.FillEllipse([System.Drawing.Brushes]::White, 12, 12, 8, 8) + $form.Icon = [System.Drawing.Icon]::FromHandle($ibmp.GetHicon()) +} catch {} + +$titel = New-Object System.Windows.Forms.Label +$titel.Text = "Rippy Encoding-Worker einrichten" +$titel.Font = New-Object System.Drawing.Font("Segoe UI", 14, [System.Drawing.FontStyle]::Bold) +$titel.ForeColor = [System.Drawing.Color]::FromArgb(79, 70, 229) +$titel.Location = New-Object System.Drawing.Point(24, 20) +$titel.Size = New-Object System.Drawing.Size(500, 30) +$form.Controls.Add($titel) + +$info = New-Object System.Windows.Forms.Label +$info.Text = "Diese Maschine uebernimmt die Video-Kompression fuer Rippy. Gerippt wird weiter auf der Rippy-Hauptmaschine." +$info.Location = New-Object System.Drawing.Point(24, 52) +$info.Size = New-Object System.Drawing.Size(500, 34) +$info.ForeColor = [System.Drawing.Color]::FromArgb(100, 116, 139) +$form.Controls.Add($info) + +# Rippy-Adresse +$lblHost = New-Object System.Windows.Forms.Label +$lblHost.Text = "LAN-IP der Rippy-Maschine (Docker-Host) - NICHT dieser PC:" +$lblHost.Location = New-Object System.Drawing.Point(24, 96) +$lblHost.Size = New-Object System.Drawing.Size(500, 18) +$form.Controls.Add($lblHost) + +$txtHost = New-Object System.Windows.Forms.TextBox +$txtHost.Location = New-Object System.Drawing.Point(24, 116) +$txtHost.Size = New-Object System.Drawing.Size(300, 26) +$txtHost.Text = $RippyHost +$txtHost.Font = New-Object System.Drawing.Font("Consolas", 10) +$form.Controls.Add($txtHost) + +# Worker-Name +$lblName = New-Object System.Windows.Forms.Label +$lblName.Text = "Name dieses Workers (frei waehlbar):" +$lblName.Location = New-Object System.Drawing.Point(24, 152) +$lblName.Size = New-Object System.Drawing.Size(500, 18) +$form.Controls.Add($lblName) + +$txtName = New-Object System.Windows.Forms.TextBox +$txtName.Location = New-Object System.Drawing.Point(24, 172) +$txtName.Size = New-Object System.Drawing.Size(300, 26) +$txtName.Text = $WorkerName +$form.Controls.Add($txtName) + +# Zielverzeichnis - frei waehlbar, Standard "Programme" +$lblPfad = New-Object System.Windows.Forms.Label +$lblPfad.Text = "Installieren nach:" +$lblPfad.Location = New-Object System.Drawing.Point(24, 206) +$lblPfad.Size = New-Object System.Drawing.Size(500, 18) +$form.Controls.Add($lblPfad) + +$txtPfad = New-Object System.Windows.Forms.TextBox +$txtPfad.Location = New-Object System.Drawing.Point(24, 226) +$txtPfad.Size = New-Object System.Drawing.Size(390, 26) +$txtPfad.Text = $StandardZiel +$txtPfad.Font = New-Object System.Drawing.Font("Consolas", 9) +$form.Controls.Add($txtPfad) + +$btnPfad = New-Object System.Windows.Forms.Button +$btnPfad.Text = "Aendern ..." +$btnPfad.Location = New-Object System.Drawing.Point(424, 225) +$btnPfad.Size = New-Object System.Drawing.Size(100, 28) +$btnPfad.FlatStyle = "Flat" +$form.Controls.Add($btnPfad) +$btnPfad.Add_Click({ + $dlg = New-Object System.Windows.Forms.FolderBrowserDialog + $dlg.Description = "Ordner waehlen - darin wird 'Rippy Worker' angelegt" + $dlg.ShowNewFolderButton = $true + if ($dlg.ShowDialog() -eq "OK") { + # Der Dialog liefert den ELTERN-Ordner; der Unterordner kommt von uns, + # damit die Deinstallation nie einen fremden Ordner mitloescht. + $txtPfad.Text = (Join-Path $dlg.SelectedPath "Rippy Worker") + } +}) + +# Autostart +$chkAuto = New-Object System.Windows.Forms.CheckBox +$chkAuto.Text = "Beim Anmelden automatisch starten (Tray-Symbol)" +$chkAuto.Location = New-Object System.Drawing.Point(24, 264) +$chkAuto.Size = New-Object System.Drawing.Size(400, 22) +$chkAuto.Checked = $true +$form.Controls.Add($chkAuto) + +# Log-Bereich +$log = New-Object System.Windows.Forms.TextBox +$log.Location = New-Object System.Drawing.Point(24, 296) +$log.Size = New-Object System.Drawing.Size(500, 160) +$log.Multiline = $true +$log.ReadOnly = $true +$log.ScrollBars = "Vertical" +$log.BackColor = [System.Drawing.Color]::FromArgb(15, 23, 42) +$log.ForeColor = [System.Drawing.Color]::FromArgb(226, 232, 240) +$log.Font = New-Object System.Drawing.Font("Consolas", 8.5) +$form.Controls.Add($log) + +# Knoepfe +$btnInstall = New-Object System.Windows.Forms.Button +$btnInstall.Text = "Installieren" +$btnInstall.Location = New-Object System.Drawing.Point(24, 470) +$btnInstall.Size = New-Object System.Drawing.Size(150, 40) +$btnInstall.BackColor = [System.Drawing.Color]::FromArgb(79, 70, 229) +$btnInstall.ForeColor = [System.Drawing.Color]::White +$btnInstall.FlatStyle = "Flat" +$btnInstall.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold) +$form.Controls.Add($btnInstall) + +$btnStart = New-Object System.Windows.Forms.Button +$btnStart.Text = "Worker starten" +$btnStart.Location = New-Object System.Drawing.Point(190, 470) +$btnStart.Size = New-Object System.Drawing.Size(150, 40) +$btnStart.BackColor = [System.Drawing.Color]::FromArgb(16, 185, 129) +$btnStart.ForeColor = [System.Drawing.Color]::White +$btnStart.FlatStyle = "Flat" +$btnStart.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold) +$btnStart.Visible = $false +$form.Controls.Add($btnStart) + +function Log($text) { + $log.AppendText($text + "`r`n") + [System.Windows.Forms.Application]::DoEvents() +} + +# --- Installations-Logik --------------------------------------------------- +function Do-Install { + $rHost = $txtHost.Text.Trim() + $wName = $txtName.Text.Trim() + if (-not $rHost) { [System.Windows.Forms.MessageBox]::Show("Bitte die LAN-IP der Rippy-Maschine eintragen.", "Fehlt"); return } + if (-not $wName) { $wName = $env:COMPUTERNAME.ToLower() } + + # Zielverzeichnis aus dem Feld. script:-Bereich, weil "Worker starten" + # und die Abschlussmeldung denselben Pfad brauchen. + $gewaehlt = $txtPfad.Text.Trim() + if (-not $gewaehlt) { $gewaehlt = $StandardZiel } + if (-not [System.IO.Path]::IsPathRooted($gewaehlt)) { + [System.Windows.Forms.MessageBox]::Show( + "Bitte einen vollstaendigen Pfad angeben, z. B. C:\Program Files\Rippy Worker.", + "Pfad unvollstaendig") + return + } + $script:InstallDir = $gewaehlt + + $btnInstall.Enabled = $false + $log.Clear() + Log "Ziel: $InstallDir" + + try { + # 1. Python finden + $python = $null + foreach ($k in @("py -3", "python")) { + try { + $v = Invoke-Expression "$k --version" 2>&1 + if ("$v" -match "Python 3\.(\d+)" -and [int]$Matches[1] -ge 10) { $python = $k; break } + } catch {} + } + if (-not $python) { + Log "FEHLER: Python 3.10+ nicht gefunden." + Log "Installieren mit: winget install Python.Python.3.12" + Log "Danach diesen Installer erneut starten." + $btnInstall.Enabled = $true + return + } + Log "Python gefunden: $python" + + # 2. Ordner + Worker-Code + # Erst PRUEFEN, ob wir dort ueberhaupt schreiben duerfen. Die .exe ist mit + # -requireAdmin gebaut und fragt per UAC, aber wer install-gui.ps1 direkt + # startet, hat die Rechte nicht - dann soll hier ein brauchbarer Satz + # stehen und nicht "Zugriff verweigert" aus irgendeiner Tiefe. + try { + New-Item -ItemType Directory -Force $InstallDir -ErrorAction Stop | Out-Null + $probe = Join-Path $InstallDir ".schreibtest" + Set-Content -Path $probe -Value "x" -ErrorAction Stop + Remove-Item -Force $probe -ErrorAction SilentlyContinue + } catch { + Log "FEHLER: In '$InstallDir' darf nicht geschrieben werden." + Log "" + Log "Das ist normal bei Ordnern unter 'Programme' - dafuer braucht es" + Log "Administratorrechte. Zwei Wege:" + Log " 1. Diesen Installer mit Rechtsklick > 'Als Administrator" + Log " ausfuehren' starten (der empfohlene Weg), ODER" + Log " 2. oben ueber 'Aendern ...' einen Ordner im eigenen" + Log " Benutzerprofil waehlen - dort geht es ohne Adminrechte." + $btnInstall.Enabled = $true + return + } + Set-Location $InstallDir + $HandBrakeVersion = "1.11.2" + try { + $rel = Invoke-RestMethod "https://api.github.com/repos/HandBrake/HandBrake/releases/latest" -TimeoutSec 15 -Headers @{ "User-Agent" = "rippy-installer" } + if ($rel.tag_name) { $HandBrakeVersion = ([string]$rel.tag_name).TrimStart("v") } + } catch {} + + Log "Lade Worker-Code von $rHost ..." + Invoke-WebRequest "http://$rHost/api/worker-setup/paket" -OutFile worker.zip -UseBasicParsing + Expand-Archive worker.zip -DestinationPath . -Force + Remove-Item worker.zip + + # 3. venv + Abhaengigkeiten + if (-not (Test-Path "venv")) { Log "Lege Python-Umgebung an ..."; Invoke-Expression "$python -m venv venv" } + Log "Installiere Python-Abhaengigkeiten (kann 1-2 min dauern) ..." + & .\venv\Scripts\python.exe -m pip install --quiet --upgrade pip + & .\venv\Scripts\pip.exe install --quiet -r requirements.txt + & .\venv\Scripts\pip.exe install --quiet pystray pillow + Log "Abhaengigkeiten installiert." + + # 4. HandBrakeCLI + if (-not (Test-Path ".\HandBrakeCLI.exe") -and -not (Get-Command HandBrakeCLI.exe -ErrorAction SilentlyContinue)) { + Log "Lade HandBrakeCLI $HandBrakeVersion (offizielles Release) ..." + $url = "https://github.com/HandBrake/HandBrake/releases/download/$HandBrakeVersion/HandBrakeCLI-$HandBrakeVersion-win-x86_64.zip" + Invoke-WebRequest $url -OutFile hb.zip -UseBasicParsing + Expand-Archive hb.zip -DestinationPath . -Force + Remove-Item hb.zip + } + Log "HandBrake bereit." + + # 5. Start-Skripte + Deinstaller erzeugen + $trayBat = "@echo off`r`ncd /d `"%~dp0`"`r`nset REDIS_URL=redis://${rHost}:6379/0`r`nset DATABASE_URL=postgresql://rippy:rippy@${rHost}:5432/rippy`r`nset API_URL=http://${rHost}:8000`r`nset WORKER_NAME=$wName`r`nset RIPPY_TRAY_HOST=$rHost`r`nset PATH=%~dp0;%PATH%`r`nstart `"`" venv\Scripts\pythonw.exe tray.py" + Set-Content -Path "start-tray.bat" -Value $trayBat -Encoding ASCII + + $workBat = "@echo off`r`ncd /d `"%~dp0`"`r`nset REDIS_URL=redis://${rHost}:6379/0`r`nset DATABASE_URL=postgresql://rippy:rippy@${rHost}:5432/rippy`r`nset API_URL=http://${rHost}:8000`r`nset WORKER_NAME=$wName`r`nset PATH=%~dp0;%PATH%`r`nvenv\Scripts\celery.exe -A celery_app worker --loglevel=info -Q transcode --pool=solo -n ${wName}@%%h" + Set-Content -Path "start-worker.bat" -Value $workBat -Encoding ASCII + + $uninstall = @" +param([switch]`$Force) +if (-not `$Force) { if ([System.Windows.Forms.MessageBox]::Show("Rippy-Worker '$wName' deinstallieren?","Rippy",4) -ne "Yes") { exit } } +Add-Type -AssemblyName System.Windows.Forms +Get-CimInstance Win32_Process | Where-Object { `$_.ExecutablePath -like "`$PSScriptRoot*" } | ForEach-Object { Stop-Process -Id `$_.ProcessId -Force -ErrorAction SilentlyContinue } +Start-Sleep 2 +# Autostart-Verknuepfung entfernen - BEIDE moeglichen Orte, weil die +# Installation je nach Zielverzeichnis persoenlich oder maschinenweit war. +foreach (`$ordner in @("Startup", "CommonStartup")) { + try { Remove-Item -Force (Join-Path ([Environment]::GetFolderPath(`$ordner)) "RippyWorker.lnk") -ErrorAction Stop } catch {} +} +# ... und die geplante Aufgabe aus aelteren Installationen, falls vorhanden. +schtasks /delete /tn "RippyWorker" /f 2>`$null | Out-Null +try { Invoke-RestMethod -Method Delete "http://$rHost/api/workers/$wName" -TimeoutSec 5 | Out-Null } catch {} +Set-Location (Split-Path `$PSScriptRoot -Parent) +Remove-Item -Recurse -Force `$PSScriptRoot +[System.Windows.Forms.MessageBox]::Show("Rippy-Worker deinstalliert.","Rippy") +"@ + Set-Content -Path "uninstall.ps1" -Value $uninstall -Encoding ASCII + + # 6. Autostart ueber den Autostart-ORDNER, nicht ueber schtasks. + # + # Befund 25.07.2026 (auf dem Commander-PC aufgetreten): schtasks /create + # mit /tn "RippyWorker" legt die Aufgabe im WURZELORDNER der + # Aufgabenplanung an, und das verlangt Administratorrechte. Der + # Installer laeuft normal ohne. schtasks schrieb deshalb + # "FEHLER: Zugriff verweigert." nach stderr, und weil oben + # $ErrorActionPreference = "Stop" steht, wurde daraus ein Abbruch der + # GESAMTEN Installation - obwohl alles ausser dem Autostart fertig war. + # Der Nutzer sah nur "FEHLER: FEHLER: Zugriff verweigert" samt dem + # irrefuehrenden Tipp, die IP koenne falsch sein. + # + # Der Autostart-Ordner braucht NIE Adminrechte, und der Nutzer kann die + # Verknuepfung dort selbst sehen und loeschen. Zwei Fliegen. + if ($chkAuto.Checked) { + try { + # Liegt das Programm unter "Programme", ist es eine Installation + # fuer die ganze Maschine - dann gehoert der Autostart in den + # Autostart-Ordner ALLER Benutzer. Das passt auch zur Rechtelage: + # der Installer laeuft dafuer ohnehin erhoeht, und bei einer + # Erhoehung ueber ein FREMDES Administratorkonto waere der + # benutzereigene Ordner der des Admins - also der falsche. + # Bei einer Installation ins eigene Profil bleibt es persoenlich. + $programme = [Environment]::GetFolderPath("ProgramFiles") + $programmeX86 = [Environment]::GetFolderPath("ProgramFilesX86") + $fuerAlle = $InstallDir.StartsWith($programme, "OrdinalIgnoreCase") ` + -or ($programmeX86 -and $InstallDir.StartsWith($programmeX86, "OrdinalIgnoreCase")) + $autostartOrdner = if ($fuerAlle) { + [Environment]::GetFolderPath("CommonStartup") + } else { + [Environment]::GetFolderPath("Startup") + } + $verknuepfung = Join-Path $autostartOrdner "RippyWorker.lnk" + $wsh = New-Object -ComObject WScript.Shell + $lnk = $wsh.CreateShortcut($verknuepfung) + $lnk.TargetPath = (Join-Path $InstallDir "start-tray.bat") + $lnk.WorkingDirectory = $InstallDir + $lnk.Description = "Rippy Encoding-Worker" + $lnk.Save() + if ($fuerAlle) { + Log "Autostart eingerichtet (fuer alle Benutzer dieser Maschine)." + } else { + Log "Autostart eingerichtet (fuer dich, startet bei der Anmeldung)." + } + } catch { + # Nur eine Warnung: der Worker selbst ist fertig und startbar. + Log "HINWEIS: Autostart konnte nicht eingerichtet werden ($($_.Exception.Message))." + Log " Der Worker ist trotzdem fertig installiert und laeuft." + Log " Von Hand: start-tray.bat in den Autostart-Ordner verknuepfen" + Log " (Windows-Taste + R, dann shell:startup eingeben)." + } + } + + Log "" + Log "FERTIG! Ordner: $InstallDir" + Log "Klick 'Worker starten' - er erscheint dann in Rippy unter Einstellungen -> Worker." + $btnStart.Visible = $true + $btnInstall.Text = "Neu installieren" + $btnInstall.Enabled = $true + } catch { + Log "" + Log "FEHLER: $_" + Log "Tipp: Ist die IP korrekt und die Rippy-Maschine im Heimnetz erreichbar?" + $btnInstall.Enabled = $true + } +} + +$btnInstall.Add_Click({ Do-Install }) +$btnStart.Add_Click({ + Start-Process -FilePath (Join-Path $InstallDir "start-tray.bat") -WindowStyle Hidden + [System.Windows.Forms.MessageBox]::Show("Worker gestartet - Symbol erscheint unten rechts neben der Uhr.", "Rippy") + $form.Close() +}) + +[void]$form.ShowDialog() diff --git a/deploy/worker-windows/install.ps1 b/deploy/worker-windows/install.ps1 index a058cc2..64531d9 100644 --- a/deploy/worker-windows/install.ps1 +++ b/deploy/worker-windows/install.ps1 @@ -1,6 +1,6 @@ # Rippy: Nativer Windows-Transcode-Worker - Installation OHNE Docker. # -# Was das Skript tut (alles in einen Unterordner "rippy-worker"): +# Was das Skript tut (alles nach -InstallDir, Standard "C:\Program Files\Rippy Worker"): # 1. Prueft Python (3.10+), legt ein venv an, installiert die Abhaengigkeiten # 2. Laedt den Worker-Code direkt von deiner Rippy-Instanz (/api/worker-setup/paket) # 3. Laedt HandBrakeCLI (gepinnte Version, offizielles GitHub-Release), @@ -18,7 +18,12 @@ param( [Parameter(Mandatory = $true)] [string]$RippyHost, [string]$WorkerName = $env:COMPUTERNAME.ToLower(), - [switch]$Autostart + [switch]$Autostart, + # Zielverzeichnis. Standard ist "Programme", wie bei jedem anderen Programm + # (Commander-Wunsch 25.07.2026). Dorthin schreiben braucht Adminrechte - + # PowerShell also "Als Administrator" starten, oder hier einen Ordner im + # eigenen Profil angeben. + [string]$InstallDir = (Join-Path ([Environment]::GetFolderPath("ProgramFiles")) "Rippy Worker") ) $ErrorActionPreference = "Stop" @@ -54,9 +59,23 @@ if (-not $python) { } Write-Host "Python gefunden: $python" -# 2. Arbeitsordner + Worker-Code von der Rippy-Instanz laden -$ziel = Join-Path (Get-Location) "rippy-worker" -New-Item -ItemType Directory -Force $ziel | Out-Null +# 2. Zielordner + Worker-Code von der Rippy-Instanz laden +$ziel = $InstallDir +try { + New-Item -ItemType Directory -Force $ziel -ErrorAction Stop | Out-Null + $probe = Join-Path $ziel ".schreibtest" + Set-Content -Path $probe -Value "x" -ErrorAction Stop + Remove-Item -Force $probe -ErrorAction SilentlyContinue +} catch { + Write-Host "" + Write-Host "FEHLER: In '$ziel' darf nicht geschrieben werden." -ForegroundColor Red + Write-Host "Das ist normal bei Ordnern unter 'Programme'. Zwei Wege:" -ForegroundColor Red + Write-Host " 1. PowerShell als Administrator starten und erneut aufrufen, ODER" -ForegroundColor Red + Write-Host " 2. ein eigenes Ziel angeben, z. B.:" -ForegroundColor Red + Write-Host " -InstallDir `"`$env:LOCALAPPDATA\Rippy Worker`"" -ForegroundColor Red + exit 1 +} +Write-Host "Ziel: $ziel" Set-Location $ziel Write-Host "Lade Worker-Code von http://$RippyHost/api/worker-setup/paket ..." @@ -132,8 +151,11 @@ Get-CimInstance Win32_Process | Where-Object { `$_.ExecutablePath -like "`$PSScriptRoot*" } | ForEach-Object { Stop-Process -Id `$_.ProcessId -Force -ErrorAction SilentlyContinue } Start-Sleep 2 -# Autostart-Verknuepfung entfernen (neuer Weg) ... -try { Remove-Item -Force (Join-Path ([Environment]::GetFolderPath("Startup")) "RippyWorker.lnk") -ErrorAction Stop } catch {} +# Autostart-Verknuepfung entfernen - BEIDE moeglichen Orte, weil die +# Installation je nach Zielverzeichnis persoenlich oder maschinenweit war. +foreach (`$ordner in @("Startup", "CommonStartup")) { + try { Remove-Item -Force (Join-Path ([Environment]::GetFolderPath(`$ordner)) "RippyWorker.lnk") -ErrorAction Stop } catch {} +} # ... und die geplante Aufgabe aus aelteren Installationen, falls vorhanden. schtasks /delete /tn "RippyWorker" /f 2>`$null | Out-Null try { @@ -157,7 +179,16 @@ Write-Host "uninstall.ps1 erzeugt." # obwohl nur der Autostart fehlte. Der Autostart-Ordner braucht nie Adminrechte. if ($Autostart) { try { - $autostartOrdner = [Environment]::GetFolderPath("Startup") + # Unter "Programme" = Installation fuer die Maschine, also Autostart fuer + # alle Benutzer. Sonst persoenlich. Siehe install-gui.ps1 fuer die + # ausfuehrliche Begruendung (Rechtelage bei fremdem Admin-Konto). + $programme = [Environment]::GetFolderPath("ProgramFiles") + $fuerAlle = $ziel.StartsWith($programme, "OrdinalIgnoreCase") + $autostartOrdner = if ($fuerAlle) { + [Environment]::GetFolderPath("CommonStartup") + } else { + [Environment]::GetFolderPath("Startup") + } $wsh = New-Object -ComObject WScript.Shell $lnk = $wsh.CreateShortcut((Join-Path $autostartOrdner "RippyWorker.lnk")) $lnk.TargetPath = (Join-Path $ziel "start-tray.bat") diff --git a/docker/ui/src/components/WorkerVerwaltung.tsx b/docker/ui/src/components/WorkerVerwaltung.tsx index 8f31c99..e878a54 100644 --- a/docker/ui/src/components/WorkerVerwaltung.tsx +++ b/docker/ui/src/components/WorkerVerwaltung.tsx @@ -86,9 +86,14 @@ export default function WorkerVerwaltung() { ` docker compose -f deploy/remote-transcode-worker.yml up -d --build`, ].join('\n') : [ + // Standard-Ziel ist "C:\Program Files\Rippy Worker" - dorthin darf nur + // ein Administrator schreiben, deshalb der Hinweis direkt im Befehl. + // Die .exe fragt selbst per UAC; hier muss die Konsole erhoeht sein. + `# PowerShell als Administrator starten (Standard-Ziel: Programme)`, `iwr http://${hostFuerBefehl}/api/worker-setup/windows -OutFile install-rippy-worker.ps1`, `powershell -ExecutionPolicy Bypass -File .\\install-rippy-worker.ps1 \``, ` -RippyHost ${hostFuerBefehl} -WorkerName mein-pc`, + `# Ohne Adminrechte: -InstallDir "$env:LOCALAPPDATA\\Rippy Worker" anhaengen`, ].join('\n') const kopieren = () => { diff --git a/docker/worker/tray.py b/docker/worker/tray.py index 07d1628..d565fa5 100644 --- a/docker/worker/tray.py +++ b/docker/worker/tray.py @@ -25,7 +25,34 @@ except ImportError: BASIS = os.path.dirname(os.path.abspath(__file__)) RIPPY_HOST = os.getenv("RIPPY_TRAY_HOST", "") WORKER_NAME = os.getenv("WORKER_NAME", "windows-worker") -LOG_PFAD = os.path.join(BASIS, "worker.log") + + +def _log_pfad() -> str: + """Wohin das Worker-Log geschrieben wird. + + NICHT ins Programmverzeichnis (Befund 25.07.2026, Commander-Wunsch): Der + Worker wird jetzt standardmäßig nach `C:\\Program Files\\Rippy Worker` + installiert, und dort darf ein normaler Benutzer nicht schreiben. Ein Log + neben dem Programm hätte den Worker beim Starten scheitern lassen — genau + die Art Fehler, die man erst merkt, wenn nichts passiert. + + Deshalb ins Benutzerprofil (`%LOCALAPPDATA%\\Rippy Worker`), wie es sich für + veränderliche Daten gehört. Nur falls das nicht klappt, bleibt das + Programmverzeichnis der Rückfall — bei einer Installation ins Benutzerprofil + (frei wählbar) ist es ohnehin beschreibbar. + """ + basis_daten = os.getenv("LOCALAPPDATA") or os.getenv("APPDATA") or "" + if basis_daten: + ordner = os.path.join(basis_daten, "Rippy Worker") + try: + os.makedirs(ordner, exist_ok=True) + return os.path.join(ordner, "worker.log") + except OSError: + pass + return os.path.join(BASIS, "worker.log") + + +LOG_PFAD = _log_pfad() prozess = None