Awake vNext - NOBLE_SIX_02162023 (#24183)

* 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 #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>
This commit is contained in:
Den
2023-03-15 01:42:47 -07:00
committed by GitHub
parent 13cb52763d
commit 466252745d
20 changed files with 664 additions and 323 deletions

View File

@@ -6,6 +6,8 @@ using System;
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;
@@ -82,31 +84,54 @@ namespace Awake.Core
}
}
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)
@@ -117,80 +142,101 @@ namespace Awake.Core
public static void SetNoKeepAwake()
{
_tokenSource.Cancel();
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeNoKeepAwakeEvent());
try
{
if (_runnerThread != null && !_runnerThread.IsCanceled)
{
_runnerThread.Wait(_threadToken);
}
}
catch (OperationCanceledException)
{
_log.Info("Confirmed background thread cancellation when disabling explicit keep awake.");
}
CancelExistingThread();
}
public static void SetTimedKeepAwake(uint seconds, Action<bool> callback, Action failureCallback, bool keepDisplayOn = true)
public static void SetExpirableKeepAwake(DateTimeOffset expireAt, Action callback, Action failureCallback, bool keepDisplayOn = true)
{
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeTimedKeepAwakeEvent());
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeExpirableKeepAwakeEvent());
_tokenSource.Cancel();
CancelExistingThread();
try
if (expireAt > DateTime.Now && expireAt != null)
{
if (_runnerThread != null && !_runnerThread.IsCanceled)
{
_runnerThread.Wait(_threadToken);
}
}
catch (OperationCanceledException)
{
_log.Info("Confirmed background thread cancellation when setting timed keep awake.");
}
_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)
{
bool success;
if (keepDisplayOn)
{
success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
_runnerThread = Task.Run(() => RunExpiringJob(expireAt, keepDisplayOn), _threadToken)
.ContinueWith((result) => callback, TaskContinuationOptions.OnlyOnRanToCompletion)
.ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion);
}
else
{
success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
// 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 callback, Action failureCallback, bool keepDisplayOn = true)
{
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeTimedKeepAwakeEvent());
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
{
success = SetAwakeStateBasedOnDisplaySetting(keepDisplayOn);
if (success)
{
_log.Info($"Initiated indefinite keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}");
_log.Info($"Initiated expirable keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}");
WaitHandle.WaitAny(new[] { _threadToken.WaitHandle });
return success;
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 indefinite keep awake.");
return success;
_log.Info("Could not successfully set up expirable keep awake.");
}
}
catch (OperationCanceledException ex)
{
// Task was clearly cancelled.
_log.Info($"Background thread termination: {PInvoke.GetCurrentThreadId()}. Message: {ex.Message}");
}
}
private static void RunIndefiniteJob(bool keepDisplayOn = false)
{
// 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 });
}
else
{
_log.Info("Could not successfully set up indefinite keep awake.");
}
}
catch (OperationCanceledException ex)
{
// Task was clearly cancelled.
_log.Info($"Background thread termination: {PInvoke.GetCurrentThreadId()}. Message: {ex.Message}");
return success;
}
}
@@ -221,59 +267,38 @@ namespace Awake.Core
}
}
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}");
_log.Info($"Initiated timed 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;
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;
}
}
@@ -357,10 +382,12 @@ namespace Awake.Core
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;
}
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Awake.Core.Models
{
public struct BatteryReportingScale
{
public uint Granularity;
public uint Capacity;
}
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Awake.Core.Models
{
// See: https://learn.microsoft.com/windows/console/handlerroutine
public enum ControlType
{
CTRL_C_EVENT = 0,
CTRL_BREAK_EVENT = 1,
CTRL_CLOSE_EVENT = 2,
CTRL_LOGOFF_EVENT = 5,
CTRL_SHUTDOWN_EVENT = 6,
}
}

View File

@@ -11,7 +11,8 @@ namespace Awake.Core.Models
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,
}
}

View File

@@ -20,6 +20,13 @@ using Windows.Win32.UI.WindowsAndMessaging;
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;
@@ -89,7 +96,7 @@ namespace Awake.Core
text,
settings.Properties.KeepDisplayOn,
settings.Properties.Mode,
settings.Properties.TrayTimeShortcuts,
settings.Properties.CustomTrayTimes,
startedFromPowerToys);
}
@@ -116,19 +123,18 @@ namespace Awake.Core
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;
}

View File

@@ -56,13 +56,13 @@ namespace Awake.Core
// Format for the timer block:
// TrayCommands.TC_TIME + ZERO_BASED_INDEX_IN_SETTINGS
AwakeSettings settings = ModuleSettings.GetSettings<AwakeSettings>(InternalConstants.AppName);
if (settings.Properties.TrayTimeShortcuts.Count == 0)
if (settings.Properties.CustomTrayTimes.Count == 0)
{
settings.Properties.TrayTimeShortcuts.AddRange(APIHelper.GetDefaultTrayOptions());
settings.Properties.CustomTrayTimes.AddRange(APIHelper.GetDefaultTrayOptions());
}
int index = (int)targetCommandIndex - (int)TrayCommands.TC_TIME;
var targetTime = settings.Properties.TrayTimeShortcuts.ElementAt(index).Value;
var targetTime = settings.Properties.CustomTrayTimes.ElementAt(index).Value;
TimedKeepAwakeCommandHandler(InternalConstants.AppName, targetTime);
break;
}
@@ -112,8 +112,8 @@ namespace Awake.Core
}
currentSettings.Properties.Mode = AwakeMode.TIMED;
currentSettings.Properties.Hours = (uint)timeSpan.Hours;
currentSettings.Properties.Minutes = (uint)timeSpan.Minutes;
currentSettings.Properties.IntervalHours = (uint)timeSpan.Hours;
currentSettings.Properties.IntervalMinutes = (uint)timeSpan.Minutes;
ModuleSettings.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName);
}

View File

@@ -2,7 +2,7 @@
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<variable name="buildId" value="LIBRARIAN_03202022" />
<variable name="buildId" value="NOBLE_SIX_02162023" />
<targets async="true">
<target name="logfile"

View File

@@ -38,7 +38,7 @@ namespace Awake
// Format of the build ID is: CODENAME_MMDDYYYY, where MMDDYYYY
// is representative of the date when the last change was made before
// the pull request is issued.
private static readonly string BuildId = "ARBITER_01312022";
private static readonly string BuildId = "NOBLE_SIX_02162023";
private static Mutex? _mutex;
private static FileSystemWatcher? _watcher;
@@ -65,7 +65,7 @@ namespace Awake
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredAwakeEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{
Exit("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.", 1, _exitSignal, true);
Exit("PowerToys.Awake tried to start with a group policy setting that disables the tool. Please contact your system administrator.", 1, _exitSignal, true);
return 0;
}
@@ -97,7 +97,7 @@ namespace Awake
_log.Info("Parsing parameters...");
var configOption = new Option<bool>(
Option<bool> configOption = new(
aliases: new[] { "--use-pt-config", "-c" },
getDefaultValue: () => false,
description: $"Specifies whether {InternalConstants.AppName} will be using the PowerToys configuration file for managing the state.")
@@ -106,11 +106,10 @@ namespace Awake
{
Arity = ArgumentArity.ZeroOrOne,
},
Required = false,
};
configOption.Required = false;
var displayOption = new Option<bool>(
Option<bool> displayOption = new(
aliases: new[] { "--display-on", "-d" },
getDefaultValue: () => true,
description: "Determines whether the display should be kept awake.")
@@ -119,11 +118,10 @@ namespace Awake
{
Arity = ArgumentArity.ZeroOrOne,
},
Required = false,
};
displayOption.Required = false;
var timeOption = new Option<uint>(
Option<uint> timeOption = new(
aliases: new[] { "--time-limit", "-t" },
getDefaultValue: () => 0,
description: "Determines the interval, in seconds, during which the computer is kept awake.")
@@ -132,34 +130,45 @@ namespace Awake
{
Arity = ArgumentArity.ExactlyOne,
},
Required = false,
};
timeOption.Required = false;
var pidOption = new Option<int>(
Option<int> pidOption = new(
aliases: new[] { "--pid", "-p" },
getDefaultValue: () => 0,
description: $"Bind the execution of {InternalConstants.AppName} to another process.")
description: $"Bind the execution of {InternalConstants.AppName} to another process. When the process ends, the system will resume managing the current sleep/display mode.")
{
Argument = new Argument<int>(() => 0)
{
Arity = ArgumentArity.ZeroOrOne,
},
Required = false,
};
pidOption.Required = false;
Option<string> expireAtOption = new(
aliases: new[] { "--expire-at", "-e" },
getDefaultValue: () => string.Empty,
description: $"Determines the end date/time when {InternalConstants.AppName} will back off and let the system manage the current sleep/display mode.")
{
Argument = new Argument<string>(() => string.Empty)
{
Arity = ArgumentArity.ZeroOrOne,
},
Required = false,
};
RootCommand? rootCommand = new RootCommand
RootCommand? rootCommand = new()
{
configOption,
displayOption,
timeOption,
pidOption,
expireAtOption,
};
rootCommand.Description = InternalConstants.AppName;
rootCommand.Handler = CommandHandler.Create<bool, bool, uint, int>(HandleCommandLineArguments);
rootCommand.Handler = CommandHandler.Create<bool, bool, uint, int, string>(HandleCommandLineArguments);
_log.Info("Parameter setup complete. Proceeding to the rest of the app initiation...");
@@ -180,7 +189,7 @@ namespace Awake
APIHelper.CompleteExit(exitCode, exitSignal, force);
}
private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid)
private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid, string expireAt)
{
_handler += ExitHandler;
APIHelper.SetConsoleControlHandler(_handler, true);
@@ -199,6 +208,7 @@ namespace Awake
_log.Info($"The value for --display-on is: {displayOn}");
_log.Info($"The value for --time-limit is: {timeLimit}");
_log.Info($"The value for --pid is: {pid}");
_log.Info($"The value for --expire is: {expireAt}");
if (usePtConfig)
{
@@ -214,41 +224,21 @@ namespace Awake
Exit("Received a signal to end the process. Making sure we quit...", 0, _exitSignal, true);
}
}).Start();
TrayHelper.InitializeTray(InternalConstants.FullAppName, new Icon("modules/awake/images/awake.ico"), _exitSignal);
TrayHelper.InitializeTray(InternalConstants.FullAppName, new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "images/awake.ico")), _exitSignal);
string? settingsPath = _settingsUtils.GetSettingsFilePath(InternalConstants.AppName);
_log.Info($"Reading configuration file: {settingsPath}");
_watcher = new FileSystemWatcher
if (!File.Exists(settingsPath))
{
#pragma warning disable CS8601 // Possible null reference assignment.
Path = Path.GetDirectoryName(settingsPath),
#pragma warning restore CS8601 // Possible null reference assignment.
EnableRaisingEvents = true,
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime,
Filter = Path.GetFileName(settingsPath),
};
string? errorString = $"The settings file does not exist. Scaffolding default configuration...";
IObservable<System.Reactive.EventPattern<FileSystemEventArgs>>? changedObservable = Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
h => _watcher.Changed += h,
h => _watcher.Changed -= h);
AwakeSettings scaffoldSettings = new AwakeSettings();
_settingsUtils.SaveSettings(JsonSerializer.Serialize(scaffoldSettings), InternalConstants.AppName);
}
IObservable<System.Reactive.EventPattern<FileSystemEventArgs>>? createdObservable = Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
cre => _watcher.Created += cre,
cre => _watcher.Created -= cre);
IObservable<System.Reactive.EventPattern<FileSystemEventArgs>>? mergedObservable = Observable.Merge(changedObservable, createdObservable);
mergedObservable.Throttle(TimeSpan.FromMilliseconds(25))
.SubscribeOn(TaskPoolScheduler.Default)
.Select(e => e.EventArgs)
.Subscribe(HandleAwakeConfigChange);
TrayHelper.SetTray(InternalConstants.FullAppName, new AwakeSettings(), _startedFromPowerToys);
// Initially the file might not be updated, so we need to start processing
// settings right away.
ProcessSettings();
ScaffoldConfiguration(settingsPath);
}
catch (Exception ex)
{
@@ -259,15 +249,43 @@ namespace Awake
}
else
{
AwakeMode mode = timeLimit <= 0 ? AwakeMode.INDEFINITE : AwakeMode.TIMED;
if (mode == AwakeMode.INDEFINITE)
// Date-based binding takes precedence over timed configuration, so we want to
// check for that first.
if (!string.IsNullOrWhiteSpace(expireAt))
{
SetupIndefiniteKeepAwake(displayOn);
try
{
DateTime expirationDateTime = DateTime.Parse(expireAt);
if (expirationDateTime > DateTime.Now)
{
// We want to have a dedicated expirable keep-awake logic instead of
// converting the target date to seconds and then passing to SetupTimedKeepAwake
// because that way we're accounting for the user potentially changing their clock
// while Awake is running.
SetupExpirableKeepAwake(expirationDateTime, displayOn);
}
else
{
_log.Info($"Target date is not in the future, therefore there is nothing to wait for.");
}
}
catch
{
_log.Error($"Could not parse date string {expireAt} into a viable date.");
}
}
else
{
SetupTimedKeepAwake(timeLimit, displayOn);
AwakeMode mode = timeLimit <= 0 ? AwakeMode.INDEFINITE : AwakeMode.TIMED;
if (mode == AwakeMode.INDEFINITE)
{
SetupIndefiniteKeepAwake(displayOn);
}
else
{
SetupTimedKeepAwake(timeLimit, displayOn);
}
}
}
@@ -283,6 +301,45 @@ namespace Awake
_exitSignal.WaitOne();
}
private static void ScaffoldConfiguration(string settingsPath)
{
try
{
_watcher = new FileSystemWatcher
{
Path = Path.GetDirectoryName(settingsPath)!,
EnableRaisingEvents = true,
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime,
Filter = Path.GetFileName(settingsPath),
};
IObservable<System.Reactive.EventPattern<FileSystemEventArgs>>? changedObservable = Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
h => _watcher.Changed += h,
h => _watcher.Changed -= h);
IObservable<System.Reactive.EventPattern<FileSystemEventArgs>>? createdObservable = Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
cre => _watcher.Created += cre,
cre => _watcher.Created -= cre);
IObservable<System.Reactive.EventPattern<FileSystemEventArgs>>? mergedObservable = Observable.Merge(changedObservable, createdObservable);
mergedObservable.Throttle(TimeSpan.FromMilliseconds(25))
.SubscribeOn(TaskPoolScheduler.Default)
.Select(e => e.EventArgs)
.Subscribe(HandleAwakeConfigChange);
TrayHelper.SetTray(InternalConstants.FullAppName, new AwakeSettings(), _startedFromPowerToys);
// Initially the file might not be updated, so we need to start processing
// settings right away.
ProcessSettings();
}
catch (Exception ex)
{
_log.Error($"An error occurred scaffolding the configuration. Error details: {ex.Message}");
}
}
private static void SetupIndefiniteKeepAwake(bool displayOn)
{
APIHelper.SetIndefiniteKeepAwake(LogCompletedKeepAwakeThread, LogUnexpectedOrCancelledKeepAwakeThreadCompletion, displayOn);
@@ -303,7 +360,7 @@ namespace Awake
if (settings != null)
{
_log.Info($"Identified custom time shortcuts for the tray: {settings.Properties.TrayTimeShortcuts.Count}");
_log.Info($"Identified custom time shortcuts for the tray: {settings.Properties.CustomTrayTimes.Count}");
switch (settings.Properties.Mode)
{
@@ -321,12 +378,19 @@ namespace Awake
case AwakeMode.TIMED:
{
uint computedTime = (settings.Properties.Hours * 60 * 60) + (settings.Properties.Minutes * 60);
uint computedTime = (settings.Properties.IntervalHours * 60 * 60) + (settings.Properties.IntervalMinutes * 60);
SetupTimedKeepAwake(computedTime, settings.Properties.KeepDisplayOn);
break;
}
case AwakeMode.EXPIRABLE:
{
SetupExpirableKeepAwake(settings.Properties.ExpirationDateTime, settings.Properties.KeepDisplayOn);
break;
}
default:
{
string? errorMessage = "Unknown mode of operation. Check config file.";
@@ -360,6 +424,13 @@ namespace Awake
APIHelper.SetNoKeepAwake();
}
private static void SetupExpirableKeepAwake(DateTimeOffset expireAt, bool displayOn)
{
_log.Info($"Expirable keep-awake. Expected expiration date/time: {expireAt} with display on setting set to {displayOn}.");
APIHelper.SetExpirableKeepAwake(expireAt, LogCompletedKeepAwakeThread, LogUnexpectedOrCancelledKeepAwakeThreadCompletion, displayOn);
}
private static void SetupTimedKeepAwake(uint time, bool displayOn)
{
_log.Info($"Timed keep-awake. Expected runtime: {time} seconds with display on setting set to {displayOn}.");
@@ -369,14 +440,14 @@ namespace Awake
private static void LogUnexpectedOrCancelledKeepAwakeThreadCompletion()
{
string? errorMessage = "The keep-awake thread was terminated early.";
string? errorMessage = "The keep awake thread was terminated early.";
_log.Info(errorMessage);
_log.Debug(errorMessage);
}
private static void LogCompletedKeepAwakeThread(bool result)
private static void LogCompletedKeepAwakeThread()
{
_log.Info($"Exited keep-awake thread successfully: {result}");
_log.Info($"Exited keep awake thread successfully.");
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Awake.Telemetry
{
[EventData]
public class AwakeExpirableKeepAwakeEvent : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Awake.Telemetry
{
[EventData]
internal class AwakeNoKeepAwakeEvent : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}