[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

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

View File

@@ -18,6 +18,7 @@
<PackageProjectUrl>https://awake.den.dev</PackageProjectUrl>
<RepositoryUrl>https://github.com/microsoft/powertoys</RepositoryUrl>
<SelfContained>true</SelfContained>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
</PropertyGroup>
<!-- SelfContained=true requires RuntimeIdentifier to be set -->
@@ -51,16 +52,16 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" />
<ItemGroup>
<None Remove="Images\Awake.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.CsWinRT" />
<PackageReference Include="NLog" />
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="System.Runtime.Caching" />
@@ -79,16 +80,17 @@
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="NLog.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Content Include="Images\Awake.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.CodeAnalysis.NetAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</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
{
internal static class InternalConstants
internal static class Constants
{
internal const string AppName = "Awake";
internal const string FullAppName = "PowerToys " + AppName;
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.
// See the LICENSE file in the project root for more information.
using Windows.Win32;
namespace Awake.Core.Models
{
internal enum TrayCommands : uint
{
TC_DISPLAY_SETTING = PInvoke.WM_USER + 1,
TC_MODE_PASSIVE = PInvoke.WM_USER + 2,
TC_MODE_INDEFINITE = PInvoke.WM_USER + 3,
TC_MODE_EXPIRABLE = PInvoke.WM_USER + 4,
TC_EXIT = PInvoke.WM_USER + 100,
TC_TIME = PInvoke.WM_USER + 101,
TC_DISPLAY_SETTING = Native.Constants.WM_USER + 1,
TC_MODE_PASSIVE = Native.Constants.WM_USER + 2,
TC_MODE_INDEFINITE = Native.Constants.WM_USER + 3,
TC_MODE_EXPIRABLE = Native.Constants.WM_USER + 4,
TC_EXIT = Native.Constants.WM_USER + 100,
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.Windows.Forms;
using Awake.Core.Models;
using Awake.Core.Native;
using ManagedCommon;
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
{
@@ -29,16 +25,14 @@ namespace Awake.Core
/// </remarks>
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; }
static TrayHelper()
{
_log = LogManager.GetCurrentClassLogger();
TrayMenu = new DestroyMenuSafeHandle();
TrayIcon = new NotifyIcon();
}
@@ -49,20 +43,23 @@ namespace Awake.Core
{
try
{
_log.Info("Setting up the tray.");
((NotifyIcon?)tray).Text = text;
((NotifyIcon?)tray).Icon = icon;
((NotifyIcon?)tray).ContextMenuStrip = contextMenu;
((NotifyIcon?)tray).Visible = true;
((NotifyIcon?)tray).MouseClick += TrayClickHandler;
Application.AddMessageFilter(new TrayMessageFilter(exitSignal));
Application.Run();
_log.Info("Tray setup complete.");
Logger.LogInfo("Setting up the tray.");
if (tray != null)
{
((NotifyIcon)tray).Text = text;
((NotifyIcon)tray).Icon = icon;
((NotifyIcon)tray).ContextMenuStrip = contextMenu;
((NotifyIcon)tray).Visible = true;
((NotifyIcon)tray).MouseClick += TrayClickHandler;
Application.AddMessageFilter(new TrayMessageFilter(exitSignal));
Application.Run();
Logger.LogInfo("Tray setup complete.");
}
}
catch (Exception ex)
{
_log.Error($"An error occurred initializing the tray. {ex.Message}");
_log.Error($"{ex.StackTrace}");
Logger.LogError($"An error occurred initializing the tray. {ex.Message}");
Logger.LogError($"{ex.StackTrace}");
}
},
TrayIcon);
@@ -81,12 +78,12 @@ namespace Awake.Core
/// <param name="e">MouseEventArgs instance containing mouse click event information.</param>
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);
PInvoke.TrackPopupMenuEx(TrayMenu, 0, Cursor.Position.X, Cursor.Position.Y, windowHandle, null);
Bridge.SetForegroundWindow(windowHandle);
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)
{
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 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");
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_STRING, (uint)TrayCommands.TC_EXIT, "Exit");
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
// reasonable initial set.
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++)
{
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)");
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");
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)");
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");
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");
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;
}
private sealed class CheckButtonToolStripMenuItemAccessibleObject : ToolStripItem.ToolStripItemAccessibleObject
{
private CheckButtonToolStripMenuItem _menuItem;
private readonly CheckButtonToolStripMenuItem _menuItem;
public CheckButtonToolStripMenuItemAccessibleObject(CheckButtonToolStripMenuItem menuItem)
: base(menuItem)
@@ -149,23 +155,13 @@ namespace Awake.Core
_menuItem = menuItem;
}
public override AccessibleRole Role
{
get
{
return AccessibleRole.CheckButton;
}
}
public override AccessibleRole Role => AccessibleRole.CheckButton;
public override string Name => _menuItem.Text + ", " + Role + ", " + (_menuItem.Checked ? "Checked" : "Unchecked");
}
private sealed class CheckButtonToolStripMenuItem : ToolStripMenuItem
{
public CheckButtonToolStripMenuItem()
{
}
protected override AccessibleObject CreateAccessibilityInstance()
{
return new CheckButtonToolStripMenuItemAccessibleObject(this);

View File

@@ -10,9 +10,6 @@ using System.Threading;
using System.Windows.Forms;
using Awake.Core.Models;
using Microsoft.PowerToys.Settings.UI.Library;
using Windows.Win32;
#pragma warning disable CS8603 // Possible null reference return.
namespace Awake.Core
{
@@ -20,7 +17,7 @@ namespace Awake.Core
{
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;
@@ -36,7 +33,7 @@ namespace Awake.Core
switch (m.Msg)
{
case (int)PInvoke.WM_COMMAND:
case (int)Native.Constants.WM_COMMAND:
var targetCommandIndex = m.WParam.ToInt64() & 0xFFFF;
switch (targetCommandIndex)
{
@@ -44,26 +41,26 @@ namespace Awake.Core
ExitCommandHandler(_exitSignal);
break;
case (long)TrayCommands.TC_DISPLAY_SETTING:
DisplaySettingCommandHandler(InternalConstants.AppName);
DisplaySettingCommandHandler(Constants.AppName);
break;
case (long)TrayCommands.TC_MODE_INDEFINITE:
IndefiniteKeepAwakeCommandHandler(InternalConstants.AppName);
IndefiniteKeepAwakeCommandHandler(Constants.AppName);
break;
case (long)TrayCommands.TC_MODE_PASSIVE:
PassiveKeepAwakeCommandHandler(InternalConstants.AppName);
PassiveKeepAwakeCommandHandler(Constants.AppName);
break;
case var _ when targetCommandIndex >= trayCommandsSize:
// Format for the timer block:
// 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)
{
settings.Properties.CustomTrayTimes.AddRange(APIHelper.GetDefaultTrayOptions());
settings.Properties.CustomTrayTimes.AddRange(Manager.GetDefaultTrayOptions());
}
int index = (int)targetCommandIndex - (int)TrayCommands.TC_TIME;
var targetTime = settings.Properties.CustomTrayTimes.ElementAt(index).Value;
TimedKeepAwakeCommandHandler(InternalConstants.AppName, targetTime);
TimedKeepAwakeCommandHandler(Constants.AppName, targetTime);
break;
}
@@ -75,7 +72,7 @@ namespace Awake.Core
private static void ExitCommandHandler(ManualResetEvent? exitSignal)
{
APIHelper.CompleteExit(0, exitSignal, true);
Manager.CompleteExit(0, exitSignal, true);
}
private static void DisplaySettingCommandHandler(string moduleName)
@@ -84,7 +81,7 @@ namespace Awake.Core
try
{
currentSettings = ModuleSettings.GetSettings<AwakeSettings>(moduleName);
currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(moduleName);
}
catch (FileNotFoundException)
{
@@ -93,7 +90,7 @@ namespace Awake.Core
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)
@@ -104,7 +101,7 @@ namespace Awake.Core
try
{
currentSettings = ModuleSettings.GetSettings<AwakeSettings>(moduleName);
currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(moduleName);
}
catch (FileNotFoundException)
{
@@ -115,7 +112,7 @@ namespace Awake.Core
currentSettings.Properties.IntervalHours = (uint)timeSpan.Hours;
currentSettings.Properties.IntervalMinutes = (uint)timeSpan.Minutes;
ModuleSettings.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName);
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName);
}
private static void PassiveKeepAwakeCommandHandler(string moduleName)
@@ -124,7 +121,7 @@ namespace Awake.Core
try
{
currentSettings = ModuleSettings.GetSettings<AwakeSettings>(moduleName);
currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(moduleName);
}
catch (FileNotFoundException)
{
@@ -133,7 +130,7 @@ namespace Awake.Core
currentSettings.Properties.Mode = AwakeMode.PASSIVE;
ModuleSettings.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName);
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName);
}
private static void IndefiniteKeepAwakeCommandHandler(string moduleName)
@@ -142,7 +139,7 @@ namespace Awake.Core
try
{
currentSettings = ModuleSettings.GetSettings<AwakeSettings>(moduleName);
currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(moduleName);
}
catch (FileNotFoundException)
{
@@ -151,7 +148,7 @@ namespace Awake.Core
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.CommandLine;
using System.CommandLine.Invocation;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
@@ -17,18 +16,10 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Awake.Core;
using interop;
using Awake.Core.Models;
using Awake.Core.Native;
using ManagedCommon;
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
{
@@ -40,7 +31,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 = "NOBLE_SIX_02162023";
private static readonly string BuildId = "ATRIOX_04132023";
private static Mutex? _mutex;
private static FileSystemWatcher? _watcher;
@@ -48,22 +39,21 @@ namespace Awake
private static bool _startedFromPowerToys;
public static Mutex LockMutex { get => _mutex; set => _mutex = value; }
private static Logger? _log;
public static Mutex? LockMutex { get => _mutex; set => _mutex = value; }
#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 SYSTEM_POWER_CAPABILITIES _powerCapabilities;
private static ConsoleEventHandler _handler;
private static SystemPowerCapabilities _powerCapabilities;
#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 int Main(string[] args)
{
// Log initialization needs to always happen before we test whether
// only one instance of Awake is running.
_log = LogManager.GetCurrentClassLogger();
_settingsUtils = new SettingsUtils();
LockMutex = new Mutex(true, Core.Constants.AppName, out bool instantiated);
Logger.InitializeLogger(Path.Combine("\\", Core.Constants.AppName, "Logs"));
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredAwakeEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{
@@ -71,20 +61,16 @@ namespace Awake
return 0;
}
LockMutex = new Mutex(true, InternalConstants.AppName, out bool 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();
_log.Info($"Launching {InternalConstants.AppName}...");
_log.Info(FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion);
_log.Info($"Build: {BuildId}");
_log.Info($"OS: {Environment.OSVersion}");
_log.Info($"OS Build: {APIHelper.GetOperatingSystemBuild()}");
Logger.LogInfo($"Launching {Core.Constants.AppName}...");
Logger.LogInfo(FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion);
Logger.LogInfo($"Build: {BuildId}");
Logger.LogInfo($"OS: {Environment.OSVersion}");
Logger.LogInfo($"OS Build: {Manager.GetOperatingSystemBuild()}");
TaskScheduler.UnobservedTaskException += (sender, args) =>
{
@@ -94,21 +80,18 @@ namespace Awake
// To make it easier to diagnose future issues, let's get the
// system power capabilities and aggregate them in the log.
PInvoke.GetPwrCapabilities(out _powerCapabilities);
_log.Info(JsonSerializer.Serialize(_powerCapabilities));
Bridge.GetPwrCapabilities(out _powerCapabilities);
Logger.LogInfo(JsonSerializer.Serialize(_powerCapabilities));
_log.Info("Parsing parameters...");
Logger.LogInfo("Parsing parameters...");
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.")
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,
},
Required = false,
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<bool> displayOption = new(
@@ -116,11 +99,8 @@ namespace Awake
getDefaultValue: () => true,
description: "Determines whether the display should be kept awake.")
{
Argument = new Argument<bool>(() => false)
{
Arity = ArgumentArity.ZeroOrOne,
},
Required = false,
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> timeOption = new(
@@ -128,35 +108,26 @@ namespace Awake
getDefaultValue: () => 0,
description: "Determines the interval, in seconds, during which the computer is kept awake.")
{
Argument = new Argument<uint>(() => 0)
{
Arity = ArgumentArity.ExactlyOne,
},
Required = false,
Arity = ArgumentArity.ExactlyOne,
IsRequired = false,
};
Option<int> pidOption = new(
aliases: new[] { "--pid", "-p" },
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,
},
Required = false,
Arity = ArgumentArity.ZeroOrOne,
IsRequired = 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.")
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,
},
Required = false,
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
RootCommand? rootCommand = new()
@@ -168,49 +139,58 @@ namespace Awake
expireAtOption,
};
rootCommand.Description = InternalConstants.AppName;
rootCommand.Description = Core.Constants.AppName;
rootCommand.Handler = CommandHandler.Create<bool, bool, uint, int, string>(HandleCommandLineArguments);
_log.Info("Parameter setup complete. Proceeding to the rest of the app initiation...");
rootCommand.SetHandler(
HandleCommandLineArguments,
configOption,
displayOption,
timeOption,
pidOption,
expireAtOption);
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);
return 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)
{
_handler += ExitHandler;
APIHelper.SetConsoleControlHandler(_handler, true);
if (pid == 0)
{
_log.Info("No PID specified. Allocating console...");
APIHelper.AllocateConsole();
Logger.LogInfo("No PID specified. Allocating console...");
Manager.AllocateConsole();
_handler += new ConsoleEventHandler(ExitHandler);
Manager.SetConsoleControlHandler(_handler, true);
Trace.Listeners.Add(new ConsoleTraceListener());
}
else
{
_startedFromPowerToys = true;
}
_log.Info($"The value for --use-pt-config is: {usePtConfig}");
_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}");
Logger.LogInfo($"The value for --use-pt-config is: {usePtConfig}");
Logger.LogInfo($"The value for --display-on is: {displayOn}");
Logger.LogInfo($"The value for --time-limit is: {timeLimit}");
Logger.LogInfo($"The value for --pid is: {pid}");
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)
{
@@ -218,7 +198,7 @@ namespace Awake
// and instead watch for changes in the file.
try
{
var eventHandle = new EventWaitHandle(false, EventResetMode.ManualReset, Constants.AwakeExitEvent());
var eventHandle = new EventWaitHandle(false, EventResetMode.ManualReset, interop.Constants.AwakeExitEvent());
new Thread(() =>
{
if (WaitHandle.WaitAny(new WaitHandle[] { _exitSignal, eventHandle }) == 1)
@@ -227,17 +207,17 @@ namespace Awake
}
}).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);
_log.Info($"Reading configuration file: {settingsPath}");
string? settingsPath = _settingsUtils!.GetSettingsFilePath(Core.Constants.AppName);
Logger.LogInfo($"Reading configuration file: {settingsPath}");
if (!File.Exists(settingsPath))
{
string? errorString = $"The settings file does not exist. Scaffolding default configuration...";
AwakeSettings scaffoldSettings = new AwakeSettings();
_settingsUtils.SaveSettings(JsonSerializer.Serialize(scaffoldSettings), InternalConstants.AppName);
_settingsUtils.SaveSettings(JsonSerializer.Serialize(scaffoldSettings), Core.Constants.AppName);
}
ScaffoldConfiguration(settingsPath);
@@ -245,8 +225,7 @@ namespace Awake
catch (Exception ex)
{
string? errorString = $"There was a problem with the configuration file. Make sure it exists.\n{ex.Message}";
_log.Info(errorString);
_log.Debug(errorString);
Logger.LogError(errorString);
}
}
else
@@ -264,16 +243,18 @@ namespace Awake
// 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.
Logger.LogInfo($"Operating in thread ID {Environment.CurrentManagedThreadId}.");
SetupExpirableKeepAwake(expirationDateTime, displayOn);
}
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
@@ -295,7 +276,7 @@ namespace Awake
{
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);
});
}
@@ -330,7 +311,7 @@ namespace Awake
.Select(e => e.EventArgs)
.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
// settings right away.
@@ -338,19 +319,19 @@ namespace Awake
}
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)
{
APIHelper.SetIndefiniteKeepAwake(LogCompletedKeepAwakeThread, LogUnexpectedOrCancelledKeepAwakeThreadCompletion, displayOn);
Manager.SetIndefiniteKeepAwake(displayOn);
}
private static void HandleAwakeConfigChange(FileSystemEventArgs fileEvent)
{
_log.Info("Detected a settings file change. Updating configuration...");
_log.Info("Resetting keep-awake to normal state due to settings change.");
Logger.LogInfo("Detected a settings file change. Updating configuration...");
Logger.LogInfo("Resetting keep-awake to normal state due to settings change.");
ProcessSettings();
}
@@ -358,11 +339,11 @@ namespace Awake
{
try
{
AwakeSettings settings = _settingsUtils.GetSettings<AwakeSettings>(InternalConstants.AppName);
AwakeSettings settings = _settingsUtils!.GetSettings<AwakeSettings>(Core.Constants.AppName);
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)
{
@@ -396,60 +377,45 @@ namespace Awake
default:
{
string? errorMessage = "Unknown mode of operation. Check config file.";
_log.Info(errorMessage);
_log.Debug(errorMessage);
Logger.LogError(errorMessage);
break;
}
}
TrayHelper.SetTray(InternalConstants.FullAppName, settings, _startedFromPowerToys);
TrayHelper.SetTray(Core.Constants.FullAppName, settings, _startedFromPowerToys);
}
else
{
string? errorMessage = "Settings are null.";
_log.Info(errorMessage);
_log.Debug(errorMessage);
Logger.LogError(errorMessage);
}
}
catch (Exception ex)
{
string? errorMessage = $"There was a problem reading the configuration file. Error: {ex.GetType()} {ex.Message}";
_log.Info(errorMessage);
_log.Debug(errorMessage);
Logger.LogError(errorMessage);
}
}
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)
{
_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)
{
_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);
}
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.");
Manager.SetTimedKeepAwake(time, displayOn);
}
}
}