Compare commits

..

15 Commits

Author SHA1 Message Date
Leilei Zhang
3c55166414 add process 2025-09-22 11:24:17 +08:00
Leilei Zhang
ec136d36ff update description 2025-09-18 16:29:47 +08:00
Leilei Zhang
68e98d62aa update description 2025-09-18 15:40:22 +08:00
Leilei Zhang
6e8142929e add status 2025-09-18 15:21:47 +08:00
Shawn Yuan
55593e5b99 Merge branch 'shawn/comServer' of https://github.com/microsoft/PowerToys into shawn/comServer 2025-09-18 14:20:12 +08:00
Shawn Yuan
8d8d451811 update
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-09-18 14:19:41 +08:00
leileizhang
d06555db93 Leilzh/rot (#41865)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-18 11:39:54 +08:00
Shawn Yuan
baf42abba4 add power management
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-09-17 17:09:01 +08:00
Shawn Yuan
c818859103 update
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-09-17 14:56:40 +08:00
Shawn Yuan
34a06715a8 Merge branch 'shawn/mcpserver' into shawn/comServer 2025-09-17 14:32:55 +08:00
Shawn Yuan
01e7b61efb transit to sqlite
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-09-17 13:56:44 +08:00
Shawn Yuan
be334fa0df added usage status record in awake
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-09-17 10:58:30 +08:00
Leilei Zhang
e4a48b85a1 add cpu 2025-09-16 21:29:50 +08:00
Shawn Yuan
1aeed1699e init
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-09-16 16:36:22 +08:00
Shawn Yuan
804de07b20 init
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-09-15 22:28:24 +08:00
153 changed files with 4553 additions and 4275 deletions

View File

@@ -159,7 +159,6 @@ BUILDARCH
BUILDNUMBER
buildtransitive
builttoroam
BUNDLEINFO
BVal
BValue
byapp
@@ -861,7 +860,6 @@ LOCKTYPE
LOGFONT
LOGFONTW
logon
LOGMSG
LOGPIXELSX
LOGPIXELSY
LOn
@@ -1035,7 +1033,6 @@ MWBEx
MYICON
NAMECHANGE
namespaceanddescendants
Namotion
nao
NCACTIVATE
ncc
@@ -1073,7 +1070,6 @@ NEWPLUSSHELLEXTENSIONWIN
newrow
nicksnettravels
NIF
NJson
NLog
NLSTEXT
NMAKE

View File

@@ -1,19 +0,0 @@
name: Automatic New Issue Deduplication
on:
issues:
types: [opened, reopened]
permissions:
models: read
issues: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
deduplicate:
runs-on: ubuntu-latest
steps:
- name: Run Deduplicate Action
uses: pelikhan/action-genai-issue-dedup@v0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
label_as_duplicate: true

2
.gitignore vendored
View File

@@ -350,9 +350,7 @@ src/common/Telemetry/*.etl
# Generated installer file for Monaco source files.
/installer/PowerToysSetup/MonacoSRC.wxs
/installer/PowerToysSetup/DscResources.wxs
/installer/PowerToysSetupVNext/MonacoSRC.wxs
/installer/PowerToysSetupVNext/DscResources.wxs
# MSBuildCache
/MSBuildCacheLogs/

View File

@@ -55,6 +55,7 @@
"PowerToys.Awake.exe",
"PowerToys.Awake.dll",
"PowerToys.FancyZonesEditor.exe",
"PowerToys.FancyZonesEditor.dll",
"PowerToys.FancyZonesEditorCommon.dll",
@@ -229,10 +230,7 @@
"PowerToys.CmdPalModuleInterface.dll",
"CmdPalKeyboardService.dll",
"*Microsoft.CmdPal.UI_*.msix",
"PowerToys.DSC.dll",
"PowerToys.DSC.exe"
"*Microsoft.CmdPal.UI_*.msix"
],
"SigningInfo": {
"Operations": [
@@ -299,9 +297,6 @@
"msvcp140_1_app.dll",
"msvcp140_2_app.dll",
"msvcp140_app.dll",
"Namotion.Reflection.dll",
"NJsonSchema.Annotations.dll",
"NJsonSchema.dll",
"vcamp140_app.dll",
"vccorlib140_app.dll",
"vcomp140_app.dll",

View File

@@ -1,88 +0,0 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$BuildPlatform,
[Parameter(Mandatory = $true)]
[string]$BuildConfiguration,
[Parameter()]
[string]$RepoRoot = (Get-Location).Path
)
$ErrorActionPreference = 'Stop'
function Resolve-PlatformDirectory {
param(
[string]$Root,
[string]$Platform
)
$normalized = $Platform.Trim()
$candidates = @()
$candidates += Join-Path $Root $normalized
$candidates += Join-Path $Root ($normalized.ToUpperInvariant())
$candidates += Join-Path $Root ($normalized.ToLowerInvariant())
$candidates = $candidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
foreach ($candidate in $candidates) {
if (Test-Path $candidate) {
return $candidate
}
}
return $candidates[0]
}
Write-Host "Repo root: $RepoRoot"
Write-Host "Requested build platform: $BuildPlatform"
Write-Host "Requested configuration: $BuildConfiguration"
# Always use x64 PowerToys.DSC.exe since CI/CD machines are x64
$exePlatform = 'x64'
$exeRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $exePlatform
$exeOutputDir = Join-Path $exeRoot $BuildConfiguration
$exePath = Join-Path $exeOutputDir 'PowerToys.DSC.exe'
Write-Host "Using x64 PowerToys.DSC.exe to generate DSC manifests for $BuildPlatform build"
if (-not (Test-Path $exePath)) {
throw "PowerToys.DSC.exe not found at '$exePath'. Make sure it has been built first."
}
Write-Host "Using PowerToys.DSC.exe at '$exePath'."
# Output DSC manifests to the target build platform directory (x64, ARM64, etc.)
$outputRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $BuildPlatform
if (-not (Test-Path $outputRoot)) {
Write-Host "Creating missing platform output root at '$outputRoot'."
New-Item -Path $outputRoot -ItemType Directory -Force | Out-Null
}
$outputDir = Join-Path $outputRoot $BuildConfiguration
if (-not (Test-Path $outputDir)) {
Write-Host "Creating missing configuration output directory at '$outputDir'."
New-Item -Path $outputDir -ItemType Directory -Force | Out-Null
}
Write-Host "DSC manifests will be generated to: '$outputDir'"
Write-Host "Cleaning previously generated DSC manifest files from '$outputDir'."
Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction SilentlyContinue | Remove-Item -Force
$arguments = @('manifest', '--resource', 'settings', '--outputDir', $outputDir)
Write-Host "Invoking DSC manifest generator: '$exePath' $($arguments -join ' ')"
& $exePath @arguments
if ($LASTEXITCODE -ne 0) {
throw "PowerToys.DSC.exe exited with code $LASTEXITCODE"
}
$generatedFiles = Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop
if ($generatedFiles.Count -eq 0) {
throw "No DSC manifest files were generated in '$outputDir'."
}
Write-Host "Generated $($generatedFiles.Count) DSC manifest file(s):"
foreach ($file in $generatedFiles) {
Write-Host " - $($file.FullName)"
}

View File

@@ -263,23 +263,6 @@ jobs:
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
# Build PowerToys.DSC.exe for ARM64 (x64 uses existing binary from previous build)
- task: VSBuild@1
displayName: Build PowerToys.DSC.exe (x64 for generating manifests)
condition: ne(variables['BuildPlatform'], 'x64')
inputs:
solution: src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj
msbuildArgs: /t:Build /m /restore
platform: x64
configuration: $(BuildConfiguration)
msbuildArchitecture: x64
maximumCpuCount: true
# Generate DSC manifests using PowerToys.DSC.exe
- pwsh: |-
& '.pipelines/generateDscManifests.ps1' -BuildPlatform '$(BuildPlatform)' -BuildConfiguration '$(BuildConfiguration)' -RepoRoot '$(Build.SourcesDirectory)'
displayName: Generate DSC manifests
- task: CopyFiles@2
displayName: Stage SDK/build
inputs:
@@ -428,28 +411,9 @@ jobs:
!**\obj\**
- pwsh: |-
$Packages = Get-ChildItem -Recurse -Filter "Microsoft.CmdPal.UI_*.msix"
Write-Host "Found $($Packages.Count) CmdPal MSIX package(s):"
foreach ($pkg in $Packages) {
Write-Host " - $($pkg.FullName)"
}
if ($Packages.Count -gt 0) {
# Priority: Look for platform-specific MSIX (x64/arm64) first, then fall back to any
$PlatformPackage = $Packages | Where-Object { $_.Name -match "Microsoft\.CmdPal\.UI_.*_(x64|arm64)\.msix$" } | Select-Object -First 1
if ($PlatformPackage) {
$Package = $PlatformPackage
Write-Host "Using platform-specific package: $($Package.FullName)"
} else {
$Package = $Packages | Select-Object -First 1
Write-Host "Using first available package: $($Package.FullName)"
}
$PackageFilename = $Package.FullName
Write-Host "##vso[task.setvariable variable=CmdPalPackagePath]${PackageFilename}"
} else {
Write-Warning "No CmdPal MSIX packages found!"
}
$Package = (Get-ChildItem -Recurse -Filter "Microsoft.CmdPal.UI_*.msix" | Select -First 1)
$PackageFilename = $Package.FullName
Write-Host "##vso[task.setvariable variable=CmdPalPackagePath]${PackageFilename}"
displayName: Locate the CmdPal MSIX
- ${{ if eq(parameters.codeSign, true) }}:

12
.vscode/mcp.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"servers": {
"powertoys-mcp": {
"command": "D:/work/PowerToys/x64/Debug/PowerToys.MCPServer.exe",
"transport": "stdio",
"args": [],
"env": {
"NODE_ENV": "production"
}
}
}
}

View File

@@ -45,7 +45,7 @@
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.8" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.15.0" />
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3179.45" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.8" />
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
@@ -57,15 +57,15 @@
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
-->
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4948" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
<PackageVersion Include="ModelContextProtocol" Version="0.3.0-preview.4" />
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
<!-- Moq to stay below v4.20 due to behavior change. need to be sure fixed -->
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="MSTest" Version="3.8.3" />
<PackageVersion Include="NJsonSchema" Version="11.4.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NLog" Version="5.2.8" />
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />

View File

@@ -1522,7 +1522,6 @@ SOFTWARE.
- ModernWpfUI
- Moq
- MSTest
- NJsonSchema
- NLog
- NLog.Extensions.Logging
- NLog.Schema

View File

@@ -797,12 +797,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E11826E1
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenRuler.UITests", "src\modules\MeasureTool\Tests\ScreenRuler.UITests\ScreenRuler.UITests.csproj", "{66C069F8-C548-4CA6-8CDE-239104D68E88}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "v3", "v3", "{9605B84E-FAC4-477B-B9EC-0753177EE6A8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerToys.DSC", "src\dsc\v3\PowerToys.DSC\PowerToys.DSC.csproj", "{94CDC147-6137-45E9-AEDE-17FF809607C0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerToys.DSC.UnitTests", "src\dsc\v3\PowerToys.DSC.UnitTests\PowerToys.DSC.UnitTests.csproj", "{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Apps.UnitTests\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj", "{E816D7B1-4688-4ECB-97CC-3D8E798F3830}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj", "{E816D7B3-4688-4ECB-97CC-3D8E798F3832}"
@@ -811,6 +805,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSea
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Shell.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Shell.UnitTests\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj", "{E816D7B4-4688-4ECB-97CC-3D8E798F3833}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MCPServer", "MCPServer", "{B637E6DD-FB81-4595-BB9C-01168556EA9E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MCPServer", "src\modules\MCPServer\MCPServer\MCPServer.csproj", "{20CBF173-9E8D-3236-6664-5B9C303794A3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2897,22 +2895,6 @@ Global
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|ARM64.Build.0 = Release|ARM64
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.ActiveCfg = Release|x64
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.Build.0 = Release|x64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|ARM64.ActiveCfg = Debug|ARM64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|ARM64.Build.0 = Debug|ARM64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|x64.ActiveCfg = Debug|x64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|x64.Build.0 = Debug|x64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|ARM64.ActiveCfg = Release|ARM64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|ARM64.Build.0 = Release|ARM64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|x64.ActiveCfg = Release|x64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|x64.Build.0 = Release|x64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|ARM64.ActiveCfg = Debug|ARM64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|ARM64.Build.0 = Debug|ARM64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|x64.ActiveCfg = Debug|x64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|x64.Build.0 = Debug|x64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|ARM64.ActiveCfg = Release|ARM64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|ARM64.Build.0 = Release|ARM64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|x64.ActiveCfg = Release|x64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|x64.Build.0 = Release|x64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.Build.0 = Debug|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.ActiveCfg = Debug|x64
@@ -2945,6 +2927,14 @@ Global
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.ActiveCfg = Release|x64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = Release|x64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|ARM64.ActiveCfg = Debug|ARM64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|ARM64.Build.0 = Debug|ARM64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|x64.ActiveCfg = Debug|x64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|x64.Build.0 = Debug|x64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|ARM64.ActiveCfg = Release|ARM64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|ARM64.Build.0 = Release|ARM64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|x64.ActiveCfg = Release|x64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2954,6 +2944,7 @@ Global
{D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
{9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9} = {264B412F-DB8B-4CF8-A74B-96998B183045}
{1AFB6476-670D-4E80-A464-657E01DFF482} = {557C4636-D7E1-4838-A504-7D19B725EE95}
{1A066C63-64B3-45F8-92FE-664E1CCE8077} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
{89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
@@ -3260,13 +3251,12 @@ Global
{00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E11826E1-76DF-42AC-985C-164CC2EE57A1} = {7AC943C9-52E8-44CF-9083-744D8049667B}
{66C069F8-C548-4CA6-8CDE-239104D68E88} = {E11826E1-76DF-42AC-985C-164CC2EE57A1}
{9605B84E-FAC4-477B-B9EC-0753177EE6A8} = {557C4636-D7E1-4838-A504-7D19B725EE95}
{94CDC147-6137-45E9-AEDE-17FF809607C0} = {9605B84E-FAC4-477B-B9EC-0753177EE6A8}
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78} = {9605B84E-FAC4-477B-B9EC-0753177EE6A8}
{E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{B637E6DD-FB81-4595-BB9C-01168556EA9E} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{20CBF173-9E8D-3236-6664-5B9C303794A3} = {B637E6DD-FB81-4595-BB9C-01168556EA9E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -1,83 +0,0 @@
# Settings resource
Manage the settings for PowerToys modules
## Commands
### ✨ Modules
List all the modules supported by the settings resource.
```shell
PS C:\> PowerToys.DSC.exe modules --resource 'settings'
AdvancedPaste
AlwaysOnTop
App
Awake
ColorPicker
CropAndLock
EnvironmentVariables
FancyZones
FileLocksmith
FindMyMouse
Hosts
ImageResizer
KeyboardManager
MeasureTool
MouseHighlighter
MouseJump
MousePointerCrosshairs
Peek
PowerAccent
PowerOCR
PowerRename
RegistryPreview
ShortcutGuide
Workspaces
ZoomIt
```
### 📄 Get
Get the settings for a specific module.
```shell
PS C:\> PowerToys.DSC.exe get --resource 'settings' --module EnvironmentVariables
{"settings":{"properties":{"LaunchAdministrator":{"value":true}},"name":"EnvironmentVariables","version":"1.0"}}
```
### 🖨️ Export
Export the settings for a specific module.
Settings resource Get and Export operation output states are identical.
```shell
PS C:\> PowerToys.DSC.exe get --resource 'settings' --module EnvironmentVariables
{"settings":{"properties":{"LaunchAdministrator":{"value":true}},"name":"EnvironmentVariables","version":"1.0"}}
```
### 📝 Set
Set the settings for a specific module. This command will update the settings to the specified values.
```shell
PS C:\> PowerToys.DSC.exe set --resource 'settings' --module Awake --input '{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000001-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"}}'
{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000001-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"}}
["settings"]
```
### 🧪 Test
Test the settings for a specific module. This command will check if the current settings match the desired state.
```shell
PS C:\> PowerToys.DSC.exe test --resource 'settings' --module Awake --input '{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000002-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"}}'
{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000001-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"},"_inDesiredState":false}
["settings"]
```
### 🛠️ Schema
Generates the JSON schema for the settings resource of a specific module.
```shell
PS C:\> PowerToys.DSC.exe schema --resource 'settings' --module Awake
{"$schema":"http://json-schema.org/draft-04/schema#","title":"SettingsResourceObjectOfAwakeSettings","type":"object","additionalProperties":false,"required":["settings"],"properties":{"_inDesiredState":{"type":["boolean","null"],"description":"Indicates whether an instance is in the desired state"},"settings":{"description":"The settings content for the module."}}}
PS E:\src\powertoys> PowerToys.DSC.exe schema --resource 'settings' --module Awake | Format-Json
```
### 📦 Manifest
Generates a manifest dsc resource JSON file for the specified module.
- If the module is not specified, it will generate a manifest for all modules.
- If the output directory is not specified, it will print the manifest to the console.
```shell
PS C:\> PowerToys.DSC.exe manifest --resource settings --module 'Awake' --outputDir "C:\manifests"
```

View File

@@ -9,6 +9,13 @@
<Fragment>
<DirectoryRef Id="WinUI3AppsInstallFolder">
<Directory Id="CmdPalInstallFolder" Name="CmdPal">
<Directory Id="CmdPalDepsInstallFolder" Name="Dependencies">
<?if $(sys.BUILDARCH) = x64 ?>
<Directory Id="CmdPalDepsX64InstallFolder" Name="x64" />
<?else ?>
<Directory Id="CmdPalDepsArm64InstallFolder" Name="arm64" />
<?endif ?>
</Directory>
</Directory>
</DirectoryRef>
@@ -26,14 +33,41 @@
</Component>
</DirectoryRef>
<?if $(sys.BUILDARCH) = x64 ?>
<DirectoryRef Id="CmdPalDepsX64InstallFolder" FileSource="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\x64">
<Component Id="Module_CmdPal_Deps" Win64="yes" Guid="C2790FC4-0665-4462-947A-D942A2AABFF0">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="Module_CmdPal_Deps" Value="" KeyPath="yes"/>
</RegistryKey>
<File Source="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\x64\Microsoft.VCLibs.x64.14.00.Desktop.appx" />
</Component>
</DirectoryRef>
<?else ?>
<DirectoryRef Id="CmdPalDepsArm64InstallFolder" FileSource="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\arm64">
<Component Id="Module_CmdPal_Deps" Win64="yes" Guid="C2790FC4-0665-4462-947A-D942A2AABFF0">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="Module_CmdPal_Deps" Value="" KeyPath="yes"/>
</RegistryKey>
<File Source="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\arm64\Microsoft.VCLibs.ARM64.14.00.Desktop.appx" />
</Component>
</DirectoryRef>
<?endif ?>
<ComponentGroup Id="CmdPalComponentGroup">
<Component Id="RemoveCmdPalFolder" Guid="2DF90C08-CC75-4245-A14E-B82904636C53" Directory="INSTALLFOLDER">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="RemoveCmdPalFolder" Value="" KeyPath="yes"/>
</RegistryKey>
<RemoveFolder Id="RemoveCmdPalInstallDirFolder" Directory="CmdPalInstallFolder" On="uninstall"/>
<RemoveFolder Id="RemoveCmdPalDepsInstallDirFolder" Directory="CmdPalDepsInstallFolder" On="uninstall"/>
<?if $(sys.BUILDARCH) = x64 ?>
<RemoveFolder Id="RemoveCmdPalDepsX64InstallDirFolder" Directory="CmdPalDepsX64InstallFolder" On="uninstall"/>
<?else ?>
<RemoveFolder Id="RemoveCmdPalDepsArm64InstallDirFolder" Directory="CmdPalDepsArm64InstallFolder" On="uninstall"/>
<?endif ?>
</Component>
<ComponentRef Id="Module_CmdPal" />
<ComponentRef Id="Module_CmdPal_Deps" />
</ComponentGroup>
</Fragment>

View File

@@ -16,7 +16,6 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" x64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs"
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
</PreBuildEvent>
</PropertyGroup>
<PropertyGroup Condition="'$(Platform)' != 'x64'">
@@ -27,7 +26,6 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" arm64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs"
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
</PreBuildEvent>
</PropertyGroup>
<PropertyGroup>
@@ -123,7 +121,6 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
<Compile Include="Hosts.wxs" />
<Compile Include="ImageResizer.wxs" />
<Compile Include="KeyboardManager.wxs" />
<Compile Include="DscResources.wxs" />
<Compile Include="Peek.wxs" />
<Compile Include="PowerRename.wxs" />
<Compile Include="RegistryPreview.wxs" />

View File

@@ -75,7 +75,6 @@
<ComponentGroupRef Id="NewPlusComponentGroup" />
<ComponentGroupRef Id="NewPlusTemplatesComponentGroup" />
<ComponentGroupRef Id="ResourcesComponentGroup" />
<ComponentGroupRef Id="DscResourcesComponentGroup" />
<ComponentGroupRef Id="WindowsAppSDKComponentGroup" />
<ComponentGroupRef Id="ToolComponentGroup" />
<ComponentGroupRef Id="MonacoSRCHeatGenerated" />
@@ -325,6 +324,7 @@
BinaryKey="PTCustomActions"
DllEntry="UninstallDSCModuleCA"
/>
<CustomAction Id="UninstallServicesTask"
Return="ignore"
Impersonate="yes"

View File

@@ -1,87 +0,0 @@
[CmdletBinding()]
Param(
[Parameter(Mandatory = $True)]
[string]$dscWxsFile,
[Parameter(Mandatory = $True)]
[string]$Platform,
[Parameter(Mandatory = $True)]
[string]$Configuration
)
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$buildOutputDir = Join-Path $scriptDir "..\..\$Platform\$Configuration"
if (-not (Test-Path $buildOutputDir)) {
Write-Error "Build output directory not found: '$buildOutputDir'"
exit 1
}
$dscFiles = Get-ChildItem -Path $buildOutputDir -Filter "microsoft.powertoys.*.settings.dsc.resource.json" -File
if (-not $dscFiles) {
Write-Warning "No DSC manifest files found in '$buildOutputDir'"
$wxsContent = @"
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<?include `$(sys.CURRENTDIR)\Common.wxi?>
<Fragment>
<ComponentGroup Id="DscResourcesComponentGroup">
</ComponentGroup>
</Fragment>
</Wix>
"@
Set-Content -Path $dscWxsFile -Value $wxsContent
exit 0
}
Write-Host "Found $($dscFiles.Count) DSC manifest file(s)"
$wxsContent = @"
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<?include `$(sys.CURRENTDIR)\Common.wxi?>
<Fragment>
<DirectoryRef Id="DSCModulesReferenceFolder">
"@
$componentRefs = @()
foreach ($file in $dscFiles) {
$componentId = "DscResource_" + ($file.BaseName -replace '[^A-Za-z0-9_]', '_')
$fileId = $componentId + "_File"
$guid = [System.Guid]::NewGuid().ToString().ToUpper()
$componentRefs += $componentId
$wxsContent += @"
<Component Id="$componentId" Guid="{$guid}">
<RegistryKey Root="`$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="$componentId" Value="" KeyPath="yes"/>
</RegistryKey>
<File Id="$fileId" Source="`$(var.BinDir)$($file.Name)" Vital="no"/>
</Component>
"@
}
$wxsContent += @"
</DirectoryRef>
</Fragment>
<Fragment>
<ComponentGroup Id="DscResourcesComponentGroup">
"@
foreach ($componentId in $componentRefs) {
$wxsContent += @"
<ComponentRef Id="$componentId"/>
"@
}
$wxsContent += @"
</ComponentGroup>
</Fragment>
</Wix>
"@
Set-Content -Path $dscWxsFile -Value $wxsContent
Write-Host "Generated DSC resources WiX fragment: '$dscWxsFile'"

View File

@@ -3,7 +3,6 @@
#include "RcResource.h"
#include <ProjectTelemetry.h>
#include <spdlog/sinks/base_sink.h>
#include <filesystem>
#include "../../src/common/logger/logger.h"
#include "../../src/common/utils/gpo.h"
@@ -233,9 +232,7 @@ UINT __stdcall LaunchPowerToysCA(MSIHANDLE hInstall)
auto action = [&commandLine](HANDLE userToken)
{
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(STARTUPINFO);
startupInfo.wShowWindow = SW_SHOWNORMAL;
STARTUPINFO startupInfo{.cb = sizeof(STARTUPINFO), .wShowWindow = SW_SHOWNORMAL};
PROCESS_INFORMATION processInformation;
PVOID lpEnvironment = NULL;
@@ -274,9 +271,7 @@ UINT __stdcall LaunchPowerToysCA(MSIHANDLE hInstall)
}
else
{
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(STARTUPINFO);
startupInfo.wShowWindow = SW_SHOWNORMAL;
STARTUPINFO startupInfo{.cb = sizeof(STARTUPINFO), .wShowWindow = SW_SHOWNORMAL};
PROCESS_INFORMATION processInformation;
@@ -429,7 +424,7 @@ UINT __stdcall InstallDSCModuleCA(MSIHANDLE hInstall)
const auto modulesPath = baseModulesPath / L"Microsoft.PowerToys.Configure" / (get_product_version(false) + L".0");
std::error_code errorCode;
std::filesystem::create_directories(modulesPath, errorCode);
fs::create_directories(modulesPath, errorCode);
if (errorCode)
{
hr = E_FAIL;
@@ -438,7 +433,7 @@ UINT __stdcall InstallDSCModuleCA(MSIHANDLE hInstall)
for (const auto *filename : {DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME})
{
std::filesystem::copy_file(std::filesystem::path(installationFolder) / "DSCModules" / filename, modulesPath / filename, std::filesystem::copy_options::overwrite_existing, errorCode);
fs::copy_file(fs::path(installationFolder) / "DSCModules" / filename, modulesPath / filename, fs::copy_options::overwrite_existing, errorCode);
if (errorCode)
{
@@ -486,7 +481,7 @@ UINT __stdcall UninstallDSCModuleCA(MSIHANDLE hInstall)
for (const auto *filename : {DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME})
{
std::filesystem::remove(versionedModulePath / filename, errorCode);
fs::remove(versionedModulePath / filename, errorCode);
if (errorCode)
{
@@ -497,7 +492,7 @@ UINT __stdcall UninstallDSCModuleCA(MSIHANDLE hInstall)
for (const auto *modulePath : {&versionedModulePath, &powerToysModulePath})
{
std::filesystem::remove(*modulePath, errorCode);
fs::remove(*modulePath, errorCode);
if (errorCode)
{
@@ -1380,120 +1375,6 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
return WcaFinalize(er);
}
UINT __stdcall SetBundleInstallLocationCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
// Declare all variables at the beginning to avoid goto issues
std::wstring customActionData;
std::wstring installationFolder;
std::wstring bundleUpgradeCode;
std::wstring installScope;
bool isPerUser = false;
size_t pos1 = std::wstring::npos;
size_t pos2 = std::wstring::npos;
std::vector<HKEY> keysToTry;
hr = WcaInitialize(hInstall, "SetBundleInstallLocationCA");
ExitOnFailure(hr, "Failed to initialize");
// Parse CustomActionData: "installFolder;upgradeCode;installScope"
hr = getInstallFolder(hInstall, customActionData);
ExitOnFailure(hr, "Failed to get CustomActionData.");
pos1 = customActionData.find(L';');
if (pos1 == std::wstring::npos)
{
hr = E_INVALIDARG;
ExitOnFailure(hr, "Invalid CustomActionData format - missing first semicolon");
}
pos2 = customActionData.find(L';', pos1 + 1);
if (pos2 == std::wstring::npos)
{
hr = E_INVALIDARG;
ExitOnFailure(hr, "Invalid CustomActionData format - missing second semicolon");
}
installationFolder = customActionData.substr(0, pos1);
bundleUpgradeCode = customActionData.substr(pos1 + 1, pos2 - pos1 - 1);
installScope = customActionData.substr(pos2 + 1);
isPerUser = (installScope == L"perUser");
// Use the appropriate registry based on install scope
HKEY targetKey = isPerUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
const wchar_t* keyName = isPerUser ? L"HKCU" : L"HKLM";
WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: Searching for Bundle in %ls registry", keyName);
HKEY uninstallKey;
LONG openResult = RegOpenKeyExW(targetKey, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", 0, KEY_READ | KEY_ENUMERATE_SUB_KEYS, &uninstallKey);
if (openResult != ERROR_SUCCESS)
{
WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: Failed to open uninstall key, error: %ld", openResult);
goto LExit;
}
DWORD index = 0;
wchar_t subKeyName[256];
DWORD subKeyNameSize = sizeof(subKeyName) / sizeof(wchar_t);
while (RegEnumKeyExW(uninstallKey, index, subKeyName, &subKeyNameSize, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS)
{
HKEY productKey;
if (RegOpenKeyExW(uninstallKey, subKeyName, 0, KEY_READ | KEY_WRITE, &productKey) == ERROR_SUCCESS)
{
wchar_t upgradeCode[256];
DWORD upgradeCodeSize = sizeof(upgradeCode);
DWORD valueType;
if (RegQueryValueExW(productKey, L"BundleUpgradeCode", nullptr, &valueType,
reinterpret_cast<LPBYTE>(upgradeCode), &upgradeCodeSize) == ERROR_SUCCESS)
{
// Remove brackets from registry upgradeCode for comparison (bundleUpgradeCode doesn't have brackets)
std::wstring regUpgradeCode = upgradeCode;
if (!regUpgradeCode.empty() && regUpgradeCode.front() == L'{' && regUpgradeCode.back() == L'}')
{
regUpgradeCode = regUpgradeCode.substr(1, regUpgradeCode.length() - 2);
}
if (_wcsicmp(regUpgradeCode.c_str(), bundleUpgradeCode.c_str()) == 0)
{
// Found matching Bundle, set InstallLocation
LONG setResult = RegSetValueExW(productKey, L"InstallLocation", 0, REG_SZ,
reinterpret_cast<const BYTE*>(installationFolder.c_str()),
static_cast<DWORD>((installationFolder.length() + 1) * sizeof(wchar_t)));
if (setResult == ERROR_SUCCESS)
{
WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: InstallLocation set successfully");
}
else
{
WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: Failed to set InstallLocation, error: %ld", setResult);
}
RegCloseKey(productKey);
RegCloseKey(uninstallKey);
goto LExit;
}
}
RegCloseKey(productKey);
}
index++;
subKeyNameSize = sizeof(subKeyName) / sizeof(wchar_t);
}
RegCloseKey(uninstallKey);
LExit:
er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
return WcaFinalize(er);
}
void initSystemLogger()
{
static std::once_flag initLoggerFlag;

View File

@@ -32,4 +32,4 @@ EXPORTS
CleanFileLocksmithRuntimeRegistryCA
CleanPowerRenameRuntimeRegistryCA
CleanNewPlusRuntimeRegistryCA
SetBundleInstallLocationCA

View File

@@ -4,6 +4,13 @@
<Fragment>
<DirectoryRef Id="WinUI3AppsInstallFolder">
<Directory Id="CmdPalInstallFolder" Name="CmdPal">
<Directory Id="CmdPalDepsInstallFolder" Name="Dependencies">
<?if $(sys.BUILDARCH) = x64 ?>
<Directory Id="CmdPalDepsX64InstallFolder" Name="x64" />
<?else?>
<Directory Id="CmdPalDepsArm64InstallFolder" Name="arm64" />
<?endif?>
</Directory>
</Directory>
</DirectoryRef>
<DirectoryRef Id="CmdPalInstallFolder" FileSource="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test">
@@ -18,14 +25,40 @@
<?endif?>
</Component>
</DirectoryRef>
<?if $(sys.BUILDARCH) = x64 ?>
<DirectoryRef Id="CmdPalDepsX64InstallFolder" FileSource="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\x64">
<Component Id="Module_CmdPal_Deps" Guid="C2790FC4-0665-4462-947A-D942A2AABFF0" Bitness="always64">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="Module_CmdPal_Deps" Value="" KeyPath="yes" />
</RegistryKey>
<File Id="Microsoft.VCLibs.x64.14.00.Desktop.appx" Source="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\x64\Microsoft.VCLibs.x64.14.00.Desktop.appx" />
</Component>
</DirectoryRef>
<?else?>
<DirectoryRef Id="CmdPalDepsArm64InstallFolder" FileSource="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\arm64">
<Component Id="Module_CmdPal_Deps" Guid="C2790FC4-0665-4462-947A-D942A2AABFF0" Bitness="always64">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="Module_CmdPal_Deps" Value="" KeyPath="yes" />
</RegistryKey>
<File Id="Microsoft.VCLibs.ARM64.14.00.Desktop.appx" Source="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\arm64\Microsoft.VCLibs.ARM64.14.00.Desktop.appx" />
</Component>
</DirectoryRef>
<?endif?>
<ComponentGroup Id="CmdPalComponentGroup">
<Component Id="RemoveCmdPalFolder" Guid="2DF90C08-CC75-4245-A14E-B82904636C53" Directory="INSTALLFOLDER">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="RemoveCmdPalFolder" Value="" KeyPath="yes" />
</RegistryKey>
<RemoveFolder Id="RemoveCmdPalInstallDirFolder" Directory="CmdPalInstallFolder" On="uninstall" />
<RemoveFolder Id="RemoveCmdPalDepsInstallDirFolder" Directory="CmdPalDepsInstallFolder" On="uninstall" />
<?if $(sys.BUILDARCH) = x64 ?>
<RemoveFolder Id="RemoveCmdPalDepsX64InstallDirFolder" Directory="CmdPalDepsX64InstallFolder" On="uninstall" />
<?else?>
<RemoveFolder Id="RemoveCmdPalDepsArm64InstallDirFolder" Directory="CmdPalDepsArm64InstallFolder" On="uninstall" />
<?endif?>
</Component>
<ComponentRef Id="Module_CmdPal" />
<ComponentRef Id="Module_CmdPal_Deps" />
</ComponentGroup>
</Fragment>
</Wix>

View File

@@ -9,25 +9,6 @@
<RegistryValue Type="string" Name="InstallScope" Value="$(var.InstallScope)" />
</RegistryKey>
</Component>
<?if $(var.PerUser) = "true" ?>
<Component Id="powertoys_env_path_user" Bitness="always64">
<!-- Anchor registry for component key path -->
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="powertoys_env_path_user" Value="" KeyPath="yes" />
</RegistryKey>
<!-- Append install folder to current user's PATH -->
<Environment Id="AddPowerToysToUserPath" Name="PATH" Action="set" Part="last" System="no" Value="[INSTALLFOLDER]" />
</Component>
<?else?>
<Component Id="powertoys_env_path_machine" Bitness="always64">
<!-- Anchor registry for component key path -->
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="powertoys_env_path_machine" Value="" KeyPath="yes" />
</RegistryKey>
<!-- Append install folder to machine PATH -->
<Environment Id="AddPowerToysToMachinePath" Name="PATH" Action="set" Part="last" System="yes" Value="[INSTALLFOLDER]" />
</Component>
<?endif?>
<Component Id="powertoys_toast_clsid" Bitness="always64">
<RemoveFolder Id="Remove_powertoys_toast_clsid" On="uninstall" />
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\CLSID\{DD5CACDA-7C2E-4997-A62A-04A597B58F76}">
@@ -128,11 +109,6 @@
<ComponentRef Id="powertoys_exe" />
<ComponentRef Id="PowerToysStartMenuShortcut" />
<ComponentRef Id="powertoys_per_machine_comp" />
<?if $(var.PerUser) = "true" ?>
<ComponentRef Id="powertoys_env_path_user" />
<?else?>
<ComponentRef Id="powertoys_env_path_machine" />
<?endif?>
<ComponentRef Id="powertoys_toast_clsid" />
<ComponentRef Id="License_rtf" />
<ComponentRef Id="Notice_md" />

View File

@@ -28,9 +28,6 @@
<Log Disable="no" Prefix="powertoys-bootstrapper-msi-$(var.Version)" Extension=".log" />
<!-- Store Bundle UpgradeCode for CustomAction -->
<Variable Name="BundleUpgradeCode" Type="string" Value="$(var.UpgradeCode)" />
<!-- Only install/upgrade if the version is greater or equal than the currently installed version of PowerToys, to handle the case in which PowerToys was installed from old MSI (before WiX bootstrapper was used) -->
<!-- If the previous installation is a bundle installation, just let WiX run its logic. -->
<Variable Name="MinimumVersion" Type="version" Value="0.0.0.0" />
@@ -61,7 +58,6 @@
<MsiPackage DisplayName="PowerToys MSI" SourceFile="$(var.PowerToysPlatform)\Release\$(var.MSIPath)\$(var.MSIName)" Compressed="yes" bal:DisplayInternalUICondition="false">
<MsiProperty Name="BOOTSTRAPPERINSTALLFOLDER" Value="[InstallFolder]" />
<MsiProperty Name="MSIRESTARTMANAGERCONTROL" Value="Disable" />
<MsiProperty Name="BUNDLEINFO" Value="[BundleUpgradeCode]" />
</MsiPackage>
</Chain>
</Bundle>

View File

@@ -14,7 +14,6 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" x64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2"
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
</PreBuildEvent>
</PropertyGroup>
<PropertyGroup Condition="'$(Platform)' != 'x64'">
@@ -25,7 +24,6 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" arm64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2"
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
</PreBuildEvent>
</PropertyGroup>
<PropertyGroup>
@@ -117,7 +115,6 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
<Compile Include="KeyboardManager.wxs" />
<Compile Include="Peek.wxs" />
<Compile Include="PowerRename.wxs" />
<Compile Include="DscResources.wxs" />
<Compile Include="RegistryPreview.wxs" />
<Compile Include="Run.wxs" />
<Compile Include="Settings.wxs" />

View File

@@ -62,7 +62,6 @@
<ComponentGroupRef Id="NewPlusComponentGroup" />
<ComponentGroupRef Id="NewPlusTemplatesComponentGroup" />
<ComponentGroupRef Id="ResourcesComponentGroup" />
<ComponentGroupRef Id="DscResourcesComponentGroup" />
<ComponentGroupRef Id="WindowsAppSDKComponentGroup" />
<ComponentGroupRef Id="ToolComponentGroup" />
<ComponentGroupRef Id="MonacoSRCHeatGenerated" />
@@ -70,8 +69,8 @@
<ComponentGroupRef Id="CmdPalComponentGroup" />
</Feature>
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" Sequence="execute" />
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" />
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
<UI>
@@ -118,8 +117,6 @@
<Custom Action="SetUnApplyModulesRegistryChangeSetsParam" Before="UnApplyModulesRegistryChangeSets" />
<Custom Action="CheckGPO" After="InstallInitialize" Condition="NOT Installed" />
<Custom Action="SetBundleInstallLocationData" Before="SetBundleInstallLocation" Condition="NOT Installed" />
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="InstallCmdPalPackage" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="override Wix4CloseApplications_$(sys.BUILDARCHSHORT)" Before="RemoveFiles" />
@@ -163,9 +160,6 @@
<CustomAction Id="SetInstallCmdPalPackageParam" Property="InstallCmdPalPackage" Value="[INSTALLFOLDER]" />
<!-- Set InstallLocation for Bundle entry as well -->
<CustomAction Id="SetBundleInstallLocationData" Property="SetBundleInstallLocation" Value="[INSTALLFOLDER];[BUNDLEINFO];[InstallScope]" />
<CustomAction Id="LaunchPowerToys" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="LaunchPowerToysCA" BinaryRef="PTCustomActions" />
<CustomAction Id="TerminateProcesses" Return="ignore" Execute="immediate" DllEntry="TerminateProcessesCA" BinaryRef="PTCustomActions" />
@@ -250,8 +244,6 @@
<CustomAction Id="InstallCmdPalPackage" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="InstallCmdPalPackageCA" BinaryRef="PTCustomActions" />
<CustomAction Id="SetBundleInstallLocation" Return="ignore" Impersonate="no" Execute="deferred" DllEntry="SetBundleInstallLocationCA" BinaryRef="PTCustomActions" />
<!-- Close 'PowerToys.exe' before uninstall-->
<Property Id="MSIRESTARTMANAGERCONTROL" Value="DisableShutdown" />
<Property Id="MSIFASTINSTALL" Value="DisableShutdown" />

View File

@@ -1,102 +0,0 @@
[CmdletBinding()]
Param(
[Parameter(Mandatory = $True)]
[string]$dscWxsFile,
[Parameter(Mandatory = $True)]
[string]$Platform,
[Parameter(Mandatory = $True)]
[string]$Configuration
)
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
# Find build output directory
$buildOutputDir = Join-Path $scriptDir "..\..\$Platform\$Configuration"
if (-not (Test-Path $buildOutputDir)) {
Write-Error "Build output directory not found: '$buildOutputDir'"
exit 1
}
# Find all DSC manifest JSON files
$dscFiles = Get-ChildItem -Path $buildOutputDir -Filter "microsoft.powertoys.*.settings.dsc.resource.json" -File
if (-not $dscFiles) {
Write-Warning "No DSC manifest files found in '$buildOutputDir'"
# Create empty component group
$wxsContent = @"
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<?include `$(sys.CURRENTDIR)\Common.wxi?>
<Fragment>
<ComponentGroup Id="DscResourcesComponentGroup">
</ComponentGroup>
</Fragment>
</Wix>
"@
Set-Content -Path $dscWxsFile -Value $wxsContent
exit 0
}
Write-Host "Found $($dscFiles.Count) DSC manifest file(s)"
# Generate WiX fragment
$wxsContent = @"
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<?include `$(sys.CURRENTDIR)\Common.wxi?>
<Fragment>
<DirectoryRef Id="DSCModulesReferenceFolder">
"@
$componentRefs = @()
foreach ($file in $dscFiles) {
$componentId = "DscResource_" + ($file.BaseName -replace '[^A-Za-z0-9_]', '_')
$fileId = $componentId + "_File"
$guid = [System.Guid]::NewGuid().ToString().ToUpper()
$componentRefs += $componentId
$wxsContent += @"
<Component Id="$componentId" Guid="{$guid}" Directory="DSCModulesReferenceFolder">
<RegistryKey Root="`$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="$componentId" Value="" KeyPath="yes"/>
</RegistryKey>
<File Id="$fileId" Source="`$(var.BinDir)$($file.Name)" Vital="no"/>
</Component>
"@
}
$wxsContent += @"
</DirectoryRef>
</Fragment>
<Fragment>
<ComponentGroup Id="DscResourcesComponentGroup">
"@
foreach ($componentId in $componentRefs) {
$wxsContent += @"
<ComponentRef Id="$componentId"/>
"@
}
$wxsContent += @"
</ComponentGroup>
</Fragment>
</Wix>
"@
# Write the WiX file
Set-Content -Path $dscWxsFile -Value $wxsContent
Write-Host "Generated DSC resources WiX fragment: '$dscWxsFile'"

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.
#nullable enable
using System;
using System.Collections.Generic;
namespace ManagedCommon
{
public sealed class ErrorResponse
{
public string Error { get; set; } = string.Empty;
public IEnumerable<string>? RegisteredModules { get; set; }
public string[]? AvailableEndpoints { get; set; }
public int? StatusCode { get; set; }
public DateTimeOffset Timestamp { get; set; }
}
}

View File

@@ -0,0 +1,26 @@
// 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.
#nullable enable
using System;
using System.Collections.Generic;
namespace ManagedCommon
{
public sealed class GlobalStatusResponse
{
public string Application { get; set; } = string.Empty;
public string? Version { get; set; }
public string Status { get; set; } = string.Empty;
public int RegisteredModules { get; set; }
public Dictionary<string, ModuleStatusResponse> Modules { get; set; } = [];
public DateTimeOffset Timestamp { get; set; }
}
}

View File

@@ -0,0 +1,358 @@
// 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.
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace ManagedCommon
{
public sealed class HttpServer : IDisposable
{
private readonly HttpListener _listener;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly Dictionary<string, IHttpRequestHandler> _requestHandlers;
private readonly JsonSerializerOptions _fallbackJsonOptions;
private Task? _listenerTask;
private bool _disposed;
public HttpServer(string prefix = "http://localhost:8080/")
{
_listener = new HttpListener();
_listener.Prefixes.Add(prefix);
_cancellationTokenSource = new CancellationTokenSource();
_requestHandlers = new Dictionary<string, IHttpRequestHandler>(StringComparer.OrdinalIgnoreCase);
// Cached fallback options for generic deserialization
_fallbackJsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
Logger.LogInfo($"HTTP server configured to listen on: {prefix}");
}
/// <summary>
/// Register a request handler for a specific module.
/// </summary>
/// <param name="handler">The request handler to register.</param>
public void RegisterHandler(IHttpRequestHandler handler)
{
ArgumentNullException.ThrowIfNull(handler);
if (string.IsNullOrWhiteSpace(handler.ModuleName))
{
throw new ArgumentException("Module name cannot be null or empty", nameof(handler));
}
_requestHandlers[handler.ModuleName] = handler;
Logger.LogInfo($"Registered HTTP handler for module: {handler.ModuleName}");
}
/// <summary>
/// Unregister a request handler for a specific module.
/// </summary>
/// <param name="moduleName">The name of the module to unregister.</param>
public void UnregisterHandler(string moduleName)
{
if (_requestHandlers.Remove(moduleName))
{
Logger.LogInfo($"Unregistered HTTP handler for module: {moduleName}");
}
}
public void Start()
{
try
{
_listener.Start();
Logger.LogInfo("HTTP server started successfully");
_listenerTask = Task.Run(async () => await ListenAsync(_cancellationTokenSource.Token));
}
catch (Exception ex)
{
Logger.LogError($"Failed to start HTTP server: {ex.Message}");
throw;
}
}
public void Stop()
{
try
{
_cancellationTokenSource.Cancel();
_listener.Stop();
_listenerTask?.Wait(TimeSpan.FromSeconds(5));
Logger.LogInfo("HTTP server stopped");
}
catch (Exception ex)
{
Logger.LogError($"Error stopping HTTP server: {ex.Message}");
}
}
/// <summary>
/// Utility method for modules to write JSON responses.
/// </summary>
/// <param name="response">The HTTP response to write to.</param>
/// <param name="data">The object to serialize as JSON.</param>
public async Task WriteJsonResponseAsync(HttpListenerResponse response, object data)
{
response.ContentType = "application/json";
response.ContentEncoding = Encoding.UTF8;
string json = data switch
{
ServerStatusResponse serverStatus => JsonSerializer.Serialize(serverStatus, HttpServerJsonContext.Default.ServerStatusResponse),
GlobalStatusResponse globalStatus => JsonSerializer.Serialize(globalStatus, HttpServerJsonContext.Default.GlobalStatusResponse),
ErrorResponse error => JsonSerializer.Serialize(error, HttpServerJsonContext.Default.ErrorResponse),
_ => JsonSerializer.Serialize(data, HttpServerJsonContext.Default.Object),
};
var buffer = Encoding.UTF8.GetBytes(json);
response.ContentLength64 = buffer.Length;
await response.OutputStream.WriteAsync(buffer.AsMemory(), _cancellationTokenSource.Token);
response.Close();
}
/// <summary>
/// Utility method for modules to read JSON request bodies as string.
/// Modules should handle their own deserialization to maintain AOT compatibility.
/// </summary>
/// <param name="request">The HTTP request to read from.</param>
/// <returns>The JSON string, or null if no body.</returns>
public async Task<string?> ReadJsonRequestBodyAsync(HttpListenerRequest request)
{
if (!request.HasEntityBody)
{
return null;
}
using var reader = new StreamReader(request.InputStream, Encoding.UTF8);
var body = await reader.ReadToEndAsync();
return string.IsNullOrWhiteSpace(body) ? null : body;
}
/// <summary>
/// Legacy utility method for modules to read JSON request bodies.
/// Warning: This method uses reflection-based deserialization and is not AOT-compatible.
/// Consider using ReadJsonRequestBodyAsync and handling deserialization in your module.
/// </summary>
/// <typeparam name="T">The type to deserialize to.</typeparam>
/// <param name="request">The HTTP request to read from.</param>
/// <returns>The deserialized object, or null if no body or invalid JSON.</returns>
[Obsolete("Use ReadJsonRequestBodyAsync and handle deserialization in your module for AOT compatibility")]
public async Task<T?> ReadJsonRequestAsync<T>(HttpListenerRequest request)
where T : class
{
var body = await ReadJsonRequestBodyAsync(request);
if (body == null)
{
return null;
}
try
{
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling
return JsonSerializer.Deserialize<T>(body, _fallbackJsonOptions);
#pragma warning restore IL3050
#pragma warning restore IL2026
}
catch (JsonException ex)
{
Logger.LogError($"Error deserializing request body: {ex.Message}");
return null;
}
}
public void Dispose()
{
if (!_disposed)
{
Stop();
_cancellationTokenSource?.Dispose();
_listener?.Close();
_disposed = true;
}
}
private async Task ListenAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested && _listener.IsListening)
{
try
{
var contextTask = _listener.GetContextAsync();
var context = await contextTask.ConfigureAwait(false);
// Handle request asynchronously without blocking the listener
_ = Task.Run(async () => await HandleRequestAsync(context), cancellationToken);
}
catch (ObjectDisposedException)
{
// Expected when listener is stopped
break;
}
catch (HttpListenerException ex) when (ex.ErrorCode == 995)
{
// Expected when listener is stopped
break;
}
catch (Exception ex)
{
Logger.LogError($"Error in HTTP listener: {ex.Message}");
}
}
}
private async Task HandleRequestAsync(HttpListenerContext context)
{
try
{
var request = context.Request;
var response = context.Response;
Logger.LogInfo($"HTTP Request: {request.HttpMethod} {request.Url?.AbsolutePath}");
// Set CORS headers
response.Headers.Add("Access-Control-Allow-Origin", "*");
response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
if (request.HttpMethod == "OPTIONS")
{
response.StatusCode = 200;
response.Close();
return;
}
var path = request.Url?.AbsolutePath?.TrimStart('/');
if (string.IsNullOrEmpty(path))
{
await HandleRootRequestAsync(response);
return;
}
// Parse the path to extract module name and sub-path
var segments = path.Split('/', 2);
var moduleName = segments[0];
var subPath = segments.Length > 1 ? segments[1] : string.Empty;
// Check for global endpoints
if (string.Equals(moduleName, "status", StringComparison.OrdinalIgnoreCase))
{
await HandleGlobalStatusAsync(response);
return;
}
// Route to module-specific handler
if (_requestHandlers.TryGetValue(moduleName, out var handler))
{
try
{
await handler.HandleRequestAsync(context, subPath);
}
catch (Exception ex)
{
Logger.LogError($"Error in module handler for {moduleName}: {ex.Message}");
await HandleErrorAsync(response, 500, $"Internal server error in {moduleName} module: {ex.Message}");
}
}
else
{
await HandleNotFoundAsync(response, moduleName);
}
}
catch (Exception ex)
{
Logger.LogError($"Unhandled error in HTTP request handler: {ex.Message}");
}
}
private async Task HandleRootRequestAsync(HttpListenerResponse response)
{
var rootInfo = new ServerStatusResponse
{
Application = "PowerToys HTTP Server",
Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(),
Status = "Running",
RegisteredModules = _requestHandlers.Keys,
AvailableEndpoints = [
"GET /status - Get server status",
"GET /{module}/... - Module-specific endpoints",
],
Timestamp = DateTimeOffset.Now,
};
await WriteJsonResponseAsync(response, rootInfo);
}
private async Task HandleGlobalStatusAsync(HttpListenerResponse response)
{
var moduleStatuses = new Dictionary<string, ModuleStatusResponse>();
foreach (var kvp in _requestHandlers)
{
moduleStatuses[kvp.Key] = new ModuleStatusResponse
{
ModuleName = kvp.Value.ModuleName,
AvailableEndpoints = kvp.Value.GetAvailableEndpoints(),
};
}
var status = new GlobalStatusResponse
{
Application = "PowerToys HTTP Server",
Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(),
Status = "Running",
RegisteredModules = _requestHandlers.Count,
Modules = moduleStatuses,
Timestamp = DateTimeOffset.Now,
};
await WriteJsonResponseAsync(response, status);
}
private async Task HandleNotFoundAsync(HttpListenerResponse response, string requestedModule)
{
var errorResponse = new ErrorResponse
{
Error = $"Module '{requestedModule}' not found",
RegisteredModules = _requestHandlers.Keys,
AvailableEndpoints = [
"GET /status - Get server status and available modules",
],
Timestamp = DateTimeOffset.Now,
};
response.StatusCode = 404;
await WriteJsonResponseAsync(response, errorResponse);
}
private async Task HandleErrorAsync(HttpListenerResponse response, int statusCode, string message)
{
response.StatusCode = statusCode;
var errorResponse = new ErrorResponse
{
Error = message,
StatusCode = statusCode,
Timestamp = DateTimeOffset.Now,
};
await WriteJsonResponseAsync(response, errorResponse);
}
}
}

View File

@@ -0,0 +1,23 @@
// 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.
#nullable enable
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace ManagedCommon
{
[JsonSerializable(typeof(ServerStatusResponse))]
[JsonSerializable(typeof(ModuleStatusResponse))]
[JsonSerializable(typeof(GlobalStatusResponse))]
[JsonSerializable(typeof(ErrorResponse))]
[JsonSerializable(typeof(Dictionary<string, ModuleStatusResponse>))]
[JsonSerializable(typeof(string[]))]
[JsonSerializable(typeof(IEnumerable<string>))]
[JsonSerializable(typeof(object))]
internal sealed partial class HttpServerJsonContext : JsonSerializerContext
{
}
}

View File

@@ -0,0 +1,36 @@
// 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.IO;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace ManagedCommon
{
public interface IHttpRequestHandler
{
/// <summary>
/// Handle an HTTP request for this module.
/// </summary>
/// <param name="context">The HTTP context containing request and response.</param>
/// <param name="path">The requested path (after module prefix).</param>
/// <returns>Task representing the async operation.</returns>
Task HandleRequestAsync(HttpListenerContext context, string path);
/// <summary>
/// Get the module name used for URL routing (e.g., "awake", "fancyzones").
/// </summary>
string ModuleName { get; }
/// <summary>
/// Get the available endpoints for this module (for documentation).
/// </summary>
string[] GetAvailableEndpoints();
}
}

View File

@@ -0,0 +1,15 @@
// 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.
#nullable enable
namespace ManagedCommon
{
public sealed class ModuleStatusResponse
{
public string ModuleName { get; set; } = string.Empty;
public string[] AvailableEndpoints { get; set; } = [];
}
}

View File

@@ -0,0 +1,225 @@
// 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.Runtime.InteropServices;
using System.Threading;
#nullable enable
#pragma warning disable IL2050 // Suppress COM interop trimming warnings for ROT hosting P/Invokes (desktop only scenario)
namespace ManagedCommon
{
/// <summary>
/// Generic helper to host a single COM-visible automation object in the Running Object Table (ROT)
/// without registry/CLSID class factory registration. Used for lightweight cross-process automation.
/// Pattern: create instance -> register with moniker -> wait until Stop.
/// Threading: spins up a dedicated STA thread so objects needing STA semantics are safe.
/// </summary>
public sealed class RotSingletonHost : IDisposable
{
private readonly Lock _sync = new();
private readonly Func<object> _factory;
private readonly string _monikerName;
private readonly string _threadName;
private readonly ManualResetEvent _shutdown = new(false);
private Thread? _thread;
private int _rotCookie;
private object? _instance; // keep alive
private IMoniker? _moniker;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="RotSingletonHost"/> class.
/// </summary>
/// <param name="monikerName">Moniker name (logical unique id), e.g. "Awake.Automation".</param>
/// <param name="factory">Factory that creates the object to expose. Should return a COM-visible object.</param>
/// <param name="threadName">Optional thread name for diagnostics.</param>
public RotSingletonHost(string monikerName, Func<object> factory, string? threadName = null)
{
_monikerName = string.IsNullOrWhiteSpace(monikerName) ? throw new ArgumentException("Moniker required", nameof(monikerName)) : monikerName;
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
_threadName = threadName ?? $"RotHost:{_monikerName}";
}
public bool IsRunning => _thread != null;
public string MonikerName => _monikerName;
public void Start()
{
lock (_sync)
{
if (_disposed)
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
if (_thread != null)
{
return; // already running
}
_thread = new Thread(ThreadMain)
{
IsBackground = true,
Name = _threadName,
};
_thread.SetApartmentState(ApartmentState.STA);
_thread.Start();
Logger.LogInfo($"ROT host starting for moniker '{_monikerName}'");
}
}
public void Stop()
{
lock (_sync)
{
if (_thread == null)
{
return;
}
_shutdown.Set();
}
_thread?.Join(3000);
_thread = null;
_shutdown.Reset();
}
private void ThreadMain()
{
int hr = Ole32.CoInitializeEx(IntPtr.Zero, Ole32.CoinitApartmentThreaded);
if (hr < 0)
{
Logger.LogError($"CoInitializeEx failed: 0x{hr:X8}");
return;
}
try
{
hr = Ole32.GetRunningObjectTable(0, out var rot);
if (hr < 0 || rot == null)
{
Logger.LogError($"GetRunningObjectTable failed: 0x{hr:X8}");
return;
}
hr = Ole32.CreateItemMoniker("!", _monikerName, out _moniker);
if (hr < 0 || _moniker == null)
{
Logger.LogError($"CreateItemMoniker failed: 0x{hr:X8}");
return;
}
_instance = _factory();
var unk = Marshal.GetIUnknownForObject(_instance);
try
{
hr = rot.Register(0x1 /* ROTFLAGS_REGISTRATIONKEEPSALIVE */, _instance, _moniker, out _rotCookie);
if (hr < 0)
{
Logger.LogError($"IRunningObjectTable.Register failed: 0x{hr:X8}");
return;
}
}
finally
{
Marshal.Release(unk);
}
Logger.LogInfo($"ROT registered: '{_monikerName}'");
WaitHandle.WaitAny(new WaitHandle[] { _shutdown });
}
catch (Exception ex)
{
Logger.LogError($"ROT host exception: {ex}");
}
finally
{
try
{
if (_rotCookie != 0 && Ole32.GetRunningObjectTable(0, out var rot2) == 0 && rot2 != null)
{
rot2.Revoke(_rotCookie);
_rotCookie = 0;
}
}
catch (Exception ex)
{
Logger.LogWarning($"Exception revoking ROT registration: {ex.Message}");
}
Ole32.CoUninitialize();
Logger.LogInfo($"ROT host stopped: '{_monikerName}'");
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
Stop();
_disposed = true;
}
private static class Ole32
{
internal const int CoinitApartmentThreaded = 0x2;
#pragma warning disable IL2050 // Suppress trimming warnings for COM interop P/Invokes; ROT hosting not used in trimmed scenarios.
[DllImport("ole32.dll")]
internal static extern int CoInitializeEx(IntPtr pvReserved, int dwCoInit);
[DllImport("ole32.dll")]
internal static extern void CoUninitialize();
[DllImport("ole32.dll")]
internal static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable? prot);
[DllImport("ole32.dll")]
internal static extern int CreateItemMoniker([MarshalAs(UnmanagedType.LPWStr)] string lpszDelim, [MarshalAs(UnmanagedType.LPWStr)] string lpszItem, out IMoniker? ppmk);
#pragma warning restore IL2050
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00000010-0000-0000-C000-000000000046")]
private interface IRunningObjectTable
{
int Register(int grfFlags, [MarshalAs(UnmanagedType.IUnknown)] object punkObject, IMoniker pmkObjectName, out int pdwRegister);
int Revoke(int dwRegister);
void IsRunning(IMoniker pmkObjectName);
int GetObject(IMoniker pmkObjectName, [MarshalAs(UnmanagedType.IUnknown)] out object? ppunkObject);
void NoteChangeTime(int dwRegister, ref FileTime pfiletime);
int GetTimeOfLastChange(IMoniker pmkObjectName, ref FileTime pfiletime);
int EnumRunning(out object ppenumMoniker);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("0000000f-0000-0000-C000-000000000046")]
private interface IMoniker
{
}
[StructLayout(LayoutKind.Sequential)]
private struct FileTime
{
public uint DwLowDateTime;
public uint DwHighDateTime;
}
}
}

View File

@@ -0,0 +1,26 @@
// 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.
#nullable enable
using System;
using System.Collections.Generic;
namespace ManagedCommon
{
public sealed class ServerStatusResponse
{
public string Application { get; set; } = string.Empty;
public string? Version { get; set; }
public string Status { get; set; } = string.Empty;
public IEnumerable<string> RegisteredModules { get; set; } = [];
public string[] AvailableEndpoints { get; set; } = [];
public DateTimeOffset Timestamp { get; set; }
}
}

View File

@@ -1,68 +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.CommandLine;
using System.CommandLine.Parsing;
using System.Globalization;
using System.IO;
using System.Resources;
using PowerToys.DSC.UnitTests.Models;
namespace PowerToys.DSC.UnitTests;
public class BaseDscTest
{
private readonly ResourceManager _resourceManager;
public BaseDscTest()
{
_resourceManager = new ResourceManager("PowerToys.DSC.Properties.Resources", typeof(PowerToys.DSC.Program).Assembly);
}
/// <summary>
/// Returns the string resource for the given name, formatted with the provided arguments.
/// </summary>
/// <param name="name">The name of the resource string.</param>
/// <param name="args">The arguments to format the resource string with.</param>
/// <returns></returns>
public string GetResourceString(string name, params string[] args)
{
return string.Format(CultureInfo.InvariantCulture, _resourceManager.GetString(name, CultureInfo.InvariantCulture), args);
}
/// <summary>
/// Execute a dsc command with the provided arguments.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="args"></param>
/// <returns></returns>
protected DscExecuteResult ExecuteDscCommand<T>(params string[] args)
where T : Command, new()
{
var originalOut = Console.Out;
var originalErr = Console.Error;
var outSw = new StringWriter();
var errSw = new StringWriter();
try
{
Console.SetOut(outSw);
Console.SetError(errSw);
var executeResult = new T().Invoke(args);
var output = outSw.ToString();
var errorOutput = errSw.ToString();
return new(executeResult == 0, output, errorOutput);
}
finally
{
Console.SetOut(originalOut);
Console.SetError(originalErr);
outSw.Dispose();
errSw.Dispose();
}
}
}

View File

@@ -1,37 +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 Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerToys.DSC.Commands;
using PowerToys.DSC.DSCResources;
namespace PowerToys.DSC.UnitTests;
[TestClass]
public sealed class CommandTest : BaseDscTest
{
[TestMethod]
public void GetResource_Found_Success()
{
// Act
var result = ExecuteDscCommand<GetCommand>("--resource", SettingsResource.ResourceName);
// Assert
Assert.IsTrue(result.Success);
}
[TestMethod]
public void GetResource_NotFound_Fail()
{
// Arrange
var availableResources = string.Join(", ", BaseCommand.AvailableResources);
// Act
var result = ExecuteDscCommand<GetCommand>("--resource", "ResourceNotFound");
// Assert
Assert.IsFalse(result.Success);
Assert.Contains(GetResourceString("InvalidResourceNameError", availableResources), result.Error);
}
}

View File

@@ -1,103 +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;
using System.Linq;
using System.Text.Json;
using PowerToys.DSC.Models;
namespace PowerToys.DSC.UnitTests.Models;
/// <summary>
/// Result of executing a DSC command.
/// </summary>
public class DscExecuteResult
{
/// <summary>
/// Initializes a new instance of the <see cref="DscExecuteResult"/> class.
/// </summary>
/// <param name="success">Value indicating whether the command execution was successful.</param>
/// <param name="output">Output stream content.</param>
/// <param name="error">Error stream content.</param>
public DscExecuteResult(bool success, string output, string error)
{
Success = success;
Output = output;
Error = error;
}
/// <summary>
/// Gets a value indicating whether the command execution was successful.
/// </summary>
public bool Success { get; }
/// <summary>
/// Gets the output stream content of the operation.
/// </summary>
public string Output { get; }
/// <summary>
/// Gets the error stream content of the operation.
/// </summary>
public string Error { get; }
/// <summary>
/// Gets the messages from the error stream.
/// </summary>
/// <returns>List of messages with their levels.</returns>
public List<(DscMessageLevel Level, string Message)> Messages()
{
var lines = Error.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
return lines.SelectMany(line =>
{
var map = JsonSerializer.Deserialize<Dictionary<string, string>>(line);
return map.Select(v => (GetMessageLevel(v.Key), v.Value)).ToList();
}).ToList();
}
/// <summary>
/// Gets the output as state.
/// </summary>
/// <returns>State.</returns>
public T OutputState<T>()
{
var lines = Output.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
Debug.Assert(lines.Length == 1, "Output should contain exactly one line.");
return JsonSerializer.Deserialize<T>(lines[0]);
}
/// <summary>
/// Gets the output as state and diff.
/// </summary>
/// <returns>State and diff.</returns>
public (T State, List<string> Diff) OutputStateAndDiff<T>()
{
var lines = Output.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
Debug.Assert(lines.Length == 2, "Output should contain exactly two lines.");
var obj = JsonSerializer.Deserialize<T>(lines[0]);
var diff = JsonSerializer.Deserialize<List<string>>(lines[1]);
return (obj, diff);
}
/// <summary>
/// Gets the message level from a string representation.
/// </summary>
/// <param name="level">The string representation of the message level.</param>
/// <returns>The level as <see cref="DscMessageLevel"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the level is unknown.</exception>
private DscMessageLevel GetMessageLevel(string level)
{
return level switch
{
"error" => DscMessageLevel.Error,
"warn" => DscMessageLevel.Warning,
"info" => DscMessageLevel.Info,
"debug" => DscMessageLevel.Debug,
"trace" => DscMessageLevel.Trace,
_ => throw new ArgumentOutOfRangeException(nameof(level), level, "Unknown message level"),
};
}
}

View File

@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<OutputPath>..\..\..\..\$(Configuration)\$(Platform)\tests\PowerToys.DSC.Tests\</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PowerToys.DSC\PowerToys.DSC.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,34 +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 ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
[TestClass]
public sealed class SettingsResourceAdvancedPasteModuleTest : SettingsResourceModuleTest<AdvancedPasteSettings>
{
public SettingsResourceAdvancedPasteModuleTest()
: base(nameof(ModuleType.AdvancedPaste))
{
}
protected override Action<AdvancedPasteSettings> GetSettingsModifier()
{
return s =>
{
s.Properties.ShowCustomPreview = !s.Properties.ShowCustomPreview;
s.Properties.CloseAfterLosingFocus = !s.Properties.CloseAfterLosingFocus;
s.Properties.IsAdvancedAIEnabled = !s.Properties.IsAdvancedAIEnabled;
s.Properties.AdvancedPasteUIShortcut = new HotkeySettings
{
Key = "mock",
Alt = true,
};
};
}
}

View File

@@ -1,28 +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 ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
[TestClass]
public sealed class SettingsResourceAlwaysOnTopModuleTest : SettingsResourceModuleTest<AlwaysOnTopSettings>
{
public SettingsResourceAlwaysOnTopModuleTest()
: base(nameof(ModuleType.AlwaysOnTop))
{
}
protected override Action<AlwaysOnTopSettings> GetSettingsModifier()
{
return s =>
{
s.Properties.RoundCornersEnabled.Value = !s.Properties.RoundCornersEnabled.Value;
s.Properties.FrameEnabled.Value = !s.Properties.FrameEnabled.Value;
};
}
}

View File

@@ -1,30 +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 Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerToys.DSC.DSCResources;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
[TestClass]
public sealed class SettingsResourceAppModuleTest : SettingsResourceModuleTest<GeneralSettings>
{
public SettingsResourceAppModuleTest()
: base(SettingsResource.AppModule)
{
}
protected override Action<GeneralSettings> GetSettingsModifier()
{
return s =>
{
s.Startup = !s.Startup;
s.ShowSysTrayIcon = !s.ShowSysTrayIcon;
s.Enabled.Awake = !s.Enabled.Awake;
s.Enabled.ColorPicker = !s.Enabled.ColorPicker;
};
}
}

View File

@@ -1,38 +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 ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
[TestClass]
public sealed class SettingsResourceAwakeModuleTest : SettingsResourceModuleTest<AwakeSettings>
{
public SettingsResourceAwakeModuleTest()
: base(nameof(ModuleType.Awake))
{
}
protected override Action<AwakeSettings> GetSettingsModifier()
{
return s =>
{
s.Properties.ExpirationDateTime = DateTimeOffset.MinValue;
s.Properties.IntervalHours = DefaultSettings.Properties.IntervalHours + 1;
s.Properties.IntervalMinutes = DefaultSettings.Properties.IntervalMinutes + 1;
s.Properties.Mode = s.Properties.Mode == AwakeMode.PASSIVE ? AwakeMode.TIMED : AwakeMode.PASSIVE;
s.Properties.KeepDisplayOn = !s.Properties.KeepDisplayOn;
s.Properties.CustomTrayTimes = new Dictionary<string, uint>
{
{ "08:00", 1 },
{ "12:00", 2 },
{ "16:00", 3 },
};
};
}
}

View File

@@ -1,28 +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 ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
[TestClass]
public sealed class SettingsResourceColorPickerModuleTest : SettingsResourceModuleTest<ColorPickerSettings>
{
public SettingsResourceColorPickerModuleTest()
: base(nameof(ModuleType.ColorPicker))
{
}
protected override Action<ColorPickerSettings> GetSettingsModifier()
{
return s =>
{
s.Properties.ShowColorName = !s.Properties.ShowColorName;
s.Properties.ColorHistoryLimit = s.Properties.ColorHistoryLimit == 0 ? 10 : 0;
};
}
}

View File

@@ -1,87 +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.Linq;
using ManagedCommon;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerToys.DSC.Commands;
using PowerToys.DSC.DSCResources;
using PowerToys.DSC.Models;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
[TestClass]
public sealed class SettingsResourceCommandTest : BaseDscTest
{
[TestMethod]
public void Modules_ListAllSupportedModules()
{
// Arrange
var expectedModules = new List<string>()
{
SettingsResource.AppModule,
nameof(ModuleType.AdvancedPaste),
nameof(ModuleType.AlwaysOnTop),
nameof(ModuleType.Awake),
nameof(ModuleType.ColorPicker),
nameof(ModuleType.CropAndLock),
nameof(ModuleType.EnvironmentVariables),
nameof(ModuleType.FancyZones),
nameof(ModuleType.FileLocksmith),
nameof(ModuleType.FindMyMouse),
nameof(ModuleType.Hosts),
nameof(ModuleType.ImageResizer),
nameof(ModuleType.KeyboardManager),
nameof(ModuleType.MouseHighlighter),
nameof(ModuleType.MouseJump),
nameof(ModuleType.MousePointerCrosshairs),
nameof(ModuleType.Peek),
nameof(ModuleType.PowerRename),
nameof(ModuleType.PowerAccent),
nameof(ModuleType.RegistryPreview),
nameof(ModuleType.MeasureTool),
nameof(ModuleType.ShortcutGuide),
nameof(ModuleType.PowerOCR),
nameof(ModuleType.Workspaces),
nameof(ModuleType.ZoomIt),
};
// Act
var result = ExecuteDscCommand<ModulesCommand>("--resource", SettingsResource.ResourceName);
// Assert
Assert.IsTrue(result.Success);
Assert.AreEqual(string.Join(Environment.NewLine, expectedModules.Order()), result.Output.Trim());
}
[TestMethod]
public void Set_EmptyInput_Fail()
{
// Act
var result = ExecuteDscCommand<SetCommand>("--resource", SettingsResource.ResourceName, "--module", "Awake");
var messages = result.Messages();
// Assert
Assert.IsFalse(result.Success);
Assert.AreEqual(1, messages.Count);
Assert.AreEqual(DscMessageLevel.Error, messages[0].Level);
Assert.AreEqual(GetResourceString("InputEmptyOrNullError"), messages[0].Message);
}
[TestMethod]
public void Test_EmptyInput_Fail()
{
// Act
var result = ExecuteDscCommand<TestCommand>("--resource", SettingsResource.ResourceName, "--module", "Awake");
var messages = result.Messages();
// Assert
Assert.IsFalse(result.Success);
Assert.AreEqual(1, messages.Count);
Assert.AreEqual(DscMessageLevel.Error, messages[0].Level);
Assert.AreEqual(GetResourceString("InputEmptyOrNullError"), messages[0].Message);
}
}

View File

@@ -1,34 +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 ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
[TestClass]
public sealed class SettingsResourceCropAndLockModuleTest : SettingsResourceModuleTest<CropAndLockSettings>
{
public SettingsResourceCropAndLockModuleTest()
: base(nameof(ModuleType.CropAndLock))
{
}
protected override Action<CropAndLockSettings> GetSettingsModifier()
{
return s =>
{
s.Properties.ThumbnailHotkey = new KeyboardKeysProperty()
{
Value = new HotkeySettings
{
Key = "mock",
Alt = true,
},
};
};
}
}

View File

@@ -1,267 +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.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerToys.DSC.Commands;
using PowerToys.DSC.DSCResources;
using PowerToys.DSC.Models.ResourceObjects;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
public abstract class SettingsResourceModuleTest<TSettingsConfig> : BaseDscTest
where TSettingsConfig : ISettingsConfig, new()
{
private readonly SettingsUtils _settingsUtils = new();
private TSettingsConfig _originalSettings;
protected TSettingsConfig DefaultSettings => new();
protected string Module { get; }
protected List<string> DiffSettings { get; } = [SettingsResourceObject<AwakeSettings>.SettingsJsonPropertyName];
protected List<string> DiffEmpty { get; } = [];
public SettingsResourceModuleTest(string module)
{
Module = module;
}
[TestInitialize]
public void TestInitialize()
{
_originalSettings = GetSettings();
ResetSettingsToDefaultValues();
}
[TestCleanup]
public void TestCleanup()
{
SaveSettings(_originalSettings);
}
[TestMethod]
public void Get_Success()
{
// Arrange
var settingsBeforeExecute = GetSettings();
// Act
var result = ExecuteDscCommand<GetCommand>("--resource", SettingsResource.ResourceName, "--module", Module);
var state = result.OutputState<SettingsResourceObject<TSettingsConfig>>();
// Assert
Assert.IsTrue(result.Success);
AssertSettingsAreEqual(settingsBeforeExecute, GetSettings());
AssertStateAndSettingsAreEqual(settingsBeforeExecute, state);
}
[TestMethod]
public void Export_Success()
{
// Arrange
var settingsBeforeExecute = GetSettings();
// Act
var result = ExecuteDscCommand<ExportCommand>("--resource", SettingsResource.ResourceName, "--module", Module);
var state = result.OutputState<SettingsResourceObject<TSettingsConfig>>();
// Assert
Assert.IsTrue(result.Success);
AssertSettingsAreEqual(settingsBeforeExecute, GetSettings());
AssertStateAndSettingsAreEqual(settingsBeforeExecute, state);
}
[TestMethod]
public void SetWithDiff_Success()
{
// Arrange
var settingsModifier = GetSettingsModifier();
var input = CreateInputResourceObject(settingsModifier);
// Act
var result = ExecuteDscCommand<SetCommand>("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input);
var (state, diff) = result.OutputStateAndDiff<SettingsResourceObject<TSettingsConfig>>();
// Assert
Assert.IsTrue(result.Success);
AssertSettingsHasChanged(settingsModifier);
AssertStateAndSettingsAreEqual(GetSettings(), state);
CollectionAssert.AreEqual(DiffSettings, diff);
}
[TestMethod]
public void SetWithoutDiff_Success()
{
// Arrange
var settingsModifier = GetSettingsModifier();
UpdateSettings(settingsModifier);
var settingsBeforeExecute = GetSettings();
var input = CreateInputResourceObject(settingsModifier);
// Act
var result = ExecuteDscCommand<SetCommand>("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input);
var (state, diff) = result.OutputStateAndDiff<SettingsResourceObject<TSettingsConfig>>();
// Assert
Assert.IsTrue(result.Success);
AssertSettingsAreEqual(settingsBeforeExecute, GetSettings());
AssertStateAndSettingsAreEqual(settingsBeforeExecute, state);
CollectionAssert.AreEqual(DiffEmpty, diff);
}
[TestMethod]
public void TestWithDiff_Success()
{
// Arrange
var settingsModifier = GetSettingsModifier();
var settingsBeforeExecute = GetSettings();
var input = CreateInputResourceObject(settingsModifier);
// Act
var result = ExecuteDscCommand<TestCommand>("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input);
var (state, diff) = result.OutputStateAndDiff<SettingsResourceObject<TSettingsConfig>>();
// Assert
Assert.IsTrue(result.Success);
AssertSettingsAreEqual(settingsBeforeExecute, GetSettings());
AssertStateAndSettingsAreEqual(settingsBeforeExecute, state);
CollectionAssert.AreEqual(DiffSettings, diff);
Assert.IsFalse(state.InDesiredState);
}
[TestMethod]
public void TestWithoutDiff_Success()
{
// Arrange
var settingsModifier = GetSettingsModifier();
UpdateSettings(settingsModifier);
var settingsBeforeExecute = GetSettings();
var input = CreateInputResourceObject(settingsModifier);
// Act
var result = ExecuteDscCommand<TestCommand>("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input);
var (state, diff) = result.OutputStateAndDiff<SettingsResourceObject<TSettingsConfig>>();
// Assert
Assert.IsTrue(result.Success);
AssertSettingsAreEqual(settingsBeforeExecute, GetSettings());
AssertStateAndSettingsAreEqual(settingsBeforeExecute, state);
CollectionAssert.AreEqual(DiffEmpty, diff);
Assert.IsTrue(state.InDesiredState);
}
/// <summary>
/// Gets the settings modifier action for the specific settings configuration.
/// </summary>
/// <returns>An action that modifies the settings configuration.</returns>
protected abstract Action<TSettingsConfig> GetSettingsModifier();
/// <summary>
/// Resets the settings to default values.
/// </summary>
private void ResetSettingsToDefaultValues()
{
SaveSettings(DefaultSettings);
}
/// <summary>
/// Get the settings for the specified module.
/// </summary>
/// <returns>An instance of the settings type with the current configuration.</returns>
private TSettingsConfig GetSettings()
{
return _settingsUtils.GetSettingsOrDefault<TSettingsConfig>(DefaultSettings.GetModuleName());
}
/// <summary>
/// Saves the settings for the specified module.
/// </summary>
/// <param name="settings">Settings to save.</param>
private void SaveSettings(TSettingsConfig settings)
{
_settingsUtils.SaveSettings(JsonSerializer.Serialize(settings), DefaultSettings.GetModuleName());
}
/// <summary>
/// Create the resource object for the operation.
/// </summary>
/// <param name="settings">Settings to include in the resource object.</param>
/// <returns>A JSON string representing the resource object.</returns>
private string CreateResourceObject(TSettingsConfig settings)
{
var resourceObject = new SettingsResourceObject<TSettingsConfig>
{
Settings = settings,
};
return JsonSerializer.Serialize(resourceObject);
}
private string CreateInputResourceObject(Action<TSettingsConfig> settingsModifier)
{
var settings = DefaultSettings;
settingsModifier(settings);
return CreateResourceObject(settings);
}
/// <summary>
/// Create the response for the Get operation.
/// </summary>
/// <returns>A JSON string representing the response.</returns>
private string CreateGetResponse()
{
return CreateResourceObject(GetSettings());
}
/// <summary>
/// Asserts that the state and settings are equal.
/// </summary>
/// <param name="settings">Settings manifest to compare against.</param>
/// <param name="state">Output state to compare.</param>
private void AssertStateAndSettingsAreEqual(TSettingsConfig settings, SettingsResourceObject<TSettingsConfig> state)
{
AssertSettingsAreEqual(settings, state.Settings);
}
/// <summary>
/// Asserts that two settings manifests are equal.
/// </summary>
/// <param name="expected">Expected settings.</param>
/// <param name="actual">Actual settings.</param>
private void AssertSettingsAreEqual(TSettingsConfig expected, TSettingsConfig actual)
{
var expectedJson = JsonSerializer.SerializeToNode(expected) as JsonObject;
var actualJson = JsonSerializer.SerializeToNode(actual) as JsonObject;
Assert.IsTrue(JsonNode.DeepEquals(expectedJson, actualJson));
}
/// <summary>
/// Asserts that the current settings have changed.
/// </summary>
/// <param name="action">Action to prepare the default settings.</param>
private void AssertSettingsHasChanged(Action<TSettingsConfig> action)
{
var currentSettings = GetSettings();
var defaultSettings = DefaultSettings;
action(defaultSettings);
AssertSettingsAreEqual(defaultSettings, currentSettings);
}
/// <summary>
/// Updates the settings.
/// </summary>
/// <param name="action">Action to modify the settings.</param>
private void UpdateSettings(Action<TSettingsConfig> action)
{
var settings = GetSettings();
action(settings);
SaveSettings(settings);
}
}

View File

@@ -1,120 +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.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.IO;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using PowerToys.DSC.DSCResources;
using PowerToys.DSC.Options;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Base class for all DSC commands.
/// </summary>
public abstract class BaseCommand : Command
{
private static readonly CompositeFormat ModuleNotSupportedByResource = CompositeFormat.Parse(Resources.ModuleNotSupportedByResource);
// Shared options for all commands
private readonly ModuleOption _moduleOption;
private readonly ResourceOption _resourceOption;
private readonly InputOption _inputOption;
// The dictionary of available resources and their factories.
private static readonly Dictionary<string, Func<string?, BaseResource>> _resourceFactories = new()
{
{ SettingsResource.ResourceName, module => new SettingsResource(module) },
// Add other resources here
};
/// <summary>
/// Gets the list of available DSC resources that can be used with the command.
/// </summary>
public static List<string> AvailableResources => [.._resourceFactories.Keys];
/// <summary>
/// Gets the DSC resource to be used by the command.
/// </summary>
protected BaseResource? Resource { get; private set; }
/// <summary>
/// Gets the input JSON provided by the user.
/// </summary>
protected string? Input { get; private set; }
/// <summary>
/// Gets the PowerToys module to be used by the command.
/// </summary>
protected string? Module { get; private set; }
public BaseCommand(string name, string description)
: base(name, description)
{
// Register the common options for all commands
_moduleOption = new ModuleOption();
AddOption(_moduleOption);
_resourceOption = new ResourceOption(AvailableResources);
AddOption(_resourceOption);
_inputOption = new InputOption();
AddOption(_inputOption);
// Register the command handler
this.SetHandler(CommandHandler);
}
/// <summary>
/// Handles the command invocation.
/// </summary>
/// <param name="context">The invocation context containing the parsed command options.</param>
public void CommandHandler(InvocationContext context)
{
Input = context.ParseResult.GetValueForOption(_inputOption);
Module = context.ParseResult.GetValueForOption(_moduleOption);
Resource = ResolvedResource(context);
// Validate the module against the resource's supported modules
var supportedModules = Resource.GetSupportedModules();
if (!string.IsNullOrEmpty(Module) && !supportedModules.Contains(Module))
{
var errorMessage = string.Format(CultureInfo.InvariantCulture, ModuleNotSupportedByResource, Module, Resource.Name);
context.Console.Error.WriteLine(errorMessage);
context.ExitCode = 1;
return;
}
// Continue with the command handler logic
CommandHandlerInternal(context);
}
/// <summary>
/// Handles the command logic internally.
/// </summary>
/// <param name="context">Invocation context containing the parsed command options.</param>
public abstract void CommandHandlerInternal(InvocationContext context);
/// <summary>
/// Resolves the resource from the provided resource name in the context.
/// </summary>
/// <param name="context">Invocation context containing the parsed command options.</param>
/// <returns>The resolved <see cref="BaseResource"/> instance.</returns>
private BaseResource ResolvedResource(InvocationContext context)
{
// Resource option has already been validated before the command
// handler is invoked.
var resourceName = context.ParseResult.GetValueForOption(_resourceOption);
Debug.Assert(!string.IsNullOrEmpty(resourceName), "Resource name must not be null or empty.");
Debug.Assert(_resourceFactories.ContainsKey(resourceName), $"Resource '{resourceName}' is not registered.");
return _resourceFactories[resourceName](Module);
}
}

View File

@@ -1,25 +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.CommandLine.Invocation;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Command to export all state instances.
/// </summary>
public sealed class ExportCommand : BaseCommand
{
public ExportCommand()
: base("export", Resources.ExportCommandDescription)
{
}
/// <inheritdoc/>
public override void CommandHandlerInternal(InvocationContext context)
{
context.ExitCode = Resource!.ExportState(Input) ? 0 : 1;
}
}

View File

@@ -1,25 +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.CommandLine.Invocation;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Command to get the resource state.
/// </summary>
public sealed class GetCommand : BaseCommand
{
public GetCommand()
: base("get", Resources.GetCommandDescription)
{
}
/// <inheritdoc/>
public override void CommandHandlerInternal(InvocationContext context)
{
context.ExitCode = Resource!.GetState(Input) ? 0 : 1;
}
}

View File

@@ -1,34 +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.CommandLine.Invocation;
using PowerToys.DSC.Options;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Command to get the manifest of the DSC resource.
/// </summary>
public sealed class ManifestCommand : BaseCommand
{
/// <summary>
/// Option to specify the output directory for the manifest.
/// </summary>
private readonly OutputDirectoryOption _outputDirectoryOption;
public ManifestCommand()
: base("manifest", Resources.ManifestCommandDescription)
{
_outputDirectoryOption = new OutputDirectoryOption();
AddOption(_outputDirectoryOption);
}
/// <inheritdoc/>
public override void CommandHandlerInternal(InvocationContext context)
{
var outputDir = context.ParseResult.GetValueForOption(_outputDirectoryOption);
context.ExitCode = Resource!.Manifest(outputDir) ? 0 : 1;
}
}

View File

@@ -1,47 +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.CommandLine;
using System.CommandLine.Invocation;
using System.Diagnostics;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Command to get all supported modules for a specific resource.
/// </summary>
/// <remarks>
/// This class is primarily used for debugging purposes and for build scripts.
/// </remarks>
public sealed class ModulesCommand : BaseCommand
{
public ModulesCommand()
: base("modules", Resources.ModulesCommandDescription)
{
}
/// <inheritdoc/>
public override void CommandHandlerInternal(InvocationContext context)
{
// Module is optional, if not provided, all supported modules for the
// resource will be printed. If provided, it must be one of the
// supported modules since it has been validated before this command is
// executed.
if (!string.IsNullOrEmpty(Module))
{
Debug.Assert(Resource!.GetSupportedModules().Contains(Module), "Module must be present in the list of supported modules.");
context.Console.WriteLine(Module);
}
else
{
// Print the supported modules for the specified resource
foreach (var module in Resource!.GetSupportedModules())
{
context.Console.WriteLine(module);
}
}
}
}

View File

@@ -1,25 +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.CommandLine.Invocation;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Command to output the schema of the resource.
/// </summary>
public sealed class SchemaCommand : BaseCommand
{
public SchemaCommand()
: base("schema", Resources.SchemaCommandDescription)
{
}
/// <inheritdoc/>
public override void CommandHandlerInternal(InvocationContext context)
{
context.ExitCode = Resource!.Schema() ? 0 : 1;
}
}

View File

@@ -1,25 +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.CommandLine.Invocation;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Command to set the resource state.
/// </summary>
public sealed class SetCommand : BaseCommand
{
public SetCommand()
: base("set", Resources.SetCommandDescription)
{
}
/// <inheritdoc/>
public override void CommandHandlerInternal(InvocationContext context)
{
context.ExitCode = Resource!.SetState(Input) ? 0 : 1;
}
}

View File

@@ -1,25 +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.CommandLine.Invocation;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Command to test the resource state.
/// </summary>
public sealed class TestCommand : BaseCommand
{
public TestCommand()
: base("test", Resources.TestCommandDescription)
{
}
/// <inheritdoc/>
public override void CommandHandlerInternal(InvocationContext context)
{
context.ExitCode = Resource!.TestState(Input) ? 0 : 1;
}
}

View File

@@ -1,134 +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.Text.Json.Nodes;
using PowerToys.DSC.Models;
namespace PowerToys.DSC.DSCResources;
/// <summary>
/// Base class for all DSC resources.
/// </summary>
public abstract class BaseResource
{
/// <summary>
/// Gets the name of the resource.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the module being used by the resource, if provided.
/// </summary>
public string? Module { get; }
public BaseResource(string name, string? module)
{
Name = name;
Module = module;
}
/// <summary>
/// Calls the get method on the resource.
/// </summary>
/// <param name="input">The input string, if any.</param>
/// <returns>True if the operation was successful; otherwise false.</returns>
public abstract bool GetState(string? input);
/// <summary>
/// Calls the set method on the resource.
/// </summary>
/// <param name="input">The input string, if any.</param>
/// <returns>True if the operation was successful; otherwise false.</returns>
public abstract bool SetState(string? input);
/// <summary>
/// Calls the test method on the resource.
/// </summary>
/// <param name="input">The input string, if any.</param>
/// <returns>True if the operation was successful; otherwise false.</returns>
public abstract bool TestState(string? input);
/// <summary>
/// Calls the export method on the resource.
/// </summary>
/// <param name="input"> The input string, if any.</param>
/// <returns>True if the operation was successful; otherwise false.</returns>
public abstract bool ExportState(string? input);
/// <summary>
/// Calls the schema method on the resource.
/// </summary>
/// <returns>True if the operation was successful; otherwise false.</returns>
public abstract bool Schema();
/// <summary>
/// Generates a DSC resource JSON manifest for the resource. If the
/// outputDir is not provided, the manifest will be printed to the console.
/// </summary>
/// <param name="outputDir"> The directory where the manifest should be
/// saved. If null, the manifest will be printed to the console.</param>
/// <returns>True if the manifest was successfully generated and saved,otherwise false.</returns>
public abstract bool Manifest(string? outputDir);
/// <summary>
/// Gets the list of supported modules for the resource.
/// </summary>
/// <returns>Gets a list of supported modules.</returns>
public abstract IList<string> GetSupportedModules();
/// <summary>
/// Writes a JSON output line to the console.
/// </summary>
/// <param name="output">The JSON output to write.</param>
protected void WriteJsonOutputLine(JsonNode output)
{
var json = output.ToJsonString(new() { WriteIndented = false });
WriteJsonOutputLine(json);
}
/// <summary>
/// Writes a JSON output line to the console.
/// </summary>
/// <param name="output">The JSON output to write.</param>
protected void WriteJsonOutputLine(string output)
{
Console.WriteLine(output);
}
/// <summary>
/// Writes a message output line to the console with the specified message level.
/// </summary>
/// <param name="level">The level of the message.</param>
/// <param name="message">The message to write.</param>
protected void WriteMessageOutputLine(DscMessageLevel level, string message)
{
var messageObj = new Dictionary<string, string>
{
[GetMessageLevel(level)] = message,
};
var messageJson = System.Text.Json.JsonSerializer.Serialize(messageObj);
Console.Error.WriteLine(messageJson);
}
/// <summary>
/// Gets the message level as a string based on the provided dsc message level enum value.
/// </summary>
/// <param name="level">The dsc message level.</param>
/// <returns>A string representation of the message level.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the provided message level is not recognized.</exception>
private static string GetMessageLevel(DscMessageLevel level)
{
return level switch
{
DscMessageLevel.Error => "error",
DscMessageLevel.Warning => "warn",
DscMessageLevel.Info => "info",
DscMessageLevel.Debug => "debug",
DscMessageLevel.Trace => "trace",
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null),
};
}
}

View File

@@ -1,248 +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;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using PowerToys.DSC.Models;
using PowerToys.DSC.Models.FunctionData;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.DSCResources;
/// <summary>
/// Represents the DSC resource for managing PowerToys settings.
/// </summary>
public sealed class SettingsResource : BaseResource
{
private static readonly CompositeFormat FailedToWriteManifests = CompositeFormat.Parse(Resources.FailedToWriteManifests);
public const string AppModule = "App";
public const string ResourceName = "settings";
private readonly Dictionary<string, Func<string?, ISettingsFunctionData>> _moduleFunctionData;
public string ModuleOrDefault => string.IsNullOrEmpty(Module) ? AppModule : Module;
public SettingsResource(string? module)
: base(ResourceName, module)
{
_moduleFunctionData = new()
{
{ AppModule, CreateModuleFunctionData<GeneralSettings> },
{ nameof(ModuleType.AdvancedPaste), CreateModuleFunctionData<AdvancedPasteSettings> },
{ nameof(ModuleType.AlwaysOnTop), CreateModuleFunctionData<AlwaysOnTopSettings> },
{ nameof(ModuleType.Awake), CreateModuleFunctionData<AwakeSettings> },
{ nameof(ModuleType.ColorPicker), CreateModuleFunctionData<ColorPickerSettings> },
{ nameof(ModuleType.CropAndLock), CreateModuleFunctionData<CropAndLockSettings> },
{ nameof(ModuleType.EnvironmentVariables), CreateModuleFunctionData<EnvironmentVariablesSettings> },
{ nameof(ModuleType.FancyZones), CreateModuleFunctionData<FancyZonesSettings> },
{ nameof(ModuleType.FileLocksmith), CreateModuleFunctionData<FileLocksmithSettings> },
{ nameof(ModuleType.FindMyMouse), CreateModuleFunctionData<FindMyMouseSettings> },
{ nameof(ModuleType.Hosts), CreateModuleFunctionData<HostsSettings> },
{ nameof(ModuleType.ImageResizer), CreateModuleFunctionData<ImageResizerSettings> },
{ nameof(ModuleType.KeyboardManager), CreateModuleFunctionData<KeyboardManagerSettings> },
{ nameof(ModuleType.MouseHighlighter), CreateModuleFunctionData<MouseHighlighterSettings> },
{ nameof(ModuleType.MouseJump), CreateModuleFunctionData<MouseJumpSettings> },
{ nameof(ModuleType.MousePointerCrosshairs), CreateModuleFunctionData<MousePointerCrosshairsSettings> },
{ nameof(ModuleType.Peek), CreateModuleFunctionData<PeekSettings> },
{ nameof(ModuleType.PowerRename), CreateModuleFunctionData<PowerRenameSettings> },
{ nameof(ModuleType.PowerAccent), CreateModuleFunctionData<PowerAccentSettings> },
{ nameof(ModuleType.RegistryPreview), CreateModuleFunctionData<RegistryPreviewSettings> },
{ nameof(ModuleType.MeasureTool), CreateModuleFunctionData<MeasureToolSettings> },
{ nameof(ModuleType.ShortcutGuide), CreateModuleFunctionData<ShortcutGuideSettings> },
{ nameof(ModuleType.PowerOCR), CreateModuleFunctionData<PowerOcrSettings> },
{ nameof(ModuleType.Workspaces), CreateModuleFunctionData<WorkspacesSettings> },
{ nameof(ModuleType.ZoomIt), CreateModuleFunctionData<ZoomItSettings> },
// The following modules are not currently supported:
// - MouseWithoutBorders Contains sensitive configuration values, making export/import potentially insecure.
// - PowerLauncher Uses absolute file paths in its settings, which are not portable across systems.
// - NewPlus Uses absolute file paths in its settings, which are not portable across systems.
};
}
/// <inheritdoc/>
public override bool ExportState(string? input)
{
var data = CreateFunctionData();
data.GetState();
WriteJsonOutputLine(data.Output.ToJson());
return true;
}
/// <inheritdoc/>
public override bool GetState(string? input)
{
return ExportState(input);
}
/// <inheritdoc/>
public override bool SetState(string? input)
{
if (string.IsNullOrEmpty(input))
{
WriteMessageOutputLine(DscMessageLevel.Error, Resources.InputEmptyOrNullError);
return false;
}
var data = CreateFunctionData(input);
data.GetState();
// Capture the diff before updating the output
var diff = data.GetDiffJson();
// Only call Set if the desired state is different from the current state
if (!data.TestState())
{
var inputSettings = data.Input.SettingsInternal;
data.Output.SettingsInternal = inputSettings;
data.SetState();
}
WriteJsonOutputLine(data.Output.ToJson());
WriteJsonOutputLine(diff);
return true;
}
/// <inheritdoc/>
public override bool TestState(string? input)
{
if (string.IsNullOrEmpty(input))
{
WriteMessageOutputLine(DscMessageLevel.Error, Resources.InputEmptyOrNullError);
return false;
}
var data = CreateFunctionData(input);
data.GetState();
data.Output.InDesiredState = data.TestState();
WriteJsonOutputLine(data.Output.ToJson());
WriteJsonOutputLine(data.GetDiffJson());
return true;
}
/// <inheritdoc/>
public override bool Schema()
{
var data = CreateFunctionData();
WriteJsonOutputLine(data.Schema());
return true;
}
/// <inheritdoc/>
/// <remarks>
/// If an output directory is specified, write the manifests to files,
/// otherwise output them to the console.
/// </remarks>
public override bool Manifest(string? outputDir)
{
var manifests = GenerateManifests();
if (!string.IsNullOrEmpty(outputDir))
{
try
{
foreach (var (name, manifest) in manifests)
{
File.WriteAllText(Path.Combine(outputDir, $"microsoft.powertoys.{name}.settings.dsc.resource.json"), manifest);
}
}
catch (Exception ex)
{
var errorMessage = string.Format(CultureInfo.InvariantCulture, FailedToWriteManifests, outputDir, ex.Message);
WriteMessageOutputLine(DscMessageLevel.Error, errorMessage);
return false;
}
}
else
{
foreach (var (_, manifest) in manifests)
{
WriteJsonOutputLine(manifest);
}
}
return true;
}
/// <summary>
/// Generates manifests for the specified module or all supported modules
/// if no module is specified.
/// </summary>
/// <returns>A list of tuples containing the module name and its corresponding manifest JSON.</returns>
private List<(string Name, string Manifest)> GenerateManifests()
{
List<(string Name, string Manifest)> manifests = [];
if (!string.IsNullOrEmpty(Module))
{
manifests.Add((Module, GenerateManifest(Module)));
}
else
{
foreach (var module in GetSupportedModules())
{
manifests.Add((module, GenerateManifest(module)));
}
}
return manifests;
}
/// <summary>
/// Generate a DSC resource JSON manifest for the specified module.
/// </summary>
/// <param name="module">The name of the module for which to generate the manifest.</param>
/// <returns>A JSON string representing the DSC resource manifest.</returns>
private string GenerateManifest(string module)
{
// Note: The description is not localized because the generated
// manifest file will be part of the package
return new DscManifest($"{module}Settings", "0.1.0")
.AddDescription($"Allows management of {module} settings state via the DSC v3 command line interface protocol.")
.AddStdinMethod("export", ["export", "--module", module, "--resource", "settings"])
.AddStdinMethod("get", ["get", "--module", module, "--resource", "settings"])
.AddJsonInputMethod("set", "--input", ["set", "--module", module, "--resource", "settings"], implementsPretest: true, stateAndDiff: true)
.AddJsonInputMethod("test", "--input", ["test", "--module", module, "--resource", "settings"], stateAndDiff: true)
.AddCommandMethod("schema", ["schema", "--module", module, "--resource", "settings"])
.ToJson();
}
/// <inheritdoc/>
public override IList<string> GetSupportedModules()
{
return [.. _moduleFunctionData.Keys.Order()];
}
/// <summary>
/// Creates the function data for the specified module or the default module if none is specified.
/// </summary>
/// <param name="input">The input string, if any.</param>
/// <returns>An instance of <see cref="ISettingsFunctionData"/> for the specified module.</returns>
public ISettingsFunctionData CreateFunctionData(string? input = null)
{
Debug.Assert(_moduleFunctionData.ContainsKey(ModuleOrDefault), "Module should be supported by the resource.");
return _moduleFunctionData[ModuleOrDefault](input);
}
/// <summary>
/// Creates the function data for a specific settings configuration type.
/// </summary>
/// <typeparam name="TSettingsConfig">The type of settings configuration to create function data for.</typeparam>
/// <param name="input">The input string, if any.</param>
/// <returns>An instance of <see cref="ISettingsFunctionData"/> for the specified settings configuration type.</returns>
private ISettingsFunctionData CreateModuleFunctionData<TSettingsConfig>(string? input)
where TSettingsConfig : ISettingsConfig, new()
{
return new SettingsFunctionData<TSettingsConfig>(input);
}
}

View File

@@ -1,152 +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.Collections.Generic;
using System.Text.Json.Nodes;
namespace PowerToys.DSC.Models;
/// <summary>
/// Class for building a DSC manifest for PowerToys resources.
/// </summary>
public sealed class DscManifest
{
private const string Schema = "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.vscode.json";
private const string Executable = @"PowerToys.DSC.exe";
private readonly string _type;
private readonly string _version;
private readonly JsonObject _manifest;
public DscManifest(string type, string version)
{
_type = type;
_version = version;
_manifest = new JsonObject
{
["$schema"] = Schema,
["type"] = $"Microsoft.PowerToys/{_type}",
["version"] = _version,
["tags"] = new JsonArray("PowerToys"),
};
}
/// <summary>
/// Adds a description to the manifest.
/// </summary>
/// <param name="description">The description to add.</param>
/// <returns>Returns the current instance of <see cref="DscManifest"/>.</returns>
public DscManifest AddDescription(string description)
{
_manifest["description"] = description;
return this;
}
/// <summary>
/// Adds a method to the manifest with the specified executable and arguments.
/// </summary>
/// <param name="method">The name of the method to add.</param>
/// <param name="inputArg">The input argument for the method</param>
/// <param name="args">The list of arguments for the method.</param>
/// <param name="implementsPretest">Whether the method implements a pretest.</param>
/// <param name="stateAndDiff">Whether the method returns state and diff.</param>
/// <returns>Returns the current instance of <see cref="DscManifest"/>.</returns>
public DscManifest AddJsonInputMethod(string method, string inputArg, List<string> args, bool? implementsPretest = null, bool? stateAndDiff = null)
{
var argsJson = CreateJsonArray(args);
argsJson.Add(new JsonObject
{
["jsonInputArg"] = inputArg,
["mandatory"] = true,
});
var methodObject = AddMethod(argsJson, implementsPretest, stateAndDiff);
_manifest[method] = methodObject;
return this;
}
/// <summary>
/// Adds a method to the manifest that reads from standard input (stdin).
/// </summary>
/// <param name="method">The name of the method to add.</param>
/// <param name="args">The list of arguments for the method.</param>
/// <param name="implementsPretest">Whether the method implements a pretest.</param>
/// <param name="stateAndDiff">Whether the method returns state and diff.</param>
/// <returns>Returns the current instance of <see cref="DscManifest"/>.</returns>
public DscManifest AddStdinMethod(string method, List<string> args, bool? implementsPretest = null, bool? stateAndDiff = null)
{
var argsJson = CreateJsonArray(args);
var methodObject = AddMethod(argsJson, implementsPretest, stateAndDiff);
methodObject["input"] = "stdin";
_manifest[method] = methodObject;
return this;
}
/// <summary>
/// Adds a command method to the manifest.
/// </summary>
/// <param name="method">The name of the method to add.</param>
/// <param name="args">The list of arguments for the method.</param>
/// <returns>Returns the current instance of <see cref="DscManifest"/>.</returns>
public DscManifest AddCommandMethod(string method, List<string> args)
{
_manifest[method] = new JsonObject
{
["command"] = AddMethod(CreateJsonArray(args)),
};
return this;
}
/// <summary>
/// Gets the JSON representation of the manifest.
/// </summary>
/// <returns>Returns the JSON string of the manifest.</returns>
public string ToJson()
{
return _manifest.ToJsonString(new() { WriteIndented = true });
}
/// <summary>
/// Add a method to the manifest with the specified arguments.
/// </summary>
/// <param name="args">The list of arguments for the method.</param>
/// <param name="implementsPretest">Whether the method implements a pretest.</param>
/// <param name="stateAndDiff">Whether the method returns state and diff.</param>
/// <returns>Returns the method object.</returns>
private JsonObject AddMethod(JsonArray args, bool? implementsPretest = null, bool? stateAndDiff = null)
{
var methodObject = new JsonObject
{
["executable"] = Executable,
["args"] = args,
};
if (implementsPretest.HasValue)
{
methodObject["implementsPretest"] = implementsPretest.Value;
}
if (stateAndDiff.HasValue)
{
methodObject["return"] = stateAndDiff.Value ? "stateAndDiff" : "state";
}
return methodObject;
}
/// <summary>
/// Creates a JSON array from a list of strings.
/// </summary>
/// <param name="args">The list of strings to convert.</param>
/// <returns>Returns the JSON array.</returns>
private JsonArray CreateJsonArray(List<string> args)
{
var jsonArray = new JsonArray();
foreach (var arg in args)
{
jsonArray.Add(arg);
}
return jsonArray;
}
}

View File

@@ -1,36 +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.
namespace PowerToys.DSC.Models;
/// <summary>
/// Specifies the severity level of a message.
/// </summary>
public enum DscMessageLevel
{
/// <summary>
/// Represents an error message.
/// </summary>
Error,
/// <summary>
/// Represents a warning message.
/// </summary>
Warning,
/// <summary>
/// Represents an informational message.
/// </summary>
Info,
/// <summary>
/// Represents a debug message.
/// </summary>
Debug,
/// <summary>
/// Represents a trace message.
/// </summary>
Trace,
}

View File

@@ -1,36 +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 Newtonsoft.Json;
using NJsonSchema.Generation;
using PowerToys.DSC.Models.ResourceObjects;
namespace PowerToys.DSC.Models.FunctionData;
/// <summary>
/// Base class for function data objects.
/// </summary>
public class BaseFunctionData
{
/// <summary>
/// Generates a JSON schema for the specified resource object type.
/// </summary>
/// <typeparam name="T">The type of the resource object.</typeparam>
/// <returns>A JSON schema string.</returns>
protected static string GenerateSchema<T>()
where T : BaseResourceObject
{
var settings = new SystemTextJsonSchemaGeneratorSettings()
{
FlattenInheritanceHierarchy = true,
SerializerOptions =
{
IgnoreReadOnlyFields = true,
},
};
var generator = new JsonSchemaGenerator(settings);
var schema = generator.Generate(typeof(T));
return schema.ToJson(Formatting.None);
}
}

View File

@@ -1,52 +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.Text.Json.Nodes;
using PowerToys.DSC.Models.ResourceObjects;
namespace PowerToys.DSC.Models.FunctionData;
/// <summary>
/// Interface for function data related to settings.
/// </summary>
public interface ISettingsFunctionData
{
/// <summary>
/// Gets the input settings resource object.
/// </summary>
public ISettingsResourceObject Input { get; }
/// <summary>
/// Gets the output settings resource object.
/// </summary>
public ISettingsResourceObject Output { get; }
/// <summary>
/// Gets the current settings.
/// </summary>
public void GetState();
/// <summary>
/// Sets the current settings.
/// </summary>
public void SetState();
/// <summary>
/// Tests if the current settings and the desired state are valid.
/// </summary>
/// <returns>True if the current settings match the desired state; otherwise false.</returns>
public bool TestState();
/// <summary>
/// Gets the difference between the current settings and the desired state in JSON format.
/// </summary>
/// <returns>A JSON array representing the differences.</returns>
public JsonArray GetDiffJson();
/// <summary>
/// Gets the schema for the settings resource object.
/// </summary>
/// <returns></returns>
public string Schema();
}

View File

@@ -1,96 +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.Diagnostics;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using PowerToys.DSC.Models.ResourceObjects;
namespace PowerToys.DSC.Models.FunctionData;
/// <summary>
/// Represents function data for the settings DSC resource.
/// </summary>
/// <typeparam name="TSettingsConfig">The module settings configuration type.</typeparam>
public sealed class SettingsFunctionData<TSettingsConfig> : BaseFunctionData, ISettingsFunctionData
where TSettingsConfig : ISettingsConfig, new()
{
private static readonly SettingsUtils _settingsUtils = new();
private static readonly TSettingsConfig _settingsConfig = new();
private readonly SettingsResourceObject<TSettingsConfig> _input;
private readonly SettingsResourceObject<TSettingsConfig> _output;
/// <inheritdoc/>
public ISettingsResourceObject Input => _input;
/// <inheritdoc/>
public ISettingsResourceObject Output => _output;
public SettingsFunctionData(string? input = null)
{
_output = new();
_input = string.IsNullOrEmpty(input) ? new() : JsonSerializer.Deserialize<SettingsResourceObject<TSettingsConfig>>(input) ?? new();
}
/// <inheritdoc/>
public void GetState()
{
_output.Settings = GetSettings();
}
/// <inheritdoc/>
public void SetState()
{
Debug.Assert(_output.Settings != null, "Output settings should not be null");
SaveSettings(_output.Settings);
}
/// <inheritdoc/>
public bool TestState()
{
var input = JsonSerializer.SerializeToNode(_input.Settings);
var output = JsonSerializer.SerializeToNode(_output.Settings);
return JsonNode.DeepEquals(input, output);
}
/// <inheritdoc/>
public JsonArray GetDiffJson()
{
var diff = new JsonArray();
if (!TestState())
{
diff.Add(SettingsResourceObject<TSettingsConfig>.SettingsJsonPropertyName);
}
return diff;
}
/// <inheritdoc/>
public string Schema()
{
return GenerateSchema<SettingsResourceObject<TSettingsConfig>>();
}
/// <summary>
/// Gets the settings configuration from the settings utils for a specific module.
/// </summary>
/// <returns>The settings configuration for the module.</returns>
private static TSettingsConfig GetSettings()
{
return _settingsUtils.GetSettingsOrDefault<TSettingsConfig>(_settingsConfig.GetModuleName());
}
/// <summary>
/// Saves the settings configuration to the settings utils for a specific module.
/// </summary>
/// <param name="settings">Settings of a specific module</param>
private static void SaveSettings(TSettingsConfig settings)
{
var inputJson = JsonSerializer.Serialize(settings);
_settingsUtils.SaveSettings(inputJson, _settingsConfig.GetModuleName());
}
}

View File

@@ -1,45 +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.ComponentModel;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace PowerToys.DSC.Models.ResourceObjects;
/// <summary>
/// Base class for all resource objects.
/// </summary>
public class BaseResourceObject
{
private readonly JsonSerializerOptions _options;
public BaseResourceObject()
{
_options = new()
{
WriteIndented = false,
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
};
}
/// <summary>
/// Gets or sets whether an instance is in the desired state.
/// </summary>
[JsonPropertyName("_inDesiredState")]
[Description("Indicates whether an instance is in the desired state")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? InDesiredState { get; set; }
/// <summary>
/// Generates a JSON representation of the resource object.
/// </summary>
/// <returns></returns>
public JsonNode ToJson()
{
return JsonSerializer.SerializeToNode(this, GetType(), _options) ?? new JsonObject();
}
}

View File

@@ -1,30 +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.Text.Json.Nodes;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace PowerToys.DSC.Models.ResourceObjects;
/// <summary>
/// Interface for settings resource objects.
/// </summary>
public interface ISettingsResourceObject
{
/// <summary>
/// Gets or sets the settings configuration.
/// </summary>
public ISettingsConfig SettingsInternal { get; set; }
/// <summary>
/// Gets or sets whether an instance is in the desired state.
/// </summary>
public bool? InDesiredState { get; set; }
/// <summary>
/// Generates a JSON representation of the resource object.
/// </summary>
/// <returns>String representation of the resource object in JSON format.</returns>
public JsonNode ToJson();
}

View File

@@ -1,34 +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.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using NJsonSchema.Annotations;
namespace PowerToys.DSC.Models.ResourceObjects;
/// <summary>
/// Represents a settings resource object for a module's settings configuration.
/// </summary>
/// <typeparam name="TSettingsConfig">The type of the settings configuration.</typeparam>
public sealed class SettingsResourceObject<TSettingsConfig> : BaseResourceObject, ISettingsResourceObject
where TSettingsConfig : ISettingsConfig, new()
{
public const string SettingsJsonPropertyName = "settings";
/// <summary>
/// Gets or sets the settings content for the module.
/// </summary>
[JsonPropertyName(SettingsJsonPropertyName)]
[Required]
[Description("The settings content for the module.")]
[JsonSchemaType(typeof(object))]
public TSettingsConfig Settings { get; set; } = new();
/// <inheritdoc/>
[JsonIgnore]
public ISettingsConfig SettingsInternal { get => Settings; set => Settings = (TSettingsConfig)value; }
}

View File

@@ -1,51 +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.CommandLine;
using System.CommandLine.Parsing;
using System.Globalization;
using System.Text;
using System.Text.Json;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Options;
/// <summary>
/// Represents an option for specifying JSON input for the dsc command.
/// </summary>
public sealed class InputOption : Option<string>
{
private static readonly CompositeFormat InvalidJsonInputError = CompositeFormat.Parse(Resources.InvalidJsonInputError);
public InputOption()
: base("--input", Resources.InputOptionDescription)
{
AddValidator(OptionValidator);
}
/// <summary>
/// Validates the JSON input provided to the option.
/// </summary>
/// <param name="result">The option result to validate.</param>
private void OptionValidator(OptionResult result)
{
var value = result.GetValueOrDefault<string>() ?? string.Empty;
if (string.IsNullOrEmpty(value))
{
result.ErrorMessage = Resources.InputEmptyOrNullError;
}
else
{
try
{
JsonDocument.Parse(value);
}
catch (Exception e)
{
result.ErrorMessage = string.Format(CultureInfo.InvariantCulture, InvalidJsonInputError, e.Message);
}
}
}
}

View File

@@ -1,19 +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.CommandLine;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Options;
/// <summary>
/// Represents an option for specifying the module name for the dsc command.
/// </summary>
public sealed class ModuleOption : Option<string?>
{
public ModuleOption()
: base("--module", Resources.ModuleOptionDescription)
{
}
}

View File

@@ -1,43 +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.CommandLine;
using System.CommandLine.Parsing;
using System.Globalization;
using System.IO;
using System.Text;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Options;
/// <summary>
/// Represents an option for specifying the output directory for the dsc command.
/// </summary>
public sealed class OutputDirectoryOption : Option<string>
{
private static readonly CompositeFormat InvalidOutputDirectoryError = CompositeFormat.Parse(Resources.InvalidOutputDirectoryError);
public OutputDirectoryOption()
: base("--outputDir", Resources.OutputDirectoryOptionDescription)
{
AddValidator(OptionValidator);
}
/// <summary>
/// Validates the output directory option.
/// </summary>
/// <param name="result">The option result to validate.</param>
private void OptionValidator(OptionResult result)
{
var value = result.GetValueOrDefault<string>() ?? string.Empty;
if (string.IsNullOrEmpty(value))
{
result.ErrorMessage = Resources.OutputDirectoryEmptyOrNullError;
}
else if (!Directory.Exists(value))
{
result.ErrorMessage = string.Format(CultureInfo.InvariantCulture, InvalidOutputDirectoryError, value);
}
}
}

View File

@@ -1,43 +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.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Globalization;
using System.Text;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Options;
/// <summary>
/// Represents an option for specifying the resource name for the dsc command.
/// </summary>
public sealed class ResourceOption : Option<string>
{
private static readonly CompositeFormat InvalidResourceNameError = CompositeFormat.Parse(Resources.InvalidResourceNameError);
private readonly IList<string> _resources = [];
public ResourceOption(IList<string> resources)
: base("--resource", Resources.ResourceOptionDescription)
{
_resources = resources;
IsRequired = true;
AddValidator(OptionValidator);
}
/// <summary>
/// Validates the resource option to ensure that the specified resource name is valid.
/// </summary>
/// <param name="result">The option result to validate.</param>
private void OptionValidator(OptionResult result)
{
var value = result.GetValueOrDefault<string>() ?? string.Empty;
if (!_resources.Contains(value))
{
result.ErrorMessage = string.Format(CultureInfo.InvariantCulture, InvalidResourceNameError, string.Join(", ", _resources));
}
}
}

View File

@@ -1,48 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<AssemblyName>PowerToys.DSC</AssemblyName>
<AssemblyDescription>PowerToys DSC</AssemblyDescription>
<RootNamespace>PowerToys.DSC</RootNamespace>
<Nullable>enable</Nullable>
<!-- Ensure WindowsDesktop runtime pack is included for consistent WindowsBase.dll version -->
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NJsonSchema" />
<PackageReference Include="System.CommandLine" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<!-- In debug mode, generate the DSC resource JSON files -->
<Target Name="GenerateDscResourceJsonFiles" AfterTargets="Build" Condition="'$(Configuration)' == 'Debug'">
<Message Text="Generating DSC resource JSON files inside ..." Importance="high" />
<Exec Command="dotnet &quot;$(TargetPath)&quot; manifest --resource settings --outputDir &quot;$(TargetDir)\&quot;" />
</Target>
</Project>

View File

@@ -1,29 +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.CommandLine;
using System.CommandLine.Parsing;
using System.Threading.Tasks;
using PowerToys.DSC.Commands;
namespace PowerToys.DSC;
/// <summary>
/// Main entry point for the PowerToys Desired State Configuration CLI application.
/// </summary>
public class Program
{
public static async Task<int> Main(string[] args)
{
var rootCommand = new RootCommand(Properties.Resources.PowerToysDSC);
rootCommand.AddCommand(new GetCommand());
rootCommand.AddCommand(new SetCommand());
rootCommand.AddCommand(new ExportCommand());
rootCommand.AddCommand(new TestCommand());
rootCommand.AddCommand(new SchemaCommand());
rootCommand.AddCommand(new ManifestCommand());
rootCommand.AddCommand(new ModulesCommand());
return await rootCommand.InvokeAsync(args);
}
}

View File

@@ -1,234 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace PowerToys.DSC.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PowerToys.DSC.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Get all state instances.
/// </summary>
internal static string ExportCommandDescription {
get {
return ResourceManager.GetString("ExportCommandDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to write manifests to directory &apos;{0}&apos;: {1}.
/// </summary>
internal static string FailedToWriteManifests {
get {
return ResourceManager.GetString("FailedToWriteManifests", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Get the resource state.
/// </summary>
internal static string GetCommandDescription {
get {
return ResourceManager.GetString("GetCommandDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Input cannot be empty or null.
/// </summary>
internal static string InputEmptyOrNullError {
get {
return ResourceManager.GetString("InputEmptyOrNullError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The JSON input.
/// </summary>
internal static string InputOptionDescription {
get {
return ResourceManager.GetString("InputOptionDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid JSON input: {0}.
/// </summary>
internal static string InvalidJsonInputError {
get {
return ResourceManager.GetString("InvalidJsonInputError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid output directory: {0}.
/// </summary>
internal static string InvalidOutputDirectoryError {
get {
return ResourceManager.GetString("InvalidOutputDirectoryError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid resource name. Valid resource names are: {0}.
/// </summary>
internal static string InvalidResourceNameError {
get {
return ResourceManager.GetString("InvalidResourceNameError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Get the manifest of the dsc resource.
/// </summary>
internal static string ManifestCommandDescription {
get {
return ResourceManager.GetString("ManifestCommandDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Module &apos;{0}&apos; is not supported for the resource {1}. Use the &apos;module&apos; command to list available modules..
/// </summary>
internal static string ModuleNotSupportedByResource {
get {
return ResourceManager.GetString("ModuleNotSupportedByResource", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The module name.
/// </summary>
internal static string ModuleOptionDescription {
get {
return ResourceManager.GetString("ModuleOptionDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Get all supported modules for a specific resource.
/// </summary>
internal static string ModulesCommandDescription {
get {
return ResourceManager.GetString("ModulesCommandDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output directory cannot be empty or null.
/// </summary>
internal static string OutputDirectoryEmptyOrNullError {
get {
return ResourceManager.GetString("OutputDirectoryEmptyOrNullError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The output directory.
/// </summary>
internal static string OutputDirectoryOptionDescription {
get {
return ResourceManager.GetString("OutputDirectoryOptionDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PowerToys Desired State Configuration commands.
/// </summary>
internal static string PowerToysDSC {
get {
return ResourceManager.GetString("PowerToysDSC", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The resource name.
/// </summary>
internal static string ResourceOptionDescription {
get {
return ResourceManager.GetString("ResourceOptionDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Outputs schema of the resource.
/// </summary>
internal static string SchemaCommandDescription {
get {
return ResourceManager.GetString("SchemaCommandDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Set the resource state.
/// </summary>
internal static string SetCommandDescription {
get {
return ResourceManager.GetString("SetCommandDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Test the resource state.
/// </summary>
internal static string TestCommandDescription {
get {
return ResourceManager.GetString("TestCommandDescription", resourceCulture);
}
}
}
}

View File

@@ -1,183 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="PowerToysDSC" xml:space="preserve">
<value>PowerToys Desired State Configuration commands</value>
<comment>{Locked="PowerToys Desired State Configuration"}</comment>
</data>
<data name="ModuleNotSupportedByResource" xml:space="preserve">
<value>Module '{0}' is not supported for the resource {1}. Use the 'module' command to list available modules.</value>
<comment>{Locked="'module'","{0}","{1}"}</comment>
</data>
<data name="ExportCommandDescription" xml:space="preserve">
<value>Get all state instances</value>
</data>
<data name="GetCommandDescription" xml:space="preserve">
<value>Get the resource state</value>
</data>
<data name="ManifestCommandDescription" xml:space="preserve">
<value>Get the manifest of the dsc resource</value>
</data>
<data name="ModulesCommandDescription" xml:space="preserve">
<value>Get all supported modules for a specific resource</value>
</data>
<data name="SchemaCommandDescription" xml:space="preserve">
<value>Outputs schema of the resource</value>
</data>
<data name="SetCommandDescription" xml:space="preserve">
<value>Set the resource state</value>
</data>
<data name="TestCommandDescription" xml:space="preserve">
<value>Test the resource state</value>
</data>
<data name="InputEmptyOrNullError" xml:space="preserve">
<value>Input cannot be empty or null</value>
</data>
<data name="FailedToWriteManifests" xml:space="preserve">
<value>Failed to write manifests to directory '{0}': {1}</value>
<comment>{Locked="{0}","{1}"}</comment>
</data>
<data name="InputOptionDescription" xml:space="preserve">
<value>The JSON input</value>
</data>
<data name="ModuleOptionDescription" xml:space="preserve">
<value>The module name</value>
</data>
<data name="OutputDirectoryOptionDescription" xml:space="preserve">
<value>The output directory</value>
</data>
<data name="ResourceOptionDescription" xml:space="preserve">
<value>The resource name</value>
</data>
<data name="InvalidJsonInputError" xml:space="preserve">
<value>Invalid JSON input: {0}</value>
<comment>{Locked="{0}"}</comment>
</data>
<data name="OutputDirectoryEmptyOrNullError" xml:space="preserve">
<value>Output directory cannot be empty or null</value>
</data>
<data name="InvalidOutputDirectoryError" xml:space="preserve">
<value>Invalid output directory: {0}</value>
<comment>{Locked="{0}"}</comment>
</data>
<data name="InvalidResourceNameError" xml:space="preserve">
<value>Invalid resource name. Valid resource names are: {0}</value>
<comment>{Locked="{0}"}</comment>
</data>
</root>

View File

@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<UseWindowsForms>false</UseWindowsForms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<AssemblyName>PowerToys.MCPServer</AssemblyName>
<AssemblyDescription>PowerToys MCP Server for Model Context Protocol</AssemblyDescription>
<DisableWinExeOutputInference>true</DisableWinExeOutputInference>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
<NoWin32Manifest>true</NoWin32Manifest>
<RootNamespace>PowerToys.MCPServer</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup>
<!-- CsWinRT configuration -->
<PropertyGroup>
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
</PropertyGroup>
<ItemGroup>
<!-- Core dependencies -->
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="ModelContextProtocol " />
<PackageReference Include="Microsoft.Data.Sqlite" />
</ItemGroup>
<ItemGroup>
<!-- PowerToys project references -->
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,34 @@
// 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.ComponentModel;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using PowerToys.MCPServer.Tools;
namespace MCPServer
{
internal sealed class Program
{
private static async Task<int> Main(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(consoleLogOptions =>
{
// Configure all logs to go to stderr
consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
await builder.Build().RunAsync();
return 0;
}
}
}

View File

@@ -0,0 +1,312 @@
// 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.ComponentModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Data.Sqlite;
using Microsoft.PowerToys.Settings.UI.Library;
using ModelContextProtocol.Server;
namespace PowerToys.MCPServer.Tools
{
[McpServerToolType]
public static class AwakeTools
{
[McpServerTool]
[Description("Echoes the message back to the client.")]
public static string SetTimeTest(string message) => $"Hello {message}";
// =============================
// HTTP client (Awake remote control)
// =============================
private static readonly HttpClient _http = new HttpClient();
// Base URL for Awake HTTP server. Default matches Awake --http-port default (8080).
// Allow override through environment variable POWERTOYS_AWAKE_HTTP (e.g. http://localhost:9090/)
private static string BaseUrl => (Environment.GetEnvironmentVariable("POWERTOYS_AWAKE_HTTP") ?? "http://localhost:8080/").TrimEnd('/') + "/";
private static string JsonError(string msg, int? status = null) => JsonSerializer.Serialize(new { success = false, error = msg, status });
private static string JsonOk(object payload) => JsonSerializer.Serialize(payload);
private static string SendAwakeRequest(string method, string relativePath, object? body = null)
{
try
{
using var req = new HttpRequestMessage(new HttpMethod(method), BaseUrl + relativePath.TrimStart('/'));
if (body != null)
{
string json = JsonSerializer.Serialize(body);
req.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
using var resp = _http.Send(req);
string respText = resp.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (!resp.IsSuccessStatusCode)
{
return JsonError($"HTTP {(int)resp.StatusCode} {resp.StatusCode}", (int)resp.StatusCode) + "\n" + respText;
}
return respText;
}
catch (HttpRequestException ex)
{
return JsonError($"Connection failed: {ex.Message}. Ensure Awake is running with --http-server.");
}
catch (Exception ex)
{
return JsonError(ex.Message);
}
}
[McpServerTool]
[Description("RECOMMENDED FOR BUILDS: Intelligently keeps the system awake during build, compile, download, or process operations. Monitors CPU/memory/network activity and prevents sleep only when system is actively working. Automatically returns to sleep when build completes. PREFERRED over indefinite mode for development workflows. Params: cpuThresholdPercent (0-100), memThresholdPercent (0-100), netThresholdKBps (KB/s), sampleIntervalSeconds (>0), inactivityTimeoutSeconds (>0), keepDisplayOn=true|false")]
public static string AwakeHttpActivityBased(uint cpuThresholdPercent = 50, uint memThresholdPercent = 50, uint netThresholdKBps = 10, uint sampleIntervalSeconds = 30, uint inactivityTimeoutSeconds = 300, bool keepDisplayOn = true)
{
if (cpuThresholdPercent > 100)
{
return JsonError("cpuThresholdPercent must be 0-100");
}
if (memThresholdPercent > 100)
{
return JsonError("memThresholdPercent must be 0-100");
}
if (sampleIntervalSeconds == 0)
{
return JsonError("sampleIntervalSeconds must be > 0");
}
if (inactivityTimeoutSeconds == 0)
{
return JsonError("inactivityTimeoutSeconds must be > 0");
}
return SendAwakeRequest("POST", "awake/activity", new
{
cpuThresholdPercent,
memThresholdPercent,
netThresholdKBps,
sampleIntervalSeconds,
inactivityTimeoutSeconds,
keepDisplayOn,
});
}
[McpServerTool]
[Description("Get Awake HTTP status (GET /awake/status). Requires Awake launched with --http-server.")]
public static string AwakeHttpStatus() => SendAwakeRequest("GET", "awake/status");
[McpServerTool]
[Description("Keeps the system awake indefinitely until manually changed. WARNING: Less efficient than activity-based mode for builds. Use only when you need guaranteed continuous awake state regardless of system activity. Params: keepDisplayOn=true|false, processId=0")]
public static string AwakeHttpIndefinite(bool keepDisplayOn = true, int processId = 0)
=> SendAwakeRequest("POST", "awake/indefinite", new { keepDisplayOn, processId });
[McpServerTool]
[Description("Set timed keep-awake via HTTP. Params: seconds (>0), keepDisplayOn=true|false")]
public static string AwakeHttpTimed(uint seconds, bool keepDisplayOn = true)
{
if (seconds == 0)
{
return JsonError("seconds must be > 0");
}
return SendAwakeRequest("POST", "awake/timed", new { seconds, keepDisplayOn });
}
[McpServerTool]
[Description("Set expirable keep-awake via HTTP. Params: expireAt (ISO 8601), keepDisplayOn=true|false")]
public static string AwakeHttpExpirable(string expireAt, bool keepDisplayOn = true)
{
if (string.IsNullOrWhiteSpace(expireAt))
{
return JsonError("expireAt required (ISO 8601)");
}
return SendAwakeRequest("POST", "awake/expirable", new { expireAt, keepDisplayOn });
}
[McpServerTool]
[Description("Set passive mode via HTTP (POST /awake/passive).")]
public static string AwakeHttpPassive() => SendAwakeRequest("POST", "awake/passive");
[McpServerTool]
[Description("Toggle display keep-on via HTTP (POST /awake/display/toggle).")]
public static string AwakeHttpToggleDisplay() => SendAwakeRequest("POST", "awake/display/toggle");
[McpServerTool]
[Description("Get Awake settings via HTTP (GET /awake/settings).")]
public static string AwakeHttpSettings() => SendAwakeRequest("GET", "awake/settings");
[McpServerTool]
[Description("Check current PowerToys Awake mode and configuration. Returns active mode (indefinite, timed, activity-based, or passive), remaining time, thresholds, and display settings. Use to verify if system is being kept awake and what settings are active.")]
public static string AwakeHttpConfig() => SendAwakeRequest("GET", "awake/config");
private sealed class AppUsageRecord
{
[JsonPropertyName("process")]
public string ProcessName { get; set; } = string.Empty;
[JsonPropertyName("totalSeconds")]
public double TotalSeconds { get; set; }
[JsonPropertyName("lastUpdatedUtc")]
public DateTime LastUpdatedUtc { get; set; }
[JsonPropertyName("firstSeenUtc")]
public DateTime FirstSeenUtc { get; set; }
}
[McpServerTool]
[Description("Get top N foreground app usage entries recorded by Awake. Reads usage.sqlite if present (preferred) else legacy usage.json. Parameters: top (default 10), days (default 7). Returns JSON array.")]
public static string GetAwakeUsageSummary(int top = 10, int days = 7)
{
try
{
SettingsUtils utils = new();
string settingsPath = utils.GetSettingsFilePath("Awake");
string directory = Path.GetDirectoryName(settingsPath)!;
string sqlitePath = Path.Combine(directory, "usage.sqlite");
string legacyJson = Path.Combine(directory, "usage.json");
if (File.Exists(sqlitePath))
{
return QuerySqlite(sqlitePath, top, days);
}
if (File.Exists(legacyJson))
{
return QueryLegacyJson(legacyJson, top, days, note: "legacy-json");
}
return JsonSerializer.Serialize(new { error = "No usage data found", sqlite = sqlitePath, legacy = legacyJson });
}
catch (Exception ex)
{
return JsonSerializer.Serialize(new { error = ex.Message });
}
}
private static string QuerySqlite(string dbPath, int top, int days)
{
try
{
int safeDays = Math.Max(1, days);
using SqliteConnection conn = new(new SqliteConnectionStringBuilder { DataSource = dbPath, Mode = SqliteOpenMode.ReadOnly }.ToString());
conn.Open();
using SqliteCommand cmd = conn.CreateCommand();
cmd.CommandText = @"SELECT process_name, SUM(total_seconds) AS total_seconds, MIN(first_seen_utc) AS first_seen_utc, MAX(last_updated_utc) AS last_updated_utc
FROM process_usage
WHERE day_utc >= date('now', @cutoff)
GROUP BY process_name
ORDER BY total_seconds DESC
LIMIT @top;";
cmd.Parameters.AddWithValue("@cutoff", $"-{safeDays} days");
cmd.Parameters.AddWithValue("@top", top);
var list = cmd.ExecuteReader()
.Cast<System.Data.Common.DbDataRecord>()
.Select(r => new AppUsageRecord
{
ProcessName = r.GetString(0),
TotalSeconds = r.GetDouble(1),
FirstSeenUtc = DateTime.Parse(r.GetString(2), null, System.Globalization.DateTimeStyles.RoundtripKind),
LastUpdatedUtc = DateTime.Parse(r.GetString(3), null, System.Globalization.DateTimeStyles.RoundtripKind),
})
.OrderByDescending(r => r.TotalSeconds)
.Select(r => new
{
process = r.ProcessName,
totalSeconds = Math.Round(r.TotalSeconds, 1),
totalHours = Math.Round(r.TotalSeconds / 3600.0, 2),
firstSeenUtc = r.FirstSeenUtc,
lastUpdatedUtc = r.LastUpdatedUtc,
source = "sqlite",
});
return JsonSerializer.Serialize(list);
}
catch (Exception ex)
{
return JsonSerializer.Serialize(new { error = "sqlite query failed", message = ex.Message, path = dbPath });
}
}
private static string QueryLegacyJson(string usageFile, int top, int days, string? note = null)
{
try
{
string json = File.ReadAllText(usageFile);
using JsonDocument doc = JsonDocument.Parse(json);
DateTime cutoff = DateTime.UtcNow.AddDays(-Math.Max(1, days));
var result = doc.RootElement
.EnumerateArray()
.Select(e => new
{
process = e.GetPropertyOrDefault("process", string.Empty),
totalSeconds = e.GetPropertyOrDefault("totalSeconds", 0.0),
lastUpdatedUtc = e.GetPropertyOrDefaultDateTime("lastUpdatedUtc"),
firstSeenUtc = e.GetPropertyOrDefaultDateTime("firstSeenUtc"),
})
.Where(r => r.lastUpdatedUtc >= cutoff)
.OrderByDescending(r => r.totalSeconds)
.Take(top)
.Select(r => new
{
r.process,
totalSeconds = Math.Round(r.totalSeconds, 1),
totalHours = Math.Round(r.totalSeconds / 3600.0, 2),
r.firstSeenUtc,
r.lastUpdatedUtc,
source = note ?? "json",
});
return JsonSerializer.Serialize(result);
}
catch (Exception ex)
{
return JsonSerializer.Serialize(new { error = "legacy json read failed", message = ex.Message, path = usageFile });
}
}
private static string GetPropertyOrDefault(this JsonElement element, string name, string defaultValue)
{
if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out JsonElement value) && value.ValueKind == JsonValueKind.String)
{
return value.GetString() ?? defaultValue;
}
return defaultValue;
}
private static double GetPropertyOrDefault(this JsonElement element, string name, double defaultValue)
{
if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out JsonElement value) && value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out double d))
{
return d;
}
return defaultValue;
}
private static DateTime GetPropertyOrDefaultDateTime(this JsonElement element, string name)
{
if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out JsonElement value))
{
if (value.ValueKind == JsonValueKind.String && value.TryGetDateTime(out DateTime dt))
{
return dt;
}
}
return DateTime.MinValue;
}
}
}

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.ComponentModel;
using ModelContextProtocol.Server;
namespace PowerToys.MCPServer.Tools
{
[McpServerToolType]
public static class EchoTool
{
[McpServerTool]
[Description("Echoes the message back to the client.")]
public static string Echo(string message) => $"Hello {message}";
}
}

View File

@@ -0,0 +1,18 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"PowerToys.MCPServer": "Debug"
}
},
"MCPServer": {
"Port": 8080,
"MaxConcurrentConnections": 100,
"RequestTimeoutSeconds": 30,
"EnableTools": true,
"EnableResources": true,
"Transport": "http"
}
}

View File

@@ -0,0 +1,2 @@
EXPORTS
powertoy_create

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\Common.Cpp.props" />
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{A8B8D654-8F2A-4E6C-9B4F-1234567890AB}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>MCPServerModuleInterface</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<ModuleDefinitionFile>MCPServerModuleInterface.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="targetver.h" />
</ItemGroup>
<ItemGroup>
<None Include="MCPServerModuleInterface.def" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\SettingsAPI\SettingsAPI.vcxproj" />
<ProjectReference Include="..\..\..\..\common\logger\logger.vcxproj" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View File

@@ -0,0 +1,298 @@
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/logger/logger.h>
#include <common/utils/resources.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/utils/winapi_error.h>
#include <shellapi.h>
namespace NonLocalizable
{
const wchar_t ModulePath[] = L"PowerToys.MCPServer.exe";
const wchar_t ModuleKey[] = L"MCPServer";
}
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
class MCPServerModuleInterface : public PowertoyModuleIface
{
public:
virtual PCWSTR get_name() override
{
return app_name.c_str();
}
virtual const wchar_t* get_key() override
{
return app_key.c_str();
}
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
{
return powertoys_gpo::gpo_rule_configured_t::gpo_rule_configured_not_configured;
}
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(L"MCP Server provides Model Context Protocol access to PowerToys functionality for AI assistants and tools");
settings.set_icon_key(L"pt-mcp-server");
// Port configuration
settings.add_int_spinner(
L"port",
L"Server Port",
m_port,
1024,
65535,
1);
// Auto start option
settings.add_bool_toggle(
L"auto_start",
L"Auto Start Server",
m_auto_start);
// Enable tools API
settings.add_bool_toggle(
L"enable_tools",
L"Enable Tools API",
m_enable_tools);
// Enable resources API
settings.add_bool_toggle(
L"enable_resources",
L"Enable Resources API",
m_enable_resources);
// Transport protocol
settings.add_dropdown(
L"transport",
L"Transport Protocol",
m_transport,
std::vector<std::pair<std::wstring, std::wstring>>{
{ L"http", L"HTTP" },
{ L"stdio", L"Standard I/O" },
{ L"tcp", L"TCP Socket" }
});
return settings.serialize_to_buffer(buffer, buffer_size);
}
virtual void set_config(const wchar_t* config) override
{
try
{
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
if (auto port = values.get_int_value(L"port"))
{
m_port = port.value();
}
if (auto auto_start = values.get_bool_value(L"auto_start"))
{
m_auto_start = auto_start.value();
}
if (auto enable_tools = values.get_bool_value(L"enable_tools"))
{
m_enable_tools = enable_tools.value();
}
if (auto enable_resources = values.get_bool_value(L"enable_resources"))
{
m_enable_resources = enable_resources.value();
}
if (auto transport = values.get_string_value(L"transport"))
{
m_transport = transport.value();
}
values.save_to_settings_file();
// If service is running, restart to apply new configuration
if (m_enabled && is_process_running())
{
StopMCPServer();
StartMCPServer();
}
}
catch (std::exception& e)
{
Logger::error("MCPServer configuration parsing failed: {}", std::string{ e.what() });
}
}
virtual void enable() override
{
Logger::info("MCPServer enabling");
m_enabled = true;
if (m_auto_start)
{
StartMCPServer();
}
}
virtual void disable() override
{
Logger::info("MCPServer disabling");
m_enabled = false;
StopMCPServer();
}
virtual bool is_enabled() override
{
return m_enabled;
}
virtual void destroy() override
{
StopMCPServer();
delete this;
}
MCPServerModuleInterface()
{
app_name = L"MCP Server";
app_key = NonLocalizable::ModuleKey;
m_port = 8080;
m_auto_start = true;
m_enable_tools = true;
m_enable_resources = true;
m_transport = L"http";
init_settings();
}
private:
void StartMCPServer()
{
if (m_hProcess && WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT)
{
return; // Already running
}
std::wstring executable_args = L"--port=" + std::to_wstring(m_port);
if (!m_enable_tools)
{
executable_args += L" --disable-tools";
}
if (!m_enable_resources)
{
executable_args += L" --disable-resources";
}
if (!m_transport.empty())
{
executable_args += L" --transport=" + m_transport;
}
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
sei.lpFile = NonLocalizable::ModulePath;
sei.nShow = SW_HIDE;
sei.lpParameters = executable_args.data();
if (ShellExecuteExW(&sei))
{
m_hProcess = sei.hProcess;
Logger::info("MCPServer started successfully on port {} with transport {}", m_port, std::string(m_transport.begin(), m_transport.end()));
}
else
{
Logger::error("Failed to start MCPServer");
auto message = get_last_error_message(GetLastError());
if (message.has_value())
{
Logger::error(message.value());
}
}
}
void StopMCPServer()
{
if (m_hProcess)
{
TerminateProcess(m_hProcess, 0);
CloseHandle(m_hProcess);
m_hProcess = nullptr;
Logger::info("MCPServer stopped");
}
}
bool is_process_running()
{
return m_hProcess && WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
}
void init_settings()
{
try
{
PowerToysSettings::PowerToyValues settings =
PowerToysSettings::PowerToyValues::load_from_settings_file(get_key());
if (auto port = settings.get_int_value(L"port"))
{
m_port = port.value();
}
if (auto auto_start = settings.get_bool_value(L"auto_start"))
{
m_auto_start = auto_start.value();
}
if (auto enable_tools = settings.get_bool_value(L"enable_tools"))
{
m_enable_tools = enable_tools.value();
}
if (auto enable_resources = settings.get_bool_value(L"enable_resources"))
{
m_enable_resources = enable_resources.value();
}
if (auto transport = settings.get_string_value(L"transport"))
{
m_transport = transport.value();
}
}
catch (std::exception&)
{
Logger::warn(L"MCPServer settings file not found, using defaults");
}
}
std::wstring app_name;
std::wstring app_key;
bool m_enabled = false;
HANDLE m_hProcess = nullptr;
int m_port = 8080;
bool m_auto_start = true;
bool m_enable_tools = true;
bool m_enable_resources = true;
std::wstring m_transport = L"http";
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new MCPServerModuleInterface();
}

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,14 @@
#pragma once
#include "targetver.h"
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <unknwn.h>
#include <restrictederrorinfo.h>
#include <hstring.h>
#include <string>
#include <vector>

View File

@@ -0,0 +1,8 @@
#pragma once
// Including SDKDDKVer.h defines the highest available Windows platform.
// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.
#include <SDKDDKVer.h>

View File

@@ -1,12 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250513003\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250513003\build\native\Microsoft.WindowsAppSDK.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
@@ -147,13 +141,7 @@
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250513003\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250513003\build\native\Microsoft.WindowsAppSDK.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
@@ -165,19 +153,7 @@
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250513003\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250513003\build\native\Microsoft.WindowsAppSDK.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250513003\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250513003\build\native\Microsoft.WindowsAppSDK.targets'))" />
</Target>
</Project>

View File

@@ -4,14 +4,5 @@
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.26100.4188" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK" version="1.8.250907003" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Base" version="1.8.250831001" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Foundation" version="1.8.250906002" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.WinUI" version="1.8.250906003" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Runtime" version="1.8.250907003" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.DWrite" version="1.8.25090401" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.InteractiveExperiences" version="1.8.250906004" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.Widgets" version="1.8.250904007" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK.AI" version="1.8.37" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools.MSIX" version="1.7.20250829.1" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK" version="1.7.250513003" targetFramework="native" />
</packages>

View File

@@ -121,8 +121,8 @@ FONT 8, "MS Shell Dlg", 0, 0, 0x0
BEGIN
DEFPUSHBUTTON "OK",IDOK,166,306,50,14
PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14
LTEXT "ZoomIt v9.01",IDC_VERSION,42,7,73,10
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,166,8
LTEXT "ZoomIt v9.0",IDC_VERSION,42,7,73,10
LTEXT "Copyright <EFBFBD> 2006-2024 Mark Russinovich",IDC_COPYRIGHT,42,17,166,8
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
"SysLink",WS_TABSTOP,42,26,150,9
ICON "APPICON",IDC_STATIC,12,9,20,20

View File

@@ -1,102 +1,103 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<UseWindowsForms>False</UseWindowsForms>
<!--Per documentation: https://learn.microsoft.com/dotnet/core/compatibility/windows-forms/5.0/automatically-infer-winexe-output-type#outputtype-set-to-winexe-for-wpf-and-winforms-apps -->
<DisableWinExeOutputInference>true</DisableWinExeOutputInference>
<AssemblyName>PowerToys.Awake</AssemblyName>
<ApplicationIcon>Assets\Awake\Awake.ico</ApplicationIcon>
<PackageProjectUrl>https://awake.den.dev</PackageProjectUrl>
<RepositoryUrl>https://github.com/microsoft/powertoys</RepositoryUrl>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
</PropertyGroup>
<PropertyGroup>
<OutputType>WinExe</OutputType>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<UseWindowsForms>False</UseWindowsForms>
<!--Per documentation: https://learn.microsoft.com/dotnet/core/compatibility/windows-forms/5.0/automatically-infer-winexe-output-type#outputtype-set-to-winexe-for-wpf-and-winforms-apps -->
<DisableWinExeOutputInference>true</DisableWinExeOutputInference>
<AssemblyName>PowerToys.Awake</AssemblyName>
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
<PropertyGroup>
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ApplicationIcon>Assets\Awake\Awake.ico</ApplicationIcon>
<PackageProjectUrl>https://awake.den.dev</PackageProjectUrl>
<RepositoryUrl>https://github.com/microsoft/powertoys</RepositoryUrl>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<None Remove="Assets\Awake\Awake.ico" />
<None Remove="Assets\Awake\disabled.ico" />
<None Remove="Assets\Awake\expirable.ico" />
<None Remove="Assets\Awake\indefinite.ico" />
<None Remove="Assets\Awake\normal.ico" />
<None Remove="Assets\Awake\scheduled.ico" />
<None Remove="Assets\Awake\timed.ico" />
</ItemGroup>
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
<PropertyGroup>
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="System.Runtime.Caching" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\Awake\Awake.ico" />
<None Remove="Assets\Awake\disabled.ico" />
<None Remove="Assets\Awake\expirable.ico" />
<None Remove="Assets\Awake\indefinite.ico" />
<None Remove="Assets\Awake\normal.ico" />
<None Remove="Assets\Awake\scheduled.ico" />
<None Remove="Assets\Awake\timed.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="System.Runtime.Caching" />
<PackageReference Include="Microsoft.Data.Sqlite" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Program.cs">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Compile>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Update="Program.cs">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Compile>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Content Include="Assets\Awake\Awake.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\disabled.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\expirable.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\indefinite.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\normal.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\scheduled.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\timed.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="Assets\Awake\Awake.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\disabled.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\expirable.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\indefinite.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\normal.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\scheduled.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\timed.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.CodeAnalysis.NetAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
<ItemGroup>
<PackageReference Update="Microsoft.CodeAnalysis.NetAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,33 @@
// 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.Runtime.InteropServices;
using ManagedCommon;
namespace Awake
{
/// <summary>
/// Automation object exposed via the Running Object Table. Intentionally minimal; methods may expand in future.
/// </summary>
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[Guid("4F1C3769-8D28-4A2D-8A6A-AB2F4C0F5F11")]
public sealed class AwakeAutomation : IAwakeAutomation
{
public string Ping() => "pong";
public void SetIndefinite() => Logger.LogInfo("Automation: SetIndefinite");
public void SetTimed(int seconds) => Logger.LogInfo($"Automation: SetTimed {seconds}s");
public void SetExpirable(int minutes) => Logger.LogInfo($"Automation: SetExpirable {minutes}m");
public void SetPassive() => Logger.LogInfo("Automation: SetPassive");
public void Cancel() => Logger.LogInfo("Automation: Cancel");
public string GetStatusJson() => "{\"ok\":true}";
}
}

View File

@@ -0,0 +1,402 @@
// 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.Globalization;
using System.Net;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
namespace Awake.Core
{
/// <summary>
/// HTTP request handler for Awake module functionality.
/// </summary>
internal sealed class AwakeHttpHandler : IHttpRequestHandler
{
public string ModuleName => "awake";
public string[] GetAvailableEndpoints()
{
return new[]
{
"GET /awake/status - Get current Awake status",
"GET /awake/config - Get current Awake configuration",
"POST /awake/indefinite - Set indefinite keep-awake (body: {\"keepDisplayOn\": true, \"processId\": 0})",
"POST /awake/timed - Set timed keep-awake (body: {\"seconds\": 3600, \"keepDisplayOn\": true})",
"POST /awake/expirable - Set expirable keep-awake (body: {\"expireAt\": \"2024-12-31T23:59:59Z\", \"keepDisplayOn\": true})",
"POST /awake/activity - Set activity-based keep-awake (body: {\"cpuThresholdPercent\": 50, \"memThresholdPercent\": 50, \"netThresholdKBps\": 10, \"sampleIntervalSeconds\": 30, \"inactivityTimeoutSeconds\": 300, \"keepDisplayOn\": true})",
"POST /awake/passive - Set passive mode (no keep-awake)",
"POST /awake/display/toggle - Toggle display setting",
"GET /awake/settings - Get current PowerToys settings",
};
}
public async Task HandleRequestAsync(HttpListenerContext context, string path)
{
var request = context.Request;
var response = context.Response;
var method = request.HttpMethod.ToUpperInvariant();
var pathLower = path.ToLowerInvariant();
try
{
switch ((method, pathLower))
{
case ("GET", "status"):
await HandleGetStatusAsync(response);
break;
case ("GET", "config"):
await HandleGetConfigAsync(response);
break;
case ("POST", "indefinite"):
await HandleSetIndefiniteAwakeAsync(request, response);
break;
case ("POST", "timed"):
await HandleSetTimedAwakeAsync(request, response);
break;
case ("POST", "expirable"):
await HandleSetExpirableAwakeAsync(request, response);
break;
case ("POST", "activity"):
await HandleSetActivityBasedAwakeAsync(request, response);
break;
case ("POST", "passive"):
await HandleSetPassiveAwakeAsync(response);
break;
case ("POST", "display/toggle"):
await HandleToggleDisplayAsync(response);
break;
case ("GET", "settings"):
await HandleGetSettingsAsync(response);
break;
default:
await HandleNotFoundAsync(response, path);
break;
}
}
catch (Exception ex)
{
Logger.LogError($"Error handling Awake request {method} /{path}: {ex.Message}");
await HandleErrorAsync(response, 500, $"Internal server error: {ex.Message}");
}
}
private async Task HandleGetStatusAsync(HttpListenerResponse response)
{
var status = new
{
Module = "Awake",
Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(),
Status = "Running",
UsingPowerToysConfig = Manager.IsUsingPowerToysConfig,
Timestamp = DateTimeOffset.Now,
};
await WriteJsonResponseAsync(response, status);
}
private async Task HandleGetConfigAsync(HttpListenerResponse response)
{
var config = Manager.GetCurrentConfig();
await WriteJsonResponseAsync(response, config);
}
private async Task HandleSetIndefiniteAwakeAsync(HttpListenerRequest request, HttpListenerResponse response)
{
var requestData = await ReadJsonRequestAsync<IndefiniteAwakeRequest>(request);
Manager.SetIndefiniteKeepAwake(
requestData?.KeepDisplayOn ?? true,
requestData?.ProcessId ?? 0,
"HttpServer");
var result = new
{
Success = true,
Mode = "Indefinite",
KeepDisplayOn = requestData?.KeepDisplayOn ?? true,
ProcessId = requestData?.ProcessId ?? 0,
};
await WriteJsonResponseAsync(response, result);
}
private async Task HandleSetTimedAwakeAsync(HttpListenerRequest request, HttpListenerResponse response)
{
var requestData = await ReadJsonRequestAsync<TimedAwakeRequest>(request);
if (requestData?.Seconds == null || requestData.Seconds <= 0)
{
await HandleErrorAsync(response, 400, "Invalid or missing 'seconds' parameter. Must be a positive integer.");
return;
}
Manager.SetTimedKeepAwake(
requestData.Seconds,
requestData.KeepDisplayOn ?? true,
"HttpServer");
var result = new
{
Success = true,
Mode = "Timed",
Seconds = requestData.Seconds,
KeepDisplayOn = requestData.KeepDisplayOn ?? true,
};
await WriteJsonResponseAsync(response, result);
}
private async Task HandleSetExpirableAwakeAsync(HttpListenerRequest request, HttpListenerResponse response)
{
var requestData = await ReadJsonRequestAsync<ExpirableAwakeRequest>(request);
if (requestData?.ExpireAt == null)
{
await HandleErrorAsync(response, 400, "Missing 'expireAt' parameter. Expected ISO 8601 format (e.g., '2024-12-31T23:59:59Z').");
return;
}
if (requestData.ExpireAt <= DateTimeOffset.Now)
{
await HandleErrorAsync(response, 400, "Expiration time must be in the future.");
return;
}
Manager.SetExpirableKeepAwake(
requestData.ExpireAt,
requestData.KeepDisplayOn ?? true,
"HttpServer");
var result = new
{
Success = true,
Mode = "Expirable",
ExpireAt = requestData.ExpireAt,
KeepDisplayOn = requestData.KeepDisplayOn ?? true,
};
await WriteJsonResponseAsync(response, result);
}
private async Task HandleSetActivityBasedAwakeAsync(HttpListenerRequest request, HttpListenerResponse response)
{
var requestData = await ReadJsonRequestAsync<ActivityBasedAwakeRequest>(request);
if (requestData == null)
{
await HandleErrorAsync(response, 400, "Invalid or missing request body. Expected JSON with cpuThresholdPercent, memThresholdPercent, netThresholdKBps, sampleIntervalSeconds, inactivityTimeoutSeconds, and optional keepDisplayOn.");
return;
}
// Validate required parameters
if (requestData.CpuThresholdPercent > 100)
{
await HandleErrorAsync(response, 400, "Invalid cpuThresholdPercent. Must be between 0 and 100.");
return;
}
if (requestData.MemThresholdPercent > 100)
{
await HandleErrorAsync(response, 400, "Invalid memThresholdPercent. Must be between 0 and 100.");
return;
}
if (requestData.SampleIntervalSeconds == 0)
{
await HandleErrorAsync(response, 400, "Invalid sampleIntervalSeconds. Must be greater than 0.");
return;
}
if (requestData.InactivityTimeoutSeconds == 0)
{
await HandleErrorAsync(response, 400, "Invalid inactivityTimeoutSeconds. Must be greater than 0.");
return;
}
Manager.SetActivityBasedKeepAwake(
requestData.CpuThresholdPercent,
requestData.MemThresholdPercent,
requestData.NetThresholdKBps,
requestData.SampleIntervalSeconds,
requestData.InactivityTimeoutSeconds,
requestData.KeepDisplayOn ?? true,
"HttpServer");
var result = new
{
Success = true,
Mode = "Activity-based",
CpuThresholdPercent = requestData.CpuThresholdPercent,
MemThresholdPercent = requestData.MemThresholdPercent,
NetThresholdKBps = requestData.NetThresholdKBps,
SampleIntervalSeconds = requestData.SampleIntervalSeconds,
InactivityTimeoutSeconds = requestData.InactivityTimeoutSeconds,
KeepDisplayOn = requestData.KeepDisplayOn ?? true,
};
await WriteJsonResponseAsync(response, result);
}
private async Task HandleSetPassiveAwakeAsync(HttpListenerResponse response)
{
Manager.SetPassiveKeepAwake(true, "HttpServer");
var result = new { Success = true, Mode = "Passive" };
await WriteJsonResponseAsync(response, result);
}
private async Task HandleToggleDisplayAsync(HttpListenerResponse response)
{
Manager.SetDisplay("HttpServer");
var result = new { Success = true, Action = "Display setting toggled" };
await WriteJsonResponseAsync(response, result);
}
private async Task HandleGetSettingsAsync(HttpListenerResponse response)
{
try
{
if (Manager.ModuleSettings != null)
{
var settings = Manager.ModuleSettings.GetSettings<AwakeSettings>(Constants.AppName);
await WriteJsonResponseAsync(response, settings);
}
else
{
await HandleErrorAsync(response, 500, "Settings module not available");
}
}
catch (Exception ex)
{
await HandleErrorAsync(response, 500, $"Error retrieving settings: {ex.Message}");
}
}
private async Task HandleNotFoundAsync(HttpListenerResponse response, string path)
{
var errorResponse = new
{
Error = $"Awake endpoint '/{path}' not found",
AvailableEndpoints = GetAvailableEndpoints(),
Module = ModuleName,
Timestamp = DateTimeOffset.Now,
};
response.StatusCode = 404;
await WriteJsonResponseAsync(response, errorResponse);
}
private async Task HandleErrorAsync(HttpListenerResponse response, int statusCode, string message)
{
response.StatusCode = statusCode;
var errorResponse = new
{
Error = message,
StatusCode = statusCode,
Module = ModuleName,
Timestamp = DateTimeOffset.Now,
};
await WriteJsonResponseAsync(response, errorResponse);
}
private async Task WriteJsonResponseAsync(HttpListenerResponse response, object data)
{
// Use HttpServer utility method via static access or DI - for now, replicate the functionality
response.ContentType = "application/json";
response.ContentEncoding = System.Text.Encoding.UTF8;
#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances
var json = System.Text.Json.JsonSerializer.Serialize(
data, new System.Text.Json.JsonSerializerOptions
{
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
WriteIndented = true,
});
#pragma warning restore CA1869 // Cache and reuse 'JsonSerializerOptions' instances
var buffer = System.Text.Encoding.UTF8.GetBytes(json);
response.ContentLength64 = buffer.Length;
await response.OutputStream.WriteAsync(buffer);
response.Close();
}
private async Task<T?> ReadJsonRequestAsync<T>(HttpListenerRequest request)
where T : class
{
if (!request.HasEntityBody)
{
return null;
}
using var reader = new System.IO.StreamReader(request.InputStream, System.Text.Encoding.UTF8);
var body = await reader.ReadToEndAsync();
if (string.IsNullOrWhiteSpace(body))
{
return null;
}
try
{
#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances
return System.Text.Json.JsonSerializer.Deserialize<T>(body, new System.Text.Json.JsonSerializerOptions
{
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
});
#pragma warning restore CA1869 // Cache and reuse 'JsonSerializerOptions' instances
}
catch (System.Text.Json.JsonException ex)
{
Logger.LogError($"Error deserializing request body: {ex.Message}");
return null;
}
}
// Request DTOs
private sealed class IndefiniteAwakeRequest
{
public bool? KeepDisplayOn { get; set; }
public int? ProcessId { get; set; }
}
private sealed class TimedAwakeRequest
{
public uint Seconds { get; set; }
public bool? KeepDisplayOn { get; set; }
}
private sealed class ExpirableAwakeRequest
{
public DateTimeOffset ExpireAt { get; set; }
public bool? KeepDisplayOn { get; set; }
}
private sealed class ActivityBasedAwakeRequest
{
public uint CpuThresholdPercent { get; set; }
public uint MemThresholdPercent { get; set; }
public uint NetThresholdKBps { get; set; }
public uint SampleIntervalSeconds { get; set; }
public uint InactivityTimeoutSeconds { get; set; }
public bool? KeepDisplayOn { get; set; }
}
}
}

View File

@@ -9,6 +9,7 @@ using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -17,6 +18,9 @@ using System.Text.Json;
using System.Threading;
using Awake.Core.Models;
using Awake.Core.Native;
// New usage tracking namespace
using Awake.Core.Usage;
using Awake.Properties;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
@@ -56,9 +60,26 @@ namespace Awake.Core
private static readonly BlockingCollection<ExecutionState> _stateQueue;
private static CancellationTokenSource _tokenSource;
// Foreground usage tracker instance (lifecycle managed by Program)
internal static ForegroundUsageTracker? UsageTracker { get; set; }
private static PowerSchemeManager powerSchemeManager;
// Power scheme auto-switch (activity mode)
private static string? _originalPowerSchemeGuid;
private static bool _powerSchemeSwitched;
// Process monitoring fields
private static List<string> _processMonitoringList = [];
private static bool _processMonitoringActive;
private static IDisposable? _processMonitoringSubscription;
private static uint _processCheckInterval;
private static bool _processKeepDisplay;
static Manager()
{
_tokenSource = new CancellationTokenSource();
powerSchemeManager = new PowerSchemeManager();
_stateQueue = [];
ModuleSettings = new SettingsUtils();
}
@@ -123,6 +144,19 @@ namespace Awake.Core
: ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_CONTINUOUS;
}
// Activity mode state
private static bool _activityActive;
private static DateTimeOffset _activityLastHigh;
private static uint _activityCpu;
private static uint _activityMem;
private static uint _activityNetKBps;
private static uint _activitySample;
private static uint _activityTimeout;
private static bool _activityKeepDisplay;
private static PerformanceCounter? _cpuCounter;
private static PerformanceCounter? _memCounter;
private static List<PerformanceCounter>? _netCounters;
internal static void CancelExistingThread()
{
Logger.LogInfo("Ensuring the thread is properly cleaned up...");
@@ -130,6 +164,10 @@ namespace Awake.Core
// Reset the thread state and handle cancellation.
_stateQueue.Add(ExecutionState.ES_CONTINUOUS);
// Clean up process monitoring subscription if active
_processMonitoringSubscription?.Dispose();
_processMonitoringSubscription = null;
if (_tokenSource != null)
{
_tokenSource.Cancel();
@@ -174,6 +212,15 @@ namespace Awake.Core
iconText = $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_TIMED}][{ScreenStateString}]";
icon = TrayHelper.TimedIcon;
break;
case AwakeMode.ACTIVITY:
iconText = $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_ACTIVITY}][{ScreenStateString}]";
icon = TrayHelper.IndefiniteIcon; // Placeholder icon
break;
case AwakeMode.PROCESS:
string processesText = _processMonitoringList.Count > 0 ? string.Join(", ", _processMonitoringList) : "None";
iconText = $"{Constants.FullAppName} [Process Monitor][{ScreenStateString}][{processesText}]";
icon = TrayHelper.IndefiniteIcon; // Use same icon as indefinite for now
break;
}
TrayHelper.SetShellIcon(
@@ -183,6 +230,94 @@ namespace Awake.Core
forceAdd ? TrayIconAction.Add : TrayIconAction.Update);
}
private static void CaptureOriginalPowerScheme()
{
try
{
powerSchemeManager.RefreshSchemes();
_originalPowerSchemeGuid = powerSchemeManager
.GetAllSchemes()
.FirstOrDefault(s => s.IsActive)?.PSGuid;
Logger.LogInfo($"Captured original power scheme: {_originalPowerSchemeGuid ?? "UNKNOWN"}");
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to capture original power scheme: {ex.Message}");
}
}
private static void SwitchToHighestPerformanceIfNeeded()
{
if (_powerSchemeSwitched)
{
return;
}
try
{
powerSchemeManager.RefreshSchemes();
var highest = powerSchemeManager.GetHighestPerformanceScheme();
if (highest == null)
{
Logger.LogInfo("No power schemes found when attempting high performance switch.");
return;
}
if (highest.IsActive)
{
Logger.LogInfo("Already on highest performance scheme no switch needed.");
return;
}
if (_originalPowerSchemeGuid == null)
{
CaptureOriginalPowerScheme();
}
if (powerSchemeManager.SwitchScheme(highest.PSGuid))
{
_powerSchemeSwitched = true;
Logger.LogInfo($"Switched to highest performance scheme: {highest.Name} ({highest.PSGuid})");
}
else
{
Logger.LogWarning($"Failed to switch to highest performance scheme: {highest.Name} ({highest.PSGuid})");
}
}
catch (Exception ex)
{
Logger.LogWarning($"Exception while attempting to switch power scheme: {ex.Message}");
}
}
private static void RestoreOriginalPowerSchemeIfNeeded()
{
if (!_powerSchemeSwitched || string.IsNullOrWhiteSpace(_originalPowerSchemeGuid))
{
return;
}
try
{
if (powerSchemeManager.SwitchScheme(_originalPowerSchemeGuid))
{
Logger.LogInfo($"Restored original power scheme: {_originalPowerSchemeGuid}");
}
else
{
Logger.LogWarning($"Failed to restore original power scheme: {_originalPowerSchemeGuid}");
}
}
catch (Exception ex)
{
Logger.LogWarning($"Exception restoring original power scheme: {ex.Message}");
}
finally
{
_powerSchemeSwitched = false;
}
}
internal static void SetIndefiniteKeepAwake(bool keepDisplayOn = false, int processId = 0, [CallerMemberName] string callerName = "")
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AwakeIndefinitelyKeepAwakeEvent());
@@ -400,18 +535,24 @@ namespace Awake.Core
internal static void CompleteExit(int exitCode)
{
SetPassiveKeepAwake(updateSettings: false);
RestoreOriginalPowerSchemeIfNeeded();
if (TrayHelper.WindowHandle != IntPtr.Zero)
{
// Delete the icon.
TrayHelper.SetShellIcon(TrayHelper.WindowHandle, string.Empty, null, TrayIconAction.Delete);
// Close the message window that we used for the tray.
Bridge.SendMessage(TrayHelper.WindowHandle, Native.Constants.WM_CLOSE, 0, 0);
Bridge.DestroyWindow(TrayHelper.WindowHandle);
}
// Dispose usage tracker (flushes data)
try
{
UsageTracker?.Dispose();
}
catch (Exception ex)
{
Logger.LogWarning($"Failed disposing UsageTracker: {ex.Message}");
}
Bridge.PostQuitMessage(exitCode);
Environment.Exit(exitCode);
}
@@ -500,6 +641,275 @@ namespace Awake.Core
SetModeShellIcon();
}
internal static void SetActivityBasedKeepAwake(
uint cpuThresholdPercent,
uint memThresholdPercent,
uint netThresholdKBps,
uint sampleIntervalSeconds,
uint inactivityTimeoutSeconds,
bool keepDisplayOn,
[CallerMemberName] string callerName = "")
{
Logger.LogInfo($"Activity-based keep-awake invoked by {callerName}. CPU>={cpuThresholdPercent}%, MEM>={memThresholdPercent}%, NET>={netThresholdKBps}KB/s sample={sampleIntervalSeconds}s timeout={inactivityTimeoutSeconds}s display={keepDisplayOn}.");
CancelExistingThread();
if (IsUsingPowerToysConfig)
{
try
{
AwakeSettings currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
bool settingsChanged =
currentSettings.Properties.Mode != AwakeMode.ACTIVITY ||
currentSettings.Properties.ActivityCpuThresholdPercent != cpuThresholdPercent ||
currentSettings.Properties.ActivityMemoryThresholdPercent != memThresholdPercent ||
currentSettings.Properties.ActivityNetworkThresholdKBps != netThresholdKBps ||
currentSettings.Properties.ActivitySampleIntervalSeconds != sampleIntervalSeconds ||
currentSettings.Properties.ActivityInactivityTimeoutSeconds != inactivityTimeoutSeconds ||
currentSettings.Properties.KeepDisplayOn != keepDisplayOn;
if (settingsChanged)
{
currentSettings.Properties.Mode = AwakeMode.ACTIVITY;
currentSettings.Properties.ActivityCpuThresholdPercent = cpuThresholdPercent;
currentSettings.Properties.ActivityMemoryThresholdPercent = memThresholdPercent;
currentSettings.Properties.ActivityNetworkThresholdKBps = netThresholdKBps;
currentSettings.Properties.ActivitySampleIntervalSeconds = sampleIntervalSeconds;
currentSettings.Properties.ActivityInactivityTimeoutSeconds = inactivityTimeoutSeconds;
currentSettings.Properties.KeepDisplayOn = keepDisplayOn;
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), Constants.AppName);
return; // Settings will be processed triggering this again
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to persist activity mode settings: {ex.Message}");
}
}
// Initialize performance counters
try
{
_cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
_memCounter = new PerformanceCounter("Memory", "% Committed Bytes In Use");
_netCounters = PerformanceCounterCategory.GetCategories()
.FirstOrDefault(c => c.CategoryName == "Network Interface")?
.GetInstanceNames()
.Select(n => new PerformanceCounter("Network Interface", "Bytes Total/sec", n))
.ToList() ?? new List<PerformanceCounter>();
_cpuCounter.NextValue(); // Prime CPU counter
}
catch (Exception ex)
{
Logger.LogError($"Failed to initialize performance counters for activity mode: {ex.Message}");
return;
}
_activityCpu = cpuThresholdPercent;
_activityMem = memThresholdPercent;
_activityNetKBps = netThresholdKBps;
_activitySample = Math.Max(1, sampleIntervalSeconds);
_activityTimeout = Math.Max(5, inactivityTimeoutSeconds);
_activityKeepDisplay = keepDisplayOn;
_activityLastHigh = DateTimeOffset.Now;
_activityActive = true;
CurrentOperatingMode = AwakeMode.ACTIVITY;
IsDisplayOn = keepDisplayOn;
SetModeShellIcon();
// Capture original scheme before any switch
CaptureOriginalPowerScheme();
TimeSpan sampleInterval = TimeSpan.FromSeconds(_activitySample);
Observable.Interval(sampleInterval).Subscribe(
_ =>
{
if (!_activityActive)
{
return;
}
float cpu = 0;
float mem = 0;
double netKBps = 0;
try
{
cpu = _cpuCounter?.NextValue() ?? 0;
mem = _memCounter?.NextValue() ?? 0;
if (_netCounters != null && _netCounters.Count > 0)
{
netKBps = _netCounters.Sum(c => c.NextValue()) / 1024.0;
}
}
catch (Exception ex)
{
Logger.LogError($"Performance counter read failure: {ex.Message}");
}
bool above =
(_activityCpu == 0 || cpu >= _activityCpu) ||
(_activityMem == 0 || mem >= _activityMem) ||
(_activityNetKBps == 0 || netKBps >= _activityNetKBps);
if (above)
{
_activityLastHigh = DateTimeOffset.Now;
_stateQueue.Add(ComputeAwakeState(_activityKeepDisplay));
SwitchToHighestPerformanceIfNeeded();
}
TrayHelper.SetShellIcon(
TrayHelper.WindowHandle,
$"{Constants.FullAppName} [Activity][{ScreenStateString}][CPU {cpu:0.#}% | MEM {mem:0.#}% | NET {netKBps:0.#}KB/s]",
TrayHelper.IndefiniteIcon,
TrayIconAction.Update);
if ((DateTimeOffset.Now - _activityLastHigh).TotalSeconds >= _activityTimeout)
{
Logger.LogInfo("Activity thresholds not met within timeout window. Ending activity mode.");
_activityActive = false;
CancelExistingThread();
RestoreOriginalPowerSchemeIfNeeded();
if (IsUsingPowerToysConfig)
{
SetPassiveKeepAwake();
}
else
{
CompleteExit(Environment.ExitCode);
}
}
},
ex =>
{
Logger.LogError($"Activity mode observable failure: {ex.Message}");
},
_tokenSource.Token);
}
internal static void SetProcessBasedKeepAwake(
List<string> processNames,
uint checkIntervalSeconds,
bool keepDisplayOn,
[CallerMemberName] string callerName = "")
{
Logger.LogInfo($"Process-based keep-awake invoked by {callerName}. Processes: [{string.Join(", ", processNames)}], CheckInterval: {checkIntervalSeconds}s, Display: {keepDisplayOn}.");
CancelExistingThread();
if (IsUsingPowerToysConfig)
{
try
{
AwakeSettings currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
bool settingsChanged =
currentSettings.Properties.Mode != AwakeMode.PROCESS ||
!currentSettings.Properties.ProcessMonitoringList.SequenceEqual(processNames) ||
currentSettings.Properties.ProcessCheckIntervalSeconds != checkIntervalSeconds ||
currentSettings.Properties.KeepDisplayOn != keepDisplayOn;
if (settingsChanged)
{
currentSettings.Properties.Mode = AwakeMode.PROCESS;
currentSettings.Properties.ProcessMonitoringList = new List<string>(processNames);
currentSettings.Properties.ProcessCheckIntervalSeconds = checkIntervalSeconds;
currentSettings.Properties.KeepDisplayOn = keepDisplayOn;
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), Constants.AppName);
return; // Settings will be processed triggering this again
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to persist process monitoring settings: {ex.Message}");
}
}
_processMonitoringList = [.. processNames];
_processCheckInterval = Math.Max(1, checkIntervalSeconds);
_processKeepDisplay = keepDisplayOn;
_processMonitoringActive = true;
CurrentOperatingMode = AwakeMode.PROCESS;
IsDisplayOn = keepDisplayOn;
SetModeShellIcon();
TimeSpan checkInterval = TimeSpan.FromSeconds(_processCheckInterval);
Observable.Interval(checkInterval).Subscribe(
_ =>
{
if (!_processMonitoringActive)
{
return;
}
bool anyTargetProcessRunning = false;
List<string> runningProcesses = new();
try
{
foreach (string processName in _processMonitoringList)
{
if (string.IsNullOrWhiteSpace(processName))
{
continue;
}
// Remove .exe extension if present for process name comparison
string processNameWithoutExt = processName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)
? processName.Substring(0, processName.Length - 4)
: processName;
Process[] processes = Process.GetProcessesByName(processNameWithoutExt);
if (processes.Length > 0)
{
anyTargetProcessRunning = true;
runningProcesses.Add(processNameWithoutExt);
}
}
}
catch (Exception ex)
{
Logger.LogError($"Process monitoring check failure: {ex.Message}");
}
if (anyTargetProcessRunning)
{
_stateQueue.Add(ComputeAwakeState(_processKeepDisplay));
TrayHelper.SetShellIcon(
TrayHelper.WindowHandle,
$"{Constants.FullAppName} [Process Monitor][{ScreenStateString}][Running: {string.Join(", ", runningProcesses)}]",
TrayHelper.IndefiniteIcon,
TrayIconAction.Update);
}
else
{
Logger.LogInfo("No target processes running. Ending process monitoring mode.");
_processMonitoringActive = false;
CancelExistingThread();
if (IsUsingPowerToysConfig)
{
SetPassiveKeepAwake();
}
else
{
CompleteExit(Environment.ExitCode);
}
}
},
ex =>
{
Logger.LogError($"Process monitoring observable failure: {ex.Message}");
},
_tokenSource.Token);
}
/// <summary>
/// Sets the display settings.
/// </summary>
@@ -533,6 +943,100 @@ namespace Awake.Core
}
}
/// <summary>
/// Gets the current Awake configuration status.
/// </summary>
/// <returns>Current configuration status information.</returns>
public static object GetCurrentConfig()
{
try
{
AwakeSettings currentSettings = ModuleSettings?.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
var baseConfig = new
{
mode = CurrentOperatingMode.ToString(),
keepDisplayOn = IsDisplayOn,
processId = ProcessId,
isUsingPowerToysConfig = IsUsingPowerToysConfig,
};
// Return different parameters based on the current mode
return CurrentOperatingMode switch
{
AwakeMode.PASSIVE => new
{
baseConfig.mode,
baseConfig.processId,
baseConfig.isUsingPowerToysConfig,
},
AwakeMode.INDEFINITE => new
{
baseConfig.mode,
baseConfig.keepDisplayOn,
baseConfig.processId,
baseConfig.isUsingPowerToysConfig,
},
AwakeMode.TIMED => new
{
baseConfig.mode,
baseConfig.keepDisplayOn,
timeRemaining = TimeRemaining,
intervalHours = currentSettings.Properties.IntervalHours,
intervalMinutes = currentSettings.Properties.IntervalMinutes,
baseConfig.processId,
baseConfig.isUsingPowerToysConfig,
},
AwakeMode.EXPIRABLE => new
{
baseConfig.mode,
baseConfig.keepDisplayOn,
expireAt = ExpireAt == DateTimeOffset.MinValue ? (DateTimeOffset?)null : ExpireAt,
expirationDateTime = currentSettings.Properties.ExpirationDateTime,
baseConfig.processId,
baseConfig.isUsingPowerToysConfig,
},
AwakeMode.ACTIVITY => new
{
baseConfig.mode,
baseConfig.keepDisplayOn,
cpuThresholdPercent = currentSettings.Properties.ActivityCpuThresholdPercent,
memoryThresholdPercent = currentSettings.Properties.ActivityMemoryThresholdPercent,
networkThresholdKBps = currentSettings.Properties.ActivityNetworkThresholdKBps,
sampleIntervalSeconds = currentSettings.Properties.ActivitySampleIntervalSeconds,
inactivityTimeoutSeconds = currentSettings.Properties.ActivityInactivityTimeoutSeconds,
activityActive = _activityActive,
lastHighActivity = _activityLastHigh,
baseConfig.processId,
baseConfig.isUsingPowerToysConfig,
},
AwakeMode.PROCESS => new
{
baseConfig.mode,
baseConfig.keepDisplayOn,
processMonitoringList = currentSettings.Properties.ProcessMonitoringList,
processCheckIntervalSeconds = currentSettings.Properties.ProcessCheckIntervalSeconds,
processMonitoringActive = _processMonitoringActive,
baseConfig.processId,
baseConfig.isUsingPowerToysConfig,
},
_ => new
{
baseConfig.mode,
baseConfig.keepDisplayOn,
baseConfig.processId,
baseConfig.isUsingPowerToysConfig,
error = "Unknown mode",
},
};
}
catch (Exception ex)
{
Logger.LogError($"Failed to get current config: {ex.Message}");
return new { error = "Failed to get current configuration", message = ex.Message };
}
}
public static Process? GetParentProcess()
{
return GetParentProcess(Process.GetCurrentProcess().Handle);

View File

@@ -10,6 +10,7 @@ namespace Awake.Core.Models
TC_MODE_PASSIVE = Native.Constants.WM_USER + 0x3,
TC_MODE_INDEFINITE = Native.Constants.WM_USER + 0x4,
TC_MODE_EXPIRABLE = Native.Constants.WM_USER + 0x5,
TC_MODE_ACTIVITY = Native.Constants.WM_USER + 0x6,
TC_EXIT = Native.Constants.WM_USER + 0x64,
TC_TIME = Native.Constants.WM_USER + 0x65,
}

View File

@@ -0,0 +1,44 @@
// 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.Runtime.InteropServices;
namespace Awake.Core.Native
{
internal static class IdleTime
{
// Keep original native field names but suppress StyleCop (interop requires exact names).
[StructLayout(LayoutKind.Sequential)]
private struct LASTINPUTINFO
{
#pragma warning disable SA1307 // Interop field naming
public uint cbSize;
public uint dwTime;
#pragma warning restore SA1307
}
[DllImport("user32.dll")]
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
public static TimeSpan GetIdleTime()
{
LASTINPUTINFO info = new()
{
cbSize = (uint)Marshal.SizeOf<LASTINPUTINFO>(),
};
if (!GetLastInputInfo(ref info))
{
return TimeSpan.Zero;
}
// Calculate elapsed milliseconds since last input considering Environment.TickCount wrap.
uint lastInputTicks = info.dwTime;
uint nowTicks = (uint)Environment.TickCount;
uint delta = nowTicks >= lastInputTicks ? nowTicks - lastInputTicks : (uint.MaxValue - lastInputTicks) + nowTicks + 1;
return TimeSpan.FromMilliseconds(delta);
}
}
}

View File

@@ -0,0 +1,136 @@
// 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;
using System.Text.RegularExpressions;
namespace Awake.Core
{
public class PowerSchemeManager
{
private readonly List<PowerScheme> _schemes = new();
public PowerSchemeManager()
{
RefreshSchemes();
}
public void RefreshSchemes()
{
_schemes.Clear();
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "powercfg",
Arguments = "/L",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
},
};
process.Start();
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
var matches = Regex.Matches(output, @"Power Scheme GUID: ([a-fA-F0-9\-]+)\s+\((.+?)\)(\s+\*)?");
foreach (Match match in matches)
{
_schemes.Add(new PowerScheme
{
PSGuid = match.Groups[1].Value,
Name = match.Groups[2].Value,
IsActive = match.Groups[3].Value.Contains('*'),
});
}
// Rank schemes by performance (descending)
_schemes.Sort((a, b) => GetScore(b).CompareTo(GetScore(a)));
}
/// <summary>
/// Returns all power schemes sorted by performance (highest first).
/// </summary>
public IReadOnlyList<PowerScheme> GetAllSchemes() => _schemes;
/// <summary>
/// Returns the highest performance scheme currently available (may already be active).
/// </summary>
public PowerScheme? GetHighestPerformanceScheme()
{
if (_schemes.Count == 0)
{
RefreshSchemes();
}
return _schemes.Count == 0 ? null : _schemes[0];
}
public bool SwitchScheme(string psGuid)
{
try
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "powercfg",
Arguments = $"/setactive {psGuid}",
UseShellExecute = false,
CreateNoWindow = true,
},
};
process.Start();
process.WaitForExit();
RefreshSchemes();
return true;
}
catch
{
return false;
}
}
private static int GetScore(PowerScheme scheme)
{
// Heuristic based on name (multi-language basic keywords).
string name = scheme.Name.ToLowerInvariant();
// High performance indicators
if (name.Contains("ultimate") || name.Contains("ultra"))
{
return 380;
}
if (name.Contains("high"))
{
return 310;
}
if (name.Contains("balanced"))
{
return 200;
}
if (name.Contains("saver"))
{
return 120;
}
// Default for unknown custom plans.
return 180;
}
public class PowerScheme
{
public string PSGuid { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public bool IsActive { get; set; }
}
}
}

View File

@@ -306,6 +306,19 @@ namespace Awake.Core
break;
}
case (uint)TrayCommands.TC_MODE_ACTIVITY:
{
AwakeSettings settings = Manager.ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName);
Manager.SetActivityBasedKeepAwake(
settings.Properties.ActivityCpuThresholdPercent,
settings.Properties.ActivityMemoryThresholdPercent,
settings.Properties.ActivityNetworkThresholdKBps,
settings.Properties.ActivitySampleIntervalSeconds,
settings.Properties.ActivityInactivityTimeoutSeconds,
settings.Properties.KeepDisplayOn);
break;
}
case (uint)TrayCommands.TC_MODE_PASSIVE:
{
Manager.SetPassiveKeepAwake();
@@ -455,6 +468,7 @@ namespace Awake.Core
InsertMenuItem(0, TrayCommands.TC_MODE_PASSIVE, Resources.AWAKE_OFF, mode == AwakeMode.PASSIVE);
InsertMenuItem(0, TrayCommands.TC_MODE_INDEFINITE, Resources.AWAKE_KEEP_INDEFINITELY, mode == AwakeMode.INDEFINITE);
InsertMenuItem(0, TrayCommands.TC_MODE_EXPIRABLE, Resources.AWAKE_KEEP_UNTIL_EXPIRATION, mode == AwakeMode.EXPIRABLE, true);
InsertMenuItem(0, TrayCommands.TC_MODE_ACTIVITY, Resources.AWAKE_KEEP_ON_ACTIVITY, mode == AwakeMode.ACTIVITY);
}
}
}

View File

@@ -0,0 +1,364 @@
// 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;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Timers;
using Awake.Core.Native;
using Awake.Core.Usage.Models;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
namespace Awake.Core.Usage
{
internal sealed class ForegroundUsageTracker : IDisposable
{
private const uint EventSystemForeground = 0x0003;
private const uint WinEventOutOfContext = 0x0000;
private const double CommitThresholdSeconds = 0.25;
private static readonly JsonSerializerOptions LegacySerializer = new()
{
WriteIndented = true,
};
private delegate void WinEventDelegate(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime);
[DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(
uint eventMin,
uint eventMax,
IntPtr hmodWinEventProc,
WinEventDelegate lpfnWinEventProc,
uint idProcess,
uint idThread,
uint dwFlags);
[DllImport("user32.dll")]
private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
private readonly object _lock = new();
private readonly string _legacyJsonPath;
private readonly string _dbPath;
private readonly Timer _flushTimer;
private readonly Timer _pollTimer;
private readonly TimeSpan _idleThreshold = TimeSpan.FromSeconds(60);
private readonly Dictionary<string, AppUsageRecord> _sessionCache = new(StringComparer.OrdinalIgnoreCase);
private IUsageStore _store;
private string? _activeProcess;
private DateTime _activeSince;
private IntPtr _hook;
private WinEventDelegate? _hookDelegate;
private IntPtr _lastHwnd;
private int _retentionDays;
private bool _disposed;
internal bool Enabled { get; private set; }
public ForegroundUsageTracker(string legacyJsonPath, int retentionDays)
{
_legacyJsonPath = legacyJsonPath;
_dbPath = Path.Combine(Path.GetDirectoryName(legacyJsonPath)!, "usage.sqlite");
_retentionDays = retentionDays;
_store = new SqliteUsageStore(_dbPath);
_flushTimer = new Timer(5000)
{
AutoReset = true,
};
_flushTimer.Elapsed += (_, _) => FlushInternal();
_pollTimer = new Timer(1000)
{
AutoReset = true,
};
_pollTimer.Elapsed += (_, _) => PollForeground();
TryImportLegacy();
}
private void TryImportLegacy()
{
try
{
if (!File.Exists(_legacyJsonPath))
{
return;
}
string json = File.ReadAllText(_legacyJsonPath);
List<AppUsageRecord> list = JsonSerializer.Deserialize<List<AppUsageRecord>>(json, LegacySerializer) ?? new();
foreach (AppUsageRecord r in list)
{
_store.AddSpan(r.ProcessName, r.TotalSeconds, r.FirstSeenUtc, r.LastUpdatedUtc, _retentionDays);
}
Logger.LogInfo("[AwakeUsage] Imported legacy usage.json into SQLite. Deleting old file.");
File.Delete(_legacyJsonPath);
}
catch (Exception ex)
{
Logger.LogWarning("[AwakeUsage] Legacy import failed: " + ex.Message);
}
}
public void Configure(bool enabled, int retentionDays)
{
_retentionDays = Math.Max(1, retentionDays);
if (enabled == Enabled)
{
return;
}
Enabled = enabled;
if (Enabled)
{
_activeSince = DateTime.UtcNow;
_hookDelegate = WinEventCallback;
_hook = SetWinEventHook(EventSystemForeground, EventSystemForeground, IntPtr.Zero, _hookDelegate, 0, 0, WinEventOutOfContext);
Logger.LogInfo(_hook != IntPtr.Zero ? "[AwakeUsage] WinEvent hook installed." : "[AwakeUsage] WinEvent hook failed (poll fallback)");
CaptureInitialForeground();
_flushTimer.Start();
_pollTimer.Start();
Logger.LogInfo("[AwakeUsage] Tracking enabled (5s flush, sqlite store).");
}
else
{
_flushTimer.Stop();
_pollTimer.Stop();
if (_hook != IntPtr.Zero)
{
UnhookWinEvent(_hook);
_hook = IntPtr.Zero;
}
CommitActiveSpan();
FlushInternal(force: true);
Logger.LogInfo("[AwakeUsage] Tracking disabled.");
}
}
private void WinEventCallback(
IntPtr hWinEventHook,
uint evt,
IntPtr hwnd,
int idObj,
int idChild,
uint thread,
uint time)
{
if (_disposed || !Enabled || evt != EventSystemForeground)
{
return;
}
HandleForegroundChange(hwnd, "hook");
}
private void PollForeground()
{
if (_disposed || !Enabled)
{
return;
}
IntPtr hwnd = GetForegroundWindow();
if (hwnd == IntPtr.Zero || hwnd == _lastHwnd)
{
return;
}
HandleForegroundChange(hwnd, "poll");
}
private void CaptureInitialForeground()
{
IntPtr hwnd = GetForegroundWindow();
if (hwnd == IntPtr.Zero)
{
return;
}
if (TryResolveProcess(hwnd, out string? name))
{
_activeProcess = name;
_activeSince = DateTime.UtcNow;
_lastHwnd = hwnd;
}
}
private bool TryResolveProcess(IntPtr hwnd, out string? name)
{
name = null;
try
{
uint pid;
uint tid = GetWindowThreadProcessId(hwnd, out pid);
if (tid == 0 || pid == 0)
{
return false;
}
using Process p = Process.GetProcessById((int)pid);
name = SafeProcessName(p);
return !string.IsNullOrWhiteSpace(name);
}
catch
{
return false;
}
}
private static string SafeProcessName(Process p)
{
try
{
return Path.GetFileName(p.MainModule?.FileName) ?? p.ProcessName;
}
catch
{
return p.ProcessName;
}
}
private void HandleForegroundChange(IntPtr hwnd, string source)
{
try
{
CommitActiveSpan();
if (!TryResolveProcess(hwnd, out string? name))
{
_activeProcess = null;
return;
}
_activeProcess = name;
_activeSince = DateTime.UtcNow;
_lastHwnd = hwnd;
}
catch (Exception ex)
{
Logger.LogWarning("[AwakeUsage] FG change failed: " + ex.Message);
}
}
private void CommitActiveSpan()
{
if (string.IsNullOrEmpty(_activeProcess))
{
return;
}
if (IdleTime.GetIdleTime() > _idleThreshold)
{
_activeProcess = null;
return;
}
double secs = (DateTime.UtcNow - _activeSince).TotalSeconds;
if (secs < CommitThresholdSeconds)
{
return;
}
lock (_lock)
{
if (!_sessionCache.TryGetValue(_activeProcess!, out AppUsageRecord? rec))
{
rec = new AppUsageRecord
{
ProcessName = _activeProcess!,
FirstSeenUtc = DateTime.UtcNow,
LastUpdatedUtc = DateTime.UtcNow,
TotalSeconds = 0,
};
_sessionCache[_activeProcess!] = rec;
}
rec.TotalSeconds += secs;
rec.LastUpdatedUtc = DateTime.UtcNow;
}
_activeSince = DateTime.UtcNow;
}
private void FlushInternal(bool force = false)
{
try
{
CommitActiveSpan();
Dictionary<string, AppUsageRecord> snapshot;
lock (_lock)
{
snapshot = _sessionCache.ToDictionary(k => k.Key, v => v.Value);
_sessionCache.Clear();
}
foreach (AppUsageRecord rec in snapshot.Values)
{
_store.AddSpan(rec.ProcessName, rec.TotalSeconds, rec.FirstSeenUtc, rec.LastUpdatedUtc, _retentionDays);
}
if (force)
{
_store.Prune(_retentionDays);
}
}
catch (Exception ex)
{
Logger.LogWarning("[AwakeUsage] Flush failed: " + ex.Message);
}
}
public IReadOnlyList<AppUsageRecord> GetSummary(int top, int days)
{
CommitActiveSpan();
FlushInternal();
try
{
return _store.Query(top, days);
}
catch (Exception ex)
{
Logger.LogWarning("[AwakeUsage] Query failed: " + ex.Message);
return Array.Empty<AppUsageRecord>();
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
Configure(false, _retentionDays);
_store.Dispose();
_flushTimer.Dispose();
_pollTimer.Dispose();
}
}
}

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.
#pragma warning disable SA1516, SA1636
using System;
using System.Collections.Generic;
using Awake.Core.Usage.Models;
namespace Awake.Core.Usage
{
internal interface IUsageStore : IDisposable
{
void AddSpan(string processName, double seconds, DateTime firstSeenUtc, DateTime lastUpdatedUtc, int retentionDays);
IReadOnlyList<AppUsageRecord> Query(int top, int days);
void Prune(int retentionDays);
}
}
#pragma warning restore SA1516, SA1636

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.
using System;
using System.Text.Json.Serialization;
namespace Awake.Core.Usage.Models
{
internal sealed class AppUsageRecord
{
[JsonPropertyName("process")]
public string ProcessName { get; set; } = string.Empty;
[JsonPropertyName("totalSeconds")]
public double TotalSeconds { get; set; }
[JsonPropertyName("lastUpdatedUtc")]
public DateTime LastUpdatedUtc { get; set; }
[JsonPropertyName("firstSeenUtc")]
public DateTime FirstSeenUtc { get; set; }
}
}

View File

@@ -0,0 +1,145 @@
// 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.
#pragma warning disable SA1516, SA1210, SA1636
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using Awake.Core.Usage.Models;
using ManagedCommon;
using Microsoft.Data.Sqlite;
namespace Awake.Core.Usage
{
internal sealed class SqliteUsageStore : IUsageStore
{
private readonly string _dbPath;
private readonly string _connectionString;
public SqliteUsageStore(string dbPath)
{
_dbPath = dbPath;
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
_connectionString = new SqliteConnectionStringBuilder
{
DataSource = _dbPath,
Mode = SqliteOpenMode.ReadWriteCreate,
}.ToString();
Initialize();
}
private void Initialize()
{
using SqliteConnection conn = new(_connectionString);
conn.Open();
using SqliteCommand cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS process_usage (
process_name TEXT NOT NULL,
day_utc TEXT NOT NULL,
total_seconds REAL NOT NULL,
first_seen_utc TEXT NOT NULL,
last_updated_utc TEXT NOT NULL,
PRIMARY KEY(process_name, day_utc)
);";
cmd.ExecuteNonQuery();
}
public void AddSpan(string processName, double seconds, DateTime firstSeenUtc, DateTime lastUpdatedUtc, int retentionDays)
{
if (seconds <= 0)
{
return;
}
string day = DateTime.UtcNow.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture);
using SqliteConnection conn = new(_connectionString);
conn.Open();
using SqliteTransaction tx = conn.BeginTransaction();
using (SqliteCommand cmd = conn.CreateCommand())
{
cmd.Transaction = tx;
cmd.CommandText = @"INSERT INTO process_usage(process_name, day_utc, total_seconds, first_seen_utc, last_updated_utc)
VALUES($p,$d,$s,$f,$l)
ON CONFLICT(process_name,day_utc) DO UPDATE SET
total_seconds = total_seconds + excluded.total_seconds,
last_updated_utc = excluded.last_updated_utc;";
cmd.Parameters.AddWithValue("$p", processName);
cmd.Parameters.AddWithValue("$d", day);
cmd.Parameters.AddWithValue("$s", seconds);
cmd.Parameters.AddWithValue("$f", firstSeenUtc.ToString("o"));
cmd.Parameters.AddWithValue("$l", lastUpdatedUtc.ToString("o"));
cmd.ExecuteNonQuery();
}
using (SqliteCommand prune = conn.CreateCommand())
{
prune.Transaction = tx;
prune.CommandText = @"DELETE FROM process_usage WHERE day_utc < date('now', @retention);";
prune.Parameters.AddWithValue("@retention", $"-{Math.Max(1, retentionDays)} days");
prune.ExecuteNonQuery();
}
tx.Commit();
}
public IReadOnlyList<AppUsageRecord> Query(int top, int days)
{
List<AppUsageRecord> result = new();
int safeDays = Math.Max(1, days);
using SqliteConnection conn = new(_connectionString);
conn.Open();
using SqliteCommand cmd = conn.CreateCommand();
cmd.CommandText = @"SELECT process_name, SUM(total_seconds) AS total_seconds, MIN(first_seen_utc) AS first_seen_utc, MAX(last_updated_utc) AS last_updated_utc
FROM process_usage
WHERE day_utc >= date('now', @cutoff)
GROUP BY process_name
ORDER BY total_seconds DESC
LIMIT @top;";
cmd.Parameters.AddWithValue("@cutoff", $"-{safeDays} days");
cmd.Parameters.AddWithValue("@top", top);
using SqliteDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
while (reader.Read())
{
try
{
string name = reader.GetString(0);
double secs = reader.GetDouble(1);
DateTime first = DateTime.Parse(reader.GetString(2), null, System.Globalization.DateTimeStyles.RoundtripKind);
DateTime last = DateTime.Parse(reader.GetString(3), null, System.Globalization.DateTimeStyles.RoundtripKind);
result.Add(new AppUsageRecord
{
ProcessName = name,
TotalSeconds = secs,
FirstSeenUtc = first,
LastUpdatedUtc = last,
});
}
catch (Exception ex)
{
Logger.LogWarning("[AwakeUsage][SQLite] Row parse failed: " + ex.Message);
}
}
return result;
}
public void Prune(int retentionDays)
{
using SqliteConnection conn = new(_connectionString);
conn.Open();
using SqliteCommand cmd = conn.CreateCommand();
cmd.CommandText = "DELETE FROM process_usage WHERE day_utc < date('now', @cutoff);";
cmd.Parameters.AddWithValue("@cutoff", $"-{Math.Max(1, retentionDays)} days");
cmd.ExecuteNonQuery();
}
public void Dispose()
{
}
}
}
#pragma warning restore SA1516, SA1210, SA1636

View File

@@ -0,0 +1,31 @@
// 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.Runtime.InteropServices;
namespace Awake
{
/// <summary>
/// COM automation interface exposed via ROT for controlling Awake.
/// </summary>
[ComVisible(true)]
[Guid("5CA92C1D-9D7E-4F6D-9B06-5B7B28BF4E21")]
public interface IAwakeAutomation
{
string Ping();
void SetIndefinite();
void SetTimed(int seconds);
void SetExpirable(int minutes);
void SetPassive();
void Cancel();
string GetStatusJson();
}
}

View File

@@ -18,6 +18,9 @@ using System.Threading.Tasks;
using Awake.Core;
using Awake.Core.Models;
using Awake.Core.Native;
// Usage tracking
using Awake.Core.Usage;
using Awake.Properties;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
@@ -33,12 +36,22 @@ namespace Awake
private static readonly string[] _aliasesPidOption = ["--pid", "-p"];
private static readonly string[] _aliasesExpireAtOption = ["--expire-at", "-e"];
private static readonly string[] _aliasesParentPidOption = ["--use-parent-pid", "-u"];
private static readonly string[] _aliasesHttpServerOption = ["--http-server", "-s"];
private static readonly string[] _aliasesHttpPortOption = ["--http-port"];
private static readonly string[] _aliasesActivityOption = ["--activity", "-a"];
private static readonly string[] _aliasesActivityCpuOption = ["--act-cpu"];
private static readonly string[] _aliasesActivityMemOption = ["--act-mem"];
private static readonly string[] _aliasesActivityNetOption = ["--act-net"];
private static readonly string[] _aliasesActivitySampleOption = ["--act-sample"];
private static readonly string[] _aliasesActivityTimeoutOption = ["--act-timeout"];
private static readonly JsonSerializerOptions _serializerOptions = new() { IncludeFields = true };
private static readonly ETWTrace _etwTrace = new();
private static FileSystemWatcher? _watcher;
private static SettingsUtils? _settingsUtils;
private static ManagedCommon.HttpServer? _httpServer;
private static AwakeHttpHandler? _awakeHttpHandler;
private static bool _startedFromPowerToys;
@@ -71,7 +84,11 @@ namespace Awake
}
await TrayHelper.InitializeTray(TrayHelper.DefaultAwakeIcon, Core.Constants.FullAppName);
AppDomain.CurrentDomain.ProcessExit += (_, _) => TrayHelper.RunOnMainThread(() => LockMutex?.ReleaseMutex());
AppDomain.CurrentDomain.ProcessExit += (_, _) => TrayHelper.RunOnMainThread(() =>
{
_httpServer?.Dispose();
LockMutex?.ReleaseMutex();
});
AppDomain.CurrentDomain.UnhandledException += AwakeUnhandledExceptionCatcher;
if (!instantiated)
@@ -96,6 +113,12 @@ namespace Awake
Logger.LogInfo($"OS: {Environment.OSVersion}");
Logger.LogInfo($"OS Build: {Manager.GetOperatingSystemBuild()}");
// Start background COM automation host (ROT) so any startup path exposes the automation surface.
// Uses default moniker; could be extended with a --rotname parameter if needed later.
var rotHost = new RotSingletonHost("Awake.Automation", () => new AwakeAutomation(), "AwakeAutomationRotThread");
rotHost.Start();
AppDomain.CurrentDomain.ProcessExit += (_, _) => rotHost.Stop();
TaskScheduler.UnobservedTaskException += (sender, args) =>
{
Trace.WriteLine($"Task scheduler error: {args.Exception.Message}"); // somebody forgot to check!
@@ -145,6 +168,50 @@ namespace Awake
IsRequired = false,
};
Option<bool> httpServerOption = new(_aliasesHttpServerOption, () => true, "Enable HTTP server for remote control")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<int> httpPortOption = new(_aliasesHttpPortOption, () => 8080, "HTTP server port (default: 8080)")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
// Activity-based keep-awake options
Option<bool> activityOption = new(_aliasesActivityOption, () => false, "Enable activity-based keep awake mode (CPU / Memory / Network)")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> activityCpuOption = new(_aliasesActivityCpuOption, () => 20, "Activity mode CPU threshold percent (0-100, 0 to ignore)")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> activityMemOption = new(_aliasesActivityMemOption, () => 50, "Activity mode Memory usage threshold percent (0-100, 0 to ignore)")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> activityNetOption = new(_aliasesActivityNetOption, () => 100, "Activity mode total network throughput threshold KB/s (0 to ignore)")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> activitySampleOption = new(_aliasesActivitySampleOption, () => 5, "Activity mode sampling interval in seconds (min 1)")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> activityTimeoutOption = new(_aliasesActivityTimeoutOption, () => 60, "Activity mode inactivity timeout in seconds before reverting (min 5)")
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
timeOption.AddValidator(result =>
{
if (result.Tokens.Count != 0 && !uint.TryParse(result.Tokens[0].Value, out _))
@@ -175,6 +242,16 @@ namespace Awake
}
});
httpPortOption.AddValidator(result =>
{
if (result.Tokens.Count != 0 && (!int.TryParse(result.Tokens[0].Value, out int port) || port < 1 || port > 65535))
{
string errorMessage = $"HTTP port value could not be parsed correctly or is out of range (1-65535). Value used: {result.Tokens[0].Value}.";
Logger.LogError(errorMessage);
result.ErrorMessage = errorMessage;
}
});
RootCommand? rootCommand =
[
configOption,
@@ -183,10 +260,56 @@ namespace Awake
pidOption,
expireAtOption,
parentPidOption,
httpServerOption,
httpPortOption,
activityOption,
activityCpuOption,
activityMemOption,
activityNetOption,
activitySampleOption,
activityTimeoutOption,
];
rootCommand.Description = Core.Constants.AppName;
rootCommand.SetHandler(HandleCommandLineArguments, configOption, displayOption, timeOption, pidOption, expireAtOption, parentPidOption);
// NOTE: The strongly-typed SetHandler overloads have an upper limit on the number of symbols
// they accept. We exceed that limit with the new activity-mode parameters. Switch to a context
// based handler and manually extract option values from the ParseResult.
rootCommand.SetHandler(context =>
{
var pr = context.ParseResult;
bool usePtConfig = pr.GetValueForOption(configOption);
bool displayOn = pr.GetValueForOption(displayOption);
uint timeLimit = pr.GetValueForOption(timeOption);
int pid = pr.GetValueForOption(pidOption);
string expireAt = pr.GetValueForOption(expireAtOption) ?? string.Empty;
bool useParentPid = pr.GetValueForOption(parentPidOption);
bool enableHttpServer = pr.GetValueForOption(httpServerOption);
int httpPort = pr.GetValueForOption(httpPortOption);
bool activityMode = pr.GetValueForOption(activityOption);
uint actCpu = pr.GetValueForOption(activityCpuOption);
uint actMem = pr.GetValueForOption(activityMemOption);
uint actNet = pr.GetValueForOption(activityNetOption);
uint actSample = pr.GetValueForOption(activitySampleOption);
uint actTimeout = pr.GetValueForOption(activityTimeoutOption);
HandleCommandLineArguments(
usePtConfig,
displayOn,
timeLimit,
pid,
expireAt,
useParentPid,
enableHttpServer,
httpPort,
activityMode,
actCpu,
actMem,
actNet,
actSample,
actTimeout);
});
return rootCommand.InvokeAsync(args).Result;
}
@@ -211,12 +334,13 @@ namespace Awake
private static void Exit(string message, int exitCode)
{
_httpServer?.Dispose();
_etwTrace?.Dispose();
Logger.LogInfo(message);
Manager.CompleteExit(exitCode);
}
private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid, string expireAt, bool useParentPid)
private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid, string expireAt, bool useParentPid, bool enableHttpServer, int httpPort, bool activityMode, uint actCpu, uint actMem, uint actNet, uint actSample, uint actTimeout)
{
if (pid == 0 && !useParentPid)
{
@@ -235,6 +359,35 @@ namespace Awake
Logger.LogInfo($"The value for --pid is: {pid}");
Logger.LogInfo($"The value for --expire-at is: {expireAt}");
Logger.LogInfo($"The value for --use-parent-pid is: {useParentPid}");
Logger.LogInfo($"The value for --http-server is: {enableHttpServer}");
Logger.LogInfo($"The value for --http-port is: {httpPort}");
Logger.LogInfo($"The value for --activity is: {activityMode}");
if (activityMode)
{
Logger.LogInfo($"Activity thresholds CPU={actCpu}% MEM={actMem}% NET={actNet}KB/s SAMPLE={actSample}s TIMEOUT={actTimeout}s");
}
// Initialize HTTP server if requested
if (enableHttpServer)
{
try
{
string prefix = $"http://localhost:{httpPort}/";
_httpServer = new ManagedCommon.HttpServer(prefix);
// Register Awake-specific handler
_awakeHttpHandler = new AwakeHttpHandler();
_httpServer.RegisterHandler(_awakeHttpHandler);
_httpServer.Start();
Logger.LogInfo($"HTTP server started on {prefix}");
Logger.LogInfo($"Available endpoints: GET {prefix}status, GET {prefix}awake/...");
}
catch (Exception ex)
{
Logger.LogError($"Failed to start HTTP server: {ex.Message}");
}
}
// Start the monitor thread that will be used to track the current state.
Manager.StartMonitor();
@@ -285,6 +438,11 @@ namespace Awake
Logger.LogError($"There was a problem with the configuration file. Make sure it exists. {ex.Message}");
}
}
else if (!usePtConfig && activityMode)
{
Logger.LogInfo("Starting activity-based keep-awake mode (CLI override).");
Manager.SetActivityBasedKeepAwake(actCpu, actMem, actNet, actSample, actTimeout, displayOn);
}
else if (pid != 0 || useParentPid)
{
// Second, we snap to process-based execution. Because this is something that
@@ -342,6 +500,33 @@ namespace Awake
}
}
}
// Initialize usage tracking
InitializeUsageTracking();
}
private static void InitializeUsageTracking()
{
try
{
string settingsPath = _settingsUtils!.GetSettingsFilePath(Core.Constants.AppName);
string directory = Path.GetDirectoryName(settingsPath)!;
string usageFile = Path.Combine(directory, "usage.json");
AwakeSettings settings = _settingsUtils.GetSettings<AwakeSettings>(Core.Constants.AppName) ?? new AwakeSettings();
if (Manager.UsageTracker == null)
{
Manager.UsageTracker = new ForegroundUsageTracker(usageFile, settings.Properties.UsageRetentionDays);
}
Manager.UsageTracker.Configure(settings.Properties.TrackUsageEnabled, settings.Properties.UsageRetentionDays);
Logger.LogInfo($"Usage tracking configured (enabled={settings.Properties.TrackUsageEnabled}, retentionDays={settings.Properties.UsageRetentionDays}).");
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize usage tracking: {ex.Message}");
}
}
private static void AllocateLocalConsole()
@@ -361,6 +546,7 @@ namespace Awake
SetupFileSystemWatcher(settingsPath);
InitializeSettings();
ProcessSettings();
InitializeUsageTracking(); // after initial settings load
}
catch (Exception ex)
{
@@ -406,6 +592,7 @@ namespace Awake
{
Logger.LogInfo("Detected a settings file change. Updating configuration...");
ProcessSettings();
InitializeUsageTracking(); // re-evaluate usage tracking on config change
}
catch (Exception e)
{
@@ -448,6 +635,22 @@ namespace Awake
Manager.SetExpirableKeepAwake(settings.Properties.ExpirationDateTime, settings.Properties.KeepDisplayOn);
break;
case AwakeMode.ACTIVITY:
Manager.SetActivityBasedKeepAwake(
settings.Properties.ActivityCpuThresholdPercent,
settings.Properties.ActivityMemoryThresholdPercent,
settings.Properties.ActivityNetworkThresholdKBps,
settings.Properties.ActivitySampleIntervalSeconds,
settings.Properties.ActivityInactivityTimeoutSeconds,
settings.Properties.KeepDisplayOn);
break;
case AwakeMode.PROCESS:
Manager.SetProcessBasedKeepAwake(
settings.Properties.ProcessMonitoringList,
settings.Properties.ProcessCheckIntervalSeconds,
settings.Properties.KeepDisplayOn);
break;
default:
Logger.LogError("Unknown mode of operation. Check config file.");

View File

@@ -213,6 +213,15 @@ namespace Awake.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Keep awake on activity (CPU / Memory / Network).
/// </summary>
internal static string AWAKE_KEEP_ON_ACTIVITY {
get {
return ResourceManager.GetString("AWAKE_KEEP_ON_ACTIVITY", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to d.
/// </summary>
@@ -339,6 +348,15 @@ namespace Awake.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Activity.
/// </summary>
internal static string AWAKE_TRAY_TEXT_ACTIVITY {
get {
return ResourceManager.GetString("AWAKE_TRAY_TEXT_ACTIVITY", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unchecked.
/// </summary>

View File

@@ -146,6 +146,10 @@
<value>Keep awake until expiration date and time</value>
<comment>Keep the system awake until expiration date and time</comment>
</data>
<data name="AWAKE_KEEP_ON_ACTIVITY" xml:space="preserve">
<value>Keep awake on activity (CPU / Memory / Network)</value>
<comment>Keep the system awake while resource activity is above configured thresholds</comment>
</data>
<data name="AWAKE_MINUTE" xml:space="preserve">
<value>{0} minute</value>
<comment>{0} shouldn't be removed. It will be replaced by the number 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
@@ -197,6 +201,9 @@
<data name="AWAKE_TRAY_TEXT_TIMED" xml:space="preserve">
<value>Interval</value>
</data>
<data name="AWAKE_TRAY_TEXT_ACTIVITY" xml:space="preserve">
<value>Activity</value>
</data>
<data name="AWAKE_LABEL_DAYS" xml:space="preserve">
<value>d</value>
<comment>Used to display number of days in the system tray tooltip.</comment>

View File

@@ -0,0 +1,131 @@
<#
AwakeRotMini.ps1 — Minimal ROT client for the Awake COM automation object.
Supported actions:
ping -> Calls Ping(), prints PING=pong
status -> Calls GetStatusJson(), prints JSON
cancel -> Calls Cancel(), prints CANCEL_OK
timed:<m> -> Calls SetTimed(<m * 60 seconds>), prints TIMED_OK (minutes input)
Assumptions:
- Server registered object in ROT via CreateItemMoniker("!", logicalName)
- We only need late binding (IDispatch InvokeMember) no type library.
Exit codes:
0 = success
2 = object not found in ROT
4 = call/parse error
NOTE: This script intentionally stays dependencylight and fast to start.
#>
param(
[string]$MonikerName = 'Awake.Automation',
[string]$Action = 'ping'
)
# ---------------------------------------------------------------------------
# Inline C# (single public class) handles:
# * ROT lookup via CreateItemMoniker("!", logicalName)
# * Late bound InvokeMember calls
# * Lightweight action dispatch
# ---------------------------------------------------------------------------
if (-not ('AwakeRotMiniClient' -as [type])) {
$code = @'
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
public static class AwakeRotMiniClient
{
// P/Invoke -------------------------------------------------------------
[DllImport("ole32.dll")] private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable rot);
[DllImport("ole32.dll")] private static extern int CreateBindCtx(int reserved, out IBindCtx ctx);
[DllImport("ole32.dll", CharSet = CharSet.Unicode)] private static extern int CreateItemMoniker(string delimiter, string item, out IMoniker mk);
// Internal helpers -----------------------------------------------------
private static void Open(out IRunningObjectTable rot, out IBindCtx ctx)
{
GetRunningObjectTable(0, out rot);
CreateBindCtx(0, out ctx);
}
private static object BindLogical(string logical)
{
Open(out var rot, out var ctx);
if (CreateItemMoniker("!", logical, out var mk) == 0)
{
try
{
rot.GetObject(mk, out var obj);
return obj;
}
catch
{
// Swallow treated as not found below.
}
}
return null;
}
private static object Call(object obj, string name, params object[] args)
{
var t = obj.GetType(); // System.__ComObject
return t.InvokeMember(
name,
System.Reflection.BindingFlags.InvokeMethod |
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance,
null,
obj,
(args == null || args.Length == 0) ? null : args);
}
// Public entry ---------------------------------------------------------
public static string Exec(string logical, string action)
{
var obj = BindLogical(logical);
if (obj == null)
return "__NOT_FOUND__";
if (string.IsNullOrEmpty(action) || action == "ping")
{
try { return "PING=" + Call(obj, "Ping"); } catch (Exception ex) { return Err(ex); }
}
try
{
if (action == "status")
return (string)Call(obj, "GetStatusJson");
if (action == "cancel")
{
Call(obj, "Cancel");
return "CANCEL_OK";
}
if (action.StartsWith("timed:", StringComparison.OrdinalIgnoreCase))
{
var slice = action.Substring(6);
if (!int.TryParse(slice, out var minutes) || minutes < 0)
return "__ERR=Format:Invalid minutes";
Call(obj, "SetTimed", minutes * 60);
return "TIMED_OK";
}
return "UNKNOWN_ACTION";
}
catch (Exception ex)
{
return Err(ex);
}
}
private static string Err(Exception ex) => "__ERR=" + ex.GetType().Name + ":" + ex.Message;
}
'@
Add-Type -TypeDefinition $code -ErrorAction Stop | Out-Null
}
$result = [AwakeRotMiniClient]::Exec($MonikerName, $Action)
switch ($result) {
'__NOT_FOUND__' { exit 2 }
{ $_ -like '__ERR=*' } { $host.UI.WriteErrorLine($result); exit 4 }
default { Write-Output $result; exit 0 }
}

Some files were not shown because too many files have changed in this diff Show More