diff options
| author | Petr Mrázek <peterix@gmail.com> | 2021-12-04 01:18:05 +0100 |
|---|---|---|
| committer | Petr Mrázek <peterix@gmail.com> | 2021-12-04 01:18:05 +0100 |
| commit | 3c46d8a412956a759f61ae802c540ef72d00b35d (patch) | |
| tree | f16564ba6be96b68ba5257a982c144320fff7911 /launcher/minecraft | |
| parent | ffcef673de9fe848a92d23e02a2abed8df16eb9f (diff) | |
| download | PrismLauncher-3c46d8a412956a759f61ae802c540ef72d00b35d.tar.gz PrismLauncher-3c46d8a412956a759f61ae802c540ef72d00b35d.tar.bz2 PrismLauncher-3c46d8a412956a759f61ae802c540ef72d00b35d.zip | |
GH-4071 Heavily refactor and rearchitect account system
This makes the account system much more modular
and makes it treat errors as something recoverable,
unless they come directly from the MSA refresh token
becoming invalid.
Diffstat (limited to 'launcher/minecraft')
57 files changed, 1681 insertions, 1248 deletions
diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 09cd2c73..fa42747e 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -41,6 +41,16 @@ enum class AccountType { Mojang }; +enum class AccountState { + Unchecked, + Offline, + Working, + Online, + Errored, + Expired, + Gone +}; + struct AccountData { QJsonObject saveState() const; bool resumeStateFromV2(QJsonObject data); @@ -77,4 +87,9 @@ struct AccountData { MinecraftProfile minecraftProfile; MinecraftEntitlement minecraftEntitlement; Katabasis::Validity validity_ = Katabasis::Validity::None; + + // runtime only information (not saved with the account) + QString internalId; + QString errorString; + AccountState accountState = AccountState::Unchecked; }; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index d7537345..c44e3e89 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -15,6 +15,7 @@ #include "AccountList.h" #include "AccountData.h" +#include "AccountTask.h" #include <QIODevice> #include <QFile> @@ -24,6 +25,7 @@ #include <QJsonObject> #include <QJsonParseError> #include <QDir> +#include <QTimer> #include <QDebug> @@ -35,7 +37,14 @@ enum AccountListVersion { MojangMSA = 3 }; -AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) { } +AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) { + m_refreshTimer = new QTimer(this); + m_refreshTimer->setSingleShot(true); + connect(m_refreshTimer, &QTimer::timeout, this, &AccountList::fillQueue); + m_nextTimer = new QTimer(this); + m_nextTimer->setSingleShot(true); + connect(m_nextTimer, &QTimer::timeout, this, &AccountList::tryNext); +} AccountList::~AccountList() noexcept {} @@ -244,13 +253,29 @@ QVariant AccountList::data(const QModelIndex &index, int role) const } case StatusColumn: { - if(account->isActive()) { - return tr("Working", "Account status"); - } - if(account->isExpired()) { - return tr("Expired", "Account status"); + switch(account->accountState()) { + case AccountState::Unchecked: { + return tr("Unchecked", "Account status"); + } + case AccountState::Offline: { + return tr("Offline", "Account status"); + } + case AccountState::Online: { + return tr("Online", "Account status"); + } + case AccountState::Working: { + return tr("Working", "Account status"); + } + case AccountState::Errored: { + return tr("Errored", "Account status"); + } + case AccountState::Expired: { + return tr("Expired", "Account status"); + } + case AccountState::Gone: { + return tr("Gone", "Account status"); + } } - return tr("Ready", "Account status"); } case ProfileNameColumn: { @@ -583,10 +608,105 @@ void AccountList::setListFilePath(QString path, bool autosave) bool AccountList::anyAccountIsValid() { - for(auto account:m_accounts) + for(auto account: m_accounts) { - if(account->accountStatus() != NotVerified) + if(account->ownsMinecraft()) { return true; + } } return false; } + +void AccountList::fillQueue() { + + if(m_defaultAccount && m_defaultAccount->shouldRefresh()) { + auto idToRefresh = m_defaultAccount->internalId(); + m_refreshQueue.push_back(idToRefresh); + qDebug() << "AccountList: Queued default account with internal ID " << idToRefresh << " to refresh first"; + } + + for(int i = 0; i < count(); i++) { + auto account = at(i); + if(account == m_defaultAccount) { + continue; + } + + if(account->shouldRefresh()) { + auto idToRefresh = account->internalId(); + m_refreshQueue.push_back(idToRefresh); + qDebug() << "AccountList: Queued account with internal ID " << idToRefresh << " to refresh"; + } + } + m_refreshQueue.removeDuplicates(); + tryNext(); +} + +void AccountList::requestRefresh(QString accountId) { + m_refreshQueue.push_back(accountId); + if(!isActive()) { + tryNext(); + } +} + +void AccountList::tryNext() { + beginActivity(); + while (m_refreshQueue.length()) { + auto accountId = m_refreshQueue.front(); + m_refreshQueue.pop_front(); + for(int i = 0; i < count(); i++) { + auto account = at(i); + if(account->internalId() == accountId) { + m_currentTask = account->refresh(); + if(m_currentTask) { + connect(m_currentTask.get(), &AccountTask::succeeded, this, &AccountList::authSucceeded); + connect(m_currentTask.get(), &AccountTask::failed, this, &AccountList::authFailed); + m_currentTask->start(); + qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() << " with internal ID " << accountId; + return; + } + } + } + qDebug() << "RefreshSchedule: Account with with internal ID " << accountId << " not found."; + } + endActivity(); + // if we get here, no account needed refreshing. Schedule refresh in an hour. + m_refreshTimer->start(std::chrono::hours(1)); +} + +void AccountList::authSucceeded() { + qDebug() << "RefreshSchedule: Background account refresh succeeded"; + m_currentTask.reset(); + endActivity(); + m_nextTimer->start(std::chrono::seconds(20)); +} + +void AccountList::authFailed(QString reason) { + qDebug() << "RefreshSchedule: Background account refresh failed: " << reason; + m_currentTask.reset(); + endActivity(); + m_nextTimer->start(std::chrono::seconds(20)); +} + +bool AccountList::isActive() const { + return m_activityCount != 0; +} + +void AccountList::beginActivity() { + bool activating = m_activityCount == 0; + m_activityCount++; + if(activating) { + emit activityChanged(true); + } +} + +void AccountList::endActivity() { + if(m_activityCount == 0) { + qWarning() << m_name << " - Activity count would become below zero"; + return; + } + bool deactivating = m_activityCount == 1; + m_activityCount--; + if(deactivating) { + emit activityChanged(false); + } +} diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index 08004628..75686c21 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -67,6 +67,8 @@ public: MinecraftAccountPtr getAccountByProfileName(const QString &profileName) const; QStringList profileNames() const; + void requestRefresh(QString accountId); + /*! * Sets the path to load/save the list file from/to. * If autosave is true, this list will automatically save to the given path whenever it changes. @@ -85,10 +87,20 @@ public: void setDefaultAccount(MinecraftAccountPtr profileId); bool anyAccountIsValid(); + bool isActive() const; + +protected: + void beginActivity(); + void endActivity(); + +private: + const char* m_name; + uint32_t m_activityCount = 0; signals: void listChanged(); void listActivityChanged(); void defaultAccountChanged(); + void activityChanged(bool active); public slots: /** @@ -101,7 +113,23 @@ public slots: */ void accountActivityChanged(bool active); + /** + * This is initially to run background account refresh tasks, or on a hourly timer + */ + void fillQueue(); + +private slots: + void tryNext(); + + void authSucceeded(); + void authFailed(QString reason); + protected: + QList<QString> m_refreshQueue; + QTimer *m_refreshTimer; + QTimer *m_nextTimer; + shared_qobject_ptr<AccountTask> m_currentTask; + /*! * Called whenever the list changes. * This emits the listChanged() signal and autosaves the list (if autosave is enabled). diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp index 25d753de..98d8d94d 100644 --- a/launcher/minecraft/auth/AccountTask.cpp +++ b/launcher/minecraft/auth/AccountTask.cpp @@ -28,40 +28,79 @@ AccountTask::AccountTask(AccountData *data, QObject *parent) : Task(parent), m_data(data) { - changeState(STATE_CREATED); + changeState(AccountTaskState::STATE_CREATED); } QString AccountTask::getStateMessage() const { - switch (m_accountState) + switch (m_taskState) { - case STATE_CREATED: + case AccountTaskState::STATE_CREATED: return "Waiting..."; - case STATE_WORKING: + case AccountTaskState::STATE_WORKING: return tr("Sending request to auth servers..."); - case STATE_SUCCEEDED: + case AccountTaskState::STATE_SUCCEEDED: return tr("Authentication task succeeded."); - case STATE_FAILED_SOFT: + case AccountTaskState::STATE_OFFLINE: return tr("Failed to contact the authentication server."); - case STATE_FAILED_HARD: - return tr("Failed to authenticate."); - case STATE_FAILED_GONE: + case AccountTaskState::STATE_FAILED_SOFT: + return tr("Encountered an error during authentication."); + case AccountTaskState::STATE_FAILED_HARD: + return tr("Failed to authenticate. The session has expired."); + case AccountTaskState::STATE_FAILED_GONE: return tr("Failed to authenticate. The account no longer exists."); default: return tr("..."); } } -void AccountTask::changeState(AccountTask::State newState, QString reason) +bool AccountTask::changeState(AccountTaskState newState, QString reason) { - m_accountState = newState; + m_taskState = newState; setStatus(getStateMessage()); - if (newState == STATE_SUCCEEDED) - { - emitSucceeded(); - } - else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT || newState == STATE_FAILED_GONE) - { - emitFailed(reason); + switch(newState) { + case AccountTaskState::STATE_CREATED: { + m_data->errorString.clear(); + return true; + } + case AccountTaskState::STATE_WORKING: { + m_data->accountState = AccountState::Working; + return true; + } + case AccountTaskState::STATE_SUCCEEDED: { + m_data->accountState = AccountState::Online; + emitSucceeded(); + return false; + } + case AccountTaskState::STATE_OFFLINE: { + m_data->errorString = reason; + m_data->accountState = AccountState::Offline; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_SOFT: { + m_data->errorString = reason; + m_data->accountState = AccountState::Errored; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_HARD: { + m_data->errorString = reason; + m_data->accountState = AccountState::Expired; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_GONE: { + m_data->errorString = reason; + m_data->accountState = AccountState::Gone; + emitFailed(reason); + return false; + } + default: { + QString error = tr("Unknown account task state: %1").arg(int(newState)); + m_data->accountState = AccountState::Errored; + emitFailed(error); + return false; + } } } diff --git a/launcher/minecraft/auth/AccountTask.h b/launcher/minecraft/auth/AccountTask.h index 4f3bd52a..dac3f1b5 100644 --- a/launcher/minecraft/auth/AccountTask.h +++ b/launcher/minecraft/auth/AccountTask.h @@ -26,62 +26,32 @@ class QNetworkReply; +/** + * Enum for describing the state of the current task. + * Used by the getStateMessage function to determine what the status message should be. + */ +enum class AccountTaskState +{ + STATE_CREATED, + STATE_WORKING, + STATE_SUCCEEDED, + STATE_FAILED_SOFT, //!< soft failure. authentication went through partially + STATE_FAILED_HARD, //!< hard failure. main tokens are invalid + STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the account no longer exists + STATE_OFFLINE //!< soft failure. authentication failed in the first step in a 'soft' way +}; + class AccountTask : public Task { - friend class AuthContext; Q_OBJECT public: explicit AccountTask(AccountData * data, QObject *parent = 0); virtual ~AccountTask() {}; - /** - * assign a session to this task. the session will be filled with required infomration - * upon completion - */ - void assignSession(AuthSessionPtr session) - { - m_session = session; - } - - /// get the assigned session for filling with information. - AuthSessionPtr getAssignedSession() - { - return m_session; - } - - /** - * Class describing a Account error response. - */ - struct Error - { - QString m_errorMessageShort; - QString m_errorMessageVerbose; - QString m_cause; - }; - - enum AbortedBy - { - BY_NOTHING, - BY_USER, - BY_TIMEOUT - } m_aborted = BY_NOTHING; - - /** - * Enum for describing the state of the current task. - * Used by the getStateMessage function to determine what the status message should be. - */ - enum State - { - STATE_CREATED, - STATE_WORKING, - STATE_FAILED_SOFT, //!< soft failure. this generally means the user auth details haven't been invalidated - STATE_FAILED_HARD, //!< hard failure. auth is invalid - STATE_FAILED_GONE, //!< hard failure. auth is invalid, and the account no longer exists - STATE_SUCCEEDED - } m_accountState = STATE_CREATED; + AccountTaskState m_taskState = AccountTaskState::STATE_CREATED; - State accountState() { - return m_accountState; + AccountTaskState taskState() { + return m_taskState; } signals: @@ -98,11 +68,9 @@ protected: virtual QString getStateMessage() const; protected slots: - void changeState(State newState, QString reason=QString()); + // NOTE: true -> non-terminal state, false -> terminal state + bool changeState(AccountTaskState newState, QString reason = QString()); protected: - // FIXME: segfault disaster waiting to happen AccountData *m_data = nullptr; - std::shared_ptr<Error> m_error; - AuthSessionPtr m_session; }; diff --git a/launcher/minecraft/auth/flows/AuthRequest.cpp b/launcher/minecraft/auth/AuthRequest.cpp index 82dba591..459d2354 100644 --- a/launcher/minecraft/auth/flows/AuthRequest.cpp +++ b/launcher/minecraft/auth/AuthRequest.cpp @@ -44,6 +44,7 @@ void AuthRequest::onRequestFinished() { if (reply_ != qobject_cast<QNetworkReply *>(sender())) { return; } + httpStatus_ = 200; finish(); } @@ -55,10 +56,11 @@ void AuthRequest::onRequestError(QNetworkReply::NetworkError error) { if (reply_ != qobject_cast<QNetworkReply *>(sender())) { return; } - qWarning() << "AuthRequest::onRequestError: Error string: " << reply_->errorString(); - int httpStatus = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + errorString_ = reply_->errorString(); + httpStatus_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); error_ = error; + qWarning() << "AuthRequest::onRequestError: Error string: " << errorString_; + qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus_ << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); // QTimer::singleShot(10, this, SLOT(finish())); } @@ -103,6 +105,8 @@ void AuthRequest::setup(const QNetworkRequest &req, QNetworkAccessManager::Opera status_ = Requesting; error_ = QNetworkReply::NoError; + errorString_.clear(); + httpStatus_ = 0; } void AuthRequest::finish() { diff --git a/launcher/minecraft/auth/flows/AuthRequest.h b/launcher/minecraft/auth/AuthRequest.h index a547aea4..89f7a123 100644 --- a/launcher/minecraft/auth/flows/AuthRequest.h +++ b/launcher/minecraft/auth/AuthRequest.h @@ -46,6 +46,11 @@ protected slots: /// Handle upload progress. void onUploadProgress(qint64 uploaded, qint64 total); +public: + QNetworkReply::NetworkError error_; + int httpStatus_ = 0; + QString errorString_; + protected: void setup(const QNetworkRequest &request, QNetworkAccessManager::Operation operation, const QByteArray &verb = QByteArray()); @@ -60,5 +65,6 @@ protected: QNetworkAccessManager::Operation operation_; QUrl url_; Katabasis::ReplyList timedReplies_; - QNetworkReply::NetworkError error_; + + QTimer *timer_; }; diff --git a/launcher/minecraft/auth/AuthStep.cpp b/launcher/minecraft/auth/AuthStep.cpp new file mode 100644 index 00000000..ffa2581b --- /dev/null +++ b/launcher/minecraft/auth/AuthStep.cpp @@ -0,0 +1,7 @@ +#include "AuthStep.h" + +AuthStep::AuthStep(AccountData *data) : QObject(nullptr), m_data(data) { |
