diff --git a/app/app.pro b/app/app.pro index 95128b4a..a2c9a939 100644 --- a/app/app.pro +++ b/app/app.pro @@ -92,6 +92,7 @@ macx { SOURCES += \ main.cpp \ backend/identitymanager.cpp \ + backend/nvcomputer.cpp \ backend/nvhttp.cpp \ backend/nvpairingmanager.cpp \ backend/computermanager.cpp \ @@ -109,6 +110,7 @@ SOURCES += \ HEADERS += \ utils.h \ backend/identitymanager.h \ + backend/nvcomputer.h \ backend/nvhttp.h \ backend/nvpairingmanager.h \ backend/computermanager.h \ diff --git a/app/backend/boxartmanager.cpp b/app/backend/boxartmanager.cpp index 55d54a9b..111bf7b1 100644 --- a/app/backend/boxartmanager.cpp +++ b/app/backend/boxartmanager.cpp @@ -36,6 +36,39 @@ BoxArtManager::getFilePathForBoxArt(NvComputer* computer, int appId) return dir.filePath(QString::number(appId) + ".png"); } +class NetworkBoxArtLoadTask : public QObject, public QRunnable +{ + Q_OBJECT + +public: + NetworkBoxArtLoadTask(BoxArtManager* boxArtManager, NvComputer* computer, NvApp& app) + : m_Bam(boxArtManager), + m_Computer(computer), + m_App(app) + { + connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)), + boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl))); + } + +signals: + void boxArtFetchCompleted(NvComputer* computer, NvApp app, QUrl image); + +private: + void run() + { + QUrl image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id); + if (image.isEmpty()) { + // Give it another shot if it fails once + image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id); + } + emit boxArtFetchCompleted(m_Computer, m_App, image); + } + + BoxArtManager* m_Bam; + NvComputer* m_Computer; + NvApp m_App; +}; + QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app) { // Try to open the cached file @@ -81,3 +114,5 @@ QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId) return QUrl(); } + +#include "boxartmanager.moc" diff --git a/app/backend/boxartmanager.h b/app/backend/boxartmanager.h index bd09fcb1..110f4fce 100644 --- a/app/backend/boxartmanager.h +++ b/app/backend/boxartmanager.h @@ -38,36 +38,3 @@ private: QDir m_BoxArtDir; QThreadPool m_ThreadPool; }; - -class NetworkBoxArtLoadTask : public QObject, public QRunnable -{ - Q_OBJECT - -public: - NetworkBoxArtLoadTask(BoxArtManager* boxArtManager, NvComputer* computer, NvApp& app) - : m_Bam(boxArtManager), - m_Computer(computer), - m_App(app) - { - connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)), - boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl))); - } - -signals: - void boxArtFetchCompleted(NvComputer* computer, NvApp app, QUrl image); - -private: - void run() - { - QUrl image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id); - if (image.isEmpty()) { - // Give it another shot if it fails once - image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id); - } - emit boxArtFetchCompleted(m_Computer, m_App, image); - } - - BoxArtManager* m_Bam; - NvComputer* m_Computer; - NvApp m_App; -}; diff --git a/app/backend/computermanager.cpp b/app/backend/computermanager.cpp index 4c80d7ba..8431799b 100644 --- a/app/backend/computermanager.cpp +++ b/app/backend/computermanager.cpp @@ -3,282 +3,141 @@ #include "settings/streamingpreferences.h" #include -#include -#include #include #define SER_HOSTS "hosts" -#define SER_NAME "hostname" -#define SER_UUID "uuid" -#define SER_MAC "mac" -#define SER_LOCALADDR "localaddress" -#define SER_REMOTEADDR "remoteaddress" -#define SER_MANUALADDR "manualaddress" -#define SER_APPLIST "apps" -#define SER_APPNAME "name" -#define SER_APPID "id" -#define SER_APPHDR "hdr" - -NvComputer::NvComputer(QSettings& settings) +class PcMonitorThread : public QThread { - this->name = settings.value(SER_NAME).toString(); - this->uuid = settings.value(SER_UUID).toString(); - this->macAddress = settings.value(SER_MAC).toByteArray(); - this->localAddress = settings.value(SER_LOCALADDR).toString(); - this->remoteAddress = settings.value(SER_REMOTEADDR).toString(); - this->manualAddress = settings.value(SER_MANUALADDR).toString(); + Q_OBJECT - int appCount = settings.beginReadArray(SER_APPLIST); - for (int i = 0; i < appCount; i++) { - NvApp app; +#define TRIES_BEFORE_OFFLINING 2 +#define POLLS_PER_APPLIST_FETCH 10 - settings.setArrayIndex(i); - - app.name = settings.value(SER_APPNAME).toString(); - app.id = settings.value(SER_APPID).toInt(); - app.hdrSupported = settings.value(SER_APPHDR).toBool(); - - this->appList.append(app); +public: + PcMonitorThread(NvComputer* computer) + : m_Computer(computer) + { + setObjectName("Polling thread for " + computer->name); } - settings.endArray(); - sortAppList(); - this->activeAddress = nullptr; - this->currentGameId = 0; - this->pairState = PS_UNKNOWN; - this->state = CS_UNKNOWN; - this->gfeVersion = nullptr; - this->appVersion = nullptr; - this->maxLumaPixelsHEVC = 0; - this->serverCodecModeSupport = 0; - this->pendingQuit = false; - this->gpuModel = nullptr; -} +private: + bool tryPollComputer(QString address, bool& changed) + { + NvHTTP http(address); -void -NvComputer::serialize(QSettings& settings) -{ - QReadLocker lock(&this->lock); - - settings.setValue(SER_NAME, name); - settings.setValue(SER_UUID, uuid); - settings.setValue(SER_MAC, macAddress); - settings.setValue(SER_LOCALADDR, localAddress); - settings.setValue(SER_REMOTEADDR, remoteAddress); - settings.setValue(SER_MANUALADDR, manualAddress); - - // Avoid deleting an existing applist if we couldn't get one - if (!appList.isEmpty()) { - settings.remove(SER_APPLIST); - settings.beginWriteArray(SER_APPLIST); - for (int i = 0; i < appList.count(); i++) { - settings.setArrayIndex(i); - - settings.setValue(SER_APPNAME, appList[i].name); - settings.setValue(SER_APPID, appList[i].id); - settings.setValue(SER_APPHDR, appList[i].hdrSupported); + QString serverInfo; + try { + serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE); + } catch (...) { + return false; } - settings.endArray(); - } -} -void NvComputer::sortAppList() -{ - std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) { - return app1.name.toLower() < app2.name.toLower(); - }); -} + NvComputer newState(address, serverInfo); -NvComputer::NvComputer(QString address, QString serverInfo) -{ - this->name = NvHTTP::getXmlString(serverInfo, "hostname"); - if (this->name.isEmpty()) { - this->name = "UNKNOWN"; - } - - this->uuid = NvHTTP::getXmlString(serverInfo, "uniqueid"); - QString newMacString = NvHTTP::getXmlString(serverInfo, "mac"); - if (newMacString != "00:00:00:00:00:00") { - QStringList macOctets = newMacString.split(':'); - for (QString macOctet : macOctets) { - this->macAddress.append((char) macOctet.toInt(nullptr, 16)); + // Ensure the machine that responded is the one we intended to contact + if (m_Computer->uuid != newState.uuid) { + qInfo() << "Found unexpected PC " << newState.name << " looking for " << m_Computer->name; + return false; } - } - QString codecSupport = NvHTTP::getXmlString(serverInfo, "ServerCodecModeSupport"); - if (!codecSupport.isEmpty()) { - this->serverCodecModeSupport = codecSupport.toInt(); - } - else { - this->serverCodecModeSupport = 0; - } - - QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC"); - if (!maxLumaPixelsHEVC.isEmpty()) { - this->maxLumaPixelsHEVC = maxLumaPixelsHEVC.toInt(); - } - else { - this->maxLumaPixelsHEVC = 0; - } - - this->displayModes = NvHTTP::getDisplayModeList(serverInfo); - std::stable_sort(this->displayModes.begin(), this->displayModes.end(), - [](const NvDisplayMode& mode1, const NvDisplayMode& mode2) { - return mode1.width * mode1.height * mode1.refreshRate < - mode2.width * mode2.height * mode2.refreshRate; - }); - - this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP"); - this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP"); - this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ? - PS_PAIRED : PS_NOT_PAIRED; - this->currentGameId = NvHTTP::getCurrentGame(serverInfo); - this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion"); - this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion"); - this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype"); - this->activeAddress = address; - this->state = NvComputer::CS_ONLINE; - this->pendingQuit = false; -} - -bool NvComputer::wake() -{ - if (state == NvComputer::CS_ONLINE) { - qWarning() << name << "is already online"; + changed = m_Computer->update(newState); return true; } - if (macAddress.isEmpty()) { - qWarning() << name << "has no MAC address stored"; - return false; - } + bool updateAppList(bool& changed) + { + Q_ASSERT(m_Computer->activeAddress != nullptr); - const quint16 WOL_PORTS[] = { - 7, 9, // Standard WOL ports - 47998, 47999, 48000, // Ports opened by GFE - }; + NvHTTP http(m_Computer->activeAddress); - // Create the WoL payload - QByteArray wolPayload; - wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF")); - for (int i = 0; i < 16; i++) { - wolPayload.append(macAddress); - } - Q_ASSERT(wolPayload.count() == 102); + QVector appList; - // Add the addresses that we know this host to be - // and broadcast addresses for this link just in - // case the host has timed out in ARP entries. - QVector addressList = uniqueAddresses(); - addressList.append("255.255.255.255"); - - // Try all unique address strings or host names - bool success = false; - for (QString& addressString : addressList) { - QHostInfo hostInfo = QHostInfo::fromName(addressString); - - if (hostInfo.error() != QHostInfo::NoError) { - qWarning() << "Error resolving" << addressString << ":" << hostInfo.errorString(); - continue; + try { + appList = http.getAppList(); + if (appList.isEmpty()) { + return false; + } + } catch (...) { + return false; } - // Try all IP addresses that this string resolves to - for (QHostAddress& address : hostInfo.addresses()) { - QUdpSocket sock; + QWriteLocker lock(&m_Computer->lock); + if (m_Computer->appList != appList) { + m_Computer->appList = appList; + m_Computer->sortAppList(); + changed = true; + } - // Bind to any address on the correct protocol - if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ? - QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) { + return true; + } - // Send to all ports - for (quint16 port : WOL_PORTS) { - if (sock.writeDatagram(wolPayload, address, port)) { - qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port; - success = true; + void run() override + { + // Always fetch the applist the first time + int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH; + while (!isInterruptionRequested()) { + bool stateChanged = false; + bool online = false; + bool wasOnline = m_Computer->state == NvComputer::CS_ONLINE; + for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) { + for (auto& address : m_Computer->uniqueAddresses()) { + if (isInterruptionRequested()) { + return; + } + + if (tryPollComputer(address, stateChanged)) { + if (!wasOnline) { + qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress; + } + online = true; + break; } } } - } - } - return success; -} - -QVector NvComputer::uniqueAddresses() -{ - QVector uniqueAddressList; - - // Start with addresses correctly ordered - uniqueAddressList.append(activeAddress); - uniqueAddressList.append(localAddress); - uniqueAddressList.append(remoteAddress); - uniqueAddressList.append(manualAddress); - - // Prune duplicates (always giving precedence to the first) - for (int i = 0; i < uniqueAddressList.count(); i++) { - if (uniqueAddressList[i].isEmpty()) { - uniqueAddressList.remove(i); - i--; - continue; - } - for (int j = i + 1; j < uniqueAddressList.count(); j++) { - if (uniqueAddressList[i] == uniqueAddressList[j]) { - // Always remove the later occurrence - uniqueAddressList.remove(j); - j--; + // Check if we failed after all retry attempts + // Note: we don't need to acquire the read lock here, + // because we're on the writing thread. + if (!online && m_Computer->state != NvComputer::CS_OFFLINE) { + qInfo() << m_Computer->name << "is now offline"; + m_Computer->state = NvComputer::CS_OFFLINE; + stateChanged = true; } + + // Grab the applist if it's empty or it's been long enough that we need to refresh + pollsSinceLastAppListFetch++; + if (m_Computer->state == NvComputer::CS_ONLINE && + m_Computer->pairState == NvComputer::PS_PAIRED && + (m_Computer->appList.isEmpty() || pollsSinceLastAppListFetch >= POLLS_PER_APPLIST_FETCH)) { + // Notify prior to the app list poll since it may take a while, and we don't + // want to delay onlining of a machine, especially if we already have a cached list. + if (stateChanged) { + emit computerStateChanged(m_Computer); + stateChanged = false; + } + + if (updateAppList(stateChanged)) { + pollsSinceLastAppListFetch = 0; + } + } + + if (stateChanged) { + // Tell anyone listening that we've changed state + emit computerStateChanged(m_Computer); + } + + // Wait a bit to poll again + QThread::sleep(3); } } - // We must have at least 1 address - Q_ASSERT(!uniqueAddressList.isEmpty()); +signals: + void computerStateChanged(NvComputer* computer); - return uniqueAddressList; -} - -bool NvComputer::update(NvComputer& that) -{ - bool changed = false; - - // Lock us for write and them for read - QWriteLocker thisLock(&this->lock); - QReadLocker thatLock(&that.lock); - - // UUID may not change or we're talking to a new PC - Q_ASSERT(this->uuid == that.uuid); - -#define ASSIGN_IF_CHANGED(field) \ - if (this->field != that.field) { \ - this->field = that.field; \ - changed = true; \ - } - -#define ASSIGN_IF_CHANGED_AND_NONEMPTY(field) \ - if (!that.field.isEmpty() && \ - this->field != that.field) { \ - this->field = that.field; \ - changed = true; \ - } - - ASSIGN_IF_CHANGED(name); - ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress); - ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress); - ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress); - ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress); - ASSIGN_IF_CHANGED(pairState); - ASSIGN_IF_CHANGED(serverCodecModeSupport); - ASSIGN_IF_CHANGED(currentGameId); - ASSIGN_IF_CHANGED(activeAddress); - ASSIGN_IF_CHANGED(state); - ASSIGN_IF_CHANGED(gfeVersion); - ASSIGN_IF_CHANGED(appVersion); - ASSIGN_IF_CHANGED(maxLumaPixelsHEVC); - ASSIGN_IF_CHANGED(gpuModel); - ASSIGN_IF_CHANGED_AND_NONEMPTY(appList); - ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes); - return changed; -} +private: + NvComputer* m_Computer; +}; ComputerManager::ComputerManager(QObject *parent) : QObject(parent), @@ -396,6 +255,25 @@ void ComputerManager::startPolling() } } +// Must hold m_Lock for write +void ComputerManager::startPollingComputer(NvComputer* computer) +{ + if (m_PollingRef == 0) { + return; + } + + if (m_PollThreads.contains(computer->uuid)) { + Q_ASSERT(m_PollThreads[computer->uuid]->isRunning()); + return; + } + + PcMonitorThread* thread = new PcMonitorThread(computer); + connect(thread, SIGNAL(computerStateChanged(NvComputer*)), + this, SLOT(handleComputerStateChanged(NvComputer*))); + m_PollThreads[computer->uuid] = thread; + thread->start(); +} + void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer, const QHostAddress& address) { @@ -407,6 +285,19 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer, computer->deleteLater(); } +void ComputerManager::handleComputerStateChanged(NvComputer* computer) +{ + emit computerStateChanged(computer); + + if (computer->pendingQuit && computer->currentGameId == 0) { + computer->pendingQuit = false; + emit quitAppCompleted(QVariant()); + } + + // Save updated hosts to QSettings + saveHosts(); +} + QVector ComputerManager::getComputers() { QReadLocker lock(&m_Lock); @@ -469,6 +360,53 @@ void ComputerManager::deleteHost(NvComputer* computer) QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer)); } +class PendingPairingTask : public QObject, public QRunnable +{ + Q_OBJECT + +public: + PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin) + : m_Computer(computer), + m_Pin(pin) + { + connect(this, &PendingPairingTask::pairingCompleted, + computerManager, &ComputerManager::pairingCompleted); + } + +signals: + void pairingCompleted(NvComputer* computer, QString error); + +private: + void run() + { + NvPairingManager pairingManager(m_Computer->activeAddress); + + try { + NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin); + switch (result) + { + case NvPairingManager::PairState::PIN_WRONG: + emit pairingCompleted(m_Computer, "The PIN from the PC didn't match. Please try again."); + break; + case NvPairingManager::PairState::FAILED: + emit pairingCompleted(m_Computer, "Pairing failed. Please try again."); + break; + case NvPairingManager::PairState::ALREADY_IN_PROGRESS: + emit pairingCompleted(m_Computer, "Another pairing attempt is already in progress."); + break; + case NvPairingManager::PairState::PAIRED: + emit pairingCompleted(m_Computer, nullptr); + break; + } + } catch (const GfeHttpResponseException& e) { + emit pairingCompleted(m_Computer, e.toQString()); + } + } + + NvComputer* m_Computer; + QString m_Pin; +}; + void ComputerManager::pairHost(NvComputer* computer, QString pin) { // Punt to a worker thread to avoid stalling the @@ -477,6 +415,49 @@ void ComputerManager::pairHost(NvComputer* computer, QString pin) QThreadPool::globalInstance()->start(pairing); } +class PendingQuitTask : public QObject, public QRunnable +{ + Q_OBJECT + +public: + PendingQuitTask(ComputerManager* computerManager, NvComputer* computer) + : m_Computer(computer) + { + connect(this, &PendingQuitTask::quitAppFailed, + computerManager, &ComputerManager::quitAppCompleted); + } + +signals: + void quitAppFailed(QString error); + +private: + void run() + { + NvHTTP http(m_Computer->activeAddress); + + try { + if (m_Computer->currentGameId != 0) { + http.quitApp(); + } + } catch (const GfeHttpResponseException& e) { + { + QWriteLocker lock(&m_Computer->lock); + m_Computer->pendingQuit = false; + } + if (e.getStatusCode() == 599) { + // 599 is a special code we make a custom message for + emit quitAppFailed("The running game wasn't started by this PC. " + "You must quit the game on the host PC manually or use the device that originally started the game."); + } + else { + emit quitAppFailed(e.toQString()); + } + } + } + + NvComputer* m_Computer; +}; + void ComputerManager::quitRunningApp(NvComputer* computer) { QWriteLocker lock(&computer->lock); @@ -524,6 +505,101 @@ void ComputerManager::stopPollingAsync() } } +class PendingAddTask : public QObject, public QRunnable +{ + Q_OBJECT + +public: + PendingAddTask(ComputerManager* computerManager, QString address, bool mdns) + : m_ComputerManager(computerManager), + m_Address(address), + m_Mdns(mdns) + { + connect(this, &PendingAddTask::computerAddCompleted, + computerManager, &ComputerManager::computerAddCompleted); + connect(this, &PendingAddTask::computerStateChanged, + computerManager, &ComputerManager::handleComputerStateChanged); + } + +signals: + void computerAddCompleted(QVariant success); + + void computerStateChanged(NvComputer* computer); + +private: + void run() + { + NvHTTP http(m_Address); + + qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user"); + + QString serverInfo; + try { + serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE); + } catch (...) { + if (!m_Mdns) { + emit computerAddCompleted(false); + } + return; + } + + NvComputer* newComputer = new NvComputer(m_Address, serverInfo); + + // Update addresses depending on the context + if (m_Mdns) { + newComputer->localAddress = m_Address; + } + else { + newComputer->manualAddress = m_Address; + } + + // Check if this PC already exists + QWriteLocker lock(&m_ComputerManager->m_Lock); + NvComputer* existingComputer = m_ComputerManager->m_KnownHosts[newComputer->uuid]; + if (existingComputer != nullptr) { + // Fold it into the existing PC + bool changed = existingComputer->update(*newComputer); + delete newComputer; + + // Drop the lock before notifying + lock.unlock(); + + // For non-mDNS clients, let them know it succeeded + if (!m_Mdns) { + emit computerAddCompleted(true); + } + + // Tell our client if something changed + if (changed) { + qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress; + emit computerStateChanged(existingComputer); + } + } + else { + // Store this in our active sets + m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer; + + // Start polling if enabled (write lock required) + m_ComputerManager->startPollingComputer(newComputer); + + // Drop the lock before notifying + lock.unlock(); + + // For non-mDNS clients, let them know it succeeded + if (!m_Mdns) { + emit computerAddCompleted(true); + } + + // Tell our client about this new PC + emit computerStateChanged(newComputer); + } + } + + ComputerManager* m_ComputerManager; + QString m_Address; + bool m_Mdns; +}; + void ComputerManager::addNewHost(QString address, bool mdns) { // Punt to a worker thread to avoid stalling the @@ -532,37 +608,4 @@ void ComputerManager::addNewHost(QString address, bool mdns) QThreadPool::globalInstance()->start(addTask); } -void -ComputerManager::handleComputerStateChanged(NvComputer* computer) -{ - emit computerStateChanged(computer); - - if (computer->pendingQuit && computer->currentGameId == 0) { - computer->pendingQuit = false; - emit quitAppCompleted(QVariant()); - } - - // Save updated hosts to QSettings - saveHosts(); -} - -// Must hold m_Lock for write -void -ComputerManager::startPollingComputer(NvComputer* computer) -{ - if (m_PollingRef == 0) { - return; - } - - if (m_PollThreads.contains(computer->uuid)) { - Q_ASSERT(m_PollThreads[computer->uuid]->isRunning()); - return; - } - - PcMonitorThread* thread = new PcMonitorThread(computer); - connect(thread, SIGNAL(computerStateChanged(NvComputer*)), - this, SLOT(handleComputerStateChanged(NvComputer*))); - m_PollThreads[computer->uuid] = thread; - thread->start(); -} - +#include "computermanager.moc" diff --git a/app/backend/computermanager.h b/app/backend/computermanager.h index 0dfdd364..18f6621a 100644 --- a/app/backend/computermanager.h +++ b/app/backend/computermanager.h @@ -1,5 +1,6 @@ #pragma once -#include "nvhttp.h" + +#include "nvcomputer.h" #include "nvpairingmanager.h" #include @@ -13,207 +14,6 @@ #include #include -class NvComputer -{ - friend class PcMonitorThread; - friend class ComputerManager; - friend class PendingQuitTask; - -private: - void sortAppList(); - - bool pendingQuit; - -public: - explicit NvComputer(QString address, QString serverInfo); - - explicit NvComputer(QSettings& settings); - - bool - update(NvComputer& that); - - bool - wake(); - - QVector - uniqueAddresses(); - - void - serialize(QSettings& settings); - - enum PairState - { - PS_UNKNOWN, - PS_PAIRED, - PS_NOT_PAIRED - }; - - enum ComputerState - { - CS_UNKNOWN, - CS_ONLINE, - CS_OFFLINE - }; - - // Ephemeral traits - ComputerState state; - PairState pairState; - QString activeAddress; - int currentGameId; - QString gfeVersion; - QString appVersion; - QVector displayModes; - int maxLumaPixelsHEVC; - int serverCodecModeSupport; - QString gpuModel; - - // Persisted traits - QString localAddress; - QString remoteAddress; - QString manualAddress; - QByteArray macAddress; - QString name; - QString uuid; - QVector appList; - - // Synchronization - QReadWriteLock lock; -}; - -// FIXME: MOC isn't finding Q_OBJECT properly when this is confined -// to computermanager.cpp as it should be. -class PcMonitorThread : public QThread -{ - Q_OBJECT - -#define TRIES_BEFORE_OFFLINING 2 -#define POLLS_PER_APPLIST_FETCH 10 - -public: - PcMonitorThread(NvComputer* computer) - : m_Computer(computer) - { - setObjectName("Polling thread for " + computer->name); - } - -private: - bool tryPollComputer(QString address, bool& changed) - { - NvHTTP http(address); - - QString serverInfo; - try { - serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE); - } catch (...) { - return false; - } - - NvComputer newState(address, serverInfo); - - // Ensure the machine that responded is the one we intended to contact - if (m_Computer->uuid != newState.uuid) { - qInfo() << "Found unexpected PC " << newState.name << " looking for " << m_Computer->name; - return false; - } - - changed = m_Computer->update(newState); - return true; - } - - bool updateAppList(bool& changed) - { - Q_ASSERT(m_Computer->activeAddress != nullptr); - - NvHTTP http(m_Computer->activeAddress); - - QVector appList; - - try { - appList = http.getAppList(); - if (appList.isEmpty()) { - return false; - } - } catch (...) { - return false; - } - - QWriteLocker lock(&m_Computer->lock); - if (m_Computer->appList != appList) { - m_Computer->appList = appList; - m_Computer->sortAppList(); - changed = true; - } - - return true; - } - - void run() override - { - // Always fetch the applist the first time - int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH; - while (!isInterruptionRequested()) { - bool stateChanged = false; - bool online = false; - bool wasOnline = m_Computer->state == NvComputer::CS_ONLINE; - for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) { - for (auto& address : m_Computer->uniqueAddresses()) { - if (isInterruptionRequested()) { - return; - } - - if (tryPollComputer(address, stateChanged)) { - if (!wasOnline) { - qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress; - } - online = true; - break; - } - } - } - - // Check if we failed after all retry attempts - // Note: we don't need to acquire the read lock here, - // because we're on the writing thread. - if (!online && m_Computer->state != NvComputer::CS_OFFLINE) { - qInfo() << m_Computer->name << "is now offline"; - m_Computer->state = NvComputer::CS_OFFLINE; - stateChanged = true; - } - - // Grab the applist if it's empty or it's been long enough that we need to refresh - pollsSinceLastAppListFetch++; - if (m_Computer->state == NvComputer::CS_ONLINE && - m_Computer->pairState == NvComputer::PS_PAIRED && - (m_Computer->appList.isEmpty() || pollsSinceLastAppListFetch >= POLLS_PER_APPLIST_FETCH)) { - // Notify prior to the app list poll since it may take a while, and we don't - // want to delay onlining of a machine, especially if we already have a cached list. - if (stateChanged) { - emit computerStateChanged(m_Computer); - stateChanged = false; - } - - if (updateAppList(stateChanged)) { - pollsSinceLastAppListFetch = 0; - } - } - - if (stateChanged) { - // Tell anyone listening that we've changed state - emit computerStateChanged(m_Computer); - } - - // Wait a bit to poll again - QThread::sleep(3); - } - } - -signals: - void computerStateChanged(NvComputer* computer); - -private: - NvComputer* m_Computer; -}; - class MdnsPendingComputer : public QObject { Q_OBJECT @@ -306,188 +106,3 @@ private: QMdnsEngine::Cache m_MdnsCache; QVector m_PendingResolution; }; - -class PendingPairingTask : public QObject, public QRunnable -{ - Q_OBJECT - -public: - PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin) - : m_Computer(computer), - m_Pin(pin) - { - connect(this, &PendingPairingTask::pairingCompleted, - computerManager, &ComputerManager::pairingCompleted); - } - -signals: - void pairingCompleted(NvComputer* computer, QString error); - -private: - void run() - { - NvPairingManager pairingManager(m_Computer->activeAddress); - - try { - NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin); - switch (result) - { - case NvPairingManager::PairState::PIN_WRONG: - emit pairingCompleted(m_Computer, "The PIN from the PC didn't match. Please try again."); - break; - case NvPairingManager::PairState::FAILED: - emit pairingCompleted(m_Computer, "Pairing failed. Please try again."); - break; - case NvPairingManager::PairState::ALREADY_IN_PROGRESS: - emit pairingCompleted(m_Computer, "Another pairing attempt is already in progress."); - break; - case NvPairingManager::PairState::PAIRED: - emit pairingCompleted(m_Computer, nullptr); - break; - } - } catch (const GfeHttpResponseException& e) { - emit pairingCompleted(m_Computer, e.toQString()); - } - } - - NvComputer* m_Computer; - QString m_Pin; -}; - -class PendingQuitTask : public QObject, public QRunnable -{ - Q_OBJECT - -public: - PendingQuitTask(ComputerManager* computerManager, NvComputer* computer) - : m_Computer(computer) - { - connect(this, &PendingQuitTask::quitAppFailed, - computerManager, &ComputerManager::quitAppCompleted); - } - -signals: - void quitAppFailed(QString error); - -private: - void run() - { - NvHTTP http(m_Computer->activeAddress); - - try { - if (m_Computer->currentGameId != 0) { - http.quitApp(); - } - } catch (const GfeHttpResponseException& e) { - { - QWriteLocker lock(&m_Computer->lock); - m_Computer->pendingQuit = false; - } - if (e.getStatusCode() == 599) { - // 599 is a special code we make a custom message for - emit quitAppFailed("The running game wasn't started by this PC. " - "You must quit the game on the host PC manually or use the device that originally started the game."); - } - else { - emit quitAppFailed(e.toQString()); - } - } - } - - NvComputer* m_Computer; -}; - -class PendingAddTask : public QObject, public QRunnable -{ - Q_OBJECT - -public: - PendingAddTask(ComputerManager* computerManager, QString address, bool mdns) - : m_ComputerManager(computerManager), - m_Address(address), - m_Mdns(mdns) - { - connect(this, &PendingAddTask::computerAddCompleted, - computerManager, &ComputerManager::computerAddCompleted); - connect(this, &PendingAddTask::computerStateChanged, - computerManager, &ComputerManager::handleComputerStateChanged); - } - -signals: - void computerAddCompleted(QVariant success); - - void computerStateChanged(NvComputer* computer); - -private: - void run() - { - NvHTTP http(m_Address); - - qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user"); - - QString serverInfo; - try { - serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE); - } catch (...) { - if (!m_Mdns) { - emit computerAddCompleted(false); - } - return; - } - - NvComputer* newComputer = new NvComputer(m_Address, serverInfo); - - // Update addresses depending on the context - if (m_Mdns) { - newComputer->localAddress = m_Address; - } - else { - newComputer->manualAddress = m_Address; - } - - // Check if this PC already exists - QWriteLocker lock(&m_ComputerManager->m_Lock); - NvComputer* existingComputer = m_ComputerManager->m_KnownHosts[newComputer->uuid]; - if (existingComputer != nullptr) { - // Fold it into the existing PC - bool changed = existingComputer->update(*newComputer); - delete newComputer; - - // Drop the lock before notifying - lock.unlock(); - - // For non-mDNS clients, let them know it succeeded - if (!m_Mdns) { - emit computerAddCompleted(true); - } - - // Tell our client if something changed - if (changed) { - qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress; - emit computerStateChanged(existingComputer); - } - } - else { - // Store this in our active sets - m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer; - - // Start polling if enabled (write lock required) - m_ComputerManager->startPollingComputer(newComputer); - - // Drop the lock before notifying - lock.unlock(); - - // For non-mDNS clients, let them know it succeeded - if (!m_Mdns) { - emit computerAddCompleted(true); - } - - // Tell our client about this new PC - emit computerStateChanged(newComputer); - } - } - - ComputerManager* m_ComputerManager; - QString m_Address; - bool m_Mdns; -}; diff --git a/app/backend/nvcomputer.cpp b/app/backend/nvcomputer.cpp new file mode 100644 index 00000000..75f1cde5 --- /dev/null +++ b/app/backend/nvcomputer.cpp @@ -0,0 +1,275 @@ +#include "nvcomputer.h" + +#include +#include + +#define SER_NAME "hostname" +#define SER_UUID "uuid" +#define SER_MAC "mac" +#define SER_LOCALADDR "localaddress" +#define SER_REMOTEADDR "remoteaddress" +#define SER_MANUALADDR "manualaddress" +#define SER_APPLIST "apps" + +#define SER_APPNAME "name" +#define SER_APPID "id" +#define SER_APPHDR "hdr" + +NvComputer::NvComputer(QSettings& settings) +{ + this->name = settings.value(SER_NAME).toString(); + this->uuid = settings.value(SER_UUID).toString(); + this->macAddress = settings.value(SER_MAC).toByteArray(); + this->localAddress = settings.value(SER_LOCALADDR).toString(); + this->remoteAddress = settings.value(SER_REMOTEADDR).toString(); + this->manualAddress = settings.value(SER_MANUALADDR).toString(); + + int appCount = settings.beginReadArray(SER_APPLIST); + for (int i = 0; i < appCount; i++) { + NvApp app; + + settings.setArrayIndex(i); + + app.name = settings.value(SER_APPNAME).toString(); + app.id = settings.value(SER_APPID).toInt(); + app.hdrSupported = settings.value(SER_APPHDR).toBool(); + + this->appList.append(app); + } + settings.endArray(); + sortAppList(); + + this->activeAddress = nullptr; + this->currentGameId = 0; + this->pairState = PS_UNKNOWN; + this->state = CS_UNKNOWN; + this->gfeVersion = nullptr; + this->appVersion = nullptr; + this->maxLumaPixelsHEVC = 0; + this->serverCodecModeSupport = 0; + this->pendingQuit = false; + this->gpuModel = nullptr; +} + +void NvComputer::serialize(QSettings& settings) +{ + QReadLocker lock(&this->lock); + + settings.setValue(SER_NAME, name); + settings.setValue(SER_UUID, uuid); + settings.setValue(SER_MAC, macAddress); + settings.setValue(SER_LOCALADDR, localAddress); + settings.setValue(SER_REMOTEADDR, remoteAddress); + settings.setValue(SER_MANUALADDR, manualAddress); + + // Avoid deleting an existing applist if we couldn't get one + if (!appList.isEmpty()) { + settings.remove(SER_APPLIST); + settings.beginWriteArray(SER_APPLIST); + for (int i = 0; i < appList.count(); i++) { + settings.setArrayIndex(i); + + settings.setValue(SER_APPNAME, appList[i].name); + settings.setValue(SER_APPID, appList[i].id); + settings.setValue(SER_APPHDR, appList[i].hdrSupported); + } + settings.endArray(); + } +} + +void NvComputer::sortAppList() +{ + std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) { + return app1.name.toLower() < app2.name.toLower(); + }); +} + +NvComputer::NvComputer(QString address, QString serverInfo) +{ + this->name = NvHTTP::getXmlString(serverInfo, "hostname"); + if (this->name.isEmpty()) { + this->name = "UNKNOWN"; + } + + this->uuid = NvHTTP::getXmlString(serverInfo, "uniqueid"); + QString newMacString = NvHTTP::getXmlString(serverInfo, "mac"); + if (newMacString != "00:00:00:00:00:00") { + QStringList macOctets = newMacString.split(':'); + for (QString macOctet : macOctets) { + this->macAddress.append((char) macOctet.toInt(nullptr, 16)); + } + } + + QString codecSupport = NvHTTP::getXmlString(serverInfo, "ServerCodecModeSupport"); + if (!codecSupport.isEmpty()) { + this->serverCodecModeSupport = codecSupport.toInt(); + } + else { + this->serverCodecModeSupport = 0; + } + + QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC"); + if (!maxLumaPixelsHEVC.isEmpty()) { + this->maxLumaPixelsHEVC = maxLumaPixelsHEVC.toInt(); + } + else { + this->maxLumaPixelsHEVC = 0; + } + + this->displayModes = NvHTTP::getDisplayModeList(serverInfo); + std::stable_sort(this->displayModes.begin(), this->displayModes.end(), + [](const NvDisplayMode& mode1, const NvDisplayMode& mode2) { + return mode1.width * mode1.height * mode1.refreshRate < + mode2.width * mode2.height * mode2.refreshRate; + }); + + this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP"); + this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP"); + this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ? + PS_PAIRED : PS_NOT_PAIRED; + this->currentGameId = NvHTTP::getCurrentGame(serverInfo); + this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion"); + this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion"); + this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype"); + this->activeAddress = address; + this->state = NvComputer::CS_ONLINE; + this->pendingQuit = false; +} + +bool NvComputer::wake() +{ + if (state == NvComputer::CS_ONLINE) { + qWarning() << name << "is already online"; + return true; + } + + if (macAddress.isEmpty()) { + qWarning() << name << "has no MAC address stored"; + return false; + } + + const quint16 WOL_PORTS[] = { + 7, 9, // Standard WOL ports + 47998, 47999, 48000, // Ports opened by GFE + }; + + // Create the WoL payload + QByteArray wolPayload; + wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF")); + for (int i = 0; i < 16; i++) { + wolPayload.append(macAddress); + } + Q_ASSERT(wolPayload.count() == 102); + + // Add the addresses that we know this host to be + // and broadcast addresses for this link just in + // case the host has timed out in ARP entries. + QVector addressList = uniqueAddresses(); + addressList.append("255.255.255.255"); + + // Try all unique address strings or host names + bool success = false; + for (QString& addressString : addressList) { + QHostInfo hostInfo = QHostInfo::fromName(addressString); + + if (hostInfo.error() != QHostInfo::NoError) { + qWarning() << "Error resolving" << addressString << ":" << hostInfo.errorString(); + continue; + } + + // Try all IP addresses that this string resolves to + for (QHostAddress& address : hostInfo.addresses()) { + QUdpSocket sock; + + // Bind to any address on the correct protocol + if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ? + QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) { + + // Send to all ports + for (quint16 port : WOL_PORTS) { + if (sock.writeDatagram(wolPayload, address, port)) { + qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port; + success = true; + } + } + } + } + } + + return success; +} + +QVector NvComputer::uniqueAddresses() +{ + QVector uniqueAddressList; + + // Start with addresses correctly ordered + uniqueAddressList.append(activeAddress); + uniqueAddressList.append(localAddress); + uniqueAddressList.append(remoteAddress); + uniqueAddressList.append(manualAddress); + + // Prune duplicates (always giving precedence to the first) + for (int i = 0; i < uniqueAddressList.count(); i++) { + if (uniqueAddressList[i].isEmpty()) { + uniqueAddressList.remove(i); + i--; + continue; + } + for (int j = i + 1; j < uniqueAddressList.count(); j++) { + if (uniqueAddressList[i] == uniqueAddressList[j]) { + // Always remove the later occurrence + uniqueAddressList.remove(j); + j--; + } + } + } + + // We must have at least 1 address + Q_ASSERT(!uniqueAddressList.isEmpty()); + + return uniqueAddressList; +} + +bool NvComputer::update(NvComputer& that) +{ + bool changed = false; + + // Lock us for write and them for read + QWriteLocker thisLock(&this->lock); + QReadLocker thatLock(&that.lock); + + // UUID may not change or we're talking to a new PC + Q_ASSERT(this->uuid == that.uuid); + +#define ASSIGN_IF_CHANGED(field) \ + if (this->field != that.field) { \ + this->field = that.field; \ + changed = true; \ + } + +#define ASSIGN_IF_CHANGED_AND_NONEMPTY(field) \ + if (!that.field.isEmpty() && \ + this->field != that.field) { \ + this->field = that.field; \ + changed = true; \ + } + + ASSIGN_IF_CHANGED(name); + ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress); + ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress); + ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress); + ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress); + ASSIGN_IF_CHANGED(pairState); + ASSIGN_IF_CHANGED(serverCodecModeSupport); + ASSIGN_IF_CHANGED(currentGameId); + ASSIGN_IF_CHANGED(activeAddress); + ASSIGN_IF_CHANGED(state); + ASSIGN_IF_CHANGED(gfeVersion); + ASSIGN_IF_CHANGED(appVersion); + ASSIGN_IF_CHANGED(maxLumaPixelsHEVC); + ASSIGN_IF_CHANGED(gpuModel); + ASSIGN_IF_CHANGED_AND_NONEMPTY(appList); + ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes); + return changed; +} diff --git a/app/backend/nvcomputer.h b/app/backend/nvcomputer.h new file mode 100644 index 00000000..542a3779 --- /dev/null +++ b/app/backend/nvcomputer.h @@ -0,0 +1,75 @@ +#pragma once + +#include "nvhttp.h" + +#include +#include +#include +#include + +class NvComputer +{ + friend class PcMonitorThread; + friend class ComputerManager; + friend class PendingQuitTask; + +private: + void sortAppList(); + + bool pendingQuit; + +public: + explicit NvComputer(QString address, QString serverInfo); + + explicit NvComputer(QSettings& settings); + + bool + update(NvComputer& that); + + bool + wake(); + + QVector + uniqueAddresses(); + + void + serialize(QSettings& settings); + + enum PairState + { + PS_UNKNOWN, + PS_PAIRED, + PS_NOT_PAIRED + }; + + enum ComputerState + { + CS_UNKNOWN, + CS_ONLINE, + CS_OFFLINE + }; + + // Ephemeral traits + ComputerState state; + PairState pairState; + QString activeAddress; + int currentGameId; + QString gfeVersion; + QString appVersion; + QVector displayModes; + int maxLumaPixelsHEVC; + int serverCodecModeSupport; + QString gpuModel; + + // Persisted traits + QString localAddress; + QString remoteAddress; + QString manualAddress; + QByteArray macAddress; + QString name; + QString uuid; + QVector appList; + + // Synchronization + QReadWriteLock lock; +};