From e3baf3e695f7b37122f4ffb2e309d8ed7eb0f1c3 Mon Sep 17 00:00:00 2001 From: John McKisson Date: Sat, 4 Apr 2026 12:11:23 -0400 Subject: [PATCH 1/7] switch to GH release feed --- MudletInstaller.cpp | 215 ++++++++++++++++++++++++++++++++++---------- MudletInstaller.h | 2 + 2 files changed, 170 insertions(+), 47 deletions(-) diff --git a/MudletInstaller.cpp b/MudletInstaller.cpp index d97ad40..831a4b0 100644 --- a/MudletInstaller.cpp +++ b/MudletInstaller.cpp @@ -19,19 +19,21 @@ #include #include -QMap getPlatformFeedMap(const QString &type) { - - const QString dblsqdFeedType = type == "PTB" ? "public-test-build" : "release"; - - const QString dblsqdFeedUrl = "https://feeds.dblsqd.com/MKMMR7HNSP65PquQQbiDIw/"; - - return { - {"mac/arm", QString("%1%2/mac/arm").arg(dblsqdFeedUrl).arg(dblsqdFeedType)}, - {"mac/x86_64", QString("%1%2/mac/x86_64").arg(dblsqdFeedUrl).arg(dblsqdFeedType)}, - {"win/x86_64", QString("%1%2/win/x86_64").arg(dblsqdFeedUrl).arg(dblsqdFeedType)}, - {"win/x86", QString("%1%2/win/x86").arg(dblsqdFeedUrl).arg(dblsqdFeedType)}, - {"linux/x86_64", QString("%1%2/linux/x86_64").arg(dblsqdFeedUrl).arg(dblsqdFeedType)} - }; +/** + * @brief Build the asset filename pattern for the current platform + * Matches the naming convention used by Mudlet's GitHub releases + */ +QString buildAssetPattern(const QString &os) { + if (os == "linux/x86_64") { + return "-linux-x64.AppImage.tar"; + } else if (os == "win/x86_64" || os == "win/x86") { + return "-windows-64-installer.exe"; + } else if (os == "mac/arm") { + return "-arm64.dmg"; + } else if (os == "mac/x86_64") { + return "-x86_64.dmg"; + } + return {}; } @@ -75,9 +77,9 @@ QString readLaunchProfileFromResource() { /** - * @brief Verify the downloaded file sha256 with the provided hash from dblsqd - * - * @param filePath Path to the file of whose hash wil be computed + * @brief Verify the downloaded file sha256 with the provided hash from the release + * + * @param filePath Path to the file of whose hash will be computed * @param expectedHash Expected sha256 hash * @return true If the expectedHash matches the sha256 hash of the file * @return false If the expectedHash does not match the sha256 hash of the file @@ -242,28 +244,33 @@ void MudletInstaller::start() { /** - * @brief Query the platform OS and fetch the proper platform feed from dblsqd + * @brief Query GitHub Releases API for the latest Mudlet release */ void MudletInstaller::fetchPlatformFeed() { QSettings settings(":/resources/launch.ini", QSettings::IniFormat); - QString releaseType = settings.value("Settings/RELEASE_TYPE", "").toString(); - QMap feedMap = getPlatformFeedMap(releaseType); - QString os = detectOS(); + assetPattern = buildAssetPattern(os); - QString feedUrl = feedMap.value(os); - - if (feedUrl.isEmpty()) { - qDebug() << "No feed URL found for platform:" << os; + if (assetPattern.isEmpty()) { + qDebug() << "No asset pattern found for platform:" << os; emit errorOccurred(); return; } - currentReply = networkManager.get(QNetworkRequest(QUrl(feedUrl))); + // Use per_page=10 for PTB (need to scan prereleases), per_page=100 for stable + bool isPTB = (releaseType == "PTB"); + int perPage = isPTB ? 10 : 100; + QString feedUrl = QString("https://api.github.com/repos/Mudlet/Mudlet/releases?per_page=%1").arg(perPage); + QNetworkRequest request(QUrl(feedUrl)); + request.setRawHeader("Accept", "application/vnd.github+json"); + request.setRawHeader("User-Agent", "MudletInstaller"); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + + currentReply = networkManager.get(request); connect(currentReply, &QNetworkReply::finished, this, &MudletInstaller::onFetchPlatformFeedFinished); // Show the progress bar window @@ -271,8 +278,8 @@ void MudletInstaller::fetchPlatformFeed() { } /** - * @brief Called upon complete receipt of the platform feed. - * Extracts the url and sha256 from the JSON and sets up a new download for the proper file. + * @brief Called upon complete receipt of the GitHub releases feed. + * Finds the latest matching release and its platform asset, then fetches SHA256SUMS.txt. */ void MudletInstaller::onFetchPlatformFeedFinished() { if (currentReply->error() != QNetworkReply::NoError) { @@ -286,34 +293,78 @@ void MudletInstaller::onFetchPlatformFeedFinished() { currentReply->deleteLater(); QJsonDocument doc = QJsonDocument::fromJson(jsonData); - if (doc.isNull() || !doc.isObject()) { - qDebug() << "Invalid JSON data."; + if (doc.isNull() || !doc.isArray()) { + // Check for GitHub API error (returns object with "message" field) + if (doc.isObject() && doc.object().contains("message")) { + qDebug() << "GitHub API error:" << doc.object().value("message").toString(); + } else { + qDebug() << "Invalid JSON data from GitHub API."; + } emit errorOccurred(); return; } - QJsonObject rootObj = doc.object(); - QJsonArray releases = rootObj.value("releases").toArray(); - if (releases.isEmpty()) { - qDebug() << "No releases found."; + QSettings settings(":/resources/launch.ini", QSettings::IniFormat); + QString releaseType = settings.value("Settings/RELEASE_TYPE", "").toString(); + bool isPTB = (releaseType == "PTB"); + + QJsonArray releasesArray = doc.array(); + QString checksumsUrl; + + for (const auto &val : releasesArray) { + QJsonObject releaseObj = val.toObject(); + + // Filter: PTB = prereleases only, stable = non-prereleases only + if (isPTB != releaseObj.value("prerelease").toBool()) { + continue; + } + // Skip drafts + if (releaseObj.value("draft").toBool()) { + continue; + } + + QJsonArray assets = releaseObj.value("assets").toArray(); + + // Search assets for our platform binary and SHA256SUMS.txt + for (const auto &assetVal : assets) { + QJsonObject asset = assetVal.toObject(); + QString name = asset.value("name").toString(); + + if (name == "SHA256SUMS.txt") { + checksumsUrl = asset.value("browser_download_url").toString(); + continue; + } + + if (info.url.isEmpty() && name.contains(assetPattern, Qt::CaseInsensitive)) { + info.url = asset.value("browser_download_url").toString(); + } + } + + // If we found a matching asset in this release, use it + if (!info.url.isEmpty()) { + QString tagName = releaseObj.value("tag_name").toString(); + qDebug() << "Found release:" << tagName; + break; + } + + // Reset for next release + checksumsUrl.clear(); + } + + if (info.url.isEmpty()) { + qDebug() << "No matching asset found for pattern:" << assetPattern; emit errorOccurred(); return; } - QJsonObject firstRelease = releases[0].toObject(); - QJsonObject download = firstRelease.value("download").toObject(); - info.sha256 = download.value("sha256").toString(); - info.url = download.value("url").toString(); - - qDebug() << "SHA-256:" << info.sha256; qDebug() << "URL:" << info.url; + // Extract the filename from the download URL QRegularExpression regex(R"(/([^/]+)\.(exe|dmg|AppImage\.tar)$)"); QRegularExpressionMatch match = regex.match(info.url); if (match.hasMatch()) { QString os = detectOS(); - info.appName = match.captured(1); if (os.startsWith("mac") || os.startsWith("linux")) { info.appName += "." + match.captured(2); @@ -332,7 +383,72 @@ void MudletInstaller::onFetchPlatformFeedFinished() { outputFile = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + outputFile; } - qDebug() << "OutputFile: " << outputFile; + qDebug() << "OutputFile:" << outputFile; + + // Fetch SHA256SUMS.txt if available + if (!checksumsUrl.isEmpty()) { + qDebug() << "Fetching checksums from:" << checksumsUrl; + QNetworkRequest request(QUrl(checksumsUrl)); + request.setRawHeader("User-Agent", "MudletInstaller"); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + currentReply = networkManager.get(request); + connect(currentReply, &QNetworkReply::finished, this, &MudletInstaller::onChecksumsFetchFinished); + } else { + qDebug() << "No SHA256SUMS.txt found, proceeding without hash verification"; + emit feedFetched(); + } +} + + +/** + * @brief Called upon receipt of SHA256SUMS.txt from the GitHub release. + * Parses the checksums file to find the hash matching our download asset. + */ +void MudletInstaller::onChecksumsFetchFinished() { + if (currentReply->error() != QNetworkReply::NoError) { + qDebug() << "Failed to fetch checksums:" << currentReply->errorString() + << "- proceeding without hash verification"; + currentReply->deleteLater(); + emit feedFetched(); + return; + } + + QString checksumData = QString::fromUtf8(currentReply->readAll()); + currentReply->deleteLater(); + + // Extract the filename portion from the download URL + QString downloadFilename = QUrl(info.url).fileName(); + + // Parse SHA256SUMS.txt lines in format: "hash filename" or "hash *filename" + QStringList lines = checksumData.split('\n', Qt::SkipEmptyParts); + QRegularExpression separatorRx(R"([\s*]+)"); + QRegularExpression hexRx(R"(^[0-9a-fA-F]{64}$)"); + + for (const auto &line : lines) { + // SHA256 hex digest is 64 characters; look for separator after that + int separatorPos = line.indexOf(separatorRx, 64); + if (separatorPos <= 0) { + continue; + } + + QString hash = line.left(separatorPos).trimmed(); + if (!hexRx.match(hash).hasMatch()) { + continue; + } + + QString filename = line.mid(separatorPos).trimmed().remove('*'); + + if (!downloadFilename.isEmpty() && filename.contains(downloadFilename, Qt::CaseInsensitive)) { + info.sha256 = hash; + qDebug() << "SHA-256:" << info.sha256; + break; + } + } + + if (info.sha256.isEmpty()) { + qDebug() << "No matching checksum found for" << downloadFilename + << "- proceeding without hash verification"; + } emit feedFetched(); } @@ -405,10 +521,8 @@ void MudletInstaller::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal /** - * @brief Verifies the sha256 hash and starts the install process if the hash matches what we got - * from the dblsqd feed. - * Called upon completion of the Mudlet installer download. - * Supports appending to existing file when resuming. + * @brief Called upon completion of the Mudlet installer download. + * Saves the file and supports appending to existing file when resuming. */ void MudletInstaller::onDownloadFinished() { if (currentReply->error() != QNetworkReply::NoError) { @@ -486,10 +600,17 @@ void MudletInstaller::onDownloadError(QNetworkReply::NetworkError error) { /** * @brief Verify the hash of outputFile with the provided sha256 - * Emits corfresponding hashValid or hashInvalid state signals - * + * Emits corresponding hashValid or hashInvalid state signals. + * If no hash is available, emits hashInvalid with an error message. */ void MudletInstaller::verifyHash() { + if (info.sha256.isEmpty()) { + qDebug() << "No SHA-256 hash available, cannot verify download integrity"; + statusLabel->setText("Could not verify download integrity: SHA-256 hash missing from release"); + emit hashInvalid(); + return; + } + statusLabel->setText("Verifying SHA256..."); statusLabel->repaint(); qDebug() << "Verifying hash"; diff --git a/MudletInstaller.h b/MudletInstaller.h index e5c190d..6486288 100644 --- a/MudletInstaller.h +++ b/MudletInstaller.h @@ -27,6 +27,7 @@ class MudletInstaller : public QObject { private slots: void fetchPlatformFeed(); void onFetchPlatformFeedFinished(); + void onChecksumsFetchFinished(); void checkExistingFile(); void startDownload(); void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal); @@ -67,6 +68,7 @@ private slots: qint64 bytesAlreadyDownloaded; static const int MAX_RETRIES = 3; QString gameName; + QString assetPattern; QStateMachine *m_stateMachine; QState *m_downloadFeedState; From 763a19d33cd0d8381ca323ab218a0f0f5f081e45 Mon Sep 17 00:00:00 2001 From: John McKisson Date: Thu, 9 Apr 2026 08:27:18 -0400 Subject: [PATCH 2/7] just fetch entire feed --- MudletInstaller.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/MudletInstaller.cpp b/MudletInstaller.cpp index 831a4b0..7e59257 100644 --- a/MudletInstaller.cpp +++ b/MudletInstaller.cpp @@ -262,8 +262,7 @@ void MudletInstaller::fetchPlatformFeed() { // Use per_page=10 for PTB (need to scan prereleases), per_page=100 for stable bool isPTB = (releaseType == "PTB"); - int perPage = isPTB ? 10 : 100; - QString feedUrl = QString("https://api.github.com/repos/Mudlet/Mudlet/releases?per_page=%1").arg(perPage); + QString feedUrl = QString("https://api.github.com/repos/Mudlet/Mudlet/releases").arg(perPage); QNetworkRequest request(QUrl(feedUrl)); request.setRawHeader("Accept", "application/vnd.github+json"); @@ -314,7 +313,7 @@ void MudletInstaller::onFetchPlatformFeedFinished() { for (const auto &val : releasesArray) { QJsonObject releaseObj = val.toObject(); - // Filter: PTB = prereleases only, stable = non-prereleases only + // Filter - PTB: prerelease=true, Release: prerelease=false if (isPTB != releaseObj.value("prerelease").toBool()) { continue; } @@ -325,7 +324,7 @@ void MudletInstaller::onFetchPlatformFeedFinished() { QJsonArray assets = releaseObj.value("assets").toArray(); - // Search assets for our platform binary and SHA256SUMS.txt + // Search assets for binary and SHA256SUMS.txt for (const auto &assetVal : assets) { QJsonObject asset = assetVal.toObject(); QString name = asset.value("name").toString(); From 0e5aa371ca23081f1b646bc6aa08fcb7bea2cd89 Mon Sep 17 00:00:00 2001 From: John McKisson Date: Thu, 9 Apr 2026 09:03:46 -0400 Subject: [PATCH 3/7] fix compile errors --- MudletInstaller.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MudletInstaller.cpp b/MudletInstaller.cpp index 7e59257..5bd333a 100644 --- a/MudletInstaller.cpp +++ b/MudletInstaller.cpp @@ -262,9 +262,9 @@ void MudletInstaller::fetchPlatformFeed() { // Use per_page=10 for PTB (need to scan prereleases), per_page=100 for stable bool isPTB = (releaseType == "PTB"); - QString feedUrl = QString("https://api.github.com/repos/Mudlet/Mudlet/releases").arg(perPage); + QString feedUrl = QString("https://api.github.com/repos/Mudlet/Mudlet/releases"); - QNetworkRequest request(QUrl(feedUrl)); + QNetworkRequest request{QUrl(feedUrl)}; request.setRawHeader("Accept", "application/vnd.github+json"); request.setRawHeader("User-Agent", "MudletInstaller"); request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); @@ -387,7 +387,7 @@ void MudletInstaller::onFetchPlatformFeedFinished() { // Fetch SHA256SUMS.txt if available if (!checksumsUrl.isEmpty()) { qDebug() << "Fetching checksums from:" << checksumsUrl; - QNetworkRequest request(QUrl(checksumsUrl)); + QNetworkRequest request{QUrl(checksumsUrl)}; request.setRawHeader("User-Agent", "MudletInstaller"); request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); currentReply = networkManager.get(request); From 82248bca3049e512e5acf5170d8fb929a6132307 Mon Sep 17 00:00:00 2001 From: John McKisson Date: Thu, 9 Apr 2026 10:05:17 -0400 Subject: [PATCH 4/7] feed fixes and debug prints --- MudletInstaller.cpp | 46 ++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/MudletInstaller.cpp b/MudletInstaller.cpp index 5bd333a..de31409 100644 --- a/MudletInstaller.cpp +++ b/MudletInstaller.cpp @@ -27,7 +27,7 @@ QString buildAssetPattern(const QString &os) { if (os == "linux/x86_64") { return "-linux-x64.AppImage.tar"; } else if (os == "win/x86_64" || os == "win/x86") { - return "-windows-64-installer.exe"; + return "-windows-64.exe"; } else if (os == "mac/arm") { return "-arm64.dmg"; } else if (os == "mac/x86_64") { @@ -247,10 +247,6 @@ void MudletInstaller::start() { * @brief Query GitHub Releases API for the latest Mudlet release */ void MudletInstaller::fetchPlatformFeed() { - - QSettings settings(":/resources/launch.ini", QSettings::IniFormat); - QString releaseType = settings.value("Settings/RELEASE_TYPE", "").toString(); - QString os = detectOS(); assetPattern = buildAssetPattern(os); @@ -260,8 +256,6 @@ void MudletInstaller::fetchPlatformFeed() { return; } - // Use per_page=10 for PTB (need to scan prereleases), per_page=100 for stable - bool isPTB = (releaseType == "PTB"); QString feedUrl = QString("https://api.github.com/repos/Mudlet/Mudlet/releases"); QNetworkRequest request{QUrl(feedUrl)}; @@ -305,49 +299,72 @@ void MudletInstaller::onFetchPlatformFeedFinished() { QSettings settings(":/resources/launch.ini", QSettings::IniFormat); QString releaseType = settings.value("Settings/RELEASE_TYPE", "").toString(); - bool isPTB = (releaseType == "PTB"); + bool wantPTB = (releaseType == "PTB"); QJsonArray releasesArray = doc.array(); QString checksumsUrl; + qDebug() << "Feed contains" << releasesArray.size() << "releases"; + qDebug() << "Looking for asset pattern:" << assetPattern << "(wantPTB:" << wantPTB << ")"; + + int releaseIndex = 0; for (const auto &val : releasesArray) { QJsonObject releaseObj = val.toObject(); + QString tagName = releaseObj.value("tag_name").toString(); + bool isPrerelease = releaseObj.value("prerelease").toBool(); + bool isDraft = releaseObj.value("draft").toBool(); + + qDebug() << "Release[" << releaseIndex << "]:" << tagName + << "prerelease:" << isPrerelease + << "draft:" << isDraft; + // Filter - PTB: prerelease=true, Release: prerelease=false - if (isPTB != releaseObj.value("prerelease").toBool()) { + if (wantPTB != isPrerelease) { + qDebug() << " Skipping: prerelease mismatch (want" << wantPTB << "got" << isPrerelease << ")"; + releaseIndex++; continue; } // Skip drafts - if (releaseObj.value("draft").toBool()) { + if (isDraft) { + qDebug() << " Skipping: draft release"; + releaseIndex++; continue; } QJsonArray assets = releaseObj.value("assets").toArray(); + qDebug() << " Scanning" << assets.size() << "assets"; // Search assets for binary and SHA256SUMS.txt for (const auto &assetVal : assets) { QJsonObject asset = assetVal.toObject(); QString name = asset.value("name").toString(); + qDebug() << " Asset:" << name; + if (name == "SHA256SUMS.txt") { checksumsUrl = asset.value("browser_download_url").toString(); + qDebug() << " -> Found checksums URL:" << checksumsUrl; continue; } if (info.url.isEmpty() && name.contains(assetPattern, Qt::CaseInsensitive)) { info.url = asset.value("browser_download_url").toString(); + qDebug() << " -> Matched asset pattern! URL:" << info.url; } } // If we found a matching asset in this release, use it if (!info.url.isEmpty()) { - QString tagName = releaseObj.value("tag_name").toString(); - qDebug() << "Found release:" << tagName; + qDebug() << "Found matching release:" << tagName; break; } + qDebug() << " No matching asset in this release, continuing..."; + // Reset for next release checksumsUrl.clear(); + releaseIndex++; } if (info.url.isEmpty()) { @@ -357,15 +374,15 @@ void MudletInstaller::onFetchPlatformFeedFinished() { } qDebug() << "URL:" << info.url; + QString osStr = detectOS(); // Extract the filename from the download URL QRegularExpression regex(R"(/([^/]+)\.(exe|dmg|AppImage\.tar)$)"); QRegularExpressionMatch match = regex.match(info.url); if (match.hasMatch()) { - QString os = detectOS(); info.appName = match.captured(1); - if (os.startsWith("mac") || os.startsWith("linux")) { + if (osStr.startsWith("mac") || osStr.startsWith("linux")) { info.appName += "." + match.captured(2); } } else { @@ -376,7 +393,6 @@ void MudletInstaller::onFetchPlatformFeedFinished() { outputFile = info.appName; - QString osStr = detectOS(); // Mac may have an issue downloading a file into the .app directory if (osStr.startsWith("mac")) { outputFile = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + outputFile; From a41e4c901987d3725ed957daf6721a3c1aeb3d4c Mon Sep 17 00:00:00 2001 From: John McKisson Date: Thu, 9 Apr 2026 10:09:21 -0400 Subject: [PATCH 5/7] skip autologin file if no profile specified in ini --- MudletInstaller.cpp | 68 ++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/MudletInstaller.cpp b/MudletInstaller.cpp index de31409..2d2da96 100644 --- a/MudletInstaller.cpp +++ b/MudletInstaller.cpp @@ -847,46 +847,46 @@ void MudletInstaller::installApplication() { // Read the profile from the .ini file QString launchProfile = readLaunchProfileFromResource(); if (launchProfile.isEmpty()) { - qDebug() << "No launch profile found. Using default."; + qDebug() << "No launch profile found, skipping autologin file creation..."; } else { // Pass along the launch profile to the environment env.insert("MUDLET_PROFILES", launchProfile); - } - - // Create autologin file for the wanted profile - QString confDirDefault = QDir::homePath() + - QDir::separator() + ".config" + - QDir::separator() + "mudlet" + - QDir::separator() + "profiles" + - QDir::separator() + launchProfile; - QDir configDir; - if (!configDir.mkpath(confDirDefault)) { - qDebug() << "Failed to create config directory:" << confDirDefault; - } else { - // Create the autologin file - QString autologinFilePath = confDirDefault + QDir::separator() + "autologin"; - QFile autologinFile(autologinFilePath); - // A constant equivalent to QDataStream::Qt_5_12 needed in several places - // which can't be pulled from Qt as it is not going to be defined for older - // versions: - static const int scmQDataStreamFormat_5_12 = 18; + // Create autologin file for the wanted profile + QString confDirDefault = QDir::homePath() + + QDir::separator() + ".config" + + QDir::separator() + "mudlet" + + QDir::separator() + "profiles" + + QDir::separator() + launchProfile; + QDir configDir; + if (!configDir.mkpath(confDirDefault)) { + qDebug() << "Failed to create config directory:" << confDirDefault; + } else { + // Create the autologin file + QString autologinFilePath = confDirDefault + QDir::separator() + "autologin"; + QFile autologinFile(autologinFilePath); + + // A constant equivalent to QDataStream::Qt_5_12 needed in several places + // which can't be pulled from Qt as it is not going to be defined for older + // versions: + static const int scmQDataStreamFormat_5_12 = 18; + + if (autologinFile.open(QIODevice::WriteOnly)) { + QDataStream out(&autologinFile); + + // Set the same data stream version that Mudlet uses for reading + if (QVersionNumber::fromString(qVersion()) >= QVersionNumber(5, 13, 0)) { + out.setVersion(scmQDataStreamFormat_5_12); + } - if (autologinFile.open(QIODevice::WriteOnly)) { - QDataStream out(&autologinFile); - - // Set the same data stream version that Mudlet uses for reading - if (QVersionNumber::fromString(qVersion()) >= QVersionNumber(5, 13, 0)) { - out.setVersion(scmQDataStreamFormat_5_12); + QString autologinData = QString::number(Qt::Checked); + out << autologinData; + + autologinFile.close(); + qDebug() << "Autologin file created successfully:" << autologinFilePath; + } else { + qWarning() << "Failed to create autologin file:" << autologinFilePath << autologinFile.errorString(); } - - QString autologinData = QString::number(Qt::Checked); - out << autologinData; - - autologinFile.close(); - qDebug() << "Autologin file created successfully:" << autologinFilePath; - } else { - qWarning() << "Failed to create autologin file:" << autologinFilePath << autologinFile.errorString(); } } From f010cfcd024e00ac94cf0d367046eb649601329a Mon Sep 17 00:00:00 2001 From: John McKisson Date: Thu, 9 Apr 2026 10:52:04 -0400 Subject: [PATCH 6/7] install console handler for windows --- main.cpp | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/main.cpp b/main.cpp index ead090b..73566fe 100644 --- a/main.cpp +++ b/main.cpp @@ -6,9 +6,43 @@ #include "MudletInstaller.h" +#ifdef Q_OS_WINDOWS +#include "windows.h" +#include + +void msys2QtMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg) +{ + Q_UNUSED(context) + switch (type) { + case QtDebugMsg: + case QtInfoMsg: + std::cout << msg.toUtf8().constData() << std::endl; + break; + case QtWarningMsg: + case QtCriticalMsg: + case QtFatalMsg: + std::cerr << msg.toUtf8().constData() << std::endl; + } +} +#endif + int main(int argc, char *argv[]) { QApplication a(argc, argv); +#ifdef Q_OS_WINDOWS + if (AttachConsole(ATTACH_PARENT_PROCESS)) { + if (qgetenv("MSYSTEM").isNull()) { + // print stdout to console if Mudlet is started in a console in Windows + // credit to https://stackoverflow.com/a/41701133 for the workaround + freopen("CONOUT$", "w", stdout); + freopen("CONOUT$", "w", stderr); + } else { + // simply print qt logs into stdout and stderr if it's MSYS2 + qInstallMessageHandler(msys2QtMessageHandler); + } + } +#endif + qDebug() << "Starting MudletDownloader..."; MudletInstaller app; From 5d9571a41ee165817fc41f576cb375b4185c9465 Mon Sep 17 00:00:00 2001 From: John McKisson Date: Tue, 14 Apr 2026 08:12:43 -0400 Subject: [PATCH 7/7] error dialog with details --- MudletInstaller.cpp | 61 ++++++++++++++++++++++++++++++++++++++++++--- MudletInstaller.h | 3 +++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/MudletInstaller.cpp b/MudletInstaller.cpp index 2d2da96..4f25dfc 100644 --- a/MudletInstaller.cpp +++ b/MudletInstaller.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -250,8 +251,13 @@ void MudletInstaller::fetchPlatformFeed() { QString os = detectOS(); assetPattern = buildAssetPattern(os); + m_diagnosticLog.clear(); + m_diagnosticLog << QString("Platform: %1").arg(os); + m_diagnosticLog << QString("Asset pattern: %1").arg(assetPattern.isEmpty() ? "(none — unsupported platform)" : assetPattern); + if (assetPattern.isEmpty()) { qDebug() << "No asset pattern found for platform:" << os; + m_diagnosticLog << "Error: Unsupported platform, cannot determine asset filename."; emit errorOccurred(); return; } @@ -300,6 +306,7 @@ void MudletInstaller::onFetchPlatformFeedFinished() { QSettings settings(":/resources/launch.ini", QSettings::IniFormat); QString releaseType = settings.value("Settings/RELEASE_TYPE", "").toString(); bool wantPTB = (releaseType == "PTB"); + m_diagnosticLog << QString("Release type: %1").arg(wantPTB ? "PTB (pre-release)" : "Stable"); QJsonArray releasesArray = doc.array(); QString checksumsUrl; @@ -357,6 +364,13 @@ void MudletInstaller::onFetchPlatformFeedFinished() { // If we found a matching asset in this release, use it if (!info.url.isEmpty()) { qDebug() << "Found matching release:" << tagName; + m_diagnosticLog << QString("Matched release: %1").arg(tagName); + m_diagnosticLog << QString("Download URL: %1").arg(info.url); + if (!checksumsUrl.isEmpty()) { + m_diagnosticLog << QString("Checksums URL: %1").arg(checksumsUrl); + } else { + m_diagnosticLog << "Checksums URL: (not found in release assets)"; + } break; } @@ -369,6 +383,8 @@ void MudletInstaller::onFetchPlatformFeedFinished() { if (info.url.isEmpty()) { qDebug() << "No matching asset found for pattern:" << assetPattern; + m_diagnosticLog << QString("Error: No release asset found matching pattern \"%1\".").arg(assetPattern); + statusLabel->setText(QString("No download found for this platform (%1)").arg(assetPattern)); emit errorOccurred(); return; } @@ -433,12 +449,14 @@ void MudletInstaller::onChecksumsFetchFinished() { // Extract the filename portion from the download URL QString downloadFilename = QUrl(info.url).fileName(); + m_diagnosticLog << QString("Searching SHA256SUMS.txt for: %1").arg(downloadFilename); // Parse SHA256SUMS.txt lines in format: "hash filename" or "hash *filename" QStringList lines = checksumData.split('\n', Qt::SkipEmptyParts); QRegularExpression separatorRx(R"([\s*]+)"); QRegularExpression hexRx(R"(^[0-9a-fA-F]{64}$)"); + QStringList checksumFilenames; for (const auto &line : lines) { // SHA256 hex digest is 64 characters; look for separator after that int separatorPos = line.indexOf(separatorRx, 64); @@ -452,6 +470,7 @@ void MudletInstaller::onChecksumsFetchFinished() { } QString filename = line.mid(separatorPos).trimmed().remove('*'); + checksumFilenames << filename; if (!downloadFilename.isEmpty() && filename.contains(downloadFilename, Qt::CaseInsensitive)) { info.sha256 = hash; @@ -460,9 +479,20 @@ void MudletInstaller::onChecksumsFetchFinished() { } } + if (!checksumFilenames.isEmpty()) { + m_diagnosticLog << QString("SHA256SUMS.txt entries (%1):").arg(checksumFilenames.size()); + for (const auto &name : checksumFilenames) { + m_diagnosticLog << QString(" %1").arg(name); + } + } else { + m_diagnosticLog << "SHA256SUMS.txt: (empty or unparseable)"; + } + if (info.sha256.isEmpty()) { - qDebug() << "No matching checksum found for" << downloadFilename - << "- proceeding without hash verification"; + qDebug() << "No matching checksum found for" << downloadFilename << "in checksums file"; + m_diagnosticLog << QString("Error: No checksum entry found for \"%1\" in SHA256SUMS.txt.").arg(downloadFilename); + } else { + m_diagnosticLog << QString("Checksum found: %1").arg(info.sha256); } emit feedFetched(); @@ -605,8 +635,11 @@ void MudletInstaller::onDownloadError(QNetworkReply::NetworkError error) { if (!isRetryable) { statusLabel->setText(QString("Download failed: %1").arg(errorString)); + m_diagnosticLog << QString("Error: Non-retryable download error: %1").arg(errorString); // Skip retry logic and go directly to error state for non-retryable errors retryCount = MAX_RETRIES; // This will force retryDownload to give up immediately + } else { + m_diagnosticLog << QString("Download error (will retry): %1").arg(errorString); } emit errorOccurred(); @@ -633,6 +666,8 @@ void MudletInstaller::verifyHash() { if (!verifyFileSha256(outputFile, info.sha256)) { qDebug() << "Checksum verification failed."; statusLabel->setText("SHA256 Verification Failed"); + m_diagnosticLog << QString("Error: SHA256 mismatch for file \"%1\".").arg(outputFile); + m_diagnosticLog << QString(" Expected: %1").arg(info.sha256); emit hashInvalid(); } else { qDebug() << "Checksum verification succeeded."; @@ -934,8 +969,25 @@ void MudletInstaller::installApplication() { */ void MudletInstaller::handleError() { qDebug() << "Handling error state"; - statusLabel->setText("An error occurred"); - // Show error dialog? + + QString errorMessage = statusLabel->text(); + if (errorMessage.isEmpty() || errorMessage == "Preparing to download...") { + errorMessage = "An unexpected error occurred during installation."; + } + + QMessageBox msgBox(progressWindow); + msgBox.setWindowTitle("Installation Error"); + msgBox.setText(errorMessage); + msgBox.setIcon(QMessageBox::Critical); + + if (!m_diagnosticLog.isEmpty()) { + QString details = "Diagnostic information (include when reporting bugs):\n\n"; + details += m_diagnosticLog.join('\n'); + msgBox.setDetailedText(details); + } + + msgBox.exec(); + emit finished(); } @@ -980,6 +1032,7 @@ void MudletInstaller::retryDownload() { } else { qDebug() << "Max retries reached, giving up"; statusLabel->setText("Download failed after maximum retries"); + m_diagnosticLog << QString("Error: Download failed after %1 retries.").arg(MAX_RETRIES); emit errorOccurred(); // This will trigger transition to error state } } diff --git a/MudletInstaller.h b/MudletInstaller.h index 6486288..a012dc1 100644 --- a/MudletInstaller.h +++ b/MudletInstaller.h @@ -10,6 +10,7 @@ #include #include #include +#include struct DownloadInfo { QString url; @@ -70,6 +71,8 @@ private slots: QString gameName; QString assetPattern; + QStringList m_diagnosticLog; + QStateMachine *m_stateMachine; QState *m_downloadFeedState; QState *m_checkExistingState;