diff --git a/Common/ConfigHelper.cpp b/Common/ConfigHelper.cpp index e58edcb..e20c5f4 100644 --- a/Common/ConfigHelper.cpp +++ b/Common/ConfigHelper.cpp @@ -228,7 +228,7 @@ bool runElevatedSelfCommand(const QStringList& arguments, const QString& targetP const QMessageBox::StandardButton choice = QMessageBox::question( nullptr, QCoreApplication::translate("ConfigHelper", "Administrator Permission Required"), - QCoreApplication::translate("ConfigHelper", "The current installation directory requires administrator permission to save configuration.\n\nTarget file: %1\nReason: %2\n\nClick OK, then choose Yes in the Windows permission confirmation dialog.") + QCoreApplication::translate("ConfigHelper", "The current operation needs administrator permission to modify a protected file.\n\nTarget file: %1\nReason: %2\n\nClick OK, then choose Yes in the Windows permission confirmation dialog.") .arg(QDir::toNativeSeparators(targetPath), originalError), QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok); diff --git a/Common/DeviceIdentityHelper.cpp b/Common/DeviceIdentityHelper.cpp index d133d48..40f88a5 100644 --- a/Common/DeviceIdentityHelper.cpp +++ b/Common/DeviceIdentityHelper.cpp @@ -257,12 +257,34 @@ bool DeviceIdentityHelper::ensureIssued( timer.stop(); const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + const QString networkError = reply->errorString(); const QByteArray raw = reply->readAll(); reply->deleteLater(); if (status != 200) { - m_error = QCoreApplication::translate("DeviceIdentityHelper", "Device identity request failed (HTTP %1): %2") - .arg(status) - .arg(QString::fromUtf8(raw)); + QJsonParseError responseError; + const QJsonDocument errorDoc = QJsonDocument::fromJson(raw, &responseError); + QString serverMessage; + if (responseError.error == QJsonParseError::NoError && errorDoc.isObject()) { + const QJsonValue detail = errorDoc.object().value(QStringLiteral("detail")); + serverMessage = detail.isObject() + ? detail.toObject().value(QStringLiteral("msg")).toString() + : detail.toString(); + } + if (serverMessage.isEmpty()) + serverMessage = QString::fromUtf8(raw).trimmed(); + + if (status == 0) { + m_error = QCoreApplication::translate( + "DeviceIdentityHelper", + "Cannot contact the update server to issue device identity.\nServer: %1\nApp: %2\nChannel: %3\nNetwork error: %4\nTimeout: %5 ms") + .arg(trimmedBaseUrl, appId, channel, networkError, QString::number(timeoutMs)); + } else { + m_error = QCoreApplication::translate( + "DeviceIdentityHelper", + "Device identity request was rejected by the update server.\nServer: %1\nHTTP status: %2\nApp: %3\nChannel: %4\nServer message: %5") + .arg(trimmedBaseUrl, QString::number(status), appId, channel, + serverMessage.isEmpty() ? QCoreApplication::translate("DeviceIdentityHelper", "") : serverMessage); + } return false; } diff --git a/Common/IntegrityHelper.cpp b/Common/IntegrityHelper.cpp index 7551828..d29f960 100644 --- a/Common/IntegrityHelper.cpp +++ b/Common/IntegrityHelper.cpp @@ -5,8 +5,9 @@ #include #include #include -#include -#include +#include +#include +#include #include #include #ifdef HAVE_OPENSSL @@ -58,19 +59,32 @@ QString IntegrityHelper::sha256(const QString& filePath) const bool IntegrityHelper::verifySignature(const QByteArray& payload, const QString& signatureBase64) { -#ifndef HAVE_OPENSSL - Q_UNUSED(payload); Q_UNUSED(signatureBase64); m_error = "OpenSSL unavailable"; return false; -#else +#ifndef HAVE_OPENSSL + Q_UNUSED(payload); Q_UNUSED(signatureBase64); + m_error = QCoreApplication::translate("IntegrityHelper", + "Cannot verify signed manifest because OpenSSL support is unavailable. Stage: installed version verification."); + return false; +#else QString keyPath = QDir(m_installDir).filePath("config/manifest_public_key.pem"); - if (!QFile::exists(keyPath)) - keyPath = QFileInfo(ConfigHelper::instance().configPath()).dir().filePath("manifest_public_key.pem"); - QFile keyFile(keyPath); - if (!keyFile.open(QIODevice::ReadOnly)) { m_error = "manifest public key missing"; return false; } + if (!QFile::exists(keyPath)) + keyPath = QFileInfo(ConfigHelper::instance().configPath()).dir().filePath("manifest_public_key.pem"); + QFile keyFile(keyPath); + if (!keyFile.open(QIODevice::ReadOnly)) { + m_error = QCoreApplication::translate("IntegrityHelper", + "Cannot open manifest public key. Stage: installed version verification. Public key path: %1.") + .arg(keyPath); + return false; + } const QByteArray keyData = keyFile.readAll(); BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size()); EVP_PKEY* key = bio ? PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr) : nullptr; if (bio) BIO_free(bio); - if (!key) { m_error = "manifest public key invalid"; return false; } + if (!key) { + m_error = QCoreApplication::translate("IntegrityHelper", + "Manifest public key is invalid. Stage: installed version verification. Public key path: %1.") + .arg(keyPath); + return false; + } EVP_MD_CTX* ctx = EVP_MD_CTX_new(); const QByteArray signature = QByteArray::fromBase64(signatureBase64.toUtf8()); const bool ok = ctx && EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, key) == 1 @@ -78,10 +92,13 @@ bool IntegrityHelper::verifySignature(const QByteArray& payload, const QString& && EVP_DigestVerifyFinal(ctx, reinterpret_cast(signature.constData()), signature.size()) == 1; if (ctx) EVP_MD_CTX_free(ctx); EVP_PKEY_free(key); - if (!ok) m_error = "manifest RSA signature invalid"; - return ok; -#endif -} + if (!ok) { + m_error = QCoreApplication::translate("IntegrityHelper", + "Manifest RSA signature is invalid. Stage: installed version verification. This usually means the cached manifest was changed, the client public key does not match the server private key, or the wrong version cache is being used."); + } + return ok; +#endif +} bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString& channel, const QString& version) @@ -96,42 +113,79 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString if (!QFile::exists(cachePath)) cachePath = legacyCachePath; QFile cache(cachePath); - if (!cache.open(QIODevice::ReadOnly)) { m_error = "signed manifest cache missing for " + version; return false; } + if (!cache.open(QIODevice::ReadOnly)) { + m_error = QCoreApplication::translate("IntegrityHelper", + "Local signed manifest cache is missing. Stage: installed version verification. Version: %1. Expected cache file: %2. This cache is created after the same version is published or installed successfully.") + .arg(version, cachePath); + return false; + } QJsonParseError wrapperError; - const QJsonDocument wrapperDoc = QJsonDocument::fromJson(cache.readAll(), &wrapperError); - if (wrapperError.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { - m_error = "manifest cache JSON invalid"; return false; - } + const QJsonDocument wrapperDoc = QJsonDocument::fromJson(cache.readAll(), &wrapperError); + if (wrapperError.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { + m_error = QCoreApplication::translate("IntegrityHelper", + "Local signed manifest cache is not valid JSON. Stage: installed version verification. Version: %1. File: %2. JSON error: %3.") + .arg(version, cachePath, wrapperError.errorString()); + return false; + } const QJsonObject wrapper = wrapperDoc.object(); const QByteArray manifestText = wrapper.value("manifest_text").toString().toUtf8(); const QString signature = wrapper.value("manifest").toObject().value("signature").toString(); - if (manifestText.isEmpty() || signature.isEmpty() || !verifySignature(manifestText, signature)) return false; + if (manifestText.isEmpty() || signature.isEmpty()) { + m_error = QCoreApplication::translate("IntegrityHelper", + "Local signed manifest cache is incomplete. Stage: installed version verification. Version: %1. File: %2.") + .arg(version, cachePath); + return false; + } + if (!verifySignature(manifestText, signature)) return false; QJsonParseError manifestError; - const QJsonDocument manifestDoc = QJsonDocument::fromJson(manifestText, &manifestError); - if (manifestError.error != QJsonParseError::NoError || !manifestDoc.isObject()) { - m_error = "signed manifest payload invalid"; return false; - } + const QJsonDocument manifestDoc = QJsonDocument::fromJson(manifestText, &manifestError); + if (manifestError.error != QJsonParseError::NoError || !manifestDoc.isObject()) { + m_error = QCoreApplication::translate("IntegrityHelper", + "Signed manifest payload is not valid JSON. Stage: installed version verification. Version: %1. File: %2. JSON error: %3.") + .arg(version, cachePath, manifestError.errorString()); + return false; + } const QJsonObject manifest = manifestDoc.object(); if (manifest.value("app_id").toString() != appId - || manifest.value("channel").toString() != channel - || manifest.value("version").toString() != version) { - m_error = "manifest identity does not match local application"; return false; - } + || manifest.value("channel").toString() != channel + || manifest.value("version").toString() != version) { + m_error = QCoreApplication::translate("IntegrityHelper", + "Local signed manifest identity does not match this application. Stage: installed version verification. Expected app/channel/version: %1 / %2 / %3. Manifest app/channel/version: %4 / %5 / %6.") + .arg(appId, channel, version, + manifest.value("app_id").toString(), + manifest.value("channel").toString(), + manifest.value("version").toString()); + return false; + } QSet declaredExecutables; for (const QJsonValue& value : manifest.value("files").toArray()) { - const QJsonObject item = value.toObject(); - const QString path = QDir::fromNativeSeparators(item.value("path").toString()); - if (!safeRelativePath(path)) { m_error = "unsafe manifest path: " + path; return false; } - if (runtimeProtectedPath(path)) continue; - const QString fullPath = QDir(m_installDir).filePath(path); - if (!QFile::exists(fullPath)) { m_error = "required file missing: " + path; return false; } - const QString expected = item.value("sha256").toString(); - const QString actual = sha256(fullPath); - if (actual.isEmpty() || actual.compare(expected, Qt::CaseInsensitive) != 0) { - m_error = "file hash mismatch: " + path; return false; - } + const QJsonObject item = value.toObject(); + const QString path = QDir::fromNativeSeparators(item.value("path").toString()); + if (!safeRelativePath(path)) { + m_error = QCoreApplication::translate("IntegrityHelper", + "Signed manifest contains an unsafe file path. Stage: installed version verification. Version: %1. Path: %2.") + .arg(version, path); + return false; + } + if (runtimeProtectedPath(path)) continue; + const QString fullPath = QDir(m_installDir).filePath(path); + if (!QFile::exists(fullPath)) { + m_error = QCoreApplication::translate("IntegrityHelper", + "A required installed file is missing. Stage: installed version verification. Version: %1. Manifest path: %2. Checked path: %3. The local installation no longer matches the published version.") + .arg(version, path, fullPath); + return false; + } + const QString expected = item.value("sha256").toString(); + const QString actual = sha256(fullPath); + if (actual.isEmpty() || actual.compare(expected, Qt::CaseInsensitive) != 0) { + m_error = QCoreApplication::translate("IntegrityHelper", + "Installed file SHA-256 does not match the local signed manifest. Stage: installed version verification. Version: %1. Manifest path: %2. Local path: %3.\nExpected SHA-256: %4\nActual SHA-256: %5\nThis means the installed file is different from the version that was published or installed. If this is a developer test machine, check whether the local Release directory was recompiled or overwritten after publishing.") + .arg(version, path, fullPath, expected, + actual.isEmpty() ? QCoreApplication::translate("IntegrityHelper", "") : actual); + return false; + } const QString suffix = QFileInfo(path).suffix().toCaseFolded(); if (suffix == "exe" || suffix == "dll") declaredExecutables.insert(path.toCaseFolded()); } @@ -150,10 +204,13 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString || folded.startsWith(runtimePrefix + "/update_temp/")); if (folded.startsWith("update/") || folded.startsWith("update_temp/") || runtimeWorkDir || runtimeProtectedPath(relative)) continue; - const QString suffix = QFileInfo(relative).suffix().toCaseFolded(); - if ((suffix == "exe" || suffix == "dll") && !declaredExecutables.contains(folded)) { - m_error = "undeclared executable or plugin: " + relative; return false; - } + const QString suffix = QFileInfo(relative).suffix().toCaseFolded(); + if ((suffix == "exe" || suffix == "dll") && !declaredExecutables.contains(folded)) { + m_error = QCoreApplication::translate("IntegrityHelper", + "An executable or DLL exists locally but is not declared in the signed manifest. Stage: installed version verification. Version: %1. Extra file: %2. Remove unexpected executable/plugin files or publish a new version that declares them.") + .arg(version, relative); + return false; + } } return true; } diff --git a/Common/LocalStateHelper.cpp b/Common/LocalStateHelper.cpp index b8438c0..aa89a84 100644 --- a/Common/LocalStateHelper.cpp +++ b/Common/LocalStateHelper.cpp @@ -1,5 +1,6 @@ #include "LocalStateHelper.h" #include "ConfigHelper.h" +#include #include #include #include @@ -29,30 +30,39 @@ bool LocalStateHelper::loadState(const QString& relativePath) {"last_success_version", QString()} }; m_loaded = true; - if (!saveState()) - { - m_error = QString("Cannot write new state file: %1").arg(m_filePath); - return false; - } - return true; + if (!saveState()) + { + m_error = QCoreApplication::translate( + "LocalStateHelper", + "Cannot create local state file: %1. Error: %2.") + .arg(m_filePath, m_error); + return false; + } + return true; } - if (!file.open(QIODevice::ReadOnly)) - { - m_error = QString("Cannot open state file: %1").arg(m_filePath); - return false; - } + if (!file.open(QIODevice::ReadOnly)) + { + m_error = QCoreApplication::translate( + "LocalStateHelper", + "Cannot open local state file: %1. Error: %2.") + .arg(m_filePath, file.errorString()); + return false; + } QByteArray raw = file.readAll(); file.close(); QJsonParseError parseError; QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError); - if (parseError.error != QJsonParseError::NoError || !doc.isObject()) - { - m_error = QString("Invalid state JSON: %1").arg(parseError.errorString()); - return false; - } + if (parseError.error != QJsonParseError::NoError || !doc.isObject()) + { + m_error = QCoreApplication::translate( + "LocalStateHelper", + "Local state file is not valid JSON. File: %1. JSON error: %2.") + .arg(m_filePath, parseError.errorString()); + return false; + } m_state = doc.object(); m_loaded = true; @@ -63,7 +73,9 @@ bool LocalStateHelper::saveState() const { if (!m_loaded) { - m_error = "State is not loaded"; + m_error = QCoreApplication::translate( + "LocalStateHelper", + "Local state has not been loaded."); return false; } @@ -71,7 +83,10 @@ bool LocalStateHelper::saveState() const QString writeError; if (!ConfigHelper::writeFileWithElevationIfNeeded(m_filePath, doc.toJson(QJsonDocument::Indented), &writeError)) { - m_error = QString("Cannot save state file: %1").arg(writeError); + m_error = QCoreApplication::translate( + "LocalStateHelper", + "Cannot save local state file: %1. Error: %2.") + .arg(m_filePath, writeError); return false; } m_error.clear(); diff --git a/Common/PolicyHelper.cpp b/Common/PolicyHelper.cpp index 16c9144..cf9d806 100644 --- a/Common/PolicyHelper.cpp +++ b/Common/PolicyHelper.cpp @@ -1,6 +1,7 @@ #include "PolicyHelper.h" #include "ConfigHelper.h" #include +#include #include #include #include @@ -29,15 +30,26 @@ static QString resolvePolicyPath(const QString& baseDir, const QString& relative bool PolicyHelper::loadPolicy(const QString& relativePath) { - QFile file(resolvePolicyPath(m_baseDir, relativePath, false)); - if (!file.open(QIODevice::ReadOnly)) { m_error = "Cannot open policy file"; return false; } - QJsonParseError error; - const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); - if (error.error != QJsonParseError::NoError || !doc.isObject()) { - m_error = "Invalid policy JSON: " + error.errorString(); return false; - } - return loadPolicyObject(doc.object()); -} + const QString path = resolvePolicyPath(m_baseDir, relativePath, false); + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + m_error = QCoreApplication::translate( + "PolicyHelper", + "Cannot open signed version policy file: %1. Error: %2.") + .arg(path, file.errorString()); + return false; + } + QJsonParseError error; + const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); + if (error.error != QJsonParseError::NoError || !doc.isObject()) { + m_error = QCoreApplication::translate( + "PolicyHelper", + "Signed version policy is not valid JSON. File: %1. JSON error: %2.") + .arg(path, error.errorString()); + return false; + } + return loadPolicyObject(doc.object()); +} bool PolicyHelper::loadPolicyObject(const QJsonObject& policy, const QString& signedText) { @@ -51,7 +63,10 @@ bool PolicyHelper::savePolicy(const QString& relativePath) const QString writeError; const QString path = resolvePolicyPath(m_baseDir, relativePath, true); if (!ConfigHelper::writeFileWithElevationIfNeeded(path, bytes, &writeError)) { - m_error = "Cannot save policy file: " + writeError; + m_error = QCoreApplication::translate( + "PolicyHelper", + "Cannot save signed version policy file: %1. Error: %2.") + .arg(path, writeError); return false; } m_error.clear(); @@ -84,35 +99,73 @@ QByteArray PolicyHelper::canonicalPolicyBytes(const QJsonObject& policy) const bool PolicyHelper::verifySignature(const QByteArray& payload, const QString& signatureBase64) const { -#ifndef HAVE_OPENSSL - Q_UNUSED(payload); Q_UNUSED(signatureBase64); m_error = "OpenSSL unavailable"; return false; -#else - QString keyPath = m_baseDir + "/config/manifest_public_key.pem"; - QFile keyFile(keyPath); - if (!keyFile.open(QIODevice::ReadOnly)) { m_error = "Cannot open policy public key"; return false; } +#ifndef HAVE_OPENSSL + Q_UNUSED(payload); + Q_UNUSED(signatureBase64); + m_error = QCoreApplication::translate( + "PolicyHelper", + "OpenSSL is unavailable, so the signed version policy cannot be verified."); + return false; +#else + QString keyPath = m_baseDir + "/config/manifest_public_key.pem"; + QFile keyFile(keyPath); + if (!keyFile.open(QIODevice::ReadOnly)) { + m_error = QCoreApplication::translate( + "PolicyHelper", + "Cannot open version policy public key: %1. Error: %2.") + .arg(keyPath, keyFile.errorString()); + return false; + } const QByteArray keyData = keyFile.readAll(); BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size()); EVP_PKEY* key = bio ? PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr) : nullptr; if (bio) BIO_free(bio); - if (!key) { m_error = "Invalid policy public key"; return false; } + if (!key) { + m_error = QCoreApplication::translate( + "PolicyHelper", + "Version policy public key is invalid: %1.") + .arg(keyPath); + return false; + } EVP_MD_CTX* ctx = EVP_MD_CTX_new(); const QByteArray signature = QByteArray::fromBase64(signatureBase64.toUtf8()); bool ok = ctx && EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, key) == 1 && EVP_DigestVerifyUpdate(ctx, payload.constData(), payload.size()) == 1 && EVP_DigestVerifyFinal(ctx, reinterpret_cast(signature.constData()), signature.size()) == 1; if (ctx) EVP_MD_CTX_free(ctx); EVP_PKEY_free(key); - if (!ok) m_error = "Invalid RSA policy signature"; - return ok; -#endif -} + if (!ok) { + m_error = QCoreApplication::translate( + "PolicyHelper", + "Version policy RSA signature is invalid. The policy file may have been changed, or the public key does not match the server private key."); + } + return ok; +#endif +} bool PolicyHelper::isValid() const { - if (!m_loaded) return false; - const QStringList required{"app_id","channel","current_version","policy_seq","allow_run", - "force_update","allow_rollback","offline_allowed","valid_until","signature_alg","key_id","signature"}; - for (const QString& key : required) if (!m_policy.contains(key)) { m_error = "Missing policy field: " + key; return false; } - if (m_policy.value("signature_alg").toString() != "RSA-2048-SHA256") return false; + if (!m_loaded) { + m_error = QCoreApplication::translate("PolicyHelper", "Version policy has not been loaded."); + return false; + } + const QStringList required{"app_id","channel","current_version","policy_seq","allow_run", + "force_update","allow_rollback","offline_allowed","valid_until","signature_alg","key_id","signature"}; + for (const QString& key : required) { + if (!m_policy.contains(key)) { + m_error = QCoreApplication::translate( + "PolicyHelper", + "Version policy is missing required field: %1.") + .arg(key); + return false; + } + } + if (m_policy.value("signature_alg").toString() != "RSA-2048-SHA256") { + m_error = QCoreApplication::translate( + "PolicyHelper", + "Version policy signature algorithm is unsupported: %1.") + .arg(m_policy.value("signature_alg").toString()); + return false; + } const QByteArray payload = m_signedText.isEmpty() ? canonicalPolicyBytes(m_policy) : m_signedText.toUtf8(); return verifySignature(payload, m_policy.value("signature").toString()); } diff --git a/Common/TicketHelper.cpp b/Common/TicketHelper.cpp index 885be5d..36dc60b 100644 --- a/Common/TicketHelper.cpp +++ b/Common/TicketHelper.cpp @@ -5,10 +5,11 @@ #include #include #include -#include -#include -#include -#include +#include +#include +#include +#include +#include static QByteArray ticketMac(const QJsonObject& payload, const QString& secret) { @@ -22,10 +23,20 @@ bool TicketHelper::createTicket(const QString& appId, const QString& deviceId, { // Launcher 启动业务主程序前生成一次性 ticket。 // ticket 只保存在临时目录、有效期 60 秒,并用 launch_token 做 HMAC,防止用户绕过 Launcher 直接启动主程序。 - if (appId.isEmpty() || version.isEmpty() || secret.isEmpty()) { - if (errorMessage) *errorMessage = "ticket identity or secret is empty"; - return false; - } + if (appId.isEmpty() || deviceId.isEmpty() || version.isEmpty() || secret.isEmpty()) { + if (errorMessage) { + QStringList missing; + if (appId.isEmpty()) missing.append(QStringLiteral("app_id")); + if (deviceId.isEmpty()) missing.append(QStringLiteral("device_id")); + if (version.isEmpty()) missing.append(QStringLiteral("current_version")); + if (secret.isEmpty()) missing.append(QStringLiteral("launch_token")); + *errorMessage = QCoreApplication::translate( + "TicketHelper", + "Cannot create launch ticket because required fields are empty: %1. Check app_config.json, registry-imported configuration and device authorization.") + .arg(missing.join(QStringLiteral(", "))); + } + return false; + } const QDateTime now = QDateTime::currentDateTimeUtc(); QJsonObject payload{ {"app_id", appId}, {"device_id", deviceId}, {"version", version}, @@ -36,14 +47,27 @@ bool TicketHelper::createTicket(const QString& appId, const QString& deviceId, }; QJsonObject wrapper{{"payload", payload}, {"signature", QString::fromLatin1(ticketMac(payload, secret))}}; const QString dirPath = QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)).filePath("marsco_tickets"); - if (!QDir().mkpath(dirPath)) { if (errorMessage) *errorMessage = "cannot create ticket directory"; return false; } + if (!QDir().mkpath(dirPath)) { + if (errorMessage) { + *errorMessage = QCoreApplication::translate( + "TicketHelper", + "Cannot create launch ticket directory: %1.") + .arg(dirPath); + } + return false; + } const QString path = QDir(dirPath).filePath("ticket_" + QUuid::createUuid().toString(QUuid::WithoutBraces) + ".json"); - QSaveFile file(path); - const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Compact); - if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) { - if (errorMessage) *errorMessage = "cannot save ticket"; - return false; - } + QSaveFile file(path); + const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Compact); + if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) { + if (errorMessage) { + *errorMessage = QCoreApplication::translate( + "TicketHelper", + "Cannot save launch ticket file: %1. Error: %2.") + .arg(path, file.errorString()); + } + return false; + } QFile::setPermissions(path, QFileDevice::ReadOwner | QFileDevice::WriteOwner); if (ticketPath) *ticketPath = path; return true; @@ -56,24 +80,40 @@ bool TicketHelper::consumeAndVerify(const QString& ticketPath, const QString& ex // 主程序启动后第一时间把 ticket 改名成 .consuming,再读取并删除。 // 这样同一张 ticket 即使校验失败也不能被重复使用,避免重放启动。 const QString consumingPath = ticketPath + ".consuming." - + QString::number(QCoreApplication::applicationPid()); - if (!QFile::rename(ticketPath, consumingPath)) { - if (errorMessage) *errorMessage = "ticket missing or already consumed"; - return false; - } - QFile file(consumingPath); - if (!file.open(QIODevice::ReadOnly)) { - QFile::remove(consumingPath); - if (errorMessage) *errorMessage = "cannot read claimed ticket"; - return false; - } + + QString::number(QCoreApplication::applicationPid()); + if (!QFile::rename(ticketPath, consumingPath)) { + if (errorMessage) { + *errorMessage = QCoreApplication::translate( + "TicketHelper", + "Launch ticket is missing or has already been consumed. Ticket file: %1. Please start the application from Launcher.") + .arg(ticketPath); + } + return false; + } + QFile file(consumingPath); + if (!file.open(QIODevice::ReadOnly)) { + QFile::remove(consumingPath); + if (errorMessage) { + *errorMessage = QCoreApplication::translate( + "TicketHelper", + "Cannot read claimed launch ticket: %1. Error: %2.") + .arg(consumingPath, file.errorString()); + } + return false; + } const QByteArray raw = file.readAll(); file.close(); QFile::remove(consumingPath); // Consume once; it must not be replayed regardless of success or failure. QJsonParseError parseError; - const QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError); - if (parseError.error != QJsonParseError::NoError || !doc.isObject()) { - if (errorMessage) *errorMessage = "invalid ticket JSON"; return false; - } + const QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError); + if (parseError.error != QJsonParseError::NoError || !doc.isObject()) { + if (errorMessage) { + *errorMessage = QCoreApplication::translate( + "TicketHelper", + "Launch ticket is not valid JSON. JSON error: %1.") + .arg(parseError.errorString()); + } + return false; + } const QJsonObject wrapper = doc.object(); const QJsonObject payload = wrapper.value("payload").toObject(); const QByteArray actual = wrapper.value("signature").toString().toLatin1(); @@ -84,27 +124,57 @@ bool TicketHelper::consumeAndVerify(const QString& ticketPath, const QString& ex const bool timeOk = issued.isValid() && expires.isValid() && issued <= now.addSecs(5) && expires >= now && issued.secsTo(expires) <= 65; if (actual.isEmpty() || actual != expected) { - if (errorMessage) *errorMessage = "ticket signature invalid; check launch_token"; + if (errorMessage) { + *errorMessage = QCoreApplication::translate( + "TicketHelper", + "Launch ticket signature is invalid. The launch_token used by Launcher and the main application is inconsistent, or the ticket content was changed."); + } return false; } if (payload.value("app_id").toString() != expectedAppId) { - if (errorMessage) *errorMessage = "ticket app_id mismatch"; + if (errorMessage) { + *errorMessage = QCoreApplication::translate( + "TicketHelper", + "Launch ticket app_id does not match. Ticket app_id: %1. Expected app_id: %2.") + .arg(payload.value("app_id").toString(), expectedAppId); + } return false; } if (payload.value("device_id").toString() != expectedDeviceId) { - if (errorMessage) *errorMessage = "ticket device_id mismatch"; + if (errorMessage) { + *errorMessage = QCoreApplication::translate( + "TicketHelper", + "Launch ticket device_id does not match. Ticket device_id: %1. Expected device_id: %2. Reauthorize the device from Launcher if the configuration was regenerated.") + .arg(payload.value("device_id").toString(), expectedDeviceId); + } return false; } if (payload.value("version").toString() != expectedVersion) { - if (errorMessage) *errorMessage = "ticket version mismatch"; + if (errorMessage) { + *errorMessage = QCoreApplication::translate( + "TicketHelper", + "Launch ticket version does not match. Ticket version: %1. Expected version: %2.") + .arg(payload.value("version").toString(), expectedVersion); + } return false; } if (!timeOk) { - if (errorMessage) *errorMessage = "ticket time invalid or expired"; + if (errorMessage) { + *errorMessage = QCoreApplication::translate( + "TicketHelper", + "Launch ticket time is invalid or expired. Issued at: %1. Expires at: %2. Current UTC time: %3.") + .arg(payload.value("issued_at").toString(), + payload.value("expires_at").toString(), + now.toString(Qt::ISODate)); + } return false; } if (payload.value("nonce").toString().isEmpty()) { - if (errorMessage) *errorMessage = "ticket nonce missing"; + if (errorMessage) { + *errorMessage = QCoreApplication::translate( + "TicketHelper", + "Launch ticket nonce is missing. The ticket is incomplete."); + } return false; } return true; diff --git a/Launcher/UpdateLogic.cpp b/Launcher/UpdateLogic.cpp index 56b1807..4a156aa 100644 --- a/Launcher/UpdateLogic.cpp +++ b/Launcher/UpdateLogic.cpp @@ -104,11 +104,13 @@ void UpdateLogic::getDownloadUrl(const QString& appId, const QString& channel, c bool UpdateLogic::cacheManifest(const QString& appId, const QString& channel, const QString& version, int versionId, const QString& cacheDir) { - m_error.clear(); - if (appId.isEmpty() || channel.isEmpty() || version.isEmpty() || versionId <= 0) { - m_error = "manifest identity is incomplete"; - return false; - } + m_error.clear(); + if (appId.isEmpty() || channel.isEmpty() || version.isEmpty() || versionId <= 0) { + m_error = QCoreApplication::translate("UpdateLogic", + "Current version manifest identity is incomplete. Stage: cache current version manifest. App: %1, channel: %2, version: %3, version id: %4.") + .arg(appId, channel, version, QString::number(versionId)); + return false; + } QJsonObject response; int statusCode = 0; @@ -122,40 +124,58 @@ bool UpdateLogic::cacheManifest(const QString& appId, const QString& channel, co statusCode = code; response = resp; }); + + if (statusCode != 200) { + const QJsonValue detail = response.value(QStringLiteral("detail")); + const QString message = detail.isObject() + ? detail.toObject().value(QStringLiteral("msg")).toString() + : detail.toString(); + m_error = QCoreApplication::translate("UpdateLogic", + "Cannot download signed manifest for the current local version. Stage: cache current version manifest. HTTP status: %1. App: %2, channel: %3, version: %4, version id: %5.%6") + .arg(QString::number(statusCode), appId, channel, version, QString::number(versionId), + message.isEmpty() ? QString() : QCoreApplication::translate("UpdateLogic", "\nServer message: %1").arg(message)); + return false; + } - if (statusCode != 200) { - m_error = QString("manifest request failed (HTTP %1)").arg(statusCode); - return false; - } - - const QJsonObject manifest = response.value("manifest").toObject(); - const QString manifestText = response.value("manifest_text").toString(); - if (manifest.isEmpty() || manifestText.isEmpty()) { - m_error = "manifest response is incomplete"; - return false; - } + const QJsonObject manifest = response.value("manifest").toObject(); + const QString manifestText = response.value("manifest_text").toString(); + if (manifest.isEmpty() || manifestText.isEmpty()) { + m_error = QCoreApplication::translate("UpdateLogic", + "The signed manifest response for the current local version is incomplete. Stage: cache current version manifest. App: %1, channel: %2, version: %3, version id: %4.") + .arg(appId, channel, version, QString::number(versionId)); + return false; + } if (manifest.value("app_id").toString() != appId - || manifest.value("channel").toString() != channel - || manifest.value("version").toString() != version) { - m_error = "manifest identity does not match current version"; - return false; - } + || manifest.value("channel").toString() != channel + || manifest.value("version").toString() != version) { + m_error = QCoreApplication::translate("UpdateLogic", + "The signed manifest identity does not match the current local version. Stage: cache current version manifest. Expected app/channel/version: %1 / %2 / %3. Manifest app/channel/version: %4 / %5 / %6.") + .arg(appId, channel, version, + manifest.value("app_id").toString(), + manifest.value("channel").toString(), + manifest.value("version").toString()); + return false; + } - QDir dir(cacheDir); - if (!dir.exists() && !dir.mkpath(".")) { - m_error = "cannot create manifest cache directory"; - return false; - } + QDir dir(cacheDir); + if (!dir.exists() && !dir.mkpath(".")) { + m_error = QCoreApplication::translate("UpdateLogic", + "Cannot create manifest cache directory. Stage: cache current version manifest. Directory: %1.") + .arg(cacheDir); + return false; + } QJsonObject wrapper; wrapper["manifest"] = manifest; wrapper["manifest_text"] = manifestText; - QSaveFile file(dir.filePath("manifest_" + version + ".json")); - const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Indented); - if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) { - m_error = "cannot save signed manifest cache"; - return false; - } + QSaveFile file(dir.filePath("manifest_" + version + ".json")); + const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Indented); + if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) { + m_error = QCoreApplication::translate("UpdateLogic", + "Cannot save signed manifest cache. Stage: cache current version manifest. File: %1. Error: %2.") + .arg(file.fileName(), file.errorString()); + return false; + } qDebug() << "Current version manifest cached to" << file.fileName(); return true; } diff --git a/Launcher/main.cpp b/Launcher/main.cpp index 44e9af1..2f7dd87 100644 --- a/Launcher/main.cpp +++ b/Launcher/main.cpp @@ -176,9 +176,29 @@ int main(int argc, char* argv[]) QCoreApplication::translate("Launcher", "Select Offline Update Package"), QString(), QCoreApplication::translate("Launcher", "Marsco offline update package (*.upd)")); - return package.isEmpty() ? false : QProcess::startDetached(updaterPath, QStringList{QString("--offline-package=%1").arg(package)}); + if (package.isEmpty()) + return false; + if (!QFileInfo::exists(updaterPath)) { + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Updater Startup Failed"), + QCoreApplication::translate( + "Launcher", + "Cannot import the offline update package because the updater executable does not exist.\nUpdater: %1\nOffline package: %2") + .arg(updaterPath, package)); + return false; + } + if (!QProcess::startDetached(updaterPath, QStringList{QString("--offline-package=%1").arg(package)})) { + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Updater Startup Failed"), + QCoreApplication::translate( + "Launcher", + "Cannot start the updater for offline package import.\nUpdater: %1\nOffline package: %2\nCheck file permissions and dependent DLLs/shared libraries.") + .arg(updaterPath, package)); + return false; + } + return true; }; - const QString deviceId = config.getValue("Update", "device_id"); + const QString deviceId = config.getValue("Update", "device_id"); if (QCoreApplication::arguments().contains("--import-offline")) { progress.close(); if (!importOfflinePackage()) @@ -188,21 +208,40 @@ int main(int argc, char* argv[]) return 0; } + QString mainStartupError; const auto startMainApp = [&]() { // 只允许 Launcher 启动业务主程序:先生成一次性 ticket,再通过 --ticket-file 传给主程序。 // 业务主程序必须消费并校验 ticket,避免用户直接双击 SimCAE/MainApp 绕过更新和授权检查。 + mainStartupError.clear(); + if (!QFileInfo::exists(mainAppPath)) { + mainStartupError = QCoreApplication::translate( + "Launcher", + "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; - if (!TicketHelper::createTicket(logic.getAppId(), deviceId, currentVersion, - launchToken, &ticketPath, &ticketError)) { - qDebug() << "Cannot create launch ticket:" << ticketError; - return false; - } - const bool started = QProcess::startDetached(mainAppPath, - QStringList{QString("--ticket-file=%1").arg(ticketPath)}); - if (!started) QFile::remove(ticketPath); - return started; - }; + QString ticketError; + if (!TicketHelper::createTicket(logic.getAppId(), deviceId, currentVersion, + launchToken, &ticketPath, &ticketError)) { + qDebug() << "Cannot create launch ticket:" << ticketError; + mainStartupError = QCoreApplication::translate( + "Launcher", + "Cannot start the main application because the one-time launch ticket could not be created.\nExecutable: %1\nDetails: %2") + .arg(mainAppPath, ticketError); + return false; + } + const bool started = QProcess::startDetached(mainAppPath, + QStringList{QString("--ticket-file=%1").arg(ticketPath)}); + if (!started) { + QFile::remove(ticketPath); + mainStartupError = QCoreApplication::translate( + "Launcher", + "Cannot start the main application process.\nExecutable: %1\nTicket file: %2\nCheck file permissions, dependent DLLs/shared libraries, and whether the executable can run independently.") + .arg(mainAppPath, ticketPath); + } + return started; + }; progress.setLabelText(QCoreApplication::translate("Launcher", "Verifying local runtime policy...")); QApplication::processEvents(); @@ -294,23 +333,38 @@ int main(int argc, char* argv[]) accepted = QMessageBox::question(nullptr, dialogTitle, prompt, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes; } - if (!accepted) { + if (!accepted) { if (!startMainApp()) { QMessageBox::critical(nullptr, QCoreApplication::translate("Launcher", "Startup Failed"), - QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath)); + mainStartupError.isEmpty() + ? QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath) + : mainStartupError); return -1; } - return 0; - } - const QStringList updaterArgs{appId, channel, latestVer, QString::number(targetVersionId)}; + return 0; + } + const QStringList updaterArgs{appId, channel, latestVer, QString::number(targetVersionId)}; + if (!QFileInfo::exists(updaterPath)) + { + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Updater Startup Failed"), + QCoreApplication::translate( + "Launcher", + "Cannot start the updater because the executable file does not exist.\nExecutable: %1\nCheck updater_executable and install_root in the generated client configuration.") + .arg(updaterPath)); + return -1; + } if (!QProcess::startDetached(updaterPath, updaterArgs)) { QMessageBox::critical(nullptr, QCoreApplication::translate("Launcher", "Updater Startup Failed"), - QCoreApplication::translate("Launcher", "Cannot start the updater: %1").arg(updaterPath)); + QCoreApplication::translate( + "Launcher", + "Cannot start the updater process.\nExecutable: %1\nArguments: %2\nCheck file permissions, dependent DLLs/shared libraries, and whether the updater can run independently.") + .arg(updaterPath, updaterArgs.join(QStringLiteral(" ")))); return -1; - } + } return 0; } @@ -357,14 +411,16 @@ int main(int argc, char* argv[]) ? QCoreApplication::translate("Launcher", "The application is up to date. Starting...") : QCoreApplication::translate("Launcher", "Offline mode is active. Starting...")); QApplication::processEvents(); - if (!startMainApp()) - { - progress.close(); + if (!startMainApp()) + { + progress.close(); QMessageBox::critical(nullptr, QCoreApplication::translate("Launcher", "Startup Failed"), - QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath)); - return -1; - } + mainStartupError.isEmpty() + ? QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath) + : mainStartupError); + return -1; + } progress.close(); return 0; } diff --git a/MainApp/main.cpp b/MainApp/main.cpp index 19561d6..71a7624 100644 --- a/MainApp/main.cpp +++ b/MainApp/main.cpp @@ -97,12 +97,14 @@ int main(int argc, char* argv[]) if (!integrity.verifyInstalledVersion( config.getValue("App", "app_id"), config.getValue("App", "channel"), config.getValue("App", "current_version"))) - { - QMessageBox::critical(nullptr, "Integrity Check Failed", - QString("Application files failed signed Manifest verification:\n%1") - .arg(integrity.errorString())); - return -1; - } + { + QMessageBox::critical(nullptr, "Integrity Check Failed", + QString("Startup blocked because the installed files failed local signed Manifest verification.\n" + "This is not a download check; it means the current installation directory does not match the signed Manifest cache for this version.\n\n" + "Details:\n%1") + .arg(integrity.errorString())); + return -1; + } MainWindow w; w.show(); diff --git a/Updater/UpdaterLogic.cpp b/Updater/UpdaterLogic.cpp index eef751b..5bfa40a 100644 --- a/Updater/UpdaterLogic.cpp +++ b/Updater/UpdaterLogic.cpp @@ -25,16 +25,32 @@ #include #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", "") : 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", "") : 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 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) diff --git a/Updater/UpdaterLogic.h b/Updater/UpdaterLogic.h index f5146db..87dc43c 100644 --- a/Updater/UpdaterLogic.h +++ b/Updater/UpdaterLogic.h @@ -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 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; -}; \ No newline at end of file + QString m_currentDownloadPath; + QString m_offlineError; + mutable QString m_error; + QElapsedTimer m_downloadTimer; +}; diff --git a/Updater/main.cpp b/Updater/main.cpp index db5fed8..8ad86fc 100644 --- a/Updater/main.cpp +++ b/Updater/main.cpp @@ -3,9 +3,10 @@ #include #include #include -#include -#include -#include +#include +#include +#include +#include #include #include #include @@ -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", "") : 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(); diff --git a/i18n/update-client_zh_CN.qm b/i18n/update-client_zh_CN.qm index 624bb37..82e6dd1 100644 Binary files a/i18n/update-client_zh_CN.qm and b/i18n/update-client_zh_CN.qm differ diff --git a/i18n/update-client_zh_CN.ts b/i18n/update-client_zh_CN.ts index 2a07fae..2084750 100644 --- a/i18n/update-client_zh_CN.ts +++ b/i18n/update-client_zh_CN.ts @@ -50,18 +50,18 @@ Failed file: %1 - The current installation directory requires administrator permission to save configuration. + The current operation needs administrator permission to modify a protected file. Target file: %1 Reason: %2 Click OK, then choose Yes in the Windows permission confirmation dialog. - 当前安装目录需要管理员权限才能保存配置。 + 当前操作需要管理员权限来修改受保护文件。 目标文件:%1 原因:%2 -点击“确定”后,请在 Windows 权限确认窗口中选择“是”。 +点击“确定”,然后在 Windows 权限确认窗口中选择“是”。 @@ -137,26 +137,135 @@ Click OK, then choose Yes in the Windows permission confirmation dialog.无法保存安装实例 ID 到 %1:%2 - - Device identity request failed (HTTP %1): %2 - 设备身份申请失败(HTTP %1):%2 + + Cannot contact the update server to issue device identity. +Server: %1 +App: %2 +Channel: %3 +Network error: %4 +Timeout: %5 ms + 无法连接更新服务器来签发设备身份。 +服务器:%1 +App:%2 +渠道:%3 +网络错误:%4 +超时时间:%5 毫秒 - + + Device identity request was rejected by the update server. +Server: %1 +HTTP status: %2 +App: %3 +Channel: %4 +Server message: %5 + 设备身份请求被更新服务器拒绝。 +服务器:%1 +HTTP 状态码:%2 +App:%3 +渠道:%4 +服务端消息:%5 + + + + <empty response> + <空响应> + + + Server returned an invalid device identity response. 服务端返回的设备身份响应格式无效。 - + Cannot save device credential to %1: %2 无法保存设备凭证到 %1:%2 - + Cannot save server device id to %1: %2 无法保存服务端设备 ID 到 %1:%2 + + IntegrityHelper + + + Cannot verify signed manifest because OpenSSL support is unavailable. Stage: installed version verification. + 无法验证签名 Manifest,因为当前程序未启用 OpenSSL。阶段:已安装版本校验。 + + + + Cannot open manifest public key. Stage: installed version verification. Public key path: %1. + 无法打开 Manifest 公钥。阶段:已安装版本校验。公钥路径:%1。 + + + + Manifest public key is invalid. Stage: installed version verification. Public key path: %1. + Manifest 公钥无效。阶段:已安装版本校验。公钥路径:%1。 + + + + Manifest RSA signature is invalid. Stage: installed version verification. This usually means the cached manifest was changed, the client public key does not match the server private key, or the wrong version cache is being used. + Manifest RSA 签名无效。阶段:已安装版本校验。通常表示本地缓存的 Manifest 被改过、客户端公钥与服务端私钥不匹配,或正在使用错误版本的缓存。 + + + + Local signed manifest cache is missing. Stage: installed version verification. Version: %1. Expected cache file: %2. This cache is created after the same version is published or installed successfully. + 本地签名 Manifest 缓存缺失。阶段:已安装版本校验。版本:%1。期望缓存文件:%2。该缓存会在同版本发布或安装成功后生成。 + + + + Local signed manifest cache is not valid JSON. Stage: installed version verification. Version: %1. File: %2. JSON error: %3. + 本地签名 Manifest 缓存不是合法 JSON。阶段:已安装版本校验。版本:%1。文件:%2。JSON 错误:%3。 + + + + Local signed manifest cache is incomplete. Stage: installed version verification. Version: %1. File: %2. + 本地签名 Manifest 缓存不完整。阶段:已安装版本校验。版本:%1。文件:%2。 + + + + Signed manifest payload is not valid JSON. Stage: installed version verification. Version: %1. File: %2. JSON error: %3. + 签名 Manifest 内容不是合法 JSON。阶段:已安装版本校验。版本:%1。文件:%2。JSON 错误:%3。 + + + + Local signed manifest identity does not match this application. Stage: installed version verification. Expected app/channel/version: %1 / %2 / %3. Manifest app/channel/version: %4 / %5 / %6. + 本地签名 Manifest 身份与当前应用不匹配。阶段:已安装版本校验。期望 app/channel/version:%1 / %2 / %3。Manifest 中的 app/channel/version:%4 / %5 / %6。 + + + + Signed manifest contains an unsafe file path. Stage: installed version verification. Version: %1. Path: %2. + 签名 Manifest 包含不安全文件路径。阶段:已安装版本校验。版本:%1。路径:%2。 + + + + A required installed file is missing. Stage: installed version verification. Version: %1. Manifest path: %2. Checked path: %3. The local installation no longer matches the published version. + 缺少必需的已安装文件。阶段:已安装版本校验。版本:%1。Manifest 路径:%2。检查路径:%3。本地安装目录已不再匹配发布版本。 + + + + Installed file SHA-256 does not match the local signed manifest. Stage: installed version verification. Version: %1. Manifest path: %2. Local path: %3. +Expected SHA-256: %4 +Actual SHA-256: %5 +This means the installed file is different from the version that was published or installed. If this is a developer test machine, check whether the local Release directory was recompiled or overwritten after publishing. + 已安装文件的 SHA-256 与本地签名 Manifest 不一致。阶段:已安装版本校验。版本:%1。Manifest 路径:%2。本地路径:%3。 +期望 SHA-256:%4 +实际 SHA-256:%5 +这表示当前安装目录里的文件和当初发布或安装成功的版本不同。如果这是开发测试机器,请检查发布后是否又重新编译或覆盖了本地 Release 目录。 + + + + <cannot read file> + <无法读取文件> + + + + An executable or DLL exists locally but is not declared in the signed manifest. Stage: installed version verification. Version: %1. Extra file: %2. Remove unexpected executable/plugin files or publish a new version that declares them. + 本地存在签名 Manifest 未声明的 EXE 或 DLL。阶段:已安装版本校验。版本:%1。额外文件:%2。请删除异常的可执行文件/插件,或发布一个声明这些文件的新版本。 + + Launcher @@ -279,210 +388,458 @@ Please enter a new License for %2: Marsco 离线更新包 (*.upd) - - + + Cannot import the offline update package because the updater executable does not exist. +Updater: %1 +Offline package: %2 + 无法导入离线更新包,因为 Updater 可执行文件不存在。 +Updater:%1 +离线包:%2 + + + + Cannot start the updater for offline package import. +Updater: %1 +Offline package: %2 +Check file permissions and dependent DLLs/shared libraries. + 无法启动 Updater 导入离线更新包。 +Updater:%1 +离线包:%2 +请检查文件权限和依赖 DLL/共享库。 + + + + Offline Update 离线更新 - + No offline update package was selected. 未选择离线更新包。 - + + Cannot start the main application because the executable file does not exist. +Executable: %1 +Check main_executable and install_root in the generated client configuration. + 无法启动主程序,因为主程序文件不存在。 +可执行文件:%1 +请检查生成的客户端配置中的 main_executable 和 install_root。 + + + + Cannot start the main application because the one-time launch ticket could not be created. +Executable: %1 +Details: %2 + 无法启动主程序,因为一次性启动 ticket 创建失败。 +可执行文件:%1 +详细信息:%2 + + + + Cannot start the main application process. +Executable: %1 +Ticket file: %2 +Check file permissions, dependent DLLs/shared libraries, and whether the executable can run independently. + 无法启动主程序进程。 +可执行文件:%1 +Ticket 文件:%2 +请检查文件权限、依赖 DLL/共享库,以及该可执行文件是否能独立运行。 + + + Verifying local runtime policy... 正在验证本地运行策略... - - - + + + Cannot Start 无法启动 - + The local version policy is invalid: %1 本地版本策略无效:%1 - + The local version policy has expired. Please connect to the network or contact the administrator. 本地版本策略已过期,请连接网络或联系管理员。 - + Current Version Cannot Run 当前版本不可运行 - + The current version %1 has been disabled by the administrator. 当前版本 %1 已被管理员停用。 - + Cannot read the local state: %1 无法读取本地状态:%1 - - + + Security Check Failed 安全检查失败 - + A version policy sequence rollback was detected. Startup has been blocked. 检测到版本策略序列回退,已阻止启动。 - + A possible system time rollback was detected. Startup has been blocked. 检测到系统时间可能被回拨,已阻止启动。 - + Generating Git tag list... 正在生成 Git 标签清单... - + Git Tag List Failed Git 标签清单生成失败 - + Cannot generate tags.txt, but startup will continue: %1 无法生成 tags.txt,但启动会继续: %1 - + Version Rollback 版本回退 - + Update Required 必须更新 - + New Version Available 发现新版本 - + The administrator provided version %1 as the rollback target. Downgrade now? 管理员提供了版本 %1 作为回退目标,是否现在降级? - + Version %1 is available. Update now? 发现新版本 %1,是否现在更新? - + Target version: %1 目标版本:%1 - - + + Startup Failed 启动失败 - - + + Cannot start the main application: %1 无法启动主程序:%1 - + + Cannot start the updater because the executable file does not exist. +Executable: %1 +Check updater_executable and install_root in the generated client configuration. + 无法启动 Updater,因为 Updater 可执行文件不存在。 +可执行文件:%1 +请检查生成的客户端配置中的 updater_executable 和 install_root。 + + + + Cannot start the updater process. +Executable: %1 +Arguments: %2 +Check file permissions, dependent DLLs/shared libraries, and whether the updater can run independently. + 无法启动 Updater 进程。 +可执行文件:%1 +参数:%2 +请检查文件权限、依赖 DLL/共享库,以及 Updater 是否能独立运行。 + + + + + + Updater Startup Failed 更新器启动失败 - - Cannot start the updater: %1 - 无法启动更新器:%1 - - - + Caching signed version manifest... 正在缓存签名版本清单... - + Manifest Cache Failed 清单缓存失败 - + Cannot cache the signed manifest for the current version: %1 无法缓存当前版本的签名清单:%1 - + Server Unavailable 服务器不可用 - + Cannot connect to the update server. Import an offline update package? 当前无法连接更新服务器。是否导入离线更新包? - + No offline update package was selected, or the updater could not be started. 未选择离线更新包或无法启动更新器。 - + Network Unavailable 网络不可用 - + Cannot connect to the update server, and the current policy does not allow offline startup. 无法连接更新服务器,且当前策略不允许离线启动。 - + The application is up to date. Starting... 当前已是最新版本,正在启动... - + Offline mode is active. Starting... 当前处于离线模式,正在启动... + + LocalStateHelper + + + Cannot create local state file: %1. Error: %2. + 无法创建本地状态文件:%1。错误:%2。 + + + + Cannot open local state file: %1. Error: %2. + 无法打开本地状态文件:%1。错误:%2。 + + + + Local state file is not valid JSON. File: %1. JSON error: %2. + 本地状态文件不是合法 JSON。文件:%1。JSON 错误:%2。 + + + + Local state has not been loaded. + 本地状态尚未加载。 + + + + Cannot save local state file: %1. Error: %2. + 无法保存本地状态文件:%1。错误:%2。 + + + + PolicyHelper + + + Cannot open signed version policy file: %1. Error: %2. + 无法打开签名版本策略文件:%1。错误:%2。 + + + + Signed version policy is not valid JSON. File: %1. JSON error: %2. + 签名版本策略不是合法 JSON。文件:%1。JSON 错误:%2。 + + + + Cannot save signed version policy file: %1. Error: %2. + 无法保存签名版本策略文件:%1。错误:%2。 + + + + OpenSSL is unavailable, so the signed version policy cannot be verified. + 当前程序未启用 OpenSSL,无法验证签名版本策略。 + + + + Cannot open version policy public key: %1. Error: %2. + 无法打开版本策略公钥:%1。错误:%2。 + + + + Version policy public key is invalid: %1. + 版本策略公钥无效:%1。 + + + + Version policy RSA signature is invalid. The policy file may have been changed, or the public key does not match the server private key. + 版本策略 RSA 签名无效。策略文件可能被修改,或客户端公钥与服务端私钥不匹配。 + + + + Version policy has not been loaded. + 版本策略尚未加载。 + + + + Version policy is missing required field: %1. + 版本策略缺少必填字段:%1。 + + + + Version policy signature algorithm is unsupported: %1. + 版本策略签名算法不支持:%1。 + + + + TicketHelper + + + Cannot create launch ticket because required fields are empty: %1. Check app_config.json, registry-imported configuration and device authorization. + 无法创建启动 ticket,因为必填字段为空:%1。请检查 app_config.json、已导入注册表的配置和设备授权状态。 + + + + Cannot create launch ticket directory: %1. + 无法创建启动 ticket 目录:%1。 + + + + Cannot save launch ticket file: %1. Error: %2. + 无法保存启动 ticket 文件:%1。错误:%2。 + + + + Launch ticket is missing or has already been consumed. Ticket file: %1. Please start the application from Launcher. + 启动 ticket 缺失或已经被消费。Ticket 文件:%1。请从 Launcher 启动主程序。 + + + + Cannot read claimed launch ticket: %1. Error: %2. + 无法读取已声明占用的启动 ticket:%1。错误:%2。 + + + + Launch ticket is not valid JSON. JSON error: %1. + 启动 ticket 不是合法 JSON。JSON 错误:%1。 + + + + Launch ticket signature is invalid. The launch_token used by Launcher and the main application is inconsistent, or the ticket content was changed. + 启动 ticket 签名无效。Launcher 和主程序使用的 launch_token 不一致,或 ticket 内容被修改过。 + + + + Launch ticket app_id does not match. Ticket app_id: %1. Expected app_id: %2. + 启动 ticket 的 app_id 不匹配。Ticket app_id:%1。期望 app_id:%2。 + + + + Launch ticket device_id does not match. Ticket device_id: %1. Expected device_id: %2. Reauthorize the device from Launcher if the configuration was regenerated. + 启动 ticket 的 device_id 不匹配。Ticket device_id:%1。期望 device_id:%2。如果刚重新生成过配置,请通过 Launcher 重新授权设备。 + + + + Launch ticket version does not match. Ticket version: %1. Expected version: %2. + 启动 ticket 的版本号不匹配。Ticket 版本:%1。期望版本:%2。 + + + + Launch ticket time is invalid or expired. Issued at: %1. Expires at: %2. Current UTC time: %3. + 启动 ticket 时间无效或已过期。签发时间:%1。过期时间:%2。当前 UTC 时间:%3。 + + + + Launch ticket nonce is missing. The ticket is incomplete. + 启动 ticket 缺少 nonce。Ticket 内容不完整。 + + UpdateLogic - + + Current version manifest identity is incomplete. Stage: cache current version manifest. App: %1, channel: %2, version: %3, version id: %4. + 当前版本 Manifest 身份信息不完整。阶段:缓存当前版本 Manifest。App:%1,渠道:%2,版本:%3,版本 ID:%4。 + + + + Cannot download signed manifest for the current local version. Stage: cache current version manifest. HTTP status: %1. App: %2, channel: %3, version: %4, version id: %5.%6 + 无法下载当前本地版本的签名 Manifest。阶段:缓存当前版本 Manifest。HTTP 状态码:%1。App:%2,渠道:%3,版本:%4,版本 ID:%5。%6 + + + + +Server message: %1 + +服务端消息:%1 + + + + The signed manifest response for the current local version is incomplete. Stage: cache current version manifest. App: %1, channel: %2, version: %3, version id: %4. + 当前本地版本的签名 Manifest 响应不完整。阶段:缓存当前版本 Manifest。App:%1,渠道:%2,版本:%3,版本 ID:%4。 + + + + The signed manifest identity does not match the current local version. Stage: cache current version manifest. Expected app/channel/version: %1 / %2 / %3. Manifest app/channel/version: %4 / %5 / %6. + 签名 Manifest 身份与当前本地版本不匹配。阶段:缓存当前版本 Manifest。期望 app/channel/version:%1 / %2 / %3。Manifest 中的 app/channel/version:%4 / %5 / %6。 + + + + Cannot create manifest cache directory. Stage: cache current version manifest. Directory: %1. + 无法创建 Manifest 缓存目录。阶段:缓存当前版本 Manifest。目录:%1。 + + + + Cannot save signed manifest cache. Stage: cache current version manifest. File: %1. Error: %2. + 无法保存签名 Manifest 缓存。阶段:缓存当前版本 Manifest。文件:%1。错误:%2。 + + + Server address is empty. 服务端地址为空。 - + Git tags request failed (HTTP %1). Git 标签请求失败(HTTP %1)。 - + Git tags response is empty. Git 标签响应为空。 - + Cannot write Git tags file: %1 无法写入 Git 标签文件:%1 @@ -490,95 +847,142 @@ Target version: %1 Updater - + Invalid Offline Package 离线包无效 - + Updater Argument Error 更新器参数错误 - + The updater is missing online update arguments or an offline update package. 更新器缺少在线更新参数或离线更新包。 - + Offline Package Not Applicable 离线包不适用 - + The update package application or channel does not match the local configuration. 更新包的应用或渠道与本机配置不一致。 - + Offline Update Rejected 离线更新被拒绝 - + The local signed policy has expired, forbids offline updates, or does not allow the target version. 本地签名策略已过期、禁止离线更新或不允许目标版本。 - + Downgrade Forbidden 禁止降级 - + The current signed policy does not allow installing an offline package with a lower version. 当前签名策略不允许安装较低版本的离线包。 - - + + Update Recovery Failed 更新恢复失败 - + An unfinished update was detected, but the old version could not be restored: %1 Do not continue running the software. Please contact the administrator. 检测到上次更新未完成,但无法恢复旧版本:%1 请不要继续运行软件,并联系管理员。 - + The old files were restored, but the version state could not be restored. Please check write permissions for the configuration directory. 旧文件已经恢复,但无法恢复版本状态,请检查配置目录写入权限。 - + Preparing update... 正在准备更新... - + Updating to %1 正在更新到 %1 - + Preparing download... 正在准备下载... - + Downloading: %1 正在下载:%1 - + + + +Details: +%1 + + +详细信息: +%1 + + + + Cannot start the main application because the executable file does not exist. +Executable: %1 +Check main_executable and install_root in the generated client configuration. + 无法启动主程序,因为主程序文件不存在。 +可执行文件:%1 +请检查生成的客户端配置中的 main_executable 和 install_root。 + + + + Cannot start the main application because the one-time launch ticket could not be created. +Executable: %1 +Details: %2 + 无法启动主程序,因为一次性启动 ticket 创建失败。 +可执行文件:%1 +详细信息:%2 + + + + Cannot start the main application process. +Executable: %1 +Ticket file: %2 +Health file: %3 +Check file permissions, dependent DLLs/shared libraries, and whether the executable can run independently. + 无法启动主程序进程。 +可执行文件:%1 +Ticket 文件:%2 +健康检查文件:%3 +请检查文件权限、依赖 DLL/共享库,以及该可执行文件是否能独立运行。 + + + + <not used> + <未使用> + + + Delegating rollback to Bootstrap... 正在将回滚工作移交给 Bootstrap... - + Cannot start Bootstrap to perform rollback. Do not continue running the software. Please contact the administrator. @@ -587,356 +991,356 @@ Cannot start Bootstrap to perform rollback. Do not continue running the software 无法启动 Bootstrap 执行回滚。请不要继续运行软件,并联系管理员。 - + Transaction Resume Failed 事务续办失败 - + Cannot read the Bootstrap update transaction: %1 无法读取 Bootstrap 更新事务:%1 - + Update Rolled Back 更新已回滚 - + The new version failed to install or start. The old version has been restored and started automatically. 新版本安装或启动失败,已自动恢复并启动旧版本。 - + The old files were restored, but the old version number could not be written back. Please check configuration directory permissions. 旧文件已经恢复,但旧版本号写回失败,请检查配置目录权限。 - + Automatic Rollback Failed 自动回滚失败 - + Bootstrap could not fully restore the old version. Do not continue running the software. Please contact the administrator. Bootstrap 无法完整恢复旧版本。请不要继续运行软件,并联系管理员。 - - + + Update Preparation Failed 更新准备失败 - + Cannot create the update transaction directory or save the transaction state. 无法创建更新事务目录或保存事务状态。 - + Cannot save the offline transaction marker. 无法保存离线事务标记。 - + Fetching and verifying version manifest... 正在获取并验证版本清单... - + Verifying offline update package... 正在验证离线更新包... - - - + + + Manifest Cache Failed 清单缓存失败 - + Cannot read the signed manifest cache after Bootstrap installation. Bootstrap 安装后无法读取签名 Manifest 缓存。 - - + + Security Verification Failed 安全验证失败 - + Cannot reverify the manifest signature after Bootstrap installation. Bootstrap 安装后无法重新验证版本清单签名。 - + The version manifest signature is invalid. The update has stopped. Please contact the administrator. 版本清单签名无效,更新已停止。请联系管理员。 - + Cannot save the new version manifest cache. 无法保存新版本 Manifest 缓存。 - + Cannot save the new version manifest cache. The update has stopped. 无法保存新版本 Manifest 缓存,更新已停止。 - + Fetching secure download URLs... 正在获取安全下载地址... - + Preparing offline package files... 正在准备离线包文件... - + No Files to Update 没有可更新文件 - + The server did not return any version files. The update has stopped. 服务器没有返回任何版本文件,更新已停止。 - + Insufficient Disk Space 磁盘空间不足 - + The update requires at least %1 of free space, but the installation drive currently has only %2. The required space includes downloaded files, old version backups, and a safety margin. 更新至少需要 %1 可用空间,安装盘当前仅剩 %2。 所需空间已包含下载文件、旧版本备份和安全余量。 - + Downloading and verifying %1 version files... 正在下载并校验 %1 个版本文件... - + Extracting and verifying %1 offline files... 正在提取并校验 %1 个离线文件... - + Offline Package Extraction Failed 离线包提取失败 - + Download Failed 下载失败 - - Some files failed to download or failed SHA-256 verification. Please check the network and try again. - 部分文件下载失败或 SHA-256 校验未通过,请检查网络后重试。 + + Some files failed to download or failed SHA-256 verification. Please check the network, update cache, or server release files and try again. + 部分文件下载失败或 SHA-256 校验失败。请检查网络、更新缓存或服务端发布文件后重试。 - + + The downloaded/staged files do not match the target version manifest. The update has stopped before replacing installed files. + 已下载/暂存的文件与目标版本 Manifest 不一致。更新已在替换已安装文件前停止。 + + + + New version files failed verification after installation. The updater will roll back to the previous version. + 新版本文件在安装后校验失败。更新器将回滚到上一版本。 + + + Verifying complete version files... 正在校验完整版本文件... - + File Verification Failed 文件校验失败 - - The staged files do not match the version manifest. The update has stopped. - 暂存文件与版本清单不一致,更新已停止。 - - - + Bootstrap Cannot Self-Update Bootstrap 无法自更新 - + This version contains a new %1. Please upgrade this component with the installer, then publish the business version again. 本次版本包含新的 %1。请使用安装包升级该组件,再重新发布业务版本。 - + Transaction Recording Failed 事务记录失败 - + Cannot save the list of verified or pending deletion files. The update has stopped. 无法保存已校验或待删除文件列表,更新已停止。 - + Closing the main application... 正在关闭主程序... - + Cannot Close Main Application 无法关闭主程序 - + %1 is still running. Please close it manually and try again. %1 仍在运行,请手动关闭后重试。 - + Backing up %1 files to be changed, including %2 files to be deleted... 正在备份 %1 个待变更文件(其中删除 %2 个)... - + Backup Failed 备份失败 - + Cannot back up current version files. The new version has not been installed. Please check disk space and directory permissions. 无法备份当前版本文件,尚未安装新版本。请检查磁盘空间和目录权限。 - - - - + + + + Bootstrap Handoff Preparation Failed 接管准备失败 - + Cannot create the Bootstrap file plan. 无法创建 Bootstrap 文件计划。 - + Cannot write the Bootstrap copy plan. 无法写入 Bootstrap 复制计划。 - + Cannot write the Bootstrap deletion plan. 无法写入 Bootstrap 删除计划。 - + Cannot commit the Bootstrap file plan or transaction state. 无法提交 Bootstrap 文件计划或事务状态。 - + Delegating installation to Bootstrap... 正在将安装工作移交给 Bootstrap... - + Bootstrap Startup Failed Bootstrap 启动失败 - + Cannot start the standalone update handoff program: %1 无法启动独立更新接管程序:%1 - + Verifying Bootstrap installation result... 正在校验 Bootstrap 安装结果... - + Installation Verification Failed 安装校验失败 - - New version files failed verification after installation. - 新版本文件安装后校验未通过。 - - - + Obsolete File Cleanup Failed 废弃文件清理失败 - + An obsolete file still exists: %1 废弃文件仍然存在:%1 - + Saving new version state... 正在保存新版本状态... - + State Save Failed 状态保存失败 - + Cannot save the current version number. 无法保存当前版本号。 - + Starting the new version and waiting for health confirmation... 正在启动新版本并等待健康确认... - + Startup Failed 启动失败 - + %1 cannot be started. %1 无法启动。 - + Startup Confirmation Failed 启动确认失败 - + The new version did not complete startup health confirmation within %1 milliseconds. 新版本在 %1 毫秒内没有完成启动健康确认。 - + Committing update transaction... 正在提交更新事务... - + Transaction Commit Failed 事务提交失败 - + The new version has started, but the update transaction could not be committed. 新版本已经启动,但无法提交更新事务。 - + Update Complete 更新完成 - + The software has been successfully updated to %1 and passed the startup health check. 软件已成功更新到 %1,并通过启动健康检查。 @@ -944,89 +1348,306 @@ The required space includes downloaded files, old version backups, and a safety UpdaterLogic - + + Target version manifest response is incomplete. Stage: download target manifest. App: %1, channel: %2, version: %3, version id: %4. + 目标版本 Manifest 响应不完整。阶段:下载目标版本 Manifest。App:%1,渠道:%2,版本:%3,版本 ID:%4。 + + + + Cannot download target version manifest. Stage: download target manifest. HTTP status: %1. App: %2, channel: %3, version: %4, version id: %5.%6 + 无法下载目标版本 Manifest。阶段:下载目标版本 Manifest。HTTP 状态码:%1。App:%2,渠道:%3,版本:%4,版本 ID:%5。%6 + + + + + +Server message: %1 + +服务端消息:%1 + + + + Cannot verify manifest signature because OpenSSL support is unavailable. Stage: manifest signature verification. + 无法验证 Manifest 签名,因为当前程序未启用 OpenSSL。阶段:Manifest 签名校验。 + + + + Cannot open manifest public key. Stage: manifest signature verification. Public key path: %1. + 无法打开 Manifest 公钥。阶段:Manifest 签名校验。公钥路径:%1。 + + + + Cannot parse manifest public key buffer. Stage: manifest signature verification. Public key path: %1. + 无法解析 Manifest 公钥内容。阶段:Manifest 签名校验。公钥路径:%1。 + + + + Manifest public key is invalid. Stage: manifest signature verification. Public key path: %1. + Manifest 公钥无效。阶段:Manifest 签名校验。公钥路径:%1。 + + + + Cannot create OpenSSL verification context. Stage: manifest signature verification. + 无法创建 OpenSSL 验签上下文。阶段:Manifest 签名校验。 + + + + 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. + Manifest RSA 签名无效。阶段:Manifest 签名校验。通常表示 Manifest 不是由匹配的服务端私钥签名、客户端公钥不正确,或 Manifest 内容被修改过。 + + + + No manifest is available for signature verification. Stage: manifest signature verification. The target manifest may not have been downloaded successfully. + 没有可用于验签的 Manifest。阶段:Manifest 签名校验。目标版本 Manifest 可能没有下载成功。 + + + + The manifest does not contain a signature. Stage: manifest signature verification. + Manifest 中没有签名字段。阶段:Manifest 签名校验。 + + + + Cannot save manifest cache because the target manifest is empty. Stage: save target manifest cache. + 无法保存 Manifest 缓存,因为目标版本 Manifest 为空。阶段:保存目标版本 Manifest 缓存。 + + + + Cannot create manifest cache directory. Stage: save target manifest cache. Directory: %1. + 无法创建 Manifest 缓存目录。阶段:保存目标版本 Manifest 缓存。目录:%1。 + + + + Cannot write manifest cache file. Stage: save target manifest cache. File: %1. Error: %2. + 无法写入 Manifest 缓存文件。阶段:保存目标版本 Manifest 缓存。文件:%1。错误:%2。 + + + + Cannot read cached signed manifest. Stage: read local manifest cache. Version: %1. File: %2. This cache is created after a version is installed successfully. + 无法读取本地签名 Manifest 缓存。阶段:读取本地 Manifest 缓存。版本:%1。文件:%2。该缓存会在版本安装成功后生成。 + + + + Cached signed manifest is not valid JSON. Stage: read local manifest cache. Version: %1. File: %2. + 本地签名 Manifest 缓存不是合法 JSON。阶段:读取本地 Manifest 缓存。版本:%1。文件:%2。 + + + + Cached signed manifest is incomplete. Stage: read local manifest cache. Version: %1. File: %2. + 本地签名 Manifest 缓存不完整。阶段:读取本地 Manifest 缓存。版本:%1。文件:%2。 + + + + installed version verification + 已安装版本校验 + + + + downloaded/staged file verification + 已下载/暂存文件校验 + + + + No manifest is available. Stage: %1. The updater cannot know which files and hashes should be verified. + 没有可用的 Manifest。阶段:%1。Updater 无法知道应该校验哪些文件和哈希。 + + + + Manifest contains an unsafe file path. Stage: %1. Version: %2. Path: %3. + Manifest 包含不安全文件路径。阶段:%1。版本:%2。路径:%3。 + + + + 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. + 缺少必需文件。阶段:%1。版本:%2。Manifest 路径:%3。检查路径:%4。如果这是下载更新阶段,说明文件没有正确下载或暂存;如果这是启动校验阶段,说明已安装文件可能被删除。 + + + + File SHA-256 does not match the signed manifest. Stage: %1. Version: %2. Manifest path: %3. Local path: %4. +Expected SHA-256: %5 +Actual SHA-256: %6 +If 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. + 文件 SHA-256 与签名 Manifest 不一致。阶段:%1。版本:%2。Manifest 路径:%3。本地路径:%4。 +期望 SHA-256:%5 +实际 SHA-256:%6 +如果发生在下载阶段,请清理更新缓存后重试。如果发生在启动或安装后校验阶段,说明本地文件与发布版本不同。 + + + + + <cannot read file> + <无法读取文件> + + + Cannot open the offline update package 无法打开离线更新包 - + The offline package format identifier is invalid 离线包格式标识无效 - + The offline package header is incomplete 离线包头不完整 - + The offline package header length is invalid 离线包头长度无效 - + The offline package header JSON is invalid 离线包头 JSON 无效 - + The offline package RSA signature is invalid 离线包 RSA 签名无效 - + The offline package signature metadata is invalid 离线包签名元数据无效 - + The manifest digest does not match the package signature Manifest 摘要与包签名不一致 - + The offline manifest is invalid 离线 Manifest 无效 - + Package information does not match manifest identity 包信息与 Manifest 身份不一致 - + The offline manifest RSA signature is invalid 离线 Manifest RSA 签名无效 - + The offline package contains an unsafe path or out-of-range data: %1 离线包包含不安全路径或越界数据:%1 - + Cannot prepare offline file: %1 无法准备离线文件:%1 - + Cannot create staged file: %1 无法创建暂存文件:%1 - + Failed to read offline file: %1 离线文件读取失败:%1 - + Offline file hash verification or write failed: %1 离线文件 Hash 或写入失败:%1 - + The offline package file count does not match the manifest 离线包文件数量与 Manifest 不一致 + + + Cannot get secure download URLs. Stage: request download URLs. HTTP status: %1. App: %2, channel: %3, version: %4, version id: %5.%6 + 无法获取安全下载链接。阶段:请求下载链接。HTTP 状态码:%1。App:%2,渠道:%3,版本:%4,版本 ID:%5。%6 + + + + Cannot create partial download directory. Stage: download file. File: %1. Partial file: %2. + 无法创建断点续传目录。阶段:下载文件。文件:%1。临时分片文件:%2。 + + + + + Downloaded file passed SHA-256 verification, but cannot move it into the staging directory. Stage: download file. File: %1. From: %2. To: %3. + 下载文件已通过 SHA-256 校验,但无法移动到暂存目录。阶段:下载文件。文件:%1。从:%2。到:%3。 + + + + Cached partial file has the expected size but wrong SHA-256. Stage: resume download. File: %1. +Expected SHA-256: %2 +Actual SHA-256: %3 +The partial cache will be deleted and downloaded again. + 缓存的断点续传文件大小正确,但 SHA-256 不一致。阶段:断点续传。文件:%1。 +期望 SHA-256:%2 +实际 SHA-256:%3 +该临时缓存会被删除并重新下载。 + + + + Cannot open partial download file. Stage: download file. File: %1. Partial file: %2. Error: %3. + 无法打开断点续传临时文件。阶段:下载文件。文件:%1。临时分片文件:%2。错误:%3。 + + + + Downloaded file does not match the signed manifest. Stage: download file. File: %1. HTTP status: %2. +Expected size: %3 bytes +Actual size: %4 bytes +Expected SHA-256: %5 +Actual SHA-256: %6 +The partial cache will be deleted and downloaded again. + 下载文件与签名 Manifest 不一致。阶段:下载文件。文件:%1。HTTP 状态码:%2。 +期望大小:%3 字节 +实际大小:%4 字节 +期望 SHA-256:%5 +实际 SHA-256:%6 +该临时缓存会被删除并重新下载。 + + + + Downloaded file is incomplete. Stage: download file. File: %1. HTTP status: %2. Expected size: %3 bytes, current size: %4 bytes. + 下载文件不完整。阶段:下载文件。文件:%1。HTTP 状态码:%2。期望大小:%3 字节,当前大小:%4 字节。 + + + + Download request failed. Stage: download file. File: %1. Attempt: %2/4. HTTP status: %3. Network error: %4. Partial file: %5. + 下载请求失败。阶段:下载文件。文件:%1。尝试次数:%2/4。HTTP 状态码:%3。网络错误:%4。临时分片文件:%5。 + + + + File download failed after retries. Stage: download file. File: %1. + 文件多次重试后仍下载失败。阶段:下载文件。文件:%1。 + + + + The target version manifest contains no downloadable files. Stage: prepare downloads. + 目标版本 Manifest 中没有可下载文件。阶段:准备下载。 + + + + Cannot create update staging directory. Stage: prepare downloads. Directory: %1. + 无法创建更新暂存目录。阶段:准备下载。目录:%1。 + + + + Cannot create download cache directory. Stage: prepare downloads. Directory: %1. + 无法创建下载缓存目录。阶段:准备下载。目录:%1。 + + + + Manifest contains an unsafe file path. Stage: prepare file download. Path: %1. + Manifest 包含不安全文件路径。阶段:准备文件下载。路径:%1。 + + + + Cannot create staging subdirectory. Stage: prepare file download. File: %1. Directory: %2. + 无法创建暂存子目录。阶段:准备文件下载。文件:%1。目录:%2。 +