Compare commits
No commits in common. "master" and "v2.0.1" have entirely different histories.
11
README.md
11
README.md
@ -61,10 +61,8 @@
|
|||||||
|
|
||||||
Приложение скачивает ZIP-пакет текущей платформы, проверяет его целостность и
|
Приложение скачивает ZIP-пакет текущей платформы, проверяет его целостность и
|
||||||
безопасность, завершает старый процесс, заменяет бинарник и все каталоги из
|
безопасность, завершает старый процесс, заменяет бинарник и все каталоги из
|
||||||
пакета, а затем запускается заново. Перед заменой создаётся резервная копия;
|
пакета, а затем запускается заново. Перед заменой создаётся резервная копия для
|
||||||
она удаляется только после подтверждения запуска новой версии. Если запуск
|
автоматического восстановления при ошибке. Файл `settings.json` и другие файлы,
|
||||||
не удался, предыдущая версия восстанавливается, а сообщение записывается
|
|
||||||
в `update-error.log`. Файл `settings.json` и другие файлы,
|
|
||||||
которых нет в архиве, сохраняются. Для работы проверки релизы репозитория должны
|
которых нет в архиве, сохраняются. Для работы проверки релизы репозитория должны
|
||||||
быть доступны без авторизации.
|
быть доступны без авторизации.
|
||||||
|
|
||||||
@ -73,11 +71,6 @@
|
|||||||
- Windows x86_64: `plate_config_app-windows-x86_64.zip`;
|
- Windows x86_64: `plate_config_app-windows-x86_64.zip`;
|
||||||
- Linux x86_64: `plate_config_app-linux-x86_64.zip`.
|
- Linux x86_64: `plate_config_app-linux-x86_64.zip`.
|
||||||
|
|
||||||
Для перехода с уже установленной v2.0.1 на v2.0.2 обновите приложение вручную:
|
|
||||||
закройте его, скачайте ZIP-пакет своей платформы и распакуйте содержимое в
|
|
||||||
каталог приложения с заменой прежних файлов. `settings.json` удалять не нужно.
|
|
||||||
Начиная с v2.0.2 последующие обновления можно устанавливать кнопкой в приложении.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Интерфейс приложения
|
## Интерфейс приложения
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
"""Plate Config App package."""
|
"""Plate Config App package."""
|
||||||
|
|
||||||
__version__ = "2.0.2"
|
__version__ = "2.0.1"
|
||||||
|
|||||||
@ -2,13 +2,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from PySide6.QtCore import QTimer
|
|
||||||
from PySide6.QtGui import QFont
|
from PySide6.QtGui import QFont
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
from .main_window import MainWindow
|
from .main_window import MainWindow
|
||||||
from .services import settings_service
|
from .services import settings_service
|
||||||
from .services.update_service import signal_update_ready
|
|
||||||
from .ui.icons import ICON_COLOR_PRIMARY, icon
|
from .ui.icons import ICON_COLOR_PRIMARY, icon
|
||||||
from .ui.theme import build_stylesheet, get_palette, create_qpalette
|
from .ui.theme import build_stylesheet, get_palette, create_qpalette
|
||||||
|
|
||||||
@ -29,5 +27,4 @@ def main() -> None:
|
|||||||
if not app_icon.isNull():
|
if not app_icon.isNull():
|
||||||
window.setWindowIcon(app_icon)
|
window.setWindowIcon(app_icon)
|
||||||
window.show()
|
window.show()
|
||||||
QTimer.singleShot(0, signal_update_ready)
|
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|||||||
@ -31,7 +31,6 @@ DOWNLOAD_CHUNK_SIZE = 1024 * 1024
|
|||||||
MAX_DOWNLOAD_BYTES = 300 * 1024 * 1024
|
MAX_DOWNLOAD_BYTES = 300 * 1024 * 1024
|
||||||
MAX_UNPACKED_BYTES = 600 * 1024 * 1024
|
MAX_UNPACKED_BYTES = 600 * 1024 * 1024
|
||||||
MAX_ARCHIVE_MEMBERS = 10_000
|
MAX_ARCHIVE_MEMBERS = 10_000
|
||||||
UPDATE_READY_ENV = "PLATE_CONFIG_UPDATE_READY_FILE"
|
|
||||||
VERSION_PATTERN = re.compile(
|
VERSION_PATTERN = re.compile(
|
||||||
r"[vV]?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
|
r"[vV]?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
|
||||||
)
|
)
|
||||||
@ -354,29 +353,18 @@ def current_executable() -> Path:
|
|||||||
return Path(sys.executable).resolve()
|
return Path(sys.executable).resolve()
|
||||||
|
|
||||||
|
|
||||||
def signal_update_ready() -> None:
|
|
||||||
ready_file = os.environ.pop(UPDATE_READY_ENV, None)
|
|
||||||
if ready_file:
|
|
||||||
try:
|
|
||||||
Path(ready_file).write_text(str(os.getpid()), encoding="ascii")
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _write_windows_updater(script_path: Path) -> None:
|
def _write_windows_updater(script_path: Path) -> None:
|
||||||
script = r'''param(
|
script = r'''param(
|
||||||
[int]$ApplicationProcessId,
|
[int]$ApplicationProcessId,
|
||||||
[string]$Staging,
|
[string]$Staging,
|
||||||
[string]$Target,
|
[string]$Target,
|
||||||
[string]$Backup,
|
[string]$Backup
|
||||||
[string]$ReadyFile
|
|
||||||
)
|
)
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$applicationDirectory = Split-Path -Parent $Target
|
$applicationDirectory = Split-Path -Parent $Target
|
||||||
$errorLog = Join-Path $applicationDirectory "update-error.log"
|
$errorLog = Join-Path $applicationDirectory "update-error.log"
|
||||||
$installed = [System.Collections.Generic.List[string]]::new()
|
$installed = [System.Collections.Generic.List[string]]::new()
|
||||||
$backedUp = [System.Collections.Generic.List[string]]::new()
|
$backedUp = [System.Collections.Generic.List[string]]::new()
|
||||||
$restarted = $null
|
|
||||||
try {
|
try {
|
||||||
Wait-Process -Id $ApplicationProcessId -ErrorAction SilentlyContinue
|
Wait-Process -Id $ApplicationProcessId -ErrorAction SilentlyContinue
|
||||||
Remove-Item -LiteralPath $Backup -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $Backup -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
@ -385,81 +373,35 @@ try {
|
|||||||
foreach ($entry in $entries) {
|
foreach ($entry in $entries) {
|
||||||
$destination = Join-Path $applicationDirectory $entry.Name
|
$destination = Join-Path $applicationDirectory $entry.Name
|
||||||
if (Test-Path -LiteralPath $destination) {
|
if (Test-Path -LiteralPath $destination) {
|
||||||
$deadline = (Get-Date).AddSeconds(30)
|
Move-Item -LiteralPath $destination -Destination $Backup -Force
|
||||||
while ($true) {
|
|
||||||
try {
|
|
||||||
Move-Item -LiteralPath $destination -Destination $Backup -Force -ErrorAction Stop
|
|
||||||
break
|
|
||||||
} catch {
|
|
||||||
if ((Get-Date) -ge $deadline) { throw }
|
|
||||||
Start-Sleep -Milliseconds 250
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$backedUp.Add($entry.Name)
|
$backedUp.Add($entry.Name)
|
||||||
}
|
}
|
||||||
Move-Item -LiteralPath $entry.FullName -Destination $destination -Force -ErrorAction Stop
|
Move-Item -LiteralPath $entry.FullName -Destination $destination -Force
|
||||||
$installed.Add($entry.Name)
|
$installed.Add($entry.Name)
|
||||||
}
|
}
|
||||||
if (-not (Test-Path -LiteralPath $Target -PathType Leaf)) {
|
if (-not (Test-Path -LiteralPath $Target -PathType Leaf)) {
|
||||||
throw "В пакете обновления отсутствует исполняемый файл"
|
throw "В пакете обновления отсутствует исполняемый файл"
|
||||||
}
|
}
|
||||||
Remove-Item -LiteralPath $ReadyFile -Force -ErrorAction SilentlyContinue
|
Start-Process -FilePath $Target -WorkingDirectory $applicationDirectory
|
||||||
$previousReadyFile = $env:PLATE_CONFIG_UPDATE_READY_FILE
|
|
||||||
$previousResetEnvironment = $env:PYINSTALLER_RESET_ENVIRONMENT
|
|
||||||
try {
|
|
||||||
$env:PLATE_CONFIG_UPDATE_READY_FILE = $ReadyFile
|
|
||||||
$env:PYINSTALLER_RESET_ENVIRONMENT = "1"
|
|
||||||
$restarted = Start-Process -FilePath $Target -WorkingDirectory $applicationDirectory -PassThru
|
|
||||||
} finally {
|
|
||||||
$env:PLATE_CONFIG_UPDATE_READY_FILE = $previousReadyFile
|
|
||||||
$env:PYINSTALLER_RESET_ENVIRONMENT = $previousResetEnvironment
|
|
||||||
}
|
|
||||||
$deadline = (Get-Date).AddSeconds(60)
|
|
||||||
while (-not (Test-Path -LiteralPath $ReadyFile -PathType Leaf)) {
|
|
||||||
if ($restarted.HasExited) { throw "Новая версия завершилась до запуска интерфейса" }
|
|
||||||
if ((Get-Date) -ge $deadline) { throw "Новая версия не подтвердила запуск" }
|
|
||||||
Start-Sleep -Milliseconds 250
|
|
||||||
}
|
|
||||||
if ($restarted.HasExited) { throw "Новая версия завершилась сразу после запуска" }
|
|
||||||
Remove-Item -LiteralPath $ReadyFile -Force -ErrorAction SilentlyContinue
|
|
||||||
Remove-Item -LiteralPath $Backup -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $Backup -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
Remove-Item -LiteralPath $Staging -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $Staging -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force
|
Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force
|
||||||
} catch {
|
} catch {
|
||||||
$failure = $_
|
|
||||||
if ($restarted -and -not $restarted.HasExited) {
|
|
||||||
& taskkill.exe /PID $restarted.Id /T /F *> $null
|
|
||||||
Start-Sleep -Milliseconds 500
|
|
||||||
}
|
|
||||||
$rollbackErrors = [System.Collections.Generic.List[string]]::new()
|
|
||||||
foreach ($name in $installed) {
|
foreach ($name in $installed) {
|
||||||
$destination = Join-Path $applicationDirectory $name
|
$destination = Join-Path $applicationDirectory $name
|
||||||
if (Test-Path -LiteralPath $destination) {
|
Remove-Item -LiteralPath $destination -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
try {
|
|
||||||
Remove-Item -LiteralPath $destination -Recurse -Force -ErrorAction Stop
|
|
||||||
} catch {
|
|
||||||
$rollbackErrors.Add($_.ToString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
foreach ($name in $backedUp) {
|
foreach ($name in $backedUp) {
|
||||||
$backupItem = Join-Path $Backup $name
|
$backupItem = Join-Path $Backup $name
|
||||||
$destination = Join-Path $applicationDirectory $name
|
$destination = Join-Path $applicationDirectory $name
|
||||||
|
Remove-Item -LiteralPath $destination -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
if (Test-Path -LiteralPath $backupItem) {
|
if (Test-Path -LiteralPath $backupItem) {
|
||||||
try {
|
Move-Item -LiteralPath $backupItem -Destination $destination -Force
|
||||||
Move-Item -LiteralPath $backupItem -Destination $destination -Force -ErrorAction Stop
|
|
||||||
} catch {
|
|
||||||
$rollbackErrors.Add($_.ToString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Remove-Item -LiteralPath $ReadyFile -Force -ErrorAction SilentlyContinue
|
|
||||||
Remove-Item -LiteralPath $Staging -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $Staging -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
if ($rollbackErrors.Count -eq 0) {
|
Remove-Item -LiteralPath $Backup -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
Remove-Item -LiteralPath $Backup -Recurse -Force -ErrorAction SilentlyContinue
|
$_ | Out-File -LiteralPath $errorLog -Encoding utf8
|
||||||
}
|
|
||||||
@($failure.ToString()) + @($rollbackErrors) | Out-File -LiteralPath $errorLog -Encoding utf8
|
|
||||||
Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force -ErrorAction SilentlyContinue
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
@ -473,40 +415,28 @@ application_pid="$1"
|
|||||||
staging_path="$2"
|
staging_path="$2"
|
||||||
target_path="$3"
|
target_path="$3"
|
||||||
backup_path="$4"
|
backup_path="$4"
|
||||||
ready_path="$5"
|
|
||||||
application_dir="$(dirname -- "$target_path")"
|
application_dir="$(dirname -- "$target_path")"
|
||||||
error_log="$application_dir/update-error.log"
|
error_log="$application_dir/update-error.log"
|
||||||
installed_file="$backup_path/.installed"
|
installed_file="$backup_path/.installed"
|
||||||
backed_up_file="$backup_path/.backed_up"
|
backed_up_file="$backup_path/.backed_up"
|
||||||
new_pid=""
|
|
||||||
rollback() {
|
rollback() {
|
||||||
exit_code="$?"
|
exit_code="$?"
|
||||||
trap - EXIT HUP INT TERM
|
trap - EXIT HUP INT TERM
|
||||||
set +e
|
|
||||||
if [ -n "$new_pid" ]; then
|
|
||||||
kill "$new_pid" 2>/dev/null
|
|
||||||
wait "$new_pid" 2>/dev/null
|
|
||||||
fi
|
|
||||||
rollback_failed=0
|
|
||||||
if [ -f "$installed_file" ]; then
|
if [ -f "$installed_file" ]; then
|
||||||
while IFS= read -r name; do
|
while IFS= read -r name; do
|
||||||
rm -rf -- "$application_dir/$name" || rollback_failed=1
|
rm -rf -- "$application_dir/$name"
|
||||||
done < "$installed_file"
|
done < "$installed_file"
|
||||||
fi
|
fi
|
||||||
if [ -f "$backed_up_file" ]; then
|
if [ -f "$backed_up_file" ]; then
|
||||||
while IFS= read -r name; do
|
while IFS= read -r name; do
|
||||||
|
rm -rf -- "$application_dir/$name"
|
||||||
if [ -e "$backup_path/$name" ]; then
|
if [ -e "$backup_path/$name" ]; then
|
||||||
mv -f -- "$backup_path/$name" "$application_dir/$name" || rollback_failed=1
|
mv -f -- "$backup_path/$name" "$application_dir/$name"
|
||||||
fi
|
fi
|
||||||
done < "$backed_up_file"
|
done < "$backed_up_file"
|
||||||
fi
|
fi
|
||||||
rm -f -- "$ready_path"
|
rm -rf -- "$staging_path" "$backup_path"
|
||||||
rm -rf -- "$staging_path"
|
|
||||||
if [ "$rollback_failed" -eq 0 ]; then
|
|
||||||
rm -rf -- "$backup_path"
|
|
||||||
fi
|
|
||||||
printf '%s\n' "Не удалось заменить приложение" >"$error_log"
|
printf '%s\n' "Не удалось заменить приложение" >"$error_log"
|
||||||
rm -f -- "$0"
|
|
||||||
if [ "$exit_code" -eq 0 ]; then
|
if [ "$exit_code" -eq 0 ]; then
|
||||||
exit_code=1
|
exit_code=1
|
||||||
fi
|
fi
|
||||||
@ -532,25 +462,8 @@ for source_path in "$staging_path"/*; do
|
|||||||
done
|
done
|
||||||
test -f "$target_path"
|
test -f "$target_path"
|
||||||
chmod 755 -- "$target_path"
|
chmod 755 -- "$target_path"
|
||||||
rm -f -- "$ready_path"
|
"$target_path" >/dev/null 2>&1 &
|
||||||
PLATE_CONFIG_UPDATE_READY_FILE="$ready_path" PYINSTALLER_RESET_ENVIRONMENT=1 "$target_path" >/dev/null 2>&1 &
|
|
||||||
new_pid=$!
|
|
||||||
attempts=0
|
|
||||||
while [ ! -f "$ready_path" ]; do
|
|
||||||
if ! kill -0 "$new_pid" 2>/dev/null; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
attempts=$((attempts + 1))
|
|
||||||
if [ "$attempts" -ge 240 ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 0.25
|
|
||||||
done
|
|
||||||
if ! kill -0 "$new_pid" 2>/dev/null; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
trap - EXIT HUP INT TERM
|
trap - EXIT HUP INT TERM
|
||||||
rm -f -- "$ready_path"
|
|
||||||
rm -rf -- "$backup_path" "$staging_path"
|
rm -rf -- "$backup_path" "$staging_path"
|
||||||
rm -f -- "$0"
|
rm -f -- "$0"
|
||||||
'''
|
'''
|
||||||
@ -581,7 +494,6 @@ def launch_update(
|
|||||||
|
|
||||||
staging_path = target.parent / f".plate_config_update-{updater_id}"
|
staging_path = target.parent / f".plate_config_update-{updater_id}"
|
||||||
backup_path = target.parent / f".plate_config_backup-{updater_id}"
|
backup_path = target.parent / f".plate_config_backup-{updater_id}"
|
||||||
ready_path = target.parent / f".plate_config_ready-{updater_id}"
|
|
||||||
script_path: Path | None = None
|
script_path: Path | None = None
|
||||||
try:
|
try:
|
||||||
_extract_update_archive(archive_path, staging_path, target.name)
|
_extract_update_archive(archive_path, staging_path, target.name)
|
||||||
@ -610,7 +522,6 @@ def launch_update(
|
|||||||
str(staging_path),
|
str(staging_path),
|
||||||
str(target),
|
str(target),
|
||||||
str(backup_path),
|
str(backup_path),
|
||||||
str(ready_path),
|
|
||||||
],
|
],
|
||||||
close_fds=True,
|
close_fds=True,
|
||||||
creationflags=creation_flags,
|
creationflags=creation_flags,
|
||||||
@ -632,7 +543,6 @@ def launch_update(
|
|||||||
str(staging_path),
|
str(staging_path),
|
||||||
str(target),
|
str(target),
|
||||||
str(backup_path),
|
str(backup_path),
|
||||||
str(ready_path),
|
|
||||||
],
|
],
|
||||||
cwd=str(target.parent),
|
cwd=str(target.parent),
|
||||||
close_fds=True,
|
close_fds=True,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ from pathlib import Path
|
|||||||
from PySide6.QtCore import QThread, QTimer, Qt, Signal
|
from PySide6.QtCore import QThread, QTimer, Qt, Signal
|
||||||
from PySide6.QtGui import QColor
|
from PySide6.QtGui import QColor
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
QFrame,
|
QFrame,
|
||||||
QGraphicsDropShadowEffect,
|
QGraphicsDropShadowEffect,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
@ -229,7 +230,9 @@ class StartTab(QWidget):
|
|||||||
path.unlink(missing_ok=True)
|
path.unlink(missing_ok=True)
|
||||||
self._on_update_failed(str(exc))
|
self._on_update_failed(str(exc))
|
||||||
return
|
return
|
||||||
self.window().close()
|
app = QApplication.instance()
|
||||||
|
if app is not None:
|
||||||
|
app.quit()
|
||||||
|
|
||||||
def _on_update_failed(self, detail: str) -> None:
|
def _on_update_failed(self, detail: str) -> None:
|
||||||
self._update_btn.setText("Скачать обновление")
|
self._update_btn.setText("Скачать обновление")
|
||||||
|
|||||||
@ -54,28 +54,10 @@ if ($InstallBuildDeps) {
|
|||||||
# ── Сборка ─────────────────────────────────────────────────────────
|
# ── Сборка ─────────────────────────────────────────────────────────
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "=== Сборка EXE ==="
|
Write-Host "=== Сборка EXE ==="
|
||||||
$pythonHome = (& $venvPython -c "import sys; print(sys.base_prefix)").Trim()
|
& $venvPython -m PyInstaller --noconfirm --clean plate_config_app.spec
|
||||||
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $pythonHome -PathType Container)) {
|
|
||||||
throw "Не удалось определить каталог Python для сборки."
|
|
||||||
}
|
|
||||||
$buildPath = @(
|
|
||||||
(Split-Path -Parent $venvPython),
|
|
||||||
$pythonHome,
|
|
||||||
(Join-Path $pythonHome "DLLs"),
|
|
||||||
(Join-Path $env:SystemRoot "System32"),
|
|
||||||
$env:SystemRoot
|
|
||||||
) -join ";"
|
|
||||||
$originalPath = $env:PATH
|
|
||||||
try {
|
|
||||||
$env:PATH = $buildPath
|
|
||||||
& $venvPython -m PyInstaller --noconfirm --clean plate_config_app.spec
|
|
||||||
$buildExitCode = $LASTEXITCODE
|
|
||||||
} finally {
|
|
||||||
$env:PATH = $originalPath
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($buildExitCode -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
throw "PyInstaller завершился с ошибкой (код $buildExitCode)."
|
throw "PyInstaller завершился с ошибкой (код $LASTEXITCODE)."
|
||||||
}
|
}
|
||||||
|
|
||||||
$builtExe = Join-Path $projectRoot "dist\plate_config_app.exe"
|
$builtExe = Join-Path $projectRoot "dist\plate_config_app.exe"
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
@ -98,18 +98,20 @@ class StartTabUpdateTests(unittest.TestCase):
|
|||||||
warning.assert_called_once()
|
warning.assert_called_once()
|
||||||
tab.shutdown()
|
tab.shutdown()
|
||||||
|
|
||||||
def test_successful_download_launches_update_and_closes_window(self) -> None:
|
def test_successful_download_launches_update_and_quits(self) -> None:
|
||||||
tab = StartTab(lambda: None)
|
tab = StartTab(lambda: None)
|
||||||
downloaded = Path("downloaded-update")
|
downloaded = Path("downloaded-update")
|
||||||
|
application = Mock()
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("app.widgets.start_tab.launch_update") as launcher,
|
patch("app.widgets.start_tab.launch_update") as launcher,
|
||||||
patch.object(tab, "close") as close_window,
|
patch("app.widgets.start_tab.QApplication") as application_class,
|
||||||
):
|
):
|
||||||
|
application_class.instance.return_value = application
|
||||||
tab._on_update_downloaded(downloaded)
|
tab._on_update_downloaded(downloaded)
|
||||||
|
|
||||||
launcher.assert_called_once_with(downloaded)
|
launcher.assert_called_once_with(downloaded)
|
||||||
close_window.assert_called_once_with()
|
application.quit.assert_called_once_with()
|
||||||
self.assertEqual("Перезапуск…", tab._update_btn.text())
|
self.assertEqual("Перезапуск…", tab._update_btn.text())
|
||||||
tab.shutdown()
|
tab.shutdown()
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,6 @@ from app.services.update_service import (
|
|||||||
parse_version,
|
parse_version,
|
||||||
platform_asset_name,
|
platform_asset_name,
|
||||||
platform_executable_name,
|
platform_executable_name,
|
||||||
signal_update_ready,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -200,17 +199,6 @@ class UpdateServiceTests(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(UpdateError, "Неизвестное"):
|
with self.assertRaisesRegex(UpdateError, "Неизвестное"):
|
||||||
archive_executable_name("update.zip")
|
archive_executable_name("update.zip")
|
||||||
|
|
||||||
def test_signals_restart_only_when_ready_file_is_requested(self) -> None:
|
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
|
||||||
marker = Path(directory) / "ready"
|
|
||||||
with patch.dict(os.environ, {update_service.UPDATE_READY_ENV: str(marker)}):
|
|
||||||
signal_update_ready()
|
|
||||||
self.assertEqual(str(os.getpid()), marker.read_text(encoding="ascii"))
|
|
||||||
self.assertNotIn(update_service.UPDATE_READY_ENV, os.environ)
|
|
||||||
marker.unlink()
|
|
||||||
signal_update_ready()
|
|
||||||
self.assertFalse(marker.exists())
|
|
||||||
|
|
||||||
def test_downloads_and_validates_windows_zip(self) -> None:
|
def test_downloads_and_validates_windows_zip(self) -> None:
|
||||||
info = self._update_info()
|
info = self._update_info()
|
||||||
data = make_update_archive(
|
data = make_update_archive(
|
||||||
@ -424,10 +412,6 @@ class UpdateServiceTests(unittest.TestCase):
|
|||||||
script = scripts[0].read_text(encoding="utf-8-sig")
|
script = scripts[0].read_text(encoding="utf-8-sig")
|
||||||
self.assertIn("$Staging", script)
|
self.assertIn("$Staging", script)
|
||||||
self.assertIn("$backedUp", script)
|
self.assertIn("$backedUp", script)
|
||||||
self.assertIn("AddSeconds(30)", script)
|
|
||||||
self.assertIn("$restarted.HasExited", script)
|
|
||||||
self.assertIn("$ReadyFile", script)
|
|
||||||
self.assertIn('$env:PYINSTALLER_RESET_ENVIRONMENT = "1"', script)
|
|
||||||
popen.assert_called_once()
|
popen.assert_called_once()
|
||||||
|
|
||||||
@unittest.skipUnless(sys.platform.startswith("linux"), "Тест предназначен для Linux")
|
@unittest.skipUnless(sys.platform.startswith("linux"), "Тест предназначен для Linux")
|
||||||
@ -461,8 +445,6 @@ class UpdateServiceTests(unittest.TestCase):
|
|||||||
script = scripts[0].read_text(encoding="utf-8")
|
script = scripts[0].read_text(encoding="utf-8")
|
||||||
self.assertIn("staging_path", script)
|
self.assertIn("staging_path", script)
|
||||||
self.assertIn("rollback", script)
|
self.assertIn("rollback", script)
|
||||||
self.assertIn("PLATE_CONFIG_UPDATE_READY_FILE", script)
|
|
||||||
self.assertIn("PYINSTALLER_RESET_ENVIRONMENT=1", script)
|
|
||||||
popen.assert_called_once()
|
popen.assert_called_once()
|
||||||
|
|
||||||
def test_extracts_external_archive_next_to_executable(self) -> None:
|
def test_extracts_external_archive_next_to_executable(self) -> None:
|
||||||
@ -579,7 +561,7 @@ class ReplacementIntegrationTests(unittest.TestCase):
|
|||||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _wait_for_rollback(
|
def _wait_for_replacement(
|
||||||
target: Path,
|
target: Path,
|
||||||
expected_digest: str,
|
expected_digest: str,
|
||||||
replaced_resource: Path,
|
replaced_resource: Path,
|
||||||
@ -589,19 +571,17 @@ class ReplacementIntegrationTests(unittest.TestCase):
|
|||||||
if (
|
if (
|
||||||
target.is_file()
|
target.is_file()
|
||||||
and ReplacementIntegrationTests._digest(target) == expected_digest
|
and ReplacementIntegrationTests._digest(target) == expected_digest
|
||||||
and replaced_resource.is_file()
|
and replaced_resource.read_text(encoding="utf-8") == "new"
|
||||||
and replaced_resource.read_text(encoding="utf-8") == "old"
|
|
||||||
and not list(target.parent.glob(".plate_config_updater-*"))
|
and not list(target.parent.glob(".plate_config_updater-*"))
|
||||||
and not list(target.parent.glob(".plate_config_update-*"))
|
and not list(target.parent.glob(".plate_config_update-*"))
|
||||||
and not list(target.parent.glob(".plate_config_backup-*"))
|
and not list(target.parent.glob(".plate_config_backup-*"))
|
||||||
and (target.parent / "update-error.log").is_file()
|
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
raise AssertionError("Внешний helper не откатил пакет за 20 секунд")
|
raise AssertionError("Внешний helper не заменил пакет за 20 секунд")
|
||||||
|
|
||||||
@unittest.skipUnless(sys.platform == "win32", "Тест предназначен для Windows")
|
@unittest.skipUnless(sys.platform == "win32", "Тест предназначен для Windows")
|
||||||
def test_windows_helper_rolls_back_when_new_app_exits_early(self) -> None:
|
def test_windows_helper_replaces_full_package(self) -> None:
|
||||||
system_root = Path(os.environ.get("SystemRoot", r"C:\Windows"))
|
system_root = Path(os.environ.get("SystemRoot", r"C:\Windows"))
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
root = Path(directory)
|
root = Path(directory)
|
||||||
@ -617,20 +597,20 @@ class ReplacementIntegrationTests(unittest.TestCase):
|
|||||||
extra_files={"commands/version.txt": b"new"},
|
extra_files={"commands/version.txt": b"new"},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
expected_digest = self._digest(target)
|
expected_digest = hashlib.sha256(new_executable).hexdigest()
|
||||||
processes = self._launch_and_capture(archive, target, "Windows")
|
processes = self._launch_and_capture(archive, target, "Windows")
|
||||||
|
|
||||||
self._wait_for_rollback(
|
self._wait_for_replacement(
|
||||||
target,
|
target,
|
||||||
expected_digest,
|
expected_digest,
|
||||||
root / "commands/version.txt",
|
root / "commands/version.txt",
|
||||||
)
|
)
|
||||||
processes[0].wait(timeout=5)
|
processes[0].wait(timeout=5)
|
||||||
self._assert_package_preserved(root)
|
self._assert_package_replaced(root)
|
||||||
self._wait_for_windows_unlock(target)
|
self._wait_for_windows_unlock(target)
|
||||||
|
|
||||||
@unittest.skipUnless(sys.platform.startswith("linux"), "Тест предназначен для Linux")
|
@unittest.skipUnless(sys.platform.startswith("linux"), "Тест предназначен для Linux")
|
||||||
def test_linux_helper_rolls_back_when_new_app_exits_early(self) -> None:
|
def test_linux_helper_replaces_full_package(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
root = Path(directory)
|
root = Path(directory)
|
||||||
target = root / "plate_config_app-linux-x86_64"
|
target = root / "plate_config_app-linux-x86_64"
|
||||||
@ -645,16 +625,16 @@ class ReplacementIntegrationTests(unittest.TestCase):
|
|||||||
extra_files={"commands/version.txt": b"new"},
|
extra_files={"commands/version.txt": b"new"},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
expected_digest = self._digest(target)
|
expected_digest = hashlib.sha256(new_executable).hexdigest()
|
||||||
processes = self._launch_and_capture(archive, target, "Linux")
|
processes = self._launch_and_capture(archive, target, "Linux")
|
||||||
|
|
||||||
self._wait_for_rollback(
|
self._wait_for_replacement(
|
||||||
target,
|
target,
|
||||||
expected_digest,
|
expected_digest,
|
||||||
root / "commands/version.txt",
|
root / "commands/version.txt",
|
||||||
)
|
)
|
||||||
processes[0].wait(timeout=5)
|
processes[0].wait(timeout=5)
|
||||||
self._assert_package_preserved(root)
|
self._assert_package_replaced(root)
|
||||||
self.assertTrue(target.stat().st_mode & stat.S_IXUSR)
|
self.assertTrue(target.stat().st_mode & stat.S_IXUSR)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -666,12 +646,12 @@ class ReplacementIntegrationTests(unittest.TestCase):
|
|||||||
(root / "settings.json").write_text("preserve", encoding="utf-8")
|
(root / "settings.json").write_text("preserve", encoding="utf-8")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _assert_package_preserved(root: Path) -> None:
|
def _assert_package_replaced(root: Path) -> None:
|
||||||
self_settings = (root / "settings.json").read_text(encoding="utf-8")
|
self_settings = (root / "settings.json").read_text(encoding="utf-8")
|
||||||
if self_settings != "preserve":
|
if self_settings != "preserve":
|
||||||
raise AssertionError("settings.json не сохранён")
|
raise AssertionError("settings.json не сохранён")
|
||||||
if not (root / "commands/obsolete.txt").exists():
|
if (root / "commands/obsolete.txt").exists():
|
||||||
raise AssertionError("Старое содержимое каталога не восстановлено")
|
raise AssertionError("Старое содержимое каталога не удалено")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _launch_and_capture(
|
def _launch_and_capture(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user