Skip to content

Commit

Permalink
Awake vNext - NOBLE_SIX_02162023 (microsoft#24183)
Browse files Browse the repository at this point in the history
* Initial scaffolding for expiration configuration

* Simplifying the code and adding support for expiration events

* Bit more cleanup

* Initial support for expirable keep-awake

* Update some of the threading logic

* Logging and timing consistency

* Initial UI scaffolding

* Fix pathing issue for the icon when using config file

* Add missing definitions

* Update with basic interface

* Cleanup redundant calls

* Update name per convention

* Simplify declaration

* Proper binding to secondary Time property

* Cleanup the terminology use

* Standardize naming conventions.

* More Awake cleanup

* Ability to update the UI when the tray icon updates

* Small tweaks before ViewModel refactor

* Refactor the view model logic

* Some consistency fixes

* Remove the build props change

* Add settings scaffolding when a file does not exist

* Update expect.txt

* Fix typos

* Update build in logs

* Updating based on discussion in microsoft#24183.
This specifically addresses the fact that the `ExpirationDateTime` property was incorrectly auto-initialized to `DateTime.MinValue` when it should've been set to `DateTimeOffset.MinValue` to be consistent with the underlying type and assumptions around date/time.

---------

Co-authored-by: Clint Rutkas <clint@rutkas.com>
  • Loading branch information
2 people authored and BLM16 committed Jun 22, 2023
1 parent e62cad6 commit b14a1f3
Show file tree
Hide file tree
Showing 20 changed files with 653 additions and 312 deletions.
1 change: 1 addition & 0 deletions .github/actions/spell-check/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ clientside
CLIPCHILDREN
Clipperton
CLIPSIBLINGS
Cloneable
clrcall
Cls
CLSCTX
Expand Down
181 changes: 104 additions & 77 deletions src/modules/awake/Awake/Core/APIHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -82,31 +84,54 @@ private static bool SetAwakeState(EXECUTION_STATE state)
}
}

public static void SetIndefiniteKeepAwake(Action<bool> callback, Action failureCallback, bool keepDisplayOn = false)
private static bool SetAwakeStateBasedOnDisplaySetting(bool keepDisplayOn)
{
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeIndefinitelyKeepAwakeEvent());
if (keepDisplayOn)
{
return SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
}
else
{
return SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
}
}

public static void CancelExistingThread()
{
_tokenSource.Cancel();

try
{
_log.Info("Attempting to ensure that the thread is properly cleaned up...");

if (_runnerThread != null && !_runnerThread.IsCanceled)
{
_runnerThread.Wait(_threadToken);
}

_log.Info("Thread is clean.");
}
catch (OperationCanceledException)
{
_log.Info("Confirmed background thread cancellation when setting indefinite keep awake.");
_log.Info("Confirmed background thread cancellation when disabling explicit keep awake.");
}

_tokenSource = new CancellationTokenSource();
_threadToken = _tokenSource.Token;

_log.Info("Instantiating of new token source and thread token completed.");
}

public static void SetIndefiniteKeepAwake(Action callback, Action failureCallback, bool keepDisplayOn = false)
{
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeIndefinitelyKeepAwakeEvent());

CancelExistingThread();

try
{
_runnerThread = Task.Run(() => RunIndefiniteLoop(keepDisplayOn), _threadToken)
.ContinueWith((result) => callback(result.Result), TaskContinuationOptions.OnlyOnRanToCompletion)
_runnerThread = Task.Run(() => RunIndefiniteJob(keepDisplayOn), _threadToken)
.ContinueWith((result) => callback, TaskContinuationOptions.OnlyOnRanToCompletion)
.ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion);
}
catch (Exception ex)
Expand All @@ -117,80 +142,101 @@ public static void SetIndefiniteKeepAwake(Action<bool> callback, Action failureC

public static void SetNoKeepAwake()
{
_tokenSource.Cancel();
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeNoKeepAwakeEvent());

try
CancelExistingThread();
}

public static void SetExpirableKeepAwake(DateTimeOffset expireAt, Action callback, Action failureCallback, bool keepDisplayOn = true)
{
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeExpirableKeepAwakeEvent());

CancelExistingThread();

if (expireAt > DateTime.Now && expireAt != null)
{
if (_runnerThread != null && !_runnerThread.IsCanceled)
{
_runnerThread.Wait(_threadToken);
}
_runnerThread = Task.Run(() => RunExpiringJob(expireAt, keepDisplayOn), _threadToken)
.ContinueWith((result) => callback, TaskContinuationOptions.OnlyOnRanToCompletion)
.ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion);
}
catch (OperationCanceledException)
else
{
_log.Info("Confirmed background thread cancellation when disabling explicit keep awake.");
// The target date is not in the future.
_log.Error("The specified target date and time is not in the future.");
_log.Error($"Current time: {DateTime.Now}\tTarget time: {expireAt}");
}
}

public static void SetTimedKeepAwake(uint seconds, Action<bool> callback, Action failureCallback, bool keepDisplayOn = true)
public static void SetTimedKeepAwake(uint seconds, Action callback, Action failureCallback, bool keepDisplayOn = true)
{
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeTimedKeepAwakeEvent());

_tokenSource.Cancel();
CancelExistingThread();

_runnerThread = Task.Run(() => RunTimedJob(seconds, keepDisplayOn), _threadToken)
.ContinueWith((result) => callback, TaskContinuationOptions.OnlyOnRanToCompletion)
.ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion);
}

private static void RunExpiringJob(DateTimeOffset expireAt, bool keepDisplayOn = false)
{
bool success = false;

// In case cancellation was already requested.
_threadToken.ThrowIfCancellationRequested();

try
{
if (_runnerThread != null && !_runnerThread.IsCanceled)
success = SetAwakeStateBasedOnDisplaySetting(keepDisplayOn);

if (success)
{
_runnerThread.Wait(_threadToken);
_log.Info($"Initiated expirable keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}");

Observable.Timer(expireAt, Scheduler.CurrentThread).Subscribe(
_ =>
{
_log.Info($"Completed expirable thread in {PInvoke.GetCurrentThreadId()}.");
CancelExistingThread();
},
_tokenSource.Token);
}
else
{
_log.Info("Could not successfully set up expirable keep awake.");
}
}
catch (OperationCanceledException)
catch (OperationCanceledException ex)
{
_log.Info("Confirmed background thread cancellation when setting timed keep awake.");
// Task was clearly cancelled.
_log.Info($"Background thread termination: {PInvoke.GetCurrentThreadId()}. Message: {ex.Message}");
}

_tokenSource = new CancellationTokenSource();
_threadToken = _tokenSource.Token;

_runnerThread = Task.Run(() => RunTimedLoop(seconds, keepDisplayOn), _threadToken)
.ContinueWith((result) => callback(result.Result), TaskContinuationOptions.OnlyOnRanToCompletion)
.ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion);
}

private static bool RunIndefiniteLoop(bool keepDisplayOn = false)
private static void RunIndefiniteJob(bool keepDisplayOn = false)
{
bool success;
if (keepDisplayOn)
{
success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
}
else
{
success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
}
// In case cancellation was already requested.
_threadToken.ThrowIfCancellationRequested();

try
{
bool success = SetAwakeStateBasedOnDisplaySetting(keepDisplayOn);

if (success)
{
_log.Info($"Initiated indefinite keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}");

WaitHandle.WaitAny(new[] { _threadToken.WaitHandle });

return success;
}
else
{
_log.Info("Could not successfully set up indefinite keep awake.");
return success;
}
}
catch (OperationCanceledException ex)
{
// Task was clearly cancelled.
_log.Info($"Background thread termination: {PInvoke.GetCurrentThreadId()}. Message: {ex.Message}");
return success;
}
}

Expand Down Expand Up @@ -221,59 +267,38 @@ internal static void CompleteExit(int exitCode, ManualResetEvent? exitSignal, bo
}
}

private static bool RunTimedLoop(uint seconds, bool keepDisplayOn = true)
private static void RunTimedJob(uint seconds, bool keepDisplayOn = true)
{
bool success = false;

// In case cancellation was already requested.
_threadToken.ThrowIfCancellationRequested();

try
{
if (keepDisplayOn)
{
success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
}
else
{
success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
}
success = SetAwakeStateBasedOnDisplaySetting(keepDisplayOn);

if (success)
{
_log.Info($"Initiated temporary keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}");

_timedLoopTimer = new System.Timers.Timer((seconds * 1000) + 1);
_timedLoopTimer.Elapsed += (s, e) =>
{
_tokenSource.Cancel();
_timedLoopTimer.Stop();
};

_timedLoopTimer.Disposed += (s, e) =>
{
_log.Info("Old timer disposed.");
};

_timedLoopTimer.Start();

WaitHandle.WaitAny(new[] { _threadToken.WaitHandle });
_timedLoopTimer.Stop();
_timedLoopTimer.Dispose();

return success;
_log.Info($"Initiated timed keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}");

Observable.Timer(TimeSpan.FromSeconds(seconds), Scheduler.CurrentThread).Subscribe(
_ =>
{
_log.Info($"Completed timed thread in {PInvoke.GetCurrentThreadId()}.");
CancelExistingThread();
},
_tokenSource.Token);
}
else
{
_log.Info("Could not set up timed keep-awake with display on.");
return success;
}
}
catch (OperationCanceledException ex)
{
// Task was clearly cancelled.
_log.Info($"Background thread termination: {PInvoke.GetCurrentThreadId()}. Message: {ex.Message}");
return success;
}
}

Expand Down Expand Up @@ -357,10 +382,12 @@ internal static HWND GetHiddenWindow()

public static Dictionary<string, int> GetDefaultTrayOptions()
{
Dictionary<string, int> optionsList = new Dictionary<string, int>();
optionsList.Add("30 minutes", 1800);
optionsList.Add("1 hour", 3600);
optionsList.Add("2 hours", 7200);
Dictionary<string, int> optionsList = new Dictionary<string, int>
{
{ "30 minutes", 1800 },
{ "1 hour", 3600 },
{ "2 hours", 7200 },
};
return optionsList;
}
}
Expand Down
12 changes: 0 additions & 12 deletions src/modules/awake/Awake/Core/Models/BatteryReportingScale.cs

This file was deleted.

16 changes: 0 additions & 16 deletions src/modules/awake/Awake/Core/Models/ControlType.cs

This file was deleted.

5 changes: 3 additions & 2 deletions src/modules/awake/Awake/Core/Models/TrayCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ internal enum TrayCommands : uint
TC_DISPLAY_SETTING = PInvoke.WM_USER + 1,
TC_MODE_PASSIVE = PInvoke.WM_USER + 2,
TC_MODE_INDEFINITE = PInvoke.WM_USER + 3,
TC_EXIT = PInvoke.WM_USER + 4,
TC_TIME = PInvoke.WM_USER + 5,
TC_MODE_EXPIRABLE = PInvoke.WM_USER + 4,
TC_EXIT = PInvoke.WM_USER + 100,
TC_TIME = PInvoke.WM_USER + 101,
}
}
20 changes: 13 additions & 7 deletions src/modules/awake/Awake/Core/TrayHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@

namespace Awake.Core
{
/// <summary>
/// Helper class used to manage the system tray.
/// </summary>
/// <remarks>
/// Because Awake is a console application, there is no built-in
/// way to embed UI components so we have to heavily rely on the native Windows API.
/// </remarks>
internal static class TrayHelper
{
private static readonly Logger _log;
Expand Down Expand Up @@ -89,7 +96,7 @@ internal static void SetTray(string text, AwakeSettings settings, bool startedFr
text,
settings.Properties.KeepDisplayOn,
settings.Properties.Mode,
settings.Properties.TrayTimeShortcuts,
settings.Properties.CustomTrayTimes,
startedFromPowerToys);
}

Expand All @@ -116,19 +123,18 @@ public static void SetTray(string text, bool keepDisplayOn, AwakeMode mode, Dict
trayTimeShortcuts.AddRange(APIHelper.GetDefaultTrayOptions());
}

// TODO: Make sure that this loads from JSON instead of being hard-coded.
var awakeTimeMenu = new DestroyMenuSafeHandle(PInvoke.CreatePopupMenu(), false);
for (int i = 0; i < trayTimeShortcuts.Count; i++)
{
PInvoke.InsertMenu(awakeTimeMenu, (uint)i, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, (uint)TrayCommands.TC_TIME + (uint)i, trayTimeShortcuts.ElementAt(i).Key);
}

var modeMenu = new DestroyMenuSafeHandle(PInvoke.CreatePopupMenu(), false);
PInvoke.InsertMenu(modeMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | (mode == AwakeMode.PASSIVE ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_PASSIVE, "Off (keep using the selected power plan)");
PInvoke.InsertMenu(modeMenu, 1, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | (mode == AwakeMode.INDEFINITE ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_INDEFINITE, "Keep awake indefinitely");
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_SEPARATOR, 0, string.Empty);

PInvoke.InsertMenu(modeMenu, 2, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_POPUP | (mode == AwakeMode.TIMED ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)awakeTimeMenu.DangerousGetHandle(), "Keep awake temporarily");
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_POPUP, (uint)modeMenu.DangerousGetHandle(), "Mode");
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | (mode == AwakeMode.PASSIVE ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_PASSIVE, "Off (keep using the selected power plan)");
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | (mode == AwakeMode.INDEFINITE ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_INDEFINITE, "Keep awake indefinitely");
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_POPUP | (mode == AwakeMode.TIMED ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)awakeTimeMenu.DangerousGetHandle(), "Keep awake on interval");
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | MENU_ITEM_FLAGS.MF_DISABLED | (mode == AwakeMode.EXPIRABLE ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_EXPIRABLE, "Keep awake until expiration date and time");

TrayIcon.Text = text;
}
Expand Down
Loading

0 comments on commit b14a1f3

Please sign in to comment.