#include "UpdateService.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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(); 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, 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 }