From 97997035f792cb9b3c5cc73eb5da18bd1d7fadaa Mon Sep 17 00:00:00 2001 From: Dave Rayment Date: Thu, 25 Dec 2025 08:31:58 +0000 Subject: [PATCH] [Awake] Fix issues with help and error text not being visible when running Awake via the command line (#41774) ## Summary of the Pull Request This fixes issues when running Awake via the command line. It allows for the display of help/usage information, parsing errors, and normal logging information to the user, whereas these were not shown previously. Note: the GPO check is now deliberately placed _after_ the parameter parsing, changing previous behaviour. This lets the user view help information about Awake even if they cannot yet run the application because of a policy rule. There is no change to the GPO check itself. ## PR Checklist - [x] Closes: #40511, #41751 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments Awake is compiled as a Windows Executable. When run via the command line, it does not have a console to which it can log information or errors. The application does open its own console under certain circumstances, but this occurs _after_ command line parameter parsing is done, which means errors and help information cannot be displayed. This fix attaches to the parent console and moves the parameter parsing to the start of `Main` so the errors and usage information can now be seen: ### Help/usage information image ### Parsing error display image ### Normal operation image ## Validation Steps Performed - Tested that all modes still perform as expected from both the command line and via PowerToys Runner / settings file. - Confirmed that there were no side-effects from attaching to the console when running in non-command line mode (`AttachConsole` fails in that instance and no other changes are apparent). --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Leilei Zhang --- src/modules/awake/Awake/Core/Native/Bridge.cs | 7 + .../awake/Awake/Core/Native/Constants.cs | 3 + src/modules/awake/Awake/Program.cs | 212 +++++++++--------- 3 files changed, 116 insertions(+), 106 deletions(-) diff --git a/src/modules/awake/Awake/Core/Native/Bridge.cs b/src/modules/awake/Awake/Core/Native/Bridge.cs index e82b698a47..cfacd83180 100644 --- a/src/modules/awake/Awake/Core/Native/Bridge.cs +++ b/src/modules/awake/Awake/Core/Native/Bridge.cs @@ -28,6 +28,13 @@ namespace Awake.Core.Native [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool AllocConsole(); + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool AttachConsole(int dwProcessId); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern void FreeConsole(); + [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool SetStdHandle(int nStdHandle, IntPtr hHandle); diff --git a/src/modules/awake/Awake/Core/Native/Constants.cs b/src/modules/awake/Awake/Core/Native/Constants.cs index 8598854ba2..156c3a9da2 100644 --- a/src/modules/awake/Awake/Core/Native/Constants.cs +++ b/src/modules/awake/Awake/Core/Native/Constants.cs @@ -49,5 +49,8 @@ namespace Awake.Core.Native // Menu Item Info Flags internal const uint MNS_AUTO_DISMISS = 0x10000000; internal const uint MIM_STYLE = 0x00000010; + + // Attach Console + internal const int ATTACH_PARENT_PROCESS = -1; } } diff --git a/src/modules/awake/Awake/Program.cs b/src/modules/awake/Awake/Program.cs index b5c3102ba0..452487f2bf 100644 --- a/src/modules/awake/Awake/Program.cs +++ b/src/modules/awake/Awake/Program.cs @@ -51,6 +51,24 @@ namespace Awake private static async Task Main(string[] args) { + var rootCommand = BuildRootCommand(); + + Bridge.AttachConsole(Core.Native.Constants.ATTACH_PARENT_PROCESS); + + var parseResult = rootCommand.Parse(args); + + if (parseResult.Tokens.Any(t => t.Value.ToLowerInvariant() is "--help" or "-h" or "-?")) + { + // Print help and exit. + return rootCommand.Invoke(args); + } + + if (parseResult.Errors.Count > 0) + { + // Shows errors and returns non-zero. + return rootCommand.Invoke(args); + } + _settingsUtils = SettingsUtils.Default; LockMutex = new Mutex(true, Core.Constants.AppName, out bool instantiated); @@ -107,116 +125,97 @@ namespace Awake Bridge.GetPwrCapabilities(out _powerCapabilities); Logger.LogInfo(JsonSerializer.Serialize(_powerCapabilities, _serializerOptions)); - Logger.LogInfo("Parsing parameters..."); - - Option configOption = new(_aliasesConfigOption, () => false, Resources.AWAKE_CMD_HELP_CONFIG_OPTION) - { - Arity = ArgumentArity.ZeroOrOne, - IsRequired = false, - }; - - Option displayOption = new(_aliasesDisplayOption, () => true, Resources.AWAKE_CMD_HELP_DISPLAY_OPTION) - { - Arity = ArgumentArity.ZeroOrOne, - IsRequired = false, - }; - - Option timeOption = new(_aliasesTimeOption, () => 0, Resources.AWAKE_CMD_HELP_TIME_OPTION) - { - Arity = ArgumentArity.ExactlyOne, - IsRequired = false, - }; - - Option pidOption = new(_aliasesPidOption, () => 0, Resources.AWAKE_CMD_HELP_PID_OPTION) - { - Arity = ArgumentArity.ZeroOrOne, - IsRequired = false, - }; - - Option expireAtOption = new(_aliasesExpireAtOption, () => string.Empty, Resources.AWAKE_CMD_HELP_EXPIRE_AT_OPTION) - { - Arity = ArgumentArity.ZeroOrOne, - IsRequired = false, - }; - - Option parentPidOption = new(_aliasesParentPidOption, () => false, Resources.AWAKE_CMD_PARENT_PID_OPTION) - { - Arity = ArgumentArity.ZeroOrOne, - IsRequired = false, - }; - - timeOption.AddValidator(result => - { - if (result.Tokens.Count != 0 && !uint.TryParse(result.Tokens[0].Value, out _)) - { - string errorMessage = $"Interval in --time-limit could not be parsed correctly. Check that the value is valid and doesn't exceed 4,294,967,295. Value used: {result.Tokens[0].Value}."; - Logger.LogError(errorMessage); - result.ErrorMessage = errorMessage; - } - }); - - pidOption.AddValidator(result => - { - if (result.Tokens.Count == 0) - { - return; - } - - string tokenValue = result.Tokens[0].Value; - - if (!int.TryParse(tokenValue, out int parsed)) - { - string errorMessage = $"PID value in --pid could not be parsed correctly. Check that the value is valid and falls within the boundaries of Windows PID process limits. Value used: {tokenValue}."; - Logger.LogError(errorMessage); - result.ErrorMessage = errorMessage; - return; - } - - if (parsed <= 0) - { - string errorMessage = $"PID value in --pid must be a positive integer. Value used: {parsed}."; - Logger.LogError(errorMessage); - result.ErrorMessage = errorMessage; - return; - } - - // Process existence check. (We also re-validate just before binding.) - if (!ProcessExists(parsed)) - { - string errorMessage = $"No running process found with an ID of {parsed}."; - Logger.LogError(errorMessage); - result.ErrorMessage = errorMessage; - } - }); - - expireAtOption.AddValidator(result => - { - if (result.Tokens.Count != 0 && !DateTimeOffset.TryParse(result.Tokens[0].Value, out _)) - { - string errorMessage = $"Date and time value in --expire-at could not be parsed correctly. Check that the value is valid date and time. Refer to https://aka.ms/powertoys/awake for format examples. Value used: {result.Tokens[0].Value}."; - Logger.LogError(errorMessage); - result.ErrorMessage = errorMessage; - } - }); - - RootCommand? rootCommand = - [ - configOption, - displayOption, - timeOption, - pidOption, - expireAtOption, - parentPidOption, - ]; - - rootCommand.Description = Core.Constants.AppName; - rootCommand.SetHandler(HandleCommandLineArguments, configOption, displayOption, timeOption, pidOption, expireAtOption, parentPidOption); - - return rootCommand.InvokeAsync(args).Result; + return await rootCommand.InvokeAsync(args); } } } + private static RootCommand BuildRootCommand() + { + Logger.LogInfo("Parsing parameters..."); + + Option configOption = new(_aliasesConfigOption, () => false, Resources.AWAKE_CMD_HELP_CONFIG_OPTION) + { + Arity = ArgumentArity.ZeroOrOne, + IsRequired = false, + }; + + Option displayOption = new(_aliasesDisplayOption, () => true, Resources.AWAKE_CMD_HELP_DISPLAY_OPTION) + { + Arity = ArgumentArity.ZeroOrOne, + IsRequired = false, + }; + + Option timeOption = new(_aliasesTimeOption, () => 0, Resources.AWAKE_CMD_HELP_TIME_OPTION) + { + Arity = ArgumentArity.ExactlyOne, + IsRequired = false, + }; + + Option pidOption = new(_aliasesPidOption, () => 0, Resources.AWAKE_CMD_HELP_PID_OPTION) + { + Arity = ArgumentArity.ZeroOrOne, + IsRequired = false, + }; + + Option expireAtOption = new(_aliasesExpireAtOption, () => string.Empty, Resources.AWAKE_CMD_HELP_EXPIRE_AT_OPTION) + { + Arity = ArgumentArity.ZeroOrOne, + IsRequired = false, + }; + + Option parentPidOption = new(_aliasesParentPidOption, () => false, Resources.AWAKE_CMD_PARENT_PID_OPTION) + { + Arity = ArgumentArity.ZeroOrOne, + IsRequired = false, + }; + + timeOption.AddValidator(result => + { + if (result.Tokens.Count != 0 && !uint.TryParse(result.Tokens[0].Value, out _)) + { + string errorMessage = $"Interval in --time-limit could not be parsed correctly. Check that the value is valid and doesn't exceed 4,294,967,295. Value used: {result.Tokens[0].Value}."; + Logger.LogError(errorMessage); + result.ErrorMessage = errorMessage; + } + }); + + pidOption.AddValidator(result => + { + if (result.Tokens.Count != 0 && !int.TryParse(result.Tokens[0].Value, out _)) + { + string errorMessage = $"PID value in --pid could not be parsed correctly. Check that the value is valid and falls within the boundaries of Windows PID process limits. Value used: {result.Tokens[0].Value}."; + Logger.LogError(errorMessage); + result.ErrorMessage = errorMessage; + } + }); + + expireAtOption.AddValidator(result => + { + if (result.Tokens.Count != 0 && !DateTimeOffset.TryParse(result.Tokens[0].Value, out _)) + { + string errorMessage = $"Date and time value in --expire-at could not be parsed correctly. Check that the value is valid date and time. Refer to https://aka.ms/powertoys/awake for format examples. Value used: {result.Tokens[0].Value}."; + Logger.LogError(errorMessage); + result.ErrorMessage = errorMessage; + } + }); + + RootCommand? rootCommand = + [ + configOption, + displayOption, + timeOption, + pidOption, + expireAtOption, + parentPidOption, + ]; + + rootCommand.Description = Core.Constants.AppName; + rootCommand.SetHandler(HandleCommandLineArguments, configOption, displayOption, timeOption, pidOption, expireAtOption, parentPidOption); + + return rootCommand; + } + private static void AwakeUnhandledExceptionCatcher(object sender, UnhandledExceptionEventArgs e) { if (e.ExceptionObject is Exception exception) @@ -264,6 +263,7 @@ namespace Awake if (pid == 0 && !useParentPid) { Logger.LogInfo("No PID specified. Allocating console..."); + Bridge.FreeConsole(); AllocateLocalConsole(); } else