[Awake]QOL changes - New build ATRIOX_04132023 (#25486)

* Update control to make interaction responsive

* Rip out NLog in favor of standard logging

* Continuing to cleanup NLog stuff

* Simplifying the code more

* Instantly let go of power settings once cancellation requested.

* Cleanup and using built-in native constructs

* Update the API

* Moving towards using a queue instead of tasks

* Code cleanup

* Thread should be flagged as background

* Clean up constants, add docs

* Code cleanup

* Cleanup

* Cleanup

* Remove unnecessary using

* Fix package definition

* Fix NuGet packages

* Update expect.txt

* Remove NLog reference and add a build update in the planning doc

* Cleanup based on report

* More cleanup

* Adding back because the word is clearly somewhere, just not anywhere
I am able to find.

* Revert .net dependency upgrades
This commit is contained in:
Den
2023-05-14 11:42:38 -07:00
committed by GitHub
parent a3227da634
commit 0c5113e908
23 changed files with 740 additions and 671 deletions

View File

@@ -80,6 +80,7 @@ APeriod
apidl apidl
APIENTRY APIENTRY
APIIs APIIs
Apm
APPBARDATA APPBARDATA
appdata appdata
APPEXECLINK APPEXECLINK
@@ -89,7 +90,6 @@ Applets
Applicationcan Applicationcan
applicationconfiguration applicationconfiguration
applicationframehost applicationframehost
applog
appmanifest appmanifest
APPNAME APPNAME
appref appref
@@ -124,6 +124,7 @@ atlcom
atleast atleast
atlfile atlfile
atlstr atlstr
ATRIOX
Attribs Attribs
aumid aumid
Aut Aut
@@ -136,7 +137,7 @@ Autorun
AUTOUPDATE AUTOUPDATE
AValid AValid
awakeness awakeness
awakeversion AWAYMODE
AYUV AYUV
azcli azcli
azman azman
@@ -819,6 +820,8 @@ hhk
HHmmss HHmmss
HHOOK HHOOK
hhx hhx
Hiber
Hiberboot
HIBYTE HIBYTE
hicon hicon
HIDEWINDOW HIDEWINDOW
@@ -1098,8 +1101,6 @@ LOCALSYSTEM
LOCATIONCHANGE LOCATIONCHANGE
LOCKBYTES LOCKBYTES
LOCKTYPE LOCKTYPE
logconsole
logfile
LOGFONT LOGFONT
LOGFONTW LOGFONTW
logon logon
@@ -1134,6 +1135,7 @@ lpsz
lpt lpt
LPTHREAD LPTHREAD
LPTOP LPTOP
lptpm
LPTSTR LPTSTR
LPVOID LPVOID
LPW LPW
@@ -1193,6 +1195,7 @@ mediatype
mef mef
Mega Mega
Melman Melman
MENUBREAK
MENUITEMINFO MENUITEMINFO
MENUITEMINFOW MENUITEMINFOW
MERGECOPY MERGECOPY
@@ -1222,7 +1225,6 @@ MINIMIZEBOX
MINIMIZEEND MINIMIZEEND
MINIMIZESTART MINIMIZESTART
miniz miniz
minlevel
MINORVERSION MINORVERSION
Miracast Miracast
mjpg mjpg
@@ -1334,8 +1336,9 @@ Newtonsoft
niels niels
nielslaute nielslaute
NIF NIF
nint
NLD NLD
nlog NLog
nls nls
NLSTEXT NLSTEXT
NOACTIVATE NOACTIVATE
@@ -1394,6 +1397,7 @@ ntdll
ntfs ntfs
NTSTATUS NTSTATUS
nugets nugets
nuint
nullonfailure nullonfailure
numberbox numberbox
NUMLOCK NUMLOCK
@@ -1450,6 +1454,7 @@ OVERLAPPEDWINDOW
overlaywindow overlaywindow
Oversampling Oversampling
OWNDC OWNDC
OWNERDRAW
Packagemanager Packagemanager
PACL PACL
PAINTSTRUCT PAINTSTRUCT
@@ -1508,7 +1513,6 @@ pgsql
pguid pguid
pkey pkey
PHANDLE PHANDLE
PHANDLER
phbm phbm
phbmp phbmp
phwnd phwnd
@@ -1908,7 +1912,6 @@ spdisp
spdlog spdlog
spdo spdo
spec'ing spec'ing
specialfolder
spesi spesi
splitwstring splitwstring
spsi spsi

View File

@@ -25,7 +25,7 @@
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" /> <PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.1722.45" /> <PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.1722.45" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" /> <PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.0.1" /> <PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.0.2" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.755" /> <PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.755" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.3.230502000" /> <PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.3.230502000" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" /> <PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
@@ -40,7 +40,7 @@
<PackageVersion Include="NLog.Schema" Version="5.0.4" /> <PackageVersion Include="NLog.Schema" Version="5.0.4" />
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" /> <PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.435" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.435" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta1.20071.2" /> <PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.ComponentModel.Composition" Version="7.0.0" /> <PackageVersion Include="System.ComponentModel.Composition" Version="7.0.0" />
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.0" /> <PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
<PackageVersion Include="System.Data.OleDb" Version="7.0.0" /> <PackageVersion Include="System.Data.OleDb" Version="7.0.0" />
@@ -48,7 +48,7 @@
<PackageVersion Include="System.IO.Abstractions" Version="17.2.3" /> <PackageVersion Include="System.IO.Abstractions" Version="17.2.3" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.2.3" /> <PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.2.3" />
<PackageVersion Include="System.Management" Version="7.0.0" /> <PackageVersion Include="System.Management" Version="7.0.0" />
<PackageVersion Include="System.Reactive" Version="5.0.0" /> <PackageVersion Include="System.Reactive" Version="6.0.0-preview.9" />
<PackageVersion Include="System.Runtime.Caching" Version="7.0.0" /> <PackageVersion Include="System.Runtime.Caching" Version="7.0.0" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="7.0.0" /> <PackageVersion Include="System.ServiceProcess.ServiceController" Version="7.0.0" />
<PackageVersion Include="UnicodeInformation" Version="2.6.0" /> <PackageVersion Include="UnicodeInformation" Version="2.6.0" />

View File

@@ -300,7 +300,7 @@ SOFTWARE.
- Microsoft.Toolkit.Uwp.Notifications 7.1.2 - Microsoft.Toolkit.Uwp.Notifications 7.1.2
- Microsoft.Web.WebView2 1.0.1722.45 - Microsoft.Web.WebView2 1.0.1722.45
- Microsoft.Windows.CsWin32 0.2.46-beta - Microsoft.Windows.CsWin32 0.2.46-beta
- Microsoft.Windows.CsWinRT 2.0.1 - Microsoft.Windows.CsWinRT 2.0.2
- Microsoft.Windows.SDK.BuildTools 10.0.22621.755 - Microsoft.Windows.SDK.BuildTools 10.0.22621.755
- Microsoft.WindowsAppSDK 1.3.230502000 - Microsoft.WindowsAppSDK 1.3.230502000
- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9 - Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9
@@ -309,12 +309,11 @@ SOFTWARE.
- Moq 4.18.3 - Moq 4.18.3
- MSTest.TestAdapter 3.0.1 - MSTest.TestAdapter 3.0.1
- MSTest.TestFramework 3.0.1 - MSTest.TestFramework 3.0.1
- NLog 5.0.4
- NLog.Extensions.Logging 5.0.4 - NLog.Extensions.Logging 5.0.4
- NLog.Schema 5.0.4 - NLog.Schema 5.0.4
- ScipBe.Common.Office.OneNote 3.0.1 - ScipBe.Common.Office.OneNote 3.0.1
- StyleCop.Analyzers 1.2.0-beta.435 - StyleCop.Analyzers 1.2.0-beta.435
- System.CommandLine 2.0.0-beta1.20071.2 - System.CommandLine 2.0.0-beta4.22272.1
- System.ComponentModel.Composition 7.0.0 - System.ComponentModel.Composition 7.0.0
- System.Configuration.ConfigurationManager 6.0.0 - System.Configuration.ConfigurationManager 6.0.0
- System.Data.OleDb 7.0.0 - System.Data.OleDb 7.0.0
@@ -322,7 +321,7 @@ SOFTWARE.
- System.IO.Abstractions 17.2.3 - System.IO.Abstractions 17.2.3
- System.IO.Abstractions.TestingHelpers 17.2.3 - System.IO.Abstractions.TestingHelpers 17.2.3
- System.Management 7.0.0 - System.Management 7.0.0
- System.Reactive 5.0.0 - System.Reactive 6.0.0-preview.9
- System.Runtime.Caching 7.0.0 - System.Runtime.Caching 7.0.0
- System.ServiceProcess.ServiceController 7.0.0 - System.ServiceProcess.ServiceController 7.0.0
- UnicodeInformation 2.6.0 - UnicodeInformation 2.6.0

View File

@@ -6,13 +6,23 @@ last-update: 3-20-2022
## Builds ## Builds
The build ID can be found in [`NLog.config`](https://github.com/microsoft/PowerToys/blob/2e3a2b3f96f67c7dfc72963e5135662d3230b5fe/src/modules/awake/Awake/NLog.config#L5) - it is a unique identifier for the current builds that allows better diagnostics (we can look up the build ID from the logs) and offers a way to triage Awake-specific issues faster independent of the PowerToys version. The build ID does not carry any significance beyond that within the PowerToys code base. The build ID can be found in `Program.cs` in the `BuildId` variable - it is a unique identifier for the current builds that allows better diagnostics (we can look up the build ID from the logs) and offers a way to triage Awake-specific issues faster independent of the PowerToys version. The build ID does not carry any significance beyond that within the PowerToys code base.
The build ID moniker is made up of two components - a reference to a [Halo](https://en.wikipedia.org/wiki/Halo_(franchise)) character, and the date when the work on the specific build started in the format of `MMDDYYYY`.
| Build ID | Build Date | | Build ID | Build Date |
|:----------------------------------------------------------|:-----------------| |:----------------------------------------------------------|:-----------------|
| [`ATRIOX_04132023`](#ATRIOX_04132023-april-13-2023) | April 13, 2023 |
| [`LIBRARIAN_03202022`](#librarian_03202022-march-20-2022) | March 20, 2022 | | [`LIBRARIAN_03202022`](#librarian_03202022-march-20-2022) | March 20, 2022 |
| `ARBITER_01312022` | January 31, 2022 | | `ARBITER_01312022` | January 31, 2022 |
### `ATRIOX_04132023` (April 13, 2023)
- Moves from using `Task.Run` to spin up threads to actually using a blocking queue that properly sets thread parameters on the same thread.
- Moves back to using native Windows APIs through P/Invoke instead of using a package.
- Move away from custom logging and to built-in logging that is consistent with the rest of PowerToys.
- Updates `System.CommandLine` and `System.Reactive` to the latest preview versions of the package.
### `LIBRARIAN_03202022` (March 20, 2022) ### `LIBRARIAN_03202022` (March 20, 2022)
- Changed the tray context menu to be following OS conventions instead of the style offered by Windows Forms. This introduces better support for DPI scaling and theming in the future. - Changed the tray context menu to be following OS conventions instead of the style offered by Windows Forms. This introduces better support for DPI scaling and theming in the future.

View File

@@ -49,7 +49,7 @@ if ($isWinAppSdkProj -eq $True) {
$fileExclusionList = @("*Test*", "*.pdb", "*.lastcodeanalysissucceeded", "createdump.exe") + $interopFilesList + $winAppSDKfilesList $fileExclusionList = @("*Test*", "*.pdb", "*.lastcodeanalysissucceeded", "createdump.exe") + $interopFilesList + $winAppSDKfilesList
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*png", "*gif", "*ico", "*cur", "*svg", "index.html", "reg.js", "monacoSpecialLanguages.js", "resources.pri", "NLog.config") $fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*png", "*gif", "*ico", "*cur", "*svg", "index.html", "reg.js", "monacoSpecialLanguages.js", "resources.pri")
$dllsToIgnore = @("System.CodeDom.dll", "WindowsBase.dll") $dllsToIgnore = @("System.CodeDom.dll", "WindowsBase.dll")

View File

@@ -5,10 +5,8 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Reflection; using System.Reflection;
using System.Runtime.Serialization;
using interop; using interop;
namespace ManagedCommon namespace ManagedCommon

View File

@@ -18,6 +18,7 @@
<PackageProjectUrl>https://awake.den.dev</PackageProjectUrl> <PackageProjectUrl>https://awake.den.dev</PackageProjectUrl>
<RepositoryUrl>https://github.com/microsoft/powertoys</RepositoryUrl> <RepositoryUrl>https://github.com/microsoft/powertoys</RepositoryUrl>
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
</PropertyGroup> </PropertyGroup>
<!-- SelfContained=true requires RuntimeIdentifier to be set --> <!-- SelfContained=true requires RuntimeIdentifier to be set -->
@@ -51,16 +52,16 @@
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" />
<ItemGroup> <ItemGroup>
<None Remove="Images\Awake.ico" /> <None Remove="Images\Awake.ico" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.CsWinRT" /> <PackageReference Include="Microsoft.Windows.CsWinRT" />
<PackageReference Include="NLog" />
<PackageReference Include="System.CommandLine" /> <PackageReference Include="System.CommandLine" />
<PackageReference Include="System.Reactive" /> <PackageReference Include="System.Reactive" />
<PackageReference Include="System.Runtime.Caching" /> <PackageReference Include="System.Runtime.Caching" />
@@ -79,16 +80,17 @@
</Compile> </Compile>
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Update="NLog.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="Images\Awake.ico"> <Content Include="Images\Awake.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.CodeAnalysis.NetAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project> </Project>

View File

@@ -1,394 +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.
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;
using Microsoft.PowerToys.Telemetry;
using Microsoft.Win32;
using NLog;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Storage.FileSystem;
using Windows.Win32.System.Console;
using Windows.Win32.System.Power;
namespace Awake.Core
{
/// <summary>
/// Helper class that allows talking to Win32 APIs without having to rely on PInvoke in other parts
/// of the codebase.
/// </summary>
public class APIHelper
{
private const string BuildRegistryLocation = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion";
private static readonly Logger _log;
private static CancellationTokenSource _tokenSource;
private static CancellationToken _threadToken;
private static Task? _runnerThread;
private static System.Timers.Timer _timedLoopTimer;
static APIHelper()
{
_timedLoopTimer = new System.Timers.Timer();
_log = LogManager.GetCurrentClassLogger();
_tokenSource = new CancellationTokenSource();
}
internal static void SetConsoleControlHandler(PHANDLER_ROUTINE handler, bool addHandler)
{
PInvoke.SetConsoleCtrlHandler(handler, addHandler);
}
public static void AllocateConsole()
{
_log.Debug("Bootstrapping the console allocation routine.");
PInvoke.AllocConsole();
_log.Debug($"Console allocation result: {Marshal.GetLastWin32Error()}");
var outputFilePointer = PInvoke.CreateFile("CONOUT$", FILE_ACCESS_FLAGS.FILE_GENERIC_READ | FILE_ACCESS_FLAGS.FILE_GENERIC_WRITE, FILE_SHARE_MODE.FILE_SHARE_WRITE, null, FILE_CREATION_DISPOSITION.OPEN_EXISTING, 0, null);
_log.Debug($"CONOUT creation result: {Marshal.GetLastWin32Error()}");
PInvoke.SetStdHandle(Windows.Win32.System.Console.STD_HANDLE.STD_OUTPUT_HANDLE, outputFilePointer);
_log.Debug($"SetStdHandle result: {Marshal.GetLastWin32Error()}");
Console.SetOut(new StreamWriter(Console.OpenStandardOutput(), Console.OutputEncoding) { AutoFlush = true });
}
/// <summary>
/// Sets the computer awake state using the native Win32 SetThreadExecutionState API. This
/// function is just a nice-to-have wrapper that helps avoid tracking the success or failure of
/// the call.
/// </summary>
/// <param name="state">Single or multiple EXECUTION_STATE entries.</param>
/// <returns>true if successful, false if failed</returns>
private static bool SetAwakeState(EXECUTION_STATE state)
{
try
{
var stateResult = PInvoke.SetThreadExecutionState(state);
return stateResult != 0;
}
catch
{
return false;
}
}
private static bool SetAwakeStateBasedOnDisplaySetting(bool keepDisplayOn)
{
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 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(() => RunIndefiniteJob(keepDisplayOn), _threadToken)
.ContinueWith((result) => callback, TaskContinuationOptions.OnlyOnRanToCompletion)
.ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion);
}
catch (Exception ex)
{
_log.Error(ex.Message);
}
}
public static void SetNoKeepAwake()
{
PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeNoKeepAwakeEvent());
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)
{
_runnerThread = Task.Run(() => RunExpiringJob(expireAt, keepDisplayOn), _threadToken)
.ContinueWith((result) => callback, TaskContinuationOptions.OnlyOnRanToCompletion)
.ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion);
}
else
{
// 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 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 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}");
}
}
internal static void CompleteExit(int exitCode, ManualResetEvent? exitSignal, bool force = false)
{
SetNoKeepAwake();
HWND windowHandle = GetHiddenWindow();
if (windowHandle != HWND.Null)
{
PInvoke.SendMessage(windowHandle, PInvoke.WM_CLOSE, 0, 0);
}
if (force)
{
PInvoke.PostQuitMessage(0);
}
try
{
exitSignal?.Set();
PInvoke.DestroyWindow(windowHandle);
}
catch (Exception ex)
{
_log.Info($"Exit signal error ${ex}");
}
}
private static void RunTimedJob(uint seconds, bool keepDisplayOn = true)
{
bool success = false;
// In case cancellation was already requested.
_threadToken.ThrowIfCancellationRequested();
try
{
success = SetAwakeStateBasedOnDisplaySetting(keepDisplayOn);
if (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.");
}
}
catch (OperationCanceledException ex)
{
// Task was clearly cancelled.
_log.Info($"Background thread termination: {PInvoke.GetCurrentThreadId()}. Message: {ex.Message}");
}
}
public static string GetOperatingSystemBuild()
{
try
{
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
RegistryKey registryKey = Registry.LocalMachine.OpenSubKey(BuildRegistryLocation);
#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.
if (registryKey != null)
{
var versionString = $"{registryKey.GetValue("ProductName")} {registryKey.GetValue("DisplayVersion")} {registryKey.GetValue("BuildLabEx")}";
return versionString;
}
else
{
_log.Info("Registry key acquisition for OS failed.");
return string.Empty;
}
}
catch (Exception ex)
{
_log.Info($"Could not get registry key for the build number. Error: {ex.Message}");
return string.Empty;
}
}
[SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "Function returns DWORD value that identifies the current thread, but we do not need it.")]
internal static IEnumerable<HWND> EnumerateWindowsForProcess(int processId)
{
var handles = new List<HWND>();
var hCurrentWnd = HWND.Null;
do
{
hCurrentWnd = PInvoke.FindWindowEx(HWND.Null, hCurrentWnd, null as string, null);
uint targetProcessId = 0;
unsafe
{
PInvoke.GetWindowThreadProcessId(hCurrentWnd, &targetProcessId);
}
if (targetProcessId == processId)
{
handles.Add(hCurrentWnd);
}
}
while (hCurrentWnd != IntPtr.Zero);
return handles;
}
[SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "In this context, the string is only converted to a hex value.")]
internal static HWND GetHiddenWindow()
{
IEnumerable<HWND> windowHandles = EnumerateWindowsForProcess(Environment.ProcessId);
var domain = AppDomain.CurrentDomain.GetHashCode().ToString("x");
string targetClass = $"{InternalConstants.TrayWindowId}{domain}";
unsafe
{
var classNameLen = 256;
Span<char> className = stackalloc char[classNameLen];
foreach (var handle in windowHandles)
{
fixed (char* ptr = className)
{
int classQueryResult = PInvoke.GetClassName(handle, ptr, classNameLen);
if (classQueryResult != 0 && className.ToString().StartsWith(targetClass, StringComparison.InvariantCultureIgnoreCase))
{
return handle;
}
}
}
}
return HWND.Null;
}
public static Dictionary<string, int> GetDefaultTrayOptions()
{
Dictionary<string, int> optionsList = new Dictionary<string, int>
{
{ "30 minutes", 1800 },
{ "1 hour", 3600 },
{ "2 hours", 7200 },
};
return optionsList;
}
}
}

View File

@@ -4,10 +4,11 @@
namespace Awake.Core namespace Awake.Core
{ {
internal static class InternalConstants internal static class Constants
{ {
internal const string AppName = "Awake"; internal const string AppName = "Awake";
internal const string FullAppName = "PowerToys " + AppName; internal const string FullAppName = "PowerToys " + AppName;
internal const string TrayWindowId = "WindowsForms10.Window.0.app.0."; internal const string TrayWindowId = "WindowsForms10.Window.0.app.0.";
internal const string BuildRegistryLocation = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion";
} }
} }

View File

@@ -0,0 +1,284 @@
// 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;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reactive.Linq;
using System.Text;
using System.Threading;
using Awake.Core.Models;
using Awake.Core.Native;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
using Microsoft.Win32;
namespace Awake.Core
{
public delegate bool ConsoleEventHandler(Models.ControlType ctrlType);
/// <summary>
/// Helper class that allows talking to Win32 APIs without having to rely on PInvoke in other parts
/// of the codebase.
/// </summary>
public class Manager
{
private static BlockingCollection<ExecutionState> _stateQueue;
private static CancellationTokenSource _tokenSource;
static Manager()
{
_tokenSource = new CancellationTokenSource();
_stateQueue = new BlockingCollection<ExecutionState>();
}
public static void StartMonitor()
{
Thread monitorThread = new(() =>
{
Thread.CurrentThread.IsBackground = true;
while (true)
{
ExecutionState state = _stateQueue.Take();
Logger.LogInfo($"Setting state to {state}");
SetAwakeState(state);
}
});
monitorThread.Start();
}
internal static void SetConsoleControlHandler(ConsoleEventHandler handler, bool addHandler)
{
Bridge.SetConsoleCtrlHandler(handler, addHandler);
}
public static void AllocateConsole()
{
Bridge.AllocConsole();
var outputFilePointer = Bridge.CreateFile("CONOUT$", Native.Constants.GENERIC_READ | Native.Constants.GENERIC_WRITE, FileShare.Write, IntPtr.Zero, FileMode.OpenOrCreate, 0, IntPtr.Zero);
Bridge.SetStdHandle(Native.Constants.STD_OUTPUT_HANDLE, outputFilePointer);
Console.SetOut(new StreamWriter(Console.OpenStandardOutput(), Console.OutputEncoding) { AutoFlush = true });
}
/// <summary>
/// Sets the computer awake state using the native Win32 SetThreadExecutionState API. This
/// function is just a nice-to-have wrapper that helps avoid tracking the success or failure of
/// the call.
/// </summary>
/// <param name="state">Single or multiple EXECUTION_STATE entries.</param>
/// <returns>true if successful, false if failed</returns>
private static bool SetAwakeState(ExecutionState state)
{
try
{
var stateResult = Bridge.SetThreadExecutionState(state);
return stateResult != 0;
}
catch
{
return false;
}
}
private static ExecutionState ComputeAwakeState(bool keepDisplayOn)
{
if (keepDisplayOn)
{
return ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_DISPLAY_REQUIRED | ExecutionState.ES_CONTINUOUS;
}
else
{
return ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_CONTINUOUS;
}
}
public static void CancelExistingThread()
{
Logger.LogInfo($"Attempting to ensure that the thread is properly cleaned up...");
// Resetting the thread state.
_stateQueue.Add(ExecutionState.ES_CONTINUOUS);
// Next, make sure that any existing background threads are terminated.
_tokenSource.Cancel();
_tokenSource.Dispose();
_tokenSource = new CancellationTokenSource();
Logger.LogInfo("Instantiating of new token source and thread token completed.");
}
public static void SetIndefiniteKeepAwake(bool keepDisplayOn = false)
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AwakeIndefinitelyKeepAwakeEvent());
CancelExistingThread();
_stateQueue.Add(ComputeAwakeState(keepDisplayOn));
}
public static void SetNoKeepAwake()
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AwakeNoKeepAwakeEvent());
CancelExistingThread();
}
public static void SetExpirableKeepAwake(DateTimeOffset expireAt, bool keepDisplayOn = true)
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AwakeExpirableKeepAwakeEvent());
CancelExistingThread();
if (expireAt > DateTime.Now && expireAt != null)
{
Logger.LogInfo($"Starting expirable log for {expireAt}");
_stateQueue.Add(ComputeAwakeState(keepDisplayOn));
Observable.Timer(expireAt).Subscribe(
_ =>
{
Logger.LogInfo($"Completed expirable keep-awake.");
CancelExistingThread();
},
_tokenSource.Token);
}
else
{
// The target date is not in the future.
Logger.LogError("The specified target date and time is not in the future.");
Logger.LogError($"Current time: {DateTime.Now}\tTarget time: {expireAt}");
}
}
public static void SetTimedKeepAwake(uint seconds, bool keepDisplayOn = true)
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AwakeTimedKeepAwakeEvent());
CancelExistingThread();
Logger.LogInfo($"Timed keep awake started for {seconds} seconds.");
_stateQueue.Add(ComputeAwakeState(keepDisplayOn));
Observable.Timer(TimeSpan.FromSeconds(seconds)).Subscribe(
_ =>
{
Logger.LogInfo($"Completed timed thread.");
CancelExistingThread();
},
_tokenSource.Token);
}
internal static void CompleteExit(int exitCode, ManualResetEvent? exitSignal, bool force = false)
{
SetNoKeepAwake();
IntPtr windowHandle = GetHiddenWindow();
if (windowHandle != IntPtr.Zero)
{
Bridge.SendMessage(windowHandle, Native.Constants.WM_CLOSE, 0, 0);
}
if (force)
{
Bridge.PostQuitMessage(exitCode);
}
try
{
exitSignal?.Set();
Bridge.DestroyWindow(windowHandle);
}
catch (Exception ex)
{
Logger.LogError($"Exit signal error ${ex}");
}
}
public static string GetOperatingSystemBuild()
{
try
{
RegistryKey? registryKey = Registry.LocalMachine.OpenSubKey(Constants.BuildRegistryLocation);
if (registryKey != null)
{
var versionString = $"{registryKey.GetValue("ProductName")} {registryKey.GetValue("DisplayVersion")} {registryKey.GetValue("BuildLabEx")}";
return versionString;
}
else
{
Logger.LogError("Registry key acquisition for OS failed.");
return string.Empty;
}
}
catch (Exception ex)
{
Logger.LogError($"Could not get registry key for the build number. Error: {ex.Message}");
return string.Empty;
}
}
[SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "Function returns DWORD value that identifies the current thread, but we do not need it.")]
internal static IEnumerable<IntPtr> EnumerateWindowsForProcess(int processId)
{
var handles = new List<IntPtr>();
var hCurrentWnd = IntPtr.Zero;
do
{
hCurrentWnd = Bridge.FindWindowEx(IntPtr.Zero, hCurrentWnd, null as string, null);
Bridge.GetWindowThreadProcessId(hCurrentWnd, out uint targetProcessId);
if (targetProcessId == processId)
{
handles.Add(hCurrentWnd);
}
}
while (hCurrentWnd != IntPtr.Zero);
return handles;
}
[SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "In this context, the string is only converted to a hex value.")]
internal static IntPtr GetHiddenWindow()
{
IEnumerable<IntPtr> windowHandles = EnumerateWindowsForProcess(Environment.ProcessId);
var domain = AppDomain.CurrentDomain.GetHashCode().ToString("x");
string targetClass = $"{Constants.TrayWindowId}{domain}";
foreach (var handle in windowHandles)
{
StringBuilder className = new(256);
int classQueryResult = Bridge.GetClassName(handle, className, className.Capacity);
if (classQueryResult != 0 && className.ToString().StartsWith(targetClass, StringComparison.InvariantCultureIgnoreCase))
{
return handle;
}
}
return IntPtr.Zero;
}
public static Dictionary<string, int> GetDefaultTrayOptions()
{
Dictionary<string, int> optionsList = new Dictionary<string, int>
{
{ "30 minutes", 1800 },
{ "1 hour", 3600 },
{ "2 hours", 7200 },
};
return optionsList;
}
}
}

View File

@@ -0,0 +1,12 @@
// 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

@@ -0,0 +1,21 @@
// 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
{
/// <summary>
/// The type of control signal received by the handler.
/// </summary>
/// <remarks>
/// See <see href="https://learn.microsoft.com/windows/console/handlerroutine">HandlerRoutine callback function</see>.
/// </remarks>
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

@@ -0,0 +1,17 @@
// 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;
namespace Awake.Core.Models
{
[Flags]
public enum ExecutionState : uint
{
ES_AWAYMODE_REQUIRED = 0x00000040,
ES_CONTINUOUS = 0x80000000,
ES_DISPLAY_REQUIRED = 0x00000002,
ES_SYSTEM_REQUIRED = 0x00000001,
}
}

View File

@@ -0,0 +1,70 @@
// 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.Runtime.InteropServices;
namespace Awake.Core.Models
{
public struct SystemPowerCapabilities
{
[MarshalAs(UnmanagedType.U1)]
public bool PowerButtonPresent;
[MarshalAs(UnmanagedType.U1)]
public bool SleepButtonPresent;
[MarshalAs(UnmanagedType.U1)]
public bool LidPresent;
[MarshalAs(UnmanagedType.U1)]
public bool SystemS1;
[MarshalAs(UnmanagedType.U1)]
public bool SystemS2;
[MarshalAs(UnmanagedType.U1)]
public bool SystemS3;
[MarshalAs(UnmanagedType.U1)]
public bool SystemS4;
[MarshalAs(UnmanagedType.U1)]
public bool SystemS5;
[MarshalAs(UnmanagedType.U1)]
public bool HiberFilePresent;
[MarshalAs(UnmanagedType.U1)]
public bool FullWake;
[MarshalAs(UnmanagedType.U1)]
public bool VideoDimPresent;
[MarshalAs(UnmanagedType.U1)]
public bool ApmPresent;
[MarshalAs(UnmanagedType.U1)]
public bool UpsPresent;
[MarshalAs(UnmanagedType.U1)]
public bool ThermalControl;
[MarshalAs(UnmanagedType.U1)]
public bool ProcessorThrottle;
public byte ProcessorMinThrottle;
public byte ProcessorMaxThrottle;
[MarshalAs(UnmanagedType.U1)]
public bool FastSystemS4;
[MarshalAs(UnmanagedType.U1)]
public bool Hiberboot;
[MarshalAs(UnmanagedType.U1)]
public bool WakeAlarmPresent;
[MarshalAs(UnmanagedType.U1)]
public bool AoAc;
[MarshalAs(UnmanagedType.U1)]
public bool DiskSpinDown;
public byte HiberFileType;
[MarshalAs(UnmanagedType.U1)]
public bool AoAcConnectivitySupported;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
private readonly byte[] spare3;
[MarshalAs(UnmanagedType.U1)]
public bool SystemBatteriesPresent;
[MarshalAs(UnmanagedType.U1)]
public bool BatteriesAreShortTerm;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public BatteryReportingScale[] BatteryScale;
public SystemPowerState AcOnLineWake;
public SystemPowerState SoftLidWake;
public SystemPowerState RtcWake;
public SystemPowerState MinDeviceWakeState;
public SystemPowerState DefaultLowLatencyWake;
}
}

View File

@@ -0,0 +1,24 @@
// 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
{
/// <summary>
/// Represents the system power state.
/// </summary>
/// <remarks>
/// See <see href="https://learn.microsoft.com/windows/win32/power/system-power-states">System power states</see>.
/// </remarks>
public enum SystemPowerState
{
PowerSystemUnspecified = 0,
PowerSystemWorking = 1,
PowerSystemSleeping1 = 2,
PowerSystemSleeping2 = 3,
PowerSystemSleeping3 = 4,
PowerSystemHibernate = 5,
PowerSystemShutdown = 6,
PowerSystemMaximum = 7,
}
}

View File

@@ -2,17 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using Windows.Win32;
namespace Awake.Core.Models namespace Awake.Core.Models
{ {
internal enum TrayCommands : uint internal enum TrayCommands : uint
{ {
TC_DISPLAY_SETTING = PInvoke.WM_USER + 1, TC_DISPLAY_SETTING = Native.Constants.WM_USER + 1,
TC_MODE_PASSIVE = PInvoke.WM_USER + 2, TC_MODE_PASSIVE = Native.Constants.WM_USER + 2,
TC_MODE_INDEFINITE = PInvoke.WM_USER + 3, TC_MODE_INDEFINITE = Native.Constants.WM_USER + 3,
TC_MODE_EXPIRABLE = PInvoke.WM_USER + 4, TC_MODE_EXPIRABLE = Native.Constants.WM_USER + 4,
TC_EXIT = PInvoke.WM_USER + 100, TC_EXIT = Native.Constants.WM_USER + 100,
TC_TIME = PInvoke.WM_USER + 101, TC_TIME = Native.Constants.WM_USER + 101,
} }
} }

View File

@@ -0,0 +1,80 @@
// 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;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Awake.Core.Models;
namespace Awake.Core.Native
{
internal sealed class Bridge
{
internal delegate bool EnumThreadDelegate(IntPtr hWnd, IntPtr lParam);
[DllImport("Powrprof.dll", SetLastError = true)]
internal static extern bool GetPwrCapabilities(out SystemPowerCapabilities lpSystemPowerCapabilities);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool SetConsoleCtrlHandler(ConsoleEventHandler handler, bool add);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
internal static extern ExecutionState SetThreadExecutionState(ExecutionState esFlags);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool SetStdHandle(int nStdHandle, IntPtr hHandle);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern uint GetCurrentThreadId();
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
internal static extern IntPtr CreateFile(
[MarshalAs(UnmanagedType.LPWStr)] string filename,
[MarshalAs(UnmanagedType.U4)] uint access,
[MarshalAs(UnmanagedType.U4)] FileShare share,
IntPtr securityAttributes,
[MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
[MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
IntPtr templateFile);
[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr CreatePopupMenu();
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool InsertMenu(IntPtr hMenu, uint uPosition, uint uFlags, uint uIDNewItem, string lpNewItem);
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool TrackPopupMenuEx(IntPtr hmenu, uint fuFlags, int x, int y, IntPtr hwnd, IntPtr lptpm);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr hWndChildAfter, string? className, string? windowTitle);
[DllImport("user32.dll", SetLastError = true)]
internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern IntPtr SendMessage(IntPtr hWnd, uint msg, nuint wParam, nint lParam);
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool DestroyMenu(IntPtr hMenu);
[DllImport("user32.dll")]
internal static extern bool DestroyWindow(IntPtr hWnd);
[DllImport("user32.dll")]
internal static extern void PostQuitMessage(int nExitCode);
}
}

View File

@@ -0,0 +1,32 @@
// 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.Native
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Win32 API convention.")]
internal sealed class Constants
{
internal const uint WM_COMMAND = 0x111;
internal const uint WM_USER = 0x400;
internal const uint WM_GETTEXT = 0x000D;
internal const uint WM_CLOSE = 0x0010;
// Popup menu constants.
internal const uint MF_BYPOSITION = 1024;
internal const uint MF_STRING = 0;
internal const uint MF_MENUBREAK = 0x00000040;
internal const uint MF_SEPARATOR = 0x00000800;
internal const uint MF_POPUP = 0x00000010;
internal const uint MF_UNCHECKED = 0x00000000;
internal const uint MF_CHECKED = 0x00000008;
internal const uint MF_OWNERDRAW = 0x00000100;
internal const uint MF_ENABLED = 0x00000000;
internal const uint MF_DISABLED = 0x00000002;
internal const int STD_OUTPUT_HANDLE = -11;
internal const uint GENERIC_WRITE = 0x40000000;
internal const uint GENERIC_READ = 0x80000000;
}
}

View File

@@ -10,13 +10,9 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using Awake.Core.Models; using Awake.Core.Models;
using Awake.Core.Native;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using NLog;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
#pragma warning disable CS8602 // Dereference of a possibly null reference.
namespace Awake.Core namespace Awake.Core
{ {
@@ -29,16 +25,14 @@ namespace Awake.Core
/// </remarks> /// </remarks>
internal static class TrayHelper internal static class TrayHelper
{ {
private static readonly Logger _log; private static IntPtr _trayMenu;
private static DestroyMenuSafeHandle TrayMenu { get; set; } private static IntPtr TrayMenu { get => _trayMenu; set => _trayMenu = value; }
private static NotifyIcon TrayIcon { get; set; } private static NotifyIcon TrayIcon { get; set; }
static TrayHelper() static TrayHelper()
{ {
_log = LogManager.GetCurrentClassLogger();
TrayMenu = new DestroyMenuSafeHandle();
TrayIcon = new NotifyIcon(); TrayIcon = new NotifyIcon();
} }
@@ -49,20 +43,23 @@ namespace Awake.Core
{ {
try try
{ {
_log.Info("Setting up the tray."); Logger.LogInfo("Setting up the tray.");
((NotifyIcon?)tray).Text = text; if (tray != null)
((NotifyIcon?)tray).Icon = icon; {
((NotifyIcon?)tray).ContextMenuStrip = contextMenu; ((NotifyIcon)tray).Text = text;
((NotifyIcon?)tray).Visible = true; ((NotifyIcon)tray).Icon = icon;
((NotifyIcon?)tray).MouseClick += TrayClickHandler; ((NotifyIcon)tray).ContextMenuStrip = contextMenu;
Application.AddMessageFilter(new TrayMessageFilter(exitSignal)); ((NotifyIcon)tray).Visible = true;
Application.Run(); ((NotifyIcon)tray).MouseClick += TrayClickHandler;
_log.Info("Tray setup complete."); Application.AddMessageFilter(new TrayMessageFilter(exitSignal));
Application.Run();
Logger.LogInfo("Tray setup complete.");
}
} }
catch (Exception ex) catch (Exception ex)
{ {
_log.Error($"An error occurred initializing the tray. {ex.Message}"); Logger.LogError($"An error occurred initializing the tray. {ex.Message}");
_log.Error($"{ex.StackTrace}"); Logger.LogError($"{ex.StackTrace}");
} }
}, },
TrayIcon); TrayIcon);
@@ -81,12 +78,12 @@ namespace Awake.Core
/// <param name="e">MouseEventArgs instance containing mouse click event information.</param> /// <param name="e">MouseEventArgs instance containing mouse click event information.</param>
private static void TrayClickHandler(object? sender, MouseEventArgs e) private static void TrayClickHandler(object? sender, MouseEventArgs e)
{ {
HWND windowHandle = APIHelper.GetHiddenWindow(); IntPtr windowHandle = Manager.GetHiddenWindow();
if (windowHandle != HWND.Null) if (windowHandle != IntPtr.Zero)
{ {
PInvoke.SetForegroundWindow(windowHandle); Bridge.SetForegroundWindow(windowHandle);
PInvoke.TrackPopupMenuEx(TrayMenu, 0, Cursor.Position.X, Cursor.Position.Y, windowHandle, null); Bridge.TrackPopupMenuEx(TrayMenu, 0, Cursor.Position.X, Cursor.Position.Y, windowHandle, IntPtr.Zero);
} }
} }
@@ -102,46 +99,55 @@ namespace Awake.Core
public static void SetTray(string text, bool keepDisplayOn, AwakeMode mode, Dictionary<string, int> trayTimeShortcuts, bool startedFromPowerToys) public static void SetTray(string text, bool keepDisplayOn, AwakeMode mode, Dictionary<string, int> trayTimeShortcuts, bool startedFromPowerToys)
{ {
TrayMenu = new DestroyMenuSafeHandle(PInvoke.CreatePopupMenu()); if (TrayMenu != IntPtr.Zero)
{
var destructionStatus = Bridge.DestroyMenu(TrayMenu);
if (destructionStatus != true)
{
Logger.LogError("Failed to destroy menu.");
}
}
if (!TrayMenu.IsInvalid) TrayMenu = Bridge.CreatePopupMenu();
if (TrayMenu != IntPtr.Zero)
{ {
if (!startedFromPowerToys) if (!startedFromPowerToys)
{ {
// If Awake is started from PowerToys, the correct way to exit it is disabling it from Settings. // If Awake is started from PowerToys, the correct way to exit it is disabling it from Settings.
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, (uint)TrayCommands.TC_EXIT, "Exit"); Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING, (uint)TrayCommands.TC_EXIT, "Exit");
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_SEPARATOR, 0, string.Empty); Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_SEPARATOR, 0, string.Empty);
} }
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | (keepDisplayOn ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED) | (mode == AwakeMode.PASSIVE ? MENU_ITEM_FLAGS.MF_DISABLED : MENU_ITEM_FLAGS.MF_ENABLED), (uint)TrayCommands.TC_DISPLAY_SETTING, "Keep screen on"); Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING | (keepDisplayOn ? Native.Constants.MF_CHECKED : Native.Constants.MF_UNCHECKED) | (mode == AwakeMode.PASSIVE ? Native.Constants.MF_DISABLED : Native.Constants.MF_ENABLED), (uint)TrayCommands.TC_DISPLAY_SETTING, "Keep screen on");
} }
// In case there are no tray shortcuts defined for the application default to a // In case there are no tray shortcuts defined for the application default to a
// reasonable initial set. // reasonable initial set.
if (trayTimeShortcuts.Count == 0) if (trayTimeShortcuts.Count == 0)
{ {
trayTimeShortcuts.AddRange(APIHelper.GetDefaultTrayOptions()); trayTimeShortcuts.AddRange(Manager.GetDefaultTrayOptions());
} }
var awakeTimeMenu = new DestroyMenuSafeHandle(PInvoke.CreatePopupMenu(), false); var awakeTimeMenu = Bridge.CreatePopupMenu();
for (int i = 0; i < trayTimeShortcuts.Count; i++) 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); Bridge.InsertMenu(awakeTimeMenu, (uint)i, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING, (uint)TrayCommands.TC_TIME + (uint)i, trayTimeShortcuts.ElementAt(i).Key);
} }
PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_SEPARATOR, 0, string.Empty); Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_SEPARATOR, 0, string.Empty);
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)"); Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING | (mode == AwakeMode.PASSIVE ? Native.Constants.MF_CHECKED : Native.Constants.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"); Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING | (mode == AwakeMode.INDEFINITE ? Native.Constants.MF_CHECKED : Native.Constants.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"); Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_POPUP | (mode == AwakeMode.TIMED ? Native.Constants.MF_CHECKED : Native.Constants.MF_UNCHECKED), (uint)awakeTimeMenu, "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"); Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING | Native.Constants.MF_DISABLED | (mode == AwakeMode.EXPIRABLE ? Native.Constants.MF_CHECKED : Native.Constants.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_EXPIRABLE, "Keep awake until expiration date and time");
TrayIcon.Text = text; TrayIcon.Text = text;
} }
private sealed class CheckButtonToolStripMenuItemAccessibleObject : ToolStripItem.ToolStripItemAccessibleObject private sealed class CheckButtonToolStripMenuItemAccessibleObject : ToolStripItem.ToolStripItemAccessibleObject
{ {
private CheckButtonToolStripMenuItem _menuItem; private readonly CheckButtonToolStripMenuItem _menuItem;
public CheckButtonToolStripMenuItemAccessibleObject(CheckButtonToolStripMenuItem menuItem) public CheckButtonToolStripMenuItemAccessibleObject(CheckButtonToolStripMenuItem menuItem)
: base(menuItem) : base(menuItem)
@@ -149,23 +155,13 @@ namespace Awake.Core
_menuItem = menuItem; _menuItem = menuItem;
} }
public override AccessibleRole Role public override AccessibleRole Role => AccessibleRole.CheckButton;
{
get
{
return AccessibleRole.CheckButton;
}
}
public override string Name => _menuItem.Text + ", " + Role + ", " + (_menuItem.Checked ? "Checked" : "Unchecked"); public override string Name => _menuItem.Text + ", " + Role + ", " + (_menuItem.Checked ? "Checked" : "Unchecked");
} }
private sealed class CheckButtonToolStripMenuItem : ToolStripMenuItem private sealed class CheckButtonToolStripMenuItem : ToolStripMenuItem
{ {
public CheckButtonToolStripMenuItem()
{
}
protected override AccessibleObject CreateAccessibilityInstance() protected override AccessibleObject CreateAccessibilityInstance()
{ {
return new CheckButtonToolStripMenuItemAccessibleObject(this); return new CheckButtonToolStripMenuItemAccessibleObject(this);

View File

@@ -10,9 +10,6 @@ using System.Threading;
using System.Windows.Forms; using System.Windows.Forms;
using Awake.Core.Models; using Awake.Core.Models;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Windows.Win32;
#pragma warning disable CS8603 // Possible null reference return.
namespace Awake.Core namespace Awake.Core
{ {
@@ -20,7 +17,7 @@ namespace Awake.Core
{ {
private static SettingsUtils? _moduleSettings; private static SettingsUtils? _moduleSettings;
private static SettingsUtils ModuleSettings { get => _moduleSettings; set => _moduleSettings = value; } private static SettingsUtils? ModuleSettings { get => _moduleSettings; set => _moduleSettings = value; }
private static ManualResetEvent? _exitSignal; private static ManualResetEvent? _exitSignal;
@@ -36,7 +33,7 @@ namespace Awake.Core
switch (m.Msg) switch (m.Msg)
{ {
case (int)PInvoke.WM_COMMAND: case (int)Native.Constants.WM_COMMAND:
var targetCommandIndex = m.WParam.ToInt64() & 0xFFFF; var targetCommandIndex = m.WParam.ToInt64() & 0xFFFF;
switch (targetCommandIndex) switch (targetCommandIndex)
{ {
@@ -44,26 +41,26 @@ namespace Awake.Core
ExitCommandHandler(_exitSignal); ExitCommandHandler(_exitSignal);
break; break;
case (long)TrayCommands.TC_DISPLAY_SETTING: case (long)TrayCommands.TC_DISPLAY_SETTING:
DisplaySettingCommandHandler(InternalConstants.AppName); DisplaySettingCommandHandler(Constants.AppName);
break; break;
case (long)TrayCommands.TC_MODE_INDEFINITE: case (long)TrayCommands.TC_MODE_INDEFINITE:
IndefiniteKeepAwakeCommandHandler(InternalConstants.AppName); IndefiniteKeepAwakeCommandHandler(Constants.AppName);
break; break;
case (long)TrayCommands.TC_MODE_PASSIVE: case (long)TrayCommands.TC_MODE_PASSIVE:
PassiveKeepAwakeCommandHandler(InternalConstants.AppName); PassiveKeepAwakeCommandHandler(Constants.AppName);
break; break;
case var _ when targetCommandIndex >= trayCommandsSize: case var _ when targetCommandIndex >= trayCommandsSize:
// Format for the timer block: // Format for the timer block:
// TrayCommands.TC_TIME + ZERO_BASED_INDEX_IN_SETTINGS // TrayCommands.TC_TIME + ZERO_BASED_INDEX_IN_SETTINGS
AwakeSettings settings = ModuleSettings.GetSettings<AwakeSettings>(InternalConstants.AppName); AwakeSettings settings = ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName);
if (settings.Properties.CustomTrayTimes.Count == 0) if (settings.Properties.CustomTrayTimes.Count == 0)
{ {
settings.Properties.CustomTrayTimes.AddRange(APIHelper.GetDefaultTrayOptions()); settings.Properties.CustomTrayTimes.AddRange(Manager.GetDefaultTrayOptions());
} }
int index = (int)targetCommandIndex - (int)TrayCommands.TC_TIME; int index = (int)targetCommandIndex - (int)TrayCommands.TC_TIME;
var targetTime = settings.Properties.CustomTrayTimes.ElementAt(index).Value; var targetTime = settings.Properties.CustomTrayTimes.ElementAt(index).Value;
TimedKeepAwakeCommandHandler(InternalConstants.AppName, targetTime); TimedKeepAwakeCommandHandler(Constants.AppName, targetTime);
break; break;
} }
@@ -75,7 +72,7 @@ namespace Awake.Core
private static void ExitCommandHandler(ManualResetEvent? exitSignal) private static void ExitCommandHandler(ManualResetEvent? exitSignal)
{ {
APIHelper.CompleteExit(0, exitSignal, true); Manager.CompleteExit(0, exitSignal, true);
} }
private static void DisplaySettingCommandHandler(string moduleName) private static void DisplaySettingCommandHandler(string moduleName)
@@ -84,7 +81,7 @@ namespace Awake.Core
try try
{ {
currentSettings = ModuleSettings.GetSettings<AwakeSettings>(moduleName); currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(moduleName);
} }
catch (FileNotFoundException) catch (FileNotFoundException)
{ {
@@ -93,7 +90,7 @@ namespace Awake.Core
currentSettings.Properties.KeepDisplayOn = !currentSettings.Properties.KeepDisplayOn; currentSettings.Properties.KeepDisplayOn = !currentSettings.Properties.KeepDisplayOn;
ModuleSettings.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName); ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName);
} }
private static void TimedKeepAwakeCommandHandler(string moduleName, int seconds) private static void TimedKeepAwakeCommandHandler(string moduleName, int seconds)
@@ -104,7 +101,7 @@ namespace Awake.Core
try try
{ {
currentSettings = ModuleSettings.GetSettings<AwakeSettings>(moduleName); currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(moduleName);
} }
catch (FileNotFoundException) catch (FileNotFoundException)
{ {
@@ -115,7 +112,7 @@ namespace Awake.Core
currentSettings.Properties.IntervalHours = (uint)timeSpan.Hours; currentSettings.Properties.IntervalHours = (uint)timeSpan.Hours;
currentSettings.Properties.IntervalMinutes = (uint)timeSpan.Minutes; currentSettings.Properties.IntervalMinutes = (uint)timeSpan.Minutes;
ModuleSettings.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName); ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName);
} }
private static void PassiveKeepAwakeCommandHandler(string moduleName) private static void PassiveKeepAwakeCommandHandler(string moduleName)
@@ -124,7 +121,7 @@ namespace Awake.Core
try try
{ {
currentSettings = ModuleSettings.GetSettings<AwakeSettings>(moduleName); currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(moduleName);
} }
catch (FileNotFoundException) catch (FileNotFoundException)
{ {
@@ -133,7 +130,7 @@ namespace Awake.Core
currentSettings.Properties.Mode = AwakeMode.PASSIVE; currentSettings.Properties.Mode = AwakeMode.PASSIVE;
ModuleSettings.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName); ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName);
} }
private static void IndefiniteKeepAwakeCommandHandler(string moduleName) private static void IndefiniteKeepAwakeCommandHandler(string moduleName)
@@ -142,7 +139,7 @@ namespace Awake.Core
try try
{ {
currentSettings = ModuleSettings.GetSettings<AwakeSettings>(moduleName); currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(moduleName);
} }
catch (FileNotFoundException) catch (FileNotFoundException)
{ {
@@ -151,7 +148,7 @@ namespace Awake.Core
currentSettings.Properties.Mode = AwakeMode.INDEFINITE; currentSettings.Properties.Mode = AwakeMode.INDEFINITE;
ModuleSettings.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName); ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName);
} }
} }
} }

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<variable name="buildId" value="NOBLE_SIX_02162023" />
<targets async="true">
<target name="logfile"
xsi:type="File"
fileName="${specialfolder:folder=LocalApplicationData}/Microsoft/PowerToys/Awake/Logs/${var:awakeversion}/applog_${date:format=yyyy-MM-dd_HH}_${var:buildId}.txt"
layout="[${longdate} ${level:uppercase=true} ${logger}] ${message}"
archiveEvery="Day"
archiveNumbering="Rolling"
maxArchiveFiles="30"/>
<target name="logconsole"
xsi:type="Console"
layout="[${longdate} ${level:uppercase=true}] ${message}" />
</targets>
<rules>
<logger name="*" minlevel="Info" writeTo="logconsole" />
<logger name="*" minlevel="Debug" writeTo="logfile" />
</rules>
</nlog>

View File

@@ -1,23 +0,0 @@
AllocConsole
CreateFile
CreatePopupMenu
DestroyMenu
DestroyWindow
FindWindowEx
GetClassName
GetCurrentThreadId
GetPwrCapabilities
GetWindowThreadProcessId
HMENU
InsertMenu
PostQuitMessage
SendMessage
SetConsoleCtrlHandler
SetForegroundWindow
SetStdHandle
SetThreadExecutionState
TrackPopupMenuEx
WM_CLOSE
WM_COMMAND
WM_GETTEXT
WM_USER

View File

@@ -4,7 +4,6 @@
using System; using System;
using System.CommandLine; using System.CommandLine;
using System.CommandLine.Invocation;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing; using System.Drawing;
using System.Globalization; using System.Globalization;
@@ -17,18 +16,10 @@ using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Awake.Core; using Awake.Core;
using interop; using Awake.Core.Models;
using Awake.Core.Native;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using NLog;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Console;
using Windows.Win32.System.Power;
using Logger = NLog.Logger;
#pragma warning disable CS8602 // Dereference of a possibly null reference.
#pragma warning disable CS8603 // Possible null reference return.
namespace Awake namespace Awake
{ {
@@ -40,7 +31,7 @@ namespace Awake
// Format of the build ID is: CODENAME_MMDDYYYY, where MMDDYYYY // Format of the build ID is: CODENAME_MMDDYYYY, where MMDDYYYY
// is representative of the date when the last change was made before // is representative of the date when the last change was made before
// the pull request is issued. // the pull request is issued.
private static readonly string BuildId = "NOBLE_SIX_02162023"; private static readonly string BuildId = "ATRIOX_04132023";
private static Mutex? _mutex; private static Mutex? _mutex;
private static FileSystemWatcher? _watcher; private static FileSystemWatcher? _watcher;
@@ -48,22 +39,21 @@ namespace Awake
private static bool _startedFromPowerToys; private static bool _startedFromPowerToys;
public static Mutex LockMutex { get => _mutex; set => _mutex = value; } public static Mutex? LockMutex { get => _mutex; set => _mutex = value; }
private static Logger? _log;
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
private static PHANDLER_ROUTINE _handler; private static ConsoleEventHandler _handler;
private static SYSTEM_POWER_CAPABILITIES _powerCapabilities; private static SystemPowerCapabilities _powerCapabilities;
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
private static ManualResetEvent _exitSignal = new ManualResetEvent(false); private static ManualResetEvent _exitSignal = new ManualResetEvent(false);
private static int Main(string[] args) private static int Main(string[] args)
{ {
// Log initialization needs to always happen before we test whether _settingsUtils = new SettingsUtils();
// only one instance of Awake is running. LockMutex = new Mutex(true, Core.Constants.AppName, out bool instantiated);
_log = LogManager.GetCurrentClassLogger();
Logger.InitializeLogger(Path.Combine("\\", Core.Constants.AppName, "Logs"));
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredAwakeEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled) if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredAwakeEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{ {
@@ -71,20 +61,16 @@ namespace Awake
return 0; return 0;
} }
LockMutex = new Mutex(true, InternalConstants.AppName, out bool instantiated);
if (!instantiated) if (!instantiated)
{ {
Exit(InternalConstants.AppName + " is already running! Exiting the application.", 1, _exitSignal, true); Exit(Core.Constants.AppName + " is already running! Exiting the application.", 1, _exitSignal, true);
} }
_settingsUtils = new SettingsUtils(); Logger.LogInfo($"Launching {Core.Constants.AppName}...");
Logger.LogInfo(FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion);
_log.Info($"Launching {InternalConstants.AppName}..."); Logger.LogInfo($"Build: {BuildId}");
_log.Info(FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion); Logger.LogInfo($"OS: {Environment.OSVersion}");
_log.Info($"Build: {BuildId}"); Logger.LogInfo($"OS Build: {Manager.GetOperatingSystemBuild()}");
_log.Info($"OS: {Environment.OSVersion}");
_log.Info($"OS Build: {APIHelper.GetOperatingSystemBuild()}");
TaskScheduler.UnobservedTaskException += (sender, args) => TaskScheduler.UnobservedTaskException += (sender, args) =>
{ {
@@ -94,21 +80,18 @@ namespace Awake
// To make it easier to diagnose future issues, let's get the // To make it easier to diagnose future issues, let's get the
// system power capabilities and aggregate them in the log. // system power capabilities and aggregate them in the log.
PInvoke.GetPwrCapabilities(out _powerCapabilities); Bridge.GetPwrCapabilities(out _powerCapabilities);
_log.Info(JsonSerializer.Serialize(_powerCapabilities)); Logger.LogInfo(JsonSerializer.Serialize(_powerCapabilities));
_log.Info("Parsing parameters..."); Logger.LogInfo("Parsing parameters...");
Option<bool> configOption = new( Option<bool> configOption = new(
aliases: new[] { "--use-pt-config", "-c" }, aliases: new[] { "--use-pt-config", "-c" },
getDefaultValue: () => false, getDefaultValue: () => false,
description: $"Specifies whether {InternalConstants.AppName} will be using the PowerToys configuration file for managing the state.") description: $"Specifies whether {Core.Constants.AppName} will be using the PowerToys configuration file for managing the state.")
{ {
Argument = new Argument<bool>(() => false) Arity = ArgumentArity.ZeroOrOne,
{ IsRequired = false,
Arity = ArgumentArity.ZeroOrOne,
},
Required = false,
}; };
Option<bool> displayOption = new( Option<bool> displayOption = new(
@@ -116,11 +99,8 @@ namespace Awake
getDefaultValue: () => true, getDefaultValue: () => true,
description: "Determines whether the display should be kept awake.") description: "Determines whether the display should be kept awake.")
{ {
Argument = new Argument<bool>(() => false) Arity = ArgumentArity.ZeroOrOne,
{ IsRequired = false,
Arity = ArgumentArity.ZeroOrOne,
},
Required = false,
}; };
Option<uint> timeOption = new( Option<uint> timeOption = new(
@@ -128,35 +108,26 @@ namespace Awake
getDefaultValue: () => 0, getDefaultValue: () => 0,
description: "Determines the interval, in seconds, during which the computer is kept awake.") description: "Determines the interval, in seconds, during which the computer is kept awake.")
{ {
Argument = new Argument<uint>(() => 0) Arity = ArgumentArity.ExactlyOne,
{ IsRequired = false,
Arity = ArgumentArity.ExactlyOne,
},
Required = false,
}; };
Option<int> pidOption = new( Option<int> pidOption = new(
aliases: new[] { "--pid", "-p" }, aliases: new[] { "--pid", "-p" },
getDefaultValue: () => 0, getDefaultValue: () => 0,
description: $"Bind the execution of {InternalConstants.AppName} to another process. When the process ends, the system will resume managing the current sleep/display mode.") description: $"Bind the execution of {Core.Constants.AppName} to another process. When the process ends, the system will resume managing the current sleep and display state.")
{ {
Argument = new Argument<int>(() => 0) Arity = ArgumentArity.ZeroOrOne,
{ IsRequired = false,
Arity = ArgumentArity.ZeroOrOne,
},
Required = false,
}; };
Option<string> expireAtOption = new( Option<string> expireAtOption = new(
aliases: new[] { "--expire-at", "-e" }, aliases: new[] { "--expire-at", "-e" },
getDefaultValue: () => string.Empty, 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.") description: $"Determines the end date/time when {Core.Constants.AppName} will back off and let the system manage the current sleep and display state.")
{ {
Argument = new Argument<string>(() => string.Empty) Arity = ArgumentArity.ZeroOrOne,
{ IsRequired = false,
Arity = ArgumentArity.ZeroOrOne,
},
Required = false,
}; };
RootCommand? rootCommand = new() RootCommand? rootCommand = new()
@@ -168,49 +139,58 @@ namespace Awake
expireAtOption, expireAtOption,
}; };
rootCommand.Description = InternalConstants.AppName; rootCommand.Description = Core.Constants.AppName;
rootCommand.Handler = CommandHandler.Create<bool, bool, uint, int, string>(HandleCommandLineArguments); rootCommand.SetHandler(
HandleCommandLineArguments,
_log.Info("Parameter setup complete. Proceeding to the rest of the app initiation..."); configOption,
displayOption,
timeOption,
pidOption,
expireAtOption);
return rootCommand.InvokeAsync(args).Result; return rootCommand.InvokeAsync(args).Result;
} }
private static BOOL ExitHandler(uint ctrlType) private static bool ExitHandler(ControlType ctrlType)
{ {
_log.Info($"Exited through handler with control type: {ctrlType}"); Logger.LogInfo($"Exited through handler with control type: {ctrlType}");
Exit("Exiting from the internal termination handler.", Environment.ExitCode, _exitSignal); Exit("Exiting from the internal termination handler.", Environment.ExitCode, _exitSignal);
return false; return false;
} }
private static void Exit(string message, int exitCode, ManualResetEvent exitSignal, bool force = false) private static void Exit(string message, int exitCode, ManualResetEvent exitSignal, bool force = false)
{ {
_log.Info(message); Logger.LogInfo(message);
APIHelper.CompleteExit(exitCode, exitSignal, force); Manager.CompleteExit(exitCode, exitSignal, force);
} }
private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid, string expireAt) private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid, string expireAt)
{ {
_handler += ExitHandler;
APIHelper.SetConsoleControlHandler(_handler, true);
if (pid == 0) if (pid == 0)
{ {
_log.Info("No PID specified. Allocating console..."); Logger.LogInfo("No PID specified. Allocating console...");
APIHelper.AllocateConsole(); Manager.AllocateConsole();
_handler += new ConsoleEventHandler(ExitHandler);
Manager.SetConsoleControlHandler(_handler, true);
Trace.Listeners.Add(new ConsoleTraceListener());
} }
else else
{ {
_startedFromPowerToys = true; _startedFromPowerToys = true;
} }
_log.Info($"The value for --use-pt-config is: {usePtConfig}"); Logger.LogInfo($"The value for --use-pt-config is: {usePtConfig}");
_log.Info($"The value for --display-on is: {displayOn}"); Logger.LogInfo($"The value for --display-on is: {displayOn}");
_log.Info($"The value for --time-limit is: {timeLimit}"); Logger.LogInfo($"The value for --time-limit is: {timeLimit}");
_log.Info($"The value for --pid is: {pid}"); Logger.LogInfo($"The value for --pid is: {pid}");
_log.Info($"The value for --expire is: {expireAt}"); Logger.LogInfo($"The value for --expire-at is: {expireAt}");
// Start the monitor thread that will be used to track the current state.
Manager.StartMonitor();
if (usePtConfig) if (usePtConfig)
{ {
@@ -218,7 +198,7 @@ namespace Awake
// and instead watch for changes in the file. // and instead watch for changes in the file.
try try
{ {
var eventHandle = new EventWaitHandle(false, EventResetMode.ManualReset, Constants.AwakeExitEvent()); var eventHandle = new EventWaitHandle(false, EventResetMode.ManualReset, interop.Constants.AwakeExitEvent());
new Thread(() => new Thread(() =>
{ {
if (WaitHandle.WaitAny(new WaitHandle[] { _exitSignal, eventHandle }) == 1) if (WaitHandle.WaitAny(new WaitHandle[] { _exitSignal, eventHandle }) == 1)
@@ -227,17 +207,17 @@ namespace Awake
} }
}).Start(); }).Start();
TrayHelper.InitializeTray(InternalConstants.FullAppName, new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "images/awake.ico")), _exitSignal); TrayHelper.InitializeTray(Core.Constants.FullAppName, new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "images/awake.ico")), _exitSignal);
string? settingsPath = _settingsUtils.GetSettingsFilePath(InternalConstants.AppName); string? settingsPath = _settingsUtils!.GetSettingsFilePath(Core.Constants.AppName);
_log.Info($"Reading configuration file: {settingsPath}"); Logger.LogInfo($"Reading configuration file: {settingsPath}");
if (!File.Exists(settingsPath)) if (!File.Exists(settingsPath))
{ {
string? errorString = $"The settings file does not exist. Scaffolding default configuration..."; string? errorString = $"The settings file does not exist. Scaffolding default configuration...";
AwakeSettings scaffoldSettings = new AwakeSettings(); AwakeSettings scaffoldSettings = new AwakeSettings();
_settingsUtils.SaveSettings(JsonSerializer.Serialize(scaffoldSettings), InternalConstants.AppName); _settingsUtils.SaveSettings(JsonSerializer.Serialize(scaffoldSettings), Core.Constants.AppName);
} }
ScaffoldConfiguration(settingsPath); ScaffoldConfiguration(settingsPath);
@@ -245,8 +225,7 @@ namespace Awake
catch (Exception ex) catch (Exception ex)
{ {
string? errorString = $"There was a problem with the configuration file. Make sure it exists.\n{ex.Message}"; string? errorString = $"There was a problem with the configuration file. Make sure it exists.\n{ex.Message}";
_log.Info(errorString); Logger.LogError(errorString);
_log.Debug(errorString);
} }
} }
else else
@@ -264,16 +243,18 @@ namespace Awake
// converting the target date to seconds and then passing to SetupTimedKeepAwake // converting the target date to seconds and then passing to SetupTimedKeepAwake
// because that way we're accounting for the user potentially changing their clock // because that way we're accounting for the user potentially changing their clock
// while Awake is running. // while Awake is running.
Logger.LogInfo($"Operating in thread ID {Environment.CurrentManagedThreadId}.");
SetupExpirableKeepAwake(expirationDateTime, displayOn); SetupExpirableKeepAwake(expirationDateTime, displayOn);
} }
else else
{ {
_log.Info($"Target date is not in the future, therefore there is nothing to wait for."); Logger.LogInfo($"Target date is not in the future, therefore there is nothing to wait for.");
} }
} }
catch catch (Exception ex)
{ {
_log.Error($"Could not parse date string {expireAt} into a viable date."); Logger.LogError($"Could not parse date string {expireAt} into a viable date.");
Logger.LogError(ex.Message);
} }
} }
else else
@@ -295,7 +276,7 @@ namespace Awake
{ {
RunnerHelper.WaitForPowerToysRunner(pid, () => RunnerHelper.WaitForPowerToysRunner(pid, () =>
{ {
_log.Info($"Triggered PID-based exit handler for PID {pid}."); Logger.LogInfo($"Triggered PID-based exit handler for PID {pid}.");
Exit("Terminating from process binding hook.", 0, _exitSignal, true); Exit("Terminating from process binding hook.", 0, _exitSignal, true);
}); });
} }
@@ -330,7 +311,7 @@ namespace Awake
.Select(e => e.EventArgs) .Select(e => e.EventArgs)
.Subscribe(HandleAwakeConfigChange); .Subscribe(HandleAwakeConfigChange);
TrayHelper.SetTray(InternalConstants.FullAppName, new AwakeSettings(), _startedFromPowerToys); TrayHelper.SetTray(Core.Constants.FullAppName, new AwakeSettings(), _startedFromPowerToys);
// Initially the file might not be updated, so we need to start processing // Initially the file might not be updated, so we need to start processing
// settings right away. // settings right away.
@@ -338,19 +319,19 @@ namespace Awake
} }
catch (Exception ex) catch (Exception ex)
{ {
_log.Error($"An error occurred scaffolding the configuration. Error details: {ex.Message}"); Logger.LogError($"An error occurred scaffolding the configuration. Error details: {ex.Message}");
} }
} }
private static void SetupIndefiniteKeepAwake(bool displayOn) private static void SetupIndefiniteKeepAwake(bool displayOn)
{ {
APIHelper.SetIndefiniteKeepAwake(LogCompletedKeepAwakeThread, LogUnexpectedOrCancelledKeepAwakeThreadCompletion, displayOn); Manager.SetIndefiniteKeepAwake(displayOn);
} }
private static void HandleAwakeConfigChange(FileSystemEventArgs fileEvent) private static void HandleAwakeConfigChange(FileSystemEventArgs fileEvent)
{ {
_log.Info("Detected a settings file change. Updating configuration..."); Logger.LogInfo("Detected a settings file change. Updating configuration...");
_log.Info("Resetting keep-awake to normal state due to settings change."); Logger.LogInfo("Resetting keep-awake to normal state due to settings change.");
ProcessSettings(); ProcessSettings();
} }
@@ -358,11 +339,11 @@ namespace Awake
{ {
try try
{ {
AwakeSettings settings = _settingsUtils.GetSettings<AwakeSettings>(InternalConstants.AppName); AwakeSettings settings = _settingsUtils!.GetSettings<AwakeSettings>(Core.Constants.AppName);
if (settings != null) if (settings != null)
{ {
_log.Info($"Identified custom time shortcuts for the tray: {settings.Properties.CustomTrayTimes.Count}"); Logger.LogInfo($"Identified custom time shortcuts for the tray: {settings.Properties.CustomTrayTimes.Count}");
switch (settings.Properties.Mode) switch (settings.Properties.Mode)
{ {
@@ -396,60 +377,45 @@ namespace Awake
default: default:
{ {
string? errorMessage = "Unknown mode of operation. Check config file."; string? errorMessage = "Unknown mode of operation. Check config file.";
_log.Info(errorMessage); Logger.LogError(errorMessage);
_log.Debug(errorMessage);
break; break;
} }
} }
TrayHelper.SetTray(InternalConstants.FullAppName, settings, _startedFromPowerToys); TrayHelper.SetTray(Core.Constants.FullAppName, settings, _startedFromPowerToys);
} }
else else
{ {
string? errorMessage = "Settings are null."; string? errorMessage = "Settings are null.";
_log.Info(errorMessage); Logger.LogError(errorMessage);
_log.Debug(errorMessage);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
string? errorMessage = $"There was a problem reading the configuration file. Error: {ex.GetType()} {ex.Message}"; string? errorMessage = $"There was a problem reading the configuration file. Error: {ex.GetType()} {ex.Message}";
_log.Info(errorMessage); Logger.LogError(errorMessage);
_log.Debug(errorMessage);
} }
} }
private static void SetupNoKeepAwake() private static void SetupNoKeepAwake()
{ {
_log.Info($"Operating in passive mode (computer's standard power plan). No custom keep awake settings enabled."); Logger.LogInfo($"Operating in passive mode (computer's standard power plan). No custom keep awake settings enabled.");
APIHelper.SetNoKeepAwake(); Manager.SetNoKeepAwake();
} }
private static void SetupExpirableKeepAwake(DateTimeOffset expireAt, bool displayOn) 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}."); Logger.LogInfo($"Expirable keep-awake. Expected expiration date/time: {expireAt} with display on setting set to {displayOn}.");
APIHelper.SetExpirableKeepAwake(expireAt, LogCompletedKeepAwakeThread, LogUnexpectedOrCancelledKeepAwakeThreadCompletion, displayOn); Manager.SetExpirableKeepAwake(expireAt, displayOn);
} }
private static void SetupTimedKeepAwake(uint time, bool 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}."); Logger.LogInfo($"Timed keep-awake. Expected runtime: {time} seconds with display on setting set to {displayOn}.");
APIHelper.SetTimedKeepAwake(time, LogCompletedKeepAwakeThread, LogUnexpectedOrCancelledKeepAwakeThreadCompletion, displayOn); Manager.SetTimedKeepAwake(time, displayOn);
}
private static void LogUnexpectedOrCancelledKeepAwakeThreadCompletion()
{
string? errorMessage = "The keep awake thread was terminated early.";
_log.Info(errorMessage);
_log.Debug(errorMessage);
}
private static void LogCompletedKeepAwakeThread()
{
_log.Info($"Exited keep awake thread successfully.");
} }
} }
} }