1
0
forked from BRT/arc

Add update mechanism for ARC: implement UpdateService for version checks, downloads, and staging; add ApplyUpdate for file copying and relaunch; integrate update actions into MainWindow and workflows.

This commit is contained in:
slava 2026-05-30 13:36:50 +05:00
parent 992073d43d
commit ce61986035
12 changed files with 622 additions and 5 deletions

View File

@ -61,14 +61,33 @@ if %ERRORLEVEL% neq 0 (
exit /b 1
)
echo === [5/5] Uploading and sending link to Telegram ===
for /f "delims=" %%B in ('powershell -NoProfile -Command "[guid]::NewGuid().ToString('N').Substring(0,16)"') do set BIN_ID=arc-%%B
curl -s -H "Content-Type: application/octet-stream" -X POST --data-binary "@%PROJECT_DIR%%ZIP_NAME%" "https://filebin.net/!BIN_ID!/%ZIP_NAME%" > nul
echo === [5/5] Uploading to update server and sending link to Telegram ===
REM Целевой сервер обновлений (нужен настроенный SSH-ключ для root@%UPDATE_HOST%).
set UPDATE_HOST=142.93.102.40
set UPDATE_DIR=/var/www/updates
set DOWNLOAD_URL=http://%UPDATE_HOST%/updates/%ZIP_NAME%
REM SHA256 архива — для проверки целостности на клиенте.
for /f "delims=" %%H in ('powershell -NoProfile -Command "(Get-FileHash '%PROJECT_DIR%%ZIP_NAME%' -Algorithm SHA256).Hash.ToLower()"') do set ZIP_SHA256=%%H
echo SHA256: !ZIP_SHA256!
REM Сначала заливаем zip, потом манифест — чтобы клиент не увидел version.json
REM на ещё не загруженный архив.
scp "%PROJECT_DIR%%ZIP_NAME%" root@%UPDATE_HOST%:%UPDATE_DIR%/%ZIP_NAME%
if %ERRORLEVEL% neq 0 (
echo ERROR: Failed to upload archive
echo ERROR: Failed to upload archive to update server
exit /b 1
)
REM Генерируем version.json (UTF-8 без BOM — иначе QJsonDocument::fromJson падает)
REM и заливаем последним.
powershell -NoProfile -Command "$j = @{ latestVersion='!CURRENT_VERSION!'; zipUrl='!DOWNLOAD_URL!'; sha256='!ZIP_SHA256!'; minVersion='1.0.0'; notes='Обновление ARC'; mandatory=$false } | ConvertTo-Json; [System.IO.File]::WriteAllText('%PROJECT_DIR%version.json', $j, (New-Object System.Text.UTF8Encoding($false)))"
scp "%PROJECT_DIR%version.json" root@%UPDATE_HOST%:%UPDATE_DIR%/version.json
if %ERRORLEVEL% neq 0 (
echo ERROR: Failed to upload version.json to update server
exit /b 1
)
set DOWNLOAD_URL=https://filebin.net/!BIN_ID!/%ZIP_NAME%
echo Upload URL: !DOWNLOAD_URL!
curl -s -X POST "https://api.telegram.org/bot%TG_BOT_TOKEN%/sendMessage" -d "chat_id=%TG_CHAT_ID%" -d "text=ARC x64 v!CURRENT_VERSION!: !DOWNLOAD_URL!"

View File

@ -24,6 +24,9 @@ pathMonitoringDump=/monitoring/dump
monitoringBase=http://142.93.102.40:8080
monitoringToken=509b91592749959b9ff56f9ed9189e5b3e91e1260003f63c477255e1c187298a
[update]
manifestUrl=http://142.93.102.40/updates/version.json
[black]
android_package_name=app.blackwallet.ar
currency=ARS

View File

@ -55,6 +55,7 @@ static LONG WINAPI crashHandler(EXCEPTION_POINTERS *ep) {
#include "dao/SettingsDAO.h"
#include "EventHandler.h"
#include "net/NetworkService.h"
#include "update/ApplyUpdate.h"
#include "black/PayByCardScript.h"
#include "ozon/PayByPhoneScript.h"
#include "widget/loader/ErrorWindow.h"
@ -301,6 +302,14 @@ int main(int argc, char *argv[]) {
#ifdef Q_OS_WIN
SetUnhandledExceptionFilter(crashHandler);
#endif
// Режим установки обновления: запущены как временная копия ARC.exe с
// --apply-update. Перехватываем ДО QApplication/БД/логгера — это headless
// файловый воркер, а не основное приложение.
if (isApplyUpdateMode(argc, argv)) {
return runApplyUpdate(argc, argv);
}
QApplication app(argc, argv);
app.setApplicationName("ARC");
app.setWindowIcon(QIcon("arc.png"));
@ -380,6 +389,7 @@ int main(int argc, char *argv[]) {
mainWindow->show();
registrationWindow->close();
NetworkService *net = startNetworkService(app);
mainWindow->setNetworkService(net);
QObject::connect(net, &NetworkService::syncHealthChanged,
mainWindow, &MainWindow::setSyncHealth,
Qt::QueuedConnection);
@ -414,6 +424,7 @@ int main(int argc, char *argv[]) {
auto *mainWindow = new MainWindow();
mainWindow->show();
NetworkService *net = startNetworkService(app);
mainWindow->setNetworkService(net);
QObject::connect(net, &NetworkService::syncHealthChanged,
mainWindow, &MainWindow::setSyncHealth,
Qt::QueuedConnection);
@ -438,6 +449,7 @@ int main(int argc, char *argv[]) {
auto *mainWindow = new MainWindow();
mainWindow->show();
NetworkService *net = startNetworkService(app);
mainWindow->setNetworkService(net);
QObject::connect(net, &NetworkService::syncHealthChanged,
mainWindow, &MainWindow::setSyncHealth,
Qt::QueuedConnection);

View File

@ -18,6 +18,7 @@
#include "dao/TransactionDAO.h"
#include "db/BankProfileInfo.h"
#include "adb/AdbUtils.h"
#include "update/UpdatePause.h"
#include "dao/GeneralLogDAO.h"
#include "net/NetworkService.h"
#include "black/GetLastTransactionsScript.h"
@ -48,6 +49,9 @@ void EventHandler::start() {
m_pollTimer = new QTimer(this);
m_pollTimer->setInterval(5000);
connect(m_pollTimer, &QTimer::timeout, this, [this]() {
// На время установки обновления не трогаем устройства и не запрашиваем задачи.
if (UpdatePause::isPaused()) return;
// Пробуем запустить задачи из очереди для свободных устройств
const QStringList onlineDevices = DeviceDAO::getOnlineDeviceCodes();
for (const QString &deviceId : onlineDevices) {

View File

@ -26,6 +26,7 @@
#include "dao/SettingsDAO.h"
#include "dao/TransactionDAO.h"
#include "db/EventInfo.h"
#include "update/UpdatePause.h"
// Убирает полный URL (хост:порт) из сообщений об ошибках, оставляя только
// Хелперы для логирования сетевых ошибок вынесены в NetworkErrorUtils.h
@ -1822,6 +1823,13 @@ void NetworkService::scheduleNextStatusSync() {
QTimer::singleShot(kPeriodicSyncMs, this, [this]() {
if (!m_running) return;
// На время установки обновления пропускаем тик: иначе sync перепушит
// локальное "active" обратно на сервер и отменит выключение профилей.
if (UpdatePause::isPaused()) {
scheduleNextStatusSync();
return;
}
// Если предыдущий тик ещё не завершился — пропускаем (правило #10).
if (m_syncInProgress.exchange(true)) {
qDebug() << "[NetworkService] periodic sync tick skipped — previous still in progress";

View File

@ -0,0 +1,129 @@
#include "ApplyUpdate.h"
#include <cstring>
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QString>
#include <QStringList>
#include <QTextStream>
#include <QThread>
#ifdef Q_OS_WIN
#include <windows.h>
#endif
namespace {
void logLine(const QString &logPath, const QString &msg) {
QFile f(logPath);
if (f.open(QIODevice::Append | QIODevice::Text)) {
QTextStream(&f) << msg << '\n';
}
fprintf(stderr, "[apply-update] %s\n", msg.toLocal8Bit().constData());
}
QString argValue(const QStringList &args, const QString &key) {
const int idx = args.indexOf(key);
if (idx >= 0 && idx + 1 < args.size()) return args.at(idx + 1);
return QString();
}
} // namespace
bool isApplyUpdateMode(int argc, char **argv) {
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--apply-update") == 0) return true;
}
return false;
}
int runApplyUpdate(int argc, char **argv) {
// Headless-приложение: даёт QProcess/QFile/QDir, но не поднимает GUI.
QCoreApplication app(argc, argv);
const QStringList args = app.arguments();
const QString src = QDir::cleanPath(argValue(args, QStringLiteral("--src")));
const QString dst = QDir::cleanPath(argValue(args, QStringLiteral("--dst")));
const QString relaunch = argValue(args, QStringLiteral("--relaunch"));
const QString pidStr = argValue(args, QStringLiteral("--pid"));
// Корень стейджинга = родитель src (…/ARC_update/unpacked → …/ARC_update).
const QString root = QFileInfo(src).path();
const QString logPath = root + QStringLiteral("/updater.log");
logLine(logPath, QStringLiteral("start: src=%1 dst=%2 pid=%3").arg(src, dst, pidStr));
if (src.isEmpty() || dst.isEmpty() || relaunch.isEmpty()) {
logLine(logPath, QStringLiteral("ERROR: missing arguments"));
return 2;
}
#ifdef Q_OS_WIN
// 1) Ждём выхода родительского процесса.
const DWORD pid = pidStr.toULong();
if (pid != 0) {
HANDLE h = OpenProcess(SYNCHRONIZE, FALSE, pid);
if (h) {
logLine(logPath, QStringLiteral("waiting for parent to exit..."));
WaitForSingleObject(h, 30000);
CloseHandle(h);
}
}
// 2) Settle-цикл: дожидаемся, пока install/ARC.exe можно открыть на запись
// (переживаем антивирус и подвисшие хэндлы). До ~12 секунд.
const QString dstExe = dst + QStringLiteral("/ARC.exe");
if (QFile::exists(dstExe)) {
const std::wstring wexe = QDir::toNativeSeparators(dstExe).toStdWString();
for (int i = 0; i < 12; ++i) {
HANDLE fh = CreateFileW(wexe.c_str(), GENERIC_WRITE, 0, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (fh != INVALID_HANDLE_VALUE) {
CloseHandle(fh);
break;
}
QThread::msleep(1000);
}
}
// 3) Копируем новые файлы поверх install-папки, сохраняя database/.
// robocopy сам ретраит залоченные файлы; коды возврата 07 = успех.
QProcess robo;
robo.start(QStringLiteral("robocopy"), {
src, dst, QStringLiteral("/E"),
QStringLiteral("/XD"), QStringLiteral("database"),
QStringLiteral("/R:5"), QStringLiteral("/W:1"),
QStringLiteral("/NFL"), QStringLiteral("/NDL"),
QStringLiteral("/NJH"), QStringLiteral("/NJS"),
});
robo.waitForFinished(-1);
const int code = robo.exitCode();
logLine(logPath, QStringLiteral("robocopy exit code: %1").arg(code));
if (code >= 8) {
logLine(logPath, QStringLiteral("ERROR: robocopy failed"));
// Всё равно пробуем перезапустить — часть файлов могла скопироваться.
}
// 4) Перезапускаем установленный (уже обновлённый) ARC.exe.
const bool relaunched = QProcess::startDetached(relaunch, {}, dst);
logLine(logPath, QStringLiteral("relaunch %1: %2")
.arg(relaunch, relaunched ? QStringLiteral("ok") : QStringLiteral("FAILED")));
// 5) Самоудаление временной папки: апдейтер не может удалить свой работающий
// exe, поэтому откладываем через cmd с задержкой.
const QString rmCmd = QStringLiteral("ping 127.0.0.1 -n 4 >nul & rmdir /s /q \"%1\"")
.arg(QDir::toNativeSeparators(root));
QProcess::startDetached(QStringLiteral("cmd.exe"),
{QStringLiteral("/c"), rmCmd});
return 0;
#else
Q_UNUSED(relaunch);
logLine(logPath, QStringLiteral("apply-update is Windows-only; no-op"));
return 0;
#endif
}

View File

@ -0,0 +1,14 @@
#pragma once
// Режим установки обновления. Запускается из свежераспакованной копии ARC.exe
// (в ней есть все Qt-DLL), ДО создания QApplication в main(). Ждёт выхода
// родительского процесса, копирует новые файлы поверх install-папки (сохраняя
// database/), перезапускает установленный ARC.exe и самоудаляет временную папку.
// true, если среди аргументов есть "--apply-update". Дёшево, без Qt — можно
// звать в самом начале main() до QApplication.
bool isApplyUpdateMode(int argc, char **argv);
// Выполняет установку. Создаёт собственный headless QCoreApplication. Возвращает
// код возврата процесса (0 — успех). На не-Windows — no-op, возвращает 0.
int runApplyUpdate(int argc, char **argv);

View File

@ -0,0 +1,16 @@
#pragma once
#include <atomic>
// Глобальная пауза на время установки обновления. Ставится из UI сразу после
// подтверждения апдейта (до выключения профилей и скачивания) и снимается, если
// обновление не удалось. Пока выставлена:
// - EventHandler не опрашивает getTasks и не запускает новые задачи;
// - периодический sync NetworkService пропускает тик (не перепушивает локальное
// "active" обратно на сервер, чтобы профили остались выключенными).
// Процесс-локальный флаг: при перезапуске после установки сбрасывается сам.
namespace UpdatePause {
inline std::atomic<bool> g_paused{false};
inline void setPaused(bool v) { g_paused.store(v); }
inline bool isPaused() { return g_paused.load(); }
} // namespace UpdatePause

View File

@ -0,0 +1,241 @@
#include "UpdateService.h"
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QDir>
#include <QEventLoop>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QProcess>
#include <QSettings>
#include <QStandardPaths>
#include <QTimer>
#include <QUrl>
namespace {
// Прокручивает event loop пока reply не завершится либо не сработает таймаут.
// true — reply finished, false — сработал наш QTimer (client timeout).
bool awaitReply(QNetworkReply *reply, int timeoutMs) {
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
timer.start(timeoutMs);
loop.exec();
return timer.isActive();
}
constexpr int kManifestTimeoutMs = 15000;
// Корень временной папки для стейджинга обновления (вне install-директории).
QString stagingRoot() {
return QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/ARC_update";
}
} // namespace
UpdateService::UpdateService(QObject *parent) : QObject(parent) {
const QSettings settings("config.ini", QSettings::IniFormat);
m_manifestUrl = settings.value("update/manifestUrl").toString();
m_currentVersion = settings.value("common/version").toString();
m_manager = new QNetworkAccessManager(this);
}
UpdateService::~UpdateService() = default;
int UpdateService::compareVersions(const QString &a, const QString &b) {
const QStringList pa = a.split('.');
const QStringList pb = b.split('.');
const int n = qMax(pa.size(), pb.size());
for (int i = 0; i < n; ++i) {
const int va = i < pa.size() ? pa[i].toInt() : 0;
const int vb = i < pb.size() ? pb[i].toInt() : 0;
if (va != vb) return va < vb ? -1 : 1;
}
return 0;
}
void UpdateService::checkForUpdates() {
if (m_manifestUrl.isEmpty()) {
emit checkFailed(QStringLiteral("Не настроен адрес обновлений (update/manifestUrl)"));
return;
}
QNetworkRequest req((QUrl(m_manifestUrl)));
QNetworkReply *reply = m_manager->get(req);
if (!awaitReply(reply, kManifestTimeoutMs)) {
reply->abort();
reply->deleteLater();
emit checkFailed(QStringLiteral("Таймаут соединения с сервером обновлений"));
return;
}
if (reply->error() != QNetworkReply::NoError) {
const QString err = reply->errorString();
reply->deleteLater();
emit checkFailed(QStringLiteral("Сеть: ") + err);
return;
}
const QByteArray body = reply->readAll();
reply->deleteLater();
const QJsonDocument doc = QJsonDocument::fromJson(body);
if (!doc.isObject()) {
emit checkFailed(QStringLiteral("Некорректный формат манифеста"));
return;
}
const QJsonObject o = doc.object();
const QString latest = o.value("latestVersion").toString();
const QString zipUrl = o.value("zipUrl").toString();
const QString sha = o.value("sha256").toString();
const QString notes = o.value("notes").toString();
if (latest.isEmpty() || zipUrl.isEmpty()) {
emit checkFailed(QStringLiteral("В манифесте нет latestVersion/zipUrl"));
return;
}
if (compareVersions(latest, m_currentVersion) <= 0) {
emit upToDate(m_currentVersion);
return;
}
emit updateAvailable(latest, notes, zipUrl, sha);
}
void UpdateService::downloadAndApply(const QString &zipUrl,
const QString &expectedSha256,
const QString &newVersion) {
const QString root = stagingRoot();
// Чистим прошлый стейджинг, если остался.
QDir(root).removeRecursively();
if (!QDir().mkpath(root)) {
emit stageFailed(QStringLiteral("Не удалось создать временную папку"));
return;
}
const QString zipFile = root + QStringLiteral("/ARC_x64_") + newVersion + QStringLiteral(".zip");
if (!downloadZip(zipUrl, zipFile)) {
QDir(root).removeRecursively();
emit stageFailed(QStringLiteral("Не удалось скачать обновление"));
return;
}
if (!expectedSha256.isEmpty() && !verifySha256(zipFile, expectedSha256)) {
QDir(root).removeRecursively();
emit stageFailed(QStringLiteral("Контрольная сумма архива не совпала"));
return;
}
const QString unpacked = root + QStringLiteral("/unpacked");
if (!QDir().mkpath(unpacked)) {
QDir(root).removeRecursively();
emit stageFailed(QStringLiteral("Не удалось создать папку распаковки"));
return;
}
if (!unpackZip(zipFile, unpacked)) {
QDir(root).removeRecursively();
emit stageFailed(QStringLiteral("Не удалось распаковать обновление"));
return;
}
if (!QFile::exists(unpacked + QStringLiteral("/ARC.exe"))) {
QDir(root).removeRecursively();
emit stageFailed(QStringLiteral("В архиве не найден ARC.exe"));
return;
}
if (!spawnUpdater(unpacked)) {
QDir(root).removeRecursively();
emit stageFailed(QStringLiteral("Не удалось запустить апдейтер"));
return;
}
emit updateStaged();
}
bool UpdateService::downloadZip(const QString &zipUrl, const QString &destFile) {
QFile file(destFile);
if (!file.open(QIODevice::WriteOnly)) {
qWarning() << "[UpdateService] cannot open" << destFile << "for write";
return false;
}
QNetworkRequest req((QUrl(zipUrl)));
QNetworkReply *reply = m_manager->get(req);
QObject::connect(reply, &QNetworkReply::downloadProgress, this,
[this](qint64 r, qint64 t) { emit downloadProgress(r, t); });
QObject::connect(reply, &QNetworkReply::readyRead, reply,
[reply, &file]() { file.write(reply->readAll()); });
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
file.write(reply->readAll());
file.close();
const bool ok = reply->error() == QNetworkReply::NoError;
if (!ok) qWarning() << "[UpdateService] download error:" << reply->errorString();
reply->deleteLater();
return ok;
}
bool UpdateService::verifySha256(const QString &file, const QString &expectedHex) {
QFile f(file);
if (!f.open(QIODevice::ReadOnly)) return false;
QCryptographicHash hash(QCryptographicHash::Sha256);
if (!hash.addData(&f)) return false;
const QString got = QString::fromLatin1(hash.result().toHex());
return got.compare(expectedHex.trimmed(), Qt::CaseInsensitive) == 0;
}
bool UpdateService::unpackZip(const QString &zipFile, const QString &destDir) {
#ifdef Q_OS_WIN
// Expand-Archive — тот же инструмент, что создаёт архив в build_x64.bat.
const QString cmd = QStringLiteral(
"Expand-Archive -Path '%1' -DestinationPath '%2' -Force").arg(zipFile, destDir);
const int rc = QProcess::execute(QStringLiteral("powershell"),
{QStringLiteral("-NoProfile"), QStringLiteral("-Command"), cmd});
return rc == 0;
#else
// Для локальной разработки/тестов на macOS/Linux.
const int rc = QProcess::execute(QStringLiteral("unzip"),
{QStringLiteral("-o"), zipFile, QStringLiteral("-d"), destDir});
return rc == 0;
#endif
}
bool UpdateService::spawnUpdater(const QString &unpackedDir) {
#ifdef Q_OS_WIN
const QString updaterExe = unpackedDir + QStringLiteral("/ARC.exe");
if (!QFile::exists(updaterExe)) {
qWarning() << "[UpdateService] updater exe not found:" << updaterExe;
return false;
}
const QString installDir = QCoreApplication::applicationDirPath();
const QString installedExe = QCoreApplication::applicationFilePath();
const qint64 pid = QCoreApplication::applicationPid();
const QStringList args = {
QStringLiteral("--apply-update"),
QStringLiteral("--pid"), QString::number(pid),
QStringLiteral("--src"), unpackedDir,
QStringLiteral("--dst"), installDir,
QStringLiteral("--relaunch"), installedExe,
};
// Рабочая папка — нейтральный temp, чтобы апдейтер ничего не держал залоченным.
const QString workDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
const bool ok = QProcess::startDetached(updaterExe, args, workDir);
if (!ok) qWarning() << "[UpdateService] failed to start updater process";
return ok;
#else
Q_UNUSED(unpackedDir);
qWarning() << "[UpdateService] spawnUpdater is Windows-only";
return false;
#endif
}

View File

@ -0,0 +1,61 @@
#pragma once
#include <QObject>
#include <QString>
class QNetworkAccessManager;
// Сервис ручной проверки и установки обновлений (Windows). Живёт в собственном
// QThread, как NetworkService. Полный цикл: GET манифеста → сравнение версий →
// скачивание zip → проверка sha256 → распаковка → запуск распакованного
// ARC.exe в режиме --apply-update → выход приложения (UI делает qApp->quit()).
//
// Установку выполняет НЕ это приложение, а свежераспакованная копия ARC.exe
// (в ней есть все Qt-DLL): она ждёт выхода текущего процесса, копирует файлы
// поверх install-папки (сохраняя database/), перезапускает установленный
// ARC.exe и самоудаляет временную папку.
class UpdateService final : public QObject {
Q_OBJECT
public:
explicit UpdateService(QObject *parent = nullptr);
~UpdateService() override;
// Сравнение версий вида "MAJOR.MINOR.PATCH" покомпонентно как чисел.
// <0 если a<b, 0 если равны, >0 если a>b. Недостающие сегменты считаются 0,
// поэтому "1.0.9" < "1.0.64" (в отличие от строкового сравнения).
static int compareVersions(const QString &a, const QString &b);
public slots:
// Шаг 1. GET манифеста, сравнение с локальной версией из config.ini.
// Эмитит upToDate / updateAvailable / checkFailed.
void checkForUpdates();
// Шаг 2. Скачать zip → sha256 → распаковать → запустить апдейтер.
// Вызывается из UI после подтверждения пользователя (через invokeMethod).
// Эмитит downloadProgress по ходу, затем updateStaged либо stageFailed.
void downloadAndApply(const QString &zipUrl,
const QString &expectedSha256,
const QString &newVersion);
signals:
void upToDate(const QString &currentVersion);
void updateAvailable(const QString &newVersion, const QString &notes,
const QString &zipUrl, const QString &sha256);
void checkFailed(const QString &reason);
void downloadProgress(qint64 received, qint64 total);
void stageFailed(const QString &reason);
// Всё готово, апдейтер запущен. UI должен вызвать qApp->quit().
void updateStaged();
private:
QString m_manifestUrl;
QString m_currentVersion;
QNetworkAccessManager *m_manager;
bool downloadZip(const QString &zipUrl, const QString &destFile);
static bool verifySha256(const QString &file, const QString &expectedHex);
bool unpackZip(const QString &zipFile, const QString &destDir);
// Запускает <unpackedDir>/ARC.exe --apply-update с PID/путями. На не-Windows
// возвращает false (апдейтер только для Windows).
bool spawnUpdater(const QString &unpackedDir);
};

View File

@ -28,8 +28,12 @@
#include "dao/SettingsDAO.h"
#include <QTimer>
#include <QCoreApplication>
#include <QApplication>
#include <QProgressDialog>
#include <QSettings>
#include "widget/common/FlowLayout.h"
#include "update/UpdateService.h"
#include "update/UpdatePause.h"
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
const QSettings settings("config.ini", QSettings::IniFormat);
@ -64,12 +68,15 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
auto *tutorialPageAction = new QAction("Инструкция", this);
auto *eventAction = new QAction("События", this);
auto *fileLogAction = new QAction("Логи", this);
auto *updateAction = new QAction("Проверить обновления", this);
m_updateAction = updateAction;
auto *exitAction = new QAction("Выйти", this);
menu->addAction(devicesPageAction);
// menu->addAction(accountPageAction);
menu->addAction(eventAction);
menu->addAction(tutorialPageAction);
menu->addAction(fileLogAction);
menu->addAction(updateAction);
menu->addAction(exitAction);
connect(devicesPageAction, &QAction::triggered, this, [=]() {
@ -117,6 +124,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
}
});
connect(updateAction, &QAction::triggered, this, &MainWindow::checkForUpdates);
connect(exitAction, &QAction::triggered, this, [this]() {
QMessageBox msgBox(this);
msgBox.setWindowTitle("Выход");
@ -162,6 +171,94 @@ void MainWindow::setSyncHealth(bool healthy, int consecutiveFailures) {
m_syncHealthLabel->setVisible(true);
}
void MainWindow::checkForUpdates() {
if (m_updateAction) m_updateAction->setEnabled(false);
auto reEnable = [this]() { if (m_updateAction) m_updateAction->setEnabled(true); };
// UpdateService живёт в своём потоке (как NetworkService). Сигналы приходят
// в главный поток через QueuedConnection — диалоги показываем здесь.
auto *thread = new QThread;
auto *svc = new UpdateService;
svc->moveToThread(thread);
connect(thread, &QThread::started, svc, &UpdateService::checkForUpdates);
connect(svc, &QObject::destroyed, thread, &QThread::quit);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
connect(svc, &UpdateService::upToDate, this, [this, svc, reEnable](const QString &ver) {
QMessageBox::information(this, "Обновление",
QString("У вас последняя версия (%1).").arg(ver));
reEnable();
svc->deleteLater();
});
connect(svc, &UpdateService::checkFailed, this, [this, svc, reEnable](const QString &reason) {
QMessageBox::warning(this, "Обновление",
"Не удалось проверить обновления:\n" + reason);
reEnable();
svc->deleteLater();
});
connect(svc, &UpdateService::updateAvailable, this,
[this, svc, reEnable](const QString &ver, const QString &notes,
const QString &zipUrl, const QString &sha) {
QString text = QString("Доступна новая версия: %1.\nУстановить сейчас?\n\n"
"Приложение перезапустится автоматически.").arg(ver);
if (!notes.isEmpty()) text += "\n\n" + notes;
const auto btn = QMessageBox::question(this, "Обновление", text,
QMessageBox::Yes | QMessageBox::No);
if (btn != QMessageBox::Yes) {
reEnable();
svc->deleteLater();
return;
}
// Ставим паузу: EventHandler перестаёт опрашивать задачи, а
// периодический sync — перепушивать "active" на сервер. Затем сразу
// выключаем все профили на сервере (на выходе stop() повторит идемпотентно).
UpdatePause::setPaused(true);
if (m_network) {
QMetaObject::invokeMethod(m_network, "disableAllProfilesOnServer",
Qt::QueuedConnection);
}
auto *progress = new QProgressDialog("Загрузка обновления…", QString(), 0, 100, this);
progress->setWindowTitle("Обновление");
progress->setWindowModality(Qt::WindowModal);
progress->setMinimumDuration(0);
progress->setAutoClose(false);
progress->setAutoReset(false);
progress->setCancelButton(nullptr);
progress->setValue(0);
connect(svc, &UpdateService::downloadProgress, progress,
[progress](qint64 r, qint64 t) {
if (t > 0) progress->setValue(static_cast<int>(r * 100 / t));
});
connect(svc, &UpdateService::stageFailed, this,
[this, svc, progress, reEnable](const QString &reason) {
progress->close();
progress->deleteLater();
// Обновление не удалось — снимаем паузу, приложение продолжает
// работу. Периодический sync восстановит профили на сервере.
UpdatePause::setPaused(false);
QMessageBox::warning(this, "Обновление", "Ошибка установки:\n" + reason);
reEnable();
svc->deleteLater();
});
connect(svc, &UpdateService::updateStaged, this, [progress]() {
progress->setLabelText("Перезапуск…");
progress->setValue(100);
qApp->quit(); // штатное завершение: aboutToQuit → stop() → disable
});
QMetaObject::invokeMethod(svc, "downloadAndApply", Qt::QueuedConnection,
Q_ARG(QString, zipUrl), Q_ARG(QString, sha),
Q_ARG(QString, ver));
});
thread->start();
}
void MainWindow::createDevicePage() {
if (m_devicesWindow) {

View File

@ -13,6 +13,9 @@
#include "log/FileLogWindow.h"
#include "widget/common/FlowLayout.h"
class NetworkService;
class QAction;
class MainWindow final : public QMainWindow {
Q_OBJECT
@ -21,6 +24,10 @@ public:
void loadDevices();
// Указатель на живой NetworkService (создаётся в main.cpp). Нужен, чтобы при
// старте обновления выключить все профили на сервере. Может быть nullptr.
void setNetworkService(NetworkService *net) { m_network = net; }
public slots:
// Подключается к NetworkService::syncHealthChanged. healthy=false показывает
// индикатор «нет связи» в status bar, healthy=true прячет (правило #9).
@ -45,8 +52,14 @@ private:
QListWidget *m_logList = nullptr;
QLabel *m_syncHealthLabel = nullptr;
NetworkService *m_network = nullptr;
QAction *m_updateAction = nullptr;
void createDevicePage();
void deleteDevicePage();
void updateLogPanel();
// Ручная проверка обновлений: поднимает UpdateService в отдельном потоке,
// показывает диалоги/прогресс, на готовности апдейтера завершает приложение.
void checkForUpdates();
void resizeEvent(QResizeEvent *event) override;
};