vmm preview

This commit is contained in:
cold-brewed
2024-06-08 13:25:09 -04:00
committed by Alexander Babikov
parent 138e54d16f
commit c6da2caff2
136 changed files with 7194 additions and 9 deletions

View File

@@ -251,6 +251,8 @@ struct accelKey def_acc_keys[NUM_ACCELS] = {
.seq="Ctrl+Alt+M" }
};
char vmm_path[1024] = { '\0'}; /* TEMPORARY - VM manager path to scan for VMs */
int vmm_enabled = 0;
/* Statistics. */
extern int mmuflush;
@@ -598,8 +600,8 @@ pc_show_usage(char *s)
#ifdef _WIN32
"-D or --debug\t\t\t- force debug output logging\n"
#endif
#if 0
"-E or --nographic\t\t- forces the old behavior\n"
#if 1
"-E or --vmmpath\t\t- vm manager path\n"
#endif
"-F or --fullscreen\t\t- start in fullscreen mode\n"
"-G or --lang langid\t\t- start with specified language\n"
@@ -732,13 +734,14 @@ usage:
} else if (!strcasecmp(argv[c], "--debug") || !strcasecmp(argv[c], "-D")) {
force_debug = 1;
#endif
#ifdef ENABLE_NG
} else if (!strcasecmp(argv[c], "--nographic") || !strcasecmp(argv[c], "-E")) {
/* Currently does nothing, but if/when we implement a built-in manager,
it's going to force the manager not to run, allowing the old usage
without parameter. */
ng = 1;
#endif
//#ifdef ENABLE_NG
} else if (!strcasecmp(argv[c], "--vmmpath") ||
!strcasecmp(argv[c], "-E")) {
/* Using this variable for vm manager path
Temporary solution!*/
if ((c+1) == argc) goto usage;
strcpy(vmm_path, argv[++c]);
//#endif
} else if (!strcasecmp(argv[c], "--fullscreen") || !strcasecmp(argv[c], "-F")) {
start_in_fullscreen = 1;
} else if (!strcasecmp(argv[c], "--logfile") || !strcasecmp(argv[c], "-L")) {
@@ -1025,6 +1028,10 @@ usage:
}
pclog("# Configuration file: %s\n#\n\n", cfg_path);
if (strlen(vmm_path) != 0) {
vmm_enabled = 1;
pclog("# VM Manager enabled. Path: %s\n", vmm_path);
}
/*
* We are about to read the configuration file, which MAY
* put data into global variables (the hard- and floppy

View File

@@ -176,6 +176,8 @@ extern char usr_path[1024]; /* path (dir) of user data */
extern char cfg_path[1024]; /* full path of config file */
extern int open_dir_usr_path; /* default file open dialog directory of usr_path */
extern char uuid[MAX_UUID_LEN]; /* UUID or machine identifier */
extern char vmm_path[1024]; /* VM Manager path to scan (temporary) */
extern int vmm_enabled;
#ifndef USE_NEW_DYNAREC
extern FILE *stdlog; /* file to log output to */
#endif

View File

@@ -189,6 +189,44 @@ add_library(ui STATIC
qt_mediahistorymanager.cpp
qt_mediahistorymanager.hpp
qt_updatecheck.cpp
qt_updatecheck.hpp
qt_updatecheckdialog.cpp
qt_updatecheckdialog.hpp
qt_updatedetails.cpp
qt_updatedetails.hpp
qt_downloader.cpp
qt_downloader.hpp
qt_vmmanager_clientsocket.cpp
qt_vmmanager_clientsocket.hpp
qt_vmmanager_serversocket.cpp
qt_vmmanager_serversocket.hpp
qt_vmmanager_protocol.cpp
qt_vmmanager_protocol.hpp
qt_vmmanager_details.hpp
qt_vmmanager_details.cpp
qt_vmmanager_details.ui
qt_vmmanager_addmachine.cpp
qt_vmmanager_addmachine.hpp
qt_vmmanager_detailsection.cpp
qt_vmmanager_detailsection.hpp
qt_vmmanager_listviewdelegate.hpp
qt_vmmanager_listviewdelegate.cpp
qt_vmmanager_preferences.cpp
qt_vmmanager_preferences.hpp
qt_vmmanager_main.hpp
qt_vmmanager_main.cpp
qt_vmmanager_main.ui
qt_vmmanager_model.cpp
qt_vmmanager_model.hpp
qt_vmmanager_system.cpp
qt_vmmanager_system.hpp
qt_vmmanager_config.cpp
qt_vmmanager_config.hpp
qt_vmmanager_mainwindow.cpp
qt_vmmanager_mainwindow.hpp
../qt_resources.qrc
./qdarkstyle/dark/darkstyle.qrc

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 B

BIN
src/qt/icons/pause-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

BIN
src/qt/icons/play-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

BIN
src/qt/icons/stop-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 B

95
src/qt/qt_downloader.cpp Normal file
View File

@@ -0,0 +1,95 @@
/*
* 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.
*
* Downloader module
*
*
*
* Authors: cold-brewed
*
* Copyright 2024 cold-brewed
*/
#include <QDir>
#include <QFile>
#include <QNetworkReply>
#include <QStandardPaths>
#include "qt_downloader.hpp"
extern "C" {
#include <86box/plat.h>
}
Downloader::
Downloader(const DownloadLocation downloadLocation, QObject *parent)
: QObject(parent)
, file(nullptr)
, reply(nullptr)
, variantData(QVariant::Invalid)
{
char PATHBUF[256];
switch (downloadLocation) {
case DownloadLocation::Data:
plat_get_global_data_dir(PATHBUF, 255);
break;
case DownloadLocation::Config:
plat_get_global_config_dir(PATHBUF, 255);
break;
case DownloadLocation::Temp:
plat_get_temp_dir(PATHBUF, 255);
break;
}
downloadDirectory = QDir(PATHBUF);
}
Downloader::~Downloader() { delete file; }
void Downloader::download(const QUrl &url, const QString &filepath, const QVariant &varData) {
variantData = varData;
// temporary until I get the plat stuff fixed
// const auto global_dir = temporaryGetGlobalDataDir();
// qDebug() << "I was passed filepath " << filepath;
// Join with filename to create final file
// const auto final_path = QDir(global_dir).filePath(filepath);
const auto final_path = downloadDirectory.filePath(filepath);
file = new QFile(final_path);
if(!file->open(QIODevice::WriteOnly)) {
qWarning() << "Unable to open file " << final_path;
return;
}
const auto nam = new QNetworkAccessManager(this);
// Create the network request and execute
const auto request = QNetworkRequest(url);
reply = nam->get(request);
// Connect to the finished signal
connect(reply, &QNetworkReply::finished, this, &Downloader::onResult);
}
void
Downloader::onResult()
{
if (reply->error()) {
qWarning() << "Error returned from QNetworkRequest: " << reply->errorString();
emit errorOccurred(reply->errorString());
reply->deleteLater();
return;
}
file->write(reply->readAll());
file->flush();
file->close();
reply->deleteLater();
qDebug() << Q_FUNC_INFO << "Downloaded complete: file written to " << file->fileName();
emit downloadCompleted(file->fileName(), variantData);
}

57
src/qt/qt_downloader.hpp Normal file
View File

@@ -0,0 +1,57 @@
/*
* 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.
*
* Header for the downloader module
*
*
*
* Authors: cold-brewed
*
* Copyright 2024 cold-brewed
*/
#ifndef QT_DOWNLOADER_HPP
#define QT_DOWNLOADER_HPP
#include <QDir>
#include <QNetworkAccessManager>
#include <QString>
#include <QFile>
class Downloader : public QObject {
Q_OBJECT
public:
enum class DownloadLocation {
Data, // AppDataLocation via plat_get_global_data_dir()
Config, // AppConfigLocation via plat_get_global_config_dir()
Temp // TempLocation via plat_get_temp_dir()
};
explicit Downloader(DownloadLocation downloadLocation = DownloadLocation::Data, QObject *parent = nullptr);
~Downloader() final;
void download(const QUrl &url, const QString &filepath, const QVariant &varData = QVariant::Invalid);
signals:
// Signal emitted when the download is successful
void downloadCompleted(QString filename, QVariant varData);
// Signal emitted when an error occurs
void errorOccurred(const QString&);
private slots:
void onResult();
private:
QFile *file;
QNetworkAccessManager nam;
QNetworkReply *reply;
QVariant variantData;
QDir downloadDirectory;
};
#endif

View File

@@ -77,6 +77,8 @@ extern "C" {
#include "qt_styleoverride.hpp"
#include "qt_unixmanagerfilter.hpp"
#include "qt_util.hpp"
#include "qt_vmmanager_clientsocket.hpp"
#include "qt_vmmanager_mainwindow.hpp"
// Void Cast
#define VC(x) const_cast<wchar_t *>(x)
@@ -662,6 +664,19 @@ main(int argc, char *argv[])
return 0;
}
if (vmm_enabled) {
// VMManagerMain vmm;
// // Hackish until there is a proper solution
// QApplication::setApplicationName("86Box VM Manager");
// QApplication::setApplicationDisplayName("86Box VM Manager");
// vmm.show();
// vmm.exec();
const auto vmm_main_window = new VMManagerMainWindow();
vmm_main_window->show();
QApplication::exec();
return 0;
}
#ifdef DISCORD
discord_load();
#endif
@@ -758,6 +773,36 @@ main(int argc, char *argv[])
socket.connectToServer(qgetenv("86BOX_MANAGER_SOCKET"));
}
VMManagerClientSocket manager_socket;
if (qgetenv("VMM_86BOX_SOCKET").size()) {
manager_socket.IPCConnect(qgetenv("VMM_86BOX_SOCKET"));
QObject::connect(&manager_socket, &VMManagerClientSocket::pause, main_window, &MainWindow::togglePause);
QObject::connect(&manager_socket, &VMManagerClientSocket::resetVM, main_window, &MainWindow::hardReset);
QObject::connect(&manager_socket, &VMManagerClientSocket::showsettings, main_window, &MainWindow::showSettings);
QObject::connect(&manager_socket, &VMManagerClientSocket::ctrlaltdel, []() { pc_send_cad(); });
QObject::connect(&manager_socket, &VMManagerClientSocket::request_shutdown, main_window, &MainWindow::close);
QObject::connect(&manager_socket, &VMManagerClientSocket::force_shutdown, []() {
do_stop();
emit main_window->close();
});
QObject::connect(main_window, &MainWindow::vmmRunningStateChanged, &manager_socket, &VMManagerClientSocket::clientRunningStateChanged);
main_window->installEventFilter(&manager_socket);
}
/* Warn the user about unsupported configs */
if (cpu_override) {
QMessageBox warningbox(QMessageBox::Icon::Warning, QObject::tr("You are loading an unsupported configuration"),
QObject::tr("CPU type filtering based on selected machine is disabled for this emulated machine.\n\nThis makes it possible to choose a CPU that is otherwise incompatible with the selected machine. However, you may run into incompatibilities with the machine BIOS or other software.\n\nEnabling this setting is not officially supported and any bug reports filed may be closed as invalid."),
QMessageBox::NoButton, main_window);
warningbox.addButton(QObject::tr("Continue"), QMessageBox::AcceptRole);
warningbox.addButton(QObject::tr("Exit"), QMessageBox::RejectRole);
warningbox.exec();
if (warningbox.result() == QDialog::Accepted) {
confirm_exit_cmdl = 0; /* skip the confirmation prompt without touching the config */
emit main_window->close();
}
}
// pc_reset_hard_init();
QTimer onesec;

View File

@@ -2131,6 +2131,7 @@ MainWindow::updateUiPauseState()
QString(tr("Pause execution"));
ui->actionPause->setIcon(pause_icon);
ui->actionPause->setToolTip(tooltip_text);
emit vmmRunningStateChanged(static_cast<VMManagerProtocol::RunningState>(dopause));
}
void

View File

@@ -13,6 +13,8 @@
#include <array>
#include <atomic>
#include "qt_vmmanager_protocol.hpp"
class MediaMenu;
class RendererStack;
@@ -63,6 +65,8 @@ signals:
void showMessageForNonQtThread(int flags, const QString &header, const QString &message, bool richText, std::atomic_bool* done);
void getTitleForNonQtThread(wchar_t *title);
void vmmRunningStateChanged(VMManagerProtocol::RunningState state);
public slots:
void showSettings();
void hardReset();

360
src/qt/qt_updatecheck.cpp Normal file
View 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;
}

104
src/qt/qt_updatecheck.hpp Normal file
View File

@@ -0,0 +1,104 @@
/*
* 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.
*
* Header for the update check module
*
*
*
* Authors: cold-brewed
*
* Copyright 2024 cold-brewed
*/
#ifndef QT_UPDATECHECK_HPP
#define QT_UPDATECHECK_HPP
#include <QJsonObject>
#include <QObject>
#include <QUrl>
#include <QWidget>
#include <optional>
class UpdateCheck final : public QObject {
Q_OBJECT
public:
enum class UpdateChannel {
Stable,
CI,
};
struct JenkinsChangeSetItem {
QString buildId; // sha hash
QString author; // github username
QString message; // commit message
QStringList affectedPaths; // list of files in the change
};
struct JenkinsReleaseInfo {
int buildNumber = 0;
QString result;
qint64 timestamp = 0;
QList<JenkinsChangeSetItem> changeSetItems;
};
struct GithubReleaseInfo {
QString name;
QString tag_name;
QString html_url;
QString target_commitish;
QString created_at;
QString published_at;
QString body;
};
struct UpdateResult {
UpdateChannel channel;
bool updateAvailable = false;
bool upToDate = false;
QString currentVersion;
QString latestVersion;
QList<GithubReleaseInfo> githubInfo;
QList<JenkinsReleaseInfo> jenkinsInfo;
};
explicit UpdateCheck(UpdateChannel channel, QObject *parent = nullptr);
~UpdateCheck() override;
void checkForUpdates();
static int versionCompare(const QString &version1, const QString &version2);
[[nodiscard]] static QString getCurrentVersion(const UpdateChannel &updateChannel = UpdateChannel::Stable);
signals:
// void updateCheckComplete(const UpdateCheck::UpdateChannel &channel, const QVariant &updateData);
void updateCheckComplete(const UpdateCheck::UpdateResult &result);
void updateCheckError(const QString &errorMsg);
private:
UpdateChannel updateChannel = UpdateChannel::Stable;
const QUrl githubReleaseApi = QUrl("https://api.github.com/repos/86box/86Box/releases");
const QUrl jenkinsLatestApi = QUrl("https://ci.86box.net/job/86box/lastSuccessfulBuild/api/json");
QString jenkinsLatestVersion;
QString currentVersion;
static QUrl jenkinsLatestNReleasesUrl(const int &count);
static std::optional<QList<JenkinsReleaseInfo>> parseJenkinsJson(const QString &filename);
static std::optional<JenkinsReleaseInfo> parseJenkinsRelease(const QJsonObject &json);
static std::optional<QList<GithubReleaseInfo>> parseGithubJson(const QString &filename);
static std::optional<GithubReleaseInfo> parseGithubRelease(const QJsonObject &json);
private slots:
void jenkinsDownloadComplete(const QString &filename, const QVariant& varData);
void githubDownloadComplete(const QString &filename, const QVariant& varData);
void generalDownloadError(const QString &error);
};
#endif // QT_UPDATECHECK_HPP

View File

@@ -0,0 +1,90 @@
/*
* 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 dialog module
*
*
*
* Authors: cold-brewed
*
* Copyright 2024 cold-brewed
*/
#include <QDir>
#include <QTimer>
#include "qt_updatecheckdialog.hpp"
#include "ui_qt_updatecheckdialog.h"
#include "qt_updatedetails.hpp"
extern "C" {
#include <86box/version.h>
}
UpdateCheckDialog::
UpdateCheckDialog(const UpdateCheck::UpdateChannel channel, QWidget *parent) : QDialog(parent), ui(new Ui::UpdateCheckDialog), updateCheck(new UpdateCheck(channel))
{
ui->setupUi(this);
setWindowTitle(tr("Update check"));
ui->statusLabel->setHidden(true);
updateChannel = channel;
currentVersion = UpdateCheck::getCurrentVersion(updateChannel);
connect(updateCheck, &UpdateCheck::updateCheckError, [=](const QString &errorMsg) {
generalDownloadError(errorMsg);
});
connect(updateCheck, &UpdateCheck::updateCheckComplete, this, &UpdateCheckDialog::downloadComplete);
QTimer::singleShot(0, [this] {
updateCheck->checkForUpdates();
});
}
UpdateCheckDialog::~
UpdateCheckDialog()
= default;
void
UpdateCheckDialog::generalDownloadError(const QString &error) const
{
ui->progressBar->setMaximum(100);
ui->progressBar->setValue(100);
ui->statusLabel->setVisible(true);
const auto statusText = tr("There was an error checking for updates:\n\n%1\n\nPlease try again later.").arg(error);
ui->statusLabel->setText(statusText);
ui->buttonBox->setStandardButtons(QDialogButtonBox::Ok);
}
void
UpdateCheckDialog::downloadComplete(const UpdateCheck::UpdateResult &result)
{
if (result.upToDate) {
upToDate();
return;
}
const auto updateDetails = new UpdateDetails(result);
connect(updateDetails, &QDialog::accepted, [this] {
accept();
});
connect(updateDetails, &QDialog::rejected, [this] {
reject();
});
updateDetails->exec();
}
void
UpdateCheckDialog::upToDate()
{
ui->titleLabel->setText(tr("Update check complete"));
ui->progressBar->setMaximum(100);
ui->progressBar->setValue(100);
ui->statusLabel->setVisible(true);
const auto statusText = tr("You are running the latest %1 version of 86Box: %2").arg(updateChannel == UpdateCheck::UpdateChannel::Stable ? "stable" : "beta", currentVersion);
ui->statusLabel->setText(statusText);
ui->buttonBox->setStandardButtons(QDialogButtonBox::Ok);
}

View File

@@ -0,0 +1,47 @@
/*
* 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.
*
* Header for the update check dialog module
*
*
*
* Authors: cold-brewed
*
* Copyright 2024 cold-brewed
*/
#ifndef QT_UPDATECHECKDIALOG_HPP
#define QT_UPDATECHECKDIALOG_HPP
#include <QDialog>
#include <qt_updatecheck.hpp>
namespace Ui {
class UpdateCheckDialog;
}
class UpdateCheckDialog final : public QDialog {
Q_OBJECT
public:
explicit UpdateCheckDialog(UpdateCheck::UpdateChannel channel, QWidget *parent = nullptr);
~UpdateCheckDialog() override;
private:
Ui::UpdateCheckDialog *ui;
UpdateCheck::UpdateChannel updateChannel = UpdateCheck::UpdateChannel::Stable;
UpdateCheck *updateCheck;
QString currentVersion;
void upToDate();
private slots:
void downloadComplete(const UpdateCheck::UpdateResult &result);
void generalDownloadError(const QString &error) const;
};
#endif // QT_UPDATECHECKDIALOG_HPP

Some files were not shown because too many files have changed in this diff Show More