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:
parent
992073d43d
commit
ce61986035
@ -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!"
|
||||
|
||||
@ -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
|
||||
|
||||
12
main.cpp
12
main.cpp
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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";
|
||||
|
||||
129
services/update/ApplyUpdate.cpp
Normal file
129
services/update/ApplyUpdate.cpp
Normal 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 сам ретраит залоченные файлы; коды возврата 0–7 = успех.
|
||||
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
|
||||
}
|
||||
14
services/update/ApplyUpdate.h
Normal file
14
services/update/ApplyUpdate.h
Normal 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);
|
||||
16
services/update/UpdatePause.h
Normal file
16
services/update/UpdatePause.h
Normal 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
|
||||
241
services/update/UpdateService.cpp
Normal file
241
services/update/UpdateService.cpp
Normal 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
|
||||
}
|
||||
61
services/update/UpdateService.h
Normal file
61
services/update/UpdateService.h
Normal 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 ¤tVersion);
|
||||
void updateAvailable(const QString &newVersion, const QString ¬es,
|
||||
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);
|
||||
};
|
||||
@ -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 ¬es,
|
||||
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) {
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user