diff --git a/PowerToys_Awake_Documentation.md b/PowerToys_Awake_Documentation.md deleted file mode 100644 index a1865fd4f4..0000000000 --- a/PowerToys_Awake_Documentation.md +++ /dev/null @@ -1,350 +0,0 @@ -# PowerToys Awake - Comprehensive Documentation - -## Table of Contents -1. [Overview](#overview) -2. [Key Features](#key-features) -3. [Installation](#installation) -4. [Command Line Usage](#command-line-usage) -5. [GUI Usage](#gui-usage) -6. [Operating Modes](#operating-modes) -7. [Configuration File](#configuration-file) -8. [Examples](#examples) -9. [Advanced Usage](#advanced-usage) -10. [Troubleshooting](#troubleshooting) -11. [Technical Details](#technical-details) - -## Overview - -PowerToys Awake is a utility designed to keep your computer awake without permanently modifying system power settings. It prevents the computer from sleeping and can optionally keep the monitor on, providing a convenient alternative to changing system power configurations. - -**Application Name**: `Awake.exe` (part of PowerToys) -**Full Name**: PowerToys Awake -**Build Version**: TILLSON_11272024 - -## Key Features - -- **Temporary Override**: Prevents system sleep without permanent power setting changes -- **Display Control**: Option to keep monitor on or allow it to turn off -- **Multiple Modes**: Support for indefinite, timed, expirable, and passive modes -- **Command Line Interface**: Full programmatic control via command-line parameters -- **Process Binding**: Bind Awake to another process's lifecycle -- **System Tray Integration**: Easy access through Windows system tray -- **PowerToys Integration**: Seamless integration with PowerToys settings - -## Installation - -Awake is included as part of Microsoft PowerToys. Install PowerToys from: -- Microsoft Store -- GitHub Releases: https://github.com/microsoft/PowerToys/releases -- Windows Package Manager: `winget install Microsoft.PowerToys` - -## Command Line Usage - -### Basic Syntax -``` -Awake.exe [OPTIONS] -``` - -### Available Options - -| Option | Short | Description | Type | Default | -|--------|-------|-------------|------|---------| -| `--use-pt-config` | `-c` | Use PowerToys configuration file for managing state | Boolean | false | -| `--display-on` | `-d` | Keep the display awake | Boolean | true | -| `--time-limit` | `-t` | Time interval in seconds to keep computer awake | Integer | 0 | -| `--pid` | `-p` | Bind to a specific process ID | Integer | 0 | -| `--expire-at` | `-e` | Expire at specific date/time | DateTime String | - | -| `--use-parent-pid` | `-u` | Bind to parent process | Boolean | false | - -### Parameter Details - -#### `--use-pt-config` / `-c` -- **Purpose**: Use PowerToys configuration file for state management -- **Behavior**: When enabled, ignores other command-line parameters -- **Usage**: `Awake.exe -c` - -#### `--display-on` / `-d` -- **Purpose**: Controls whether the display stays on -- **Values**: `true` (keep display on) or `false` (allow display to turn off) -- **Usage**: `Awake.exe -d false` - -#### `--time-limit` / `-t` -- **Purpose**: Keep computer awake for specified seconds -- **Range**: 0 to 4,294,967,295 seconds -- **Note**: 0 means indefinite -- **Usage**: `Awake.exe -t 3600` (1 hour) - -#### `--pid` / `-p` -- **Purpose**: Bind Awake to another process -- **Behavior**: Awake terminates when the target process ends -- **Usage**: `Awake.exe -p 1234` - -#### `--expire-at` / `-e` -- **Purpose**: Set expiration date and time -- **Format**: ISO 8601 date/time format -- **Usage**: `Awake.exe -e "2025-07-07T15:30:00"` - -#### `--use-parent-pid` / `-u` -- **Purpose**: Bind to the parent process of Awake -- **Behavior**: Automatically determines parent PID -- **Usage**: `Awake.exe -u` - -## GUI Usage - -Access Awake through the PowerToys system tray icon: - -1. **Right-click** the PowerToys tray icon -2. **Navigate** to Awake submenu -3. **Select** desired mode: - - Off (Passive) - - Keep awake indefinitely - - Keep awake for interval - - Keep awake until expiration - -### PowerToys Settings - -Open PowerToys Settings → Awake to configure: -- **Enable/Disable** Awake module -- **Mode Selection**: Passive, Indefinite, Timed, Expirable -- **Display Settings**: Keep screen on/off -- **Time Configuration**: Hours and minutes for timed mode -- **Expiration Settings**: Date and time for expirable mode - -## Operating Modes - -### 1. Passive Mode (`PASSIVE`) -- **Description**: Uses system's default power plan -- **Command**: `Awake.exe -c` (with passive mode in config) -- **Behavior**: No keep-awake functionality active - -### 2. Indefinite Mode (`INDEFINITE`) -- **Description**: Keeps computer awake indefinitely -- **Command**: `Awake.exe -t 0` or `Awake.exe` (default) -- **Behavior**: Prevents sleep until manually stopped - -### 3. Timed Mode (`TIMED`) -- **Description**: Keeps computer awake for specified duration -- **Command**: `Awake.exe -t ` -- **Behavior**: Automatically returns to passive mode after timeout - -### 4. Expirable Mode (`EXPIRABLE`) -- **Description**: Keeps computer awake until specific date/time -- **Command**: `Awake.exe -e "YYYY-MM-DDTHH:MM:SS"` -- **Behavior**: Automatically returns to passive mode at expiration - -## Configuration File - -When using `--use-pt-config`, Awake reads settings from PowerToys configuration: - -```json -{ - "properties": { - "keepDisplayOn": true, - "mode": 1, - "intervalHours": 2, - "intervalMinutes": 30, - "expirationDateTime": "2025-07-07T15:30:00-07:00", - "customTrayTimes": { - "30 minutes": 1800, - "1 hour": 3600, - "2 hours": 7200 - } - }, - "name": "Awake", - "version": "1.0" -} -``` - -### Mode Values -- `0`: PASSIVE -- `1`: INDEFINITE -- `2`: TIMED -- `3`: EXPIRABLE - -## Examples - -### Basic Usage - -```powershell -# Keep computer awake indefinitely with display on -Awake.exe - -# Keep computer awake for 1 hour (3600 seconds) -Awake.exe -t 3600 - -# Keep computer awake for 30 minutes without display -Awake.exe -t 1800 -d false - -# Keep computer awake until specific time -Awake.exe -e "2025-07-07T17:00:00" - -# Use PowerToys configuration -Awake.exe --use-pt-config -``` - -### Process Binding - -```powershell -# Bind to specific process ID -Awake.exe -p 1234 - -# Bind to parent process -Awake.exe -u - -# Bind to PowerToys runner with display control -Awake.exe -p 5678 -d true -``` - -### Advanced Scenarios - -```powershell -# Long-running development session (8 hours) -Awake.exe -t 28800 -d false - -# Presentation mode (keep display on indefinitely) -Awake.exe -t 0 -d true - -# Overnight process (expire at 8 AM next day) -Awake.exe -e "2025-07-08T08:00:00" - -# Bind to Visual Studio process -$vsProcess = Get-Process "devenv" | Select-Object -First 1 -Awake.exe -p $vsProcess.Id -``` - -## Advanced Usage - -### Custom Tray Times - -Configure custom time shortcuts in PowerToys settings: - -```json -"customTrayTimes": { - "Quick break": 900, // 15 minutes - "Lunch break": 3600, // 1 hour - "Meeting": 5400, // 1.5 hours - "Long task": 14400 // 4 hours -} -``` - -### Integration with Scripts - -```powershell -# PowerShell script to start/stop Awake -function Start-AwakeSession { - param( - [int]$Hours = 0, - [int]$Minutes = 30, - [bool]$KeepDisplay = $true - ) - - $seconds = ($Hours * 3600) + ($Minutes * 60) - $displayArg = if ($KeepDisplay) { "true" } else { "false" } - - Start-Process "Awake.exe" -ArgumentList "-t", $seconds, "-d", $displayArg -} - -# Usage -Start-AwakeSession -Hours 2 -Minutes 15 -KeepDisplay $false -``` - -### Batch File Examples - -```batch -@echo off -REM Quick 30-minute session -"C:\Program Files\PowerToys\Awake.exe" -t 1800 - -REM All-day work session until 6 PM -"C:\Program Files\PowerToys\Awake.exe" -e "2025-07-07T18:00:00" - -REM Bind to current PowerShell session -for /f "tokens=2" %%i in ('tasklist /fi "imagename eq powershell.exe" /fo csv ^| findstr /v "PID"') do ( - "C:\Program Files\PowerToys\Awake.exe" -p %%i - goto :done -) -:done -``` - -## Troubleshooting - -### Common Issues - -#### 1. Awake Already Running -**Error**: "PowerToys.Awake is already running! Exiting the application." -**Solution**: Only one instance can run at a time. Close existing instance first. - -#### 2. Invalid Time Limit -**Error**: Time limit parsing error -**Solution**: Ensure value is between 0 and 4,294,967,295 seconds. - -#### 3. Invalid PID -**Error**: PID parsing error -**Solution**: Verify the process ID exists and is valid. - -#### 4. Invalid Date Format -**Error**: Date/time parsing error -**Solution**: Use ISO 8601 format: "YYYY-MM-DDTHH:MM:SS" - -#### 5. Group Policy Restrictions -**Error**: "Group policy setting disables the tool" -**Solution**: Contact system administrator to enable PowerToys Awake. - -### Debug Information - -Awake logs information to: -- **Location**: `%LOCALAPPDATA%\Microsoft\PowerToys\Awake\Logs` -- **File Format**: Timestamped log files -- **Content**: Startup, mode changes, errors, exit events - -### System Requirements - -- **OS**: Windows 10 version 2004 (build 19041) or later -- **Architecture**: x64, ARM64 -- **Dependencies**: .NET runtime (included with PowerToys) -- **Permissions**: Standard user (no administrator required for basic functionality) - -## Technical Details - -### Power Management - -Awake uses Windows Power Management APIs: -- **SetThreadExecutionState**: Prevents system sleep -- **ES_CONTINUOUS**: Maintains execution state -- **ES_SYSTEM_REQUIRED**: Prevents system sleep -- **ES_DISPLAY_REQUIRED**: Prevents display sleep - -### System Integration - -- **Mutex**: `PowerToys.Awake` prevents multiple instances -- **Event Handling**: `AwakeExitEvent` for clean shutdown -- **Tray Icons**: Different icons for each mode -- **File Watcher**: Monitors configuration file changes - -### Exit Conditions - -Awake terminates when: -1. Manual exit via tray menu or Ctrl+C -2. Time limit reached (timed mode) -3. Expiration time reached (expirable mode) -4. Bound process terminates (PID binding) -5. PowerToys shutdown signal -6. System shutdown/restart - -### Performance Impact - -- **CPU Usage**: Minimal (~0% when idle) -- **Memory Usage**: ~10-20 MB -- **Battery Impact**: Prevents sleep-related power savings -- **Network**: No network activity required - -## Version Information - -- **Current Build**: TILLSON_11272024 -- **Assembly Version**: Retrieved at runtime -- **PowerToys Integration**: Full integration with PowerToys settings -- **Telemetry**: Basic usage telemetry (configurable in PowerToys settings) - ---- - -*For the latest updates and documentation, visit the [PowerToys GitHub repository](https://github.com/microsoft/PowerToys).* diff --git a/deps/cziplib b/deps/cziplib new file mode 160000 index 0000000000..81314fff0a --- /dev/null +++ b/deps/cziplib @@ -0,0 +1 @@ +Subproject commit 81314fff0a882b72a9ad321e7a3311660125b56e diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp index c7d22d474f..04e70bded6 100644 --- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp +++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp @@ -901,6 +901,60 @@ public: { return m_enabled; } + + virtual std::wstring describe() override + { + try + { + auto baseDescription = PowertoyModuleIface::describe(); + auto descriptionObject = winrt::Windows::Data::Json::JsonObject::Parse(winrt::hstring(baseDescription)); + auto methodsArray = descriptionObject.GetNamedArray(L"methods"); + + winrt::Windows::Data::Json::JsonObject launchMethod; + launchMethod.SetNamedValue(L"name", winrt::Windows::Data::Json::JsonValue::CreateStringValue(L"launch")); + launchMethod.SetNamedValue(L"description", winrt::Windows::Data::Json::JsonValue::CreateStringValue(L"Open Advanced Paste UI")); + methodsArray.Append(launchMethod); + + return descriptionObject.Stringify().c_str(); + } + catch (...) + { + return PowertoyModuleIface::describe(); + } + } + + virtual std::wstring invoke(const wchar_t* method, const wchar_t* jsonParams) override + { + if (method != nullptr && wcscmp(method, L"launch") == 0) + { + if (!is_enabled()) + { + try + { + enable(); + } + catch (...) + { + return L"{\"ok\":false,\"error\":\"EnableFailed\"}"; + } + } + + try + { + m_process_manager.start(); + m_process_manager.bring_to_front(); + m_process_manager.send_message(CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_MESSAGE); + Trace::AdvancedPaste_Invoked(L"AdvancedPasteUI"); + return L"{\"ok\":true}"; + } + catch (...) + { + return L"{\"ok\":false,\"error\":\"LaunchFailed\"}"; + } + } + + return PowertoyModuleIface::invoke(method, jsonParams); + } }; extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj b/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj index 261cfab1e6..8dafe9d5e1 100644 --- a/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj +++ b/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj @@ -169,7 +169,10 @@ ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories) - $(CoreLibraryDependencies);%(AdditionalDependencies);advapi32.lib + $(CoreLibraryDependencies);%(AdditionalDependencies);advapi32.lib;Shlwapi.lib + $(CoreLibraryDependencies);%(AdditionalDependencies);advapi32.lib;Shlwapi.lib + $(CoreLibraryDependencies);%(AdditionalDependencies);advapi32.lib;Shlwapi.lib + $(CoreLibraryDependencies);%(AdditionalDependencies);advapi32.lib;Shlwapi.lib @@ -222,4 +225,4 @@ - \ No newline at end of file + diff --git a/src/modules/cmdpal/doc/powertoys-commandpalette-extension.md b/src/modules/cmdpal/doc/powertoys-commandpalette-extension.md index cc0ceed856..eb14254955 100644 --- a/src/modules/cmdpal/doc/powertoys-commandpalette-extension.md +++ b/src/modules/cmdpal/doc/powertoys-commandpalette-extension.md @@ -305,6 +305,8 @@ typedef const char* (__stdcall *describe_fn)(void); __declspec(dllexport) int __stdcall PT_GetModule(IPTModule** out); ``` +PowerToys ships a helper implementation in `PowertoyModuleIface`. Each module inherits default `describe` / `invoke` implementations so that, at a minimum, a `navigateToSettings` verb is exposed. Modules can override those members to add richer metadata or behaviors, but no module can opt out of providing at least the settings deep link. + 9.2 Method routing * Module receives method and params JSON; validate and route to a function: @@ -858,4 +860,4 @@ Each module exposes a capabilities blob via Describe(): ] } } -``` \ No newline at end of file +``` diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Classes/PowerToysModuleEntry.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Classes/PowerToysModuleEntry.cs index 8d7e999cb3..e37f69a975 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Classes/PowerToysModuleEntry.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Classes/PowerToysModuleEntry.cs @@ -2,6 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.CmdPal.Ext.PowerToys.Helper; using Microsoft.CommandPalette.Extensions.Toolkit; using static Common.UI.SettingsDeepLink; @@ -13,6 +14,12 @@ internal sealed class PowerToysModuleEntry public void NavigateToSettingsPage() { + var moduleKey = Module.ModuleKey(); + if (PowerToysRpcClient.TryInvoke(moduleKey, "navigateToSettings")) + { + return; + } + OpenSettings(Module); } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helper/PowerToysResourcesHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helper/PowerToysResourcesHelper.cs index ecf7d6d537..a207a6e2fd 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helper/PowerToysResourcesHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helper/PowerToysResourcesHelper.cs @@ -97,4 +97,44 @@ public static class PowerToysResourcesHelper _ => throw new NotImplementedException(), }; } + + public static string ModuleKey(this SettingsWindow module) + { + return module switch + { + SettingsWindow.Dashboard => "Dashboard", + SettingsWindow.Overview => "Overview", + SettingsWindow.AlwaysOnTop => "AlwaysOnTop", + SettingsWindow.Awake => "Awake", + SettingsWindow.ColorPicker => "ColorPicker", + SettingsWindow.CmdNotFound => "CmdNotFound", + SettingsWindow.LightSwitch => "LightSwitch", + SettingsWindow.FancyZones => "FancyZones", + SettingsWindow.FileLocksmith => "FileLocksmith", + SettingsWindow.Run => "Run", + SettingsWindow.ImageResizer => "ImageResizer", + SettingsWindow.KBM => "KBM", + SettingsWindow.MouseUtils => "MouseUtils", + SettingsWindow.MouseWithoutBorders => "MouseWithoutBorders", + SettingsWindow.Peek => "Peek", + SettingsWindow.PowerAccent => "PowerAccent", + SettingsWindow.PowerLauncher => "PowerLauncher", + SettingsWindow.PowerPreview => "PowerPreview", + SettingsWindow.PowerRename => "PowerRename", + SettingsWindow.FileExplorer => "FileExplorer", + SettingsWindow.ShortcutGuide => "ShortcutGuide", + SettingsWindow.Hosts => "Hosts", + SettingsWindow.MeasureTool => "MeasureTool", + SettingsWindow.PowerOCR => "PowerOcr", + SettingsWindow.Workspaces => "Workspaces", + SettingsWindow.RegistryPreview => "RegistryPreview", + SettingsWindow.CropAndLock => "CropAndLock", + SettingsWindow.EnvironmentVariables => "EnvironmentVariables", + SettingsWindow.AdvancedPaste => "AdvancedPaste", + SettingsWindow.NewPlus => "NewPlus", + SettingsWindow.CmdPal => "CmdPal", + SettingsWindow.ZoomIt => "ZoomIt", + _ => throw new NotImplementedException(), + }; + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helper/PowerToysRpcClient.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helper/PowerToysRpcClient.cs new file mode 100644 index 0000000000..11bd0b0273 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helper/PowerToysRpcClient.cs @@ -0,0 +1,92 @@ +// 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.IO.Pipes; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.CmdPal.Ext.PowerToys.Helper; + +internal static class PowerToysRpcClient +{ + private const string PipeName = "PowerToys.CmdPal.Rpc"; + + public static bool TryInvoke(string module, string method, object? parameters = null, int timeoutMs = 5000) + { + var request = new RpcRequest + { + Module = module, + Method = method, + Parameters = parameters ?? new { }, + TimeoutMs = timeoutMs, + }; + + try + { + using var pipe = new NamedPipeClientStream(".", PipeName, PipeDirection.InOut, PipeOptions.None); + pipe.Connect(timeoutMs); + + var payload = JsonSerializer.SerializeToUtf8Bytes(request, PowerToysRpcClientContext.Default.RpcRequest); + using (var writer = new BinaryWriter(pipe, Encoding.UTF8, leaveOpen: true)) + { + writer.Write(payload.Length); + writer.Flush(); + } + + pipe.Write(payload, 0, payload.Length); + pipe.Flush(); + + using var reader = new BinaryReader(pipe, Encoding.UTF8, leaveOpen: true); + var responseLength = reader.ReadInt32(); + if (responseLength <= 0) + { + return false; + } + + var buffer = new byte[responseLength]; + var totalRead = 0; + while (totalRead < responseLength) + { + var read = pipe.Read(buffer, totalRead, responseLength - totalRead); + if (read == 0) + { + return false; + } + + totalRead += read; + } + + using var document = JsonDocument.Parse(buffer); + return document.RootElement.TryGetProperty("ok", out var okProperty) && okProperty.GetBoolean(); + } + catch + { + return false; + } + } + + internal sealed class RpcRequest + { + [JsonPropertyName("version")] + public string Version { get; init; } = "1.0"; + + [JsonPropertyName("id")] + public string Id { get; init; } = Guid.NewGuid().ToString(); + + [JsonPropertyName("module")] + public string Module { get; init; } = string.Empty; + + [JsonPropertyName("method")] + public string Method { get; init; } = string.Empty; + + [JsonPropertyName("params")] + public object Parameters { get; init; } = new { }; + + [JsonPropertyName("timeoutMs")] + public int TimeoutMs { get; init; } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helper/PowerToysRpcClientContext.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helper/PowerToysRpcClientContext.cs new file mode 100644 index 0000000000..f2513ef231 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helper/PowerToysRpcClientContext.cs @@ -0,0 +1,13 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.CmdPal.Ext.PowerToys.Helper; + +[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Default)] +[JsonSerializable(typeof(PowerToysRpcClient.RpcRequest))] +internal sealed partial class PowerToysRpcClientContext : JsonSerializerContext +{ +} diff --git a/src/modules/cmdpal/ext/PowerToysCommandPaletteExtension/PowerToysCommandPaletteExtension.csproj b/src/modules/cmdpal/ext/PowerToysCommandPaletteExtension/PowerToysCommandPaletteExtension.csproj index 13dbdfcbff..603c89ed39 100644 --- a/src/modules/cmdpal/ext/PowerToysCommandPaletteExtension/PowerToysCommandPaletteExtension.csproj +++ b/src/modules/cmdpal/ext/PowerToysCommandPaletteExtension/PowerToysCommandPaletteExtension.csproj @@ -20,8 +20,6 @@ - - Assets\SplashScreen.scale-200.png diff --git a/src/modules/interface/powertoy_module_interface.h b/src/modules/interface/powertoy_module_interface.h index b88763d1a3..5104e949d9 100644 --- a/src/modules/interface/powertoy_module_interface.h +++ b/src/modules/interface/powertoy_module_interface.h @@ -1,7 +1,15 @@ #pragma once #include +#include +#include +#include +#include +#include +#include + #include +#include /* DLL Interface for PowerToys. The powertoy_create() (see below) must return @@ -37,6 +45,13 @@ class PowertoyModuleIface { public: + struct CmdPalCommand + { + std::wstring name; + std::wstring method; + std::wstring description; + }; + /* Describes a hotkey which can trigger an action in the PowerToy */ struct Hotkey { @@ -155,6 +170,86 @@ public: return powertoys_gpo::gpo_rule_configured_not_configured; } + virtual std::wstring describe() + { + auto moduleName = std::wstring(get_name()); + auto moduleKey = std::wstring(get_key()); + std::wstring description = L"{\"name\":\"" + moduleKey + L"\","; + description += L"\"displayName\":\"" + moduleName + L"\","; + description += L"\"methods\":["; + description += L"{\"name\":\"navigateToSettings\",\"description\":\"Open " + moduleName + L" settings\"},"; + description += L"{\"name\":\"enable\",\"description\":\"Enable the module\"},"; + description += L"{\"name\":\"disable\",\"description\":\"Disable the module\"}"; + description += L"]}"; + return description; + } + + virtual std::wstring invoke(const wchar_t* method, const wchar_t* /*jsonParams*/) + { + if (method != nullptr && wcscmp(method, L"navigateToSettings") == 0) + { + const auto moduleKey = std::wstring(get_key()); + const auto exePath = get_module_folderpath() + L"\\PowerToys.exe"; + if (!std::filesystem::exists(exePath)) + { + return L"{\"ok\":false,\"error\":\"PowerToysExeNotFound\"}"; + } + + std::wstring args = L"--open-settings=" + moduleKey; + std::wstring commandLine = L"\"" + exePath + L"\" " + args; + std::vector commandLineBuffer(commandLine.begin(), commandLine.end()); + commandLineBuffer.push_back(L'\0'); + + STARTUPINFO startupInfo{}; + startupInfo.cb = sizeof(startupInfo); + PROCESS_INFORMATION processInformation{}; + + if (CreateProcessW(exePath.c_str(), + commandLineBuffer.data(), + nullptr, + nullptr, + FALSE, + 0, + nullptr, + nullptr, + &startupInfo, + &processInformation)) + { + CloseHandle(processInformation.hProcess); + CloseHandle(processInformation.hThread); + return L"{\"ok\":true}"; + } + + return L"{\"ok\":false,\"error\":\"LaunchFailed\"}"; + } + else if (method != nullptr && wcscmp(method, L"enable") == 0) + { + try + { + enable(); + return L"{\"ok\":true,\"result\":{\"enabled\":true}}"; + } + catch (...) + { + return L"{\"ok\":false,\"error\":\"EnableFailed\"}"; + } + } + else if (method != nullptr && wcscmp(method, L"disable") == 0) + { + try + { + disable(); + return L"{\"ok\":true,\"result\":{\"enabled\":false}}"; + } + catch (...) + { + return L"{\"ok\":false,\"error\":\"DisableFailed\"}"; + } + } + + return L"{\"ok\":false,\"error\":\"Method.NotFound\"}"; + } + // Some actions like AdvancedPaste generate new inputs, which we don't want to catch again. // The flag was purposefully chose to not collide with other keyboard manager flags. const static inline ULONG_PTR CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG = 0x110; diff --git a/src/runner/cmdpal_rpc_server.cpp b/src/runner/cmdpal_rpc_server.cpp new file mode 100644 index 0000000000..0f9a035bcb --- /dev/null +++ b/src/runner/cmdpal_rpc_server.cpp @@ -0,0 +1,327 @@ +#include "pch.h" +#include "cmdpal_rpc_server.h" + +#include "powertoy_module.h" +#include + +using namespace winrt::Windows::Data::Json; + +namespace +{ + constexpr wchar_t PIPE_NAME[] = LR"(\\.\pipe\PowerToys.CmdPal.Rpc)"; + + std::string ToUtf8(const winrt::hstring& value) + { + return winrt::to_string(value); + } + + winrt::hstring ToHString(const std::string& value) + { + return winrt::to_hstring(value); + } +} + +CmdPalRpcServer::CmdPalRpcServer() = default; + +CmdPalRpcServer::~CmdPalRpcServer() +{ + Stop(); +} + +void CmdPalRpcServer::Start() +{ + if (m_running.exchange(true)) + { + return; + } + + m_worker = std::thread([this]() { Run(); }); +} + +void CmdPalRpcServer::Stop() +{ + if (!m_running.exchange(false)) + { + return; + } + + // Trigger the server loop to exit by briefly connecting to the pipe. + for (int attempt = 0; attempt < 5; ++attempt) + { + auto pipe = CreateFileW(PIPE_NAME, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); + if (pipe != INVALID_HANDLE_VALUE) + { + CloseHandle(pipe); + break; + } + + auto error = GetLastError(); + if (error == ERROR_FILE_NOT_FOUND) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + else if (error == ERROR_PIPE_BUSY) + { + WaitNamedPipeW(PIPE_NAME, 200); + } + else + { + break; + } + } + + if (m_worker.joinable()) + { + m_worker.join(); + } +} + +void CmdPalRpcServer::Run() +{ + while (m_running.load()) + { + HANDLE pipe = CreateNamedPipeW(PIPE_NAME, + PIPE_ACCESS_DUPLEX, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + PIPE_UNLIMITED_INSTANCES, + 64 * 1024, + 64 * 1024, + 0, + nullptr); + + if (pipe == INVALID_HANDLE_VALUE) + { + Logger::error(L"CmdPalRpcServer: failed to create pipe. Error: {}", GetLastError()); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + continue; + } + + BOOL connected = ConnectNamedPipe(pipe, nullptr) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED); + if (!connected) + { + CloseHandle(pipe); + continue; + } + + HandleClient(pipe); + } +} + +void CmdPalRpcServer::HandleClient(HANDLE pipe) +{ + auto closePipe = wil::scope_exit([pipe]() { + FlushFileBuffers(pipe); + DisconnectNamedPipe(pipe); + CloseHandle(pipe); + }); + + while (m_running.load()) + { + uint32_t length = 0; + DWORD bytesRead = 0; + if (!ReadFile(pipe, &length, sizeof(length), &bytesRead, nullptr) || bytesRead == 0) + { + break; + } + + std::string payload; + payload.resize(length); + DWORD totalRead = 0; + while (totalRead < length) + { + DWORD chunkRead = 0; + if (!ReadFile(pipe, payload.data() + totalRead, length - totalRead, &chunkRead, nullptr) || chunkRead == 0) + { + return; + } + totalRead += chunkRead; + } + + auto response = ProcessMessage(payload); + uint32_t responseLength = static_cast(response.size()); + DWORD bytesWritten = 0; + if (!WriteFile(pipe, &responseLength, sizeof(responseLength), &bytesWritten, nullptr)) + { + break; + } + + if (responseLength > 0) + { + DWORD totalWritten = 0; + while (totalWritten < responseLength) + { + DWORD chunkWritten = 0; + if (!WriteFile(pipe, response.data() + totalWritten, responseLength - totalWritten, &chunkWritten, nullptr)) + { + return; + } + totalWritten += chunkWritten; + } + } + } +} + +std::string CmdPalRpcServer::ProcessMessage(const std::string& message) +{ + try + { + auto request = JsonObject::Parse(ToHString(message)); + std::wstring id; + if (auto idValue = request.TryLookup(L"id")) + { + if (idValue.ValueType() == JsonValueType::String) + { + id = idValue.GetString().c_str(); + } + } + + if (!request.HasKey(L"module") || !request.HasKey(L"method")) + { + return BuildErrorResponse(id, L"Bad.Request", L"Missing module or method"); + } + + auto moduleName = request.Lookup(L"module").GetString(); + auto methodName = request.Lookup(L"method").GetString(); + + if (moduleName == L"core" && methodName == L"listModules") + { + return ListModulesResponse(id); + } + + return ProcessModuleRequest(request, id); + } + catch (...) + { + return BuildErrorResponse(L"", L"Bad.Request", L"Malformed JSON"); + } +} + +std::string CmdPalRpcServer::ProcessModuleRequest(const JsonObject& request, const std::wstring& id) +{ + std::wstring moduleKey = request.Lookup(L"module").GetString().c_str(); + auto methodName = request.Lookup(L"method").GetString(); + + auto& loadedModules = modules(); + auto moduleIt = loadedModules.find(moduleKey); + if (moduleIt == loadedModules.end()) + { + return BuildErrorResponse(id, L"Module.NotFound", L"Requested module is not available"); + } + + std::wstring params = L"{}"; + if (auto paramsValue = request.TryLookup(L"params")) + { + params = paramsValue.Stringify().c_str(); + } + + auto start = std::chrono::steady_clock::now(); + std::wstring moduleResponse; + try + { + moduleResponse = moduleIt->second->invoke(methodName.c_str(), params.c_str()); + } + catch (...) + { + return BuildErrorResponse(id, L"Module.Failure", L"Module threw an exception"); + } + auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); + + JsonObject response; + if (!id.empty()) + { + response.SetNamedValue(L"id", JsonValue::CreateStringValue(id)); + } + response.SetNamedValue(L"elapsedMs", JsonValue::CreateNumberValue(static_cast(elapsed))); + + try + { + auto moduleJsonValue = JsonValue::Parse(winrt::hstring(moduleResponse.c_str())); + bool ok = true; + if (moduleJsonValue.ValueType() == JsonValueType::Object) + { + auto moduleObject = moduleJsonValue.GetObject(); + if (moduleObject.HasKey(L"ok")) + { + ok = moduleObject.GetNamedBoolean(L"ok"); + } + } + + response.SetNamedValue(L"ok", JsonValue::CreateBooleanValue(ok)); + if (ok) + { + response.SetNamedValue(L"result", moduleJsonValue); + } + else if (moduleJsonValue.ValueType() == JsonValueType::Object) + { + auto moduleObject = moduleJsonValue.GetObject(); + if (moduleObject.HasKey(L"error")) + { + response.SetNamedValue(L"error", moduleObject.Lookup(L"error")); + } + else + { + response.SetNamedValue(L"error", moduleJsonValue); + } + } + else + { + response.SetNamedValue(L"error", moduleJsonValue); + } + + return ToUtf8(response.Stringify()); + } + catch (...) + { + return BuildErrorResponse(id, L"Module.Failure", L"Module returned invalid JSON"); + } +} + +std::string CmdPalRpcServer::ListModulesResponse(const std::wstring& id) +{ + JsonObject response; + if (!id.empty()) + { + response.SetNamedValue(L"id", JsonValue::CreateStringValue(id)); + } + response.SetNamedValue(L"ok", JsonValue::CreateBooleanValue(true)); + + JsonArray modulesArray; + for (auto& entry : modules()) + { + try + { + auto describeJson = JsonValue::Parse(winrt::hstring(entry.second->describe())); + if (describeJson.ValueType() == JsonValueType::Object) + { + modulesArray.Append(describeJson.GetObject()); + } + } + catch (...) + { + JsonObject fallback; + fallback.SetNamedValue(L"name", JsonValue::CreateStringValue(entry.first)); + modulesArray.Append(fallback); + } + } + + JsonObject payload; + payload.SetNamedValue(L"modules", modulesArray); + response.SetNamedValue(L"result", payload); + + return ToUtf8(response.Stringify()); +} + +std::string CmdPalRpcServer::BuildErrorResponse(const std::wstring& id, const std::wstring_view code, const std::wstring_view message) +{ + JsonObject response; + if (!id.empty()) + { + response.SetNamedValue(L"id", JsonValue::CreateStringValue(id)); + } + response.SetNamedValue(L"ok", JsonValue::CreateBooleanValue(false)); + JsonObject error; + error.SetNamedValue(L"code", JsonValue::CreateStringValue(winrt::hstring(code.data(), static_cast(code.size())))); + error.SetNamedValue(L"message", JsonValue::CreateStringValue(winrt::hstring(message.data(), static_cast(message.size())))); + response.SetNamedValue(L"error", error); + return ToUtf8(response.Stringify()); +} diff --git a/src/runner/cmdpal_rpc_server.h b/src/runner/cmdpal_rpc_server.h new file mode 100644 index 0000000000..6327ff8e6a --- /dev/null +++ b/src/runner/cmdpal_rpc_server.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +#include +#include + +class CmdPalRpcServer +{ +public: + CmdPalRpcServer(); + ~CmdPalRpcServer(); + + CmdPalRpcServer(const CmdPalRpcServer&) = delete; + CmdPalRpcServer& operator=(const CmdPalRpcServer&) = delete; + + void Start(); + void Stop(); + +private: + void Run(); + void HandleClient(HANDLE pipe); + std::string ProcessMessage(const std::string& message); + std::string ProcessModuleRequest(const winrt::Windows::Data::Json::JsonObject& request, const std::wstring& id); + std::string ListModulesResponse(const std::wstring& id); + std::string BuildErrorResponse(const std::wstring& id, const std::wstring_view code, const std::wstring_view message); + + std::atomic_bool m_running{ false }; + std::thread m_worker; +}; diff --git a/src/runner/main.cpp b/src/runner/main.cpp index c20293f9ed..5cdb982322 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -35,7 +35,8 @@ #include #include #include "centralized_kb_hook.h" -#include "centralized_hotkeys.h" +#include "centralized_hotkeys.h" +#include "cmdpal_rpc_server.h" #if _DEBUG && _WIN64 #include "unhandled_exception_handler.h" @@ -93,10 +94,10 @@ void open_menu_from_another_instance(std::optional settings_window) SetForegroundWindow(hwnd_main); // Bring the settings window to the front } -int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow, bool openOobe, bool openScoobe, bool showRestartNotificationAfterUpdate) -{ - Logger::info("Runner is starting. Elevated={} openOobe={} openScoobe={} showRestartNotificationAfterUpdate={}", isProcessElevated, openOobe, openScoobe, showRestartNotificationAfterUpdate); - DPIAware::EnableDPIAwarenessForThisProcess(); +int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow, bool openOobe, bool openScoobe, bool showRestartNotificationAfterUpdate) +{ + Logger::info("Runner is starting. Elevated={} openOobe={} openScoobe={} showRestartNotificationAfterUpdate={}", isProcessElevated, openOobe, openScoobe, showRestartNotificationAfterUpdate); + DPIAware::EnableDPIAwarenessForThisProcess(); #if _DEBUG && _WIN64 //Global error handlers to diagnose errors. @@ -104,9 +105,10 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow //init_global_error_handlers(); #endif Trace::RegisterProvider(); - start_tray_icon(isProcessElevated); - set_tray_icon_visible(get_general_settings().showSystemTrayIcon); - CentralizedKeyboardHook::Start(); + start_tray_icon(isProcessElevated); + set_tray_icon_visible(get_general_settings().showSystemTrayIcon); + CentralizedKeyboardHook::Start(); + CmdPalRpcServer cmdPalRpcServer; int result = -1; try @@ -207,8 +209,9 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow #endif } } - // Start initial powertoys - start_enabled_powertoys(); + // Start initial powertoys + start_enabled_powertoys(); + cmdPalRpcServer.Start(); std::wstring product_version = get_product_version(); Trace::EventLaunch(product_version, isProcessElevated); PTSettingsHelper::save_last_version_run(product_version); diff --git a/src/runner/runner.vcxproj b/src/runner/runner.vcxproj index 1eae5a3573..755fd21f33 100644 --- a/src/runner/runner.vcxproj +++ b/src/runner/runner.vcxproj @@ -65,7 +65,8 @@ Create - + + @@ -87,7 +88,8 @@ - + + @@ -175,4 +177,4 @@ - \ No newline at end of file +