Release SyncVito 1.0.13
This commit is contained in:
1 parent
b6e32a1b18
commit
cdcd03d7b0
8 files changed
+284
-20
No files matched your search
@@ -1,5 +1,14 @@
|
|||||||
# Änderungsprotokoll
|
# Änderungsprotokoll
|
||||||
|
|
||||||
|
## 1.0.13 – 2026-09-28
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
|
||||||
|
- **Zuverlässiger Windows-Autostart via Aufgabenplanung:** Da SyncVito mit Administratorrechten (`requireAdministrator`) läuft, blockierte der Windows Explorer beim Systemstart den Aufruf aus dem Registry-`Run`-Schlüssel. Der Autostart wird nun über eine geplante Windows-Aufgabe (`RunLevel=HighestAvailable`) bei Benutzeranmeldung ausgeführt, sodass SyncVito nach dem Hochfahren zuverlässig und ohne UAC-Dialog im Infobereich startet.
|
||||||
|
- **Warteschleife für den Windows-Infobereich:** Falls der Windows Explorer beim Booten den Benachrichtigungsbereich noch nicht initialisiert hat, wartet SyncVito bis zu 15 Sekunden auf den Tray, statt sich sofort mit einem Fehler zu beenden.
|
||||||
|
- **Automatische Autostart-Migration:** Bestehende Konfigurationen mit aktiviertem Autostart werden beim Programmstart automatisch auf die geplante Aufgabe umgestellt; alte Einträge im Run-Schlüssel werden bereinigt.
|
||||||
|
- **Saubere Deinstallation:** Bei einer Deinstallation über Inno Setup wird die registrierte Windows-Aufgabe für den Autostart automatisch entfernt.
|
||||||
|
|
||||||
## 1.0.12 – 2026-09-27
|
## 1.0.12 – 2026-09-27
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
|
|||||||
@@ -55,3 +55,6 @@ Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: de
|
|||||||
[Run]
|
[Run]
|
||||||
Filename: "{app}\{#MyAppExeName}"; Description: "{#MyAppName} starten"; Flags: nowait postinstall skipifsilent runascurrentuser
|
Filename: "{app}\{#MyAppExeName}"; Description: "{#MyAppName} starten"; Flags: nowait postinstall skipifsilent runascurrentuser
|
||||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--updated"; Flags: nowait skipifnotsilent runascurrentuser
|
Filename: "{app}\{#MyAppExeName}"; Parameters: "--updated"; Flags: nowait skipifnotsilent runascurrentuser
|
||||||
|
|
||||||
|
[UninstallRun]
|
||||||
|
Filename: "schtasks.exe"; Parameters: "/delete /tn ""{#MyAppName}"" /f"; Flags: runhidden
|
||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "syncvito"
|
name = "syncvito"
|
||||||
version = "1.0.12"
|
version = "1.0.13"
|
||||||
description = "Windows-Desktopwerkzeug für lokale und NAS-Synchronisation"
|
description = "Windows-Desktopwerkzeug für lokale und NAS-Synchronisation"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
+20
-2
@@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PyQt6.QtCore import QLockFile, QStandardPaths
|
from PyQt6.QtCore import QLockFile, QStandardPaths
|
||||||
from PyQt6.QtWidgets import QApplication, QMessageBox, QSystemTrayIcon
|
from PyQt6.QtWidgets import QApplication, QMessageBox, QSystemTrayIcon
|
||||||
|
|
||||||
|
from syncvito.autostart import set_autostart
|
||||||
from syncvito.config_store import ConfigError, ConfigStore
|
from syncvito.config_store import ConfigError, ConfigStore
|
||||||
from syncvito.icons import app_icon
|
from syncvito.icons import app_icon
|
||||||
from syncvito.main_window import MainWindow
|
from syncvito.main_window import MainWindow
|
||||||
@@ -18,6 +21,19 @@ from syncvito.version import __version__
|
|||||||
__all__ = ["main"]
|
__all__ = ["main"]
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_for_system_tray(application: QApplication, timeout_seconds: float = 15.0) -> bool:
|
||||||
|
"""Wartet beim Windows-Start auf die Verfügbarkeit des Infobereichs."""
|
||||||
|
if QSystemTrayIcon.isSystemTrayAvailable():
|
||||||
|
return True
|
||||||
|
deadline = time.monotonic() + timeout_seconds
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
application.processEvents()
|
||||||
|
time.sleep(0.25)
|
||||||
|
if QSystemTrayIcon.isSystemTrayAvailable():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
"""Startet SyncVito als einzelne Windows-Instanz."""
|
"""Startet SyncVito als einzelne Windows-Instanz."""
|
||||||
if "--version" in sys.argv:
|
if "--version" in sys.argv:
|
||||||
@@ -33,11 +49,11 @@ def main() -> int:
|
|||||||
|
|
||||||
lock_path = Path(QStandardPaths.writableLocation(QStandardPaths.StandardLocation.TempLocation)) / "syncvito.lock"
|
lock_path = Path(QStandardPaths.writableLocation(QStandardPaths.StandardLocation.TempLocation)) / "syncvito.lock"
|
||||||
lock = QLockFile(str(lock_path))
|
lock = QLockFile(str(lock_path))
|
||||||
lock.setStaleLockTime(0)
|
lock.setStaleLockTime(30000)
|
||||||
if not lock.tryLock(100):
|
if not lock.tryLock(100):
|
||||||
QMessageBox.information(None, "SyncVito läuft bereits", "SyncVito ist bereits im Infobereich aktiv.")
|
QMessageBox.information(None, "SyncVito läuft bereits", "SyncVito ist bereits im Infobereich aktiv.")
|
||||||
return 0
|
return 0
|
||||||
if not QSystemTrayIcon.isSystemTrayAvailable():
|
if not _wait_for_system_tray(application):
|
||||||
QMessageBox.critical(None, "Infobereich nicht verfügbar", "SyncVito benötigt den Windows-Infobereich.")
|
QMessageBox.critical(None, "Infobereich nicht verfügbar", "SyncVito benötigt den Windows-Infobereich.")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
@@ -47,6 +63,8 @@ def main() -> int:
|
|||||||
except ConfigError as exc:
|
except ConfigError as exc:
|
||||||
QMessageBox.warning(None, "Konfiguration zurückgesetzt", str(exc))
|
QMessageBox.warning(None, "Konfiguration zurückgesetzt", str(exc))
|
||||||
config = AppConfig()
|
config = AppConfig()
|
||||||
|
if os.name == "nt" and config.settings.autostart:
|
||||||
|
set_autostart(True)
|
||||||
apply_theme(application, config.settings)
|
apply_theme(application, config.settings)
|
||||||
window = MainWindow(config, store)
|
window = MainWindow(config, store)
|
||||||
if "--minimized" not in sys.argv:
|
if "--minimized" not in sys.argv:
|
||||||
|
|||||||
+140
-15
@@ -3,29 +3,154 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from xml.sax.saxutils import escape as xml_escape
|
||||||
|
|
||||||
__all__ = ["set_autostart"]
|
__all__ = ["RUN_KEY_PATH", "TASK_NAME", "generate_task_xml", "set_autostart"]
|
||||||
|
|
||||||
|
TASK_NAME = "SyncVito"
|
||||||
|
RUN_KEY_PATH = r"Software\Microsoft\Windows\CurrentVersion\Run"
|
||||||
|
|
||||||
|
|
||||||
def set_autostart(enabled: bool) -> None:
|
def generate_task_xml(command_exe: Path, arguments: str = "--minimized") -> str:
|
||||||
"""Setzt oder entfernt SyncVito unter HKCU\\...\\Run."""
|
"""Erzeugt die XML-Definition für die Windows-Aufgabenplanung."""
|
||||||
|
escaped_exe = xml_escape(str(command_exe.resolve()))
|
||||||
|
escaped_args = xml_escape(arguments)
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="UTF-16"?>\n'
|
||||||
|
'<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">\n'
|
||||||
|
" <RegistrationInfo>\n"
|
||||||
|
" <Description>SyncVito Windows Autostart</Description>\n"
|
||||||
|
" </RegistrationInfo>\n"
|
||||||
|
" <Triggers>\n"
|
||||||
|
" <LogonTrigger>\n"
|
||||||
|
" <Enabled>true</Enabled>\n"
|
||||||
|
" <Delay>PT5S</Delay>\n"
|
||||||
|
" </LogonTrigger>\n"
|
||||||
|
" </Triggers>\n"
|
||||||
|
" <Principals>\n"
|
||||||
|
' <Principal id="Author">\n'
|
||||||
|
" <LogonType>InteractiveToken</LogonType>\n"
|
||||||
|
" <RunLevel>HighestAvailable</RunLevel>\n"
|
||||||
|
" </Principal>\n"
|
||||||
|
" </Principals>\n"
|
||||||
|
" <Settings>\n"
|
||||||
|
" <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n"
|
||||||
|
" <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>\n"
|
||||||
|
" <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>\n"
|
||||||
|
" <AllowHardTerminate>true</AllowHardTerminate>\n"
|
||||||
|
" <StartWhenAvailable>true</StartWhenAvailable>\n"
|
||||||
|
" <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>\n"
|
||||||
|
" <IdleSettings>\n"
|
||||||
|
" <StopOnIdleEnd>false</StopOnIdleEnd>\n"
|
||||||
|
" <RestartOnIdle>false</RestartOnIdle>\n"
|
||||||
|
" </IdleSettings>\n"
|
||||||
|
" <AllowStartOnDemand>true</AllowStartOnDemand>\n"
|
||||||
|
" <Enabled>true</Enabled>\n"
|
||||||
|
" <Hidden>false</Hidden>\n"
|
||||||
|
" <RunOnlyIfIdle>false</RunOnlyIfIdle>\n"
|
||||||
|
" <WakeToRun>false</WakeToRun>\n"
|
||||||
|
" <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>\n"
|
||||||
|
" <Priority>7</Priority>\n"
|
||||||
|
" </Settings>\n"
|
||||||
|
' <Actions Context="Author">\n'
|
||||||
|
" <Exec>\n"
|
||||||
|
f" <Command>{escaped_exe}</Command>\n"
|
||||||
|
f" <Arguments>{escaped_args}</Arguments>\n"
|
||||||
|
" </Exec>\n"
|
||||||
|
" </Actions>\n"
|
||||||
|
"</Task>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_schtasks(args: list[str]) -> subprocess.CompletedProcess[str]:
|
||||||
|
flags = getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0
|
||||||
|
return subprocess.run(
|
||||||
|
["schtasks", *args],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
creationflags=flags,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_run_key_entry() -> None:
|
||||||
if os.name != "nt":
|
if os.name != "nt":
|
||||||
return
|
return
|
||||||
import winreg
|
import winreg
|
||||||
|
|
||||||
key_path = r"Software\Microsoft\Windows\CurrentVersion\Run"
|
with (
|
||||||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_SET_VALUE) as key:
|
suppress(FileNotFoundError, OSError),
|
||||||
if enabled:
|
winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY_PATH, 0, winreg.KEY_SET_VALUE) as key,
|
||||||
executable = Path(sys.executable)
|
):
|
||||||
if getattr(sys, "frozen", False):
|
winreg.DeleteValue(key, TASK_NAME)
|
||||||
command = f'"{executable}" --minimized'
|
|
||||||
else:
|
|
||||||
entry = Path(__file__).resolve().parents[2] / "main.py"
|
def _set_run_key_entry(command: str) -> None:
|
||||||
command = f'"{executable}" "{entry}" --minimized'
|
if os.name != "nt":
|
||||||
winreg.SetValueEx(key, "SyncVito", 0, winreg.REG_SZ, command)
|
return
|
||||||
|
import winreg
|
||||||
|
|
||||||
|
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY_PATH, 0, winreg.KEY_SET_VALUE) as key:
|
||||||
|
winreg.SetValueEx(key, TASK_NAME, 0, winreg.REG_SZ, command)
|
||||||
|
|
||||||
|
|
||||||
|
def _register_scheduled_task(command_exe: Path, arguments: str) -> bool:
|
||||||
|
xml_content = generate_task_xml(command_exe, arguments)
|
||||||
|
fd, tmp_file = tempfile.mkstemp(suffix=".xml")
|
||||||
|
tmp_path = Path(tmp_file)
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, "w", encoding="utf-16") as f:
|
||||||
|
f.write(xml_content)
|
||||||
|
result = _run_schtasks(["/create", "/tn", TASK_NAME, "/xml", str(tmp_path), "/f"])
|
||||||
|
return result.returncode == 0
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
with suppress(OSError):
|
||||||
|
tmp_path.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_scheduled_task() -> bool:
|
||||||
|
result = _run_schtasks(["/delete", "/tn", TASK_NAME, "/f"])
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def set_autostart(enabled: bool) -> bool:
|
||||||
|
"""Aktiviert oder entfernt SyncVito im Windows-Autostart.
|
||||||
|
|
||||||
|
Da SyncVito Administratorrechte (requireAdministrator) benötigt,
|
||||||
|
ignoriert der Windows-Explorer Einträge unter HKCU\\...\\Run beim Systemstart
|
||||||
|
vollständig, um unerwünschte UAC-Abfragen beim Logon zu verhindern.
|
||||||
|
Für erhöhte Anwendungen muss daher ein Scheduled Task in der Windows-
|
||||||
|
Aufgabenplanung mit 'RunLevel=HighestAvailable' verwendet werden.
|
||||||
|
"""
|
||||||
|
if os.name != "nt":
|
||||||
|
return False
|
||||||
|
|
||||||
|
if enabled:
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
command_exe = Path(sys.executable)
|
||||||
|
arguments = "--minimized"
|
||||||
|
reg_command = f'"{command_exe}" --minimized'
|
||||||
else:
|
else:
|
||||||
with suppress(FileNotFoundError):
|
command_exe = Path(sys.executable)
|
||||||
winreg.DeleteValue(key, "SyncVito")
|
entry = Path(__file__).resolve().parents[2] / "main.py"
|
||||||
|
arguments = f'"{entry}" --minimized'
|
||||||
|
reg_command = f'"{command_exe}" "{entry}" --minimized'
|
||||||
|
|
||||||
|
if _register_scheduled_task(command_exe, arguments):
|
||||||
|
_delete_run_key_entry()
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Fallback auf Run-Schlüssel (z. B. nicht-erhöhte Entwicklungsumgebung)
|
||||||
|
_set_run_key_entry(reg_command)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
_delete_scheduled_task()
|
||||||
|
_delete_run_key_entry()
|
||||||
|
return True
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import syncvito.autostart as autostart
|
||||||
|
from syncvito.autostart import generate_task_xml, set_autostart
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_task_xml_structure() -> None:
|
||||||
|
cmd = Path("C:/Program Files/SyncVito/SyncVito.exe")
|
||||||
|
args = "--minimized"
|
||||||
|
xml = generate_task_xml(cmd, args)
|
||||||
|
|
||||||
|
assert '<?xml version="1.0" encoding="UTF-16"?>' in xml
|
||||||
|
assert "<RunLevel>HighestAvailable</RunLevel>" in xml
|
||||||
|
assert "<LogonType>InteractiveToken</LogonType>" in xml
|
||||||
|
assert "<Delay>PT5S</Delay>" in xml
|
||||||
|
assert "<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>" in xml
|
||||||
|
assert "<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>" in xml
|
||||||
|
assert "<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>" in xml
|
||||||
|
assert "<Command>C:\\Program Files\\SyncVito\\SyncVito.exe</Command>" in xml
|
||||||
|
assert "<Arguments>--minimized</Arguments>" in xml
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_autostart_non_windows(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(autostart.os, "name", "posix")
|
||||||
|
assert not set_autostart(True)
|
||||||
|
assert not set_autostart(False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_autostart_enable_scheduled_task_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(autostart.os, "name", "nt")
|
||||||
|
monkeypatch.setattr(sys, "frozen", True, raising=False)
|
||||||
|
monkeypatch.setattr(sys, "executable", r"C:\Program Files\SyncVito\SyncVito.exe")
|
||||||
|
|
||||||
|
mock_run = MagicMock(return_value=subprocess.CompletedProcess(args=[], returncode=0))
|
||||||
|
monkeypatch.setattr(autostart, "_run_schtasks", mock_run)
|
||||||
|
|
||||||
|
deleted_run_key = False
|
||||||
|
|
||||||
|
def mock_delete_run_key() -> None:
|
||||||
|
nonlocal deleted_run_key
|
||||||
|
deleted_run_key = True
|
||||||
|
|
||||||
|
monkeypatch.setattr(autostart, "_delete_run_key_entry", mock_delete_run_key)
|
||||||
|
|
||||||
|
result = set_autostart(True)
|
||||||
|
assert result is True
|
||||||
|
assert mock_run.called
|
||||||
|
create_call_args = mock_run.call_args[0][0]
|
||||||
|
assert create_call_args[0] == "/create"
|
||||||
|
assert create_call_args[1] == "/tn"
|
||||||
|
assert create_call_args[2] == "SyncVito"
|
||||||
|
assert deleted_run_key is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_autostart_enable_fallback_registry(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(autostart.os, "name", "nt")
|
||||||
|
monkeypatch.setattr(sys, "frozen", True, raising=False)
|
||||||
|
monkeypatch.setattr(sys, "executable", r"C:\Program Files\SyncVito\SyncVito.exe")
|
||||||
|
|
||||||
|
# schtasks fails (e.g. non-elevated prompt)
|
||||||
|
mock_run = MagicMock(return_value=subprocess.CompletedProcess(args=[], returncode=1))
|
||||||
|
monkeypatch.setattr(autostart, "_run_schtasks", mock_run)
|
||||||
|
|
||||||
|
registry_command = None
|
||||||
|
|
||||||
|
def mock_set_run_key(cmd: str) -> None:
|
||||||
|
nonlocal registry_command
|
||||||
|
registry_command = cmd
|
||||||
|
|
||||||
|
monkeypatch.setattr(autostart, "_set_run_key_entry", mock_set_run_key)
|
||||||
|
|
||||||
|
result = set_autostart(True)
|
||||||
|
assert result is False
|
||||||
|
assert registry_command == r'"C:\Program Files\SyncVito\SyncVito.exe" --minimized'
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_autostart_disable(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(autostart.os, "name", "nt")
|
||||||
|
|
||||||
|
mock_run = MagicMock(return_value=subprocess.CompletedProcess(args=[], returncode=0))
|
||||||
|
monkeypatch.setattr(autostart, "_run_schtasks", mock_run)
|
||||||
|
|
||||||
|
deleted_run_key = False
|
||||||
|
|
||||||
|
def mock_delete_run_key() -> None:
|
||||||
|
nonlocal deleted_run_key
|
||||||
|
deleted_run_key = True
|
||||||
|
|
||||||
|
monkeypatch.setattr(autostart, "_delete_run_key_entry", mock_delete_run_key)
|
||||||
|
|
||||||
|
result = set_autostart(False)
|
||||||
|
assert result is True
|
||||||
|
assert mock_run.called
|
||||||
|
delete_call_args = mock_run.call_args[0][0]
|
||||||
|
assert delete_call_args == ["/delete", "/tn", "SyncVito", "/f"]
|
||||||
|
assert deleted_run_key is True
|
||||||
@@ -3,9 +3,17 @@ from pathlib import Path
|
|||||||
|
|
||||||
def test_installer_launches_admin_application_with_elevated_setup_token() -> None:
|
def test_installer_launches_admin_application_with_elevated_setup_token() -> None:
|
||||||
script = Path("installer/SyncVito.iss").read_text(encoding="utf-8")
|
script = Path("installer/SyncVito.iss").read_text(encoding="utf-8")
|
||||||
run_entries = [line for line in script.splitlines() if line.startswith("Filename:")]
|
run_section = script.split("[Run]")[1].split("[")[0] if "[Run]" in script else ""
|
||||||
|
run_entries = [line for line in run_section.splitlines() if line.startswith("Filename:")]
|
||||||
|
|
||||||
assert len(run_entries) == 2
|
assert len(run_entries) == 2
|
||||||
assert all("runascurrentuser" in entry for entry in run_entries)
|
assert all("runascurrentuser" in entry for entry in run_entries)
|
||||||
assert "postinstall" in run_entries[0]
|
assert "postinstall" in run_entries[0]
|
||||||
assert 'Parameters: "--updated"' in run_entries[1]
|
assert 'Parameters: "--updated"' in run_entries[1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_installer_cleans_up_scheduled_task_on_uninstall() -> None:
|
||||||
|
script = Path("installer/SyncVito.iss").read_text(encoding="utf-8")
|
||||||
|
uninstall_section = script.split("[UninstallRun]")[1].split("[")[0] if "[UninstallRun]" in script else ""
|
||||||
|
assert "schtasks.exe" in uninstall_section
|
||||||
|
assert "{#MyAppName}" in uninstall_section
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1.0.12
|
1.0.13
|
||||||
Reference in new issue
Block a user