From 65edf8369ee3915d1ed3f819d3755dd94d77a578 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sat, 25 Jul 2026 22:20:50 +0200 Subject: [PATCH] feat(worker-windows): Installation nach "Programme", Zielordner frei waehlbar Commander-Wunsch: nicht ins Benutzerprofil, sondern wie jedes andere Programm nach C:\Program Files - und selbst waehlbar, mit diesem Standard. ## Was neu ist - Feld "Installieren nach" mit Knopf "Aendern ..." (Ordner-Dialog). Standard ist [Environment]::GetFolderPath("ProgramFiles") + "Rippy Worker" - der ECHTE Pfad; deutsche Windows-Versionen zeigen im Explorer "Programme", der Pfad heisst trotzdem "Program Files". - Der Ordner-Dialog liefert den ELTERN-Ordner, "Rippy Worker" haengt der Installer selbst an. Sonst koennte die Deinstallation einen fremden Ordner mitloeschen - sie macht Remove-Item -Recurse auf das Zielverzeichnis. - install.ps1 bekommt -InstallDir mit demselben Standard. ## Drei Folgen, die mitbehandelt werden mussten 1. RECHTE. In C:\Program Files darf nur ein Administrator schreiben. Die .exe traegt jetzt ein requireAdministrator-Manifest (-requireAdmin in build-exe.ps1), fragt also beim Doppelklick EINMAL per UAC - danach stimmen die Rechte fuer alles Weitere. Verifiziert: "requireAdministrator" steckt im Manifest, "asInvoker" nicht mehr. Zusaetzlich prueft der Installer VOR dem Entpacken, ob er dort schreiben darf, und nennt bei Nein die zwei Wege (als Administrator starten ODER Ziel ins eigene Profil legen) - statt irgendwo tief "Zugriff verweigert" zu produzieren. 2. DAS LOG. tray.py schrieb worker.log NEBEN das Programm. Unter Program Files darf ein normaler Benutzer das nicht - der Worker waere beim Starten gescheitert, und zwar still. Das Log liegt jetzt in %LOCALAPPDATA%\Rippy Worker\worker.log, also da, wo veraenderliche Daten hingehoeren. Rueckfall auf das Programmverzeichnis bleibt fuer Installationen ins eigene Profil. 3. DER AUTOSTART. Bei einer Installation unter Programme ist es eine Installation FUER DIE MASCHINE - der Autostart geht deshalb in den Autostart-Ordner aller Benutzer (CommonStartup). Das loest zugleich ein Rechte-Problem: erhoeht man ueber ein FREMDES Administratorkonto, waere GetFolderPath("Startup") der Ordner dieses Admins, also der falsche. Bei einem Ziel im eigenen Profil bleibt es persoenlich. uninstall.ps1 raeumt beide Orte ab. ## Geprueft - Leerzeichen im Pfad: die erzeugte .bat mit cd /d "%~dp0", PATH-Erweiterung und relativem Aufruf laeuft aus "C:\...\Rippy Test Ordner" korrekt durch. - Alle drei .ps1 ASCII-rein und fehlerfrei geparst. - RippyWorkerSetup.exe neu gebaut (52736 -> 57344 Bytes, Version 1.0.1.0). UI: Der Copy-Paste-Befehl fuer den CLI-Weg sagt jetzt, dass PowerShell als Administrator laufen muss, und nennt -InstallDir als Ausweg ohne Adminrechte. Co-Authored-By: Claude Opus 5 --- deploy/worker-windows/RippyWorkerSetup.exe | Bin 52736 -> 57344 bytes deploy/worker-windows/build-exe.ps1 | 13 +- deploy/worker-windows/install-gui.ps1 | 632 ++++++++++-------- deploy/worker-windows/install.ps1 | 47 +- docker/ui/src/components/WorkerVerwaltung.tsx | 5 + docker/worker/tray.py | 29 +- 6 files changed, 447 insertions(+), 279 deletions(-) diff --git a/deploy/worker-windows/RippyWorkerSetup.exe b/deploy/worker-windows/RippyWorkerSetup.exe index 9ac3fba40dac827ecaaa3fb8293e7f9c768cd22e..ab72a3ae6e14e4b2002332fa59c630b8f3f5dcc6 100644 GIT binary patch delta 7067 zcmai3du&_Rc_$qwjpfj85=-_h$2z8Hi;74}q~5YEiION=*3+Ua`6=g;JR)y&d9QqV zY0GjvW=;+>a&-u=GzQ^x-=;hyd?B)B9z4j}?&V&3#pY!v;x5K~NzuV`#kBP(f z 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 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