You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
475 lines
14 KiB
475 lines
14 KiB
#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 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 返回 -1,left == right 返回 0,left > 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)};
|
|
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;
|
|
}
|
|
|