#pragma once #include "nvhttp.h" #include "nvpairingmanager.h" #include #include #include #include #include #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; }; // 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)) { 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 public: explicit MdnsPendingComputer(QMdnsEngine::Server* server, QMdnsEngine::Cache* cache, const QMdnsEngine::Service& service) : m_Hostname(service.hostname()), m_Resolver(server, m_Hostname, cache) { connect(&m_Resolver, SIGNAL(resolved(QHostAddress)), this, SLOT(handleResolved(QHostAddress))); } QString hostname() { return m_Hostname; } private slots: void handleResolved(const QHostAddress& address) { if (address.protocol() == QAbstractSocket::IPv4Protocol) { m_Resolver.disconnect(); emit resolvedv4(this, address); } } signals: void resolvedv4(MdnsPendingComputer*, const QHostAddress&); private: QByteArray m_Hostname; QMdnsEngine::Resolver m_Resolver; }; class ComputerManager : public QObject { Q_OBJECT friend class DeferredHostDeletionTask; friend class PendingAddTask; public: explicit ComputerManager(QObject *parent = nullptr); virtual ~ComputerManager(); Q_INVOKABLE void startPolling(); Q_INVOKABLE void stopPollingAsync(); Q_INVOKABLE void addNewHost(QString address, bool mdns); void pairHost(NvComputer* computer, QString pin); void quitRunningApp(NvComputer* computer); QVector getComputers(); // computer is deleted inside this call void deleteHost(NvComputer* computer); signals: void computerStateChanged(NvComputer* computer); void pairingCompleted(NvComputer* computer, QString error); void computerAddCompleted(QVariant success); void quitAppCompleted(QVariant error); private slots: void handleComputerStateChanged(NvComputer* computer); void handleMdnsServiceResolved(MdnsPendingComputer* computer, const QHostAddress& address); private: void saveHosts(); void startPollingComputer(NvComputer* computer); int m_PollingRef; QReadWriteLock m_Lock; QMap m_KnownHosts; QMap m_PollThreads; QMdnsEngine::Server m_MdnsServer; QMdnsEngine::Browser* m_MdnsBrowser; 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; };