From a774b3d24816aa0010e1d5f67b20acb99e7181ac Mon Sep 17 00:00:00 2001 From: Sky Date: Sun, 12 Jan 2014 18:28:42 +0000 Subject: [PATCH] Show Mojang service statuses in status bar --- CMakeLists.txt | 4 + MultiMC.cpp | 6 ++ MultiMC.h | 7 ++ gui/MainWindow.cpp | 79 +++++++++++++++++++ gui/MainWindow.h | 11 +++ logic/net/URLConstants.h | 2 + logic/status/StatusChecker.cpp | 138 +++++++++++++++++++++++++++++++++ logic/status/StatusChecker.h | 67 ++++++++++++++++ 8 files changed, 314 insertions(+) create mode 100644 logic/status/StatusChecker.cpp create mode 100644 logic/status/StatusChecker.h diff --git a/CMakeLists.txt b/CMakeLists.txt index dcb4cbaa..7aba8832 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -393,6 +393,10 @@ logic/news/NewsChecker.cpp logic/news/NewsEntry.h logic/news/NewsEntry.cpp +# Status system +logic/status/StatusChecker.h +logic/status/StatusChecker.cpp + # legacy instances logic/LegacyInstance.h logic/LegacyInstance.cpp diff --git a/MultiMC.cpp b/MultiMC.cpp index 17fc2e0a..819091cd 100644 --- a/MultiMC.cpp +++ b/MultiMC.cpp @@ -20,8 +20,11 @@ #include "logic/news/NewsChecker.h" +#include "logic/status/StatusChecker.h" + #include "logic/InstanceLauncher.h" #include "logic/net/HttpMetaCache.h" +#include "logic/net/URLConstants.h" #include "logic/JavaUtils.h" @@ -181,6 +184,9 @@ MultiMC::MultiMC(int &argc, char **argv, bool root_override) // initialize the news checker m_newsChecker.reset(new NewsChecker(NEWS_RSS_URL)); + // initialize the status checker + m_statusChecker.reset(new StatusChecker()); + // and instances auto InstDirSetting = m_settings->getSetting("InstanceDir"); m_instances.reset(new InstanceList(InstDirSetting->get().toString(), this)); diff --git a/MultiMC.h b/MultiMC.h index 18c7aab7..638a442f 100644 --- a/MultiMC.h +++ b/MultiMC.h @@ -21,6 +21,7 @@ class JavaVersionList; class UpdateChecker; class NotificationChecker; class NewsChecker; +class StatusChecker; #if defined(MMC) #undef MMC @@ -113,6 +114,11 @@ public: return m_newsChecker; } + std::shared_ptr statusChecker() + { + return m_statusChecker; + } + std::shared_ptr lwjgllist(); std::shared_ptr forgelist(); @@ -183,6 +189,7 @@ private: std::shared_ptr m_updateChecker; std::shared_ptr m_notificationChecker; std::shared_ptr m_newsChecker; + std::shared_ptr m_statusChecker; std::shared_ptr m_accounts; std::shared_ptr m_icons; std::shared_ptr m_qnam; diff --git a/gui/MainWindow.cpp b/gui/MainWindow.cpp index dba15e98..cc6d4abb 100644 --- a/gui/MainWindow.cpp +++ b/gui/MainWindow.cpp @@ -77,6 +77,8 @@ #include "logic/news/NewsChecker.h" +#include "logic/status/StatusChecker.h" + #include "logic/net/URLConstants.h" #include "logic/BaseInstance.h" @@ -199,7 +201,27 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi connect(MMC->instances().get(), SIGNAL(dataIsInvalid()), SLOT(selectionBad())); m_statusLeft = new QLabel(tr("No instance selected"), this); + m_statusRight = new QLabel(tr("No status available"), this); + m_statusRefresh = new QToolButton(this); + m_statusRefresh->setToolButtonStyle(Qt::ToolButtonIconOnly); + m_statusRefresh->setIcon( + QPixmap(":/icons/toolbar/refresh").scaled(16, 16, Qt::KeepAspectRatio)); + statusBar()->addPermanentWidget(m_statusLeft, 1); + statusBar()->addPermanentWidget(m_statusRight, 0); + statusBar()->addPermanentWidget(m_statusRefresh, 0); + + // Start status checker + { + connect(MMC->statusChecker().get(), &StatusChecker::statusLoaded, this, &MainWindow::updateStatusUI); + connect(MMC->statusChecker().get(), &StatusChecker::statusLoadingFailed, this, &MainWindow::updateStatusFailedUI); + + connect(m_statusRefresh, &QAbstractButton::clicked, this, &MainWindow::reloadStatus); + connect(&statusTimer, &QTimer::timeout, this, &MainWindow::reloadStatus); + statusTimer.setSingleShot(true); + + reloadStatus(); + } // Add "manage accounts" button, right align QWidget *spacer = new QWidget(); @@ -498,6 +520,63 @@ void MainWindow::updateNewsLabel() } } +static QString convertStatus(const QString &status) +{ + if(status == "green") return "↑"; + else if(status == "yellow") return "-"; + else if(status == "red") return "↓"; + else return "?"; +} + +void MainWindow::reloadStatus() +{ + MMC->statusChecker()->reloadStatus(); + updateStatusUI(); +} + +static QString makeStatusString(const QMap statuses) +{ + QString status = ""; + status += "Web: " + convertStatus(statuses["minecraft.net"]); + status += " Account: " + convertStatus(statuses["account.mojang.com"]); + status += " Skins: " + convertStatus(statuses["skins.minecraft.net"]); + status += " Auth: " + convertStatus(statuses["authserver.mojang.com"]); + status += " Session: " + convertStatus(statuses["sessionserver.mojang.com"]); + + return status; +} + +void MainWindow::updateStatusUI() +{ + auto statusChecker = MMC->statusChecker(); + auto statuses = statusChecker->getStatusEntries(); + + QString status = makeStatusString(statuses); + if(statusChecker->isLoadingStatus()) + { + m_statusRefresh->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + m_statusRefresh->setText(tr("Loading...")); + } + else + { + m_statusRefresh->setToolButtonStyle(Qt::ToolButtonIconOnly); + m_statusRefresh->setText(tr("")); + } + + m_statusRight->setText(status); + + statusTimer.start(60 * 1000); +} + +void MainWindow::updateStatusFailedUI() +{ + m_statusRight->setText(makeStatusString(QMap())); + m_statusRefresh->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + m_statusRefresh->setText(tr("Failed.")); + + statusTimer.start(60 * 1000); +} + void MainWindow::updateAvailable(QString repo, QString versionName, int versionId) { UpdateDialog dlg; diff --git a/gui/MainWindow.h b/gui/MainWindow.h index 12d76da4..eb478776 100644 --- a/gui/MainWindow.h +++ b/gui/MainWindow.h @@ -17,6 +17,7 @@ #include #include +#include #include "logic/lists/InstanceList.h" #include "logic/BaseInstance.h" @@ -168,6 +169,12 @@ slots: void repopulateAccountsMenu(); void updateNewsLabel(); + + void updateStatusUI(); + + void updateStatusFailedUI(); + + void reloadStatus(); /*! * Runs the DownloadUpdateTask and installs updates. @@ -198,8 +205,12 @@ private: Task *m_versionLoadTask; QLabel *m_statusLeft; + QLabel *m_statusRight; + QToolButton *m_statusRefresh; QMenu *accountMenu; QToolButton *accountMenuButton; QAction *manageAccountsAction; + + QTimer statusTimer; }; diff --git a/logic/net/URLConstants.h b/logic/net/URLConstants.h index 9579198d..8cb1f3fd 100644 --- a/logic/net/URLConstants.h +++ b/logic/net/URLConstants.h @@ -31,4 +31,6 @@ const QString SKINS_BASE("skins.minecraft.net/MinecraftSkins/"); const QString AUTH_BASE("authserver.mojang.com/"); const QString FORGE_LEGACY_URL("http://files.minecraftforge.net/minecraftforge/json"); const QString FORGE_GRADLE_URL("http://files.minecraftforge.net/maven/net/minecraftforge/forge/json"); +const QString MOJANG_STATUS_URL("http://status.mojang.com/check"); +const QString MOJANG_STATUS_NEWS_URL("http://status.mojang.com/news"); } diff --git a/logic/status/StatusChecker.cpp b/logic/status/StatusChecker.cpp new file mode 100644 index 00000000..54cf077a --- /dev/null +++ b/logic/status/StatusChecker.cpp @@ -0,0 +1,138 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "StatusChecker.h" + +#include + +#include +#include + +#include + +StatusChecker::StatusChecker() +{ + +} + +void StatusChecker::reloadStatus() +{ + if (isLoadingStatus()) + { + QLOG_INFO() << "Ignored request to reload status. Currently reloading already."; + return; + } + + QLOG_INFO() << "Reloading status."; + + NetJob* job = new NetJob("Status JSON"); + job->addNetAction(ByteArrayDownload::make(URLConstants::MOJANG_STATUS_URL)); + QObject::connect(job, &NetJob::succeeded, this, &StatusChecker::statusDownloadFinished); + QObject::connect(job, &NetJob::failed, this, &StatusChecker::statusDownloadFailed); + m_statusNetJob.reset(job); + job->start(); +} + +void StatusChecker::statusDownloadFinished() +{ + // Parse the XML file and process the RSS feed entries. + QLOG_DEBUG() << "Finished loading status JSON."; + + QByteArray data; + { + ByteArrayDownloadPtr dl = std::dynamic_pointer_cast(m_statusNetJob->first()); + data = dl->m_data; + m_statusNetJob.reset(); + } + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + + if (jsonError.error != QJsonParseError::NoError) + { + fail("Error parsing status JSON:" + jsonError.errorString()); + return; + } + + if (!jsonDoc.isArray()) + { + fail("Error parsing status JSON: JSON root is not an array"); + return; + } + + QJsonArray root = jsonDoc.array(); + + for(auto status = root.begin(); status != root.end(); ++status) + { + QVariantMap map = (*status).toObject().toVariantMap(); + + for (QVariantMap::const_iterator iter = map.begin(); iter != map.end(); ++iter) + { + QString key = iter.key(); + QVariant value = iter.value(); + + if(value.type() == QVariant::Type::String) + { + m_statusEntries.insert(key, value.toString()); + QLOG_DEBUG() << "Status JSON object: " << key << m_statusEntries[key]; + } + else + { + fail("Malformed status JSON: expected status type to be a string."); + return; + } + } + } + + succeed(); +} + +void StatusChecker::statusDownloadFailed() +{ + fail("Failed to load status JSON."); +} + + +QMap StatusChecker::getStatusEntries() const +{ + return m_statusEntries; +} + +bool StatusChecker::isLoadingStatus() const +{ + return m_statusNetJob.get() != nullptr; +} + +QString StatusChecker::getLastLoadErrorMsg() const +{ + return m_lastLoadError; +} + +void StatusChecker::succeed() +{ + m_lastLoadError = ""; + QLOG_DEBUG() << "Status loading succeeded."; + m_statusNetJob.reset(); + emit statusLoaded(); +} + +void StatusChecker::fail(const QString& errorMsg) +{ + m_lastLoadError = errorMsg; + QLOG_DEBUG() << "Failed to load status:" << errorMsg; + m_statusNetJob.reset(); + emit statusLoadingFailed(errorMsg); +} + diff --git a/logic/status/StatusChecker.h b/logic/status/StatusChecker.h new file mode 100644 index 00000000..b81050ab --- /dev/null +++ b/logic/status/StatusChecker.h @@ -0,0 +1,67 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include + +class StatusChecker : public QObject +{ + Q_OBJECT +public: + StatusChecker(); + + QString getLastLoadErrorMsg() const; + + bool isStatusLoaded() const; + + bool isLoadingStatus() const; + + QMap getStatusEntries() const; + + void Q_SLOT reloadStatus(); + +signals: + void statusLoaded(); + void statusLoadingFailed(QString errorMsg); + +protected slots: + void statusDownloadFinished(); + void statusDownloadFailed(); + +protected: + QMap m_statusEntries; + NetJobPtr m_statusNetJob; + + //! True if news has been loaded. + bool m_loadedStatus; + + QString m_lastLoadError; + + /*! + * Emits newsLoaded() and sets m_lastLoadError to empty string. + */ + void Q_SLOT succeed(); + + /*! + * Emits newsLoadingFailed() and sets m_lastLoadError to the given message. + */ + void Q_SLOT fail(const QString& errorMsg); +}; +