mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-04 10:16:24 +02:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request CmdPal is leaking extensions processes as the process is immediately killed. This PR fixes the 1.5s wait for the process to end correctly and also close extensions. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ ] **Closes:** #xxx - [ ] **Communication:** I've discussed this with core contributors already. If work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end user facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed - Verified that if CmdPal closes in 1.5s, extensions and process are closed (note that GitHub extension doesn't close probably due to https://github.com/microsoft/PowerToys/pull/39209) - Verified that if CmdPal doesn't close in 1.5s the process is killed and extensions aren't closed (tested adding a delay in the code)
356 lines
10 KiB
C++
356 lines
10 KiB
C++
// dllmain.cpp : Defines the entry point for the DLL application.
|
|
#include "pch.h"
|
|
|
|
#include <interface/powertoy_module_interface.h>
|
|
|
|
#include <atomic>
|
|
#include <common/logger/logger.h>
|
|
#include <common/utils/logger_helper.h>
|
|
#include <common/SettingsAPI/settings_helpers.h>
|
|
#include <common/SettingsAPI/settings_objects.h>
|
|
#include <common/utils/resources.h>
|
|
#include <common/utils/package.h>
|
|
#include <common/utils/process_path.h>
|
|
#include <common/utils/winapi_error.h>
|
|
#include <common/interop/shared_constants.h>
|
|
#include <Psapi.h>
|
|
#include <TlHelp32.h>
|
|
#include <thread>
|
|
|
|
HINSTANCE g_hInst_cmdPal = 0;
|
|
|
|
BOOL APIENTRY DllMain(HMODULE hInstance,
|
|
DWORD ul_reason_for_call,
|
|
LPVOID)
|
|
{
|
|
switch (ul_reason_for_call)
|
|
{
|
|
case DLL_PROCESS_ATTACH:
|
|
g_hInst_cmdPal = hInstance;
|
|
break;
|
|
case DLL_THREAD_ATTACH:
|
|
case DLL_THREAD_DETACH:
|
|
case DLL_PROCESS_DETACH:
|
|
break;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
class CmdPal : public PowertoyModuleIface
|
|
{
|
|
private:
|
|
std::wstring app_name;
|
|
|
|
//contains the non localized key of the powertoy
|
|
std::wstring app_key;
|
|
|
|
HANDLE m_hTerminateEvent;
|
|
|
|
// Track if this is the first call to enable
|
|
bool firstEnableCall = true;
|
|
|
|
static bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated, bool silentFail)
|
|
{
|
|
std::wstring dir = std::filesystem::path(appPath).parent_path();
|
|
|
|
SHELLEXECUTEINFO sei = { 0 };
|
|
sei.cbSize = sizeof(SHELLEXECUTEINFO);
|
|
sei.hwnd = nullptr;
|
|
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;
|
|
if (silentFail)
|
|
{
|
|
sei.fMask = sei.fMask | SEE_MASK_FLAG_NO_UI;
|
|
}
|
|
sei.lpVerb = elevated ? L"runas" : L"open";
|
|
sei.lpFile = appPath.c_str();
|
|
sei.lpParameters = commandLineArgs.c_str();
|
|
sei.lpDirectory = dir.c_str();
|
|
sei.nShow = SW_SHOWNORMAL;
|
|
|
|
if (!ShellExecuteEx(&sei))
|
|
{
|
|
std::wstring error = get_last_error_or_default(GetLastError());
|
|
Logger::error(L"Failed to launch process. {}", error);
|
|
return false;
|
|
}
|
|
|
|
m_launched.store(true);
|
|
return true;
|
|
}
|
|
|
|
std::vector<DWORD> GetProcessesIdByName(const std::wstring& processName)
|
|
{
|
|
std::vector<DWORD> processIds;
|
|
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
|
|
|
if (snapshot != INVALID_HANDLE_VALUE)
|
|
{
|
|
PROCESSENTRY32 processEntry;
|
|
processEntry.dwSize = sizeof(PROCESSENTRY32);
|
|
|
|
if (Process32First(snapshot, &processEntry))
|
|
{
|
|
do
|
|
{
|
|
if (_wcsicmp(processEntry.szExeFile, processName.c_str()) == 0)
|
|
{
|
|
processIds.push_back(processEntry.th32ProcessID);
|
|
}
|
|
} while (Process32Next(snapshot, &processEntry));
|
|
}
|
|
|
|
CloseHandle(snapshot);
|
|
}
|
|
|
|
return processIds;
|
|
}
|
|
|
|
void TerminateCmdPal()
|
|
{
|
|
auto processIds = GetProcessesIdByName(L"Microsoft.CmdPal.UI.exe");
|
|
|
|
if (processIds.size() == 0)
|
|
{
|
|
Logger::trace(L"Nothing To PROCESS_TERMINATE");
|
|
return;
|
|
}
|
|
|
|
for (DWORD pid : processIds)
|
|
{
|
|
HANDLE hProcess = OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE, FALSE, pid);
|
|
|
|
if (hProcess != NULL)
|
|
{
|
|
SetEvent(m_hTerminateEvent);
|
|
|
|
// Wait for 1.5 seconds for the process to end correctly, allowing time for ETW tracer and extensions to stop
|
|
if (WaitForSingleObject(hProcess, 1500) == WAIT_TIMEOUT)
|
|
{
|
|
TerminateProcess(hProcess, 0);
|
|
}
|
|
|
|
CloseHandle(hProcess);
|
|
}
|
|
}
|
|
}
|
|
|
|
public:
|
|
static std::atomic<bool> m_enabled;
|
|
static std::atomic<bool> m_launched;
|
|
|
|
CmdPal()
|
|
{
|
|
app_name = L"CmdPal";
|
|
app_key = L"CmdPal";
|
|
LoggerHelpers::init_logger(app_key, L"ModuleInterface", "CmdPal");
|
|
|
|
m_hTerminateEvent = CreateDefaultEvent(CommonSharedConstants::CMDPAL_EXIT_EVENT);
|
|
}
|
|
|
|
~CmdPal()
|
|
{
|
|
CmdPal::m_enabled.store(false);
|
|
}
|
|
|
|
// Destroy the powertoy and free memory
|
|
virtual void destroy() override
|
|
{
|
|
Logger::trace("CmdPal::destroy()");
|
|
TerminateCmdPal();
|
|
delete this;
|
|
}
|
|
|
|
// Return the localized display name of the powertoy
|
|
virtual const wchar_t* get_name() override
|
|
{
|
|
return app_name.c_str();
|
|
}
|
|
|
|
// Return the non localized key of the powertoy, this will be cached by the runner
|
|
virtual const wchar_t* get_key() override
|
|
{
|
|
return app_key.c_str();
|
|
}
|
|
|
|
// Return the configured status for the gpo policy for the module
|
|
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
|
|
{
|
|
return powertoys_gpo::getConfiguredCmdPalEnabledValue();
|
|
}
|
|
|
|
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
|
|
{
|
|
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
|
|
|
|
// Create a Settings object.
|
|
PowerToysSettings::Settings settings(hinstance, get_name());
|
|
|
|
return settings.serialize_to_buffer(buffer, buffer_size);
|
|
}
|
|
|
|
virtual void call_custom_action(const wchar_t* /*action*/) override
|
|
{
|
|
}
|
|
|
|
virtual void set_config(const wchar_t* config) override
|
|
{
|
|
try
|
|
{
|
|
// Parse the input JSON string.
|
|
PowerToysSettings::PowerToyValues values =
|
|
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
|
|
|
|
// If you don't need to do any custom processing of the settings, proceed
|
|
// to persists the values calling:
|
|
values.save_to_settings_file();
|
|
// Otherwise call a custom function to process the settings before saving them to disk:
|
|
// save_settings();
|
|
}
|
|
catch (std::exception&)
|
|
{
|
|
// Improper JSON.
|
|
}
|
|
}
|
|
|
|
virtual void enable()
|
|
{
|
|
Logger::trace("CmdPal::enable()");
|
|
|
|
CmdPal::m_enabled.store(true);
|
|
|
|
std::wstring packageName = L"Microsoft.CommandPalette";
|
|
// Launch CmdPal as normal user using explorer
|
|
std::wstring launchPath = L"explorer.exe";
|
|
std::wstring launchArgs = L"x-cmdpal://background";
|
|
#ifdef IS_DEV_BRANDING
|
|
packageName = L"Microsoft.CommandPalette.Dev";
|
|
#endif
|
|
|
|
if (!package::GetRegisteredPackage(packageName, false).has_value())
|
|
{
|
|
try
|
|
{
|
|
Logger::info(L"CmdPal not installed. Installing...");
|
|
|
|
std::wstring installationFolder = get_module_folderpath();
|
|
#ifdef _DEBUG
|
|
std::wstring archSubdir = L"x64";
|
|
#ifdef _M_ARM64
|
|
archSubdir = L"ARM64";
|
|
#endif
|
|
auto msix = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\CmdPal\\AppPackages\\Microsoft.CmdPal.UI_0.0.1.0_Debug_Test\\", false);
|
|
auto dependencies = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\CmdPal\\AppPackages\\Microsoft.CmdPal.UI_0.0.1.0_Debug_Test\\Dependencies\\" + archSubdir + L"\\", true);
|
|
#else
|
|
auto msix = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\CmdPal\\", false);
|
|
auto dependencies = package::FindMsixFile(installationFolder + L"\\WinUI3Apps\\CmdPal\\Dependencies\\", true);
|
|
#endif
|
|
|
|
if (!msix.empty())
|
|
{
|
|
auto msixPath = msix[0];
|
|
|
|
if (!package::RegisterPackage(msixPath, dependencies))
|
|
{
|
|
Logger::error(L"Failed to install CmdPal package");
|
|
}
|
|
}
|
|
}
|
|
catch (std::exception& e)
|
|
{
|
|
std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
|
|
errorMessage += e.what();
|
|
Logger::error(errorMessage);
|
|
}
|
|
}
|
|
|
|
if (!package::GetRegisteredPackage(packageName, false).has_value())
|
|
{
|
|
Logger::error("Cmdpal is not registered, quit..");
|
|
return;
|
|
}
|
|
|
|
if (!firstEnableCall)
|
|
{
|
|
Logger::trace("Not first attempt, try to launch");
|
|
LaunchApp(launchPath, launchArgs, false /*no elevated*/, false /*error pop up*/);
|
|
}
|
|
else
|
|
{
|
|
// If first time enable, do retry launch.
|
|
Logger::trace("First attempt, try to launch");
|
|
std::thread launchThread(&CmdPal::RetryLaunch, launchPath, launchArgs);
|
|
launchThread.detach();
|
|
}
|
|
|
|
firstEnableCall = false;
|
|
}
|
|
|
|
virtual void disable()
|
|
{
|
|
Logger::trace("CmdPal::disable()");
|
|
TerminateCmdPal();
|
|
|
|
CmdPal::m_enabled.store(false);
|
|
}
|
|
|
|
static void RetryLaunch(std::wstring path, std::wstring cmdArgs)
|
|
{
|
|
const int base_delay_milliseconds = 1000;
|
|
int max_retry = 9; // 2**9 - 1 seconds. Control total wait time within 10 min.
|
|
int retry = 0;
|
|
do
|
|
{
|
|
auto launch_result = LaunchApp(path, cmdArgs, false, retry < max_retry);
|
|
if (launch_result)
|
|
{
|
|
Logger::info(L"CmdPal launched successfully after {} retries.", retry);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
Logger::error(L"Retry {} launch CmdPal launch failed.", retry);
|
|
}
|
|
|
|
// When we got max retry, we don't need to wait for the next retry.
|
|
if (retry < max_retry)
|
|
{
|
|
int delay = base_delay_milliseconds * (1 << (retry));
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(delay));
|
|
}
|
|
++retry;
|
|
} while (retry <= max_retry && m_enabled.load() && !m_launched.load());
|
|
|
|
if (!m_enabled.load() || m_launched.load())
|
|
{
|
|
Logger::error(L"Retry cancelled. CmdPal is disabled or already launched.");
|
|
}
|
|
else
|
|
{
|
|
Logger::error(L"CmdPal launch failed after {} attempts.", retry);
|
|
}
|
|
}
|
|
|
|
virtual bool on_hotkey(size_t) override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
virtual size_t get_hotkeys(Hotkey*, size_t) override
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
virtual bool is_enabled() override
|
|
{
|
|
return CmdPal::m_enabled.load();
|
|
}
|
|
};
|
|
|
|
std::atomic<bool> CmdPal::m_enabled{ false };
|
|
std::atomic<bool> CmdPal::m_launched{ false };
|
|
|
|
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
|
{
|
|
return new CmdPal();
|
|
}
|