2018-06-24 22:13:37 +00:00
# include "computermanager.h"
2020-05-30 04:44:38 +00:00
# include "boxartmanager.h"
2018-06-24 22:13:37 +00:00
# include "nvhttp.h"
2018-09-09 19:33:19 +00:00
# include "settings/streamingpreferences.h"
2018-06-24 22:13:37 +00:00
2018-10-27 03:44:51 +00:00
# include <Limelight.h>
# include <QtEndian>
2018-06-24 22:13:37 +00:00
# include <QThread>
2018-07-06 06:12:55 +00:00
# include <QThreadPool>
2019-04-06 20:11:36 +00:00
# include <QCoreApplication>
2018-06-24 22:13:37 +00:00
2022-08-24 05:21:25 +00:00
# include <random>
2018-06-27 07:43:46 +00:00
# define SER_HOSTS "hosts"
2018-09-29 09:19:44 +00:00
class PcMonitorThread : public QThread
{
Q_OBJECT
2018-06-27 07:43:46 +00:00
2018-09-29 09:19:44 +00:00
# define TRIES_BEFORE_OFFLINING 2
# define POLLS_PER_APPLIST_FETCH 10
2018-06-27 07:43:46 +00:00
2018-09-29 09:19:44 +00:00
public :
PcMonitorThread ( NvComputer * computer )
: m_Computer ( computer )
{
setObjectName ( " Polling thread for " + computer - > name ) ;
2018-06-27 07:43:46 +00:00
}
2018-09-29 09:19:44 +00:00
private :
2021-07-03 04:34:54 +00:00
bool tryPollComputer ( NvAddress address , bool & changed )
2018-09-29 09:19:44 +00:00
{
2021-07-03 05:00:27 +00:00
NvHTTP http ( address , 0 , m_Computer - > serverCert ) ;
2018-06-27 07:43:46 +00:00
2018-09-29 09:19:44 +00:00
QString serverInfo ;
try {
2019-12-30 23:51:23 +00:00
serverInfo = http . getServerInfo ( NvHTTP : : NvLogLevel : : NVLL_NONE , true ) ;
2018-09-29 09:19:44 +00:00
} catch ( . . . ) {
return false ;
}
2018-07-06 03:54:31 +00:00
2021-07-02 22:14:48 +00:00
NvComputer newState ( http , serverInfo ) ;
2018-06-27 04:47:01 +00:00
2018-09-29 09:19:44 +00:00
// 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 ;
2018-06-27 04:47:01 +00:00
}
2018-09-29 09:19:44 +00:00
changed = m_Computer - > update ( newState ) ;
return true ;
2018-07-09 03:53:24 +00:00
}
2018-09-29 09:19:44 +00:00
bool updateAppList ( bool & changed )
{
2021-07-02 22:14:48 +00:00
NvHTTP http ( m_Computer ) ;
2018-07-04 05:11:21 +00:00
2018-09-29 09:19:44 +00:00
QVector < NvApp > appList ;
2018-07-04 05:11:21 +00:00
2018-09-29 09:19:44 +00:00
try {
appList = http . getAppList ( ) ;
if ( appList . isEmpty ( ) ) {
return false ;
}
} catch ( . . . ) {
return false ;
}
2018-07-04 05:11:21 +00:00
2018-09-29 09:19:44 +00:00
QWriteLocker lock ( & m_Computer - > lock ) ;
2020-08-02 04:06:01 +00:00
changed = m_Computer - > updateAppList ( appList ) ;
2018-09-29 09:19:44 +00:00
return true ;
}
2018-07-04 05:11:21 +00:00
2018-09-29 09:19:44 +00:00
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 ;
2019-12-30 23:51:23 +00:00
for ( int i = 0 ; i < ( wasOnline ? TRIES_BEFORE_OFFLINING : 1 ) & & ! online ; i + + ) {
2018-09-29 09:19:44 +00:00
for ( auto & address : m_Computer - > uniqueAddresses ( ) ) {
if ( isInterruptionRequested ( ) ) {
return ;
}
2018-07-04 05:11:21 +00:00
2018-09-29 09:19:44 +00:00
if ( tryPollComputer ( address , stateChanged ) ) {
if ( ! wasOnline ) {
2021-07-03 04:34:54 +00:00
qInfo ( ) < < m_Computer - > name < < " is now online at " < < m_Computer - > activeAddress . toString ( ) ;
2018-09-29 09:19:44 +00:00
}
online = true ;
break ;
2018-07-04 05:11:21 +00:00
}
}
}
2018-09-29 09:19:44 +00:00
// 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 ;
2018-07-04 05:11:21 +00:00
}
2018-09-29 09:19:44 +00:00
// 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 ;
}
2018-06-27 04:47:01 +00:00
2018-09-29 09:19:44 +00:00
if ( updateAppList ( stateChanged ) ) {
pollsSinceLastAppListFetch = 0 ;
}
}
2018-06-27 04:47:01 +00:00
2018-09-29 09:19:44 +00:00
if ( stateChanged ) {
// Tell anyone listening that we've changed state
emit computerStateChanged ( m_Computer ) ;
}
2018-06-27 04:47:01 +00:00
2019-03-17 19:23:13 +00:00
// 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 ) ;
}
2018-09-29 09:19:44 +00:00
}
2018-06-27 04:47:01 +00:00
}
2018-09-29 09:19:44 +00:00
signals :
void computerStateChanged ( NvComputer * computer ) ;
2018-06-27 04:47:01 +00:00
2018-09-29 09:19:44 +00:00
private :
NvComputer * m_Computer ;
} ;
2018-06-27 04:47:01 +00:00
2018-06-28 02:55:44 +00:00
ComputerManager : : ComputerManager ( QObject * parent )
: QObject ( parent ) ,
2018-07-09 05:07:20 +00:00
m_PollingRef ( 0 ) ,
2021-05-01 01:05:38 +00:00
m_MdnsBrowser ( nullptr ) ,
m_CompatFetcher ( nullptr )
2018-06-24 22:13:37 +00:00
{
2018-06-27 07:43:46 +00:00
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 ( ) ;
2019-04-06 20:11:36 +00:00
2021-05-01 01:05:38 +00:00
// Fetch latest compatibility data asynchronously
m_CompatFetcher . start ( ) ;
2019-04-06 20:11:36 +00:00
// 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.
2021-03-03 00:14:15 +00:00
connect ( QCoreApplication : : instance ( ) , & QCoreApplication : : aboutToQuit , this , & ComputerManager : : handleAboutToQuit ) ;
2018-06-27 07:43:46 +00:00
}
2018-07-19 04:27:43 +00:00
ComputerManager : : ~ ComputerManager ( )
{
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 ;
2019-01-20 03:16:09 +00:00
// Interrupt polling
for ( ComputerPollingEntry * entry : m_PollEntries ) {
entry - > interrupt ( ) ;
2018-07-19 04:27:43 +00:00
}
2019-01-20 03:16:09 +00:00
// Delete all polling entries (and associated threads)
for ( ComputerPollingEntry * entry : m_PollEntries ) {
delete entry ;
}
2018-07-19 04:27:43 +00:00
2019-01-20 03:16:09 +00:00
// Destroy all NvComputer objects now that polling is halted
for ( NvComputer * computer : m_KnownHosts ) {
delete computer ;
2018-07-19 04:27:43 +00:00
}
}
2018-06-27 07:43:46 +00:00
void ComputerManager : : saveHosts ( )
{
QSettings settings ;
2018-06-27 09:08:56 +00:00
QReadLocker lock ( & m_Lock ) ;
2018-06-24 22:13:37 +00:00
2018-06-27 07:43:46 +00:00
settings . remove ( SER_HOSTS ) ;
settings . beginWriteArray ( SER_HOSTS ) ;
2020-02-25 01:44:21 +00:00
int i = 0 ;
for ( const NvComputer * computer : m_KnownHosts ) {
settings . setArrayIndex ( i + + ) ;
computer - > serialize ( settings ) ;
2018-06-27 07:43:46 +00:00
}
settings . endArray ( ) ;
2018-06-24 22:13:37 +00:00
}
2019-07-14 22:28:50 +00:00
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 ( ) ;
}
2018-06-24 22:13:37 +00:00
void ComputerManager : : startPolling ( )
{
2018-06-27 04:47:01 +00:00
QWriteLocker lock ( & m_Lock ) ;
2018-06-24 22:13:37 +00:00
2018-07-09 05:07:20 +00:00
if ( + + m_PollingRef > 1 ) {
2018-07-01 06:07:31 +00:00
return ;
}
2018-09-09 19:33:19 +00:00
StreamingPreferences prefs ;
2018-07-01 06:07:31 +00:00
2018-09-09 19:33:19 +00:00
if ( prefs . enableMdns ) {
// Start an MDNS query for GameStream hosts
m_MdnsBrowser = new QMdnsEngine : : Browser ( & m_MdnsServer , " _nvstream._tcp.local. " , & m_MdnsCache ) ;
connect ( m_MdnsBrowser , & QMdnsEngine : : Browser : : serviceAdded ,
this , [ this ] ( const QMdnsEngine : : Service & service ) {
qInfo ( ) < < " Discovered mDNS host: " < < service . hostname ( ) ;
2019-08-05 01:04:07 +00:00
MdnsPendingComputer * pendingComputer = new MdnsPendingComputer ( & m_MdnsServer , service ) ;
2019-07-14 22:28:50 +00:00
connect ( pendingComputer , & MdnsPendingComputer : : resolvedHost ,
this , & ComputerManager : : handleMdnsServiceResolved ) ;
2018-09-09 19:33:19 +00:00
m_PendingResolution . append ( pendingComputer ) ;
} ) ;
}
else {
qWarning ( ) < < " mDNS is disabled by user preference " ;
}
2018-07-01 06:07:31 +00:00
// Start polling threads for each known host
2018-06-27 04:47:01 +00:00
QMapIterator < QString , NvComputer * > i ( m_KnownHosts ) ;
while ( i . hasNext ( ) ) {
i . next ( ) ;
startPollingComputer ( i . value ( ) ) ;
}
2018-06-24 22:13:37 +00:00
}
2018-09-29 09:19:44 +00:00
// Must hold m_Lock for write
void ComputerManager : : startPollingComputer ( NvComputer * computer )
{
if ( m_PollingRef = = 0 ) {
return ;
}
2019-01-20 03:16:09 +00:00
ComputerPollingEntry * pollingEntry ;
if ( ! m_PollEntries . contains ( computer - > uuid ) ) {
pollingEntry = m_PollEntries [ computer - > uuid ] = new ComputerPollingEntry ( ) ;
}
else {
pollingEntry = m_PollEntries [ computer - > uuid ] ;
2018-09-29 09:19:44 +00:00
}
2019-01-20 03:16:09 +00:00
if ( ! pollingEntry - > isActive ( ) ) {
PcMonitorThread * thread = new PcMonitorThread ( computer ) ;
2021-03-03 00:14:15 +00:00
connect ( thread , & PcMonitorThread : : computerStateChanged ,
this , & ComputerManager : : handleComputerStateChanged ) ;
2019-01-20 03:16:09 +00:00
pollingEntry - > setActiveThread ( thread ) ;
thread - > start ( ) ;
}
2018-09-29 09:19:44 +00:00
}
2018-07-01 06:07:31 +00:00
void ComputerManager : : handleMdnsServiceResolved ( MdnsPendingComputer * computer ,
2019-07-14 22:28:50 +00:00
QVector < QHostAddress > & addresses )
2018-07-01 06:07:31 +00:00
{
2019-07-14 22:28:50 +00:00
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.
2021-07-03 04:34:54 +00:00
addNewHost ( NvAddress ( address , computer - > port ( ) ) , true , NvAddress ( v6Global , computer - > port ( ) ) ) ;
2019-07-14 22:28:50 +00:00
added = true ;
break ;
}
}
2018-07-01 06:07:31 +00:00
2019-07-14 22:28:50 +00:00
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 ) ) {
2021-07-03 04:34:54 +00:00
addNewHost ( NvAddress ( address , computer - > port ( ) ) , true , NvAddress ( v6Global , computer - > port ( ) ) ) ;
2019-07-14 22:28:50 +00:00
break ;
}
}
}
}
2018-07-01 06:07:31 +00:00
m_PendingResolution . removeOne ( computer ) ;
computer - > deleteLater ( ) ;
}
2018-09-29 09:19:44 +00:00
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 ( ) ;
}
2018-06-28 05:16:57 +00:00
QVector < NvComputer * > ComputerManager : : getComputers ( )
{
QReadLocker lock ( & m_Lock ) ;
return QVector < NvComputer * > : : fromList ( m_KnownHosts . values ( ) ) ;
}
2018-07-06 06:12:55 +00:00
class DeferredHostDeletionTask : public QRunnable
2018-06-28 05:16:57 +00:00
{
2018-07-06 06:12:55 +00:00
public :
DeferredHostDeletionTask ( ComputerManager * cm , NvComputer * computer )
: m_Computer ( computer ) ,
m_ComputerManager ( cm ) { }
void run ( )
{
2019-01-20 03:16:09 +00:00
ComputerPollingEntry * pollingEntry ;
2018-07-06 06:12:55 +00:00
2018-09-22 03:20:16 +00:00
// 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 ) ;
2019-01-20 03:16:09 +00:00
pollingEntry = m_ComputerManager - > m_PollEntries . take ( m_Computer - > uuid ) ;
2018-09-22 03:20:16 +00:00
m_ComputerManager - > m_KnownHosts . remove ( m_Computer - > uuid ) ;
}
// Persist the new host list
m_ComputerManager - > saveHosts ( ) ;
2019-01-20 03:16:09 +00:00
// Delete the polling entry first. This will stop all polling threads too.
delete pollingEntry ;
2018-06-28 05:16:57 +00:00
2020-05-30 04:44:38 +00:00
// Delete cached box art
BoxArtManager : : deleteBoxArt ( m_Computer ) ;
2018-09-22 03:20:16 +00:00
// Finally, delete the computer itself. This must be done
// last because the polling thread might be using it.
2018-07-06 06:12:55 +00:00
delete m_Computer ;
2018-06-28 05:16:57 +00:00
}
2018-07-06 06:12:55 +00:00
private :
NvComputer * m_Computer ;
ComputerManager * m_ComputerManager ;
} ;
2018-06-28 05:16:57 +00:00
2018-07-06 06:12:55 +00:00
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 ) ) ;
}
2020-05-02 01:34:15 +00:00
void ComputerManager : : renameHost ( NvComputer * computer , QString name )
{
{
2021-03-03 00:14:15 +00:00
QWriteLocker lock ( & computer - > lock ) ;
2020-05-02 01:34:15 +00:00
computer - > name = name ;
computer - > hasCustomName = true ;
}
// Notify the UI of the state change
handleComputerStateChanged ( computer ) ;
}
2020-08-02 04:06:01 +00:00
void ComputerManager : : clientSideAttributeUpdated ( NvComputer * computer )
{
// Persist the change
saveHosts ( ) ;
// Notify the UI of the state change
handleComputerStateChanged ( computer ) ;
}
2019-04-06 20:11:36 +00:00
void ComputerManager : : handleAboutToQuit ( )
{
2022-10-25 04:55:10 +00:00
QReadLocker lock ( & m_Lock ) ;
2019-04-06 20:11:36 +00:00
// Interrupt polling threads immediately, so they
// avoid making additional requests while quitting
for ( ComputerPollingEntry * entry : m_PollEntries ) {
entry - > interrupt ( ) ;
}
}
2018-09-29 09:19:44 +00:00
class PendingPairingTask : public QObject , public QRunnable
{
Q_OBJECT
public :
PendingPairingTask ( ComputerManager * computerManager , NvComputer * computer , QString pin )
2022-08-24 05:20:02 +00:00
: m_ComputerManager ( computerManager ) ,
m_Computer ( computer ) ,
2018-09-29 09:19:44 +00:00
m_Pin ( pin )
{
connect ( this , & PendingPairingTask : : pairingCompleted ,
computerManager , & ComputerManager : : pairingCompleted ) ;
}
signals :
void pairingCompleted ( NvComputer * computer , QString error ) ;
private :
void run ( )
{
2021-07-02 22:14:48 +00:00
NvPairingManager pairingManager ( m_Computer ) ;
2018-09-29 09:19:44 +00:00
try {
2018-12-22 02:08:07 +00:00
NvPairingManager : : PairState result = pairingManager . pair ( m_Computer - > appVersion , m_Pin , m_Computer - > serverCert ) ;
2018-09-29 09:19:44 +00:00
switch ( result )
{
case NvPairingManager : : PairState : : PIN_WRONG :
2022-10-13 03:20:15 +00:00
emit pairingCompleted ( m_Computer , tr ( " The PIN from the PC didn't match. Please try again. " ) ) ;
2018-09-29 09:19:44 +00:00
break ;
case NvPairingManager : : PairState : : FAILED :
2022-10-13 03:15:58 +00:00
if ( m_Computer - > currentGameId ! = 0 ) {
2022-10-13 03:20:15 +00:00
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. " ) ) ;
2022-10-13 03:15:58 +00:00
}
else {
2022-10-13 03:20:15 +00:00
emit pairingCompleted ( m_Computer , tr ( " Pairing failed. Please try again. " ) ) ;
2022-10-13 03:15:58 +00:00
}
2018-09-29 09:19:44 +00:00
break ;
case NvPairingManager : : PairState : : ALREADY_IN_PROGRESS :
2022-10-13 03:20:15 +00:00
emit pairingCompleted ( m_Computer , tr ( " Another pairing attempt is already in progress. " ) ) ;
2018-09-29 09:19:44 +00:00
break ;
case NvPairingManager : : PairState : : PAIRED :
2022-08-24 05:20:02 +00:00
// Persist the newly pinned server certificate for this host
m_ComputerManager - > saveHosts ( ) ;
2018-09-29 09:19:44 +00:00
emit pairingCompleted ( m_Computer , nullptr ) ;
break ;
}
} catch ( const GfeHttpResponseException & e ) {
2022-10-13 03:20:15 +00:00
emit pairingCompleted ( m_Computer , tr ( " GeForce Experience returned error: %1 " ) . arg ( e . toQString ( ) ) ) ;
2018-09-29 10:01:49 +00:00
} catch ( const QtNetworkReplyException & e ) {
emit pairingCompleted ( m_Computer , e . toQString ( ) ) ;
2018-09-29 09:19:44 +00:00
}
}
2022-08-24 05:20:02 +00:00
ComputerManager * m_ComputerManager ;
2018-09-29 09:19:44 +00:00
NvComputer * m_Computer ;
QString m_Pin ;
} ;
2018-07-06 06:12:55 +00:00
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 ) ;
2018-06-28 05:16:57 +00:00
}
2018-09-29 09:19:44 +00:00
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 ( )
{
2021-07-02 22:14:48 +00:00
NvHTTP http ( m_Computer ) ;
2018-09-29 09:19:44 +00:00
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
2020-11-21 19:15:54 +00:00
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. " ) ) ;
2018-09-29 09:19:44 +00:00
}
else {
emit quitAppFailed ( e . toQString ( ) ) ;
}
2018-09-29 10:01:49 +00:00
} catch ( const QtNetworkReplyException & e ) {
{
QWriteLocker lock ( & m_Computer - > lock ) ;
m_Computer - > pendingQuit = false ;
}
emit quitAppFailed ( e . toQString ( ) ) ;
2018-09-29 09:19:44 +00:00
}
}
NvComputer * m_Computer ;
} ;
2018-08-01 05:21:39 +00:00
void ComputerManager : : quitRunningApp ( NvComputer * computer )
{
QWriteLocker lock ( & computer - > lock ) ;
computer - > pendingQuit = true ;
PendingQuitTask * quit = new PendingQuitTask ( this , computer ) ;
QThreadPool : : globalInstance ( ) - > start ( quit ) ;
}
2018-06-27 04:47:01 +00:00
void ComputerManager : : stopPollingAsync ( )
2018-06-24 22:13:37 +00:00
{
2018-06-27 04:47:01 +00:00
QWriteLocker lock ( & m_Lock ) ;
2018-06-24 22:13:37 +00:00
2018-07-09 05:07:20 +00:00
Q_ASSERT ( m_PollingRef > 0 ) ;
if ( - - m_PollingRef > 0 ) {
2018-07-01 06:07:31 +00:00
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 to stop discovery
delete m_MdnsBrowser ;
m_MdnsBrowser = nullptr ;
2018-06-27 04:47:01 +00:00
// Interrupt all threads, but don't wait for them to terminate
2019-01-20 03:16:09 +00:00
for ( ComputerPollingEntry * entry : m_PollEntries ) {
entry - > interrupt ( ) ;
2018-06-24 22:13:37 +00:00
}
2018-06-27 04:47:01 +00:00
}
2018-06-24 22:13:37 +00:00
2021-07-03 04:34:54 +00:00
void ComputerManager : : addNewHostManually ( QString address )
{
QUrl url = QUrl : : fromUserInput ( address ) ;
2021-07-03 04:54:17 +00:00
if ( url . isValid ( ) & & ! url . host ( ) . isEmpty ( ) ) {
2021-07-03 04:34:54 +00:00
// If there wasn't a port specified, use the default
2021-07-03 05:00:27 +00:00
addNewHost ( NvAddress ( url . host ( ) , url . port ( DEFAULT_HTTP_PORT ) ) , false ) ;
2021-07-03 04:34:54 +00:00
}
else {
emit computerAddCompleted ( false , false ) ;
}
}
2018-09-29 09:19:44 +00:00
class PendingAddTask : public QObject , public QRunnable
2018-06-27 04:47:01 +00:00
{
2018-09-29 09:19:44 +00:00
Q_OBJECT
2018-06-27 09:08:56 +00:00
2018-09-29 09:19:44 +00:00
public :
2021-07-03 04:34:54 +00:00
PendingAddTask ( ComputerManager * computerManager , NvAddress address , NvAddress mdnsIpv6Address , bool mdns )
2018-09-29 09:19:44 +00:00
: m_ComputerManager ( computerManager ) ,
m_Address ( address ) ,
2019-07-14 22:28:50 +00:00
m_MdnsIpv6Address ( mdnsIpv6Address ) ,
2022-10-25 04:46:57 +00:00
m_Mdns ( mdns ) ,
m_AboutToQuit ( false )
2018-09-29 09:19:44 +00:00
{
connect ( this , & PendingAddTask : : computerAddCompleted ,
computerManager , & ComputerManager : : computerAddCompleted ) ;
connect ( this , & PendingAddTask : : computerStateChanged ,
computerManager , & ComputerManager : : handleComputerStateChanged ) ;
2022-10-25 04:46:57 +00:00
connect ( QCoreApplication : : instance ( ) , & QCoreApplication : : aboutToQuit ,
this , & PendingAddTask : : handleAboutToQuit ) ;
2018-08-01 05:21:39 +00:00
}
2018-09-29 09:19:44 +00:00
signals :
2020-08-09 01:11:25 +00:00
void computerAddCompleted ( QVariant success , QVariant detectedPortBlocking ) ;
2018-06-27 04:47:01 +00:00
2018-09-29 09:19:44 +00:00
void computerStateChanged ( NvComputer * computer ) ;
2018-06-24 22:13:37 +00:00
2018-09-29 09:19:44 +00:00
private :
2022-10-25 04:46:57 +00:00
void handleAboutToQuit ( )
{
m_AboutToQuit = true ;
}
2018-12-23 03:55:28 +00:00
QString fetchServerInfo ( NvHTTP & http )
2018-09-29 09:19:44 +00:00
{
QString serverInfo ;
2022-10-25 04:46:57 +00:00
// Do nothing if we're quitting
if ( m_AboutToQuit ) {
return QString ( ) ;
}
2018-09-29 09:19:44 +00:00
try {
2018-09-29 10:01:49 +00:00
// 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 {
2019-01-20 05:32:35 +00:00
serverInfo = http . getServerInfo ( NvHTTP : : NVLL_VERBOSE ) ;
2018-09-29 10:01:49 +00:00
} catch ( const QtNetworkReplyException & e ) {
if ( e . getError ( ) = = QNetworkReply : : ServiceUnavailableError ) {
qWarning ( ) < < " Retrying request in 5 seconds after ServiceUnavailableError " ;
QThread : : sleep ( 5 ) ;
2019-01-20 05:32:35 +00:00
serverInfo = http . getServerInfo ( NvHTTP : : NVLL_VERBOSE ) ;
2018-09-29 10:01:49 +00:00
qInfo ( ) < < " Retry successful " ;
}
else {
// Rethrow other errors
throw e ;
}
}
2018-12-23 03:55:28 +00:00
return serverInfo ;
2018-09-29 09:19:44 +00:00
} catch ( . . . ) {
if ( ! m_Mdns ) {
2020-08-09 01:29:36 +00:00
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 ,
2020-08-09 01:11:25 +00:00
ML_PORT_FLAG_TCP_47984 | ML_PORT_FLAG_TCP_47989 ) ;
2020-08-09 01:29:36 +00:00
}
else {
portTestResult = 0 ;
}
2020-08-09 01:11:25 +00:00
emit computerAddCompleted ( false , portTestResult ! = 0 & & portTestResult ! = ML_TEST_RESULT_INCONCLUSIVE ) ;
2018-09-29 09:19:44 +00:00
}
2018-12-23 03:55:28 +00:00
return QString ( ) ;
}
}
void run ( )
{
2021-07-03 05:00:27 +00:00
NvHTTP http ( m_Address , 0 , QSslCertificate ( ) ) ;
2018-12-23 03:55:28 +00:00
2021-07-03 04:34:54 +00:00
qInfo ( ) < < " Processing new PC at " < < m_Address . toString ( ) < < " from " < < ( m_Mdns ? " mDNS " : " user " ) < < " with IPv6 address " < < m_MdnsIpv6Address . toString ( ) ;
2018-12-23 03:55:28 +00:00
// Perform initial serverinfo fetch over HTTP since we don't know which cert to use
QString serverInfo = fetchServerInfo ( http ) ;
2019-08-01 05:07:20 +00:00
if ( serverInfo . isEmpty ( ) & & ! m_MdnsIpv6Address . isNull ( ) ) {
// Retry using the global IPv6 address if the IPv4 or link-local IPv6 address fails
2021-07-03 04:34:54 +00:00
http . setAddress ( m_MdnsIpv6Address ) ;
2019-08-01 05:07:20 +00:00
serverInfo = fetchServerInfo ( http ) ;
}
2018-12-23 03:55:28 +00:00
if ( serverInfo . isEmpty ( ) ) {
2018-09-29 09:19:44 +00:00
return ;
}
2018-12-23 03:55:28 +00:00
// Create initial newComputer using HTTP serverinfo with no pinned cert
2021-07-02 22:14:48 +00:00
NvComputer * newComputer = new NvComputer ( http , serverInfo ) ;
2018-09-29 09:19:44 +00:00
2018-12-23 03:55:28 +00:00
// Check if we have a record of this host UUID to pull the pinned cert
NvComputer * existingComputer ;
{
QReadLocker lock ( & m_ComputerManager - > m_Lock ) ;
2018-12-26 08:59:30 +00:00
existingComputer = m_ComputerManager - > m_KnownHosts . value ( newComputer - > uuid ) ;
2018-12-23 03:55:28 +00:00
if ( existingComputer ! = nullptr ) {
http . setServerCert ( existingComputer - > serverCert ) ;
}
}
// Fetch serverinfo again over HTTPS with the pinned cert
if ( existingComputer ! = nullptr ) {
2021-07-03 05:00:27 +00:00
Q_ASSERT ( http . httpsPort ( ) ! = 0 ) ;
2018-12-23 03:55:28 +00:00
serverInfo = fetchServerInfo ( http ) ;
if ( serverInfo . isEmpty ( ) ) {
return ;
}
// Update the polled computer with the HTTPS information
2021-07-02 22:14:48 +00:00
NvComputer httpsComputer ( http , serverInfo ) ;
2018-12-23 03:55:28 +00:00
newComputer - > update ( httpsComputer ) ;
}
2018-09-29 09:19:44 +00:00
// Update addresses depending on the context
if ( m_Mdns ) {
2019-08-01 05:15:29 +00:00
// 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 ;
}
2018-10-27 03:44:51 +00:00
2019-07-15 08:07:31 +00:00
// Get the WAN IP address using STUN if we're on mDNS over IPv4
2021-07-03 04:34:54 +00:00
if ( QHostAddress ( newComputer - > localAddress . address ( ) ) . protocol ( ) = = QAbstractSocket : : IPv4Protocol ) {
2019-07-15 08:07:31 +00:00
quint32 addr ;
int err = LiFindExternalAddressIP4 ( " stun.moonlight-stream.org " , 3478 , & addr ) ;
if ( err = = 0 ) {
2021-07-03 04:34:54 +00:00
newComputer - > setRemoteAddress ( QHostAddress ( qFromBigEndian ( addr ) ) ) ;
2019-07-15 08:07:31 +00:00
}
else {
qWarning ( ) < < " STUN failed to get WAN address: " < < err ;
}
2018-10-27 03:54:17 +00:00
}
2019-07-14 22:28:50 +00:00
if ( ! m_MdnsIpv6Address . isNull ( ) ) {
2021-07-03 04:34:54 +00:00
Q_ASSERT ( QHostAddress ( m_MdnsIpv6Address . address ( ) ) . protocol ( ) = = QAbstractSocket : : IPv6Protocol ) ;
newComputer - > ipv6Address = m_MdnsIpv6Address ;
2019-07-14 22:28:50 +00:00
}
2018-09-29 09:19:44 +00:00
}
else {
newComputer - > manualAddress = m_Address ;
}
2021-07-03 04:34:54 +00:00
QHostAddress hostAddress ( m_Address . address ( ) ) ;
2020-08-31 02:03:55 +00:00
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 ) ;
2018-12-23 03:55:28 +00:00
{
2022-10-25 04:55:10 +00:00
// Check if this PC already exists using opportunistic read lock
m_ComputerManager - > m_Lock . lockForRead ( ) ;
2018-12-26 08:59:30 +00:00
NvComputer * existingComputer = m_ComputerManager - > m_KnownHosts . value ( newComputer - > uuid ) ;
2022-10-25 04:55:10 +00:00
// 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 ) ;
}
2018-12-23 03:55:28 +00:00
if ( existingComputer ! = nullptr ) {
// Fold it into the existing PC
bool changed = existingComputer - > update ( * newComputer ) ;
delete newComputer ;
// Drop the lock before notifying
2022-10-25 04:55:10 +00:00
m_ComputerManager - > m_Lock . unlock ( ) ;
2018-12-23 03:55:28 +00:00
// For non-mDNS clients, let them know it succeeded
if ( ! m_Mdns ) {
2020-08-09 01:11:25 +00:00
emit computerAddCompleted ( true , false ) ;
2018-12-23 03:55:28 +00:00
}
2018-09-29 09:19:44 +00:00
2018-12-23 03:55:28 +00:00
// Tell our client if something changed
if ( changed ) {
2021-07-03 04:34:54 +00:00
qInfo ( ) < < existingComputer - > name < < " is now at " < < existingComputer - > activeAddress . toString ( ) ;
2018-12-23 03:55:28 +00:00
emit computerStateChanged ( existingComputer ) ;
}
2018-09-29 09:19:44 +00:00
}
2018-12-23 03:55:28 +00:00
else {
// Store this in our active sets
m_ComputerManager - > m_KnownHosts [ newComputer - > uuid ] = newComputer ;
2018-09-29 09:19:44 +00:00
2018-12-23 03:55:28 +00:00
// Start polling if enabled (write lock required)
m_ComputerManager - > startPollingComputer ( newComputer ) ;
2018-09-29 09:19:44 +00:00
2018-12-23 03:55:28 +00:00
// Drop the lock before notifying
2022-10-25 04:55:10 +00:00
m_ComputerManager - > m_Lock . unlock ( ) ;
2018-09-29 09:19:44 +00:00
2020-09-03 05:56:59 +00:00
// If this wasn't added via mDNS but it is a RFC 1918 IPv4 address and not a VPN,
2020-08-31 02:03:55 +00:00
// go ahead and do the STUN request now to populate an external address.
2020-09-03 05:56:59 +00:00
if ( ! m_Mdns & & addressIsSiteLocalV4 & & ! newComputer - > isReachableOverVpn ( ) ) {
2020-08-31 02:03:55 +00:00
quint32 addr ;
int err = LiFindExternalAddressIP4 ( " stun.moonlight-stream.org " , 3478 , & addr ) ;
if ( err = = 0 ) {
2021-07-03 04:34:54 +00:00
newComputer - > setRemoteAddress ( QHostAddress ( qFromBigEndian ( addr ) ) ) ;
2020-08-31 02:03:55 +00:00
}
else {
qWarning ( ) < < " STUN failed to get WAN address: " < < err ;
}
}
2018-12-23 03:55:28 +00:00
// For non-mDNS clients, let them know it succeeded
if ( ! m_Mdns ) {
2020-08-09 01:11:25 +00:00
emit computerAddCompleted ( true , false ) ;
2018-12-23 03:55:28 +00:00
}
2018-09-29 09:19:44 +00:00
2018-12-23 03:55:28 +00:00
// Tell our client about this new PC
emit computerStateChanged ( newComputer ) ;
2018-09-29 09:19:44 +00:00
}
}
2018-06-24 22:13:37 +00:00
}
2018-09-29 09:19:44 +00:00
ComputerManager * m_ComputerManager ;
2021-07-03 04:34:54 +00:00
NvAddress m_Address ;
NvAddress m_MdnsIpv6Address ;
2018-09-29 09:19:44 +00:00
bool m_Mdns ;
2022-10-25 04:46:57 +00:00
bool m_AboutToQuit ;
2018-09-29 09:19:44 +00:00
} ;
2021-07-03 04:34:54 +00:00
void ComputerManager : : addNewHost ( NvAddress address , bool mdns , NvAddress mdnsIpv6Address )
2018-09-29 09:19:44 +00:00
{
// Punt to a worker thread to avoid stalling the
// UI while waiting for serverinfo query to complete
2019-07-14 22:28:50 +00:00
PendingAddTask * addTask = new PendingAddTask ( this , address , mdnsIpv6Address , mdns ) ;
2018-09-29 09:19:44 +00:00
QThreadPool : : globalInstance ( ) - > start ( addTask ) ;
2018-06-27 04:47:01 +00:00
}
2018-06-24 22:13:37 +00:00
2022-08-24 05:21:25 +00:00
// 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 ) ) ;
}
2018-09-29 09:19:44 +00:00
# include "computermanager.moc"