vmm preview
This commit is contained in:
committed by
Alexander Babikov
parent
138e54d16f
commit
c6da2caff2
360
src/qt/qt_updatecheck.cpp
Normal file
360
src/qt/qt_updatecheck.cpp
Normal file
@@ -0,0 +1,360 @@
|
||||
/*
|
||||
* 86Box A hypervisor and IBM PC system emulator that specializes in
|
||||
* running old operating systems and software designed for IBM
|
||||
* PC systems and compatibles from 1981 through fairly recent
|
||||
* system designs based on the PCI bus.
|
||||
*
|
||||
* This file is part of the 86Box distribution.
|
||||
*
|
||||
* Update check module
|
||||
*
|
||||
*
|
||||
*
|
||||
* Authors: cold-brewed
|
||||
*
|
||||
* Copyright 2024 cold-brewed
|
||||
*/
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
|
||||
#include "qt_updatecheck.hpp"
|
||||
#include "qt_downloader.hpp"
|
||||
#include "qt_updatedetails.hpp"
|
||||
|
||||
extern "C" {
|
||||
#include <86box/version.h>
|
||||
}
|
||||
|
||||
UpdateCheck::
|
||||
UpdateCheck(const UpdateChannel channel, QObject *parent) : QObject(parent)
|
||||
{
|
||||
updateChannel = channel;
|
||||
currentVersion = getCurrentVersion(channel);
|
||||
}
|
||||
|
||||
UpdateCheck::~
|
||||
UpdateCheck()
|
||||
= default;
|
||||
|
||||
void
|
||||
UpdateCheck::checkForUpdates()
|
||||
{
|
||||
if (updateChannel == UpdateChannel::Stable) {
|
||||
const auto githubDownloader = new Downloader(Downloader::DownloadLocation::Temp);
|
||||
connect(githubDownloader, &Downloader::downloadCompleted, this, &UpdateCheck::githubDownloadComplete);
|
||||
connect(githubDownloader, &Downloader::errorOccurred, this, &UpdateCheck::generalDownloadError);
|
||||
githubDownloader->download(QUrl(githubReleaseApi), "github_releases.json");
|
||||
} else {
|
||||
const auto jenkinsDownloader = new Downloader(Downloader::DownloadLocation::Temp);
|
||||
connect(jenkinsDownloader, &Downloader::downloadCompleted, this, &UpdateCheck::jenkinsDownloadComplete);
|
||||
connect(jenkinsDownloader, &Downloader::errorOccurred, this, &UpdateCheck::generalDownloadError);
|
||||
jenkinsDownloader->download(jenkinsLatestNReleasesUrl(10), "jenkins_list.json");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
UpdateCheck::jenkinsDownloadComplete(const QString &filename, const QVariant &varData)
|
||||
{
|
||||
auto generalError = tr("Unable to determine release information");
|
||||
auto jenkinsReleaseListResult = parseJenkinsJson(filename);
|
||||
auto latestVersion = 0; // NOLINT (Default value as a fallback)
|
||||
|
||||
if(!jenkinsReleaseListResult.has_value() || jenkinsReleaseListResult.value().isEmpty()) {
|
||||
generalDownloadError(generalError);
|
||||
return;
|
||||
}
|
||||
const auto jenkinsReleaseList = jenkinsReleaseListResult.value();
|
||||
latestVersion = jenkinsReleaseListResult->first().buildNumber;
|
||||
|
||||
// If we can't determine the local build (blank current version), always show an update as available.
|
||||
// Callers can adjust accordingly.
|
||||
// Otherwise, do a comparison with EMU_BUILD_NUM
|
||||
bool updateAvailable = false;
|
||||
bool upToDate = true;
|
||||
if(currentVersion.isEmpty() || EMU_BUILD_NUM < latestVersion) {
|
||||
updateAvailable = true;
|
||||
upToDate = false;
|
||||
}
|
||||
|
||||
const auto updateResult = UpdateResult {
|
||||
.channel = updateChannel,
|
||||
.updateAvailable = updateAvailable,
|
||||
.upToDate = upToDate,
|
||||
.currentVersion = currentVersion,
|
||||
.latestVersion = QString::number(latestVersion),
|
||||
.githubInfo = {},
|
||||
.jenkinsInfo = jenkinsReleaseList,
|
||||
};
|
||||
|
||||
emit updateCheckComplete(updateResult);
|
||||
}
|
||||
|
||||
void
|
||||
UpdateCheck::generalDownloadError(const QString &error)
|
||||
{
|
||||
emit updateCheckError(error);
|
||||
}
|
||||
|
||||
void
|
||||
UpdateCheck::githubDownloadComplete(const QString &filename, const QVariant &varData)
|
||||
{
|
||||
const auto generalError = tr("Unable to determine release information");
|
||||
const auto githubReleaseListResult = parseGithubJson(filename);
|
||||
QString latestVersion = "0.0";
|
||||
if(!githubReleaseListResult.has_value() || githubReleaseListResult.value().isEmpty()) {
|
||||
generalDownloadError(generalError);
|
||||
}
|
||||
auto githubReleaseList = githubReleaseListResult.value();
|
||||
// Warning: this check (using the tag name) relies on a consistent naming scheme: "v<number>"
|
||||
// where <number> is the release number. For example, 4.2 from v4.2 as the tag name.
|
||||
// Another option would be parsing the name field which is generally "86Box <number>" but
|
||||
// either option requires a consistent naming scheme.
|
||||
latestVersion = githubReleaseList.first().tag_name.replace("v", "");
|
||||
for (const auto &release: githubReleaseList) {
|
||||
qDebug().noquote().nospace() << release.name << ": " << release.html_url << " (" << release.created_at << ")";
|
||||
}
|
||||
|
||||
// const auto updateDetails = new UpdateDetails(githubReleaseList, currentVersion);
|
||||
bool updateAvailable = false;
|
||||
bool upToDate = true;
|
||||
if(currentVersion.isEmpty() || (versionCompare(currentVersion, latestVersion) < 0)) {
|
||||
updateAvailable = true;
|
||||
upToDate = false;
|
||||
}
|
||||
|
||||
const auto updateResult = UpdateResult {
|
||||
.channel = updateChannel,
|
||||
.updateAvailable = updateAvailable,
|
||||
.upToDate = upToDate,
|
||||
.currentVersion = currentVersion,
|
||||
.latestVersion = latestVersion,
|
||||
.githubInfo = githubReleaseList,
|
||||
.jenkinsInfo = {},
|
||||
};
|
||||
|
||||
emit updateCheckComplete(updateResult);
|
||||
|
||||
}
|
||||
|
||||
QUrl
|
||||
UpdateCheck::jenkinsLatestNReleasesUrl(const int &count)
|
||||
{
|
||||
const auto urlPath = QString("https://ci.86box.net/job/86box/api/json?tree=builds[number,result,timestamp,changeSets[items[commitId,affectedPaths,author[fullName],msg,id]]]{0,%1}").arg(count);
|
||||
return { urlPath };
|
||||
}
|
||||
|
||||
QString
|
||||
UpdateCheck::getCurrentVersion(const UpdateChannel &updateChannel)
|
||||
{
|
||||
if (updateChannel == UpdateChannel::Stable) {
|
||||
return {EMU_VERSION};
|
||||
}
|
||||
// If EMU_BUILD_NUM is anything other than the default of zero it was set by the build process
|
||||
if constexpr (EMU_BUILD_NUM != 0) {
|
||||
return QString::number(EMU_BUILD_NUM); // NOLINT because EMU_BUILD_NUM is defined as 0 by default and is set at build time
|
||||
}
|
||||
// EMU_BUILD_NUM is not set, most likely a local build
|
||||
return {}; // NOLINT (Having EMU_BUILD_NUM assigned to a default number throws off the linter)
|
||||
}
|
||||
|
||||
std::optional<QList<UpdateCheck::JenkinsReleaseInfo>>
|
||||
UpdateCheck::parseJenkinsJson(const QString &filename)
|
||||
{
|
||||
QList<JenkinsReleaseInfo> releaseInfoList;
|
||||
QFile json_file(filename);
|
||||
if (!json_file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
qWarning() << "Couldn't open the json file: error" << json_file.error();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QString read_file = json_file.readAll();
|
||||
json_file.close();
|
||||
|
||||
const auto json_doc = QJsonDocument::fromJson(read_file.toUtf8());
|
||||
|
||||
if (json_doc.isNull()) {
|
||||
qWarning("Failed to create QJsonDocument, possibly invalid JSON");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
qWarning("JSON does not have the expected format (object in root), cannot continue");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto json_object = json_doc.object();
|
||||
|
||||
// The json contains multiple release
|
||||
if(json_object.contains("builds") && json_object["builds"].isArray()) {
|
||||
|
||||
QJsonArray builds = json_object["builds"].toArray();
|
||||
for (const auto &each_build: builds) {
|
||||
if (auto build = parseJenkinsRelease(each_build.toObject()); build.has_value() && build.value().result == "SUCCESS") {
|
||||
releaseInfoList.append(build.value());
|
||||
}
|
||||
}
|
||||
} else if(json_object.contains("changeSets") && json_object["changeSets"].isArray()) {
|
||||
// The json contains only one release, as obtained by the lastSuccessfulBuild api
|
||||
if (const auto build = parseJenkinsRelease(json_object); build.has_value()) {
|
||||
releaseInfoList.append(build.value());
|
||||
}
|
||||
} else {
|
||||
qWarning("JSON is missing data or has invalid data, cannot continue");
|
||||
qDebug() << json_object;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return releaseInfoList;
|
||||
}
|
||||
|
||||
std::optional<UpdateCheck::JenkinsReleaseInfo>
|
||||
UpdateCheck::parseJenkinsRelease(const QJsonObject &json)
|
||||
{
|
||||
// The root should contain number, result, and timestamp.
|
||||
if (!json.contains("number") || !json.contains("result") || !json.contains("timestamp")) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto releaseInfo = JenkinsReleaseInfo {
|
||||
.buildNumber = json["number"].toInt(),
|
||||
.result = json["result"].toString(),
|
||||
.timestamp = static_cast<qint64>(json["timestamp"].toDouble())
|
||||
};
|
||||
|
||||
// Overview
|
||||
// Each build should contain a changeSets object with an array. Only the first element is needed.
|
||||
// The first element should be an object containing an items object with an array.
|
||||
// Each array element in the items object has information releated to the build. More or less: commit data
|
||||
// In jq parlance it would be similar to `builds[].changeSets[0].items[]`
|
||||
|
||||
// To break down the somewhat complicated if-init statement below:
|
||||
// * Get the object for `changeSets`
|
||||
// * Convert the value to array
|
||||
// * Grab the first element in the array
|
||||
// Proceed if
|
||||
// * the element (first in changeSets) is an object that contains the key `items`
|
||||
if (const auto changeSet = json["changeSets"].toArray().first(); changeSet.isObject() && changeSet.toObject().contains("items")) {
|
||||
// Then proceed to process each `items` array element
|
||||
for (const auto &item : changeSet.toObject()["items"].toArray()) {
|
||||
auto itemObject = item.toObject();
|
||||
// Basic validation
|
||||
if (!itemObject.contains("commitId") || !itemObject.contains("msg") || !itemObject.contains("affectedPaths")) {
|
||||
return std::nullopt;
|
||||
}
|
||||
// Convert the paths for each commit to a string list
|
||||
QStringList paths;
|
||||
for (const auto &each_path : itemObject["affectedPaths"].toArray().toVariantList()) {
|
||||
if (each_path.type() == QVariant::String) {
|
||||
paths.append(each_path.toString());
|
||||
}
|
||||
}
|
||||
// Build the structure
|
||||
const auto releaseItem = JenkinsChangeSetItem {
|
||||
.buildId = itemObject["commitId"].toString(),
|
||||
.author = itemObject["author"].toObject()["fullName"].toString(),
|
||||
.message = itemObject["msg"].toString(),
|
||||
.affectedPaths = paths,
|
||||
};
|
||||
releaseInfo.changeSetItems.append(releaseItem);
|
||||
}
|
||||
} else {
|
||||
qWarning("Could not parse release information, possibly invalid JSON");
|
||||
}
|
||||
return releaseInfo;
|
||||
}
|
||||
|
||||
std::optional<QList<UpdateCheck::GithubReleaseInfo>>
|
||||
UpdateCheck::parseGithubJson(const QString &filename)
|
||||
{
|
||||
QList<GithubReleaseInfo> releaseInfoList;
|
||||
QFile json_file(filename);
|
||||
if (!json_file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
qWarning("Couldn't open the json file: error %d", json_file.error());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QString read_file = json_file.readAll();
|
||||
json_file.close();
|
||||
|
||||
const auto json_doc = QJsonDocument::fromJson(read_file.toUtf8());
|
||||
|
||||
if (json_doc.isNull()) {
|
||||
qWarning("Failed to create QJsonDocument, possibly invalid JSON");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!json_doc.isArray()) {
|
||||
qWarning("JSON does not have the expected format (array in root), cannot continue");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto release_array = json_doc.array();
|
||||
|
||||
for (const auto &each_release: release_array) {
|
||||
if (auto release = parseGithubRelease(each_release.toObject()); release.has_value()) {
|
||||
releaseInfoList.append(release.value());
|
||||
}
|
||||
}
|
||||
return releaseInfoList;
|
||||
}
|
||||
std::optional<UpdateCheck::GithubReleaseInfo>
|
||||
UpdateCheck::parseGithubRelease(const QJsonObject &json)
|
||||
{
|
||||
// Perform some basic validation
|
||||
if (!json.contains("name") || !json.contains("tag_name") || !json.contains("html_url")) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto githubRelease = GithubReleaseInfo {
|
||||
.name = json["name"].toString(),
|
||||
.tag_name = json["tag_name"].toString(),
|
||||
.html_url = json["html_url"].toString(),
|
||||
.target_commitish = json["target_commitish"].toString(),
|
||||
.created_at = json["created_at"].toString(),
|
||||
.published_at = json["published_at"].toString(),
|
||||
.body = json["body"].toString(),
|
||||
};
|
||||
|
||||
return githubRelease;
|
||||
}
|
||||
|
||||
// A simple method to compare version numbers
|
||||
// Should work for comparing x.y.z and x.y. Missing
|
||||
// values (parts) will be treated as zeroes
|
||||
int
|
||||
UpdateCheck::versionCompare(const QString &version1, const QString &version2)
|
||||
{
|
||||
// Split both
|
||||
QStringList v1List = version1.split('.');
|
||||
QStringList v2List = version2.split('.');
|
||||
|
||||
// Out of the two versions get the maximum amount of "parts"
|
||||
const int maxParts = std::max(v1List.size(), v2List.size());
|
||||
|
||||
// Initialize both with zeros
|
||||
QVector<int> v1Parts(maxParts, 0);
|
||||
QVector<int> v2Parts(maxParts, 0);
|
||||
|
||||
for (int i = 0; i < v1List.size(); ++i) {
|
||||
v1Parts[i] = v1List[i].toInt();
|
||||
}
|
||||
|
||||
for (int i = 0; i < v2List.size(); ++i) {
|
||||
v2Parts[i] = v2List[i].toInt();
|
||||
}
|
||||
|
||||
for (int i = 0; i < maxParts; ++i) {
|
||||
// First version is greater
|
||||
if (v1Parts[i] > v2Parts[i])
|
||||
return 1;
|
||||
// First version is less
|
||||
if (v1Parts[i] < v2Parts[i])
|
||||
return -1;
|
||||
}
|
||||
// They are equal
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user