[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;
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<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)
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);
/// <summary>
/// Handles the common logic that should execute when a keep-awake timer completes. Resets
/// the application state to Passive if configured; otherwise it exits.
/// </summary>
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);
}
}
/// <summary>