Compare commits

..

12 Commits

Author SHA1 Message Date
Muyuan Li (from Dev Box)
df1d6ea7e0 Fix ObtainInstaller offline regression, add missing test includes
- Move readyToInstall and upToDate state checks before the GitHub API
  call so already-downloaded installers can proceed when GitHub is
  unreachable
- Add missing <algorithm>, <iterator>, <vector> includes in
  UpdatingTests.cpp to avoid transitive-include breakage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-14 18:12:20 +08:00
Muyuan Li (from Dev Box)
31e30280de Style: remove extra blank line in Stage 2 relaunch block (PR #46889) 2026-05-14 08:58:55 +08:00
Clint Rutkas
5074588c9a Fix remaining pre-existing bugs D3-D7 in update system
D3 (#46965): Fix CommandLineToArgvW memory leak — added wil::scope_exit
    to LocalFree(args) on all exit paths.

D4 (#46966): Wait for PowerToys to exit after WM_CLOSE — Stage 1 now
    calls GetWindowThreadProcessId + WaitForSingleObject (10s timeout)
    before launching Stage 2, preventing file-in-use installer failures.

D5 (#46967): Use unique temp path — CopySelfToTempDir now appends PID
    to the temp filename, preventing collision on concurrent updates.

D6 (#46968): Remove dead code — deleted unused UPDATE_STAGE2_RESTART_PT
    and UPDATE_STAGE2_DONT_START_PT constants from UpdateUtils.h.

D7 (#46969): Restore configs on failed install — RestoreCorruptedConfigs
    now runs after Stage 2 regardless of success/failure, since a failed
    install may still corrupt config files.

All 30 tests pass locally (vstest.console.exe, x64 Release).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-12 20:12:46 -07:00
Clint Rutkas
df7a41b457 Fix critical pre-existing bugs in ObtainInstaller and Stage 2 args
D1 (CRITICAL): ObtainInstaller dereferenced *new_version_info via
std::holds_alternative BEFORE checking !new_version_info for errors.
When GitHub API is unreachable, this is undefined behavior / crash.
Fix: check !new_version_info first, return early on error.
All 3 independent code reviewers flagged this.

D2 (HIGH): Stage 2 WinMain used args[2] without checking nArgs >= 3.
If -update_now_stage_2 was invoked without an installer path, this
was an out-of-bounds array read.
Fix: add nArgs < 3 check before Stage 2 processing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-12 19:27:36 -07:00
Clint Rutkas
b0b073f088 Post-review fixes: add backup integrity check, detailed comments, cut/add tests
Multi-agent code review (Opus 4.6 + GPT-5.4) identified:
- B1: RestoreCorruptedConfigs now validates backup integrity before
  restoring — won't copy corrupted backup over corrupted original
- Cut 2 redundant tests (BackupCreatesConfigBackupDirectory,
  SimulateUpgradeWithNoCorruption)
- Added 4 new tests: RestoreSkipsDeletedOriginals,
  RestoreSkipsCorruptedBackup, BackupSkipsNonJsonFilesInModuleDirs,
  BackupEmptyRootDirSucceeds
- Merged CanRelaunch true/false into single CanRelaunchReflectsArgCount
- Added detailed comments to all 30 tests explaining what product
  code each tests and why
- BuildPowerToysExePath now uses fs::path for correctness
- TempDir uses unique paths per test instance

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-12 19:23:11 -07:00
Clint Rutkas
c56258d2d3 Extract update lifecycle logic and add Stage 1/2 handoff tests
The relaunch failure (#42004, #43011, #44071) was caused by Stage 2
never receiving the install directory and never relaunching PT.
Extract the argument-building and path-construction logic into
common/updating/updateLifecycle.h so it can be tested:

- BuildStage2Arguments: builds quoted command line with installer
  path + install dir (the install dir was completely missing before)
- BuildPowerToysExePath: constructs the PT.exe relaunch path
- CanRelaunchAfterUpdate: checks if Stage 1 provided the install dir

New UpdateLifecycleTests (8 tests):
- Stage 2 args contain both installer and install dir
- Both paths are properly quoted for spaces
- PT.exe path built correctly (with/without trailing backslash)
- CanRelaunch returns false for old Stage 1 (3 args = no install dir)
- CommandLineToArgvW round-trip: proves the exact Windows command
  line parsing produces the correct argv[2] and argv[3] values

All 29 tests built and pass locally (vstest.console.exe, x64 Release).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 22:56:41 -07:00
Clint Rutkas
236c241107 Add upgrade simulation tests, verify all 21 tests pass locally
Added UpgradeSimulationTests class with 3 end-to-end scenarios:
- SimulateUpgradeWithCorruption: 5 modules, 2 corrupted by installer,
  restore recovers corrupted files, leaves clean ones untouched
- SimulateUpgradeWithNoCorruption: clean install, user changes
  between backup and restore are preserved (not overwritten)
- SimulateUpgradeFromVeryOldVersion: old version with fewer modules,
  new modules created by installer are not affected by restore

All 21 tests built and run locally with vstest.console.exe:
  7 IsJsonFileCorrupted tests
  5 BackupConfigFiles tests
  5 RestoreCorruptedConfigs tests
  3 UpgradeSimulation tests (new)
  1 FullBackupAndRestoreRoundTrip

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 22:31:52 -07:00
Clint Rutkas
d6f61c8439 Fix linker errors: remove VersionHelper tests and strip external deps
VersionHelper tests caused LNK2001 because the test project didn't
link version.lib. Rather than adding the dependency, remove those
tests (already covered by UnitTests-CommonLib) and strip the project
to zero external deps:
- Remove CppWinRT NuGet (not needed for pure file I/O tests)
- Remove RuntimeObject.lib
- Remove WinRT includes from pch.h
- Remove packages.config
- Keep only configBackup tests (IsJsonFileCorrupted, Backup, Restore)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 15:58:22 -07:00
Clint Rutkas
b387bbffdd Fix CI failures: remove spdlog dependency from test, fix settings test default
1. Remove logger/SettingsAPI dependencies from configBackup.h so the
   test project compiles without spdlog NuGet. Logging is done at
   call sites in PowerToys.Update.cpp instead.
2. Fix SetSettingCommandTests: AutoDownloadUpdates test now sets
   'false' (not 'true') since the default changed to true.
3. Remove UpdateState test class that also needed spdlog-dependent
   headers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 15:20:24 -07:00
Clint Rutkas
5003c6c758 Add Updating.UnitTests project to PowerToys.slnx
Register the new test project in the solution so it is built and
run by CI (VSBuild Build;Test targets). The project name ends with
'Tests' so it is correctly skipped in release builds when
BuildTests=false.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 14:21:37 -07:00
Clint Rutkas
b2ef4c85bc Extract config backup to shared header and add unit tests
- Extract BackupConfigFiles, RestoreCorruptedConfigs, and
  IsJsonFileCorrupted into common/updating/configBackup.h so they
  can be tested independently and reused by other components
- Add path-parameterized overloads for test isolation
- Add Updating.UnitTests project (CppUnitTest framework) with:
  - IsJsonFileCorruptedTests: clean files, null bytes, large files,
    edge cases matching #46179 corruption pattern
  - BackupConfigFilesTests: directory creation, JSON-only copy,
    module subdirectories, skips Updates/ConfigBackup dirs
  - RestoreCorruptedConfigsTests: selective restore of corrupted
    files, leaves clean files untouched, full round-trip test
  - UpdateState and VersionHelper serialization sanity checks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 22:41:58 -07:00
Clint Rutkas
24d56f0524 Fix auto-update relaunch, add config backup, enable auto-download by default
Addresses three critical issues with the PowerToys update experience:

1. Fix relaunch after update (#42004, #43011, #44071):
   - Stage 1 now passes the install directory to Stage 2
   - Stage 2 relaunches PowerToys.exe with -report_update_success
     after a successful install, so PT restarts automatically

2. Add config backup/restore to prevent data corruption (#46179):
   - BackupConfigFiles() snapshots all JSON configs before update
   - RestoreCorruptedConfigs() detects null-byte corruption after
     install and restores from backup automatically

3. Enable auto-download by default (GeneralSettings.cs):
   - New installations now default to AutoDownloadUpdates=true
   - Existing user preferences are preserved (read from settings.json)
   - Combined with the relaunch fix, this means most users will
     seamlessly stay current without manual intervention

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 22:24:12 -07:00
176 changed files with 1873 additions and 3690 deletions

View File

@@ -107,7 +107,7 @@
^src/common/sysinternals/Eula/
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherComparisonTests.cs$
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherDiacriticsTests.cs$
^doc/devdocs/modules/cmdpal/initial-sdk-spec/list-elements-mock-002\.pdn$
^src/modules/cmdpal/doc/initial-sdk-spec/list-elements-mock-002\.pdn$
^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$
^src/modules/cmdpal/Microsoft\.CmdPal\.UI/Settings/InternalPage\.SampleData\.cs$
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Common\.UnitTests/.*\.TestData\.cs$

View File

@@ -1641,7 +1641,6 @@ tracelogging
tracerpt
trackbar
trafficmanager
traies
transicc
TRAYMOUSEMESSAGE
triaging
@@ -1662,7 +1661,6 @@ UBR
UCallback
ucrt
ucrtd
udit
uefi
uesc
UFlags
@@ -2343,9 +2341,3 @@ YTimer
zamora
zonability
Zorder
LLMHF
RIGHTBUTTON
SIZEALL
grabandmove
GRABANDMOVEMODULEINTERFACE
INITCOMMONCONTROLSEX

View File

@@ -250,9 +250,6 @@
"PowerToys.ZoomItModuleInterface.dll",
"PowerToys.ZoomItSettingsInterop.dll",
"PowerToys.GrabAndMove.exe",
"PowerToys.GrabAndMoveModuleInterface.dll",
"WinUI3Apps\\PowerToys.Settings.dll",
"WinUI3Apps\\PowerToys.Settings.exe",

View File

@@ -108,7 +108,6 @@ Thank you for using PowerToys!
| Microsoft.PowerToys.AwakeIndefinitelyKeepAwakeEvent | Triggered when the system is set to stay awake indefinitely. |
| Microsoft.PowerToys.AwakeNoKeepAwakeEvent | Occurs when Awake is turned off, allowing the computer to enter sleep mode. |
| Microsoft.PowerToys.AwakeTimedKeepAwakeEvent | Triggered when the system is kept awake for a specified time duration. |
| Microsoft.PowerToys.Awake_CLICommand | Triggered when an Awake CLI command is executed, logging the command name and success status. |
### Color Picker
@@ -205,7 +204,6 @@ Thank you for using PowerToys!
| Microsoft.PowerToys.FileLocksmith_Invoked | Occurs when File Locksmith is invoked. |
| Microsoft.PowerToys.FileLocksmith_InvokedRet | Triggered when File Locksmith invocation returns a result. |
| Microsoft.PowerToys.FileLocksmith_QueryContextMenuError | Occurs when there is an error querying the context menu for File Locksmith. |
| Microsoft.PowerToys.FileLocksmith_CLICommand | Triggered when a File Locksmith CLI command is executed, logging the operation mode (query, kill, query-wait, query-json, or help) and success status. |
### FileExplorerAddOns
@@ -260,7 +258,6 @@ Thank you for using PowerToys!
| Microsoft.PowerToys.ImageResizer_Invoked | Occurs when Image Resizer is invoked by the user. |
| Microsoft.PowerToys.ImageResizer_InvokedRet | Fires when the Image Resizer operation is completed and returns a result. |
| Microsoft.PowerToys.ImageResizer_QueryContextMenuError | Triggered when there is an error querying the context menu for Image Resizer. |
| Microsoft.PowerToys.ImageResizer_CLICommand | Triggered when an Image Resizer CLI command is executed, logging the command name and success status. |
### Keyboard Manager

View File

@@ -57,6 +57,7 @@
<Project Path="src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj" Id="1a066c63-64b3-45f8-92fe-664e1cce8077" />
<Project Path="src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.vcxproj" Id="8b5cfb38-ccba-40a8-ad7a-89c57b070884" />
<Project Path="src/common/updating/updating.vcxproj" Id="17da04df-e393-4397-9cf0-84dabe11032e" />
<Project Path="src/common/updating/UnitTests/UpdatingUnitTests.vcxproj" Id="a1b2c3d4-e5f6-7890-abcd-ef1234567890" />
<Project Path="src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
</Folder>
<Folder Name="/common/interop/">
@@ -1036,10 +1037,6 @@
<Project Path="src/modules/ZoomIt/ZoomItModuleInterface/ZoomItModuleInterface.vcxproj" Id="e4585179-2ac1-4d5f-a3ff-cfc5392f694c" />
<Project Path="src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettingsInterop.vcxproj" Id="ca7d8106-30b9-4aec-9d05-b69b31b8c461" />
</Folder>
<Folder Name="/modules/GrabAndMove/">
<Project Path="src/modules/GrabAndMove/GrabAndMove/GrabAndMove.vcxproj" Id="568c4c30-2e3c-4c2c-a691-007362073765" />
<Project Path="src/modules/GrabAndMove/GrabAndMoveModuleInterface/GrabAndMoveModuleInterface.vcxproj" Id="2c3f7770-4e57-46b7-8dc1-7428a383d0db" />
</Folder>
<Folder Name="/settings-ui/">
<Project Path="src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
@@ -1104,8 +1101,6 @@
<BuildDependency Project="src/modules/launcher/Microsoft.Launcher/Microsoft.Launcher.vcxproj" />
<BuildDependency Project="src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj" />
<BuildDependency Project="src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj" />
<BuildDependency Project="src/modules/GrabAndMove/GrabAndMoveModuleInterface/GrabAndMoveModuleInterface.vcxproj" />
<BuildDependency Project="src/modules/GrabAndMove/GrabAndMove/GrabAndMove.vcxproj" />
<BuildDependency Project="src/modules/powerrename/dll/PowerRenameExt.vcxproj" />
<BuildDependency Project="src/modules/powerrename/lib/PowerRenameLib.vcxproj" />
<BuildDependency Project="src/modules/previewpane/Common/PreviewHandlerCommon.csproj" />

View File

@@ -14,6 +14,8 @@
#include <common/updating/updating.h>
#include <common/updating/updateState.h>
#include <common/updating/installer.h>
#include <common/updating/configBackup.h>
#include <common/updating/updateLifecycle.h>
#include <common/utils/elevation.h>
#include <common/utils/HttpClient.h>
@@ -21,6 +23,8 @@
#include <common/utils/resources.h>
#include <common/utils/timeutil.h>
#include <wil/resource.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/logger/logger.h>
@@ -38,15 +42,16 @@ namespace fs = std::filesystem;
std::optional<fs::path> CopySelfToTempDir()
{
// D5 fix: Use unique temp path with PID to avoid collision on concurrent updates
std::error_code error;
auto dst_path = fs::temp_directory_path() / "PowerToys.Update.exe";
auto dst_path = fs::temp_directory_path() / (L"PowerToys.Update." + std::to_wstring(GetCurrentProcessId()) + L".exe");
fs::copy_file(get_module_filename(), dst_path, fs::copy_options::overwrite_existing, error);
if (error)
{
return std::nullopt;
}
return std::move(dst_path);
return dst_path;
}
std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
@@ -57,34 +62,9 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
auto state = UpdateState::read();
const auto new_version_info = std::move(get_github_version_info_async()).get();
if (std::holds_alternative<version_up_to_date>(*new_version_info))
{
isUpToDate = true;
Logger::error("Invoked with -update_now argument, but no update was available");
return std::nullopt;
}
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
{
if (!new_version_info)
{
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
return std::nullopt;
}
// Cleanup old updates before downloading the latest
updating::cleanup_updates();
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
if (!downloaded_installer)
{
Logger::error("Couldn't download new installer");
}
return downloaded_installer;
}
else if (state.state == UpdateState::readyToInstall)
// Handle readyToInstall first — the installer is already on disk,
// so we don't need a GitHub API call (which may fail if offline).
if (state.state == UpdateState::readyToInstall)
{
fs::path installer{ get_pending_updates_path() / state.downloadedInstallerFilename };
if (fs::is_regular_file(installer))
@@ -97,12 +77,44 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
return std::nullopt;
}
}
else if (state.state == UpdateState::upToDate)
if (state.state == UpdateState::upToDate)
{
isUpToDate = true;
return std::nullopt;
}
const auto new_version_info = std::move(get_github_version_info_async()).get();
// Check for error BEFORE dereferencing — the old code crashed here
// when GitHub API was unreachable (new_version_info held an error string).
if (!new_version_info)
{
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
return std::nullopt;
}
if (std::holds_alternative<version_up_to_date>(*new_version_info))
{
isUpToDate = true;
Logger::error("Invoked with -update_now argument, but no update was available");
return std::nullopt;
}
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
{
// Cleanup old updates before downloading the latest
updating::cleanup_updates();
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
if (!downloaded_installer)
{
Logger::error("Couldn't download new installer");
}
return downloaded_installer;
}
Logger::error("Invoked with -update_now argument, but update state was invalid");
return std::nullopt;
}
@@ -116,13 +128,29 @@ bool InstallNewVersionStage1(fs::path installer)
if (pt_main_window != nullptr)
{
// Get the process that owns the tray window so we can wait for it to exit
DWORD ptProcessId = 0;
GetWindowThreadProcessId(pt_main_window, &ptProcessId);
SendMessageW(pt_main_window, WM_CLOSE, 0, 0);
// D4 fix: Wait for PT to actually exit before launching installer.
// Without this, the installer may find PT files locked.
if (ptProcessId != 0)
{
wil::unique_handle ptProcess{ OpenProcess(SYNCHRONIZE, FALSE, ptProcessId) };
if (ptProcess)
{
WaitForSingleObject(ptProcess.get(), 10000); // 10 second timeout
}
}
}
std::wstring arguments{ UPDATE_NOW_LAUNCH_STAGE2 };
arguments += L" \"";
arguments += installer.c_str();
arguments += L"\"";
// Pass the install directory so Stage 2 can relaunch PowerToys after install
const std::wstring installDir = get_module_folderpath();
std::wstring arguments = updating::BuildStage2Arguments(
UPDATE_NOW_LAUNCH_STAGE2, installer, fs::path(installDir));
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
sei.lpFile = copy_in_temp->c_str();
@@ -190,9 +218,16 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
LPWSTR* args = CommandLineToArgvW(GetCommandLineW(), &nArgs);
if (!args || nArgs < 2)
{
if (args)
{
LocalFree(args);
}
return 1;
}
// D3 fix: ensure args is freed on all exit paths
auto freeArgs = wil::scope_exit([&] { LocalFree(args); });
std::wstring_view action{ args[1] };
std::filesystem::path logFilePath(PTSettingsHelper::get_root_save_folder_location());
@@ -201,6 +236,10 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
if (action == UPDATE_NOW_LAUNCH_STAGE1)
{
// Backup config files before the update to protect against corruption
Logger::info("Backing up config files before update");
updating::BackupConfigFiles(fs::path(PTSettingsHelper::get_root_save_folder_location()));
bool isUpToDate = false;
auto installerPath = ObtainInstaller(isUpToDate);
bool failed = !installerPath.has_value();
@@ -217,6 +256,12 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
}
else if (action == UPDATE_NOW_LAUNCH_STAGE2)
{
if (nArgs < 3)
{
Logger::error("Stage 2 invoked without installer path argument");
return 1;
}
using namespace std::string_view_literals;
const bool failed = !InstallNewVersionStage2(args[2]);
if (failed)
@@ -227,6 +272,37 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
state.state = UpdateState::errorDownloading;
});
}
// D7 fix: Always check for corrupted configs after Stage 2, regardless
// of install success/failure. A failed install may still corrupt configs.
Logger::info("Checking for corrupted config files after update");
updating::RestoreCorruptedConfigs(fs::path(PTSettingsHelper::get_root_save_folder_location()));
if (!failed)
{
// Relaunch PowerToys from the install directory
if (updating::CanRelaunchAfterUpdate(nArgs))
{
std::wstring ptExePath = updating::BuildPowerToysExePath(args[3]);
Logger::info(L"Relaunching PowerToys after update: {}", ptExePath);
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
sei.lpFile = ptExePath.c_str();
sei.nShow = SW_SHOWNORMAL;
sei.lpParameters = UPDATE_REPORT_SUCCESS;
if (!ShellExecuteExW(&sei))
{
Logger::error(L"Failed to relaunch PowerToys after update");
}
}
else
{
Logger::warn("Install directory not provided to Stage 2 - cannot relaunch PowerToys");
}
}
return failed;
}

View File

@@ -28,7 +28,6 @@ namespace ExprtkCalculator::internal
std::wstring ToWStringFullPrecision(double value)
{
std::wostringstream oss;
oss.imbue(std::locale::classic());
oss << std::fixed << std::setprecision(15) << value;
return oss.str();
}

View File

@@ -36,10 +36,6 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredPowerDisplayEnabledValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredGrabAndMoveEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredGrabAndMoveEnabledValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredFancyZonesEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredFancyZonesEnabledValue());

View File

@@ -15,7 +15,6 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
static GpoRuleConfigured GetConfiguredPowerDisplayEnabledValue();
static GpoRuleConfigured GetConfiguredGrabAndMoveEnabledValue();
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();

View File

@@ -19,7 +19,6 @@ namespace PowerToys
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
static GpoRuleConfigured GetConfiguredPowerDisplayEnabledValue();
static GpoRuleConfigured GetConfiguredGrabAndMoveEnabledValue();
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();

View File

@@ -71,10 +71,5 @@ namespace PowerToys.GPOWrapperProjection
{
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredLightSwitchEnabledValue();
}
public static GpoRuleConfigured GetConfiguredGrabAndMoveEnabledValue()
{
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredGrabAndMoveEnabledValue();
}
}
}

View File

@@ -36,7 +36,6 @@ namespace ManagedCommon
ShortcutGuide,
PowerOCR,
Workspaces,
GrabAndMove,
ZoomIt,
GeneralSettings,
}

View File

@@ -145,10 +145,6 @@ namespace CommonSharedConstants
// Path to the events used by ZoomIt
const wchar_t ZOOMIT_REFRESH_SETTINGS_EVENT[] = L"Local\\PowerToysZoomIt-RefreshSettingsEvent-f053a563-d519-4b0d-8152-a54489c13324";
// Path to the events used by GrabAndMove
const wchar_t GRABANDMOVE_REFRESH_SETTINGS_EVENT[] = L"Local\\PowerToysGrabAndMove-RefreshSettingsEvent-a7b3c1d2-4e5f-6a7b-8c9d-0e1f2a3b4c5d";
const wchar_t GRABANDMOVE_EXIT_EVENT[] = L"Local\\PowerToysGrabAndMove-ExitEvent-b8c4d2e3-5f6a-7b8c-9d0e-1f2a3b4c5d6e";
const wchar_t ZOOMIT_EXIT_EVENT[] = L"Local\\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220";
const wchar_t ZOOMIT_ZOOM_EVENT[] = L"Local\\PowerToysZoomIt-ZoomEvent-1e4190d7-94bc-4ad5-adc0-9a8fd07cb393";
const wchar_t ZOOMIT_DRAW_EVENT[] = L"Local\\PowerToysZoomIt-DrawEvent-56338997-404d-4549-bd9a-d132b6766975";

View File

@@ -84,7 +84,6 @@ struct LogSettings
inline const static std::string zoomItLoggerName = "zoom-it";
inline const static std::string lightSwitchLoggerName = "light-switch";
inline const static std::string powerDisplayLoggerName = "powerdisplay";
inline const static std::string grabAndMoveLoggerName = "grabandmove";
inline const static int retention = 30;
std::wstring logLevel;
LogSettings();

View File

@@ -0,0 +1,679 @@
// 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.
#include "pch.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
#include <common/updating/configBackup.h>
#include <common/updating/updateLifecycle.h>
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace fs = std::filesystem;
namespace UpdatingUnitTests
{
// Helper to create a temp directory for test isolation.
// Each instance gets a unique subdirectory to prevent test interference.
class TempDir
{
public:
TempDir()
{
wchar_t tempPath[MAX_PATH + 1];
GetTempPathW(MAX_PATH, tempPath);
static std::atomic<int> counter{0};
m_path = fs::path(tempPath) / (L"PowerToysUpdateTests_" + std::to_wstring(counter++));
// Ensure clean state
std::error_code ec;
fs::remove_all(m_path, ec);
fs::create_directories(m_path, ec);
}
~TempDir()
{
std::error_code ec;
fs::remove_all(m_path, ec);
}
const fs::path& path() const { return m_path; }
// Write a file with the given content
void WriteFile(const fs::path& relativePath, const std::string& content)
{
auto fullPath = m_path / relativePath;
fs::create_directories(fullPath.parent_path());
std::ofstream file(fullPath, std::ios::binary);
file.write(content.data(), content.size());
}
// Write a file with raw bytes (including null bytes for corruption testing)
void WriteFileBytes(const fs::path& relativePath, const std::vector<char>& bytes)
{
auto fullPath = m_path / relativePath;
fs::create_directories(fullPath.parent_path());
std::ofstream file(fullPath, std::ios::binary);
file.write(bytes.data(), bytes.size());
}
// Read file content as string
std::string ReadFile(const fs::path& relativePath)
{
auto fullPath = m_path / relativePath;
std::ifstream file(fullPath, std::ios::binary);
return std::string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
}
bool FileExists(const fs::path& relativePath)
{
return fs::exists(m_path / relativePath);
}
private:
fs::path m_path;
};
TEST_CLASS(IsJsonFileCorruptedTests)
{
public:
// Tests IsJsonFileCorrupted: valid JSON with no null bytes returns false.
// Covers: configBackup.h IsJsonFileCorrupted — happy path, full file scan.
TEST_METHOD(CleanJsonFileIsNotCorrupted)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark","startup":true})");
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
}
// Tests IsJsonFileCorrupted: zero-length file returns false (empty is not corrupted).
// Covers: configBackup.h IsJsonFileCorrupted — file.read returns 0 bytes immediately.
TEST_METHOD(EmptyFileIsNotCorrupted)
{
TempDir dir;
dir.WriteFile(L"empty.json", "");
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"empty.json"));
}
// Tests IsJsonFileCorrupted: file containing embedded null bytes returns true.
// Covers: configBackup.h IsJsonFileCorrupted — null byte detection within buffer.
TEST_METHOD(FileWithNullBytesIsCorrupted)
{
TempDir dir;
std::vector<char> corrupted = { '{', '"', 'a', '"', ':', '\0', '\0', '\0', '}' };
dir.WriteFileBytes(L"corrupted.json", corrupted);
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"corrupted.json"));
}
// Tests IsJsonFileCorrupted: file entirely filled with 0x00 bytes returns true.
// Reproduces the exact bug from #46179 where installer zeroed out JSON files.
// Covers: configBackup.h IsJsonFileCorrupted — first byte is null.
TEST_METHOD(FileFilledWithNullBytesIsCorrupted)
{
TempDir dir;
std::vector<char> allNulls(1024, '\0');
dir.WriteFileBytes(L"workspaces.json", allNulls);
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"workspaces.json"));
}
// Tests IsJsonFileCorrupted: path that does not exist returns false.
// Covers: configBackup.h IsJsonFileCorrupted — file.is_open() check.
TEST_METHOD(NonExistentFileIsNotCorrupted)
{
TempDir dir;
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"missing.json"));
}
// Tests IsJsonFileCorrupted: file larger than the 4096-byte read chunk
// with no null bytes returns false.
// Covers: configBackup.h IsJsonFileCorrupted — multi-chunk while loop.
TEST_METHOD(LargeCleanFileIsNotCorrupted)
{
TempDir dir;
std::string largeContent(8192, 'x');
dir.WriteFile(L"large.json", largeContent);
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"large.json"));
}
// Tests IsJsonFileCorrupted: null byte placed after the first 4096-byte
// chunk boundary is still detected.
// Covers: configBackup.h IsJsonFileCorrupted — second chunk scan.
TEST_METHOD(NullByteAtEndOfLargeFileIsDetected)
{
TempDir dir;
std::string content(5000, 'x');
content[4999] = '\0';
std::vector<char> bytes(content.begin(), content.end());
dir.WriteFileBytes(L"sneaky.json", bytes);
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"sneaky.json"));
}
};
TEST_CLASS(BackupConfigFilesTests)
{
public:
// Tests BackupConfigFiles: root-level .json files are copied to ConfigBackup.
// Covers: configBackup.h BackupConfigFiles — root directory_iterator,
// is_regular_file && extension == ".json" branch.
// Setup: Two root-level JSON files.
TEST_METHOD(BackupCopiesRootJsonFiles)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
dir.WriteFile(L"UpdateState.json", R"({"state":0})");
updating::BackupConfigFiles(dir.path());
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\UpdateState.json"));
Assert::AreEqual(std::string(R"({"theme":"dark"})"), dir.ReadFile(L"ConfigBackup\\settings.json"));
}
// Tests BackupConfigFiles: .json files inside module subdirectories are
// copied to ConfigBackup/<module>/.
// Covers: configBackup.h BackupConfigFiles — is_directory branch,
// module directory_iterator with extension filter.
// Setup: Root JSON + two module directories with JSON files.
TEST_METHOD(BackupCopiesModuleJsonFiles)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[]})");
dir.WriteFile(L"Workspaces\\workspaces.json", R"({"workspaces":[]})");
updating::BackupConfigFiles(dir.path());
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\Workspaces\\workspaces.json"));
Assert::AreEqual(std::string(R"({"zones":[]})"),
dir.ReadFile(L"ConfigBackup\\FancyZones\\settings.json"));
}
// Tests BackupConfigFiles: non-.json files at root level are not copied.
// Covers: configBackup.h BackupConfigFiles — extension filter excludes .log.
// Setup: One JSON file + one .log file at root.
TEST_METHOD(BackupSkipsNonJsonFiles)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
dir.WriteFile(L"debug.log", "log data");
updating::BackupConfigFiles(dir.path());
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\debug.log"));
}
// Tests BackupConfigFiles: the "Updates" directory is explicitly skipped.
// Covers: configBackup.h BackupConfigFiles — dirName == L"Updates" continue.
// Setup: Root JSON + Updates directory containing a file.
TEST_METHOD(BackupSkipsUpdatesDirectory)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
dir.WriteFile(L"Updates\\installer.exe", "fake exe");
updating::BackupConfigFiles(dir.path());
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\Updates"));
}
// Tests BackupConfigFiles: running backup twice overwrites the previous
// backup with current file content.
// Covers: configBackup.h BackupConfigFiles — fs::remove_all(backupDir) +
// copy_options::overwrite_existing.
// Setup: Backup, modify original, backup again.
TEST_METHOD(BackupOverwritesPreviousBackup)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"version":1})");
updating::BackupConfigFiles(dir.path());
// Update the original
dir.WriteFile(L"settings.json", R"({"version":2})");
updating::BackupConfigFiles(dir.path());
Assert::AreEqual(std::string(R"({"version":2})"), dir.ReadFile(L"ConfigBackup\\settings.json"));
}
// Tests BackupConfigFiles: non-.json files inside module subdirectories
// (e.g., FancyZones/zones.dat) should NOT be backed up.
// Covers: configBackup.h BackupConfigFiles — extension filter in module loop.
TEST_METHOD(BackupSkipsNonJsonFilesInModuleDirs)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({})");
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[]})");
dir.WriteFile(L"FancyZones\\zones.dat", "binary data");
updating::BackupConfigFiles(dir.path());
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\FancyZones\\zones.dat"));
}
// Tests BackupConfigFiles: empty root directory with no files produces
// an empty ConfigBackup dir without errors.
// Covers: configBackup.h BackupConfigFiles — empty directory_iterator.
TEST_METHOD(BackupEmptyRootDirSucceeds)
{
TempDir dir;
// Root dir exists but has no files
updating::BackupConfigFiles(dir.path());
Assert::IsTrue(dir.FileExists(L"ConfigBackup"));
}
};
TEST_CLASS(RestoreCorruptedConfigsTests)
{
public:
// Tests RestoreCorruptedConfigs: corrupted root-level JSON file is restored
// from the good backup copy.
// Covers: configBackup.h RestoreCorruptedConfigs — root file restore branch,
// fs::exists + IsJsonFileCorrupted + backup integrity check.
// Setup: Good file -> backup -> corrupt original -> restore.
TEST_METHOD(RestoreFixesCorruptedRootFile)
{
TempDir dir;
const std::string goodContent = R"({"theme":"dark"})";
dir.WriteFile(L"settings.json", goodContent);
// Backup
updating::BackupConfigFiles(dir.path());
// Corrupt the original
std::vector<char> corrupted(goodContent.size(), '\0');
dir.WriteFileBytes(L"settings.json", corrupted);
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
// Restore
updating::RestoreCorruptedConfigs(dir.path());
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
Assert::AreEqual(goodContent, dir.ReadFile(L"settings.json"));
}
// Tests RestoreCorruptedConfigs: corrupted module-level JSON file is restored
// from the good backup copy.
// Covers: configBackup.h RestoreCorruptedConfigs — module directory branch,
// moduleBackupEntry restore with integrity check.
// Setup: Module file + root file -> backup -> corrupt module file -> restore.
TEST_METHOD(RestoreFixesCorruptedModuleFile)
{
TempDir dir;
const std::string goodContent = R"({"workspaces":[]})";
dir.WriteFile(L"Workspaces\\workspaces.json", goodContent);
dir.WriteFile(L"settings.json", R"({})");
updating::BackupConfigFiles(dir.path());
// Corrupt the module file
std::vector<char> corrupted(goodContent.size(), '\0');
dir.WriteFileBytes(L"Workspaces\\workspaces.json", corrupted);
updating::RestoreCorruptedConfigs(dir.path());
Assert::AreEqual(goodContent, dir.ReadFile(L"Workspaces\\workspaces.json"));
}
// Tests RestoreCorruptedConfigs: clean (non-corrupted) files are NOT
// overwritten by backup — preserves user changes made after backup.
// Covers: configBackup.h RestoreCorruptedConfigs — IsJsonFileCorrupted
// returns false, copy_file is skipped.
// Setup: File -> backup -> modify (but keep valid) -> restore.
TEST_METHOD(RestoreLeavesCleanFilesUntouched)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"version":1})");
updating::BackupConfigFiles(dir.path());
// Modify original (but keep it clean JSON)
dir.WriteFile(L"settings.json", R"({"version":2})");
updating::RestoreCorruptedConfigs(dir.path());
// Should NOT have been restored since it's not corrupted
Assert::AreEqual(std::string(R"({"version":2})"), dir.ReadFile(L"settings.json"));
}
// Tests RestoreCorruptedConfigs: when no ConfigBackup directory exists,
// restore silently does nothing (no crash, no data loss).
// Covers: configBackup.h RestoreCorruptedConfigs — !fs::exists(backupDir)
// early return.
// Setup: File with no prior backup.
TEST_METHOD(RestoreHandlesMissingBackupDirectory)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
// No backup was created - restore should silently do nothing
updating::RestoreCorruptedConfigs(dir.path());
Assert::AreEqual(std::string(R"({"theme":"dark"})"), dir.ReadFile(L"settings.json"));
}
// Tests RestoreCorruptedConfigs: end-to-end scenario with multiple modules,
// some corrupted and some clean, verifying selective restore.
// Covers: configBackup.h RestoreCorruptedConfigs — both root and module
// branches, selective restore based on corruption status.
// Setup: 4 modules -> backup -> corrupt 2 -> restore -> verify all 4.
TEST_METHOD(FullBackupAndRestoreRoundTrip)
{
TempDir dir;
// Set up a realistic config structure
dir.WriteFile(L"settings.json", R"({"startup":true,"theme":"dark"})");
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[{"id":1}]})");
dir.WriteFile(L"Workspaces\\workspaces.json", R"({"workspaces":[{"name":"dev"}]})");
dir.WriteFile(L"KeyboardManager\\default.json", R"({"remaps":[]})");
// Backup
updating::BackupConfigFiles(dir.path());
// Corrupt some files (simulating #46179 scenario)
dir.WriteFileBytes(L"Workspaces\\workspaces.json", std::vector<char>(100, '\0'));
dir.WriteFileBytes(L"settings.json", std::vector<char>(50, '\0'));
// Leave FancyZones and KBM clean
// Restore
updating::RestoreCorruptedConfigs(dir.path());
// Corrupted files should be restored
Assert::AreEqual(std::string(R"({"startup":true,"theme":"dark"})"), dir.ReadFile(L"settings.json"));
Assert::AreEqual(std::string(R"({"workspaces":[{"name":"dev"}]})"), dir.ReadFile(L"Workspaces\\workspaces.json"));
// Clean files should be unchanged
Assert::AreEqual(std::string(R"({"zones":[{"id":1}]})"), dir.ReadFile(L"FancyZones\\settings.json"));
Assert::AreEqual(std::string(R"({"remaps":[]})"), dir.ReadFile(L"KeyboardManager\\default.json"));
}
// Tests RestoreCorruptedConfigs: when the original file has been deleted
// (not corrupted), restore should NOT recreate it from backup. The installer
// may have intentionally removed obsolete config files.
// Covers: configBackup.h RestoreCorruptedConfigs — fs::exists guard.
TEST_METHOD(RestoreSkipsDeletedOriginals)
{
TempDir dir;
dir.WriteFile(L"obsolete.json", R"({"old":true})");
updating::BackupConfigFiles(dir.path());
// Installer deletes the file
std::error_code ec;
fs::remove(dir.path() / L"obsolete.json", ec);
updating::RestoreCorruptedConfigs(dir.path());
// Should NOT be recreated
Assert::IsFalse(dir.FileExists(L"obsolete.json"));
}
// Tests RestoreCorruptedConfigs: when the backup file itself is corrupted
// (e.g., disk error during backup), restore should NOT copy corrupted
// backup over the original — that would make things worse.
// Covers: configBackup.h RestoreCorruptedConfigs — backup integrity check (B2 fix).
TEST_METHOD(RestoreSkipsCorruptedBackup)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
updating::BackupConfigFiles(dir.path());
// Corrupt BOTH the original AND the backup
std::vector<char> nulls(50, '\0');
dir.WriteFileBytes(L"settings.json", nulls);
dir.WriteFileBytes(L"ConfigBackup\\settings.json", nulls);
updating::RestoreCorruptedConfigs(dir.path());
// Original should still be corrupted — we don't restore from bad backup
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
}
};
// Simulates what actually happens during a PowerToys upgrade:
// 1. User has settings from normal use
// 2. Updater backs up before install (Stage 1)
// 3. Installer runs and corrupts some files (simulated)
// 4. Updater restores corrupted files (Stage 2)
// 5. PT relaunches and finds working configs
TEST_CLASS(UpgradeSimulationTests)
{
public:
// Tests full upgrade simulation: backup -> installer corrupts files -> restore.
// Verifies that corrupted files are restored and clean files are untouched.
// Covers: configBackup.h BackupConfigFiles + RestoreCorruptedConfigs —
// end-to-end with 5 modules, 2 corrupted, 3 clean.
// Setup: Realistic config structure with multiple modules.
TEST_METHOD(SimulateUpgradeWithCorruption)
{
TempDir dir;
// === User's real config state before upgrade ===
dir.WriteFile(L"settings.json",
R"({"startup":true,"theme":"dark","run_elevated":false,"download_updates_automatically":true})");
dir.WriteFile(L"FancyZones\\settings.json",
R"({"zones":[{"id":1,"rect":{"x":0,"y":0,"w":960,"h":1080}}]})");
dir.WriteFile(L"Workspaces\\workspaces.json",
R"({"workspaces":[{"name":"dev","apps":["code","terminal"]}]})");
dir.WriteFile(L"KeyboardManager\\default.json",
R"({"remapKeys":{"inProcess":[{"original":"0x41","new":"0x42"}]}})");
dir.WriteFile(L"MouseWithoutBorders\\settings.json",
R"({"machineKey":"abc123","connectToAll":true})");
// Non-JSON files that should be left alone
dir.WriteFile(L"update.log", "2026-04-11 update started");
// === Stage 1: Backup before killing PT ===
updating::BackupConfigFiles(dir.path());
// Verify backup was created correctly
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\Workspaces\\workspaces.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\KeyboardManager\\default.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\MouseWithoutBorders\\settings.json"));
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\update.log"));
// === Installer runs: some files get corrupted (the #46179 scenario) ===
// Workspaces JSON filled with null bytes
dir.WriteFileBytes(L"Workspaces\\workspaces.json", std::vector<char>(512, '\0'));
// Main settings partially corrupted (null bytes injected)
std::vector<char> partialCorrupt = { '{', '"', 's', '\0', '\0', '\0', '\0', '}' };
dir.WriteFileBytes(L"settings.json", partialCorrupt);
// FancyZones, KBM, and MWB survive the install fine
// (this is realistic - not all files get corrupted)
// === Stage 2: Restore after install completes ===
updating::RestoreCorruptedConfigs(dir.path());
// === Verify: PT relaunches and finds working configs ===
// Corrupted files should be restored from backup
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"Workspaces\\workspaces.json"));
Assert::AreEqual(
std::string(R"({"startup":true,"theme":"dark","run_elevated":false,"download_updates_automatically":true})"),
dir.ReadFile(L"settings.json"));
Assert::AreEqual(
std::string(R"({"workspaces":[{"name":"dev","apps":["code","terminal"]}]})"),
dir.ReadFile(L"Workspaces\\workspaces.json"));
// Clean files should be untouched (not overwritten with backup)
Assert::AreEqual(
std::string(R"({"zones":[{"id":1,"rect":{"x":0,"y":0,"w":960,"h":1080}}]})"),
dir.ReadFile(L"FancyZones\\settings.json"));
Assert::AreEqual(
std::string(R"({"remapKeys":{"inProcess":[{"original":"0x41","new":"0x42"}]}})"),
dir.ReadFile(L"KeyboardManager\\default.json"));
Assert::AreEqual(
std::string(R"({"machineKey":"abc123","connectToAll":true})"),
dir.ReadFile(L"MouseWithoutBorders\\settings.json"));
}
// Tests upgrade from an old version that has fewer modules than the new version.
// Verifies that new module configs (created by the installer) are not touched
// by restore, while corrupted old configs are restored.
// Covers: configBackup.h RestoreCorruptedConfigs — module dir in root that
// has no corresponding backup entry.
// Setup: Old version with 1 module -> backup -> new installer adds module -> corrupt old -> restore.
TEST_METHOD(SimulateUpgradeFromVeryOldVersion)
{
TempDir dir;
// Old version had fewer modules - only settings.json
dir.WriteFile(L"settings.json", R"({"theme":"dark","powertoys_version":"v0.60.0"})");
// Backup
updating::BackupConfigFiles(dir.path());
// New installer creates new module dirs that didn't exist before
dir.WriteFile(L"NewModule\\settings.json", R"({"enabled":true})");
// Old settings get corrupted during upgrade
dir.WriteFileBytes(L"settings.json", std::vector<char>(100, '\0'));
// Restore
updating::RestoreCorruptedConfigs(dir.path());
// Old settings restored
Assert::AreEqual(
std::string(R"({"theme":"dark","powertoys_version":"v0.60.0"})"),
dir.ReadFile(L"settings.json"));
// New module settings untouched (no backup existed for them)
Assert::AreEqual(
std::string(R"({"enabled":true})"),
dir.ReadFile(L"NewModule\\settings.json"));
}
};
// Tests for the update lifecycle: argument passing between Stage 1 and Stage 2,
// relaunch path construction, and the handoff that was broken in #42004/#43011/#44071.
TEST_CLASS(UpdateLifecycleTests)
{
public:
// Tests BuildStage2Arguments: output contains the stage 2 flag, installer path,
// and install directory — all three components needed for Stage 2.
// Covers: updateLifecycle.h BuildStage2Arguments — concatenation logic.
// Setup: Typical paths with spaces (Program Files).
TEST_METHOD(BuildStage2ArgumentsContainsInstallerAndInstallDir)
{
const auto args = updating::BuildStage2Arguments(
L"-update_now_stage_2",
L"C:\\Users\\test\\AppData\\Local\\PowerToys\\Updates\\powertoyssetup-x64.exe",
L"C:\\Program Files\\PowerToys");
// Must contain the stage 2 flag
Assert::IsTrue(args.find(L"-update_now_stage_2") != std::wstring::npos);
// Must contain the installer path (quoted)
Assert::IsTrue(args.find(L"powertoyssetup-x64.exe") != std::wstring::npos);
// Must contain the install directory (quoted) — this was MISSING before our fix
Assert::IsTrue(args.find(L"C:\\Program Files\\PowerToys") != std::wstring::npos);
}
// Tests BuildStage2Arguments: both paths are wrapped in double quotes to
// survive CommandLineToArgvW parsing when paths contain spaces.
// Covers: updateLifecycle.h BuildStage2Arguments — quote wrapping.
// Setup: Installer path with spaces.
TEST_METHOD(BuildStage2ArgumentsQuotesBothPaths)
{
const auto args = updating::BuildStage2Arguments(
L"-update_now_stage_2",
L"C:\\path with spaces\\installer.exe",
L"C:\\Program Files\\PowerToys");
// Count quotes — should have 4 (open/close for each path)
size_t quoteCount = std::count(args.begin(), args.end(), L'"');
Assert::AreEqual(size_t{ 4 }, quoteCount);
}
// Tests BuildPowerToysExePath: appends "PowerToys.exe" to the install dir.
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path / operator.
// Setup: Standard install path without trailing backslash.
TEST_METHOD(BuildPowerToysExePathAppendsExeName)
{
const auto path = updating::BuildPowerToysExePath(L"C:\\Program Files\\PowerToys");
Assert::AreEqual(std::wstring(L"C:\\Program Files\\PowerToys\\PowerToys.exe"), path);
}
// Tests BuildPowerToysExePath: trailing backslash does not produce double
// backslash (e.g., "...PowerToys\\PowerToys.exe").
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path normalizes separators.
// Setup: Install path with trailing backslash.
TEST_METHOD(BuildPowerToysExePathHandlesTrailingBackslash)
{
const auto path = updating::BuildPowerToysExePath(L"C:\\Program Files\\PowerToys\\");
Assert::AreEqual(std::wstring(L"C:\\Program Files\\PowerToys\\PowerToys.exe"), path);
}
// Tests BuildPowerToysExePath: empty string produces just "PowerToys.exe".
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path with empty input.
// Setup: Empty install directory string.
TEST_METHOD(BuildPowerToysExePathHandlesEmptyString)
{
const auto path = updating::BuildPowerToysExePath(L"");
Assert::AreEqual(std::wstring(L"PowerToys.exe"), path);
}
// Tests CanRelaunchAfterUpdate: returns true when Stage 2 receives
// the install directory (argCount >= 4), false otherwise.
// This is the gate that prevents relaunch when using an old Stage 1
// that didn't pass the install dir (#42004/#43011/#44071).
// Covers: updateLifecycle.h CanRelaunchAfterUpdate.
TEST_METHOD(CanRelaunchReflectsArgCount)
{
// Old Stage 1 (pre-fix): only passed action + installer = 3 args
Assert::IsFalse(updating::CanRelaunchAfterUpdate(0));
Assert::IsFalse(updating::CanRelaunchAfterUpdate(1));
Assert::IsFalse(updating::CanRelaunchAfterUpdate(2));
Assert::IsFalse(updating::CanRelaunchAfterUpdate(3));
// New Stage 1 (post-fix): passes action + installer + installDir = 4 args
Assert::IsTrue(updating::CanRelaunchAfterUpdate(4));
Assert::IsTrue(updating::CanRelaunchAfterUpdate(5));
}
// Tests BuildStage2Arguments + CommandLineToArgvW round-trip: the exact
// scenario where Stage 1 builds args and Windows parses them in Stage 2.
// Verifies quoting is correct so paths with spaces survive the round trip.
// Covers: updateLifecycle.h BuildStage2Arguments — quote correctness.
// Setup: Realistic paths with spaces and version numbers.
TEST_METHOD(Stage2ArgumentsCanBeRoundTrippedThroughCommandLineToArgvW)
{
const std::wstring installerPath = L"C:\\Users\\test user\\AppData\\Local\\PowerToys\\Updates\\powertoyssetup-0.86.0-x64.exe";
const std::wstring installDir = L"C:\\Program Files\\PowerToys";
const auto args = updating::BuildStage2Arguments(L"-update_now_stage_2", installerPath, installDir);
// Simulate what Windows does: prepend a fake exe name and parse
std::wstring commandLine = L"PowerToys.Update.exe " + args;
int argc = 0;
LPWSTR* argv = CommandLineToArgvW(commandLine.c_str(), &argc);
Assert::IsNotNull(argv);
Assert::AreEqual(4, argc);
Assert::AreEqual(std::wstring(L"-update_now_stage_2"), std::wstring(argv[1]));
Assert::AreEqual(installerPath, std::wstring(argv[2]));
Assert::AreEqual(installDir, std::wstring(argv[3]));
LocalFree(argv);
}
};
}

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>UpdatingUnitTests</RootNamespace>
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
<ProjectName>Updating.UnitTests</ProjectName>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseOfMfc>false</UseOfMfc>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\tests\UpdatingUnitTests\</OutDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\;..\..\;..\..\..\;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="UpdatingTests.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View File

@@ -0,0 +1,5 @@
// 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.
#include "pch.h"

View File

@@ -0,0 +1,17 @@
// 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.
#ifndef PCH_H
#define PCH_H
#include <atomic>
#include <Windows.h>
// Suppressing 26466 - Don't use static_cast downcasts - in CppUnitTest.h
#pragma warning(push)
#pragma warning(disable : 26466)
#include "CppUnitTest.h"
#pragma warning(pop)
#endif //PCH_H

View File

@@ -0,0 +1,170 @@
// 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.
#pragma once
#include <filesystem>
#include <fstream>
#include <string>
namespace updating
{
namespace fs = std::filesystem;
// Check if a JSON file is corrupted (contains null bytes, as seen in #46179)
inline bool IsJsonFileCorrupted(const fs::path& filePath)
{
try
{
std::ifstream file(filePath, std::ios::binary);
if (!file.is_open())
{
return false;
}
constexpr size_t c_readChunkSize{ 4096 };
char buffer[c_readChunkSize];
while (file.read(buffer, c_readChunkSize) || file.gcount() > 0)
{
const auto bytesRead = file.gcount();
for (std::streamsize i = 0; i < bytesRead; ++i)
{
if (buffer[i] == '\0')
{
return true;
}
}
}
return false;
}
catch (...)
{
return true;
}
}
// Backup all JSON config files before update to protect against corruption (#46179)
inline void BackupConfigFiles(const fs::path& rootPath)
{
try
{
const fs::path backupDir = rootPath / L"ConfigBackup";
std::error_code ec;
fs::remove_all(backupDir, ec);
// Note: remove_all failure means stale backup may persist; continue anyway
// since create_directories will overlay
fs::create_directories(backupDir, ec);
if (ec)
{
return;
}
for (const auto& entry : fs::directory_iterator(rootPath, ec))
{
if (ec)
{
break;
}
if (entry.is_regular_file() && entry.path().extension() == L".json")
{
fs::copy_file(entry.path(), backupDir / entry.path().filename(), fs::copy_options::overwrite_existing, ec);
}
else if (entry.is_directory())
{
const auto dirName = entry.path().filename().wstring();
if (dirName == L"ConfigBackup" || dirName == L"Updates")
{
continue;
}
const auto moduleBackup = backupDir / entry.path().filename();
fs::create_directories(moduleBackup, ec);
std::error_code moduleEc;
for (const auto& moduleEntry : fs::directory_iterator(entry.path(), moduleEc))
{
if (moduleEc)
{
break;
}
if (moduleEntry.is_regular_file() && moduleEntry.path().extension() == L".json")
{
fs::copy_file(moduleEntry.path(), moduleBackup / moduleEntry.path().filename(), fs::copy_options::overwrite_existing, moduleEc);
}
}
}
}
}
catch (...)
{
// Intentionally swallowed — update must not fail due to backup errors.
// Logging would require spdlog dependency which is unavailable in test context.
}
}
// Restore JSON configs from backup if corruption is detected after update
inline void RestoreCorruptedConfigs(const fs::path& rootPath)
{
try
{
const fs::path backupDir = rootPath / L"ConfigBackup";
if (!fs::exists(backupDir))
{
return;
}
std::error_code ec;
for (const auto& backupEntry : fs::directory_iterator(backupDir, ec))
{
if (ec)
{
break;
}
if (backupEntry.is_regular_file() && backupEntry.path().extension() == L".json")
{
const auto originalPath = rootPath / backupEntry.path().filename();
// Only restore if the backup itself is valid
if (fs::exists(originalPath) && IsJsonFileCorrupted(originalPath) && !IsJsonFileCorrupted(backupEntry.path()))
{
fs::copy_file(backupEntry.path(), originalPath, fs::copy_options::overwrite_existing, ec);
}
}
else if (backupEntry.is_directory())
{
const auto moduleDir = rootPath / backupEntry.path().filename();
std::error_code moduleEc;
for (const auto& moduleBackupEntry : fs::directory_iterator(backupEntry.path(), moduleEc))
{
if (moduleEc)
{
break;
}
if (moduleBackupEntry.is_regular_file() && moduleBackupEntry.path().extension() == L".json")
{
const auto originalModulePath = moduleDir / moduleBackupEntry.path().filename();
// Only restore if the backup itself is valid
if (fs::exists(originalModulePath) && IsJsonFileCorrupted(originalModulePath) && !IsJsonFileCorrupted(moduleBackupEntry.path()))
{
fs::copy_file(moduleBackupEntry.path(), originalModulePath, fs::copy_options::overwrite_existing, moduleEc);
}
}
}
}
}
}
catch (...)
{
// Intentionally swallowed — update must not fail due to backup errors.
// Logging would require spdlog dependency which is unavailable in test context.
}
}
}

View File

@@ -0,0 +1,47 @@
// 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.
#pragma once
#include <filesystem>
#include <string>
namespace updating
{
namespace fs = std::filesystem;
// Build the command-line arguments for Stage 2.
// Stage 1 passes the installer path and the PT install directory
// so Stage 2 can run the installer and relaunch PowerToys afterward.
// Note: paths containing embedded double-quote characters are not supported.
// This is safe because install paths come from get_module_folderpath().
inline std::wstring BuildStage2Arguments(
const std::wstring& stage2Flag,
const fs::path& installerPath,
const fs::path& installDir)
{
std::wstring arguments{ stage2Flag };
arguments += L" \"";
arguments += installerPath.c_str();
arguments += L"\" \"";
arguments += installDir.c_str();
arguments += L"\"";
return arguments;
}
// Build the full path to PowerToys.exe from the install directory.
// Used by Stage 2 to relaunch PT after a successful update.
inline std::wstring BuildPowerToysExePath(const std::wstring& installDir)
{
return (std::filesystem::path(installDir) / L"PowerToys.exe").wstring();
}
// Determine whether Stage 2 has enough information to relaunch PT.
// Returns true if the install directory argument was provided.
inline bool CanRelaunchAfterUpdate(int argCount)
{
// args[0]=exe, args[1]=action, args[2]=installer, args[3]=installDir
return argCount >= 4;
}
}

View File

@@ -33,7 +33,6 @@ namespace powertoys_gpo
const std::wstring POLICY_CONFIGURE_ENABLED_CROP_AND_LOCK = L"ConfigureEnabledUtilityCropAndLock";
const std::wstring POLICY_CONFIGURE_ENABLED_LIGHT_SWITCH = L"ConfigureEnabledUtilityLightSwitch";
const std::wstring POLICY_CONFIGURE_ENABLED_POWER_DISPLAY = L"ConfigureEnabledUtilityPowerDisplay";
const std::wstring POLICY_CONFIGURE_ENABLED_GRAB_AND_MOVE = L"ConfigureEnabledUtilityGrabAndMove";
const std::wstring POLICY_CONFIGURE_ENABLED_FANCYZONES = L"ConfigureEnabledUtilityFancyZones";
const std::wstring POLICY_CONFIGURE_ENABLED_FILE_LOCKSMITH = L"ConfigureEnabledUtilityFileLocksmith";
const std::wstring POLICY_CONFIGURE_ENABLED_SVG_PREVIEW = L"ConfigureEnabledUtilityFileExplorerSVGPreview";
@@ -318,11 +317,6 @@ namespace powertoys_gpo
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_POWER_DISPLAY);
}
inline gpo_rule_configured_t getConfiguredGrabAndMoveEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_GRAB_AND_MOVE);
}
inline gpo_rule_configured_t getConfiguredFancyZonesEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_FANCYZONES);

View File

@@ -29,7 +29,6 @@
<definition name="SUPPORTED_POWERTOYS_0_96_0" displayName="$(string.SUPPORTED_POWERTOYS_0_96_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_97_0" displayName="$(string.SUPPORTED_POWERTOYS_0_97_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_98_0" displayName="$(string.SUPPORTED_POWERTOYS_0_98_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_99_0" displayName="$(string.SUPPORTED_POWERTOYS_0_99_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1)"/>
</definitions>
</supportedOn>
@@ -153,7 +152,7 @@
</policy>
<policy name="ConfigureEnabledUtilityPowerDisplay" class="Both" displayName="$(string.ConfigureEnabledUtilityPowerDisplay)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityPowerDisplay">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_99_0" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_95_0" />
<enabledValue>
<decimal value="1" />
</enabledValue>

View File

@@ -36,7 +36,6 @@
<string id="SUPPORTED_POWERTOYS_0_96_0">PowerToys version 0.96.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_97_0">PowerToys version 0.97.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_98_0">PowerToys version 0.98.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_99_0">PowerToys version 0.99.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1">From PowerToys version 0.64.0 until PowerToys version 0.87.1</string>
<string id="ConfigureAllUtilityGlobalEnabledStateDescription">This policy configures the enabled state for all PowerToys utilities.

View File

@@ -496,119 +496,23 @@ private:
if (!GetGUIThreadInfo(0, &gui_info))
{
Logger::warn(L"Auto-copy: GetGUIThreadInfo failed (error={})", GetLastError());
return false;
}
HWND target = gui_info.hwndFocus ? gui_info.hwndFocus : gui_info.hwndActive;
if (!target)
{
Logger::warn(L"Auto-copy: no focused or active window found");
return false;
}
DWORD_PTR result = 0;
auto sendResult = SendMessageTimeout(target, WM_COPY, 0, 0, SMTO_ABORTIFHUNG | SMTO_BLOCK, 50, &result);
return sendResult != 0;
}
// Helper: poll clipboard sequence number for a change from initial_sequence.
// Returns true if the sequence number changed within the given number of polls.
bool poll_clipboard_sequence(DWORD initial_sequence, int poll_attempts, std::chrono::milliseconds poll_delay)
{
for (int poll = 0; poll < poll_attempts; ++poll)
{
if (GetClipboardSequenceNumber() != initial_sequence)
{
return true;
}
std::this_thread::sleep_for(poll_delay);
}
return false;
}
// Helper: send Ctrl+C via SendInput, releasing any held modifier keys first
// (the hotkey combination may still have modifiers physically pressed).
bool send_ctrl_c_input()
{
std::vector<INPUT> inputs;
// Release all modifier keys that are currently held down from the hotkey.
// Without this, the target app sees e.g. Win+Shift+Ctrl+C instead of just Ctrl+C.
try_inject_modifier_key_up(inputs, VK_LCONTROL);
try_inject_modifier_key_up(inputs, VK_RCONTROL);
try_inject_modifier_key_up(inputs, VK_LWIN);
try_inject_modifier_key_up(inputs, VK_RWIN);
try_inject_modifier_key_up(inputs, VK_LSHIFT);
try_inject_modifier_key_up(inputs, VK_RSHIFT);
try_inject_modifier_key_up(inputs, VK_LMENU);
try_inject_modifier_key_up(inputs, VK_RMENU);
// Ctrl down
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = VK_CONTROL;
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
// C down
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = 0x43; // C
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
// C up
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = 0x43; // C
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
// Ctrl up
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = VK_CONTROL;
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
// Restore modifiers that were held down
try_inject_modifier_key_restore(inputs, VK_LCONTROL);
try_inject_modifier_key_restore(inputs, VK_RCONTROL);
try_inject_modifier_key_restore(inputs, VK_LWIN);
try_inject_modifier_key_restore(inputs, VK_RWIN);
try_inject_modifier_key_restore(inputs, VK_LSHIFT);
try_inject_modifier_key_restore(inputs, VK_RSHIFT);
try_inject_modifier_key_restore(inputs, VK_LMENU);
try_inject_modifier_key_restore(inputs, VK_RMENU);
// Prevent Start Menu from activating after Win key release/restore
INPUT dummyEvent = {};
dummyEvent.type = INPUT_KEYBOARD;
dummyEvent.ki.wVk = 0xFF;
dummyEvent.ki.dwFlags = KEYEVENTF_KEYUP;
inputs.push_back(dummyEvent);
auto uSent = SendInput(static_cast<UINT>(inputs.size()), inputs.data(), sizeof(INPUT));
if (uSent != inputs.size())
{
DWORD errorCode = GetLastError();
auto errorMessage = get_last_error_message(errorCode);
Logger::error(L"SendInput failed for Ctrl+C. Expected to send {} inputs and sent only {}. {}", inputs.size(), uSent, errorMessage.has_value() ? errorMessage.value() : L"");
Trace::AdvancedPaste_Error(errorCode, errorMessage.has_value() ? errorMessage.value() : L"", L"input.SendInput");
return false;
}
return true;
return SendMessageTimeout(target,
WM_COPY,
0,
0,
SMTO_ABORTIFHUNG | SMTO_BLOCK,
50,
&result) != 0;
}
bool send_copy_selection()
@@ -622,30 +526,78 @@ private:
for (int attempt = 0; attempt < copy_attempts; ++attempt)
{
const auto initial_sequence = GetClipboardSequenceNumber();
copy_succeeded = try_send_copy_message();
// Strategy 1: Try WM_COPY message (works for standard Win32 controls)
bool wm_copy_sent = try_send_copy_message();
if (wm_copy_sent)
if (!copy_succeeded)
{
if (poll_clipboard_sequence(initial_sequence, clipboard_poll_attempts, clipboard_poll_delay))
std::vector<INPUT> inputs;
// send Ctrl+C (key downs and key ups)
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = VK_CONTROL;
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = 0x43; // C
// Avoid triggering detection by the centralized keyboard hook.
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = 0x43; // C
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
// Avoid triggering detection by the centralized keyboard hook.
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = VK_CONTROL;
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
auto uSent = SendInput(static_cast<UINT>(inputs.size()), inputs.data(), sizeof(INPUT));
if (uSent != inputs.size())
{
DWORD errorCode = GetLastError();
auto errorMessage = get_last_error_message(errorCode);
Logger::error(L"SendInput failed for Ctrl+C. Expected to send {} inputs and sent only {}. {}", inputs.size(), uSent, errorMessage.has_value() ? errorMessage.value() : L"");
Trace::AdvancedPaste_Error(errorCode, errorMessage.has_value() ? errorMessage.value() : L"", L"input.SendInput");
}
else
{
copy_succeeded = true;
}
}
// Strategy 2: If WM_COPY didn't work, try SendInput Ctrl+C (works for Electron, browsers, etc.)
if (!copy_succeeded)
if (copy_succeeded)
{
const auto sequence_before_ctrl_c = GetClipboardSequenceNumber();
if (send_ctrl_c_input())
bool sequence_changed = false;
for (int poll_attempt = 0; poll_attempt < clipboard_poll_attempts; ++poll_attempt)
{
if (poll_clipboard_sequence(sequence_before_ctrl_c, clipboard_poll_attempts, clipboard_poll_delay))
if (GetClipboardSequenceNumber() != initial_sequence)
{
copy_succeeded = true;
sequence_changed = true;
break;
}
std::this_thread::sleep_for(clipboard_poll_delay);
}
copy_succeeded = sequence_changed;
}
if (copy_succeeded)
@@ -659,11 +611,6 @@ private:
}
}
if (!copy_succeeded)
{
Logger::warn(L"Auto-copy: all {} copy attempts failed — the target application did not update the clipboard after WM_COPY and Ctrl+C", copy_attempts);
}
return copy_succeeded;
}
@@ -1030,7 +977,6 @@ public:
{
if (!send_copy_selection())
{
Logger::warn(L"Auto-copy: failed to copy selection for custom action index {} — aborting action", custom_action_index);
return false;
}
}

View File

@@ -121,7 +121,7 @@ CommandResult run_command(int argc, wchar_t* argv[], IProcessFinder& finder, IPr
if (argc < 2)
{
Logger::warn("No arguments provided");
return { 1, get_usage(strings), L"help" };
return { 1, get_usage(strings) };
}
bool json_output = false;
@@ -156,18 +156,18 @@ CommandResult run_command(int argc, wchar_t* argv[], IProcessFinder& finder, IPr
catch (...)
{
Logger::error("Invalid timeout value");
return { 1, strings.GetString(IDS_ERROR_INVALID_TIMEOUT), L"query-wait" };
return { 1, strings.GetString(IDS_ERROR_INVALID_TIMEOUT) };
}
}
else
{
Logger::error("Timeout argument missing");
return { 1, strings.GetString(IDS_ERROR_TIMEOUT_ARG), L"query-wait" };
return { 1, strings.GetString(IDS_ERROR_TIMEOUT_ARG) };
}
}
else if (arg == L"--help")
{
return { 0, get_usage(strings), L"help" };
return { 0, get_usage(strings) };
}
else
{
@@ -178,7 +178,7 @@ CommandResult run_command(int argc, wchar_t* argv[], IProcessFinder& finder, IPr
if (paths.empty())
{
Logger::error("No paths specified");
return { 1, strings.GetString(IDS_ERROR_NO_PATHS), L"query" };
return { 1, strings.GetString(IDS_ERROR_NO_PATHS) };
}
Logger::info("Processing {} paths", paths.size());
@@ -213,13 +213,13 @@ CommandResult run_command(int argc, wchar_t* argv[], IProcessFinder& finder, IPr
{
Logger::warn("Timeout waiting for files to be unlocked");
ss << strings.GetString(IDS_TIMEOUT);
return { 1, ss.str(), L"query-wait" };
return { 1, ss.str() };
}
}
Sleep(200);
}
return { 0, ss.str(), L"query-wait" };
return { 0, ss.str() };
}
auto results = finder.find(paths);
@@ -244,6 +244,5 @@ CommandResult run_command(int argc, wchar_t* argv[], IProcessFinder& finder, IPr
output_ss << get_text(results, strings);
}
std::wstring cmd_name = kill ? L"kill" : (json_output ? L"query-json" : L"query");
return { 0, output_ss.str(), cmd_name };
return { 0, output_ss.str() };
}

View File

@@ -8,7 +8,6 @@ struct CommandResult
{
int exit_code;
std::wstring output;
std::wstring command_name;
};
struct IProcessFinder

View File

@@ -1,7 +1,6 @@
#include "pch.h"
#include "CLILogic.h"
#include "FileLocksmithLib/FileLocksmith.h"
#include "FileLocksmithLib/Trace.h"
#include <iostream>
#include "resource.h"
#include <common/logger/logger.h>
@@ -48,7 +47,6 @@ struct RealStringProvider : IStringProvider
int wmain(int argc, wchar_t* argv[])
{
winrt::init_apartment();
Trace::RegisterProvider();
LoggerHelpers::init_logger(L"FileLocksmithCLI", L"", LogSettings::fileLocksmithLoggerName);
Logger::info("FileLocksmithCLI started");
@@ -67,10 +65,7 @@ int wmain(int argc, wchar_t* argv[])
Logger::info("Command succeeded");
}
Trace::CLICommand(result.command_name.c_str(), result.exit_code == 0);
std::wcout << result.output;
Trace::UnregisterProvider();
return result.exit_code;
}
#endif

View File

@@ -52,7 +52,6 @@ namespace FileLocksmithCLIUnitTests
auto result = run_command(1, argv, finder, terminator, strings);
Assert::AreEqual(1, result.exit_code);
Assert::AreEqual(std::wstring(L"help"), result.command_name);
}
TEST_METHOD(TestHelp)
@@ -65,7 +64,6 @@ namespace FileLocksmithCLIUnitTests
auto result = run_command(2, argv, finder, terminator, strings);
Assert::AreEqual(0, result.exit_code);
Assert::AreEqual(std::wstring(L"help"), result.command_name);
}
TEST_METHOD(TestFindProcesses)
@@ -79,7 +77,6 @@ namespace FileLocksmithCLIUnitTests
auto result = run_command(2, argv, finder, terminator, strings);
Assert::AreEqual(0, result.exit_code);
Assert::AreEqual(std::wstring(L"query"), result.command_name);
Assert::IsTrue(result.output.find(L"123") != std::wstring::npos);
Assert::IsTrue(result.output.find(L"process") != std::wstring::npos);
}
@@ -97,7 +94,6 @@ namespace FileLocksmithCLIUnitTests
Microsoft::VisualStudio::CppUnitTestFramework::Logger::WriteMessage(result.output.c_str());
Assert::AreEqual(0, result.exit_code);
Assert::AreEqual(std::wstring(L"query-json"), result.command_name);
Assert::IsTrue(result.output.find(L"\"pid\"") != std::wstring::npos);
Assert::IsTrue(result.output.find(L"123") != std::wstring::npos);
}
@@ -113,7 +109,6 @@ namespace FileLocksmithCLIUnitTests
auto result = run_command(3, argv, finder, terminator, strings);
Assert::AreEqual(0, result.exit_code);
Assert::AreEqual(std::wstring(L"kill"), result.command_name);
Assert::AreEqual((size_t)1, terminator.terminatedPids.size());
Assert::AreEqual((DWORD)123, terminator.terminatedPids[0]);
}
@@ -130,7 +125,6 @@ namespace FileLocksmithCLIUnitTests
auto result = run_command(5, argv, finder, terminator, strings);
Assert::AreEqual(1, result.exit_code);
Assert::AreEqual(std::wstring(L"query-wait"), result.command_name);
}
};
}

View File

@@ -49,14 +49,3 @@ void Trace::QueryContextMenuError(_In_ HRESULT hr) noexcept
TraceLoggingHResult(hr),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}
void Trace::CLICommand(_In_ PCWSTR commandName, _In_ bool successful) noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"FileLocksmith_CLICommand",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingWideString(commandName, "CommandName"),
TraceLoggingBoolean(successful, "Successful"));
}

View File

@@ -11,5 +11,4 @@ public:
static void Invoked() noexcept;
static void InvokedRet(_In_ HRESULT hr) noexcept;
static void QueryContextMenuError(_In_ HRESULT hr) noexcept;
static void CLICommand(_In_ PCWSTR commandName, _In_ bool successful) noexcept;
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 766 B

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"
xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

View File

@@ -1,102 +0,0 @@
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"
#include "../../../common/version/version.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (United States) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_APP_ICON ICON "GrabAndMove.ico"
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", ORIGINAL_FILENAME
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
END
END
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED

View File

@@ -1,197 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{568c4c30-2e3c-4c2c-a691-007362073765}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>GrabAndMove</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<ProjectName>GrabAndMove</ProjectName>
<TargetName>PowerToys.GrabAndMove</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(RepoRoot)$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>
$(RepoRoot)src\common;
$(RepoRoot)src\common\SettingsAPI;
$(RepoRoot)src\;
%(AdditionalIncludeDirectories)
</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies>WindowsApp.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;UNICODE;_UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EntryPointSymbol>wWinMainCRTStartup</EntryPointSymbol>
<AdditionalDependencies>comctl32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;UNICODE;_UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EntryPointSymbol>wWinMainCRTStartup</EntryPointSymbol>
<AdditionalDependencies>comctl32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;UNICODE;_UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EntryPointSymbol>wWinMainCRTStartup</EntryPointSymbol>
<AdditionalDependencies>comctl32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;UNICODE;_UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EntryPointSymbol>wWinMainCRTStartup</EntryPointSymbol>
<AdditionalDependencies>comctl32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="GrabAndMove.rc" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)src\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446D-23F7-4023-9BB3-8657F904AF99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Image Include="GrabAndMove.ico" />
</ItemGroup>
<ItemGroup>
<Manifest Include="GrabAndMove.manifest" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View File

@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClInclude Include="resource.h" />
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="Resource Files">
<UniqueIdentifier>{195243ad-53ca-40c9-8879-b9efef4fc26d}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files">
<UniqueIdentifier>{9bdf974b-a58f-4af8-aed8-4882381f7172}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{0e19fd51-9939-4511-b8cb-d144c0e2a670}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<Image Include="GrabAndMove.ico">
<Filter>Resource Files</Filter>
</Image>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="GrabAndMove.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
#include "pch.h"

View File

@@ -1,14 +0,0 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h>
#include <commctrl.h>
#include <TraceLoggingProvider.h>
#include <limits>
#include <mutex>
#include <string>
#include <thread>
#include <vector>

View File

@@ -1,21 +0,0 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by GrabAndMove.rc
//
#pragma once
#define IDI_APP_ICON 101
#define IDR_TRAY_MENU 102
#define IDM_EXIT 1001
#define WM_TRAY_ICON (WM_USER + 1)
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "PowerToys GrabAndMove"
#define INTERNAL_NAME "PowerToys.GrabAndMove"
#define ORIGINAL_FILENAME "PowerToys.GrabAndMove.exe"
// Non-localizable
//////////////////////////////

View File

@@ -1,36 +0,0 @@
#include <windows.h>
#include "resource.h"
#include "../../../common/version/version.h"
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", ORIGINAL_FILENAME
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
END
END

View File

@@ -1,208 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{2c3f7770-4e57-46b7-8dc1-7428a383d0db}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>GrabAndMoveModuleInterface</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<ProjectName>GrabAndMoveModuleInterface</ProjectName>
<TargetName>PowerToys.GrabAndMoveModuleInterface</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(RepoRoot)$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;GRABANDMOVEMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;GRABANDMOVEMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;GRABANDMOVEMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;GRABANDMOVEMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(RepoRoot)src\common\inc;$(RepoRoot)src\common\Telemetry;..\..\;$(RepoRoot)src\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="trace.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Create</PrecompiledHeader>
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">pch.h</PrecompiledHeaderFile>
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Release|x64'">pch.h</PrecompiledHeaderFile>
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">pch.h</PrecompiledHeaderFile>
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">pch.h</PrecompiledHeaderFile>
</ClCompile>
<ClCompile Include="trace.cpp" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="GrabAndMoveModuleInterface.rc" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)src\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="$(RepoRoot)src\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="$(RepoRoot)deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -1,216 +0,0 @@
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include "trace.h"
#include <common/logger/logger.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/utils/logger_helper.h>
#include <common/interop/shared_constants.h>
extern "C" IMAGE_DOS_HEADER __ImageBase;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider();
break;
}
return TRUE;
}
// The PowerToy name that will be shown in the settings.
const static wchar_t* MODULE_NAME = L"GrabAndMove";
// Add a description that will be shown in the module settings page.
const static wchar_t* MODULE_DESC = L"Move and resize windows with Alt+Drag (left button to move, right button to resize).";
class GrabAndMoveInterface : public PowertoyModuleIface
{
private:
bool m_enabled = false;
HANDLE m_process{ nullptr };
HANDLE m_reload_settings_event_handle{ nullptr };
HANDLE m_exit_event_handle{ nullptr };
public:
GrabAndMoveInterface()
{
LoggerHelpers::init_logger(L"GrabAndMove", L"ModuleInterface", LogSettings::grabAndMoveLoggerName);
m_reload_settings_event_handle = CreateDefaultEvent(CommonSharedConstants::GRABANDMOVE_REFRESH_SETTINGS_EVENT);
m_exit_event_handle = CreateDefaultEvent(CommonSharedConstants::GRABANDMOVE_EXIT_EVENT);
}
virtual const wchar_t* get_key() override
{
return L"GrabAndMove";
}
virtual void destroy() override
{
disable();
delete this;
}
virtual const wchar_t* get_name() override
{
return MODULE_NAME;
}
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
{
return powertoys_gpo::getConfiguredGrabAndMoveEnabledValue();
}
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(MODULE_DESC);
settings.set_overview_link(L"https://aka.ms/powertoys");
return settings.serialize_to_buffer(buffer, buffer_size);
}
virtual void set_config(const wchar_t* config) override
{
try
{
auto values = PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
values.save_to_settings_file();
// Signal the GrabAndMove process to reload settings
if (m_reload_settings_event_handle)
{
SetEvent(m_reload_settings_event_handle);
}
}
catch (const std::exception&)
{
Logger::error("[GrabAndMove] set_config: Failed to parse or apply config.");
}
}
virtual void enable()
{
Logger::info(L"Enabling GrabAndMove module...");
if (m_process && WaitForSingleObject(m_process, 0) == WAIT_TIMEOUT)
{
m_enabled = true;
Trace::Enable(true);
Logger::debug(L"GrabAndMove process already running.");
return;
}
if (m_process)
{
CloseHandle(m_process);
m_process = nullptr;
}
m_enabled = false;
unsigned long powertoys_pid = GetCurrentProcessId();
std::wstring args = std::to_wstring(powertoys_pid);
std::wstring exe_name = L"PowerToys.GrabAndMove.exe";
std::wstring resolved_path(MAX_PATH, L'\0');
DWORD result = SearchPathW(
nullptr,
exe_name.c_str(),
nullptr,
static_cast<DWORD>(resolved_path.size()),
resolved_path.data(),
nullptr);
if (result == 0 || result >= resolved_path.size())
{
Logger::error(
L"Failed to locate GrabAndMove executable named '{}' at location '{}'",
exe_name,
resolved_path.c_str());
return;
}
resolved_path.resize(result);
Logger::debug(L"Resolved executable path: {}", resolved_path);
std::wstring command_line = L"\"" + resolved_path + L"\" " + args;
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
if (!CreateProcessW(
resolved_path.c_str(),
command_line.data(),
nullptr,
nullptr,
TRUE,
0,
nullptr,
nullptr,
&si,
&pi))
{
Logger::error(L"Failed to launch GrabAndMove process. {}", get_last_error_or_default(GetLastError()));
return;
}
Logger::info(L"GrabAndMove process launched successfully (PID: {}).", pi.dwProcessId);
m_process = pi.hProcess;
m_enabled = true;
Trace::Enable(true);
CloseHandle(pi.hThread);
}
virtual void disable()
{
Logger::info("GrabAndMove disabling");
m_enabled = false;
if (m_exit_event_handle)
{
SetEvent(m_exit_event_handle);
}
if (m_process)
{
constexpr DWORD timeout_ms = 1500;
DWORD result = WaitForSingleObject(m_process, timeout_ms);
if (result == WAIT_TIMEOUT)
{
Logger::warn("GrabAndMove: Process didn't exit in time. Forcing termination.");
TerminateProcess(m_process, 0);
}
CloseHandle(m_process);
m_process = nullptr;
}
Trace::Enable(false);
}
virtual bool is_enabled() override
{
return m_enabled;
}
virtual bool is_enabled_by_default() const override
{
return false;
}
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new GrabAndMoveInterface();
}

View File

@@ -1 +0,0 @@
#include "pch.h"

View File

@@ -1,9 +0,0 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/utils/gpo.h>
#include <common/utils/winapi_error.h>
#include <shlwapi.h>
#include <shellapi.h>

View File

@@ -1,12 +0,0 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "GrabAndMove Module"
#define INTERNAL_NAME "GrabAndMove"
#define ORIGINAL_FILENAME "PowerToys.GrabAndMoveModuleInterface.dll"
// Non-localizable
//////////////////////////////

View File

@@ -1,30 +0,0 @@
#include "pch.h"
#include "trace.h"
#include <TraceLoggingProvider.h>
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
void Trace::RegisterProvider()
{
TraceLoggingRegister(g_hProvider);
}
void Trace::UnregisterProvider()
{
TraceLoggingUnregister(g_hProvider);
}
void Trace::Enable(bool enabled) noexcept
{
TraceLoggingWrite(
g_hProvider,
"GrabAndMove_EnableGrabAndMove",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(enabled, "Enabled"));
}

View File

@@ -1,15 +0,0 @@
#pragma once
#include <windows.h>
#include <TraceLoggingActivity.h>
#include <common/telemetry/ProjectTelemetry.h>
TRACELOGGING_DECLARE_PROVIDER(g_hProvider);
class Trace
{
public:
static void RegisterProvider();
static void UnregisterProvider();
static void Enable(bool enabled) noexcept;
};

View File

@@ -3,101 +3,16 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PowerOCR"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Exit="Application_Exit"
ShutdownMode="OnExplicitShutdown"
Startup="Application_Startup"
ThemeMode="System">
Startup="Application_Startup">
<Application.Resources>
<ResourceDictionary>
<FontFamily x:Key="SymbolThemeFontFamily">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
<Style
x:Key="SubtleButtonStyle"
BasedOn="{StaticResource {x:Type Button}}"
TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border
x:Name="Border"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
SnapsToDevicePixels="True">
<ContentPresenter
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Focusable="False"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="SubtleToggleButtonStyle"
BasedOn="{StaticResource {x:Type ToggleButton}}"
TargetType="ToggleButton">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border
x:Name="Border"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
SnapsToDevicePixels="True">
<ContentPresenter
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Focusable="False"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource AccentFillColorDefaultBrush}" />
<Setter Property="Foreground" Value="{DynamicResource TextOnAccentFillColorPrimaryBrush}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ResourceDictionary.MergedDictionaries>
<ui:ThemesDictionary Theme="Dark" />
<ui:ControlsDictionary />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -1,12 +1,14 @@
<Window
<Window
x:Class="PowerOCR.OCROverlay"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:p="clr-namespace:PowerOCR.Properties"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
x:Name="TextExtractorWindow"
Title="TextExtractor"
ui:Design.Background="Transparent"
AllowsTransparency="True"
Background="Transparent"
Loaded="Window_Loaded"
@@ -20,6 +22,26 @@
WindowStyle="None"
mc:Ignorable="d">
<Window.Resources>
<Style BasedOn="{StaticResource DefaultToggleButtonStyle}" TargetType="{x:Type ToggleButton}">
<Setter Property="Margin" Value="4,0" />
<Setter Property="Padding" Value="0" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="32" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style BasedOn="{StaticResource DefaultButtonStyle}" TargetType="{x:Type Button}">
<Setter Property="Margin" Value="4,0" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Padding" Value="0" />
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="32" />
<Setter Property="Background" Value="Transparent" />
</Style>
</Window.Resources>
<Grid>
<Viewbox>
<Image x:Name="BackgroundImage" Stretch="UniformToFill" />
@@ -79,11 +101,17 @@
HorizontalAlignment="Center"
VerticalAlignment="Top"
d:Visibility="Visible"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
BorderBrush="{DynamicResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="1"
Background="{DynamicResource ApplicationBackgroundBrush}"
CornerRadius="8"
Visibility="Collapsed">
<Border.Effect>
<DropShadowEffect
BlurRadius="32"
Opacity="0.28"
RenderingBias="Performance"
ShadowDepth="1" />
</Border.Effect>
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Top"
@@ -105,67 +133,35 @@
</ComboBox>
<ToggleButton
x:Name="SingleLineToggleButton"
Width="32"
Height="32"
Margin="4,0"
Padding="0"
d:IsChecked="True"
AutomationProperties.Name="{x:Static p:Resources.ResultTextSingleLine}"
Click="SingleLineMenuItem_Click"
IsChecked="{Binding IsChecked, ElementName=SingleLineMenuItem, Mode=TwoWay}"
Style="{DynamicResource SubtleToggleButtonStyle}"
ToolTip="{x:Static p:Resources.ResultTextSingleLineShortcut}">
<TextBlock
FontFamily="{DynamicResource SymbolThemeFontFamily}"
FontSize="16"
Text="&#xF16E;" />
<ui:SymbolIcon FontSize="18" Symbol="SubtractSquare24" />
</ToggleButton>
<ToggleButton
x:Name="TableToggleButton"
Width="32"
Height="32"
Margin="4,0"
Padding="0"
d:IsChecked="True"
AutomationProperties.Name="{x:Static p:Resources.ResultTextTable}"
Click="TableToggleButton_Click"
IsChecked="{Binding IsChecked, ElementName=TableMenuItem, Mode=TwoWay}"
Style="{DynamicResource SubtleToggleButtonStyle}"
ToolTip="{x:Static p:Resources.ResultTextTableShortcut}">
<TextBlock
FontFamily="{DynamicResource SymbolThemeFontFamily}"
FontSize="16"
Text="&#xE80A;" />
<ui:SymbolIcon FontSize="18" Symbol="Table24" />
</ToggleButton>
<Button
x:Name="SettingsButton"
Width="32"
Height="32"
Margin="4,0"
Padding="0"
AutomationProperties.Name="{x:Static p:Resources.Settings}"
Click="SettingsMenuItem_Click"
Style="{DynamicResource SubtleButtonStyle}"
ToolTip="{x:Static p:Resources.Settings}">
<TextBlock
FontFamily="{DynamicResource SymbolThemeFontFamily}"
FontSize="16"
Text="&#xE713;" />
<ui:SymbolIcon FontSize="18" Symbol="Settings24" />
</Button>
<Button
x:Name="CancelButton"
Width="32"
Height="32"
Margin="4,0"
Padding="0"
AutomationProperties.Name="{x:Static p:Resources.Cancel}"
Click="CancelMenuItem_Click"
Style="{DynamicResource SubtleButtonStyle}"
ToolTip="{x:Static p:Resources.CancelShortcut}">
<TextBlock
FontFamily="{DynamicResource SymbolThemeFontFamily}"
FontSize="16"
Text="&#xE711;" />
<ui:SymbolIcon FontSize="18" Symbol="Dismiss24" />
</Button>
</StackPanel>
</Border>

View File

@@ -64,6 +64,8 @@ public partial class OCROverlay : Window
InitializeComponent();
Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this, Wpf.Ui.Controls.WindowBackdropType.None);
PopulateLanguageMenu();
}

View File

@@ -31,6 +31,7 @@
<ItemGroup>
<PackageReference Include="System.ComponentModel.Composition" />
<PackageReference Include="System.Drawing.Common" />
<PackageReference Include="WPF-UI" />
</ItemGroup>
<ItemGroup>

View File

@@ -252,13 +252,11 @@ void AlwaysOnTop::ProcessCommand(HWND window)
}
Sound::Type soundType = Sound::Type::Off;
bool stateChanged = false;
bool topmost = IsTopmost(window);
if (topmost)
{
if (UnpinTopmostWindow(window))
{
stateChanged = true;
auto iter = m_topmostWindows.find(window);
if (iter != m_topmostWindows.end())
{
@@ -276,7 +274,6 @@ void AlwaysOnTop::ProcessCommand(HWND window)
{
if (PinTopmostWindow(window))
{
stateChanged = true;
soundType = Sound::Type::On;
AssignBorder(window);
@@ -284,7 +281,7 @@ void AlwaysOnTop::ProcessCommand(HWND window)
}
}
if (stateChanged && AlwaysOnTopSettings::settings()->enableSound)
if (AlwaysOnTopSettings::settings()->enableSound)
{
m_sound.Play(soundType);
}

View File

@@ -19,7 +19,6 @@ using Awake.Core;
using Awake.Core.Models;
using Awake.Core.Native;
using Awake.Properties;
using Awake.Telemetry;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
@@ -69,7 +68,6 @@ namespace Awake
if (parseResult.Errors.Count > 0)
{
// Shows errors and returns non-zero.
LogCLITelemetry(successful: false);
return rootCommand.Invoke(args);
}
@@ -98,7 +96,6 @@ namespace Awake
{
// Awake is already running - there is no need for us to process
// anything further
LogCLITelemetry(successful: false);
Exit(Core.Constants.AppName + " is already running! Exiting the application.", 1);
return 1;
}
@@ -106,7 +103,6 @@ namespace Awake
{
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredAwakeEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{
LogCLITelemetry(successful: false);
Exit("PowerToys.Awake tried to start with a group policy setting that disables the tool. Please contact your system administrator.", 1);
return 1;
}
@@ -129,9 +125,7 @@ namespace Awake
Bridge.GetPwrCapabilities(out _powerCapabilities);
Logger.LogInfo(JsonSerializer.Serialize(_powerCapabilities, _serializerOptions));
var result = await rootCommand.InvokeAsync(args);
LogCLITelemetry(successful: result == 0);
return result;
return await rootCommand.InvokeAsync(args);
}
}
}
@@ -222,22 +216,6 @@ namespace Awake
return rootCommand;
}
private static void LogCLITelemetry(bool successful)
{
try
{
PowerToysTelemetry.Log.WriteEvent(new AwakeCLICommandEvent
{
CommandName = "awake",
Successful = successful,
});
}
catch (Exception ex)
{
Logger.LogError($"Failed to log CLI telemetry: {ex.Message}");
}
}
private static void AwakeUnhandledExceptionCatcher(object sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject is Exception exception)

View File

@@ -1,37 +0,0 @@
// 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.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Awake.Telemetry
{
/// <summary>
/// Telemetry event for Awake CLI command execution.
/// </summary>
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class AwakeCLICommandEvent : EventBase, IEvent
{
public AwakeCLICommandEvent()
{
EventName = "Awake_CLICommand";
CommandName = string.Empty;
}
/// <summary>
/// Gets or sets the name of the CLI command that was executed.
/// </summary>
public string CommandName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the command executed successfully.
/// </summary>
public bool Successful { get; set; }
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@@ -1,57 +0,0 @@
{
"solution": {
"path": "..\\..\\..\\PowerToys.slnx",
"projects": [
"src\\common\\CalculatorEngineCommon\\CalculatorEngineCommon.vcxproj",
"src\\common\\Common.UI.Controls\\Common.UI.Controls.csproj",
"src\\common\\ManagedCommon\\ManagedCommon.csproj",
"src\\common\\ManagedCsWin32\\ManagedCsWin32.csproj",
"src\\common\\ManagedTelemetry\\Telemetry\\ManagedTelemetry.csproj",
"src\\common\\interop\\PowerToys.Interop.vcxproj",
"src\\common\\version\\version.vcxproj",
"src\\modules\\cmdpal\\CmdPalKeyboardService\\CmdPalKeyboardService.vcxproj",
"src\\modules\\cmdpal\\CmdPalModuleInterface\\CmdPalModuleInterface.vcxproj",
"src\\modules\\cmdpal\\Microsoft.CmdPal.Common\\Microsoft.CmdPal.Common.csproj",
"src\\modules\\cmdpal\\Microsoft.CmdPal.UI.ViewModels\\Microsoft.CmdPal.UI.ViewModels.csproj",
"src\\modules\\cmdpal\\Microsoft.CmdPal.UI\\Microsoft.CmdPal.UI.csproj",
"src\\modules\\cmdpal\\Microsoft.Terminal.UI\\Microsoft.Terminal.UI.vcxproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Common.UnitTests\\Microsoft.CmdPal.Common.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Apps.UnitTests\\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Calc.UnitTests\\Microsoft.CmdPal.Ext.Calc.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Registry.UnitTests\\Microsoft.CmdPal.Ext.Registry.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests\\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Shell.UnitTests\\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.System.UnitTests\\Microsoft.CmdPal.Ext.System.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.TimeDate.UnitTests\\Microsoft.CmdPal.Ext.TimeDate.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.UnitTestsBase\\Microsoft.CmdPal.Ext.UnitTestBase.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.WebSearch.UnitTests\\Microsoft.CmdPal.Ext.WebSearch.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.WindowWalker.UnitTests\\Microsoft.CmdPal.Ext.WindowWalker.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.UI.ViewModels.UnitTests\\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.UITests\\Microsoft.CmdPal.UITests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CommandPalette.Extensions.Toolkit.UnitTests\\Microsoft.CommandPalette.Extensions.Toolkit.UnitTests.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Apps\\Microsoft.CmdPal.Ext.Apps.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Bookmark\\Microsoft.CmdPal.Ext.Bookmarks.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Calc\\Microsoft.CmdPal.Ext.Calc.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.ClipboardHistory\\Microsoft.CmdPal.Ext.ClipboardHistory.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Indexer\\Microsoft.CmdPal.Ext.Indexer.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.PerformanceMonitor\\Microsoft.CmdPal.Ext.PerformanceMonitor.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Registry\\Microsoft.CmdPal.Ext.Registry.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.RemoteDesktop\\Microsoft.CmdPal.Ext.RemoteDesktop.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Shell\\Microsoft.CmdPal.Ext.Shell.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.System\\Microsoft.CmdPal.Ext.System.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.TimeDate\\Microsoft.CmdPal.Ext.TimeDate.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.WebSearch\\Microsoft.CmdPal.Ext.WebSearch.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.WinGet\\Microsoft.CmdPal.Ext.WinGet.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.WindowWalker\\Microsoft.CmdPal.Ext.WindowWalker.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.WindowsServices\\Microsoft.CmdPal.Ext.WindowsServices.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.WindowsSettings\\Microsoft.CmdPal.Ext.WindowsSettings.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.WindowsTerminal\\Microsoft.CmdPal.Ext.WindowsTerminal.csproj",
"src\\modules\\cmdpal\\ext\\ProcessMonitorExtension\\ProcessMonitorExtension.csproj",
"src\\modules\\cmdpal\\ext\\SamplePagesExtension\\SamplePagesExtension.csproj",
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions.Toolkit\\Microsoft.CommandPalette.Extensions.Toolkit.csproj",
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions\\Microsoft.CommandPalette.Extensions.vcxproj"
]
}
}

View File

@@ -7,14 +7,8 @@
"src\\common\\ManagedCommon\\ManagedCommon.csproj",
"src\\common\\ManagedCsWin32\\ManagedCsWin32.csproj",
"src\\common\\ManagedTelemetry\\Telemetry\\ManagedTelemetry.csproj",
"src\\common\\SettingsAPI\\SettingsAPI.vcxproj",
"src\\common\\UITestAutomation\\UITestAutomation.csproj",
"src\\common\\interop\\PowerToys.Interop.vcxproj",
"src\\common\\logger\\logger.vcxproj",
"src\\common\\version\\version.vcxproj",
"src\\logging\\logging.vcxproj",
"src\\modules\\MouseUtils\\MouseJump.Common\\MouseJump.Common.csproj",
"src\\modules\\ZoomIt\\ZoomItSettingsInterop\\ZoomItSettingsInterop.vcxproj",
"src\\modules\\cmdpal\\CmdPalKeyboardService\\CmdPalKeyboardService.vcxproj",
"src\\modules\\cmdpal\\CmdPalModuleInterface\\CmdPalModuleInterface.vcxproj",
"src\\modules\\cmdpal\\Microsoft.CmdPal.Common\\Microsoft.CmdPal.Common.csproj",
@@ -57,9 +51,7 @@
"src\\modules\\cmdpal\\ext\\ProcessMonitorExtension\\ProcessMonitorExtension.csproj",
"src\\modules\\cmdpal\\ext\\SamplePagesExtension\\SamplePagesExtension.csproj",
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions.Toolkit\\Microsoft.CommandPalette.Extensions.Toolkit.csproj",
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions\\Microsoft.CommandPalette.Extensions.vcxproj",
"src\\modules\\powerdisplay\\PowerDisplay.Models\\PowerDisplay.Models.csproj",
"src\\settings-ui\\Settings.UI.Library\\Settings.UI.Library.csproj"
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions\\Microsoft.CommandPalette.Extensions.vcxproj"
]
}
}

View File

@@ -2,7 +2,6 @@
// 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.Globalization;
using Microsoft.CmdPal.Ext.Calc.Helper;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -11,25 +10,6 @@ namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
[TestClass]
public class QueryHelperTests
{
private CultureInfo originalCulture;
private CultureInfo originalUiCulture;
[TestInitialize]
public void Setup()
{
originalCulture = CultureInfo.CurrentCulture;
CultureInfo.CurrentCulture = new CultureInfo("en-us", false);
originalUiCulture = CultureInfo.CurrentUICulture;
CultureInfo.CurrentUICulture = new CultureInfo("en-us", false);
}
[TestCleanup]
public void CleanUp()
{
CultureInfo.CurrentCulture = originalCulture;
CultureInfo.CurrentUICulture = originalUiCulture;
}
[DataTestMethod]
[DataRow("2²", "4")]
[DataRow("2³", "8")]

View File

@@ -2,7 +2,6 @@
// 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.Globalization;
using System.Linq;
using Microsoft.CmdPal.Ext.Calc.Helper;
using Microsoft.CmdPal.Ext.Calc.Pages;
@@ -15,25 +14,6 @@ namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
[TestClass]
public class QueryTests : CommandPaletteUnitTestBase
{
private CultureInfo originalCulture;
private CultureInfo originalUiCulture;
[TestInitialize]
public void Setup()
{
originalCulture = CultureInfo.CurrentCulture;
CultureInfo.CurrentCulture = new CultureInfo("en-us", false);
originalUiCulture = CultureInfo.CurrentUICulture;
CultureInfo.CurrentUICulture = new CultureInfo("en-us", false);
}
[TestCleanup]
public void CleanUp()
{
CultureInfo.CurrentCulture = originalCulture;
CultureInfo.CurrentUICulture = originalUiCulture;
}
[DataTestMethod]
[DataRow("2+2", "4")]
[DataRow("5*3", "15")]
@@ -104,40 +84,6 @@ public class QueryTests : CommandPaletteUnitTestBase
Assert.IsTrue(result.Title.Contains(expected, System.StringComparison.Ordinal), $"Calc trigMode convert result isn't correct. Current result: {result.Title}");
}
[DataTestMethod]
[DataRow("sin(60)", "-0.30481", CalculateEngine.TrigMode.Radians, "de-DE")]
[DataRow("sin(60)", "0.866025", CalculateEngine.TrigMode.Degrees, "de-DE")]
[DataRow("sin(60)", "0.809016", CalculateEngine.TrigMode.Gradians, "de-DE")]
[DataRow("sin(60)", "-0.30481", CalculateEngine.TrigMode.Radians, "fr-FR")]
[DataRow("sin(60)", "0.866025", CalculateEngine.TrigMode.Degrees, "fr-FR")]
[DataRow("sin(60)", "0.809016", CalculateEngine.TrigMode.Gradians, "fr-FR")]
public void TrigModeSettingsTest_NonEnglishCulture(string input, string expected, CalculateEngine.TrigMode trigMode, string cultureName)
{
var previousCulture = CultureInfo.CurrentCulture;
var previousUiCulture = CultureInfo.CurrentUICulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo(cultureName, false);
CultureInfo.CurrentUICulture = new CultureInfo(cultureName, false);
var settings = new Settings(trigUnit: trigMode, outputUseEnglishFormat: true);
var page = new CalculatorListPage(settings);
page.UpdateSearchText(string.Empty, input);
var result = page.GetItems().FirstOrDefault();
Assert.IsNotNull(result);
Assert.IsTrue(result.Title.Contains(expected, System.StringComparison.Ordinal), $"Calc trigMode convert result isn't correct for culture '{cultureName}'. Current result: {result.Title}");
}
finally
{
CultureInfo.CurrentCulture = previousCulture;
CultureInfo.CurrentUICulture = previousUiCulture;
}
}
[DataTestMethod]
[DataRow("2^64", "18446744073709551616", "0x10000000000000000")]
[DataRow("0-(2^64)", "-18446744073709551616", "-0x10000000000000000")]

View File

Before

Width:  |  Height:  |  Size: 497 KiB

After

Width:  |  Height:  |  Size: 497 KiB

View File

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 276 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

Before

Width:  |  Height:  |  Size: 269 KiB

After

Width:  |  Height:  |  Size: 269 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 228 KiB

View File

Before

Width:  |  Height:  |  Size: 491 KiB

After

Width:  |  Height:  |  Size: 491 KiB

View File

Before

Width:  |  Height:  |  Size: 706 KiB

After

Width:  |  Height:  |  Size: 706 KiB

View File

Before

Width:  |  Height:  |  Size: 408 KiB

After

Width:  |  Height:  |  Size: 408 KiB

View File

Before

Width:  |  Height:  |  Size: 384 KiB

After

Width:  |  Height:  |  Size: 384 KiB

View File

Before

Width:  |  Height:  |  Size: 493 KiB

After

Width:  |  Height:  |  Size: 493 KiB

View File

Before

Width:  |  Height:  |  Size: 492 KiB

After

Width:  |  Height:  |  Size: 492 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

Before

Width:  |  Height:  |  Size: 311 KiB

After

Width:  |  Height:  |  Size: 311 KiB

View File

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 297 KiB

View File

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 284 KiB

View File

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 225 KiB

View File

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

View File

Before

Width:  |  Height:  |  Size: 890 KiB

After

Width:  |  Height:  |  Size: 890 KiB

View File

Before

Width:  |  Height:  |  Size: 858 KiB

After

Width:  |  Height:  |  Size: 858 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -108,6 +108,7 @@ internal static class ResultHelper
tags.Add(new Tag
{
Text = Resources.windowwalker_NotResponding,
Foreground = ColorHelpers.FromRgb(220, 20, 60),
});
}

View File

@@ -7,9 +7,7 @@ using System.Globalization;
using System.Text;
using ImageResizer.Cli;
using ImageResizer.Cli.Telemetry;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
namespace ImageResizerCLI;
@@ -39,33 +37,14 @@ internal static class Program
try
{
var executor = new ImageResizerCliExecutor();
int result = executor.Run(args);
LogCLITelemetry(executor.CommandName, result == 0);
return result;
return executor.Run(args);
}
catch (Exception ex)
{
CliLogger.Error($"Unhandled exception: {ex.Message}");
CliLogger.Error($"Stack trace: {ex.StackTrace}");
Console.Error.WriteLine($"Fatal error: {ex.Message}");
LogCLITelemetry("resize", successful: false);
return 1;
}
}
private static void LogCLITelemetry(string commandName, bool successful)
{
try
{
PowerToysTelemetry.Log.WriteEvent(new ImageResizerCLICommandEvent
{
CommandName = commandName,
Successful = successful,
});
}
catch (Exception ex)
{
CliLogger.Error($"Failed to log CLI telemetry: {ex.Message}");
}
}
}

View File

@@ -19,11 +19,6 @@ namespace ImageResizer.Cli
/// </summary>
public class ImageResizerCliExecutor
{
/// <summary>
/// Gets the name of the last CLI operation that was executed.
/// </summary>
public string CommandName { get; private set; } = "resize";
/// <summary>
/// Runs the CLI executor with the provided command-line arguments.
/// </summary>
@@ -42,21 +37,18 @@ namespace ImageResizer.Cli
}
CliOptions.PrintUsage();
CommandName = "error";
return 1;
}
if (cliOptions.ShowHelp)
{
CliOptions.PrintUsage();
CommandName = "help";
return 0;
}
if (cliOptions.ShowConfig)
{
CliOptions.PrintConfig(Settings.Default);
CommandName = "show-config";
return 0;
}
@@ -64,7 +56,6 @@ namespace ImageResizer.Cli
{
Console.WriteLine(Resources.CLI_NoInputFiles);
CliOptions.PrintUsage();
CommandName = "error";
return 1;
}

View File

@@ -1,37 +0,0 @@
// 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.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace ImageResizer.Cli.Telemetry
{
/// <summary>
/// Telemetry event for Image Resizer CLI command execution.
/// </summary>
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class ImageResizerCLICommandEvent : EventBase, IEvent
{
public ImageResizerCLICommandEvent()
{
EventName = "ImageResizer_CLICommand";
CommandName = string.Empty;
}
/// <summary>
/// Gets or sets the name of the CLI command that was executed.
/// </summary>
public string CommandName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the command executed successfully.
/// </summary>
public bool Successful { get; set; }
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

Some files were not shown because too many files have changed in this diff Show More