This commit is contained in:
kaitao-ms
2025-11-19 09:46:59 +08:00
parent 66def1dfd8
commit a3d6a2ee43
15 changed files with 687 additions and 368 deletions

View File

@@ -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 <seconds>`
- **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).*

1
deps/cziplib vendored Submodule

Submodule deps/cziplib added at 81314fff0a

View File

@@ -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()

View File

@@ -169,7 +169,10 @@
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(CoreLibraryDependencies);%(AdditionalDependencies);advapi32.lib</AdditionalDependencies>
<AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(CoreLibraryDependencies);%(AdditionalDependencies);advapi32.lib;Shlwapi.lib</AdditionalDependencies>
<AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(CoreLibraryDependencies);%(AdditionalDependencies);advapi32.lib;Shlwapi.lib</AdditionalDependencies>
<AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(CoreLibraryDependencies);%(AdditionalDependencies);advapi32.lib;Shlwapi.lib</AdditionalDependencies>
<AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">$(CoreLibraryDependencies);%(AdditionalDependencies);advapi32.lib;Shlwapi.lib</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>

View File

@@ -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:

View File

@@ -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);
}
}

View File

@@ -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(),
};
}
}

View File

@@ -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; }
}
}

View File

@@ -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
{
}

View File

@@ -20,8 +20,6 @@
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<Import Project="..\Microsoft.CmdPal.Ext.PowerToys\PowerToysExtensionAssets.props" />
<ItemGroup>
<Content Include="..\Microsoft.CmdPal.UI\Assets\Stable\SplashScreen.scale-200.png">
<Link>Assets\SplashScreen.scale-200.png</Link>

View File

@@ -1,7 +1,15 @@
#pragma once
#include <compare>
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
#include <cwchar>
#include <windows.h>
#include <common/utils/gpo.h>
#include <common/utils/process_path.h>
/*
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<wchar_t> 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;

View File

@@ -0,0 +1,327 @@
#include "pch.h"
#include "cmdpal_rpc_server.h"
#include "powertoy_module.h"
#include <common/logger/logger.h>
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<uint32_t>(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::milliseconds>(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<double>(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<uint32_t>(code.size()))));
error.SetNamedValue(L"message", JsonValue::CreateStringValue(winrt::hstring(message.data(), static_cast<uint32_t>(message.size()))));
response.SetNamedValue(L"error", error);
return ToUtf8(response.Stringify());
}

View File

@@ -0,0 +1,32 @@
#pragma once
#include <atomic>
#include <string>
#include <thread>
#include <winrt/Windows.Data.Json.h>
#include <Windows.h>
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;
};

View File

@@ -36,6 +36,7 @@
#include <RestartManager.h>
#include "centralized_kb_hook.h"
#include "centralized_hotkeys.h"
#include "cmdpal_rpc_server.h"
#if _DEBUG && _WIN64
#include "unhandled_exception_handler.h"
@@ -107,6 +108,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
start_tray_icon(isProcessElevated);
set_tray_icon_visible(get_general_settings().showSystemTrayIcon);
CentralizedKeyboardHook::Start();
CmdPalRpcServer cmdPalRpcServer;
int result = -1;
try
@@ -209,6 +211,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
}
// 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);

View File

@@ -66,6 +66,7 @@
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="powertoy_module.cpp" />
<ClCompile Include="cmdpal_rpc_server.cpp" />
<ClCompile Include="main.cpp" />
<ClCompile Include="restart_elevated.cpp" />
<ClCompile Include="centralized_kb_hook.cpp" />
@@ -88,6 +89,7 @@
<ClInclude Include="settings_telemetry.h" />
<ClInclude Include="UpdateUtils.h" />
<ClInclude Include="powertoy_module.h" />
<ClInclude Include="cmdpal_rpc_server.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="restart_elevated.h" />
<ClInclude Include="settings_window.h" />