mirror of
https://github.com/moonlight-stream/moonlight-qt
synced 2024-11-10 13:44:17 +00:00
ef988cc8af
These set background timers to refresh state, rebind sockets, and refresh the cache. This is all stuff that we don't want to be doing while streaming.
930 lines
32 KiB
C++
930 lines
32 KiB
C++
#include "computermanager.h"
|
|
#include "boxartmanager.h"
|
|
#include "nvhttp.h"
|
|
#include "settings/streamingpreferences.h"
|
|
|
|
#include <Limelight.h>
|
|
#include <QtEndian>
|
|
|
|
#include <QThread>
|
|
#include <QThreadPool>
|
|
#include <QCoreApplication>
|
|
|
|
#include <random>
|
|
|
|
#define SER_HOSTS "hosts"
|
|
|
|
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(NvAddress address, bool& changed)
|
|
{
|
|
NvHTTP http(address, 0, m_Computer->serverCert);
|
|
|
|
QString serverInfo;
|
|
try {
|
|
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE, true);
|
|
} catch (...) {
|
|
return false;
|
|
}
|
|
|
|
NvComputer newState(http, 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)
|
|
{
|
|
NvHTTP http(m_Computer);
|
|
|
|
QVector<NvApp> appList;
|
|
|
|
try {
|
|
appList = http.getAppList();
|
|
if (appList.isEmpty()) {
|
|
return false;
|
|
}
|
|
} catch (...) {
|
|
return false;
|
|
}
|
|
|
|
QWriteLocker lock(&m_Computer->lock);
|
|
changed = m_Computer->updateAppList(appList);
|
|
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 < (wasOnline ? TRIES_BEFORE_OFFLINING : 1) && !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.toString();
|
|
}
|
|
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, but do it in 100 ms chunks
|
|
// so we can be interrupted reasonably quickly.
|
|
// FIXME: QWaitCondition would be better.
|
|
for (int i = 0; i < 30 && !isInterruptionRequested(); i++) {
|
|
QThread::msleep(100);
|
|
}
|
|
}
|
|
}
|
|
|
|
signals:
|
|
void computerStateChanged(NvComputer* computer);
|
|
|
|
private:
|
|
NvComputer* m_Computer;
|
|
};
|
|
|
|
ComputerManager::ComputerManager(QObject *parent)
|
|
: QObject(parent),
|
|
m_PollingRef(0),
|
|
m_MdnsBrowser(nullptr),
|
|
m_CompatFetcher(nullptr),
|
|
m_NeedsDelayedFlush(false)
|
|
{
|
|
QSettings settings;
|
|
|
|
// Inflate our hosts from QSettings
|
|
int hosts = settings.beginReadArray(SER_HOSTS);
|
|
for (int i = 0; i < hosts; i++) {
|
|
settings.setArrayIndex(i);
|
|
NvComputer* computer = new NvComputer(settings);
|
|
m_KnownHosts[computer->uuid] = computer;
|
|
}
|
|
settings.endArray();
|
|
|
|
// Fetch latest compatibility data asynchronously
|
|
m_CompatFetcher.start();
|
|
|
|
// Start the delayed flush thread to handle saveHosts() calls
|
|
m_DelayedFlushThread = new DelayedFlushThread(this);
|
|
m_DelayedFlushThread->start();
|
|
|
|
// To quit in a timely manner, we must block additional requests
|
|
// after we receive the aboutToQuit() signal. This is neccessary
|
|
// because NvHTTP uses aboutToQuit() to abort requests in progres
|
|
// while quitting, however this is a one time signal - additional
|
|
// requests would not be aborted and block termination.
|
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit);
|
|
}
|
|
|
|
ComputerManager::~ComputerManager()
|
|
{
|
|
// Stop the delayed flush thread before acquiring the lock in write mode
|
|
// to avoid deadlocking with a flush that needs the lock in read mode.
|
|
{
|
|
// Wake the delayed flush thread
|
|
m_DelayedFlushThread->requestInterruption();
|
|
m_DelayedFlushCondition.wakeOne();
|
|
|
|
// Wait for it to terminate (and finish any pending flush)
|
|
m_DelayedFlushThread->wait();
|
|
delete m_DelayedFlushThread;
|
|
|
|
// Delayed flushes should have completed by now
|
|
Q_ASSERT(!m_NeedsDelayedFlush);
|
|
}
|
|
|
|
QWriteLocker lock(&m_Lock);
|
|
|
|
// Delete machines that haven't been resolved yet
|
|
while (!m_PendingResolution.isEmpty()) {
|
|
MdnsPendingComputer* computer = m_PendingResolution.first();
|
|
delete computer;
|
|
m_PendingResolution.removeFirst();
|
|
}
|
|
|
|
// Delete the browser to stop discovery
|
|
delete m_MdnsBrowser;
|
|
m_MdnsBrowser = nullptr;
|
|
|
|
// Interrupt polling
|
|
for (ComputerPollingEntry* entry : m_PollEntries) {
|
|
entry->interrupt();
|
|
}
|
|
|
|
// Delete all polling entries (and associated threads)
|
|
for (ComputerPollingEntry* entry : m_PollEntries) {
|
|
delete entry;
|
|
}
|
|
|
|
// Destroy all NvComputer objects now that polling is halted
|
|
for (NvComputer* computer : m_KnownHosts) {
|
|
delete computer;
|
|
}
|
|
}
|
|
|
|
void DelayedFlushThread::run() {
|
|
for (;;) {
|
|
// Wait for a delayed flush request or an interruption
|
|
{
|
|
QMutexLocker locker(&m_ComputerManager->m_DelayedFlushMutex);
|
|
|
|
while (!QThread::currentThread()->isInterruptionRequested() && !m_ComputerManager->m_NeedsDelayedFlush) {
|
|
m_ComputerManager->m_DelayedFlushCondition.wait(&m_ComputerManager->m_DelayedFlushMutex);
|
|
}
|
|
|
|
// Bail without flushing if we woke up for an interruption alone.
|
|
// If we have both an interruption and a flush request, do the flush.
|
|
if (!m_ComputerManager->m_NeedsDelayedFlush) {
|
|
Q_ASSERT(QThread::currentThread()->isInterruptionRequested());
|
|
break;
|
|
}
|
|
|
|
// Reset the delayed flush flag to ensure any racing saveHosts() call will set it again
|
|
m_ComputerManager->m_NeedsDelayedFlush = false;
|
|
}
|
|
|
|
// Perform the flush
|
|
{
|
|
QReadLocker lock(&m_ComputerManager->m_Lock);
|
|
|
|
QSettings settings;
|
|
settings.remove(SER_HOSTS);
|
|
settings.beginWriteArray(SER_HOSTS);
|
|
int i = 0;
|
|
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
|
settings.setArrayIndex(i++);
|
|
computer->serialize(settings);
|
|
}
|
|
settings.endArray();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ComputerManager::saveHosts()
|
|
{
|
|
Q_ASSERT(m_DelayedFlushThread != nullptr && m_DelayedFlushThread->isRunning());
|
|
|
|
// Punt to a worker thread because QSettings on macOS can take ages (> 500 ms)
|
|
// to persist our host list to disk (especially when a host has a bunch of apps).
|
|
QMutexLocker locker(&m_DelayedFlushMutex);
|
|
m_NeedsDelayedFlush = true;
|
|
m_DelayedFlushCondition.wakeOne();
|
|
}
|
|
|
|
QHostAddress ComputerManager::getBestGlobalAddressV6(QVector<QHostAddress> &addresses)
|
|
{
|
|
for (const QHostAddress& address : addresses) {
|
|
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
|
|
if (address.isInSubnet(QHostAddress("fe80::"), 10)) {
|
|
// Link-local
|
|
continue;
|
|
}
|
|
|
|
if (address.isInSubnet(QHostAddress("fec0::"), 10)) {
|
|
qInfo() << "Ignoring site-local address:" << address;
|
|
continue;
|
|
}
|
|
|
|
if (address.isInSubnet(QHostAddress("fc00::"), 7)) {
|
|
qInfo() << "Ignoring ULA:" << address;
|
|
continue;
|
|
}
|
|
|
|
if (address.isInSubnet(QHostAddress("2002::"), 16)) {
|
|
qInfo() << "Ignoring 6to4 address:" << address;
|
|
continue;
|
|
}
|
|
|
|
if (address.isInSubnet(QHostAddress("2001::"), 32)) {
|
|
qInfo() << "Ignoring Teredo address:" << address;
|
|
continue;
|
|
}
|
|
|
|
return address;
|
|
}
|
|
}
|
|
|
|
return QHostAddress();
|
|
}
|
|
|
|
void ComputerManager::startPolling()
|
|
{
|
|
QWriteLocker lock(&m_Lock);
|
|
|
|
if (++m_PollingRef > 1) {
|
|
return;
|
|
}
|
|
|
|
StreamingPreferences prefs;
|
|
|
|
if (prefs.enableMdns) {
|
|
// Start an MDNS query for GameStream hosts
|
|
m_MdnsServer.reset(new QMdnsEngine::Server());
|
|
m_MdnsBrowser = new QMdnsEngine::Browser(m_MdnsServer.get(), "_nvstream._tcp.local.");
|
|
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
|
|
this, [this](const QMdnsEngine::Service& service) {
|
|
qInfo() << "Discovered mDNS host:" << service.hostname();
|
|
|
|
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(m_MdnsServer, service);
|
|
connect(pendingComputer, &MdnsPendingComputer::resolvedHost,
|
|
this, &ComputerManager::handleMdnsServiceResolved);
|
|
m_PendingResolution.append(pendingComputer);
|
|
});
|
|
}
|
|
else {
|
|
qWarning() << "mDNS is disabled by user preference";
|
|
}
|
|
|
|
// Start polling threads for each known host
|
|
QMapIterator<QString, NvComputer*> i(m_KnownHosts);
|
|
while (i.hasNext()) {
|
|
i.next();
|
|
startPollingComputer(i.value());
|
|
}
|
|
}
|
|
|
|
// Must hold m_Lock for write
|
|
void ComputerManager::startPollingComputer(NvComputer* computer)
|
|
{
|
|
if (m_PollingRef == 0) {
|
|
return;
|
|
}
|
|
|
|
ComputerPollingEntry* pollingEntry;
|
|
|
|
if (!m_PollEntries.contains(computer->uuid)) {
|
|
pollingEntry = m_PollEntries[computer->uuid] = new ComputerPollingEntry();
|
|
}
|
|
else {
|
|
pollingEntry = m_PollEntries[computer->uuid];
|
|
}
|
|
|
|
if (!pollingEntry->isActive()) {
|
|
PcMonitorThread* thread = new PcMonitorThread(computer);
|
|
connect(thread, &PcMonitorThread::computerStateChanged,
|
|
this, &ComputerManager::handleComputerStateChanged);
|
|
pollingEntry->setActiveThread(thread);
|
|
thread->start();
|
|
}
|
|
}
|
|
|
|
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
|
QVector<QHostAddress>& addresses)
|
|
{
|
|
QHostAddress v6Global = getBestGlobalAddressV6(addresses);
|
|
bool added = false;
|
|
|
|
// Add the host using the IPv4 address
|
|
for (const QHostAddress& address : addresses) {
|
|
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
|
|
// NB: We don't just call addNewHost() here with v6Global because the IPv6
|
|
// address may not be reachable (if the user hasn't installed the IPv6 helper yet
|
|
// or if this host lacks outbound IPv6 capability). We want to add IPv6 even if
|
|
// it's not currently reachable.
|
|
addNewHost(NvAddress(address, computer->port()), true, NvAddress(v6Global, computer->port()));
|
|
added = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!added) {
|
|
// If we get here, there wasn't an IPv4 address so we'll do it v6-only
|
|
for (const QHostAddress& address : addresses) {
|
|
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
|
|
// Use a link-local or site-local address for the "local address"
|
|
if (address.isInSubnet(QHostAddress("fe80::"), 10) ||
|
|
address.isInSubnet(QHostAddress("fec0::"), 10) ||
|
|
address.isInSubnet(QHostAddress("fc00::"), 7)) {
|
|
addNewHost(NvAddress(address, computer->port()), true, NvAddress(v6Global, computer->port()));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
m_PendingResolution.removeOne(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<NvComputer*> ComputerManager::getComputers()
|
|
{
|
|
QReadLocker lock(&m_Lock);
|
|
|
|
return QVector<NvComputer*>::fromList(m_KnownHosts.values());
|
|
}
|
|
|
|
class DeferredHostDeletionTask : public QRunnable
|
|
{
|
|
public:
|
|
DeferredHostDeletionTask(ComputerManager* cm, NvComputer* computer)
|
|
: m_Computer(computer),
|
|
m_ComputerManager(cm) {}
|
|
|
|
void run()
|
|
{
|
|
ComputerPollingEntry* pollingEntry;
|
|
|
|
// Only do the minimum amount of work while holding the writer lock.
|
|
// We must release it before calling saveHosts().
|
|
{
|
|
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
|
|
|
pollingEntry = m_ComputerManager->m_PollEntries.take(m_Computer->uuid);
|
|
|
|
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
|
}
|
|
|
|
// Persist the new host list
|
|
m_ComputerManager->saveHosts();
|
|
|
|
// Delete the polling entry first. This will stop all polling threads too.
|
|
delete pollingEntry;
|
|
|
|
// Delete cached box art
|
|
BoxArtManager::deleteBoxArt(m_Computer);
|
|
|
|
// Finally, delete the computer itself. This must be done
|
|
// last because the polling thread might be using it.
|
|
delete m_Computer;
|
|
}
|
|
|
|
private:
|
|
NvComputer* m_Computer;
|
|
ComputerManager* m_ComputerManager;
|
|
};
|
|
|
|
void ComputerManager::deleteHost(NvComputer* computer)
|
|
{
|
|
// Punt to a worker thread to avoid stalling the
|
|
// UI while waiting for the polling thread to die
|
|
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
|
|
}
|
|
|
|
void ComputerManager::renameHost(NvComputer* computer, QString name)
|
|
{
|
|
{
|
|
QWriteLocker lock(&computer->lock);
|
|
|
|
computer->name = name;
|
|
computer->hasCustomName = true;
|
|
}
|
|
|
|
// Notify the UI of the state change
|
|
handleComputerStateChanged(computer);
|
|
}
|
|
|
|
void ComputerManager::clientSideAttributeUpdated(NvComputer* computer)
|
|
{
|
|
// Persist the change
|
|
saveHosts();
|
|
|
|
// Notify the UI of the state change
|
|
handleComputerStateChanged(computer);
|
|
}
|
|
|
|
void ComputerManager::handleAboutToQuit()
|
|
{
|
|
QReadLocker lock(&m_Lock);
|
|
|
|
// Interrupt polling threads immediately, so they
|
|
// avoid making additional requests while quitting
|
|
for (ComputerPollingEntry* entry : m_PollEntries) {
|
|
entry->interrupt();
|
|
}
|
|
}
|
|
|
|
class PendingPairingTask : public QObject, public QRunnable
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin)
|
|
: m_ComputerManager(computerManager),
|
|
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);
|
|
|
|
try {
|
|
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin, m_Computer->serverCert);
|
|
switch (result)
|
|
{
|
|
case NvPairingManager::PairState::PIN_WRONG:
|
|
emit pairingCompleted(m_Computer, tr("The PIN from the PC didn't match. Please try again."));
|
|
break;
|
|
case NvPairingManager::PairState::FAILED:
|
|
if (m_Computer->currentGameId != 0) {
|
|
emit pairingCompleted(m_Computer, tr("You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again."));
|
|
}
|
|
else {
|
|
emit pairingCompleted(m_Computer, tr("Pairing failed. Please try again."));
|
|
}
|
|
break;
|
|
case NvPairingManager::PairState::ALREADY_IN_PROGRESS:
|
|
emit pairingCompleted(m_Computer, tr("Another pairing attempt is already in progress."));
|
|
break;
|
|
case NvPairingManager::PairState::PAIRED:
|
|
// Persist the newly pinned server certificate for this host
|
|
m_ComputerManager->saveHosts();
|
|
|
|
emit pairingCompleted(m_Computer, nullptr);
|
|
break;
|
|
}
|
|
} catch (const GfeHttpResponseException& e) {
|
|
emit pairingCompleted(m_Computer, tr("GeForce Experience returned error: %1").arg(e.toQString()));
|
|
} catch (const QtNetworkReplyException& e) {
|
|
emit pairingCompleted(m_Computer, e.toQString());
|
|
}
|
|
}
|
|
|
|
ComputerManager* m_ComputerManager;
|
|
NvComputer* m_Computer;
|
|
QString m_Pin;
|
|
};
|
|
|
|
void ComputerManager::pairHost(NvComputer* computer, QString pin)
|
|
{
|
|
// Punt to a worker thread to avoid stalling the
|
|
// UI while waiting for pairing to complete
|
|
PendingPairingTask* pairing = new PendingPairingTask(this, computer, 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);
|
|
|
|
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(tr("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());
|
|
}
|
|
} catch (const QtNetworkReplyException& e) {
|
|
{
|
|
QWriteLocker lock(&m_Computer->lock);
|
|
m_Computer->pendingQuit = false;
|
|
}
|
|
emit quitAppFailed(e.toQString());
|
|
}
|
|
}
|
|
|
|
NvComputer* m_Computer;
|
|
};
|
|
|
|
void ComputerManager::quitRunningApp(NvComputer* computer)
|
|
{
|
|
QWriteLocker lock(&computer->lock);
|
|
computer->pendingQuit = true;
|
|
|
|
PendingQuitTask* quit = new PendingQuitTask(this, computer);
|
|
QThreadPool::globalInstance()->start(quit);
|
|
}
|
|
|
|
void ComputerManager::stopPollingAsync()
|
|
{
|
|
QWriteLocker lock(&m_Lock);
|
|
|
|
Q_ASSERT(m_PollingRef > 0);
|
|
if (--m_PollingRef > 0) {
|
|
return;
|
|
}
|
|
|
|
// Delete machines that haven't been resolved yet
|
|
while (!m_PendingResolution.isEmpty()) {
|
|
MdnsPendingComputer* computer = m_PendingResolution.first();
|
|
computer->deleteLater();
|
|
m_PendingResolution.removeFirst();
|
|
}
|
|
|
|
// Delete the browser and server to stop discovery and refresh polling
|
|
delete m_MdnsBrowser;
|
|
m_MdnsBrowser = nullptr;
|
|
m_MdnsServer.reset();
|
|
|
|
// Interrupt all threads, but don't wait for them to terminate
|
|
for (ComputerPollingEntry* entry : m_PollEntries) {
|
|
entry->interrupt();
|
|
}
|
|
}
|
|
|
|
void ComputerManager::addNewHostManually(QString address)
|
|
{
|
|
QUrl url = QUrl::fromUserInput("moonlight://" + address);
|
|
if (url.isValid() && !url.host().isEmpty() && url.scheme() == "moonlight") {
|
|
// If there wasn't a port specified, use the default
|
|
addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false);
|
|
}
|
|
else {
|
|
emit computerAddCompleted(false, false);
|
|
}
|
|
}
|
|
|
|
class PendingAddTask : public QObject, public QRunnable
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
PendingAddTask(ComputerManager* computerManager, NvAddress address, NvAddress mdnsIpv6Address, bool mdns)
|
|
: m_ComputerManager(computerManager),
|
|
m_Address(address),
|
|
m_MdnsIpv6Address(mdnsIpv6Address),
|
|
m_Mdns(mdns),
|
|
m_AboutToQuit(false)
|
|
{
|
|
connect(this, &PendingAddTask::computerAddCompleted,
|
|
computerManager, &ComputerManager::computerAddCompleted);
|
|
connect(this, &PendingAddTask::computerStateChanged,
|
|
computerManager, &ComputerManager::handleComputerStateChanged);
|
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
|
|
this, &PendingAddTask::handleAboutToQuit);
|
|
}
|
|
|
|
signals:
|
|
void computerAddCompleted(QVariant success, QVariant detectedPortBlocking);
|
|
|
|
void computerStateChanged(NvComputer* computer);
|
|
|
|
private:
|
|
void handleAboutToQuit()
|
|
{
|
|
m_AboutToQuit = true;
|
|
}
|
|
|
|
QString fetchServerInfo(NvHTTP& http)
|
|
{
|
|
QString serverInfo;
|
|
|
|
// Do nothing if we're quitting
|
|
if (m_AboutToQuit) {
|
|
return QString();
|
|
}
|
|
|
|
try {
|
|
// There's a race condition between GameStream servers reporting presence over
|
|
// mDNS and the HTTPS server being ready to respond to our queries. To work
|
|
// around this issue, we will issue the request again after a few seconds if
|
|
// we see a ServiceUnavailableError error.
|
|
try {
|
|
serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE);
|
|
} catch (const QtNetworkReplyException& e) {
|
|
if (e.getError() == QNetworkReply::ServiceUnavailableError) {
|
|
qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError";
|
|
QThread::sleep(5);
|
|
serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE);
|
|
qInfo() << "Retry successful";
|
|
}
|
|
else {
|
|
// Rethrow other errors
|
|
throw e;
|
|
}
|
|
}
|
|
return serverInfo;
|
|
} catch (...) {
|
|
if (!m_Mdns) {
|
|
StreamingPreferences prefs;
|
|
int portTestResult;
|
|
|
|
if (prefs.detectNetworkBlocking) {
|
|
// We failed to connect to the specified PC. Let's test to make sure this network
|
|
// isn't blocking Moonlight, so we can tell the user about it.
|
|
portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443,
|
|
ML_PORT_FLAG_TCP_47984 | ML_PORT_FLAG_TCP_47989);
|
|
}
|
|
else {
|
|
portTestResult = 0;
|
|
}
|
|
|
|
emit computerAddCompleted(false, portTestResult != 0 && portTestResult != ML_TEST_RESULT_INCONCLUSIVE);
|
|
}
|
|
return QString();
|
|
}
|
|
}
|
|
|
|
void run()
|
|
{
|
|
NvHTTP http(m_Address, 0, QSslCertificate());
|
|
|
|
qInfo() << "Processing new PC at" << m_Address.toString() << "from" << (m_Mdns ? "mDNS" : "user") << "with IPv6 address" << m_MdnsIpv6Address.toString();
|
|
|
|
// Perform initial serverinfo fetch over HTTP since we don't know which cert to use
|
|
QString serverInfo = fetchServerInfo(http);
|
|
if (serverInfo.isEmpty() && !m_MdnsIpv6Address.isNull()) {
|
|
// Retry using the global IPv6 address if the IPv4 or link-local IPv6 address fails
|
|
http.setAddress(m_MdnsIpv6Address);
|
|
serverInfo = fetchServerInfo(http);
|
|
}
|
|
if (serverInfo.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// Create initial newComputer using HTTP serverinfo with no pinned cert
|
|
NvComputer* newComputer = new NvComputer(http, serverInfo);
|
|
|
|
// Check if we have a record of this host UUID to pull the pinned cert
|
|
NvComputer* existingComputer;
|
|
{
|
|
QReadLocker lock(&m_ComputerManager->m_Lock);
|
|
existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
|
if (existingComputer != nullptr) {
|
|
http.setServerCert(existingComputer->serverCert);
|
|
}
|
|
}
|
|
|
|
// Fetch serverinfo again over HTTPS with the pinned cert
|
|
if (existingComputer != nullptr) {
|
|
Q_ASSERT(http.httpsPort() != 0);
|
|
serverInfo = fetchServerInfo(http);
|
|
if (serverInfo.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// Update the polled computer with the HTTPS information
|
|
NvComputer httpsComputer(http, serverInfo);
|
|
newComputer->update(httpsComputer);
|
|
}
|
|
|
|
// Update addresses depending on the context
|
|
if (m_Mdns) {
|
|
// Only update local address if we actually reached the PC via this address.
|
|
// If we reached it via the IPv6 address after the local address failed,
|
|
// don't store the non-working local address.
|
|
if (http.address() == m_Address) {
|
|
newComputer->localAddress = m_Address;
|
|
}
|
|
|
|
// Get the WAN IP address using STUN if we're on mDNS over IPv4
|
|
if (QHostAddress(newComputer->localAddress.address()).protocol() == QAbstractSocket::IPv4Protocol) {
|
|
quint32 addr;
|
|
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
|
if (err == 0) {
|
|
newComputer->setRemoteAddress(QHostAddress(qFromBigEndian(addr)));
|
|
}
|
|
else {
|
|
qWarning() << "STUN failed to get WAN address:" << err;
|
|
}
|
|
}
|
|
|
|
if (!m_MdnsIpv6Address.isNull()) {
|
|
Q_ASSERT(QHostAddress(m_MdnsIpv6Address.address()).protocol() == QAbstractSocket::IPv6Protocol);
|
|
newComputer->ipv6Address = m_MdnsIpv6Address;
|
|
}
|
|
}
|
|
else {
|
|
newComputer->manualAddress = m_Address;
|
|
}
|
|
|
|
QHostAddress hostAddress(m_Address.address());
|
|
bool addressIsSiteLocalV4 =
|
|
hostAddress.isInSubnet(QHostAddress("10.0.0.0"), 8) ||
|
|
hostAddress.isInSubnet(QHostAddress("172.16.0.0"), 12) ||
|
|
hostAddress.isInSubnet(QHostAddress("192.168.0.0"), 16);
|
|
|
|
{
|
|
// Check if this PC already exists using opportunistic read lock
|
|
m_ComputerManager->m_Lock.lockForRead();
|
|
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
|
|
|
// If it doesn't already exist, convert to a write lock in preparation for updating.
|
|
//
|
|
// NB: ComputerManager's lock protects the host map itself, not the elements inside.
|
|
// Those are protected by their individual locks. Since we only mutate the map itself
|
|
// when the PC doesn't exist, we need the lock in write-mode for that case only.
|
|
if (existingComputer == nullptr) {
|
|
m_ComputerManager->m_Lock.unlock();
|
|
m_ComputerManager->m_Lock.lockForWrite();
|
|
|
|
// Since we had to unlock to lock for write, someone could have raced and added
|
|
// this PC before us. We have to check again whether it already exists.
|
|
existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
|
}
|
|
|
|
if (existingComputer != nullptr) {
|
|
// Fold it into the existing PC
|
|
bool changed = existingComputer->update(*newComputer);
|
|
delete newComputer;
|
|
|
|
// Drop the lock before notifying
|
|
m_ComputerManager->m_Lock.unlock();
|
|
|
|
// For non-mDNS clients, let them know it succeeded
|
|
if (!m_Mdns) {
|
|
emit computerAddCompleted(true, false);
|
|
}
|
|
|
|
// Tell our client if something changed
|
|
if (changed) {
|
|
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress.toString();
|
|
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
|
|
m_ComputerManager->m_Lock.unlock();
|
|
|
|
// If this wasn't added via mDNS but it is a RFC 1918 IPv4 address and not a VPN,
|
|
// go ahead and do the STUN request now to populate an external address.
|
|
if (!m_Mdns && addressIsSiteLocalV4 && newComputer->getActiveAddressReachability() != NvComputer::RI_VPN) {
|
|
quint32 addr;
|
|
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
|
if (err == 0) {
|
|
newComputer->setRemoteAddress(QHostAddress(qFromBigEndian(addr)));
|
|
}
|
|
else {
|
|
qWarning() << "STUN failed to get WAN address:" << err;
|
|
}
|
|
}
|
|
|
|
// For non-mDNS clients, let them know it succeeded
|
|
if (!m_Mdns) {
|
|
emit computerAddCompleted(true, false);
|
|
}
|
|
|
|
// Tell our client about this new PC
|
|
emit computerStateChanged(newComputer);
|
|
}
|
|
}
|
|
}
|
|
|
|
ComputerManager* m_ComputerManager;
|
|
NvAddress m_Address;
|
|
NvAddress m_MdnsIpv6Address;
|
|
bool m_Mdns;
|
|
bool m_AboutToQuit;
|
|
};
|
|
|
|
void ComputerManager::addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address)
|
|
{
|
|
// Punt to a worker thread to avoid stalling the
|
|
// UI while waiting for serverinfo query to complete
|
|
PendingAddTask* addTask = new PendingAddTask(this, address, mdnsIpv6Address, mdns);
|
|
QThreadPool::globalInstance()->start(addTask);
|
|
}
|
|
|
|
// TODO: Use QRandomGenerator when we drop Qt 5.9 support
|
|
QString ComputerManager::generatePinString()
|
|
{
|
|
std::uniform_int_distribution<int> dist(0, 9999);
|
|
std::random_device rd;
|
|
std::mt19937 engine(rd());
|
|
|
|
return QString::asprintf("%04u", dist(engine));
|
|
}
|
|
|
|
#include "computermanager.moc"
|