"""Stop an existing instance of this application's Windows server.""" import json import os import subprocess from pathlib import Path _CONFIRM_SCRIPT = r''' function Confirm-ServerRestart { Add-Type -AssemblyName System.Windows.Forms $answer = [System.Windows.Forms.MessageBox]::Show( "Es läuft bereits eine Version des Tätigkeitsnachweis-Generators.`n`nSoll das alte Programm beendet und die neue Version gestartet werden?`nDie aktuellen Auswahlen gehen dabei verloren.", 'Tätigkeitsnachweis-Generator', [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question, [System.Windows.Forms.MessageBoxDefaultButton]::Button2 ) return $answer -eq [System.Windows.Forms.DialogResult]::Yes } ''' def stop_existing_server(port: int = 8501) -> bool: """Ask before stopping this app. Return False if the user declines.""" if os.name != "nt": raise RuntimeError("Der kontrollierte Server-Neustart unterstützt nur Windows.") app_path = str(Path(__file__).resolve().with_name("app.py")) # Pass paths through the environment, never interpolate them into shell code. env = dict(os.environ, APP_RESTART_PATH=app_path, APP_RESTART_PID=str(os.getpid()), APP_RESTART_PORT=str(port), APP_RESTART_VENV=str(Path(app_path).parent / ".venv" / "Scripts")) script = _CONFIRM_SCRIPT + r''' [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new() $ErrorActionPreference = 'Stop' try { $port = [int]$env:APP_RESTART_PORT $listeners = @(Get-NetTCPConnection -State Listen -ErrorAction Stop | Where-Object { $_.LocalPort -eq $port }) $processIds = @($listeners | Select-Object -ExpandProperty OwningProcess -Unique) $targets = @() foreach ($processId in $processIds) { if ($processId -eq [int]$env:APP_RESTART_PID) { throw "Der aktuelle Prozess belegt bereits Port $port." } $process = Get-CimInstance Win32_Process -Filter "ProcessId = $processId" if (-not $process) { continue } $isExe = $process.Name -eq 'Tätigkeitsnachweis-Generator.exe' $command = $process.CommandLine $appArgument = '(?i)\bstreamlit(?:\.exe)?"?\s+run\s+"?' + [regex]::Escape($env:APP_RESTART_PATH) + '"?(?:\s|$)' $isDev = $process.Name -match '^(python(?:w)?(?:\d+(?:\.\d+)*)?(?:w)?|streamlit)\.exe$' -and $command -match '(?i)\bstreamlit(?:\.exe)?"?\s+run\s+' -and ($command -match $appArgument -or ($command -match '(?i)\bstreamlit(?:\.exe)?"?\s+run\s+"?app\.py"?(?:\s|$)' -and [IO.Path]::GetDirectoryName($process.ExecutablePath) -eq $env:APP_RESTART_VENV)) if (-not ($isExe -or $isDev)) { throw "Port $port wird von einem anderen Programm verwendet (PID $processId, $($process.Name)). Bitte manuell prüfen." } $targets += $processId } if ($targets.Count -gt 0 -and -not (Confirm-ServerRestart)) { exit 2 } foreach ($processId in $targets) { Stop-Process -Id $processId -Force -ErrorAction Stop } $deadline = (Get-Date).AddSeconds(10) do { $remaining = @(Get-NetTCPConnection -State Listen -ErrorAction Stop | Where-Object { $_.LocalPort -eq $port }) if ($remaining.Count -eq 0) { break } Start-Sleep -Milliseconds 200 } while ((Get-Date) -lt $deadline) if ($remaining.Count -gt 0) { throw "Port $port wurde nicht rechtzeitig freigegeben." } ConvertTo-Json -Compress -InputObject @($targets) } catch { [Console]::Error.WriteLine($_.Exception.Message) exit 1 } ''' result = subprocess.run( ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", script], env=env, capture_output=True, text=True, encoding="utf-8", errors="replace", ) if result.returncode == 2: return False if result.returncode: raise RuntimeError((result.stderr or "").strip() or "Der bestehende Server konnte nicht beendet werden.") stopped = json.loads(result.stdout.strip() or "[]") if stopped: print(f"[INFO] Vorherige Instanz beendet (PID: {stopped}).", flush=True) return True if __name__ == "__main__": try: if not stop_existing_server(): print("[INFO] Die bisherige Version bleibt geöffnet. Keine neue Instanz gestartet.") raise SystemExit(2) except (RuntimeError, OSError, subprocess.SubprocessError) as exc: print(f"[FEHLER] {exc}") raise SystemExit(1)