[Launcher] Use a keyboard hook in the runner to invoke the Launcher (#6660)

* Added a keyboard hook to the runner

* Update RootKeyboardHook

* Enable reading the whole JsonObject property

* Renamed RootKeyboardHook to CentralizedKeyboardHook

* Fixed build break, changed callback return type to bool

* Added Hotkey struct which somehow went missing

+ Cherry-pick fixes

* Reorganized the kb hook

* Basic version works

* Various fixes

* Finishing touches

* Fix potential threading issue

* int -> size_t

* Add default initializers to the Hotkey struct

* Added a suggested comment

* Unified a constant

* Use C# classes instead of native calls for sync

* Added a claryfing comment

* Use std::move

* Renamed a method

* Possible fix for compilation errors

* Fix a regression

* Show a message on failure

* Added DISABLE_LOWLEVEL_HOOK support

* Allow running Launcher as standalone

* Rename string constants
This commit is contained in:
Ivan Stošić
2020-09-21 12:44:16 +02:00
committed by GitHub
parent e135153c45
commit b266e336b5
20 changed files with 392 additions and 40 deletions

View File

@@ -190,7 +190,8 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
}
else
{
params = args[nextArg];
params += args[nextArg];
params += L' ';
nextArg++;
}
}

View File

@@ -127,5 +127,10 @@ public
{
public:
literal int VK_WIN_BOTH = CommonSharedConstants::VK_WIN_BOTH;
static String^ PowerLauncherSharedEvent()
{
return gcnew String(CommonSharedConstants::POWER_LAUNCHER_SHARED_EVENT);
}
};
}

View File

@@ -343,6 +343,11 @@ namespace PowerToysSettings
return m_json.GetNamedObject(L"properties").GetNamedObject(property_name).GetNamedObject(L"value");
}
json::JsonObject PowerToyValues::get_raw_json()
{
return m_json;
}
std::wstring PowerToyValues::serialize()
{
set_version();

View File

@@ -83,6 +83,7 @@ namespace PowerToysSettings
std::optional<int> get_int_value(std::wstring_view property_name);
std::optional<std::wstring> get_string_value(std::wstring_view property_name);
std::optional<json::JsonObject> get_json(std::wstring_view property_name);
json::JsonObject get_raw_json();
std::wstring serialize();
void save_to_settings_file();

View File

@@ -8,4 +8,7 @@ namespace CommonSharedConstants
// Fake key code to represent VK_WIN.
inline const DWORD VK_WIN_BOTH = 0x104;
}
// Path to the event used by PowerLauncher
const wchar_t POWER_LAUNCHER_SHARED_EVENT[] = L"Local\\PowerToysRunInvokeEvent-30f26ad7-d36d-4c0e-ab02-68bb5ff3c4ab";
}

View File

@@ -1,5 +1,7 @@
#pragma once
#include <compare>
/*
DLL Interface for PowerToys. The powertoy_create() (see below) must return
an object that implements this interface.
@@ -13,6 +15,7 @@
On the received object, the runner will call:
- get_name() to get the name of the PowerToy,
- enable() to initialize the PowerToy.
- get_hotkeys() to register the hotkeys the PowerToy uses.
While running, the runner might call the following methods between create_powertoy()
and destroy():
@@ -20,15 +23,31 @@
- get_config() to get the available configuration settings,
- set_config() to set various settings,
- call_custom_action() when the user selects clicks a custom action in settings,
- get_hotkeys() when the settings change, to make sure the hotkey(s) are up to date.
- on_hotkey() when the corresponding hotkey is pressed.
When terminating, the runner will:
- call destroy() which should free all the memory and delete the PowerToy object,
- unload the DLL.
The runner will call on_hotkey() even if the module is disabled.
*/
class PowertoyModuleIface
{
public:
/* Describes a hotkey which can trigger an action in the PowerToy */
struct Hotkey
{
bool win = false;
bool ctrl = false;
bool shift = false;
bool alt = false;
unsigned char key = 0;
std::strong_ordering operator<=>(const Hotkey&) const = default;
};
/* Returns the name of the PowerToy, this will be cached by the runner. */
virtual const wchar_t* get_name() = 0;
/* Fills a buffer with the available configuration settings.
@@ -49,6 +68,18 @@ public:
virtual bool is_enabled() = 0;
/* Destroy the PowerToy and free all memory. */
virtual void destroy() = 0;
/* Get the list of hotkeys. Should return the number of available hotkeys and
* fill up the buffer to the minimum of the number of hotkeys and its size.
* Modules do not need to override this method, it will return zero by default.
* This method is called even when the module is disabled.
*/
virtual size_t get_hotkeys(Hotkey* buffer, size_t buffer_size) { return 0; }
/* Called when one of the registered hotkeys is pressed. Should return true
* if the key press is to be swallowed.
*/
virtual bool on_hotkey(size_t hotkeyId) { return false; }
};
/*

View File

@@ -2,6 +2,7 @@
#include <interface/powertoy_module_interface.h>
#include <common/settings_objects.h>
#include <common/common.h>
#include <common/shared_constants.h>
#include "trace.h"
#include "Generated Files/resource.h"
#include <common/os-detect.h>
@@ -10,7 +11,14 @@ extern "C" IMAGE_DOS_HEADER __ImageBase;
namespace
{
#define POWER_LAUNCHER_PID_SHARED_FILE L"Local\\3cbfbad4-199b-4e2c-9825-942d5d3d3c74"
const wchar_t POWER_LAUNCHER_PID_SHARED_FILE[] = L"Local\\3cbfbad4-199b-4e2c-9825-942d5d3d3c74";
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_WIN[] = L"win";
const wchar_t JSON_KEY_ALT[] = L"alt";
const wchar_t JSON_KEY_CTRL[] = L"ctrl";
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_OPEN_POWERLAUNCHER[] = L"open_powerlauncher";
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
@@ -54,12 +62,27 @@ private:
// Time to wait for process to close after sending WM_CLOSE signal
static const int MAX_WAIT_MILLISEC = 10000;
// Hotkey to invoke the module
Hotkey m_hotkey = { .key = 0 };
// Helper function to extract the hotkey from the settings
void parse_hotkey(PowerToysSettings::PowerToyValues& settings);
// Handle to event used to invoke the Runner
HANDLE m_hEvent;
public:
// Constructor
Microsoft_Launcher()
{
app_name = GET_RESOURCE_STRING(IDS_LAUNCHER_NAME);
init_settings();
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.bInheritHandle = false;
sa.lpSecurityDescriptor = NULL;
m_hEvent = CreateEventW(&sa, FALSE, FALSE, CommonSharedConstants::POWER_LAUNCHER_SHARED_EVENT);
};
~Microsoft_Launcher()
@@ -122,6 +145,7 @@ public:
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::from_json_string(config);
parse_hotkey(values);
// If you don't need to do any custom processing of the settings, proceed
// to persists the values calling:
values.save_to_settings_file();
@@ -137,6 +161,7 @@ public:
// Enable the powertoy
virtual void enable()
{
ResetEvent(m_hEvent);
// Start PowerLauncher.exe only if the OS is 19H1 or higher
if (UseNewSettings())
{
@@ -144,8 +169,10 @@ public:
if (!is_process_elevated(false))
{
std::wstring executable_args = L"";
executable_args.append(std::to_wstring(powertoys_pid));
std::wstring executable_args;
executable_args += L" -powerToysPid ";
executable_args += std::to_wstring(powertoys_pid);
executable_args += L" --centralized-kb-hook";
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
@@ -165,7 +192,8 @@ public:
params += L"-target modules\\launcher\\PowerLauncher.exe ";
params += L"-pidFile ";
params += POWER_LAUNCHER_PID_SHARED_FILE;
params += L" " + std::to_wstring(powertoys_pid) + L" ";
params += L" -powerToysPid " + std::to_wstring(powertoys_pid) + L" ";
params += L"--centralized-kb-hook ";
action_runner_path += L"\\action_runner.exe";
// Set up the shared file from which to retrieve the PID of PowerLauncher
@@ -206,6 +234,7 @@ public:
{
if (m_enabled)
{
ResetEvent(m_hEvent);
terminateProcess();
}
@@ -218,6 +247,37 @@ public:
return m_enabled;
}
// Return the invocation hotkey
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
if (m_hotkey.key)
{
if (hotkeys && buffer_size >= 1)
{
hotkeys[0] = m_hotkey;
}
return 1;
}
else
{
return 0;
}
}
// Process the hotkey event
virtual bool on_hotkey(size_t hotkeyId) override
{
// For now, hotkeyId will always be zero
if (m_enabled)
{
SetEvent(m_hEvent);
return true;
}
return false;
}
// Callback to send WM_CLOSE signal to each top level window.
static BOOL CALLBACK requestMainWindowClose(HWND nextWindow, LPARAM closePid)
{
@@ -251,6 +311,8 @@ void Microsoft_Launcher::init_settings()
// Load and parse the settings file for this PowerToy.
PowerToysSettings::PowerToyValues settings =
PowerToysSettings::PowerToyValues::load_from_settings_file(get_name());
parse_hotkey(settings);
}
catch (std::exception ex)
{
@@ -258,6 +320,23 @@ void Microsoft_Launcher::init_settings()
}
}
void Microsoft_Launcher::parse_hotkey(PowerToysSettings::PowerToyValues& settings)
{
try
{
auto jsonHotkeyObject = settings.get_raw_json().GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_OPEN_POWERLAUNCHER);
m_hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
m_hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
m_hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
}
catch (...)
{
m_hotkey.key = 0;
}
}
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new Microsoft_Launcher();

View File

@@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using ManagedCommon;
using Microsoft.PowerLauncher.Telemetry;
@@ -30,7 +31,6 @@ namespace PowerLauncher
private const string Unique = "PowerLauncher_Unique_Application_Mutex";
private static bool _disposed;
private static int _powerToysPid;
private Settings _settings;
private MainViewModel _mainVM;
private MainWindow _mainWindow;
@@ -40,15 +40,10 @@ namespace PowerLauncher
private SettingsWatcher _settingsWatcher;
[STAThread]
public static void Main(string[] args)
public static void Main()
{
if (SingleInstance<App>.InitializeAsFirstInstance(Unique))
{
if (args?.Length > 0)
{
_ = int.TryParse(args[0], out _powerToysPid);
}
using (var application = new App())
{
application.InitializeComponent();
@@ -59,17 +54,29 @@ namespace PowerLauncher
private void OnStartup(object sender, StartupEventArgs e)
{
RunnerHelper.WaitForPowerToysRunner(_powerToysPid, () =>
for (int i = 0; i + 1 < e.Args.Length; i++)
{
try
if (e.Args[i] == "-powerToysPid")
{
Dispose();
int powerToysPid;
if (int.TryParse(e.Args[i + 1], out powerToysPid))
{
RunnerHelper.WaitForPowerToysRunner(powerToysPid, () =>
{
try
{
Dispose();
}
finally
{
Environment.Exit(0);
}
});
}
break;
}
finally
{
Environment.Exit(0);
}
});
}
var bootTime = new System.Diagnostics.Stopwatch();
bootTime.Start();
@@ -85,6 +92,7 @@ namespace PowerLauncher
_settingsVM = new SettingWindowViewModel();
_settings = _settingsVM.Settings;
_settings.UsePowerToysRunnerKeyboardHook = e.Args.Contains("--centralized-kb-hook");
_alphabet.Initialize(_settings);
_stringMatcher = new StringMatcher(_alphabet);

View File

@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Windows;
namespace PowerLauncher.Helper
{
public static class NativeEventWaiter
{
public static void WaitForEventLoop(string eventName, Action callback)
{
new Thread(() =>
{
var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
while (true)
{
if (eventHandle.WaitOne())
{
Application.Current.Dispatcher.Invoke(callback);
}
}
}).Start();
}
}
}

View File

@@ -55,7 +55,6 @@ namespace PowerLauncher.ViewModel
public MainViewModel(Settings settings)
{
HotkeyManager = new HotkeyManager();
_saved = false;
_queryTextBeforeLeaveResults = string.Empty;
_currentQuery = _emptyQuery;
@@ -78,27 +77,36 @@ namespace PowerLauncher.ViewModel
InitializeKeyCommands();
RegisterResultsUpdatedEvent();
_settings.PropertyChanged += (s, e) =>
if (settings != null && settings.UsePowerToysRunnerKeyboardHook)
{
if (e.PropertyName == nameof(Settings.Hotkey))
NativeEventWaiter.WaitForEventLoop(Constants.PowerLauncherSharedEvent(), OnHotkey);
_hotkeyHandle = 0;
}
else
{
HotkeyManager = new HotkeyManager();
_settings.PropertyChanged += (s, e) =>
{
Application.Current.Dispatcher.Invoke(() =>
if (e.PropertyName == nameof(Settings.Hotkey))
{
if (!string.IsNullOrEmpty(_settings.PreviousHotkey))
Application.Current.Dispatcher.Invoke(() =>
{
HotkeyManager.UnregisterHotkey(_hotkeyHandle);
}
if (!string.IsNullOrEmpty(_settings.PreviousHotkey))
{
HotkeyManager.UnregisterHotkey(_hotkeyHandle);
}
if (!string.IsNullOrEmpty(_settings.Hotkey))
{
SetHotkey(_settings.Hotkey, OnHotkey);
}
});
}
};
if (!string.IsNullOrEmpty(_settings.Hotkey))
{
SetHotkey(_settings.Hotkey, OnHotkey);
}
});
}
};
SetHotkey(_settings.Hotkey, OnHotkey);
SetCustomPluginHotkey();
SetHotkey(_settings.Hotkey, OnHotkey);
SetCustomPluginHotkey();
}
}
private void RegisterResultsUpdatedEvent()

View File

@@ -172,6 +172,8 @@ namespace Wox.Infrastructure.UserSettings
public bool IgnoreHotkeysOnFullscreen { get; set; }
public bool UsePowerToysRunnerKeyboardHook { get; set; }
public HttpProxy Proxy { get; set; } = new HttpProxy();
[JsonConverter(typeof(StringEnumConverter))]

View File

@@ -0,0 +1,131 @@
#include "pch.h"
#include "centralized_kb_hook.h"
#include "common/common.h"
namespace CentralizedKeyboardHook
{
struct HotkeyDescriptor
{
Hotkey hotkey;
std::wstring moduleName;
std::function<bool()> action;
bool operator<(const HotkeyDescriptor& other) const
{
return hotkey < other.hotkey;
};
};
std::multiset<HotkeyDescriptor> hotkeyDescriptors;
std::mutex mutex;
HHOOK hHook{};
struct DestroyOnExit
{
~DestroyOnExit()
{
Stop();
}
} destroyOnExitObj;
LRESULT CALLBACK KeyboardHookProc(_In_ int nCode, _In_ WPARAM wParam, _In_ LPARAM lParam)
{
if (nCode < 0 || ((wParam != WM_KEYDOWN) && (wParam != WM_SYSKEYDOWN)))
{
return CallNextHookEx(hHook, nCode, wParam, lParam);
}
const auto& keyPressInfo = *reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
Hotkey hotkey{
.win = (GetAsyncKeyState(VK_LWIN) & 0x8000) || (GetAsyncKeyState(VK_RWIN) & 0x8000),
.ctrl = static_cast<bool>(GetAsyncKeyState(VK_CONTROL) & 0x8000),
.shift = static_cast<bool>(GetAsyncKeyState(VK_SHIFT) & 0x8000),
.alt = static_cast<bool>(GetAsyncKeyState(VK_MENU) & 0x8000),
.key = static_cast<unsigned char>(keyPressInfo.vkCode)
};
std::function<bool()> action;
{
// Hold the lock for the shortest possible duration
std::unique_lock lock{ mutex };
HotkeyDescriptor dummy{ .hotkey = hotkey };
auto it = hotkeyDescriptors.find(dummy);
if (it != hotkeyDescriptors.end())
{
action = it->action;
}
}
if (action)
{
if (action())
{
// After invoking the hotkey send a dummy key to prevent Start Menu from activating
INPUT dummyEvent[1] = {};
dummyEvent[0].type = INPUT_KEYBOARD;
dummyEvent[0].ki.wVk = 0xFF;
dummyEvent[0].ki.dwFlags = KEYEVENTF_KEYUP;
SendInput(1, dummyEvent, sizeof(INPUT));
// Swallow the key press
return 1;
}
}
return CallNextHookEx(hHook, nCode, wParam, lParam);
}
void SetHotkeyAction(const std::wstring& moduleName, const Hotkey& hotkey, std::function<bool()>&& action) noexcept
{
std::unique_lock lock{ mutex };
hotkeyDescriptors.insert({ .hotkey = hotkey, .moduleName = moduleName, .action = std::move(action) });
}
void ClearModuleHotkeys(const std::wstring& moduleName) noexcept
{
std::unique_lock lock{ mutex };
auto it = hotkeyDescriptors.begin();
while (it != hotkeyDescriptors.end())
{
if (it->moduleName == moduleName)
{
it = hotkeyDescriptors.erase(it);
}
else
{
++it;
}
}
}
void Start() noexcept
{
#if defined(DISABLE_LOWLEVEL_HOOKS_WHEN_DEBUGGED)
const bool hook_disabled = IsDebuggerPresent();
#else
const bool hook_disabled = false;
#endif
if (!hook_disabled)
{
if (!hHook)
{
hHook = SetWindowsHookExW(WH_KEYBOARD_LL, KeyboardHookProc, NULL, NULL);
if (!hHook)
{
DWORD errorCode = GetLastError();
show_last_error_message(L"SetWindowsHookEx", errorCode, L"centralized_kb_hook");
}
}
}
}
void Stop() noexcept
{
if (hHook && UnhookWindowsHookEx(hHook))
{
hHook = NULL;
}
}
}

View File

@@ -0,0 +1,13 @@
#include "pch.h"
#include "../modules/interface/powertoy_module_interface.h"
namespace CentralizedKeyboardHook
{
using Hotkey = PowertoyModuleIface::Hotkey;
void Start() noexcept;
void Stop() noexcept;
void SetHotkeyAction(const std::wstring& moduleName, const Hotkey& hotkey, std::function<bool()>&& action) noexcept;
void ClearModuleHotkeys(const std::wstring& moduleName) noexcept;
};

View File

@@ -27,6 +27,7 @@
#include <Psapi.h>
#include <RestartManager.h>
#include "centralized_kb_hook.h"
#if _DEBUG && _WIN64
#include "unhandled_exception_handler.h"
@@ -89,6 +90,7 @@ int runner(bool isProcessElevated)
#endif
Trace::RegisterProvider();
start_tray_icon();
CentralizedKeyboardHook::Start();
int result = -1;
try

View File

@@ -24,6 +24,7 @@
#include <tuple>
#include <unordered_set>
#include <string>
#include <set>
#include <ProjectTelemetry.h>
#include <winrt/Windows.ApplicationModel.h>

View File

@@ -1,5 +1,6 @@
#include "pch.h"
#include "powertoy_module.h"
#include "centralized_kb_hook.h"
std::map<std::wstring, PowertoyModule>& modules()
{
@@ -42,4 +43,24 @@ PowertoyModule::PowertoyModule(PowertoyModuleIface* module, HMODULE handle) :
{
throw std::runtime_error("Module not initialized");
}
update_hotkeys();
}
void PowertoyModule::update_hotkeys()
{
CentralizedKeyboardHook::ClearModuleHotkeys(module->get_name());
size_t hotkeyCount = module->get_hotkeys(nullptr, 0);
std::vector<PowertoyModuleIface::Hotkey> hotkeys(hotkeyCount);
module->get_hotkeys(hotkeys.data(), hotkeyCount);
auto modulePtr = module.get();
for (size_t i = 0; i < hotkeyCount; i++)
{
CentralizedKeyboardHook::SetHotkeyAction(module->get_name(), hotkeys[i], [modulePtr, i] {
return modulePtr->on_hotkey(i);
});
}
}

View File

@@ -40,6 +40,8 @@ public:
json::JsonObject json_config() const;
void update_hotkeys();
private:
std::unique_ptr<HMODULE, PowertoyModuleDLLDeleter> handle;
std::unique_ptr<PowertoyModuleIface, PowertoyModuleDeleter> module;

View File

@@ -120,6 +120,7 @@
<ClCompile Include="powertoy_module.cpp" />
<ClCompile Include="main.cpp" />
<ClCompile Include="restart_elevated.cpp" />
<ClCompile Include="centralized_kb_hook.cpp" />
<ClCompile Include="settings_window.cpp" />
<ClCompile Include="trace.cpp" />
<ClCompile Include="tray_icon.cpp" />
@@ -132,6 +133,7 @@
<ClInclude Include="auto_start_helper.h" />
<ClInclude Include="general_settings.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="centralized_kb_hook.h" />
<ClInclude Include="update_utils.h" />
<ClInclude Include="update_state.h" />
<ClInclude Include="powertoy_module.h" />

View File

@@ -36,6 +36,9 @@
<ClCompile Include="action_runner_utils.cpp">
<Filter>Utils</Filter>
</ClCompile>
<ClCompile Include="centralized_kb_hook.cpp">
<Filter>Utils</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
@@ -73,7 +76,10 @@
<Filter>Utils</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
<Filter>Utils</Filter>
</ClInclude>
<ClInclude Include="centralized_kb_hook.h">
<Filter>Utils</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>

View File

@@ -12,6 +12,7 @@
#include "common/common.h"
#include "restart_elevated.h"
#include "update_utils.h"
#include "centralized_kb_hook.h"
#include <common/json.h>
#include <common\settings_helpers.cpp>
@@ -106,9 +107,11 @@ std::optional<std::wstring> dispatch_json_action_to_module(const json::JsonObjec
void send_json_config_to_module(const std::wstring& module_key, const std::wstring& settings)
{
if (modules().find(module_key) != modules().end())
auto moduleIt = modules().find(module_key);
if (moduleIt != modules().end())
{
modules().at(module_key)->set_config(settings.c_str());
moduleIt->second->set_config(settings.c_str());
moduleIt->second.update_hotkeys();
}
}