From 471022e8421eda6608aa0ea19fefdb1eb1e76143 Mon Sep 17 00:00:00 2001 From: Dave Rayment Date: Tue, 14 Oct 2025 09:52:27 +0100 Subject: [PATCH] [Awake] Fix for countdown timer drift (#41684) ## Summary of the Pull Request - Fixes the countdown timer drift issue #41671 - Includes minor refactoring to consolidate identical timer completion code in `SetExpirableKeepAwake` and `SetTimedKeepAwake`. - Removes the ~50 day restriction on timed keep-awake. The timer may now be `uint.MaxValue` seconds, or ~136 years. ## PR Checklist - [x] Closes: #41671 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments This replaces the combined `Observable.Timer` and `Observable.Interval` timers with a single 1-second Interval timer which checks against a fixed expiry time. ## Validation Steps Performed Checked that: 1. The timed keep-awake works via the `--time-limit` parameter, and expiry occurs on time. 2. The countdown timer in the systray menu correctly counts down for small values: image 3. The countdown timer in the systray menu counts down for larger values than were previously possible: image 4. On a heavily CPU-loaded system, the previous countdown drift does not happen. 5. The expirable keep-awake mode still functions as expected. --- src/modules/awake/Awake/Core/Manager.cs | 85 ++++++++++--------------- 1 file changed, 34 insertions(+), 51 deletions(-) diff --git a/src/modules/awake/Awake/Core/Manager.cs b/src/modules/awake/Awake/Core/Manager.cs index df4ac87581..ad4c417b31 100644 --- a/src/modules/awake/Awake/Core/Manager.cs +++ b/src/modules/awake/Awake/Core/Manager.cs @@ -281,21 +281,7 @@ namespace Awake.Core TimeSpan remainingTime = expireAt - DateTimeOffset.Now; Observable.Timer(remainingTime).Subscribe( - _ => - { - Logger.LogInfo("Completed expirable keep-awake."); - CancelExistingThread(); - - if (IsUsingPowerToysConfig) - { - SetPassiveKeepAwake(); - } - else - { - Logger.LogInfo("Exiting after expirable keep awake."); - CompleteExit(Environment.ExitCode); - } - }, + _ => HandleTimerCompletion("expirable"), _tokenSource.Token); } @@ -348,49 +334,46 @@ namespace Awake.Core SetModeShellIcon(); - ulong desiredDuration = (ulong)seconds * 1000; - ulong targetDuration = Math.Min(desiredDuration, uint.MaxValue - 1) / 1000; + var targetExpiryTime = DateTimeOffset.Now.AddSeconds(seconds); - if (desiredDuration > uint.MaxValue) - { - Logger.LogInfo($"The desired interval of {seconds} seconds ({desiredDuration}ms) exceeds the limit. Defaulting to maximum possible value: {targetDuration} seconds. Read more about existing limits in the official documentation: https://aka.ms/powertoys/awake"); - } - - IObservable timerObservable = Observable.Timer(TimeSpan.FromSeconds(targetDuration)); - IObservable intervalObservable = Observable.Interval(TimeSpan.FromSeconds(1)).TakeUntil(timerObservable); - IObservable combinedObservable = Observable.CombineLatest(intervalObservable, timerObservable.StartWith(0), (elapsedSeconds, _) => elapsedSeconds + 1); - - combinedObservable.Subscribe( - elapsedSeconds => - { - TimeRemaining = (uint)targetDuration - (uint)elapsedSeconds; - if (TimeRemaining >= 0) + Observable.Interval(TimeSpan.FromSeconds(1)) + .Select(_ => targetExpiryTime - DateTimeOffset.Now) + .TakeWhile(remaining => remaining.TotalSeconds > 0) + .Subscribe( + remainingTimeSpan => { + TimeRemaining = (uint)remainingTimeSpan.TotalSeconds; + TrayHelper.SetShellIcon( TrayHelper.WindowHandle, - $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_TIMED}][{ScreenStateString}][{TimeSpan.FromSeconds(TimeRemaining).ToHumanReadableString()}]", + $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_TIMED}][{ScreenStateString}][{remainingTimeSpan.ToHumanReadableString()}]", TrayHelper.TimedIcon, TrayIconAction.Update); - } - }, - () => - { - Logger.LogInfo("Completed timed thread."); - CancelExistingThread(); + }, + _ => HandleTimerCompletion("timed"), + _tokenSource.Token); + } - if (IsUsingPowerToysConfig) - { - // If we're using PowerToys settings, we need to make sure that - // we just switch over the Passive Keep-Awake. - SetPassiveKeepAwake(); - } - else - { - Logger.LogInfo("Exiting after timed keep-awake."); - CompleteExit(Environment.ExitCode); - } - }, - _tokenSource.Token); + /// + /// Handles the common logic that should execute when a keep-awake timer completes. Resets + /// the application state to Passive if configured; otherwise it exits. + /// + private static void HandleTimerCompletion(string timerType) + { + Logger.LogInfo($"Completed {timerType} keep-awake."); + CancelExistingThread(); + + if (IsUsingPowerToysConfig) + { + // If running under PowerToys settings, just revert to the default Passive state. + SetPassiveKeepAwake(); + } + else + { + // If running as a standalone process, exit cleanly. + Logger.LogInfo($"Exiting after {timerType} keep-awake."); + CompleteExit(Environment.ExitCode); + } } ///