Update-Fix: Installer beendet sich nicht mehr selbst, Neustart in die neue Version

Ursache: Der Installer beendete laufende Playtube-Prozesse per "taskkill /F /T". Der
Updater startet ihn aus Playtube.exe heraus, er ist also ein Kindprozess - solange
Playtube noch beendet wurde, schoss "/T" den Installer selbst mit ab, bevor er etwas
installiert hatte (kein Kopieren, kein Neustart, nur der Download blieb im Temp-Ordner).

- playtube.iss: taskkill ohne /T; Setup wartet per /WAITPID=<PID> (max. 20 s) darauf, dass
  sich die alte App selbst beendet; uebrig gebliebene QtWebEngine-Prozesse aus dem
  Installationsordner werden gezielt nach Pfad beendet
- updater: uebergibt /WAITPID und /LOG (Setup-Protokoll nach %TEMP%); robocopy im
  ZIP-Fallback mit /R:3 /W:2, damit gesperrte Dateien das Update nicht endlos haengen
- mainwindow: hartes Beenden nach 8 s, falls Qt/QtWebEngine beim Beenden haengt
  (sonst wartet der Update-Helfer ewig und die alte Version bleibt laufen)
This commit is contained in:
2026-09-19 20:11:32 +02:00
parent 1acd08934e
commit 5f93be4325
3 changed files with 65 additions and 8 deletions
+40 -6
View File
@@ -98,27 +98,61 @@ begin
Result := ExpandConstant('{param:RELAUNCH|0}') = '1'; Result := ExpandConstant('{param:RELAUNCH|0}') = '1';
end; end;
procedure StopRunningPlaytube(); function PowerShellPath(): String;
begin
Result := ExpandConstant('{sys}\WindowsPowerShell\v1.0\powershell.exe');
end;
procedure WaitForOldInstance();
var
OldPid, ResultCode: Integer;
begin
{ Playtubes Updater startet Setup aus der laufenden App heraus und uebergibt deren
Prozess-ID (/WAITPID=...). Setup wartet, bis sich diese Instanz selbst beendet hat,
statt sie hart abzuschiessen (max. 20 s). Ohne Angabe (Setup per Doppelklick) passiert
hier nichts. }
OldPid := StrToIntDef(ExpandConstant('{param:WAITPID|0}'), 0);
if OldPid > 0 then
Exec(PowerShellPath(),
'-NoProfile -NonInteractive -WindowStyle Hidden -Command "Wait-Process -Id ' + IntToStr(OldPid) + ' -Timeout 20 -ErrorAction SilentlyContinue"',
'', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
procedure StopLeftoverProcesses();
var var
ResultCode: Integer; ResultCode: Integer;
begin begin
{ /T beendet auch die Kindprozesse (QtWebEngine); der Hilfsprozess wird zusaetzlich { WICHTIG: bewusst OHNE "/T" (Prozessbaum mitbeenden). Der Updater startet Setup aus
einzeln beendet, falls er als Waise ueberlebt hat. } Playtube.exe heraus - Setup ist also ein Kindprozess. Solange Playtube noch beendet
Exec(ExpandConstant('{sys}\taskkill.exe'), '/F /T /IM {#AppExeName}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); wird, wuerde "/T" den Installer selbst mit abschiessen, bevor er etwas installiert hat
(so ging frueher ein Update still verloren: kein Kopieren, kein Neustart). }
Exec(ExpandConstant('{sys}\taskkill.exe'), '/F /IM {#AppExeName}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec(ExpandConstant('{sys}\taskkill.exe'), '/F /IM {#AppHelperName}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); Exec(ExpandConstant('{sys}\taskkill.exe'), '/F /IM {#AppHelperName}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
{ QtWebEngine-Kindprozesse, die nach dem Ende von Playtube noch aus dem
Installationsordner laufen, wuerden Dateien sperren - gezielt nach Pfad beenden
(nicht per Namen: QtWebEngineProcess.exe heisst auch bei anderen Qt-Programmen). }
if DirExists(ExpandConstant('{app}')) then
Exec(PowerShellPath(),
'-NoProfile -NonInteractive -WindowStyle Hidden -Command "$d = ''' + ExpandConstant('{app}') + '\''; ' +
'Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -and $_.ExecutablePath.StartsWith($d, [StringComparison]::OrdinalIgnoreCase) } | ' +
'ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"',
'', SW_HIDE, ewWaitUntilTerminated, ResultCode);
{ Dateisperren freigeben lassen, bevor kopiert wird. } { Dateisperren freigeben lassen, bevor kopiert wird. }
Sleep(800); Sleep(800);
end; end;
function PrepareToInstall(var NeedsRestart: Boolean): String; function PrepareToInstall(var NeedsRestart: Boolean): String;
begin begin
StopRunningPlaytube(); WaitForOldInstance();
StopLeftoverProcesses();
Result := ''; Result := '';
end; end;
function InitializeUninstall(): Boolean; function InitializeUninstall(): Boolean;
begin begin
StopRunningPlaytube(); StopLeftoverProcesses();
Result := True; Result := True;
end; end;
+14
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import os import os
import sys import sys
import threading
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -28,6 +29,15 @@ from .updater import UpdateChecker, UpdateInstaller
ASSETS_DIR = Path(__file__).resolve().parent.parent / "assets" ASSETS_DIR = Path(__file__).resolve().parent.parent / "assets"
def _force_exit_after(seconds: float) -> None:
"""Beendet den Prozess nach `seconds` hart (os._exit), falls das normale Beenden bis
dahin noch nicht durch ist. Der Timer laeuft als Daemon-Thread und stoert das
normale, schnelle Beenden nicht."""
timer = threading.Timer(seconds, os._exit, args=(0,))
timer.daemon = True
timer.start()
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
def __init__(self, config: dict[str, Any]): def __init__(self, config: dict[str, Any]):
super().__init__() super().__init__()
@@ -359,6 +369,10 @@ class MainWindow(QMainWindow):
"Installation abgeschlossen. Playtube wird neu gestartet …" "Installation abgeschlossen. Playtube wird neu gestartet …"
) )
self._tray.setToolTip(f"{APP_NAME} – wird neu gestartet …") self._tray.setToolTip(f"{APP_NAME} – wird neu gestartet …")
# Haengt sich Qt/QtWebEngine beim Beenden auf, wuerde der Update-Helfer (bzw.
# Setup, siehe /WAITPID) ewig auf das Ende dieses Prozesses warten - und die
# alte Version bliebe einfach weiterlaufen. Nach 8 s hart beenden.
_force_exit_after(8.0)
self._quit() self._quit()
else: else:
QMessageBox.information( QMessageBox.information(
+11 -2
View File
@@ -466,7 +466,12 @@ rm -rf "{staging}"
packaging/playtube.iss) und startet Playtube dank /RELAUNCH=1 danach neu. Die packaging/playtube.iss) und startet Playtube dank /RELAUNCH=1 danach neu. Die
laufende App beendet sich direkt nach diesem Aufruf (siehe MainWindow), muss laufende App beendet sich direkt nach diesem Aufruf (siehe MainWindow), muss
hier also nicht auf Setup warten. Setup wird abgekoppelt gestartet, damit es das hier also nicht auf Setup warten. Setup wird abgekoppelt gestartet, damit es das
Beenden dieses Prozesses ueberlebt.""" Beenden dieses Prozesses ueberlebt.
/WAITPID=<eigene PID>: Setup wartet, bis sich diese App wirklich beendet hat,
bevor es Dateien tauscht. /LOG (ohne Pfad): Setup schreibt ein Protokoll nach
%TEMP% ("Setup Log ....txt") - so laesst sich ein fehlgeschlagenes Update
nachtraeglich nachvollziehen, obwohl die Installation selbst still laeuft."""
subprocess.Popen( subprocess.Popen(
[ [
str(setup_path), str(setup_path),
@@ -475,6 +480,8 @@ rm -rf "{staging}"
"/NORESTART", "/NORESTART",
"/NOCANCEL", "/NOCANCEL",
"/RELAUNCH=1", "/RELAUNCH=1",
f"/WAITPID={os.getpid()}",
"/LOG",
], ],
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP, creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
close_fds=True, close_fds=True,
@@ -497,7 +504,9 @@ $form.Refresh()
# Ueber Start-Process (statt direktem Aufruf) gestartet und per Polling statt -Wait # Ueber Start-Process (statt direktem Aufruf) gestartet und per Polling statt -Wait
# abgewartet, damit die Fensternachrichtenschleife per DoEvents() weiterlaeuft - # abgewartet, damit die Fensternachrichtenschleife per DoEvents() weiterlaeuft -
# sonst wuerde Windows das Fenster waehrend robocopy als "Keine Rueckmeldung" anzeigen. # sonst wuerde Windows das Fenster waehrend robocopy als "Keine Rueckmeldung" anzeigen.
$roboArgs = @("{source_dir}", "{install_dir}", "/MIR", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP") # /R:3 /W:2 begrenzt die Wiederholungen bei gesperrten Dateien (Standard: praktisch endlos,
# das Update haengt dann sichtbar "fuer immer" im Fortschrittsfenster).
$roboArgs = @("{source_dir}", "{install_dir}", "/MIR", "/R:3", "/W:2", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP")
$roboProc = Start-Process -FilePath "robocopy" -ArgumentList $roboArgs -WindowStyle Hidden -PassThru $roboProc = Start-Process -FilePath "robocopy" -ArgumentList $roboArgs -WindowStyle Hidden -PassThru
while (-not $roboProc.HasExited) {{ while (-not $roboProc.HasExited) {{
Start-Sleep -Milliseconds 200 Start-Sleep -Milliseconds 200