Awake and DevEx improvements (#44795)

This PR contains a set of bug fixes and general improvements to
[Awake](https://awake.den.dev/) and developer experience tooling for
building the module.

### Awake Fixes

- **#32544** - Fixed an issue where Awake settings became non-functional
after the PC wakes from sleep. Added `WM_POWERBROADCAST` handling to
detect system resume events (`PBT_APMRESUMEAUTOMATIC`,
`PBT_APMRESUMESUSPEND`) and re-apply `SetThreadExecutionState` to
restore the awake state.

- **#36150** - Fixed an issue where Awake would not prevent sleep when
AC power is connected. Added `PBT_APMPOWERSTATUSCHANGE` handling to
re-apply `SetThreadExecutionState` when the power source changes
(AC/battery transitions).

- **#41674** - Fixed silent failure when `SetThreadExecutionState`
fails. The monitor thread now handles the return value, logs an error,
and reverts to passive mode with updated tray icon.

- **#41738** - Fixed `--display-on` CLI flag default from `true` to
`false` to align with documentation and PowerToys settings behavior.
This is a breaking change for scripts relying on the undocumented
default.

- **#41918** - Fixed `WM_COMMAND` message processing flaw in
`TrayHelper.WndProc` that incorrectly compared enum values against enum
count. Added proper bounds checking for custom tray time entries.

- **#44134** - Documented that `ES_DISPLAY_REQUIRED` (used when "Keep
display on" is enabled) blocks Task Scheduler idle detection, preventing
scheduled maintenance tasks like SSD TRIM. Workaround: disable "Keep
display on" or manually run `Optimize-Volume -DriveLetter C -ReTrim`.

- **#38770** - Fixed tray icon failing to appear after Windows updates.
Increased retry attempts and delays for icon Add operations (10
attempts, up to ~15.5 seconds total) while keeping existing fast retry
behavior for Update/Delete operations.

- **#40501** - Fixed tray icon not disappearing when Awake is disabled.
The `SetShellIcon` function was incorrectly requiring an icon for Delete
operations, causing the `NIM_DELETE` message to never be sent.

- Fixed an issue where toggling "Keep screen on" during an active timed
session would disrupt the countdown timer. The display setting now
updates directly without restarting the timer, preserving the exact
remaining time.

### Performance Optimizations

- Fixed O(n²) loop in `TrayHelper.CreateAwakeTimeSubMenu` by replacing
`ElementAt(i)` with `foreach` iteration.

- Fixed Observable subscription leak in `Manager.cs` by storing
`IDisposable` and disposing in `CancelExistingThread()`. Also removed
dead `_tokenSource` code that was no longer used.

- Reduced allocations in `SingleThreadSynchronizationContext` by
changing `Tuple<>` to `ValueTuple`.

- Replaced dedicated exit event thread with
`ThreadPool.RegisterWaitForSingleObject()` to reduce resource usage.

### Code Quality

- Replaced `Console.WriteLine` with `Logger.LogError` in `TrayHelper.cs`
for consistent logging.

- Added proper error logging to silent exception catches in
`AwakeService.cs`.

- Removed dead `Math.Min(minutes, int.MaxValue)` code where `minutes` is
already an `int`.

- Extracted hardcoded tray icon ID to named constant `TrayIconId`.

- Standardized null coalescing for `GetSettings<AwakeSettings>()` calls
across all files.

### Debugging Experience Fixes

- Fixed first-chance exceptions in `settings_window.cpp` during
debugging. Added `HasKey()` check before accessing `hotkey_changed`
property to prevent `hresult_error` exceptions when the property doesn't
exist in module settings.

- Fixed first-chance exceptions in FindMyMouse `parse_settings` during
debugging. Refactored to extract the properties object once and added
`HasKey()` checks before all `GetNamedObject()` calls. This prevents
`winrt::hresult_error` exceptions when optional settings keys (like
legacy `overlay_opacity`) don't exist, improving the debugging
experience by eliminating spurious exception breaks.

- Fixed LightSwitch.UITests build failures when building from a clean
state. Added missing project references (`ManagedCommon`,
`LightSwitchModuleInterface`) with `ReferenceOutputAssembly=false` to
ensure proper build ordering, and added existence check for the native
DLL copy operation.

### Developer Experience

- Added `setup-dev-environment.ps1` script to automate development
environment setup.
- Added `clean-artifacts.ps1` script to resolve build errors from
corrupted build state or missing image files.
- Added build script that allows standalone command line build of the
Awake module.
- Added troubleshooting section to
`doc/devdocs/development/debugging.md` with guidance on resolving common
build errors.
This commit is contained in:
Den Delimarsky
2026-01-20 21:27:45 -08:00
committed by GitHub
parent 2a0d0a1210
commit 27dcd1e5bc
22 changed files with 1506 additions and 231 deletions

View File

@@ -238,12 +238,30 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
{
auto settingsObject = settings.get_raw_json();
FindMyMouseSettings findMyMouseSettings;
if (settingsObject.GetView().Size())
if (!settingsObject.GetView().Size())
{
Logger::info("Find My Mouse settings are empty");
m_findMyMouseSettings = findMyMouseSettings;
return;
}
// Early exit if no properties object exists
if (!settingsObject.HasKey(JSON_KEY_PROPERTIES))
{
Logger::info("Find My Mouse settings have no properties");
m_findMyMouseSettings = findMyMouseSettings;
return;
}
auto properties = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
// Parse Activation Method
if (properties.HasKey(JSON_KEY_ACTIVATION_METHOD))
{
try
{
// Parse Activation Method
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_METHOD);
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_ACTIVATION_METHOD);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value < static_cast<int>(FindMyMouseActivationMethod::EnumElements) && value >= 0)
{
@@ -266,34 +284,50 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
{
Logger::warn("Failed to initialize Activation Method from settings. Will use default value");
}
}
// Parse Include Win Key
if (properties.HasKey(JSON_KEY_INCLUDE_WIN_KEY))
{
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_INCLUDE_WIN_KEY);
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_INCLUDE_WIN_KEY);
findMyMouseSettings.includeWinKey = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
Logger::warn("Failed to get 'include windows key with ctrl' setting");
}
}
// Parse Do Not Activate On Game Mode
if (properties.HasKey(JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE))
{
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE);
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE);
findMyMouseSettings.doNotActivateOnGameMode = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
Logger::warn("Failed to get 'do not activate on game mode' setting");
}
// Colors + legacy overlay opacity migration
// Desired behavior:
// - Old schema: colors stored as RGB (no alpha) + separate overlay_opacity (0-100). We should migrate by applying that opacity as alpha.
// - New schema: colors stored as ARGB (alpha embedded). Ignore overlay_opacity even if still present.
int legacyOverlayOpacity = -1;
bool backgroundColorHadExplicitAlpha = false;
bool spotlightColorHadExplicitAlpha = false;
}
// Colors + legacy overlay opacity migration
// Desired behavior:
// - Old schema: colors stored as RGB (no alpha) + separate overlay_opacity (0-100). We should migrate by applying that opacity as alpha.
// - New schema: colors stored as ARGB (alpha embedded). Ignore overlay_opacity even if still present.
int legacyOverlayOpacity = -1;
bool backgroundColorHadExplicitAlpha = false;
bool spotlightColorHadExplicitAlpha = false;
// Parse Legacy Overlay Opacity (may not exist in newer settings)
if (properties.HasKey(JSON_KEY_OVERLAY_OPACITY))
{
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_OVERLAY_OPACITY);
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_OVERLAY_OPACITY);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0 && value <= 100)
{
@@ -302,11 +336,16 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
}
catch (...)
{
// overlay_opacity may not exist anymore
// overlay_opacity may have invalid data
}
}
// Parse Background Color
if (properties.HasKey(JSON_KEY_BACKGROUND_COLOR))
{
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_BACKGROUND_COLOR);
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_BACKGROUND_COLOR);
auto backgroundColorStr = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE);
uint8_t a = 255, r, g, b;
bool parsed = false;
@@ -333,9 +372,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
{
Logger::warn("Failed to initialize background color from settings. Will use default value");
}
}
// Parse Spotlight Color
if (properties.HasKey(JSON_KEY_SPOTLIGHT_COLOR))
{
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SPOTLIGHT_COLOR);
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SPOTLIGHT_COLOR);
auto spotlightColorStr = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE);
uint8_t a = 255, r, g, b;
bool parsed = false;
@@ -362,10 +406,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
{
Logger::warn("Failed to initialize spotlight color from settings. Will use default value");
}
}
// Parse Spotlight Radius
if (properties.HasKey(JSON_KEY_SPOTLIGHT_RADIUS))
{
try
{
// Parse Spotlight Radius
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SPOTLIGHT_RADIUS);
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SPOTLIGHT_RADIUS);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
@@ -380,10 +428,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
{
Logger::warn("Failed to initialize Spotlight Radius from settings. Will use default value");
}
}
// Parse Animation Duration
if (properties.HasKey(JSON_KEY_ANIMATION_DURATION_MS))
{
try
{
// Parse Animation Duration
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ANIMATION_DURATION_MS);
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_ANIMATION_DURATION_MS);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
@@ -398,10 +450,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
{
Logger::warn("Failed to initialize Animation Duration from settings. Will use default value");
}
}
// Parse Spotlight Initial Zoom
if (properties.HasKey(JSON_KEY_SPOTLIGHT_INITIAL_ZOOM))
{
try
{
// Parse Spotlight Initial Zoom
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SPOTLIGHT_INITIAL_ZOOM);
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SPOTLIGHT_INITIAL_ZOOM);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
@@ -416,10 +472,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
{
Logger::warn("Failed to initialize Spotlight Initial Zoom from settings. Will use default value");
}
}
// Parse Excluded Apps
if (properties.HasKey(JSON_KEY_EXCLUDED_APPS))
{
try
{
// Parse Excluded Apps
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_EXCLUDED_APPS);
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_EXCLUDED_APPS);
std::wstring apps = jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE).c_str();
std::vector<std::wstring> excludedApps;
auto excludedUppercase = apps;
@@ -441,10 +501,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
{
Logger::warn("Failed to initialize Excluded Apps from settings. Will use default value");
}
}
// Parse Shaking Minimum Distance
if (properties.HasKey(JSON_KEY_SHAKING_MINIMUM_DISTANCE))
{
try
{
// Parse Shaking Minimum Distance
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SHAKING_MINIMUM_DISTANCE);
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SHAKING_MINIMUM_DISTANCE);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
@@ -459,10 +523,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
{
Logger::warn("Failed to initialize Shaking Minimum Distance from settings. Will use default value");
}
}
// Parse Shaking Interval Milliseconds
if (properties.HasKey(JSON_KEY_SHAKING_INTERVAL_MS))
{
try
{
// Parse Shaking Interval Milliseconds
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SHAKING_INTERVAL_MS);
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SHAKING_INTERVAL_MS);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
@@ -477,10 +545,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
{
Logger::warn("Failed to initialize Shaking Interval Milliseconds from settings. Will use default value");
}
}
// Parse Shaking Factor
if (properties.HasKey(JSON_KEY_SHAKING_FACTOR))
{
try
{
// Parse Shaking Factor
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SHAKING_FACTOR);
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SHAKING_FACTOR);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
@@ -495,11 +567,14 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
{
Logger::warn("Failed to initialize Shaking Factor from settings. Will use default value");
}
}
// Parse HotKey
if (properties.HasKey(JSON_KEY_ACTIVATION_SHORTCUT))
{
try
{
// Parse HotKey
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
m_hotkey = HotkeyEx();
if (hotkey.win_pressed())
@@ -528,18 +603,15 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
{
Logger::warn("Failed to initialize Activation Shortcut from settings. Will use default value");
}
}
if (!m_hotkey.modifiersMask)
{
Logger::info("Using default Activation Shortcut");
m_hotkey.modifiersMask = MOD_SHIFT | MOD_WIN;
m_hotkey.vkCode = 0x46; // F key
}
}
else
if (!m_hotkey.modifiersMask)
{
Logger::info("Find My Mouse settings are empty");
Logger::info("Using default Activation Shortcut");
m_hotkey.modifiersMask = MOD_SHIFT | MOD_WIN;
m_hotkey.vkCode = 0x46; // F key
}
m_findMyMouseSettings = findMyMouseSettings;
}