1
0
forked from BRT/arc
arc/services/update/UpdateService.cpp

242 lines
9.0 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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
}