Files
Updater/Src/updaterdialog.cpp
T

489 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "updaterdialog.h"
#include "ui_updaterdialog.h"
#include <QApplication>
#include <QDesktopServices>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageBox>
#include <QMetaEnum>
#include <QUrl>
#include <QtGlobal>
#include <private/qzipreader_p.h>
// QString CHECK_URL = QStringLiteral("https://gitea.tianzd.cn/TianZD/deploy/raw/branch/master/%1/updates.json");
QString CHECK_URL = QStringLiteral("https://easy.tianzd.cn/app/%1/updates.json");
//QString DOWNLOAD_URL = QStringLiteral("https://update.tianzd.cn/deploy/");
QString PLATFORM = "windows";
QString APPVERSION = "";
QString APPNAME = "";
QString APPDATE = "0";
QString MODIFYCNT = "0";
namespace
{
/**
* @brief 比较两个 x.y.z 形式的版本号。
* @param left 左侧版本号。
* @param right 右侧版本号。
* @return left < right 返回 -1left == right 返回 0left > right 返回 1。
*
* 设计意图:统一版本比较规则,缺失段按 0 处理,非数字段也按 0 处理并保持保守行为。
* 边界条件:空版本号小于任何非空版本号;两者均空则视为相等。
*/
int compareVersionString(const QString &left, const QString &right)
{
const QStringList leftParts = left.trimmed().split(".", Qt::SkipEmptyParts);
const QStringList rightParts = right.trimmed().split(".", Qt::SkipEmptyParts);
const int partCount = qMax(leftParts.count(), rightParts.count());
for(int i = 0; i < partCount; ++i)
{
const uint leftValue = (i < leftParts.count()) ? leftParts.at(i).toUInt() : 0U;
const uint rightValue = (i < rightParts.count()) ? rightParts.at(i).toUInt() : 0U;
if(leftValue < rightValue)
{
return -1;
}
if(leftValue > rightValue)
{
return 1;
}
}
return 0;
}
/**
* @brief 拼接基础 URL 和文件相对路径。
* @param baseUrl 远程基础地址,允许带或不带末尾斜杠。
* @param appName 应用名目录。
* @param fileName 文件名。
* @return 标准化后的下载 URL 字符串。
*/
QString buildDownloadUrl(QString baseUrl, const QString &appName, const QString &fileName)
{
if(!baseUrl.endsWith('/'))
{
baseUrl.append('/');
}
return baseUrl + appName + "/" + fileName;
}
}
UpdaterDialog::UpdaterDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::UpdaterDialog)
{
ui->setupUi(this);
this->setWindowTitle("Updater");
setUpdateButtonsEnabled(false);
m_totalSize = 0;
m_version = APPVERSION;
ui->versionInput->setText(m_version);
ui->appNameInput->setText(APPNAME);
ui->show->setEnabled(0);
ui->label_2->setText("");
ui->label_3->setText("");
m_pDownloader = new Downloader(this);
connect(m_pDownloader, &Downloader::doShowInfo, this, [this](const QString &s)
{
showStatus(s);
});
connect(m_pDownloader, &Downloader::onError, this, [this](QNetworkReply::NetworkError e)
{
const QMetaEnum metaEnum = QMetaEnum::fromType<QNetworkReply::NetworkError>();
const QString str = QString::fromLatin1(metaEnum.valueToKey(e));
ui->show->appendPlainText(str);
setUpdateButtonsEnabled(true);
});
connect(m_pDownloader, &Downloader::doFinished, this, [this]()
{
ui->show->appendPlainText("download finished");
onDownloadFinished();
});
connect(m_pDownloader, &Downloader::doProgress, this, [this](qint64 received, qint64 total)
{
m_totalSize = total > 0 ? static_cast<quint64>(total) : 0;
if(total > 0)
{
ui->progressBar->setMinimum(0);
ui->progressBar->setMaximum(100);
ui->progressBar->setValue(static_cast<int>((received * 100) / total));
calculateSizes(received, total);
calculateTimeRemaining(received, total);
}
else
{
ui->progressBar->setMinimum(0);
ui->progressBar->setMaximum(0);
ui->progressBar->setValue(-1);
ui->label_2->setText(tr("Downloading updates"));
}
});
}
UpdaterDialog::~UpdaterDialog()
{
delete m_UpdateProc;
delete ui;
}
void UpdaterDialog::showStatus(const QString &s)
{
ui->show->appendPlainText(s);
}
void UpdaterDialog::on_btn_check_clicked()
{
APPNAME = ui->appNameInput->text().trimmed();
if(APPNAME.isEmpty())
{
ui->show->appendPlainText("Please input app name");
return;
}
ui->show->clear();
setUpdateButtonsEnabled(false);
m_version = ui->versionInput->text().trimmed();
if(!m_pManager)
{
m_pManager = new QNetworkAccessManager(this);
connect(m_pManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(onCheckReply(QNetworkReply *)));
}
const QString url = CHECK_URL.arg(APPNAME);
QNetworkRequest request{QUrl(url)};
// 用户名和密码
QString user = "TianZD";
QString password = "P@ssw0rd!!!";
// Basic Auth: base64(username:password)
QByteArray userPassword = QString("%1:%2").arg(user, password).toUtf8();
QByteArray authHeader = "Basic " + userPassword.toBase64();
request.setRawHeader("Authorization", authHeader);
// 建议允许重定向
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
m_pManager->get(request);
}
bool UpdaterDialog::checkVersion(const QString &_localVersion, const QString &_remoteVersion)
{
// 本地版本为空或为 0 时视为首次安装/未知版本,优先下载完整包降低补丁失败风险。
if(_localVersion.trimmed().isEmpty() || _localVersion.trimmed() == "0")
{
m_bDownloadFullExe = true;
return true;
}
const int versionCompareResult = compareVersionString(_localVersion, _remoteVersion);
if(versionCompareResult > 0)
{
return false;
}
bool isNotLatest = versionCompareResult < 0;
if(!isNotLatest)
{
// 版本号一致时再比较构建日期和修改计数,兼容同版本热修复包。
if(m_lastChangeTime.toUInt() > APPDATE.toUInt())
{
isNotLatest = true;
}
if(!m_modifyCnt.trimmed().isEmpty() && !MODIFYCNT.trimmed().isEmpty())
{
if(m_modifyCnt.toUInt() > MODIFYCNT.toUInt())
{
isNotLatest = true;
}
}
}
return isNotLatest;
}
void UpdaterDialog::onCheckReply(QNetworkReply *reply)
{
reply->deleteLater();
if(reply->error() != QNetworkReply::NoError)
{
showStatus("check error: " + reply->errorString());
return;
}
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(document.isNull() || !document.isObject())
{
showStatus("reply doc is null");
return;
}
const QJsonObject updates = document.object().value("updates").toObject();
const QJsonObject platform = updates.value(PLATFORM).toObject();
if(platform.isEmpty())
{
showStatus(tr("no update information for platform: %1").arg(PLATFORM));
return;
}
m_changelog = platform.value("changelog").toString();
m_patchVersion = platform.value("patch-version").toString();
m_fullVersion = platform.value("full-version").toString();
m_lastChangeTime = platform.value("modify-time").toString();
m_modifyCnt = platform.value("modifyCnt").toString();
m_bDownloadFullExe = platform.value("download-full").toBool();
m_patchBaseUrl = platform.value("patch-url").toString();
m_fullBaseUrl = platform.value("full-url").toString();
if(m_version.isEmpty() || m_version == "0")
{
m_bDownloadFullExe = true;
}
rebuildDownloadInfo();
const QString remoteVersion = m_bDownloadFullExe ? m_fullVersion : m_patchVersion;
const bool isNotLatest = checkVersion(m_version, remoteVersion);
if(isNotLatest)
{
showStatus("======================================"
+ tr("\ncurrent version: v") + m_version
+ tr("\nmodify time: ") + APPDATE
+ tr("\nmodify cnt: ") + MODIFYCNT
+ "\n ------------------------------"
+ tr("\nlatest version: v") + remoteVersion
+ tr("\nmodify time: ") + m_lastChangeTime
+ tr("\nmodify cnt: ") + m_modifyCnt
+ tr("\nchange log: ") + m_changelog
+ "\n======================================");
if(m_bDownloadFullExe)
{
showStatus("need download full exe");
}
showStatus(tr("click update button to update"));
setUpdateButtonsEnabled(true);
}
else
{
showStatus("======================================"
+ tr("\ncurrent software is up-to-date")
+ tr("\nversion : v") + remoteVersion
+ tr("\nmodify time: ") + m_lastChangeTime
+ tr("\nmodify cnt: ") + m_modifyCnt
+ "\n======================================"
+ tr("\nno need to update")
+ tr("\nclick cancel button to quit"));
setUpdateButtonsEnabled(false);
}
}
void UpdaterDialog::calculateSizes(qint64 received, qint64 total)
{
const QString totalSize = formatBytes(total);
const QString receivedSize = formatBytes(received);
ui->label_2->setText(tr("Downloading updates") + " (" + receivedSize + " " + tr("of") + " " + totalSize
+ ")");
}
void UpdaterDialog::calculateTimeRemaining(qint64 received, qint64 total)
{
if(total <= 0 || received <= 0)
{
return;
}
const qint64 currentMs = m_downloadTimer.elapsed();
const qint64 elapsedSinceLast = currentMs - m_lastProgressMs;
double speed = 0.0;
if(elapsedSinceLast > 0)
{
speed = (received - m_lastProgressBytes) / 1024.0 / elapsedSinceLast * 1000.0;
}
m_lastProgressMs = currentMs;
m_lastProgressBytes = received;
const qint64 elapsedSeconds = qMax<qint64>(1, currentMs / 1000);
qreal timeRemaining = (total - received) / (received / static_cast<qreal>(elapsedSeconds));
QString timeString;
if(timeRemaining > 7200)
{
timeRemaining /= 3600;
const int hours = int(timeRemaining + 0.5);
timeString = hours > 1 ? tr("about %1 hours").arg(hours) : tr("about one hour");
}
else if(timeRemaining > 60)
{
timeRemaining /= 60;
const int minutes = int(timeRemaining + 0.5);
timeString = minutes > 1 ? tr("%1 minutes").arg(minutes) : tr("1 minute");
}
else
{
const int seconds = int(timeRemaining + 0.5);
timeString = seconds > 1 ? tr("%1 seconds").arg(seconds) : tr("1 second");
}
ui->label_3->setText(tr("speed: %1 kb/s ").arg(speed, 0, 'f', 1) + tr("Time remaining") + ": " + timeString);
}
void UpdaterDialog::on_btn_update_clicked()
{
rebuildDownloadInfo();
m_pDownloader->setFileName(m_fileName);
m_downloadTimer.restart();
m_lastProgressMs = 0;
m_lastProgressBytes = 0;
const QString url = m_bDownloadFullExe ? m_fullUrl : m_patchUrl;
m_pDownloader->startDownload(url);
}
void UpdaterDialog::on_btn_cancel_clicked()
{
if(restartApplication())
{
QApplication::quit();
}
}
void UpdaterDialog::onDownloadFinished()
{
const qreal totalSeconds = qMax<qreal>(0.001, m_downloadTimer.elapsed() / 1000.0);
if(m_totalSize > 0)
{
ui->label_3->setText("total time: " + QString::number(totalSeconds, 'f', 1) +
"s average speed: " + QString::number((m_totalSize / 1024.0 / totalSeconds), 'g', 3) + "kb/s");
}
const auto localInformation = QMessageBox::question(this, "update", "download finished\nstart update?");
if(localInformation != QMessageBox::Yes)
{
return;
}
QString appName;
if(!m_bDownloadFullExe)
{
// 补丁包下载完成后在应用目录解压;解压失败时保留更新器,避免退出后主程序缺文件。
QFileInfo fileInfo(m_pDownloader->getFile());
QZipReader zipReader(fileInfo.absoluteFilePath());
if(!zipReader.extractAll(fileInfo.absolutePath()))
{
showStatus("extract patch failed");
return;
}
appName = APPNAME + ".exe";
}
else
{
appName = m_pDownloader->fileName();
}
const QFileInfo appFile(QCoreApplication::applicationDirPath() + "/" + appName);
if(appFile.exists())
{
QDesktopServices::openUrl(QUrl::fromLocalFile(appFile.absoluteFilePath()));
QApplication::quit();
}
else
{
showStatus("update target file not exists: " + appFile.absoluteFilePath());
}
}
void UpdaterDialog::on_btn_update_patch_clicked()
{
m_bDownloadFullExe = false;
rebuildDownloadInfo();
m_pDownloader->setFileName(m_fileName);
m_downloadTimer.restart();
m_lastProgressMs = 0;
m_lastProgressBytes = 0;
m_pDownloader->startDownload(m_patchUrl);
}
void UpdaterDialog::setUpdateButtonsEnabled(bool enabled)
{
ui->btn_update->setEnabled(enabled);
ui->btn_update_patch->setEnabled(enabled && !m_patchUrl.isEmpty());
}
void UpdaterDialog::rebuildDownloadInfo()
{
const QString patchFileName = APPNAME + "-patch-v" + m_patchVersion + ".zip";
const QString fullFileName = APPNAME + "-full-v" + m_fullVersion + ".exe";
m_patchUrl = buildDownloadUrl(m_patchBaseUrl, APPNAME, patchFileName);
m_fullUrl = buildDownloadUrl(m_fullBaseUrl, APPNAME, fullFileName);
m_fileName = m_bDownloadFullExe ? fullFileName : patchFileName;
}
QString UpdaterDialog::formatBytes(qint64 bytes) const
{
if(bytes < 0)
{
return tr("unknown");
}
if(bytes < 1024)
{
return tr("%1 bytes").arg(bytes);
}
if(bytes < 1048576)
{
return tr("%1 KB").arg(QString::number(bytes / 1024.0, 'f', 1));
}
return tr("%1 MB").arg(QString::number(bytes / 1048576.0, 'f', 2));
}
bool UpdaterDialog::restartApplication()
{
if(APPNAME.trimmed().isEmpty())
{
showStatus("app name is empty");
return false;
}
QString program;
#if defined Q_OS_WIN
program = APPNAME + ".exe";
#elif defined Q_OS_LINUX
program = "./" + APPNAME;
#else
program = APPNAME;
#endif
// 取消更新后需要让原程序独立运行,使用 startDetached 避免更新器退出时影响子进程生命周期。
if(!QProcess::startDetached(program, QStringList()))
{
showStatus("restart app failed");
return false;
}
return true;
}