fix: 优化客户端错误提示和校验诊断信息
This commit is contained in:
+367
-209
@@ -25,16 +25,32 @@
|
||||
#include <openssl/err.h>
|
||||
#endif
|
||||
|
||||
UpdaterLogic::UpdaterLogic(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
m_serverAddr = ConfigHelper::instance().getValue("Server", "api_base_url");
|
||||
}
|
||||
UpdaterLogic::UpdaterLogic(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
m_serverAddr = ConfigHelper::instance().getValue("Server", "api_base_url");
|
||||
}
|
||||
|
||||
namespace {
|
||||
QString serverDetailMessage(const QJsonObject& response)
|
||||
{
|
||||
const QJsonValue detail = response.value(QStringLiteral("detail"));
|
||||
if (detail.isObject()) {
|
||||
const QJsonObject obj = detail.toObject();
|
||||
const QString msg = obj.value(QStringLiteral("msg")).toString();
|
||||
if (!msg.isEmpty()) return msg;
|
||||
const QString error = obj.value(QStringLiteral("error")).toString();
|
||||
if (!error.isEmpty()) return error;
|
||||
}
|
||||
return detail.toString();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdaterLogic::getManifest(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
|
||||
{
|
||||
// Manifest 由服务端按版本动态生成,描述目标版本包含哪些文件以及每个文件的 SHA256。
|
||||
// Updater 先拿到 Manifest,再请求下载 URL,最后按 Manifest 校验本地文件。
|
||||
m_error.clear();
|
||||
QString url = m_serverAddr + "/api/v1/update/manifest";
|
||||
QJsonObject body;
|
||||
body["app_id"] = appId;
|
||||
@@ -42,7 +58,7 @@ void UpdaterLogic::getManifest(const QString& appId, const QString& channel, con
|
||||
body["version"] = targetVer;
|
||||
body["version_id"] = versionId;
|
||||
|
||||
m_http.postRequest(url, body, [this](int code, const QJsonObject& resp)
|
||||
m_http.postRequest(url, body, [this, appId, channel, targetVer, versionId](int code, const QJsonObject& resp)
|
||||
{
|
||||
qDebug() << "Manifest API returned code:" << code;
|
||||
m_manifest = QJsonObject();
|
||||
@@ -68,61 +84,82 @@ void UpdaterLogic::getManifest(const QString& appId, const QString& channel, con
|
||||
m_fileItems.append(fi);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Manifest response missing fields";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Failed to get manifest";
|
||||
}
|
||||
emit fetchUrlFinished();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Manifest response missing fields";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Target version manifest response is incomplete. Stage: download target manifest. App: %1, channel: %2, version: %3, version id: %4.")
|
||||
.arg(appId, channel, targetVer, QString::number(versionId));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Failed to get manifest";
|
||||
const QString detail = serverDetailMessage(resp);
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot download target version manifest. Stage: download target manifest. HTTP status: %1. App: %2, channel: %3, version: %4, version id: %5.%6")
|
||||
.arg(QString::number(code), appId, channel, targetVer, QString::number(versionId),
|
||||
detail.isEmpty() ? QString() : QCoreApplication::translate("UpdaterLogic", "\nServer message: %1").arg(detail));
|
||||
}
|
||||
emit fetchUrlFinished();
|
||||
});
|
||||
}
|
||||
|
||||
bool UpdaterLogic::verifySignature(const QByteArray& payload, const QString& signatureBase64, const QString& publicKeyPath) const
|
||||
{
|
||||
#ifndef HAVE_OPENSSL
|
||||
Q_UNUSED(payload);
|
||||
Q_UNUSED(signatureBase64);
|
||||
Q_UNUSED(publicKeyPath);
|
||||
qDebug() << "OpenSSL not available, cannot verify manifest signature";
|
||||
return false;
|
||||
#else
|
||||
QFile keyFile(publicKeyPath);
|
||||
if (!keyFile.open(QIODevice::ReadOnly))
|
||||
{
|
||||
qDebug() << "Cannot open public key file:" << publicKeyPath;
|
||||
return false;
|
||||
}
|
||||
#ifndef HAVE_OPENSSL
|
||||
Q_UNUSED(payload);
|
||||
Q_UNUSED(signatureBase64);
|
||||
Q_UNUSED(publicKeyPath);
|
||||
qDebug() << "OpenSSL not available, cannot verify manifest signature";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot verify manifest signature because OpenSSL support is unavailable. Stage: manifest signature verification.");
|
||||
return false;
|
||||
#else
|
||||
QFile keyFile(publicKeyPath);
|
||||
if (!keyFile.open(QIODevice::ReadOnly))
|
||||
{
|
||||
qDebug() << "Cannot open public key file:" << publicKeyPath;
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot open manifest public key. Stage: manifest signature verification. Public key path: %1.")
|
||||
.arg(publicKeyPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray keyData = keyFile.readAll();
|
||||
keyFile.close();
|
||||
|
||||
BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size());
|
||||
if (!bio)
|
||||
{
|
||||
qDebug() << "BIO_new_mem_buf failed";
|
||||
return false;
|
||||
}
|
||||
if (!bio)
|
||||
{
|
||||
qDebug() << "BIO_new_mem_buf failed";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot parse manifest public key buffer. Stage: manifest signature verification. Public key path: %1.")
|
||||
.arg(publicKeyPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
EVP_PKEY* pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
|
||||
BIO_free(bio);
|
||||
if (!pkey)
|
||||
{
|
||||
qDebug() << "PEM_read_bio_PUBKEY failed";
|
||||
return false;
|
||||
}
|
||||
if (!pkey)
|
||||
{
|
||||
qDebug() << "PEM_read_bio_PUBKEY failed";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Manifest public key is invalid. Stage: manifest signature verification. Public key path: %1.")
|
||||
.arg(publicKeyPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray signature = QByteArray::fromBase64(signatureBase64.toUtf8());
|
||||
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
|
||||
if (!mdctx)
|
||||
{
|
||||
EVP_PKEY_free(pkey);
|
||||
qDebug() << "EVP_MD_CTX_new failed";
|
||||
return false;
|
||||
}
|
||||
if (!mdctx)
|
||||
{
|
||||
EVP_PKEY_free(pkey);
|
||||
qDebug() << "EVP_MD_CTX_new failed";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot create OpenSSL verification context. Stage: manifest signature verification.");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
if (EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pkey) == 1)
|
||||
@@ -139,29 +176,35 @@ bool UpdaterLogic::verifySignature(const QByteArray& payload, const QString& sig
|
||||
EVP_MD_CTX_free(mdctx);
|
||||
EVP_PKEY_free(pkey);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
qDebug() << "Manifest signature verification failed";
|
||||
}
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
if (!ok)
|
||||
{
|
||||
qDebug() << "Manifest signature verification failed";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Manifest RSA signature is invalid. Stage: manifest signature verification. This usually means the manifest was not signed by the matching server private key, the client public key is wrong, or the manifest content was changed.");
|
||||
}
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool UpdaterLogic::verifyManifestSignature(const QString& publicKeyPath) const
|
||||
{
|
||||
// 验签用的是客户端随 SDK 分发的公钥。
|
||||
// 只要服务端私钥没有泄漏,客户端就能发现被篡改的 Manifest。
|
||||
if (m_manifest.isEmpty() || m_manifestText.isEmpty())
|
||||
{
|
||||
qDebug() << "No manifest available to verify";
|
||||
return false;
|
||||
}
|
||||
{
|
||||
qDebug() << "No manifest available to verify";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"No manifest is available for signature verification. Stage: manifest signature verification. The target manifest may not have been downloaded successfully.");
|
||||
return false;
|
||||
}
|
||||
QString signature = m_manifest.value("signature").toString();
|
||||
if (signature.isEmpty())
|
||||
{
|
||||
qDebug() << "Manifest signature empty";
|
||||
return false;
|
||||
}
|
||||
if (signature.isEmpty())
|
||||
{
|
||||
qDebug() << "Manifest signature empty";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"The manifest does not contain a signature. Stage: manifest signature verification.");
|
||||
return false;
|
||||
}
|
||||
|
||||
QString path = publicKeyPath;
|
||||
if (!QFile::exists(path))
|
||||
@@ -175,18 +218,29 @@ bool UpdaterLogic::saveManifestCache(const QString& cacheDir) const
|
||||
{
|
||||
// 启动后的完整性检查依赖本地 Manifest 缓存。
|
||||
// 升级成功后缓存签名清单,下一次离线启动也能校验当前版本文件。
|
||||
if (m_manifest.isEmpty())
|
||||
return false;
|
||||
|
||||
QDir dir(cacheDir);
|
||||
if (!dir.exists() && !dir.mkpath("."))
|
||||
return false;
|
||||
if (m_manifest.isEmpty()) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot save manifest cache because the target manifest is empty. Stage: save target manifest cache.");
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir dir(cacheDir);
|
||||
if (!dir.exists() && !dir.mkpath(".")) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot create manifest cache directory. Stage: save target manifest cache. Directory: %1.")
|
||||
.arg(cacheDir);
|
||||
return false;
|
||||
}
|
||||
|
||||
QString version = m_manifest.value("version").toString();
|
||||
QString filePath = cacheDir + "/manifest_" + version + ".json";
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
||||
return false;
|
||||
QString filePath = cacheDir + "/manifest_" + version + ".json";
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot write manifest cache file. Stage: save target manifest cache. File: %1. Error: %2.")
|
||||
.arg(filePath, file.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject wrapper;
|
||||
wrapper["manifest"] = m_manifest;
|
||||
@@ -194,33 +248,48 @@ bool UpdaterLogic::saveManifestCache(const QString& cacheDir) const
|
||||
|
||||
QJsonDocument doc(wrapper);
|
||||
file.write(doc.toJson(QJsonDocument::Indented));
|
||||
file.close();
|
||||
qDebug() << "Manifest cached to" << filePath;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::loadManifestCache(const QString& cacheDir, const QString& version)
|
||||
{
|
||||
QString filePath = cacheDir + "/manifest_" + version + ".json";
|
||||
QFile file(filePath);
|
||||
if (!file.exists() || !file.open(QIODevice::ReadOnly))
|
||||
return false;
|
||||
file.close();
|
||||
qDebug() << "Manifest cached to" << filePath;
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::loadManifestCache(const QString& cacheDir, const QString& version)
|
||||
{
|
||||
m_error.clear();
|
||||
QString filePath = cacheDir + "/manifest_" + version + ".json";
|
||||
QFile file(filePath);
|
||||
if (!file.exists() || !file.open(QIODevice::ReadOnly)) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot read cached signed manifest. Stage: read local manifest cache. Version: %1. File: %2. This cache is created after a version is installed successfully.")
|
||||
.arg(version, filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray raw = file.readAll();
|
||||
file.close();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(raw);
|
||||
if (!doc.isObject())
|
||||
return false;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(raw);
|
||||
if (!doc.isObject()) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cached signed manifest is not valid JSON. Stage: read local manifest cache. Version: %1. File: %2.")
|
||||
.arg(version, filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject wrapper = doc.object();
|
||||
if (!wrapper.contains("manifest") || !wrapper.contains("manifest_text")) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cached signed manifest is incomplete. Stage: read local manifest cache. Version: %1. File: %2.")
|
||||
.arg(version, filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject wrapper = doc.object();
|
||||
if (!wrapper.contains("manifest") || !wrapper.contains("manifest_text"))
|
||||
return false;
|
||||
|
||||
m_manifest = wrapper["manifest"].toObject();
|
||||
m_manifestText = wrapper["manifest_text"].toString();
|
||||
qDebug() << "Loaded cached manifest" << version;
|
||||
return true;
|
||||
}
|
||||
m_manifest = wrapper["manifest"].toObject();
|
||||
m_manifestText = wrapper["manifest_text"].toString();
|
||||
qDebug() << "Loaded cached manifest" << version;
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
QByteArray UpdaterLogic::canonicalManifestBytes(const QJsonObject& manifest) const
|
||||
{
|
||||
@@ -311,43 +380,63 @@ QStringList UpdaterLogic::obsoleteFilesComparedTo(const QJsonObject& oldManifest
|
||||
return obsolete;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::validateLocalFiles(const QString& stagingDir, const QString& installedDir) const
|
||||
{
|
||||
if (m_manifest.isEmpty())
|
||||
return false;
|
||||
|
||||
const QJsonArray files = m_manifest.value("files").toArray();
|
||||
for (const auto& item : files)
|
||||
bool UpdaterLogic::validateLocalFiles(const QString& stagingDir, const QString& installedDir) const
|
||||
{
|
||||
m_error.clear();
|
||||
const QString stage = installedDir.isEmpty()
|
||||
? QCoreApplication::translate("UpdaterLogic", "installed version verification")
|
||||
: QCoreApplication::translate("UpdaterLogic", "downloaded/staged file verification");
|
||||
const QString version = m_manifest.value(QStringLiteral("version")).toString();
|
||||
if (m_manifest.isEmpty()) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"No manifest is available. Stage: %1. The updater cannot know which files and hashes should be verified.")
|
||||
.arg(stage);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonArray files = m_manifest.value("files").toArray();
|
||||
for (const auto& item : files)
|
||||
{
|
||||
const QJsonObject fileObject = item.toObject();
|
||||
const QString path = QDir::fromNativeSeparators(fileObject.value("path").toString());
|
||||
const QString expectedSha = fileObject.value("sha256").toString();
|
||||
if (!isSafeRelativePath(path))
|
||||
return false;
|
||||
if (isRuntimeProtectedPath(path)) {
|
||||
qDebug() << "Runtime-protected manifest entry ignored:" << path;
|
||||
continue;
|
||||
const QString path = QDir::fromNativeSeparators(fileObject.value("path").toString());
|
||||
const QString expectedSha = fileObject.value("sha256").toString();
|
||||
if (!isSafeRelativePath(path)) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Manifest contains an unsafe file path. Stage: %1. Version: %2. Path: %3.")
|
||||
.arg(stage, version, path);
|
||||
return false;
|
||||
}
|
||||
if (isRuntimeProtectedPath(path)) {
|
||||
qDebug() << "Runtime-protected manifest entry ignored:" << path;
|
||||
continue;
|
||||
}
|
||||
|
||||
QString fullPath = QDir(stagingDir).filePath(path);
|
||||
if (!QFile::exists(fullPath) && !installedDir.isEmpty())
|
||||
fullPath = QDir(installedDir).filePath(path);
|
||||
|
||||
if (!QFile::exists(fullPath))
|
||||
{
|
||||
qDebug() << "Manifest file missing from staging and installation:" << path;
|
||||
return false;
|
||||
}
|
||||
const QString actualSha = calcLocalFileSha256(fullPath);
|
||||
if (actualSha.compare(expectedSha, Qt::CaseInsensitive) != 0)
|
||||
{
|
||||
qDebug() << "File hash mismatch:" << path << actualSha << expectedSha;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
qDebug() << "Full manifest validation passed using staging plus installed files";
|
||||
return true;
|
||||
}
|
||||
if (!QFile::exists(fullPath))
|
||||
{
|
||||
qDebug() << "Manifest file missing from staging and installation:" << path;
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"A required file is missing. Stage: %1. Version: %2. Manifest path: %3. Checked path: %4. If this is a downloaded update, the file was not downloaded or staged correctly; if this is startup verification, the installed file may have been deleted.")
|
||||
.arg(stage, version, path, fullPath);
|
||||
return false;
|
||||
}
|
||||
const QString actualSha = calcLocalFileSha256(fullPath);
|
||||
if (actualSha.compare(expectedSha, Qt::CaseInsensitive) != 0)
|
||||
{
|
||||
qDebug() << "File hash mismatch:" << path << actualSha << expectedSha;
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"File SHA-256 does not match the signed manifest. Stage: %1. Version: %2. Manifest path: %3. Local path: %4.\nExpected SHA-256: %5\nActual SHA-256: %6\nIf this happens during download, clear the update cache and retry. If this happens during startup or after installation, the local file differs from the published version.")
|
||||
.arg(stage, version, path, fullPath, expectedSha, actualSha.isEmpty() ? QCoreApplication::translate("UpdaterLogic", "<cannot read file>") : actualSha);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
qDebug() << "Full manifest validation passed using staging plus installed files";
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString& stagingDir)
|
||||
{
|
||||
@@ -395,9 +484,10 @@ bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString&
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
|
||||
{
|
||||
QString url = m_serverAddr + "/api/v1/update/download-url";
|
||||
void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
|
||||
{
|
||||
m_error.clear();
|
||||
QString url = m_serverAddr + "/api/v1/update/download-url";
|
||||
QJsonObject body;
|
||||
body["app_id"] = appId;
|
||||
body["channel"] = channel;
|
||||
@@ -407,7 +497,7 @@ void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel,
|
||||
QJsonArray emptyFiles;
|
||||
body["files"] = emptyFiles;
|
||||
|
||||
m_http.postRequest(url, body, [this](int code, const QJsonObject& resp)
|
||||
m_http.postRequest(url, body, [this, appId, channel, targetVer, versionId](int code, const QJsonObject& resp)
|
||||
{
|
||||
qDebug() << "Download URL API returned code:" << code;
|
||||
m_fileItems.clear();
|
||||
@@ -427,13 +517,18 @@ void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel,
|
||||
qDebug() << "File info:" << fi.path << fi.url << fi.sha256;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Failed to get download URL";
|
||||
}
|
||||
emit fetchUrlFinished();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Failed to get download URL";
|
||||
const QString detail = serverDetailMessage(resp);
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot get secure download URLs. Stage: request download URLs. HTTP status: %1. App: %2, channel: %3, version: %4, version id: %5.%6")
|
||||
.arg(QString::number(code), appId, channel, targetVer, QString::number(versionId),
|
||||
detail.isEmpty() ? QString() : QCoreApplication::translate("UpdaterLogic", "\nServer message: %1").arg(detail));
|
||||
}
|
||||
emit fetchUrlFinished();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
QString UpdaterLogic::calcLocalFileSha256(const QString& filePath) const
|
||||
@@ -454,13 +549,21 @@ QString UpdaterLogic::calcLocalFileSha256(const QString& filePath) const
|
||||
bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePath,
|
||||
const QString& expectSha256, qint64 expectedSize,
|
||||
const QString& resumePartPath)
|
||||
{
|
||||
const QString partPath = resumePartPath.isEmpty() ? savePath + ".part" : resumePartPath;
|
||||
if (!QDir().mkpath(QFileInfo(partPath).path())) return false;
|
||||
if (QFile::exists(savePath)
|
||||
&& calcLocalFileSha256(savePath).compare(expectSha256, Qt::CaseInsensitive) == 0)
|
||||
return true;
|
||||
QFile::remove(savePath);
|
||||
{
|
||||
const QString partPath = resumePartPath.isEmpty() ? savePath + ".part" : resumePartPath;
|
||||
const QString displayPath = m_currentDownloadPath.isEmpty() ? savePath : m_currentDownloadPath;
|
||||
if (!QDir().mkpath(QFileInfo(partPath).path())) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot create partial download directory. Stage: download file. File: %1. Partial file: %2.")
|
||||
.arg(displayPath, partPath);
|
||||
return false;
|
||||
}
|
||||
if (QFile::exists(savePath)
|
||||
&& calcLocalFileSha256(savePath).compare(expectSha256, Qt::CaseInsensitive) == 0) {
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
QFile::remove(savePath);
|
||||
|
||||
for (int attempt = 0; attempt < 4; ++attempt)
|
||||
{
|
||||
@@ -472,23 +575,37 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
|
||||
}
|
||||
if (expectedSize >= 0 && existingSize == expectedSize && existingSize > 0)
|
||||
{
|
||||
if (calcLocalFileSha256(partPath).compare(expectSha256, Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
QFile::remove(savePath);
|
||||
return QFile::rename(partPath, savePath);
|
||||
}
|
||||
QFile::remove(partPath);
|
||||
existingSize = 0;
|
||||
}
|
||||
if (calcLocalFileSha256(partPath).compare(expectSha256, Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
QFile::remove(savePath);
|
||||
if (QFile::rename(partPath, savePath)) {
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Downloaded file passed SHA-256 verification, but cannot move it into the staging directory. Stage: download file. File: %1. From: %2. To: %3.")
|
||||
.arg(displayPath, partPath, savePath);
|
||||
return false;
|
||||
}
|
||||
const QString actualSha = calcLocalFileSha256(partPath);
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cached partial file has the expected size but wrong SHA-256. Stage: resume download. File: %1.\nExpected SHA-256: %2\nActual SHA-256: %3\nThe partial cache will be deleted and downloaded again.")
|
||||
.arg(displayPath, expectSha256, actualSha);
|
||||
QFile::remove(partPath);
|
||||
existingSize = 0;
|
||||
}
|
||||
|
||||
QFile partFile(partPath);
|
||||
const QIODevice::OpenMode mode = QIODevice::WriteOnly
|
||||
| (existingSize > 0 ? QIODevice::Append : QIODevice::Truncate);
|
||||
if (!partFile.open(mode))
|
||||
{
|
||||
qDebug() << "Cannot open partial download:" << partPath;
|
||||
return false;
|
||||
}
|
||||
if (!partFile.open(mode))
|
||||
{
|
||||
qDebug() << "Cannot open partial download:" << partPath;
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot open partial download file. Stage: download file. File: %1. Partial file: %2. Error: %3.")
|
||||
.arg(displayPath, partPath, partFile.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
QNetworkAccessManager manager;
|
||||
manager.setProxy(QNetworkProxy::NoProxy);
|
||||
@@ -533,27 +650,45 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
|
||||
else if (networkOk && writeOk && (httpStatus == 200 || httpStatus == 206))
|
||||
{
|
||||
const qint64 actualSize = QFileInfo(partPath).size();
|
||||
if ((expectedSize < 0 || actualSize == expectedSize)
|
||||
&& calcLocalFileSha256(partPath).compare(expectSha256, Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
QFile::remove(savePath);
|
||||
if (QFile::rename(partPath, savePath))
|
||||
{
|
||||
qDebug() << "Download completed/resumed & sha pass:" << savePath;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (expectedSize >= 0 && actualSize >= expectedSize)
|
||||
{
|
||||
qDebug() << "Completed partial file has invalid size or SHA; restart:" << savePath;
|
||||
QFile::remove(partPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Download attempt failed; partial file retained:" << attempt + 1
|
||||
<< savePath << httpStatus << networkError << "bytes" << QFileInfo(partPath).size();
|
||||
}
|
||||
if ((expectedSize < 0 || actualSize == expectedSize)
|
||||
&& calcLocalFileSha256(partPath).compare(expectSha256, Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
QFile::remove(savePath);
|
||||
if (QFile::rename(partPath, savePath))
|
||||
{
|
||||
qDebug() << "Download completed/resumed & sha pass:" << savePath;
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Downloaded file passed SHA-256 verification, but cannot move it into the staging directory. Stage: download file. File: %1. From: %2. To: %3.")
|
||||
.arg(displayPath, partPath, savePath);
|
||||
return false;
|
||||
}
|
||||
else if (expectedSize >= 0 && actualSize >= expectedSize)
|
||||
{
|
||||
qDebug() << "Completed partial file has invalid size or SHA; restart:" << savePath;
|
||||
const QString actualSha = calcLocalFileSha256(partPath);
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Downloaded file does not match the signed manifest. Stage: download file. File: %1. HTTP status: %2.\nExpected size: %3 bytes\nActual size: %4 bytes\nExpected SHA-256: %5\nActual SHA-256: %6\nThe partial cache will be deleted and downloaded again.")
|
||||
.arg(displayPath, QString::number(httpStatus), QString::number(expectedSize), QString::number(actualSize),
|
||||
expectSha256, actualSha.isEmpty() ? QCoreApplication::translate("UpdaterLogic", "<cannot read file>") : actualSha);
|
||||
QFile::remove(partPath);
|
||||
}
|
||||
else {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Downloaded file is incomplete. Stage: download file. File: %1. HTTP status: %2. Expected size: %3 bytes, current size: %4 bytes.")
|
||||
.arg(displayPath, QString::number(httpStatus), QString::number(expectedSize), QString::number(actualSize));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Download attempt failed; partial file retained:" << attempt + 1
|
||||
<< savePath << httpStatus << networkError << "bytes" << QFileInfo(partPath).size();
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Download request failed. Stage: download file. File: %1. Attempt: %2/4. HTTP status: %3. Network error: %4. Partial file: %5.")
|
||||
.arg(displayPath, QString::number(attempt + 1), QString::number(httpStatus), networkError, partPath);
|
||||
}
|
||||
|
||||
if (attempt < 3)
|
||||
{
|
||||
@@ -566,9 +701,14 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
|
||||
QThread::msleep(50);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (m_error.isEmpty()) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"File download failed after retries. Stage: download file. File: %1.")
|
||||
.arg(displayPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::isRuntimeProtectedPath(const QString& path) const
|
||||
{
|
||||
@@ -632,20 +772,31 @@ qint64 UpdaterLogic::estimateAdditionalDiskBytes(const QString& targetDir,
|
||||
return required + reserve;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targetDir)
|
||||
{
|
||||
if (m_fileItems.isEmpty())
|
||||
{
|
||||
m_downloadAllOk = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir stagingDir(tempDir);
|
||||
if (!FileHelper::createDir(tempDir))
|
||||
return false;
|
||||
const QString resumeCacheDir = QDir(ConfigHelper::instance().updateRoot()).filePath("download_cache");
|
||||
if (!FileHelper::createDir(resumeCacheDir))
|
||||
return false;
|
||||
bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targetDir)
|
||||
{
|
||||
m_error.clear();
|
||||
if (m_fileItems.isEmpty())
|
||||
{
|
||||
m_downloadAllOk = false;
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"The target version manifest contains no downloadable files. Stage: prepare downloads.");
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir stagingDir(tempDir);
|
||||
if (!FileHelper::createDir(tempDir)) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot create update staging directory. Stage: prepare downloads. Directory: %1.")
|
||||
.arg(tempDir);
|
||||
return false;
|
||||
}
|
||||
const QString resumeCacheDir = QDir(ConfigHelper::instance().updateRoot()).filePath("download_cache");
|
||||
if (!FileHelper::createDir(resumeCacheDir)) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot create download cache directory. Stage: prepare downloads. Directory: %1.")
|
||||
.arg(resumeCacheDir);
|
||||
return false;
|
||||
}
|
||||
QSet<QString> activePartialNames;
|
||||
for (const auto& item : m_fileItems)
|
||||
activePartialNames.insert(item.sha256.toLower() + ".part");
|
||||
@@ -671,12 +822,15 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
|
||||
|
||||
for (const auto& fi : m_fileItems)
|
||||
{
|
||||
if (!isSafeRelativePath(fi.path))
|
||||
{
|
||||
qDebug() << "Unsafe relative path in manifest:" << fi.path;
|
||||
m_downloadAllOk = false;
|
||||
return false;
|
||||
}
|
||||
if (!isSafeRelativePath(fi.path))
|
||||
{
|
||||
qDebug() << "Unsafe relative path in manifest:" << fi.path;
|
||||
m_downloadAllOk = false;
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Manifest contains an unsafe file path. Stage: prepare file download. Path: %1.")
|
||||
.arg(fi.path);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString relativePath = QDir::fromNativeSeparators(fi.path);
|
||||
if (isRuntimeProtectedPath(relativePath)) {
|
||||
@@ -693,12 +847,15 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
|
||||
|
||||
const QString tempFile = QDir(tempDir).filePath(relativePath);
|
||||
const QString parentDir = QFileInfo(tempFile).path();
|
||||
if (!QDir().mkpath(parentDir))
|
||||
{
|
||||
qDebug() << "Cannot create staging subdirectory:" << parentDir;
|
||||
m_downloadAllOk = false;
|
||||
return false;
|
||||
}
|
||||
if (!QDir().mkpath(parentDir))
|
||||
{
|
||||
qDebug() << "Cannot create staging subdirectory:" << parentDir;
|
||||
m_downloadAllOk = false;
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot create staging subdirectory. Stage: prepare file download. File: %1. Directory: %2.")
|
||||
.arg(relativePath, parentDir);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString resumePartPath = QDir(resumeCacheDir).filePath(fi.sha256.toLower() + ".part");
|
||||
m_currentDownloadPath = relativePath;
|
||||
@@ -717,9 +874,10 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
|
||||
m_currentDownloadPath, speed);
|
||||
}
|
||||
|
||||
m_downloadAllOk = true;
|
||||
return true;
|
||||
}
|
||||
m_downloadAllOk = true;
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdaterLogic::reportDownloadResult(const QString& appId, const QString& channel,
|
||||
const QString& version, bool success)
|
||||
|
||||
@@ -29,8 +29,9 @@ public:
|
||||
bool validateLocalFiles(const QString& stagingDir, const QString& installedDir = QString()) const;
|
||||
bool saveManifestCache(const QString& cacheDir) const;
|
||||
bool loadManifestCache(const QString& cacheDir, const QString& version);
|
||||
bool loadOfflinePackage(const QString& packagePath, const QString& stagingDir = QString());
|
||||
QString offlineError() const { return m_offlineError; }
|
||||
bool loadOfflinePackage(const QString& packagePath, const QString& stagingDir = QString());
|
||||
QString offlineError() const { return m_offlineError; }
|
||||
QString errorString() const { return m_error; }
|
||||
|
||||
void getDownloadUrl(const QString& appId, const QString& channel, const QString& targetVer, int versionId);
|
||||
void reportResult(const QString& deviceId, const QString& fromVer, const QString& toVer, bool success);
|
||||
@@ -63,11 +64,12 @@ private:
|
||||
QJsonObject m_manifest;
|
||||
QString m_manifestText;
|
||||
QList<FileDownloadItem> m_fileItems;
|
||||
bool m_downloadAllOk = false;
|
||||
bool m_downloadAllOk = false;
|
||||
qint64 m_downloadTotalBytes = 0;
|
||||
qint64 m_downloadCompletedBytes = 0;
|
||||
qint64 m_sessionDownloadedBytes = 0;
|
||||
QString m_currentDownloadPath;
|
||||
QString m_offlineError;
|
||||
QElapsedTimer m_downloadTimer;
|
||||
};
|
||||
QString m_currentDownloadPath;
|
||||
QString m_offlineError;
|
||||
mutable QString m_error;
|
||||
QElapsedTimer m_downloadTimer;
|
||||
};
|
||||
|
||||
+76
-40
@@ -3,9 +3,10 @@
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QMessageBox>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
#include <QProcess>
|
||||
#include <QProgressDialog>
|
||||
#include <QSaveFile>
|
||||
@@ -164,14 +165,19 @@ int main(int argc, char* argv[])
|
||||
if (offlinePackagePath.isEmpty())
|
||||
logic.reportResult(deviceId, fromVersion, targetVersion, success);
|
||||
};
|
||||
const auto fail = [&](const QString& title, const QString& message,
|
||||
const QString& errorCode = QString("update_failed")) {
|
||||
transaction.markFailed(errorCode, message);
|
||||
reportUpdateResult(false);
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, title, message);
|
||||
return -1;
|
||||
};
|
||||
const auto fail = [&](const QString& title, const QString& message,
|
||||
const QString& errorCode = QString("update_failed")) {
|
||||
transaction.markFailed(errorCode, message);
|
||||
reportUpdateResult(false);
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, title, message);
|
||||
return -1;
|
||||
};
|
||||
const auto withDetails = [](const QString& message, const QString& details) {
|
||||
return details.trimmed().isEmpty()
|
||||
? message
|
||||
: message + QCoreApplication::translate("Updater", "\n\nDetails:\n%1").arg(details);
|
||||
};
|
||||
const auto configuredName = [&](const QString& key, const QString& fallback) {
|
||||
return ConfigHelper::executableNameForCurrentPlatform(
|
||||
config.getValue("Runtime", key), fallback);
|
||||
@@ -182,25 +188,44 @@ int main(int argc, char* argv[])
|
||||
bool timeoutOk = false;
|
||||
int healthCheckTimeoutMs = config.getValue("Runtime", "health_check_timeout_ms").toInt(&timeoutOk);
|
||||
if (!timeoutOk || healthCheckTimeoutMs < 1000) healthCheckTimeoutMs = 15000;
|
||||
const QString mainAppPath = QDir(runtimeDir).filePath(mainExecutable);
|
||||
const QString updaterPath = QDir(runtimeDir).filePath(updaterExecutable);
|
||||
const QString bootstrapPath = QDir(runtimeDir).filePath(bootstrapExecutable);
|
||||
const QString launchToken = config.getValue("App", "launch_token");
|
||||
const auto launchMainApp = [&](const QString& healthFile = QString()) {
|
||||
QString ticketPath;
|
||||
QString ticketError;
|
||||
const QString launchVersion = config.getValue("App", "current_version");
|
||||
if (!TicketHelper::createTicket(appId, deviceId, launchVersion, launchToken,
|
||||
&ticketPath, &ticketError)) {
|
||||
qDebug() << "Cannot create launch ticket:" << ticketError;
|
||||
return false;
|
||||
}
|
||||
QStringList args{QString("--ticket-file=%1").arg(ticketPath)};
|
||||
if (!healthFile.isEmpty()) args.append(QString("--health-file=%1").arg(healthFile));
|
||||
const bool started = QProcess::startDetached(mainAppPath, args);
|
||||
if (!started) QFile::remove(ticketPath);
|
||||
return started;
|
||||
};
|
||||
const QString mainAppPath = QDir(runtimeDir).filePath(mainExecutable);
|
||||
const QString updaterPath = QDir(runtimeDir).filePath(updaterExecutable);
|
||||
const QString bootstrapPath = QDir(runtimeDir).filePath(bootstrapExecutable);
|
||||
const QString launchToken = config.getValue("App", "launch_token");
|
||||
QString mainStartupError;
|
||||
const auto launchMainApp = [&](const QString& healthFile = QString()) {
|
||||
mainStartupError.clear();
|
||||
if (!QFileInfo::exists(mainAppPath)) {
|
||||
mainStartupError = QCoreApplication::translate(
|
||||
"Updater",
|
||||
"Cannot start the main application because the executable file does not exist.\nExecutable: %1\nCheck main_executable and install_root in the generated client configuration.")
|
||||
.arg(mainAppPath);
|
||||
return false;
|
||||
}
|
||||
QString ticketPath;
|
||||
QString ticketError;
|
||||
const QString launchVersion = config.getValue("App", "current_version");
|
||||
if (!TicketHelper::createTicket(appId, deviceId, launchVersion, launchToken,
|
||||
&ticketPath, &ticketError)) {
|
||||
qDebug() << "Cannot create launch ticket:" << ticketError;
|
||||
mainStartupError = QCoreApplication::translate(
|
||||
"Updater",
|
||||
"Cannot start the main application because the one-time launch ticket could not be created.\nExecutable: %1\nDetails: %2")
|
||||
.arg(mainAppPath, ticketError);
|
||||
return false;
|
||||
}
|
||||
QStringList args{QString("--ticket-file=%1").arg(ticketPath)};
|
||||
if (!healthFile.isEmpty()) args.append(QString("--health-file=%1").arg(healthFile));
|
||||
const bool started = QProcess::startDetached(mainAppPath, args);
|
||||
if (!started) {
|
||||
QFile::remove(ticketPath);
|
||||
mainStartupError = QCoreApplication::translate(
|
||||
"Updater",
|
||||
"Cannot start the main application process.\nExecutable: %1\nTicket file: %2\nHealth file: %3\nCheck file permissions, dependent DLLs/shared libraries, and whether the executable can run independently.")
|
||||
.arg(mainAppPath, ticketPath, healthFile.isEmpty() ? QCoreApplication::translate("Updater", "<not used>") : healthFile);
|
||||
}
|
||||
return started;
|
||||
};
|
||||
const auto bootstrapPlanFile = [&]() {
|
||||
return QDir(updateDir).filePath("bootstrap_plan_" + transaction.transactionId() + ".txt");
|
||||
};
|
||||
@@ -282,16 +307,19 @@ int main(int argc, char* argv[])
|
||||
if (resumingFromBootstrap) {
|
||||
if (!logic.loadManifestCache(manifestCacheDir, targetVersion))
|
||||
return delegateRollback(QCoreApplication::translate("Updater", "Manifest Cache Failed"),
|
||||
QCoreApplication::translate("Updater", "Cannot read the signed manifest cache after Bootstrap installation."));
|
||||
withDetails(QCoreApplication::translate("Updater", "Cannot read the signed manifest cache after Bootstrap installation."),
|
||||
logic.errorString()));
|
||||
} else if (offlinePackagePath.isEmpty()) {
|
||||
logic.getManifest(appId, channel, targetVersion, targetVersionId);
|
||||
}
|
||||
if (!logic.verifyManifestSignature()) {
|
||||
if (resumingFromBootstrap)
|
||||
return delegateRollback(QCoreApplication::translate("Updater", "Security Verification Failed"),
|
||||
QCoreApplication::translate("Updater", "Cannot reverify the manifest signature after Bootstrap installation."));
|
||||
withDetails(QCoreApplication::translate("Updater", "Cannot reverify the manifest signature after Bootstrap installation."),
|
||||
logic.errorString()));
|
||||
return fail(QCoreApplication::translate("Updater", "Security Verification Failed"),
|
||||
QCoreApplication::translate("Updater", "The version manifest signature is invalid. The update has stopped. Please contact the administrator."),
|
||||
withDetails(QCoreApplication::translate("Updater", "The version manifest signature is invalid. The update has stopped. Please contact the administrator."),
|
||||
logic.errorString()),
|
||||
"manifest_signature_invalid");
|
||||
}
|
||||
QStringList obsoletePaths;
|
||||
@@ -309,9 +337,11 @@ int main(int argc, char* argv[])
|
||||
if (!logic.saveManifestCache(manifestCacheDir)) {
|
||||
if (resumingFromBootstrap)
|
||||
return delegateRollback(QCoreApplication::translate("Updater", "Manifest Cache Failed"),
|
||||
QCoreApplication::translate("Updater", "Cannot save the new version manifest cache."));
|
||||
withDetails(QCoreApplication::translate("Updater", "Cannot save the new version manifest cache."),
|
||||
logic.errorString()));
|
||||
return fail(QCoreApplication::translate("Updater", "Manifest Cache Failed"),
|
||||
QCoreApplication::translate("Updater", "Cannot save the new version manifest cache. The update has stopped."),
|
||||
withDetails(QCoreApplication::translate("Updater", "Cannot save the new version manifest cache. The update has stopped."),
|
||||
logic.errorString()),
|
||||
"manifest_cache_failed");
|
||||
}
|
||||
|
||||
@@ -324,7 +354,8 @@ int main(int argc, char* argv[])
|
||||
logic.getDownloadUrl(appId, channel, targetVersion, targetVersionId);
|
||||
if (logic.getFileList().isEmpty())
|
||||
return fail(QCoreApplication::translate("Updater", "No Files to Update"),
|
||||
QCoreApplication::translate("Updater", "The server did not return any version files. The update has stopped."),
|
||||
withDetails(QCoreApplication::translate("Updater", "The server did not return any version files. The update has stopped."),
|
||||
logic.errorString()),
|
||||
"empty_file_list");
|
||||
|
||||
QStorageInfo storage(updateDir);
|
||||
@@ -353,7 +384,8 @@ int main(int argc, char* argv[])
|
||||
if (!logic.downloadAllFiles(stagingDir, targetDir)) {
|
||||
logic.reportDownloadResult(appId, channel, targetVersion, false);
|
||||
return fail(QCoreApplication::translate("Updater", "Download Failed"),
|
||||
QCoreApplication::translate("Updater", "Some files failed to download or failed SHA-256 verification. Please check the network and try again."),
|
||||
withDetails(QCoreApplication::translate("Updater", "Some files failed to download or failed SHA-256 verification. Please check the network, update cache, or server release files and try again."),
|
||||
logic.errorString()),
|
||||
"download_failed");
|
||||
}
|
||||
logic.reportDownloadResult(appId, channel, targetVersion, true);
|
||||
@@ -362,7 +394,8 @@ int main(int argc, char* argv[])
|
||||
setProgress(58, QCoreApplication::translate("Updater", "Verifying complete version files..."));
|
||||
if (!logic.validateLocalFiles(stagingDir, targetDir))
|
||||
return fail(QCoreApplication::translate("Updater", "File Verification Failed"),
|
||||
QCoreApplication::translate("Updater", "The staged files do not match the version manifest. The update has stopped."),
|
||||
withDetails(QCoreApplication::translate("Updater", "The downloaded/staged files do not match the target version manifest. The update has stopped before replacing installed files."),
|
||||
logic.errorString()),
|
||||
"staging_verify_failed");
|
||||
|
||||
QStringList changedPaths;
|
||||
@@ -441,7 +474,8 @@ int main(int argc, char* argv[])
|
||||
setProgress(82, QCoreApplication::translate("Updater", "Verifying Bootstrap installation result..."));
|
||||
if (!transaction.markPostVerify() || !logic.validateLocalFiles(targetDir))
|
||||
return delegateRollback(QCoreApplication::translate("Updater", "Installation Verification Failed"),
|
||||
QCoreApplication::translate("Updater", "New version files failed verification after installation."));
|
||||
withDetails(QCoreApplication::translate("Updater", "New version files failed verification after installation. The updater will roll back to the previous version."),
|
||||
logic.errorString()));
|
||||
for (const QString& path : transaction.obsoletePaths()) {
|
||||
if (QFile::exists(QDir(targetDir).filePath(path)))
|
||||
return delegateRollback(QCoreApplication::translate("Updater", "Obsolete File Cleanup Failed"),
|
||||
@@ -459,7 +493,9 @@ int main(int argc, char* argv[])
|
||||
setProgress(94, QCoreApplication::translate("Updater", "Starting the new version and waiting for health confirmation..."));
|
||||
if (!launchMainApp(healthFile))
|
||||
return delegateRollback(QCoreApplication::translate("Updater", "Startup Failed"),
|
||||
QCoreApplication::translate("Updater", "%1 cannot be started.").arg(mainExecutable));
|
||||
mainStartupError.isEmpty()
|
||||
? QCoreApplication::translate("Updater", "%1 cannot be started.").arg(mainExecutable)
|
||||
: mainStartupError);
|
||||
|
||||
QElapsedTimer healthTimer;
|
||||
healthTimer.start();
|
||||
|
||||
Reference in New Issue
Block a user