diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index e044a38a94..2d2a74ac3b 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -192,6 +192,13 @@ + + + + + + + + Advertise="yes"> + + + @@ -209,6 +219,9 @@ + + + @@ -292,6 +305,8 @@ + + diff --git a/src/common/com_object_factory.h b/src/common/com_object_factory.h new file mode 100644 index 0000000000..69b21dd287 --- /dev/null +++ b/src/common/com_object_factory.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include + +template +class com_object_factory : public IClassFactory +{ +public: + HRESULT __stdcall QueryInterface(const IID & riid, void** ppv) override + { + static const QITAB qit[] = { + QITABENT(com_object_factory, IClassFactory), + { 0 } + }; + return QISearch(this, qit, riid, ppv); + } + + ULONG __stdcall AddRef() override + { + return ++_refCount; + } + + ULONG __stdcall Release() override + { + LONG refCount = --_refCount; + return refCount; + } + + HRESULT __stdcall CreateInstance(IUnknown* punkOuter, const IID & riid, void** ppv) + { + *ppv = nullptr; + HRESULT hr; + if (punkOuter) + { + hr = CLASS_E_NOAGGREGATION; + } + else + { + T* psrm = new (std::nothrow) T(); + HRESULT hr = psrm ? S_OK : E_OUTOFMEMORY; + if (SUCCEEDED(hr)) + { + hr = psrm->QueryInterface(riid, ppv); + psrm->Release(); + } + return hr; + } + return hr; + } + + HRESULT __stdcall LockServer(BOOL) + { + return S_OK; + } + +private: + std::atomic _refCount; +}; \ No newline at end of file diff --git a/src/common/common.h b/src/common/common.h index d52adf98b6..0ba059d8c5 100644 --- a/src/common/common.h +++ b/src/common/common.h @@ -119,3 +119,11 @@ struct on_scope_exit _f(); } }; + +template +struct overloaded : Ts... +{ + using Ts::operator()...; +}; +template +overloaded(Ts...)->overloaded; diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj index 8cbf18cfe5..c9ba33133d 100644 --- a/src/common/common.vcxproj +++ b/src/common/common.vcxproj @@ -105,6 +105,7 @@ + diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters index 1cf60d71d1..0efa7e98bf 100644 --- a/src/common/common.vcxproj.filters +++ b/src/common/common.vcxproj.filters @@ -87,6 +87,9 @@ Header Files + + Header Files + @@ -145,4 +148,4 @@ Source Files - \ No newline at end of file + diff --git a/src/common/notifications.cpp b/src/common/notifications.cpp index 96c64df79c..0aa4d0175f 100644 --- a/src/common/notifications.cpp +++ b/src/common/notifications.cpp @@ -1,4 +1,7 @@ #include "pch.h" + +#include "common.h" +#include "com_object_factory.h" #include "notifications.h" #include @@ -9,6 +12,13 @@ #include #include +#include "winstore.h" + +#include +#include + +#include "notifications_winrt/handler_functions.h" + using namespace winrt::Windows::ApplicationModel::Background; using winrt::Windows::Data::Xml::Dom::XmlDocument; using winrt::Windows::UI::Notifications::ToastNotification; @@ -19,10 +29,94 @@ namespace constexpr std::wstring_view TASK_NAME = L"PowerToysBackgroundNotificationsHandler"; constexpr std::wstring_view TASK_ENTRYPOINT = L"PowerToysNotifications.BackgroundHandler"; constexpr std::wstring_view APPLICATION_ID = L"PowerToys"; + + constexpr std::wstring_view WIN32_AUMID = L"Microsoft.PowerToysWin32"; +} + +static DWORD loop_thread_id() +{ + static const DWORD thread_id = GetCurrentThreadId(); + return thread_id; +} + +class DECLSPEC_UUID("DD5CACDA-7C2E-4997-A62A-04A597B58F76") NotificationActivator : public INotificationActivationCallback +{ +public: + HRESULT __stdcall QueryInterface(_In_ REFIID iid, _Outptr_ void** resultInterface) override + { + static const QITAB qit[] = { + QITABENT(NotificationActivator, INotificationActivationCallback), + { 0 } + }; + return QISearch(this, qit, iid, resultInterface); + } + + ULONG __stdcall AddRef() override + { + return ++_refCount; + } + + ULONG __stdcall Release() override + { + LONG refCount = --_refCount; + if (refCount == 0) + { + PostThreadMessage(loop_thread_id(), WM_QUIT, 0, 0); + delete this; + } + return refCount; + } + + virtual HRESULT STDMETHODCALLTYPE Activate( + LPCWSTR appUserModelId, + LPCWSTR invokedArgs, + const NOTIFICATION_USER_INPUT_DATA*, + ULONG) override + { + auto lib = LoadLibraryW(L"Notifications.dll"); + if (!lib) + { + return 1; + } + auto dispatcher = reinterpret_cast(GetProcAddress(lib, "dispatch_to_backround_handler")); + if (!dispatcher) + { + return 1; + } + + dispatcher(invokedArgs); + + return 0; + } + +private: + std::atomic _refCount; +}; + +void notifications::run_desktop_app_activator_loop() +{ + com_object_factory factory; + + (void)loop_thread_id(); + + DWORD token; + auto res = CoRegisterClassObject(__uuidof(NotificationActivator), &factory, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &token); + if (!SUCCEEDED(res)) + { + return; + } + + run_message_loop(); + CoRevokeClassObject(token); } void notifications::register_background_toast_handler() { + if (!winstore::running_as_packaged()) + { + // The WIX installer will have us registered via the registry + return; + } try { // Re-request access to clean up from previous PowerToys installations @@ -54,36 +148,89 @@ void notifications::register_background_toast_handler() void notifications::show_toast(std::wstring_view message) { // The toast won't be actually activated in the background, since it doesn't have any buttons - show_toast_background_activated(message, {}, {}); + show_toast_with_activations(message, {}, {}); } -void notifications::show_toast_background_activated(std::wstring_view message, std::wstring_view background_handler_id, std::vector button_labels) +inline void xml_escape(std::wstring data) +{ + std::wstring buffer; + buffer.reserve(data.size()); + for (size_t pos = 0; pos != data.size(); ++pos) + { + switch (data[pos]) + { + case L'&': + buffer.append(L"&"); + break; + case L'\"': + buffer.append(L"""); + break; + case L'\'': + buffer.append(L"'"); + break; + case L'<': + buffer.append(L"<"); + break; + case L'>': + buffer.append(L">"); + break; + default: + buffer.append(&data[pos], 1); + break; + } + } + data.swap(buffer); +} + +void notifications::show_toast_with_activations(std::wstring_view message, std::wstring_view background_handler_id, std::vector buttons) { // DO NOT LOCALIZE any string in this function, because they're XML tags and a subject to // https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/toast-xml-schema std::wstring toast_xml; toast_xml.reserve(1024); - toast_xml += LR"(PowerToys)"; + std::wstring title{L"PowerToys"}; + if (winstore::running_as_packaged()) + { + title += L" (Experimental)"; + } + + toast_xml += LR"()"; + toast_xml += title; + toast_xml += L""; toast_xml += message; toast_xml += L""; - for (size_t i = 0; i < size(button_labels); ++i) + for (size_t i = 0; i < size(buttons); ++i) { - toast_xml += LR"()"; + std::visit(overloaded{ + [&](const link_button& b) { + toast_xml += LR"()"; + }, + [&](const background_activated_button& b) { + toast_xml += LR"()"; + }, + }, + buttons[i]); } toast_xml += L""; XmlDocument toast_xml_doc; + xml_escape(toast_xml); toast_xml_doc.LoadXml(toast_xml); ToastNotification notification{ toast_xml_doc }; - const auto notifier = ToastNotificationManager::ToastNotificationManager::CreateToastNotifier(); + const auto notifier = winstore::running_as_packaged() ? ToastNotificationManager::ToastNotificationManager::CreateToastNotifier() : + ToastNotificationManager::ToastNotificationManager::CreateToastNotifier(WIN32_AUMID); notifier.Show(notification); } diff --git a/src/common/notifications.h b/src/common/notifications.h index f0b525f7a8..c4c8e9ebce 100644 --- a/src/common/notifications.h +++ b/src/common/notifications.h @@ -2,12 +2,29 @@ #include #include +#include namespace notifications { + constexpr inline const wchar_t TOAST_ACTIVATED_LAUNCH_ARG[] = L"-ToastActivated"; + void register_background_toast_handler(); - // Make sure your plaintext_message argument is properly XML-escaped + void run_desktop_app_activator_loop(); + + struct link_button + { + std::wstring_view label; + std::wstring_view url; + }; + + struct background_activated_button + { + std::wstring_view label; + }; + + using button_t = std::variant; + void show_toast(std::wstring_view plaintext_message); - void show_toast_background_activated(std::wstring_view plaintext_message, std::wstring_view background_handler_id, std::vector plaintext_button_labels); + void show_toast_with_activations(std::wstring_view plaintext_message, std::wstring_view background_handler_id, std::vector buttons); } diff --git a/src/common/notifications/notifications.def b/src/common/notifications/notifications.def index 6f14af44a9..99071d3f72 100644 --- a/src/common/notifications/notifications.def +++ b/src/common/notifications/notifications.def @@ -3,3 +3,4 @@ DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE DllRegisterServer PRIVATE DllUnregisterServer PRIVATE +dispatch_to_backround_handler PRIVATE \ No newline at end of file diff --git a/src/common/notifications_winrt/BackgroundHandler.cpp b/src/common/notifications_winrt/BackgroundHandler.cpp index 6a73ff6c72..67766d1bc0 100644 --- a/src/common/notifications_winrt/BackgroundHandler.cpp +++ b/src/common/notifications_winrt/BackgroundHandler.cpp @@ -8,7 +8,6 @@ namespace winrt::PowerToysNotifications::implementation { using Windows::ApplicationModel::Background::IBackgroundTaskInstance; using Windows::UI::Notifications::ToastNotificationActionTriggerDetail; - using Windows::Foundation::WwwFormUrlDecoder; void BackgroundHandler::Run(IBackgroundTaskInstance bti) { @@ -18,10 +17,6 @@ namespace winrt::PowerToysNotifications::implementation return; } - WwwFormUrlDecoder decoder{details.Argument()}; - - const size_t button_id = std::stoi(decoder.GetFirstValueByName(L"button_id").c_str()); - auto handler = decoder.GetFirstValueByName(L"handler"); - dispatch_to_backround_handler(std::move(handler), std::move(bti), button_id); + dispatch_to_backround_handler(details.Argument()); } } diff --git a/src/common/notifications_winrt/handler_functions.cpp b/src/common/notifications_winrt/handler_functions.cpp index 39e04b2038..d4e7bc5ef1 100644 --- a/src/common/notifications_winrt/handler_functions.cpp +++ b/src/common/notifications_winrt/handler_functions.cpp @@ -2,19 +2,26 @@ #include "handler_functions.h" -using handler_function_t = void (*)(IBackgroundTaskInstance, const size_t button_id); +#include + +using handler_function_t = void (*)(const size_t button_id); namespace { const std::unordered_map handlers_map; } -void dispatch_to_backround_handler(std::wstring_view background_handler_id, IBackgroundTaskInstance bti, const size_t button_id) +void dispatch_to_backround_handler(std::wstring_view argument) { - const auto found_handler = handlers_map.find(background_handler_id); + winrt::Windows::Foundation::WwwFormUrlDecoder decoder{ argument }; + + const size_t button_id = std::stoi(decoder.GetFirstValueByName(L"button_id").c_str()); + auto handler = decoder.GetFirstValueByName(L"handler"); + + const auto found_handler = handlers_map.find(handler); if (found_handler == end(handlers_map)) { return; } - found_handler->second(std::move(bti), button_id); + found_handler->second(button_id); } diff --git a/src/common/notifications_winrt/handler_functions.h b/src/common/notifications_winrt/handler_functions.h index 098a0c18e5..8e35f66d11 100644 --- a/src/common/notifications_winrt/handler_functions.h +++ b/src/common/notifications_winrt/handler_functions.h @@ -1,5 +1,5 @@ #pragma once -using winrt::Windows::ApplicationModel::Background::IBackgroundTaskInstance; +#include -void dispatch_to_backround_handler(std::wstring_view background_handler_id, IBackgroundTaskInstance bti, const size_t button_id); +void dispatch_to_backround_handler(std::wstring_view argument); diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 012f67c087..badced2b6b 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -120,10 +120,7 @@ int runner(bool isProcessElevated) int result = -1; try { - if (winstore::running_as_packaged()) - { - notifications::register_background_toast_handler(); - } + notifications::register_background_toast_handler(); chdir_current_executable(); // Load Powertyos DLLS @@ -165,8 +162,46 @@ int runner(bool isProcessElevated) return result; } +// If the PT runner is launched as part of some action and manually by a user, e.g. being activated as a COM server +// for background toast notification handling, we should execute corresponding code flow instead of the main code flow. +enum class SpecialMode +{ + None, + Win32ToastNotificationCOMServer +}; + +SpecialMode should_run_in_special_mode() +{ + int nArgs; + LPWSTR* szArglist = CommandLineToArgvW(GetCommandLineW(), &nArgs); + for (size_t i = 1; i < nArgs; ++i) + { + if (!wcscmp(notifications::TOAST_ACTIVATED_LAUNCH_ARG, szArglist[i])) + return SpecialMode::Win32ToastNotificationCOMServer; + } + + return SpecialMode::None; +} + +int win32_toast_notification_COM_server_mode() +{ + notifications::run_desktop_app_activator_loop(); + return 0; +} + int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { + winrt::init_apartment(); + + switch (should_run_in_special_mode()) + { + case SpecialMode::Win32ToastNotificationCOMServer: + return win32_toast_notification_COM_server_mode(); + case SpecialMode::None: + // continue as usual + break; + } + wil::unique_mutex_nothrow msi_mutex; wil::unique_mutex_nothrow msix_mutex; @@ -235,12 +270,9 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine int result = 0; try { - winrt::init_apartment(); if (winstore::running_as_packaged()) { - notifications::register_background_toast_handler(); - std::thread{ [] { start_msi_uninstallation_sequence(); } }.detach();