[Awake] Fix for countdown timer drift (#41684)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

- 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.

<!-- Please review the items on the PR checklist before submitting-->
## 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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

This replaces the combined `Observable.Timer` and `Observable.Interval`
timers with a single 1-second Interval timer which checks against a
fixed expiry time.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## 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:

<img width="386" height="109" alt="image"
src="https://github.com/user-attachments/assets/b282dfd8-38e7-48ab-b17c-99756ef73b99"
/>

3. The countdown timer in the systray menu counts down for larger values
than were previously possible:

<img width="380" height="104" alt="image"
src="https://github.com/user-attachments/assets/7a807a37-8945-4048-a86c-05e6ac9310a9"
/>

4. On a heavily CPU-loaded system, the previous countdown drift does not
happen.
5. The expirable keep-awake mode still functions as expected.
This commit is contained in:
Dave Rayment
2025-10-14 09:52:27 +01:00
committed by GitHub
parent bb6f9a8b08
commit 471022e842

View File

@@ -281,21 +281,7 @@ namespace Awake.Core
TimeSpan remainingTime = expireAt - DateTimeOffset.Now; TimeSpan remainingTime = expireAt - DateTimeOffset.Now;
Observable.Timer(remainingTime).Subscribe( Observable.Timer(remainingTime).Subscribe(
_ => _ => HandleTimerCompletion("expirable"),
{
Logger.LogInfo("Completed expirable keep-awake.");
CancelExistingThread();
if (IsUsingPowerToysConfig)
{
SetPassiveKeepAwake();
}
else
{
Logger.LogInfo("Exiting after expirable keep awake.");
CompleteExit(Environment.ExitCode);
}
},
_tokenSource.Token); _tokenSource.Token);
} }
@@ -348,49 +334,46 @@ namespace Awake.Core
SetModeShellIcon(); SetModeShellIcon();
ulong desiredDuration = (ulong)seconds * 1000; var targetExpiryTime = DateTimeOffset.Now.AddSeconds(seconds);
ulong targetDuration = Math.Min(desiredDuration, uint.MaxValue - 1) / 1000;
if (desiredDuration > uint.MaxValue) Observable.Interval(TimeSpan.FromSeconds(1))
{ .Select(_ => targetExpiryTime - DateTimeOffset.Now)
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"); .TakeWhile(remaining => remaining.TotalSeconds > 0)
} .Subscribe(
remainingTimeSpan =>
IObservable<long> timerObservable = Observable.Timer(TimeSpan.FromSeconds(targetDuration));
IObservable<long> intervalObservable = Observable.Interval(TimeSpan.FromSeconds(1)).TakeUntil(timerObservable);
IObservable<long> combinedObservable = Observable.CombineLatest(intervalObservable, timerObservable.StartWith(0), (elapsedSeconds, _) => elapsedSeconds + 1);
combinedObservable.Subscribe(
elapsedSeconds =>
{
TimeRemaining = (uint)targetDuration - (uint)elapsedSeconds;
if (TimeRemaining >= 0)
{ {
TimeRemaining = (uint)remainingTimeSpan.TotalSeconds;
TrayHelper.SetShellIcon( TrayHelper.SetShellIcon(
TrayHelper.WindowHandle, 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, TrayHelper.TimedIcon,
TrayIconAction.Update); TrayIconAction.Update);
} },
}, _ => HandleTimerCompletion("timed"),
() => _tokenSource.Token);
{ }
Logger.LogInfo("Completed timed thread.");
CancelExistingThread();
if (IsUsingPowerToysConfig) /// <summary>
{ /// Handles the common logic that should execute when a keep-awake timer completes. Resets
// If we're using PowerToys settings, we need to make sure that /// the application state to Passive if configured; otherwise it exits.
// we just switch over the Passive Keep-Awake. /// </summary>
SetPassiveKeepAwake(); private static void HandleTimerCompletion(string timerType)
} {
else Logger.LogInfo($"Completed {timerType} keep-awake.");
{ CancelExistingThread();
Logger.LogInfo("Exiting after timed keep-awake.");
CompleteExit(Environment.ExitCode); if (IsUsingPowerToysConfig)
} {
}, // If running under PowerToys settings, just revert to the default Passive state.
_tokenSource.Token); SetPassiveKeepAwake();
}
else
{
// If running as a standalone process, exit cleanly.
Logger.LogInfo($"Exiting after {timerType} keep-awake.");
CompleteExit(Environment.ExitCode);
}
} }
/// <summary> /// <summary>