From 0ab07303c91d07db72a871e5d3e56a4270e188f8 Mon Sep 17 00:00:00 2001 From: Janne Hakonen Date: Thu, 6 Dec 2018 04:45:28 +0200 Subject: [PATCH] Add quit cli command and app quit option after stream session. Fixes #92 (#138) * Add quit cli command and app quit option after stream session. Fixes #92 * Code review fixes. --- app/app.pro | 4 + app/backend/computermanager.cpp | 6 + app/backend/computermanager.h | 3 + app/backend/computerseeker.cpp | 59 +++++++++ app/backend/computerseeker.h | 33 +++++ app/backend/nvhttp.cpp | 7 +- app/cli/commandlineparser.cpp | 50 +++++++- app/cli/commandlineparser.h | 15 +++ app/cli/quitstream.cpp | 171 ++++++++++++++++++++++++++ app/cli/quitstream.h | 40 ++++++ app/cli/startstream.cpp | 130 +++++++++++++------- app/cli/startstream.h | 6 + app/gui/CliQuitStreamSegue.qml | 70 +++++++++++ app/gui/CliStartStreamSegue.qml | 49 +++++++- app/gui/SettingsView.qml | 10 ++ app/gui/StreamSegue.qml | 57 ++++++--- app/main.cpp | 14 +++ app/qml.qrc | 1 + app/settings/streamingpreferences.cpp | 3 + app/settings/streamingpreferences.h | 3 + app/streaming/session.cpp | 10 ++ app/streaming/session.h | 4 + 22 files changed, 678 insertions(+), 67 deletions(-) create mode 100644 app/backend/computerseeker.cpp create mode 100644 app/backend/computerseeker.h create mode 100644 app/cli/quitstream.cpp create mode 100644 app/cli/quitstream.h create mode 100644 app/gui/CliQuitStreamSegue.qml diff --git a/app/app.pro b/app/app.pro index 70df8caf..a942a0d9 100644 --- a/app/app.pro +++ b/app/app.pro @@ -100,6 +100,7 @@ macx { SOURCES += \ main.cpp \ + backend/computerseeker.cpp \ backend/identitymanager.cpp \ backend/nvcomputer.cpp \ backend/nvhttp.cpp \ @@ -107,6 +108,7 @@ SOURCES += \ backend/computermanager.cpp \ backend/boxartmanager.cpp \ cli/commandlineparser.cpp \ + cli/quitstream.cpp \ cli/startstream.cpp \ settings/streamingpreferences.cpp \ streaming/input.cpp \ @@ -123,6 +125,7 @@ SOURCES += \ HEADERS += \ utils.h \ + backend/computerseeker.h \ backend/identitymanager.h \ backend/nvcomputer.h \ backend/nvhttp.h \ @@ -130,6 +133,7 @@ HEADERS += \ backend/computermanager.h \ backend/boxartmanager.h \ cli/commandlineparser.h \ + cli/quitstream.h \ cli/startstream.h \ settings/streamingpreferences.h \ streaming/input.h \ diff --git a/app/backend/computermanager.cpp b/app/backend/computermanager.cpp index 4b1970f0..2d0bdf3f 100644 --- a/app/backend/computermanager.cpp +++ b/app/backend/computermanager.cpp @@ -1,6 +1,7 @@ #include "computermanager.h" #include "nvhttp.h" #include "settings/streamingpreferences.h" +#include "streaming/session.h" #include #include @@ -478,6 +479,11 @@ void ComputerManager::quitRunningApp(NvComputer* computer) QThreadPool::globalInstance()->start(quit); } +void ComputerManager::quitRunningApp(Session *session) +{ + quitRunningApp(session->getComputer()); +} + void ComputerManager::stopPollingAsync() { QWriteLocker lock(&m_Lock); diff --git a/app/backend/computermanager.h b/app/backend/computermanager.h index 18f6621a..2be9a95a 100644 --- a/app/backend/computermanager.h +++ b/app/backend/computermanager.h @@ -14,6 +14,8 @@ #include #include +class Session; + class MdnsPendingComputer : public QObject { Q_OBJECT @@ -72,6 +74,7 @@ public: void pairHost(NvComputer* computer, QString pin); void quitRunningApp(NvComputer* computer); + Q_INVOKABLE void quitRunningApp(Session* session); QVector getComputers(); diff --git a/app/backend/computerseeker.cpp b/app/backend/computerseeker.cpp new file mode 100644 index 00000000..58771ff1 --- /dev/null +++ b/app/backend/computerseeker.cpp @@ -0,0 +1,59 @@ +#include "computerseeker.h" +#include "computermanager.h" +#include + +ComputerSeeker::ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent) + : QObject(parent), m_ComputerManager(manager), m_ComputerName(computerName), + m_TimeoutTimer(new QTimer(this)) +{ + m_TimeoutTimer->setSingleShot(true); + connect(m_TimeoutTimer, &QTimer::timeout, + this, &ComputerSeeker::onTimeout); + connect(m_ComputerManager, &ComputerManager::computerStateChanged, + this, &ComputerSeeker::onComputerUpdated); +} + +void ComputerSeeker::start(int timeout) +{ + m_TimeoutTimer->start(timeout); + // Seek desired computer by both connecting to it directly (this may fail + // if m_ComputerName is UUID, or the name that doesn't resolve to an IP + // address) and by polling it using mDNS, hopefully one of these methods + // would find the host + m_ComputerManager->addNewHost(m_ComputerName, false); + m_ComputerManager->startPolling(); +} + +void ComputerSeeker::onComputerUpdated(NvComputer *computer) +{ + if (!m_TimeoutTimer->isActive()) { + return; + } + if (matchComputer(computer) && isOnline(computer)) { + m_ComputerManager->stopPollingAsync(); + m_TimeoutTimer->stop(); + emit computerFound(computer); + } +} + +bool ComputerSeeker::matchComputer(NvComputer *computer) const +{ + QString value = m_ComputerName.toLower(); + return computer->name.toLower() == value || + computer->localAddress.toLower() == value || + computer->remoteAddress.toLower() == value || + computer->manualAddress.toLower() == value || + computer->uuid.toLower() == value; +} + +bool ComputerSeeker::isOnline(NvComputer *computer) const +{ + return computer->state == NvComputer::CS_ONLINE; +} + +void ComputerSeeker::onTimeout() +{ + m_TimeoutTimer->stop(); + m_ComputerManager->stopPollingAsync(); + emit errorTimeout(); +} diff --git a/app/backend/computerseeker.h b/app/backend/computerseeker.h new file mode 100644 index 00000000..8f5c19ae --- /dev/null +++ b/app/backend/computerseeker.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +class ComputerManager; +class NvComputer; +class QTimer; + +class ComputerSeeker : public QObject +{ + Q_OBJECT +public: + explicit ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent = nullptr); + + void start(int timeout); + +signals: + void computerFound(NvComputer *computer); + void errorTimeout(); + +private slots: + void onComputerUpdated(NvComputer *computer); + void onTimeout(); + +private: + bool matchComputer(NvComputer *computer) const; + bool isOnline(NvComputer *computer) const; + +private: + ComputerManager *m_ComputerManager; + QString m_ComputerName; + QTimer *m_TimeoutTimer; +}; diff --git a/app/backend/nvhttp.cpp b/app/backend/nvhttp.cpp index 3cb713ea..248862f3 100644 --- a/app/backend/nvhttp.cpp +++ b/app/backend/nvhttp.cpp @@ -57,8 +57,13 @@ NvHTTP::getCurrentGame(QString serverInfo) // GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer // has the semantics that its name would indicate. To contain the effects of this change as much // as possible, we'll force the current game to zero if the server isn't in a streaming session. + // + // However, current game info must be available also in other states than just _SERVER_BUSY as + // it is required for quitting currently running app. Quitting app occurs at end of stream if + // configured so. At that point the server state may be in some other state than _SERVER_BUSY + // for a short while, but that must not prevent quitting of the app. QString serverState = getXmlString(serverInfo, "state"); - if (serverState != nullptr && serverState.endsWith("_SERVER_BUSY")) + if (serverState != nullptr && !serverState.endsWith("_SERVER_AVAILABLE")) { return getXmlString(serverInfo, "currentgame").toInt(); } diff --git a/app/cli/commandlineparser.cpp b/app/cli/commandlineparser.cpp index e6ee1342..7808eb80 100644 --- a/app/cli/commandlineparser.cpp +++ b/app/cli/commandlineparser.cpp @@ -164,6 +164,7 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin "Starts Moonlight normally if no arguments are given.\n" "\n" "Available actions:\n" + " quit Quit the currently running app\n" " stream Start streaming an app\n" "\n" "See 'moonlight --help' for help of specific action." @@ -179,6 +180,8 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin parser.handleHelpAndVersionOptions(); parser.handleUnknownOptions(); return NormalStartRequested; + } else if (action == "quit") { + return QuitRequested; } else if (action == "stream") { return StreamRequested; } else { @@ -186,6 +189,48 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin } } +QuitCommandLineParser::QuitCommandLineParser() +{ +} + +QuitCommandLineParser::~QuitCommandLineParser() +{ +} + +void QuitCommandLineParser::parse(const QStringList &args) +{ + CommandLineParser parser; + parser.setupCommonOptions(); + parser.setApplicationDescription( + "\n" + "Quit the currently running app on the given host." + ); + parser.addPositionalArgument("quit", "quit running app"); + parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", ""); + + if (!parser.parse(args)) { + parser.showError(parser.errorText()); + } + + parser.handleUnknownOptions(); + + // This method will not return and terminates the process if --version or + // --help is specified + parser.handleHelpAndVersionOptions(); + + // Verify that host has been provided + auto posArgs = parser.positionalArguments(); + if (posArgs.length() < 2) { + parser.showError("Host not provided"); + } + m_Host = parser.positionalArguments().at(1); +} + +QString QuitCommandLineParser::getHost() const +{ + return m_Host; +} + StreamCommandLineParser::StreamCommandLineParser() { m_WindowModeMap = { @@ -238,6 +283,7 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys()); parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys()); parser.addToggleOption("multi-controller", "multiple controller support"); + parser.addToggleOption("quit-after", "quit app after session"); parser.addToggleOption("mouse-acceleration", "mouse acceleration"); parser.addToggleOption("game-optimization", "game optimizations"); parser.addToggleOption("audio-on-host", "audio on host PC"); @@ -314,6 +360,9 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference // Resolve --multi-controller and --no-multi-controller options preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController); + // Resolve --quit-after and --no-quit-after options + preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter); + // Resolve --mouse-acceleration and --no-mouse-acceleration options preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration); @@ -359,4 +408,3 @@ QString StreamCommandLineParser::getAppName() const { return m_AppName; } - diff --git a/app/cli/commandlineparser.h b/app/cli/commandlineparser.h index 7f7c3c94..4f76d2cf 100644 --- a/app/cli/commandlineparser.h +++ b/app/cli/commandlineparser.h @@ -11,6 +11,7 @@ public: enum ParseResult { NormalStartRequested, StreamRequested, + QuitRequested, }; GlobalCommandLineParser(); @@ -20,6 +21,20 @@ public: }; +class QuitCommandLineParser +{ +public: + QuitCommandLineParser(); + virtual ~QuitCommandLineParser(); + + void parse(const QStringList &args); + + QString getHost() const; + +private: + QString m_Host; +}; + class StreamCommandLineParser { public: diff --git a/app/cli/quitstream.cpp b/app/cli/quitstream.cpp new file mode 100644 index 00000000..04081b9c --- /dev/null +++ b/app/cli/quitstream.cpp @@ -0,0 +1,171 @@ +#include "quitstream.h" + +#include "backend/computermanager.h" +#include "backend/computerseeker.h" +#include "streaming/session.h" + +#include +#include + +#define COMPUTER_SEEK_TIMEOUT 10000 + +namespace CliQuitStream +{ + +enum State { + StateInit, + StateSeekComputer, + StateQuitApp, + StateFailure, +}; + +class Event +{ +public: + enum Type { + AppQuitCompleted, + ComputerFound, + ComputerSeekTimedout, + Executed, + }; + + Event(Type type) + : type(type), computerManager(nullptr), computer(nullptr) {} + + Type type; + ComputerManager *computerManager; + NvComputer *computer; + QString errorMessage; +}; + +class LauncherPrivate +{ + Q_DECLARE_PUBLIC(Launcher) + +public: + LauncherPrivate(Launcher *q) : q_ptr(q) {} + + void handleEvent(Event event) + { + Q_Q(Launcher); + NvApp app; + + switch (event.type) { + // Occurs when CliQuitStreamSegue becomes visible and the UI calls launcher's execute() + case Event::Executed: + if (m_State == StateInit) { + m_State = StateSeekComputer; + m_ComputerManager = event.computerManager; + q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted, + q, &Launcher::onQuitAppCompleted); + + m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q); + q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound, + q, &Launcher::onComputerFound); + q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout, + q, &Launcher::onComputerSeekTimeout); + m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT); + + emit q->searchingComputer(); + } + break; + // Occurs when computer search timed out + case Event::ComputerSeekTimedout: + if (m_State == StateSeekComputer) { + m_State = StateFailure; + emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName)); + } + break; + // Occurs when searched computer is found + case Event::ComputerFound: + if (m_State == StateSeekComputer) { + if (event.computer->pairState == NvComputer::PS_PAIRED) { + m_State = StateQuitApp; + emit q->quittingApp(); + m_ComputerManager->quitRunningApp(event.computer); + } else { + m_State = StateFailure; + QString msg = QString("Computer %1 has not been paired. " + "Please open Moonlight to pair before streaming.") + .arg(event.computer->name); + emit q->failed(msg); + } + } + break; + // Occurs when app quit completed (error message is set if failed) + case Event::AppQuitCompleted: + if (m_State == StateQuitApp) { + if (event.errorMessage.isEmpty()) { + QCoreApplication::exit(0); + } else { + m_State = StateFailure; + emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage)); + } + } + break; + } + } + + Launcher *q_ptr; + ComputerManager *m_ComputerManager; + QString m_ComputerName; + ComputerSeeker *m_ComputerSeeker; + State m_State; + QTimer *m_TimeoutTimer; +}; + +Launcher::Launcher(QString computer, QObject *parent) + : QObject(parent), + m_DPtr(new LauncherPrivate(this)) +{ + Q_D(Launcher); + d->m_ComputerName = computer; + d->m_State = StateInit; + d->m_TimeoutTimer = new QTimer(this); + d->m_TimeoutTimer->setSingleShot(true); + connect(d->m_TimeoutTimer, &QTimer::timeout, + this, &Launcher::onComputerSeekTimeout); +} + +Launcher::~Launcher() +{ +} + +void Launcher::execute(ComputerManager *manager) +{ + Q_D(Launcher); + Event event(Event::Executed); + event.computerManager = manager; + d->handleEvent(event); +} + +bool Launcher::isExecuted() const +{ + Q_D(const Launcher); + return d->m_State != StateInit; +} + +void Launcher::onComputerFound(NvComputer *computer) +{ + Q_D(Launcher); + Event event(Event::ComputerFound); + event.computer = computer; + d->handleEvent(event); +} + +void Launcher::onComputerSeekTimeout() +{ + Q_D(Launcher); + Event event(Event::ComputerSeekTimedout); + d->handleEvent(event); +} + +void Launcher::onQuitAppCompleted(QVariant error) +{ + Q_D(Launcher); + Event event(Event::AppQuitCompleted); + event.errorMessage = error.toString(); + d->handleEvent(event); +} + +} diff --git a/app/cli/quitstream.h b/app/cli/quitstream.h new file mode 100644 index 00000000..3515526d --- /dev/null +++ b/app/cli/quitstream.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +class ComputerManager; +class NvComputer; + +namespace CliQuitStream +{ + +class LauncherPrivate; + +class Launcher : public QObject +{ + Q_OBJECT + Q_DECLARE_PRIVATE_D(m_DPtr, Launcher) + +public: + explicit Launcher(QString computer, QObject *parent = nullptr); + ~Launcher(); + + Q_INVOKABLE void execute(ComputerManager *manager); + Q_INVOKABLE bool isExecuted() const; + +signals: + void searchingComputer(); + void quittingApp(); + void failed(QString text); + +private slots: + void onComputerFound(NvComputer *computer); + void onComputerSeekTimeout(); + void onQuitAppCompleted(QVariant error); + +private: + QScopedPointer m_DPtr; +}; + +} diff --git a/app/cli/startstream.cpp b/app/cli/startstream.cpp index dcca89c0..4bb706f8 100644 --- a/app/cli/startstream.cpp +++ b/app/cli/startstream.cpp @@ -1,7 +1,9 @@ #include "startstream.h" #include "backend/computermanager.h" +#include "backend/computerseeker.h" #include "streaming/session.h" +#include #include #define COMPUTER_SEEK_TIMEOUT 10000 @@ -22,6 +24,9 @@ class Event { public: enum Type { + AppQuitCompleted, + AppQuitRequested, + ComputerFound, ComputerUpdated, Executed, Timedout, @@ -33,6 +38,7 @@ public: Type type; ComputerManager *computerManager; NvComputer *computer; + QString errorMessage; }; class LauncherPrivate @@ -49,58 +55,78 @@ public: NvApp app; switch (event.type) { + // Occurs when CliStartStreamSegue becomes visible and the UI calls launcher's execute() case Event::Executed: if (m_State == StateInit) { m_State = StateSeekComputer; - m_TimeoutTimer->start(COMPUTER_SEEK_TIMEOUT); m_ComputerManager = event.computerManager; + + m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q); + q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound, + q, &Launcher::onComputerFound); + q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout, + q, &Launcher::onTimeout); + m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT); + q->connect(m_ComputerManager, &ComputerManager::computerStateChanged, q, &Launcher::onComputerUpdated); - // Seek desired computer by both connecting to it directly (this may fail - // if m_ComputerName is UUID, or the name that doesn't resolve to an IP - // address) and by polling it using mDNS, hopefully one of these methods - // would find the host - m_ComputerManager->addNewHost(m_ComputerName, false); - m_ComputerManager->startPolling(); + q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted, + q, &Launcher::onQuitAppCompleted); + emit q->searchingComputer(); } break; - case Event::ComputerUpdated: + // Occurs when searched computer is found + case Event::ComputerFound: if (m_State == StateSeekComputer) { - if (matchComputer(event.computer) && isOnline(event.computer)) { - if (isPaired(event.computer)) { - m_State = StateSeekApp; - m_TimeoutTimer->start(APP_SEEK_TIMEOUT); - m_Computer = event.computer; - m_ComputerManager->stopPollingAsync(); - emit q->searchingApp(); - } else { - m_State = StateFailure; - QString msg = QString("Computer %1 has not been paired. " - "Please open Moonlight to pair before streaming.") - .arg(event.computer->name); - emit q->failed(msg); - } + if (event.computer->pairState == NvComputer::PS_PAIRED) { + m_State = StateSeekApp; + m_Computer = event.computer; + m_TimeoutTimer->start(APP_SEEK_TIMEOUT); + emit q->searchingApp(); + } else { + m_State = StateFailure; + QString msg = QString("Computer %1 has not been paired. " + "Please open Moonlight to pair before streaming.") + .arg(event.computer->name); + emit q->failed(msg); } } + break; + // Occurs when a computer is updated + case Event::ComputerUpdated: if (m_State == StateSeekApp) { int index = getAppIndex(); if (-1 != index) { app = m_Computer->appList[index]; + m_TimeoutTimer->stop(); if (isNotStreaming() || isStreamingApp(app)) { m_State = StateStartSession; - m_TimeoutTimer->stop(); session = new Session(m_Computer, app, m_Preferences); emit q->sessionCreated(app.name, session); } else { - m_State = StateFailure; - QString msg = QString("%1 is already running. Please quit %1 to stream %2.") - .arg(getCurrentAppName(), app.name); - emit q->failed(msg); + emit q->appQuitRequired(getCurrentAppName()); } } } break; + // Occurs when there was another app running on computer and user accepted quit + // confirmation dialog + case Event::AppQuitRequested: + if (m_State == StateSeekApp) { + m_ComputerManager->quitRunningApp(m_Computer); + } + break; + // Occurs when the previous app quit has been completed, handles quitting errors if any + // happended. ComputerUpdated event's handler handles session start when previous app has + // quit. + case Event::AppQuitCompleted: + if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) { + m_State = StateFailure; + emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage)); + } + break; + // Occurs when computer or app search timed out case Event::Timedout: if (m_State == StateSeekComputer) { m_State = StateFailure; @@ -114,26 +140,6 @@ public: } } - bool matchComputer(NvComputer *computer) const - { - QString value = m_ComputerName.toLower(); - return computer->name.toLower() == value || - computer->localAddress.toLower() == value || - computer->remoteAddress.toLower() == value || - computer->manualAddress.toLower() == value || - computer->uuid.toLower() == value; - } - - bool isOnline(NvComputer *computer) const - { - return computer->state == NvComputer::CS_ONLINE; - } - - bool isPaired(NvComputer *computer) const - { - return computer->pairState == NvComputer::PS_PAIRED; - } - int getAppIndex() const { for (int i = 0; i < m_Computer->appList.length(); i++) { @@ -169,6 +175,7 @@ public: QString m_AppName; StreamingPreferences *m_Preferences; ComputerManager *m_ComputerManager; + ComputerSeeker *m_ComputerSeeker; NvComputer *m_Computer; State m_State; QTimer *m_TimeoutTimer; @@ -202,6 +209,27 @@ void Launcher::execute(ComputerManager *manager) d->handleEvent(event); } +void Launcher::quitRunningApp() +{ + Q_D(Launcher); + Event event(Event::AppQuitRequested); + d->handleEvent(event); +} + +bool Launcher::isExecuted() const +{ + Q_D(const Launcher); + return d->m_State != StateInit; +} + +void Launcher::onComputerFound(NvComputer *computer) +{ + Q_D(Launcher); + Event event(Event::ComputerFound); + event.computer = computer; + d->handleEvent(event); +} + void Launcher::onComputerUpdated(NvComputer *computer) { Q_D(Launcher); @@ -217,4 +245,12 @@ void Launcher::onTimeout() d->handleEvent(event); } +void Launcher::onQuitAppCompleted(QVariant error) +{ + Q_D(Launcher); + Event event(Event::AppQuitCompleted); + event.errorMessage = error.toString(); + d->handleEvent(event); +} + } diff --git a/app/cli/startstream.h b/app/cli/startstream.h index 63372154..0927cda1 100644 --- a/app/cli/startstream.h +++ b/app/cli/startstream.h @@ -1,6 +1,7 @@ #pragma once #include +#include class ComputerManager; class NvComputer; @@ -24,16 +25,21 @@ public: QObject *parent = nullptr); ~Launcher(); Q_INVOKABLE void execute(ComputerManager *manager); + Q_INVOKABLE void quitRunningApp(); + Q_INVOKABLE bool isExecuted() const; signals: void searchingComputer(); void searchingApp(); void sessionCreated(QString appName, Session *session); void failed(QString text); + void appQuitRequired(QString appName); private slots: + void onComputerFound(NvComputer *computer); void onComputerUpdated(NvComputer *computer); void onTimeout(); + void onQuitAppCompleted(QVariant error); private: QScopedPointer m_DPtr; diff --git a/app/gui/CliQuitStreamSegue.qml b/app/gui/CliQuitStreamSegue.qml new file mode 100644 index 00000000..31e239ed --- /dev/null +++ b/app/gui/CliQuitStreamSegue.qml @@ -0,0 +1,70 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.2 +import QtQuick.Dialogs 1.2 + +import ComputerManager 1.0 +import Session 1.0 + +Item { + anchors.fill: parent + + function onSearchingComputer() { + stageLabel.text = "Establishing connection to PC..." + } + + function onQuittingApp() { + stageLabel.text = "Quitting app..." + } + + function onFailure(message) { + errorDialog.text = message + errorDialog.open() + } + + // The StackView will trigger a visibility change when + // we're pushed onto it, causing our onVisibleChanged + // routine to run, but only if we start as invisible + visible: false + + onVisibleChanged: { + if (visible && !launcher.isExecuted()) { + toolBar.visible = false + launcher.searchingComputer.connect(onSearchingComputer) + launcher.quittingApp.connect(onQuittingApp) + launcher.failed.connect(onFailure) + launcher.execute(ComputerManager) + } + } + + Row { + anchors.centerIn: parent + spacing: 5 + + BusyIndicator { + id: stageSpinner + } + + Label { + id: stageLabel + height: stageSpinner.height + text: stageText + font.pointSize: 20 + verticalAlignment: Text.AlignVCenter + + wrapMode: Text.Wrap + } + } + + MessageDialog { + id: errorDialog + modality:Qt.WindowModal + icon: StandardIcon.Critical + standardButtons: StandardButton.Ok + + onVisibleChanged: { + if (!visible) { + Qt.quit() + } + } + } +} diff --git a/app/gui/CliStartStreamSegue.qml b/app/gui/CliStartStreamSegue.qml index 54fde982..739216a0 100644 --- a/app/gui/CliStartStreamSegue.qml +++ b/app/gui/CliStartStreamSegue.qml @@ -1,3 +1,4 @@ +import QtQml 2.2 import QtQuick 2.0 import QtQuick.Controls 2.2 import QtQuick.Dialogs 1.2 @@ -30,13 +31,19 @@ Item { errorDialog.open() } + function onAppQuitRequired(appName) { + quitAppDialog.appName = appName + quitAppDialog.open() + } + onVisibleChanged: { - if (visible) { + if (visible && !launcher.isExecuted()) { toolBar.visible = false launcher.searchingComputer.connect(onSearchingComputer) launcher.searchingApp.connect(onSearchingApp) launcher.sessionCreated.connect(onSessionCreated) launcher.failed.connect(onLaunchFailed) + launcher.appQuitRequired.connect(onAppQuitRequired) launcher.execute(ComputerManager) } } @@ -74,4 +81,44 @@ Item { } } + MessageDialog { + id: quitAppDialog + modality:Qt.WindowModal + text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost." + standardButtons: StandardButton.Yes | StandardButton.No + property string appName : "" + + function quitApp() { + var component = Qt.createComponent("QuitSegue.qml") + var params = {"appName": appName} + stackView.push(component.createObject(stackView, params)) + // Trigger the quit after pushing the quit segue on screen + launcher.quitRunningApp() + } + + onYes: quitApp() + + // For keyboard/gamepad navigation + onAccepted: quitApp() + + // Exit process if app quit is rejected (reacts also to closing of the + // dialog from title bar's close button). + // Note: this depends on undocumented behavior of visibleChanged() + // signal being emitted before yes() or accepted() has been emitted. + onVisibleChanged: { + if (!visible) { + quitTimer.start() + } + } + Component.onCompleted: { + yes.connect(quitTimer.stop) + accepted.connect(quitTimer.stop) + } + } + + Timer { + id: quitTimer + interval: 100 + onTriggered: Qt.quit() + } } diff --git a/app/gui/SettingsView.qml b/app/gui/SettingsView.qml index 5d275bd8..6d3afd5c 100644 --- a/app/gui/SettingsView.qml +++ b/app/gui/SettingsView.qml @@ -701,6 +701,16 @@ Flickable { } } } + + CheckBox { + id: quitAppAfter + text: "Quit app after quitting session" + font.pointSize: 12 + checked: prefs.quitAppAfter + onCheckedChanged: { + prefs.quitAppAfter = checked + } + } } } } diff --git a/app/gui/StreamSegue.qml b/app/gui/StreamSegue.qml index a1f99372..20a34e5b 100644 --- a/app/gui/StreamSegue.qml +++ b/app/gui/StreamSegue.qml @@ -3,6 +3,7 @@ import QtQuick.Controls 2.2 import QtQuick.Dialogs 1.2 import QtQuick.Window 2.2 +import ComputerManager 1.0 import SdlGamepadKeyNavigation 1.0 import Session 1.0 @@ -11,6 +12,7 @@ Item { property string appName property string stageText : "Starting " + appName + "..." property bool quitAfter : false + property bool sessionLaunched : false anchors.fill: parent @@ -59,6 +61,27 @@ Item { toast.visible = true } + function streamingFinished() { + if (quitAfter) { + window.visible = false + Qt.quit() + } else { + // Show the Qt window again after streaming + window.visible = true + + // Exit this view + stackView.pop() + + // Display any launch errors. We do this after + // the Qt UI is visible again to prevent losing + // focus on the dialog which would impact gamepad + // users. + if (errorDialog.text) { + errorDialog.open() + } + } + } + // It's important that we don't call enable() here // or it may interfere with the Session instance // getting notified of initial connected gamepads. @@ -68,6 +91,12 @@ Item { onVisibleChanged: { if (visible) { + // Prevent session restart after execution returns from QuitSegue + if (sessionLaunched) { + return + } + sessionLaunched = true + // Hide the toolbar before we start loading toolBar.visible = false @@ -87,27 +116,21 @@ Item { session.displayLaunchWarning.connect(displayLaunchWarning) // Run the streaming session to completion - session.exec(Screen.virtualX, Screen.virtualY) + session.exec(Screen.virtualX, Screen.virtualY); - if (quitAfter) { - Qt.quit() - } else { - // Show the Qt window again after streaming + if (!errorDialog.text && session.shouldQuitAppAfter()) { + // Show the Qt window again to show quit segue window.visible = true - - // Exit this view - stackView.pop() - - // Display any launch errors. We do this after - // the Qt UI is visible again to prevent losing - // focus on the dialog which would impact gamepad - // users. - if (errorDialog.text) { - errorDialog.open() - } + var component = Qt.createComponent("QuitSegue.qml") + stackView.push(component.createObject(stackView, {"appName": appName})) + // Quit app + ComputerManager.quitAppCompleted.connect(streamingFinished) + ComputerManager.quitRunningApp(session) + } else { + streamingFinished() } } - else { + else if (!quitAfter) { // Show the toolbar again when we become hidden toolBar.visible = true } diff --git a/app/main.cpp b/app/main.cpp index 0dbfe3da..2e3fa271 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -21,6 +21,7 @@ #include "antihookingprotection.h" #endif +#include "cli/quitstream.h" #include "cli/startstream.h" #include "cli/commandlineparser.h" #include "path.h" @@ -342,6 +343,15 @@ int main(int argc, char *argv[]) engine.rootContext()->setContextProperty("launcher", launcher); break; } + case GlobalCommandLineParser::QuitRequested: + { + initialView = "CliQuitStreamSegue.qml"; + QuitCommandLineParser quitParser; + quitParser.parse(app.arguments()); + auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app); + engine.rootContext()->setContextProperty("launcher", launcher); + break; + } } engine.rootContext()->setContextProperty("initialView", initialView); @@ -369,5 +379,9 @@ int main(int argc, char *argv[]) int err = app.exec(); + // Give worker tasks time to properly exit. Fixes PendingQuitTask + // sometimes freezing and blocking process exit. + QThreadPool::globalInstance()->waitForDone(30000); + return err; } diff --git a/app/qml.qrc b/app/qml.qrc index 4d73e388..f74f5d68 100644 --- a/app/qml.qrc +++ b/app/qml.qrc @@ -10,6 +10,7 @@ gui/NavigableToolButton.qml gui/NavigableItemDelegate.qml gui/NavigableMenuItem.qml + gui/CliQuitStreamSegue.qml gui/CliStartStreamSegue.qml gui/AutoResizingComboBox.qml diff --git a/app/settings/streamingpreferences.cpp b/app/settings/streamingpreferences.cpp index 56aa7fc8..e288ef71 100644 --- a/app/settings/streamingpreferences.cpp +++ b/app/settings/streamingpreferences.cpp @@ -20,6 +20,7 @@ #define SER_WINDOWMODE "windowmode" #define SER_UNSUPPORTEDFPS "unsupportedfps" #define SER_MDNS "mdns" +#define SER_QUITAPPAFTER "quitAppAfter" #define SER_MOUSEACCELERATION "mouseacceleration" #define SER_STARTWINDOWED "startwindowed" @@ -43,6 +44,7 @@ void StreamingPreferences::reload() multiController = settings.value(SER_MULTICONT, true).toBool(); unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool(); enableMdns = settings.value(SER_MDNS, true).toBool(); + quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool(); mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool(); startWindowed = settings.value(SER_STARTWINDOWED, false).toBool(); audioConfig = static_cast(settings.value(SER_AUDIOCFG, @@ -71,6 +73,7 @@ void StreamingPreferences::save() settings.setValue(SER_MULTICONT, multiController); settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps); settings.setValue(SER_MDNS, enableMdns); + settings.setValue(SER_QUITAPPAFTER, quitAppAfter); settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration); settings.setValue(SER_STARTWINDOWED, startWindowed); settings.setValue(SER_AUDIOCFG, static_cast(audioConfig)); diff --git a/app/settings/streamingpreferences.h b/app/settings/streamingpreferences.h index c151435d..5539a0a3 100644 --- a/app/settings/streamingpreferences.h +++ b/app/settings/streamingpreferences.h @@ -73,6 +73,7 @@ public: Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged) Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged) Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged) + Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged) Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged) Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged) Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged) @@ -91,6 +92,7 @@ public: bool multiController; bool unsupportedFps; bool enableMdns; + bool quitAppAfter; bool mouseAcceleration; bool startWindowed; AudioConfig audioConfig; @@ -107,6 +109,7 @@ signals: void multiControllerChanged(); void unsupportedFpsChanged(); void enableMdnsChanged(); + void quitAppAfterChanged(); void mouseAccelerationChanged(); void audioConfigChanged(); void videoCodecConfigChanged(); diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index 774ed9c0..d591b39e 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -275,6 +275,16 @@ int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection return caps; } +NvComputer *Session::getComputer() const +{ + return m_Computer; +} + +bool Session::shouldQuitAppAfter() const +{ + return m_Preferences->quitAppAfter; +} + Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences) : m_Preferences(preferences ? preferences : new StreamingPreferences(this)), m_Computer(computer), diff --git a/app/streaming/session.h b/app/streaming/session.h index 51f2e655..63dfb49d 100644 --- a/app/streaming/session.h +++ b/app/streaming/session.h @@ -30,6 +30,10 @@ public: int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds, int videoFormat, int width, int height, int frameRate); + NvComputer* getComputer() const; + + Q_INVOKABLE bool shouldQuitAppAfter() const; + signals: void stageStarting(QString stage);