-
Notifications
You must be signed in to change notification settings - Fork 59
Add protocol activation support for Frontends #288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |||||||||||||||||||||||||||||||
| #include "pch.h" | ||||||||||||||||||||||||||||||||
| #include <Utils.hpp> | ||||||||||||||||||||||||||||||||
| #include "MoonlightWelcome.xaml.h" | ||||||||||||||||||||||||||||||||
| #include "Pages\StreamPage.xaml.h" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| using namespace moonlight_xbox_dx; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -85,10 +86,136 @@ void App::OnLaunched(Windows::ApplicationModel::Activation::LaunchActivatedEvent | |||||||||||||||||||||||||||||||
| auto state = GetApplicationState(); | ||||||||||||||||||||||||||||||||
| auto that = this; | ||||||||||||||||||||||||||||||||
| state->Init().then([that](){ | ||||||||||||||||||||||||||||||||
| that->m_stateLoaded = true; | ||||||||||||||||||||||||||||||||
| that->m_menuPage->OnStateLoaded(); | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| displayRequest->RequestActive(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| namespace { | ||||||||||||||||||||||||||||||||
| // Parsed from a protocol activation, e.g. moonlight:?host=192.168.1.10&appName=Desktop&launchOnExit=retropass: | ||||||||||||||||||||||||||||||||
| struct ProtocolLaunchRequest { | ||||||||||||||||||||||||||||||||
| bool hasTarget = false; | ||||||||||||||||||||||||||||||||
| std::wstring host; | ||||||||||||||||||||||||||||||||
| int appId = -1; | ||||||||||||||||||||||||||||||||
| std::wstring appName; | ||||||||||||||||||||||||||||||||
| bool resume = false; | ||||||||||||||||||||||||||||||||
| bool hasLaunchOnExit = false; | ||||||||||||||||||||||||||||||||
| Platform::String^ launchOnExit; | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Flag-style parameters count as enabled when present without a value | ||||||||||||||||||||||||||||||||
| bool IsTruthyParam(Platform::String^ value) { | ||||||||||||||||||||||||||||||||
| if (value == nullptr || value->IsEmpty()) return true; | ||||||||||||||||||||||||||||||||
| return _wcsicmp(value->Data(), L"true") == 0 || _wcsicmp(value->Data(), L"1") == 0 || _wcsicmp(value->Data(), L"yes") == 0; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ProtocolLaunchRequest ParseProtocolUri(Windows::Foundation::Uri^ uri) { | ||||||||||||||||||||||||||||||||
| ProtocolLaunchRequest request; | ||||||||||||||||||||||||||||||||
| Windows::Foundation::WwwFormUrlDecoder^ query = nullptr; | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| query = uri->QueryParsed; | ||||||||||||||||||||||||||||||||
| } catch (...) { | ||||||||||||||||||||||||||||||||
| moonlight_xbox_dx::Utils::Log("Protocol activation: failed to parse query string\n"); | ||||||||||||||||||||||||||||||||
| return request; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if (query == nullptr) return request; | ||||||||||||||||||||||||||||||||
| for (unsigned int i = 0; i < query->Size; i++) { | ||||||||||||||||||||||||||||||||
| auto entry = query->GetAt(i); | ||||||||||||||||||||||||||||||||
| if (entry == nullptr || entry->Name == nullptr) continue; | ||||||||||||||||||||||||||||||||
| const wchar_t* name = entry->Name->Data(); | ||||||||||||||||||||||||||||||||
| Platform::String^ value = entry->Value; | ||||||||||||||||||||||||||||||||
| bool hasValue = value != nullptr && !value->IsEmpty(); | ||||||||||||||||||||||||||||||||
| if (_wcsicmp(name, L"host") == 0 && hasValue) { | ||||||||||||||||||||||||||||||||
| request.host = value->Data(); | ||||||||||||||||||||||||||||||||
| request.hasTarget = true; | ||||||||||||||||||||||||||||||||
| } else if (_wcsicmp(name, L"appId") == 0 && hasValue) { | ||||||||||||||||||||||||||||||||
| request.appId = (int)wcstol(value->Data(), nullptr, 10); | ||||||||||||||||||||||||||||||||
| request.hasTarget = true; | ||||||||||||||||||||||||||||||||
| } else if (_wcsicmp(name, L"appName") == 0 && hasValue) { | ||||||||||||||||||||||||||||||||
| request.appName = value->Data(); | ||||||||||||||||||||||||||||||||
| request.hasTarget = true; | ||||||||||||||||||||||||||||||||
| } else if (_wcsicmp(name, L"desktop") == 0) { | ||||||||||||||||||||||||||||||||
| if (IsTruthyParam(value)) { | ||||||||||||||||||||||||||||||||
| request.appName = L"Desktop"; | ||||||||||||||||||||||||||||||||
| request.hasTarget = true; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } else if (_wcsicmp(name, L"resume") == 0) { | ||||||||||||||||||||||||||||||||
| if (IsTruthyParam(value)) { | ||||||||||||||||||||||||||||||||
| request.resume = true; | ||||||||||||||||||||||||||||||||
| request.hasTarget = true; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } else if (_wcsicmp(name, L"launchOnExit") == 0 && hasValue) { | ||||||||||||||||||||||||||||||||
| // Return URI provided by the launching frontend (e.g. "retropass:"), passed through as-is | ||||||||||||||||||||||||||||||||
| request.launchOnExit = value; | ||||||||||||||||||||||||||||||||
| request.hasLaunchOnExit = true; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return request; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||
| /// Invoked when the application is activated through a URI scheme (moonlight:). | ||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||
| void App::OnActivated(Windows::ApplicationModel::Activation::IActivatedEventArgs^ e) | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| if (e->Kind != Windows::ApplicationModel::Activation::ActivationKind::Protocol) { | ||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| auto protocolArgs = dynamic_cast<Windows::ApplicationModel::Activation::ProtocolActivatedEventArgs^>(e); | ||||||||||||||||||||||||||||||||
| if (protocolArgs == nullptr || protocolArgs->Uri == nullptr) { | ||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| moonlight_xbox_dx::Utils::Logf("Protocol activation: %S\n", protocolArgs->Uri->AbsoluteUri->Data()); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ProtocolLaunchRequest request = ParseProtocolUri(protocolArgs->Uri); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| auto rootFrame = dynamic_cast<Frame^>(Window::Current->Content); | ||||||||||||||||||||||||||||||||
| bool isColdStart = (rootFrame == nullptr); | ||||||||||||||||||||||||||||||||
| if (rootFrame == nullptr) | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| rootFrame = ref new Frame(); | ||||||||||||||||||||||||||||||||
| rootFrame->NavigationFailed += ref new Windows::UI::Xaml::Navigation::NavigationFailedEventHandler(this, &App::OnNavigationFailed); | ||||||||||||||||||||||||||||||||
| Window::Current->Content = rootFrame; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Never interrupt an active streaming session, just bring the window to the foreground | ||||||||||||||||||||||||||||||||
| if (dynamic_cast<StreamPage^>(rootFrame->Content) != nullptr) { | ||||||||||||||||||||||||||||||||
| moonlight_xbox_dx::Utils::Log("Protocol activation ignored: a stream is currently active\n"); | ||||||||||||||||||||||||||||||||
| Window::Current->Activate(); | ||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| auto state = GetApplicationState(); | ||||||||||||||||||||||||||||||||
| state->pendingProtocolHostSelect = request.hasTarget; | ||||||||||||||||||||||||||||||||
| state->pendingProtocolHost = request.host; | ||||||||||||||||||||||||||||||||
| state->pendingProtocolAppId = request.appId; | ||||||||||||||||||||||||||||||||
| state->pendingProtocolAppName = request.appName; | ||||||||||||||||||||||||||||||||
| state->pendingProtocolResume = request.resume; | ||||||||||||||||||||||||||||||||
| if (request.hasLaunchOnExit) { | ||||||||||||||||||||||||||||||||
| state->launchOnExitUri = request.launchOnExit; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+196
to
+198
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
📍 Affects 2 files
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| rootFrame->Navigate(TypeName(HostSelectorPage::typeid)); | ||||||||||||||||||||||||||||||||
| rootFrame->BackStack->Clear(); | ||||||||||||||||||||||||||||||||
| m_menuPage = dynamic_cast<HostSelectorPage^>(rootFrame->Content); | ||||||||||||||||||||||||||||||||
|
Comment on lines
+200
to
+202
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Check the
🐛 Proposed fix to guard the navigation result- rootFrame->Navigate(TypeName(HostSelectorPage::typeid));
- rootFrame->BackStack->Clear();
- m_menuPage = dynamic_cast<HostSelectorPage^>(rootFrame->Content);
+ if (!rootFrame->Navigate(TypeName(HostSelectorPage::typeid))) {
+ moonlight_xbox_dx::Utils::Log("Protocol activation: navigation to HostSelectorPage failed\n");
+ Window::Current->Activate();
+ return;
+ }
+ rootFrame->BackStack->Clear();
+ m_menuPage = dynamic_cast<HostSelectorPage^>(rootFrame->Content);
+ if (m_menuPage == nullptr) {
+ moonlight_xbox_dx::Utils::Log("Protocol activation: HostSelectorPage instance unavailable\n");
+ Window::Current->Activate();
+ return;
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Window::Current->Activate(); | ||||||||||||||||||||||||||||||||
| if (isColdStart) { | ||||||||||||||||||||||||||||||||
| displayRequest->RequestActive(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| auto that = this; | ||||||||||||||||||||||||||||||||
| if (!m_stateLoaded) { | ||||||||||||||||||||||||||||||||
| state->Init().then([that]() { | ||||||||||||||||||||||||||||||||
| that->m_stateLoaded = true; | ||||||||||||||||||||||||||||||||
| that->m_menuPage->OnStateLoaded(); | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| m_menuPage->OnStateLoaded(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+209
to
+217
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Two problems exist in this block.
The continuation also has no error handler. 🐛 Proposed fix to share one initialization task and observe errors auto that = this;
if (!m_stateLoaded) {
- state->Init().then([that]() {
- that->m_stateLoaded = true;
- that->m_menuPage->OnStateLoaded();
- });
+ if (!m_initStarted) {
+ m_initStarted = true;
+ m_initTask = state->Init();
+ }
+ m_initTask.then([that]() {
+ that->m_stateLoaded = true;
+ if (that->m_menuPage != nullptr) that->m_menuPage->OnStateLoaded();
+ }, concurrency::task_continuation_context::get_current_winrt_context())
+ .then([](concurrency::task<void> t) {
+ try { t.get(); }
+ catch (const std::exception& ex) { moonlight_xbox_dx::Utils::Logf("Protocol activation Init exception: %s\n", ex.what()); }
+ catch (...) { moonlight_xbox_dx::Utils::Log("Protocol activation Init unknown exception\n"); }
+ });
} else {
m_menuPage->OnStateLoaded();
}This needs matching members in bool m_initStarted = false;
Concurrency::task<void> m_initTask;Apply the same shared-task pattern in 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||
| /// Invoked when application execution is being suspended. Application state is saved | ||||||||||||||||||||||||||||||||
| /// without knowing whether the application will be terminated or resumed with the contents | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -118,7 +118,44 @@ void AppPage::OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ | |
| } | ||
| }); | ||
|
|
||
| if (host->AutostartID >= 0 && GetApplicationState()->shouldAutoConnect) { | ||
| auto state = GetApplicationState(); | ||
| bool hasProtocolRequest = state->pendingProtocolAppId >= 0 || !state->pendingProtocolAppName.empty() || state->pendingProtocolResume; | ||
| if (hasProtocolRequest) { | ||
| int requestedAppId = state->pendingProtocolAppId; | ||
| std::wstring requestedAppName = state->pendingProtocolAppName; | ||
| bool resumeRequested = state->pendingProtocolResume; | ||
| state->pendingProtocolAppId = -1; | ||
| state->pendingProtocolAppName.clear(); | ||
| state->pendingProtocolResume = false; | ||
|
|
||
| // Dispatched at High priority so this runs after UpdateApps() has filled the Apps list | ||
| Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( | ||
| Windows::UI::Core::CoreDispatcherPriority::High, ref new Windows::UI::Core::DispatchedHandler([this, requestedAppId, requestedAppName, resumeRequested]() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Capture a tracked reference instead of raw In C++/CX a lambda that captures
🐛 Proposed fix to hold a tracked reference+ auto that = this;
// Dispatched at High priority so this runs after UpdateApps() has filled the Apps list
Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(
- Windows::UI::Core::CoreDispatcherPriority::High, ref new Windows::UI::Core::DispatchedHandler([this, requestedAppId, requestedAppName, resumeRequested]() {
+ Windows::UI::Core::CoreDispatcherPriority::High, ref new Windows::UI::Core::DispatchedHandler([that, requestedAppId, requestedAppName, resumeRequested]() {Replace the 🤖 Prompt for AI Agents |
||
| int targetId = -1; | ||
| if (resumeRequested && host->CurrentlyRunningAppId != 0) { | ||
| targetId = host->CurrentlyRunningAppId; | ||
| } | ||
| if (targetId < 0 && requestedAppId >= 0) { | ||
| targetId = requestedAppId; | ||
| } | ||
| if (targetId < 0 && !requestedAppName.empty()) { | ||
| for (unsigned int i = 0; i < host->Apps->Size; ++i) { | ||
| auto app = host->Apps->GetAt(i); | ||
| if (app != nullptr && app->Name != nullptr && _wcsicmp(app->Name->Data(), requestedAppName.c_str()) == 0) { | ||
| targetId = app->Id; | ||
| break; | ||
| } | ||
| } | ||
| if (targetId < 0) { | ||
| Utils::Log("Protocol activation: no app matched the requested name, staying on the app list\n"); | ||
| } | ||
| } | ||
| if (targetId >= 0) { | ||
| this->Connect(targetId); | ||
| } | ||
| })); | ||
| } | ||
| else if (host->AutostartID >= 0 && GetApplicationState()->shouldAutoConnect) { | ||
| GetApplicationState()->shouldAutoConnect = false; | ||
| Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( | ||
| Windows::UI::Core::CoreDispatcherPriority::High, ref new Windows::UI::Core::DispatchedHandler([this]() { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Validate the
appIdconversion before you accept it.wcstolreturns 0 for any non-numeric input.moonlight:?appId=abctherefore setspendingProtocolAppId = 0. InPages/AppPage.xaml.cppline 122 the checkpendingProtocolAppId >= 0passes, line 139 setstargetId = 0, and line 154 callsConnect(0). The app then navigates toStreamPagewith an invalid app id instead of falling back toappNameor the app list.Line 135 of
Pages/AppPage.xaml.cpptreatsCurrentlyRunningAppId != 0as "nothing running", so 0 is not a usable app id in this codebase. Reject non-numeric input and non-positive results.🐛 Proposed fix to validate the parsed app id
} else if (_wcsicmp(name, L"appId") == 0 && hasValue) { - request.appId = (int)wcstol(value->Data(), nullptr, 10); - request.hasTarget = true; + wchar_t* end = nullptr; + const wchar_t* begin = value->Data(); + long parsed = wcstol(begin, &end, 10); + if (end != begin && *end == L'\0' && parsed > 0 && parsed <= INT_MAX) { + request.appId = (int)parsed; + request.hasTarget = true; + } else { + moonlight_xbox_dx::Utils::Log("Protocol activation: ignoring invalid appId\n"); + } } else if (_wcsicmp(name, L"appName") == 0 && hasValue) {📝 Committable suggestion
🤖 Prompt for AI Agents