From 0d00ddb6273cf0b84b726ec7874d21d92072c54c Mon Sep 17 00:00:00 2001 From: danprice142 Date: Wed, 19 Aug 2026 21:04:00 +0100 Subject: [PATCH] Add protocol activation support for Frontends Register the 'moonlight' URI protocol and handle protocol activations. Adds App::OnActivated and URI parsing to accept parameters (host, appId, appName, desktop, resume, launchOnExit) and a truthy flag parser. New ApplicationState fields track pending protocol host/app/resume and a launchOnExit URI. HostSelectorPage gains HandleProtocolHostSelect to resolve and auto-connect saved hosts; AppPage applies pending app/connect requests after apps are fetched. Streaming exit now launches the return URI (launchOnExit) if provided. Ensures cold-start behavior and avoids interrupting an active stream. --- App.xaml.cpp | 127 ++++++++++++++++++++++++++++ App.xaml.h | 2 + Package.appxmanifest | 7 ++ Pages/AppPage.xaml.cpp | 39 ++++++++- Pages/HostSelectorPage.xaml.cpp | 60 +++++++++++++ Pages/HostSelectorPage.xaml.h | 1 + State/ApplicationState.h | 8 ++ Streaming/moonlight_xbox_dxMain.cpp | 24 ++++++ 8 files changed, 267 insertions(+), 1 deletion(-) diff --git a/App.xaml.cpp b/App.xaml.cpp index e25e67e8..101729f5 100644 --- a/App.xaml.cpp +++ b/App.xaml.cpp @@ -6,6 +6,7 @@ #include "pch.h" #include #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; + } +} + +/// +/// Invoked when the application is activated through a URI scheme (moonlight:). +/// +void App::OnActivated(Windows::ApplicationModel::Activation::IActivatedEventArgs^ e) +{ + if (e->Kind != Windows::ApplicationModel::Activation::ActivationKind::Protocol) { + return; + } + auto protocolArgs = dynamic_cast(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(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(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; + } + + rootFrame->Navigate(TypeName(HostSelectorPage::typeid)); + rootFrame->BackStack->Clear(); + m_menuPage = dynamic_cast(rootFrame->Content); + + 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(); + } +} /// /// Invoked when application execution is being suspended. Application state is saved /// without knowing whether the application will be terminated or resumed with the contents diff --git a/App.xaml.h b/App.xaml.h index 0e7486c6..531e0725 100644 --- a/App.xaml.h +++ b/App.xaml.h @@ -18,6 +18,7 @@ namespace moonlight_xbox_dx public: App(); virtual void OnLaunched(Windows::ApplicationModel::Activation::LaunchActivatedEventArgs^ e) override; + virtual void OnActivated(Windows::ApplicationModel::Activation::IActivatedEventArgs^ e) override; void OnStateLoaded(); private: void OnSuspending(Platform::Object^ sender, Windows::ApplicationModel::SuspendingEventArgs^ e); @@ -25,5 +26,6 @@ namespace moonlight_xbox_dx void OnNavigationFailed(Platform::Object ^sender, Windows::UI::Xaml::Navigation::NavigationFailedEventArgs ^e); HostSelectorPage^ m_menuPage; Windows::System::Display::DisplayRequest^ displayRequest; + bool m_stateLoaded = false; }; } diff --git a/Package.appxmanifest b/Package.appxmanifest index 424aa372..f558a96e 100644 --- a/Package.appxmanifest +++ b/Package.appxmanifest @@ -41,6 +41,13 @@ + + + + Moonlight UWP + + + diff --git a/Pages/AppPage.xaml.cpp b/Pages/AppPage.xaml.cpp index cf084c77..f87b4db4 100644 --- a/Pages/AppPage.xaml.cpp +++ b/Pages/AppPage.xaml.cpp @@ -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]() { + 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]() { diff --git a/Pages/HostSelectorPage.xaml.cpp b/Pages/HostSelectorPage.xaml.cpp index 5a96ec39..471d5c09 100644 --- a/Pages/HostSelectorPage.xaml.cpp +++ b/Pages/HostSelectorPage.xaml.cpp @@ -223,6 +223,10 @@ void HostSelectorPage::OnStateLoaded() { a->UpdateHostInfo(false); } }).then([this]() { + if (GetApplicationState()->pendingProtocolHostSelect) { + this->HandleProtocolHostSelect(); + return; + } if (GetApplicationState()->autostartInstance.size() > 0) { auto pii = Utils::StringFromStdString(GetApplicationState()->autostartInstance); for (unsigned int i = 0; i < GetApplicationState()->SavedHosts->Size; i++) { @@ -252,6 +256,62 @@ void HostSelectorPage::OnStateLoaded() { }); } +// The host query can match the instance id, the computer name or the hostname/IP +void HostSelectorPage::HandleProtocolHostSelect() { + auto state = GetApplicationState(); + state->pendingProtocolHostSelect = false; + std::wstring query = state->pendingProtocolHost; + + MoonlightHost^ target = nullptr; + if (query.empty()) { + if (state->SavedHosts->Size == 1) { + target = state->SavedHosts->GetAt(0); + } + else if (state->autostartInstance.size() > 0) { + auto pii = Utils::StringFromStdString(state->autostartInstance); + for (unsigned int i = 0; i < state->SavedHosts->Size; i++) { + auto host = state->SavedHosts->GetAt(i); + if (host->InstanceId != nullptr && host->InstanceId->Equals(pii)) { + target = host; + break; + } + } + } + } + else { + auto matches = [&query](Platform::String^ value) { + return value != nullptr && _wcsicmp(value->Data(), query.c_str()) == 0; + }; + for (unsigned int i = 0; i < state->SavedHosts->Size; i++) { + auto host = state->SavedHosts->GetAt(i); + if (matches(host->InstanceId) || matches(host->ComputerName) || matches(host->LastHostname)) { + target = host; + break; + } + } + } + + // Drop the pending app request too, so it doesn't leak into a later manual host selection + if (target == nullptr || !target->Connected) { + Utils::Log(target == nullptr + ? "Protocol activation: no saved host matched the requested host\n" + : "Protocol activation: the requested host is not reachable\n"); + state->pendingProtocolAppId = -1; + state->pendingProtocolAppName.clear(); + state->pendingProtocolResume = false; + return; + } + + auto that = this; + MoonlightHost^ host = target; + Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( + Windows::UI::Core::CoreDispatcherPriority::High, + ref new Windows::UI::Core::DispatchedHandler([that, host]() { + that->Connect(host); + }) + ); +} + void HostSelectorPage::Connect(MoonlightHost^ host) { if (!host->Connected)return; if (!host->Paired) { diff --git a/Pages/HostSelectorPage.xaml.h b/Pages/HostSelectorPage.xaml.h index a2008481..8e4139eb 100644 --- a/Pages/HostSelectorPage.xaml.h +++ b/Pages/HostSelectorPage.xaml.h @@ -45,6 +45,7 @@ namespace moonlight_xbox_dx void SettingsButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); std::atomic continueFetch; void OnKeyDown(Platform::Object^ sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs^ e); + void HandleProtocolHostSelect(); void wakeHostButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); void testConnectionButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); void ShowHostActions(Windows::UI::Xaml::FrameworkElement^ anchor, MoonlightHost^ host); diff --git a/State/ApplicationState.h b/State/ApplicationState.h index 7841411d..66dcd275 100644 --- a/State/ApplicationState.h +++ b/State/ApplicationState.h @@ -24,6 +24,14 @@ namespace moonlight_xbox_dx { bool enableKeyboard = false; bool shouldAutoConnect = false; + // Protocol (URI scheme) launch support, filled by App::OnActivated + bool pendingProtocolHostSelect = false; + std::wstring pendingProtocolHost; + int pendingProtocolAppId = -1; + std::wstring pendingProtocolAppName; + bool pendingProtocolResume = false; + Platform::String^ launchOnExitUri; + bool WakeHost(MoonlightHost^ host); void Validate_WoL(MoonlightHost^ host); std::string WoL_Payload(std::string macAddress); diff --git a/Streaming/moonlight_xbox_dxMain.cpp b/Streaming/moonlight_xbox_dxMain.cpp index 803aad77..bd4f5298 100644 --- a/Streaming/moonlight_xbox_dxMain.cpp +++ b/Streaming/moonlight_xbox_dxMain.cpp @@ -871,6 +871,30 @@ void moonlight_xbox_dxMain::CloseApp() { void moonlight_xbox_dxMain::ExitStreamPage() { + // If a frontend launched us with a launchOnExit return URI, go back to it and exit + auto state = GetApplicationState(); + Platform::String ^ returnUri = state->launchOnExitUri; + if (returnUri != nullptr && !returnUri->IsEmpty()) { + state->launchOnExitUri = nullptr; + try { + auto uri = ref new Windows::Foundation::Uri(returnUri); + concurrency::create_task(Windows::System::Launcher::LaunchUriAsync(uri)).then([](concurrency::task t) { + try { + if (t.get()) { + Windows::ApplicationModel::Core::CoreApplication::Exit(); + } else { + Utils::Log("ExitStreamPage: failed to launch the return URI\n"); + } + } catch (...) { + Utils::Log("ExitStreamPage: failed to launch the return URI\n"); + } + }); + } catch (...) { + Utils::Log("ExitStreamPage: the return URI is not a valid URI\n"); + } + // Keep navigating back below so the app is in a sane state if the launch fails + } + bool reachedAppPage = false; try {