mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-04 11:26:36 +01:00
Compare commits
88 Commits
leilzh/fix
...
dev/vanzue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c897105b42 | ||
|
|
1d13779697 | ||
|
|
37bd24db36 | ||
|
|
557a07589d | ||
|
|
2603efc8a9 | ||
|
|
dd138fb94b | ||
|
|
7cd201d355 | ||
|
|
9aab0f3893 | ||
|
|
91b7a99e76 | ||
|
|
d38edf798d | ||
|
|
17668047bf | ||
|
|
7b0b284d40 | ||
|
|
9aca6d136f | ||
|
|
4b2ee60b42 | ||
|
|
e37a328624 | ||
|
|
66e96bbe9d | ||
|
|
e13d6a78aa | ||
|
|
73786cd2be | ||
|
|
4de4d5f310 | ||
|
|
f8c5ff8c0c | ||
|
|
d32ea86314 | ||
|
|
177f144e6d | ||
|
|
7b469f6327 | ||
|
|
94ace730c8 | ||
|
|
a08fc0921f | ||
|
|
995bbdc62d | ||
|
|
f822826cf1 | ||
|
|
97c1de8bf6 | ||
|
|
0f8cf94d90 | ||
|
|
620f67a3ba | ||
|
|
23bc278cc8 | ||
|
|
73e379238b | ||
|
|
4710b816b4 | ||
|
|
b8a0163419 | ||
|
|
06fcbdac40 | ||
|
|
d515c67def | ||
|
|
9439b6df41 | ||
|
|
a37add8f08 | ||
|
|
60deec6815 | ||
|
|
7e791f2815 | ||
|
|
2b0ecc2979 | ||
|
|
45cf3de15d | ||
|
|
bf8c548501 | ||
|
|
121c6c0712 | ||
|
|
e68526b8d8 | ||
|
|
995a699de7 | ||
|
|
5f6df35d8d | ||
|
|
cffdecbc1b | ||
|
|
a4d8405957 | ||
|
|
29858c6782 | ||
|
|
aa0dd19b64 | ||
|
|
39afe4f196 | ||
|
|
5b6645ac27 | ||
|
|
c67f5d52f1 | ||
|
|
ce7cfee3f3 | ||
|
|
b354f56a72 | ||
|
|
737b092022 | ||
|
|
8c5fb85abb | ||
|
|
098244d7f2 | ||
|
|
8b94d8c233 | ||
|
|
9f1caa5ff2 | ||
|
|
fdc61e5dff | ||
|
|
dbc30045b5 | ||
|
|
a7587a9af1 | ||
|
|
fe236fb20c | ||
|
|
78df0c6b9c | ||
|
|
5fb5c91703 | ||
|
|
e120eca6c9 | ||
|
|
591371bbba | ||
|
|
0bce23fff0 | ||
|
|
4bdb9de7d6 | ||
|
|
75354e676e | ||
|
|
95294419ba | ||
|
|
a3a07e3a0e | ||
|
|
ac38d9abaf | ||
|
|
210124510a | ||
|
|
7f055d11b5 | ||
|
|
f891ca2f27 | ||
|
|
f6b97cc16d | ||
|
|
56611d07ff | ||
|
|
278667834f | ||
|
|
bdf3bff18d | ||
|
|
a100d9b352 | ||
|
|
7f5b5d57ad | ||
|
|
63b6ccc2c5 | ||
|
|
c005996b0c | ||
|
|
0303d59d86 | ||
|
|
dee6a62a68 |
11
.github/actions/spell-check/allow/code.txt
vendored
11
.github/actions/spell-check/allow/code.txt
vendored
@@ -95,6 +95,7 @@ OTP
|
||||
Yubi
|
||||
Yubico
|
||||
Perplexity
|
||||
Groq
|
||||
svgl
|
||||
|
||||
# KEYS
|
||||
@@ -328,3 +329,13 @@ FFF
|
||||
HHH
|
||||
riday
|
||||
YYY
|
||||
|
||||
# GitHub issue/PR commands
|
||||
azp
|
||||
feedbackhub
|
||||
needinfo
|
||||
reportbug
|
||||
|
||||
#ffmpeg
|
||||
crf
|
||||
nostdin
|
||||
|
||||
16
.github/actions/spell-check/expect.txt
vendored
16
.github/actions/spell-check/expect.txt
vendored
@@ -141,8 +141,11 @@ BITSPIXEL
|
||||
bla
|
||||
BLACKFRAME
|
||||
BLENDFUNCTION
|
||||
blittable
|
||||
Blockquotes
|
||||
blt
|
||||
bluelightreduction
|
||||
bluelightreductionstate
|
||||
BLURBEHIND
|
||||
BLURREGION
|
||||
bmi
|
||||
@@ -249,6 +252,7 @@ colorformat
|
||||
colorhistory
|
||||
colorhistorylimit
|
||||
COLORKEY
|
||||
colorref
|
||||
comctl
|
||||
comdlg
|
||||
comexp
|
||||
@@ -320,7 +324,6 @@ CUSTOMFORMATPLACEHOLDER
|
||||
CVal
|
||||
cvd
|
||||
CVirtual
|
||||
CVS
|
||||
CWMO
|
||||
CXSCREEN
|
||||
CXSMICON
|
||||
@@ -728,9 +731,9 @@ HWNDPARENT
|
||||
HWNDPREV
|
||||
hyjiacan
|
||||
IAI
|
||||
icf
|
||||
ICONERROR
|
||||
ICONLOCATION
|
||||
icf
|
||||
IDCANCEL
|
||||
IDD
|
||||
idk
|
||||
@@ -1111,6 +1114,7 @@ NEWPLUSSHELLEXTENSIONWIN
|
||||
newrow
|
||||
nicksnettravels
|
||||
NIF
|
||||
nightlight
|
||||
NLog
|
||||
NLSTEXT
|
||||
NMAKE
|
||||
@@ -1481,6 +1485,7 @@ rgh
|
||||
rgn
|
||||
rgs
|
||||
rguid
|
||||
rhk
|
||||
RIDEV
|
||||
RIGHTSCROLLBAR
|
||||
riid
|
||||
@@ -1586,6 +1591,7 @@ SHGDNF
|
||||
SHGFI
|
||||
SHIL
|
||||
shinfo
|
||||
shk
|
||||
shlwapi
|
||||
shobjidl
|
||||
SHORTCUTATLEAST
|
||||
@@ -1795,6 +1801,7 @@ tlb
|
||||
tlbimp
|
||||
tlc
|
||||
tmain
|
||||
tml
|
||||
TNP
|
||||
Toolhelp
|
||||
toolwindow
|
||||
@@ -1844,6 +1851,8 @@ uitests
|
||||
UITo
|
||||
ULONGLONG
|
||||
ums
|
||||
UMax
|
||||
UMin
|
||||
uncompilable
|
||||
UNCPRIORITY
|
||||
UNDNAME
|
||||
@@ -1857,6 +1866,7 @@ unittests
|
||||
UNLEN
|
||||
UNORM
|
||||
unremapped
|
||||
Unsubscribes
|
||||
unvirtualized
|
||||
unwide
|
||||
unzoom
|
||||
@@ -1941,7 +1951,7 @@ vswhere
|
||||
Vtbl
|
||||
WANTNUKEWARNING
|
||||
WANTPALM
|
||||
wasdk
|
||||
WASDK
|
||||
wbem
|
||||
WBounds
|
||||
Wca
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
"PowerToys.FancyZonesEditorCommon.dll",
|
||||
"PowerToys.FancyZonesModuleInterface.dll",
|
||||
"PowerToys.FancyZones.exe",
|
||||
"FancyZonesCLI.exe",
|
||||
"FancyZonesCLI.dll",
|
||||
|
||||
"PowerToys.GcodePreviewHandler.dll",
|
||||
"PowerToys.GcodePreviewHandler.exe",
|
||||
@@ -351,6 +353,11 @@
|
||||
"Microsoft.SemanticKernel.Connectors.Ollama.dll",
|
||||
"OllamaSharp.dll",
|
||||
|
||||
"boost_regex-vc143-mt-gd-x32-1_87.dll",
|
||||
"boost_regex-vc143-mt-gd-x64-1_87.dll",
|
||||
"boost_regex-vc143-mt-x32-1_87.dll",
|
||||
"boost_regex-vc143-mt-x64-1_87.dll",
|
||||
|
||||
"UnitsNet.dll",
|
||||
"UtfUnknown.dll",
|
||||
"Wpf.Ui.dll"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Param(
|
||||
# Using the default value of 1.7 for winAppSdkVersionNumber and useExperimentalVersion as false
|
||||
[Parameter(Mandatory=$False,Position=1)]
|
||||
[string]$winAppSdkVersionNumber = "1.7",
|
||||
[string]$winAppSdkVersionNumber = "1.8",
|
||||
|
||||
# When the pipeline calls the PS1 file, the passed parameters are converted to string type
|
||||
[Parameter(Mandatory=$False,Position=2)]
|
||||
@@ -16,32 +16,7 @@ Param(
|
||||
[string]$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
|
||||
)
|
||||
|
||||
function Update-NugetConfig {
|
||||
param (
|
||||
[string]$filePath = [System.IO.Path]::Combine($rootPath, "nuget.config")
|
||||
)
|
||||
|
||||
Write-Host "Updating nuget.config file"
|
||||
[xml]$xml = Get-Content -Path $filePath
|
||||
|
||||
# Add localpackages source into nuget.config
|
||||
$packageSourcesNode = $xml.configuration.packageSources
|
||||
$addNode = $xml.CreateElement("add")
|
||||
$addNode.SetAttribute("key", "localpackages")
|
||||
$addNode.SetAttribute("value", "localpackages")
|
||||
$packageSourcesNode.AppendChild($addNode) | Out-Null
|
||||
|
||||
# Remove <packageSourceMapping> tag and its content
|
||||
$packageSourceMappingNode = $xml.configuration.packageSourceMapping
|
||||
if ($packageSourceMappingNode) {
|
||||
$xml.configuration.RemoveChild($packageSourceMappingNode) | Out-Null
|
||||
}
|
||||
|
||||
# print nuget.config after modification
|
||||
$xml.OuterXml
|
||||
# Save the modified nuget.config file
|
||||
$xml.Save($filePath)
|
||||
}
|
||||
|
||||
function Read-FileWithEncoding {
|
||||
param (
|
||||
@@ -71,6 +46,132 @@ function Write-FileWithEncoding {
|
||||
$writer.Close()
|
||||
}
|
||||
|
||||
|
||||
function Add-NuGetSourceAndMapping {
|
||||
param (
|
||||
[xml]$Xml,
|
||||
[string]$Key,
|
||||
[string]$Value,
|
||||
[string[]]$Patterns
|
||||
)
|
||||
|
||||
# Ensure packageSources exists
|
||||
if (-not $Xml.configuration.packageSources) {
|
||||
$Xml.configuration.AppendChild($Xml.CreateElement("packageSources")) | Out-Null
|
||||
}
|
||||
$sources = $Xml.configuration.packageSources
|
||||
|
||||
# Add/Update Source
|
||||
$sourceNode = $sources.SelectSingleNode("add[@key='$Key']")
|
||||
if (-not $sourceNode) {
|
||||
$sourceNode = $Xml.CreateElement("add")
|
||||
$sourceNode.SetAttribute("key", $Key)
|
||||
$sources.AppendChild($sourceNode) | Out-Null
|
||||
}
|
||||
$sourceNode.SetAttribute("value", $Value)
|
||||
|
||||
# Ensure packageSourceMapping exists
|
||||
if (-not $Xml.configuration.packageSourceMapping) {
|
||||
$Xml.configuration.AppendChild($Xml.CreateElement("packageSourceMapping")) | Out-Null
|
||||
}
|
||||
$mapping = $Xml.configuration.packageSourceMapping
|
||||
|
||||
# Remove invalid packageSource nodes (missing key or empty key)
|
||||
$invalidNodes = $mapping.SelectNodes("packageSource[not(@key) or @key='']")
|
||||
if ($invalidNodes) {
|
||||
foreach ($node in $invalidNodes) {
|
||||
$mapping.RemoveChild($node) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Add/Update Mapping Source
|
||||
$mappingSource = $mapping.SelectSingleNode("packageSource[@key='$Key']")
|
||||
if (-not $mappingSource) {
|
||||
$mappingSource = $Xml.CreateElement("packageSource")
|
||||
$mappingSource.SetAttribute("key", $Key)
|
||||
# Insert at top for priority
|
||||
if ($mapping.HasChildNodes) {
|
||||
$mapping.InsertBefore($mappingSource, $mapping.FirstChild) | Out-Null
|
||||
} else {
|
||||
$mapping.AppendChild($mappingSource) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Double check and force attribute
|
||||
if (-not $mappingSource.HasAttribute("key")) {
|
||||
$mappingSource.SetAttribute("key", $Key)
|
||||
}
|
||||
|
||||
# Update Patterns
|
||||
# RemoveAll() removes all child nodes AND attributes, so we must re-set the key afterwards
|
||||
$mappingSource.RemoveAll()
|
||||
$mappingSource.SetAttribute("key", $Key)
|
||||
|
||||
foreach ($pattern in $Patterns) {
|
||||
$pkg = $Xml.CreateElement("package")
|
||||
$pkg.SetAttribute("pattern", $pattern)
|
||||
$mappingSource.AppendChild($pkg) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-WinAppSdkSplitDependencies {
|
||||
Write-Host "Version $WinAppSDKVersion detected. Resolving split dependencies..."
|
||||
$installDir = Join-Path $rootPath "localpackages\output"
|
||||
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
|
||||
|
||||
# Create a temporary nuget.config to avoid interference from the repo's config
|
||||
$tempConfig = Join-Path $env:TEMP "nuget_$(Get-Random).config"
|
||||
Set-Content -Path $tempConfig -Value "<?xml version='1.0' encoding='utf-8'?><configuration><packageSources><clear /><add key='TempSource' value='$sourceLink' /></packageSources></configuration>"
|
||||
|
||||
try {
|
||||
# Extract BuildTools version from Directory.Packages.props to ensure we have the required version
|
||||
$dirPackagesProps = Join-Path $rootPath "Directory.Packages.props"
|
||||
if (Test-Path $dirPackagesProps) {
|
||||
$propsContent = Get-Content $dirPackagesProps -Raw
|
||||
if ($propsContent -match '<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="([^"]+)"') {
|
||||
$buildToolsVersion = $Matches[1]
|
||||
Write-Host "Downloading Microsoft.Windows.SDK.BuildTools version $buildToolsVersion..."
|
||||
$nugetArgsBuildTools = "install Microsoft.Windows.SDK.BuildTools -Version $buildToolsVersion -ConfigFile $tempConfig -OutputDirectory $installDir -NonInteractive -NoCache"
|
||||
Invoke-Expression "nuget $nugetArgsBuildTools" | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Download package to inspect nuspec and keep it for the build
|
||||
$nugetArgs = "install Microsoft.WindowsAppSDK -Version $WinAppSDKVersion -ConfigFile $tempConfig -OutputDirectory $installDir -NonInteractive -NoCache"
|
||||
Invoke-Expression "nuget $nugetArgs" | Out-Null
|
||||
|
||||
# Parse dependencies from the installed folders
|
||||
# Folder structure is typically {PackageId}.{Version}
|
||||
$directories = Get-ChildItem -Path $installDir -Directory
|
||||
$allLocalPackages = @()
|
||||
foreach ($dir in $directories) {
|
||||
# Match any package pattern: PackageId.Version
|
||||
if ($dir.Name -match "^(.+?)\.(\d+\..*)$") {
|
||||
$pkgId = $Matches[1]
|
||||
$pkgVer = $Matches[2]
|
||||
$allLocalPackages += $pkgId
|
||||
|
||||
$packageVersions[$pkgId] = $pkgVer
|
||||
Write-Host "Found dependency: $pkgId = $pkgVer"
|
||||
}
|
||||
}
|
||||
|
||||
# Update repo's nuget.config to use localpackages
|
||||
$nugetConfig = Join-Path $rootPath "nuget.config"
|
||||
$configData = Read-FileWithEncoding -Path $nugetConfig
|
||||
[xml]$xml = $configData.Content
|
||||
|
||||
Add-NuGetSourceAndMapping -Xml $xml -Key "localpackages" -Value $installDir -Patterns $allLocalPackages
|
||||
|
||||
$xml.Save($nugetConfig)
|
||||
Write-Host "Updated nuget.config with localpackages mapping."
|
||||
} catch {
|
||||
Write-Warning "Failed to resolve dependencies: $_"
|
||||
} finally {
|
||||
Remove-Item $tempConfig -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# Execute nuget list and capture the output
|
||||
if ($useExperimentalVersion) {
|
||||
# The nuget list for experimental versions will cost more time
|
||||
@@ -112,56 +213,36 @@ if ($latestVersion) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Update packages.config files
|
||||
Get-ChildItem -Path $rootPath -Recurse packages.config | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match 'package id="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = 'package id="Microsoft.WindowsAppSDK" version="' + $WinAppSDKVersion + '"'
|
||||
$oldVersionString = 'package id="Microsoft.WindowsAppSDK" version="[-.0-9a-zA-Z]*"'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
# Resolve dependencies for 1.8+
|
||||
$packageVersions = @{ "Microsoft.WindowsAppSDK" = $WinAppSDKVersion }
|
||||
|
||||
Resolve-WinAppSdkSplitDependencies
|
||||
|
||||
# Update Directory.Packages.props file
|
||||
Get-ChildItem -Path $rootPath -Recurse "Directory.Packages.props" | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match '<PackageVersion Include="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="' + $WinAppSDKVersion + '" />'
|
||||
$oldVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*" />'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
$isModified = $false
|
||||
|
||||
foreach ($pkgId in $packageVersions.Keys) {
|
||||
$ver = $packageVersions[$pkgId]
|
||||
# Escape dots in package ID for regex
|
||||
$pkgIdRegex = $pkgId -replace '\.', '\.'
|
||||
|
||||
$newVersionString = "<PackageVersion Include=""$pkgId"" Version=""$ver"" />"
|
||||
$oldVersionString = "<PackageVersion Include=""$pkgIdRegex"" Version=""[-.0-9a-zA-Z]*"" />"
|
||||
|
||||
if ($content -match "<PackageVersion Include=""$pkgIdRegex""") {
|
||||
# Update existing package
|
||||
if ($content -notmatch [regex]::Escape($newVersionString)) {
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
$isModified = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isModified) {
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
# Update .vcxproj files
|
||||
Get-ChildItem -Path $rootPath -Recurse *.vcxproj | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match '\\Microsoft.WindowsAppSDK.') {
|
||||
$newVersionString = '\Microsoft.WindowsAppSDK.' + $WinAppSDKVersion
|
||||
$oldVersionString = '\\Microsoft.WindowsAppSDK.(?=[-.0-9a-zA-Z]*\d)[-.0-9a-zA-Z]*' #positive lookahead for at least a digit
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
# Update .csproj files
|
||||
Get-ChildItem -Path $rootPath -Recurse *.csproj | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match 'PackageReference Include="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="'+ $WinAppSDKVersion + '"'
|
||||
$oldVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*"'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
Update-NugetConfig
|
||||
|
||||
@@ -19,7 +19,7 @@ parameters:
|
||||
- name: enableMsBuildCaching
|
||||
type: boolean
|
||||
displayName: "Enable MSBuild Caching"
|
||||
default: true
|
||||
default: false
|
||||
- name: runTests
|
||||
type: boolean
|
||||
displayName: "Run Tests"
|
||||
@@ -33,7 +33,7 @@ parameters:
|
||||
default: true
|
||||
- name: winAppSDKVersionNumber
|
||||
type: string
|
||||
default: 1.7
|
||||
default: 1.8
|
||||
- name: useExperimentalVersion
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
MSBuildMainBuildTargets: Build
|
||||
${{ insert }}: ${{ parameters.variables }}
|
||||
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
|
||||
RestoreAdditionalProjectSourcesArg: '/p:RestoreAdditionalProjectSources="$(Build.SourcesDirectory)\localpackages\NugetPackages"'
|
||||
RestoreAdditionalProjectSourcesArg: '/p:RestoreAdditionalProjectSources="$(Build.SourcesDirectory)\localpackages\NugetPackages" /p:IgnoreExperimentalWarnings=true'
|
||||
${{ else }}:
|
||||
RestoreAdditionalProjectSourcesArg: ''
|
||||
displayName: Build
|
||||
|
||||
@@ -19,48 +19,20 @@ steps:
|
||||
-useExperimentalVersion $${{ parameters.useExperimentalVersion }}
|
||||
-rootPath "$(build.sourcesdirectory)"
|
||||
|
||||
- script: echo $(WinAppSDKVersion)
|
||||
displayName: 'Display WinAppSDK Version Found'
|
||||
# - task: NuGetCommand@2
|
||||
# displayName: 'Restore NuGet packages (slnx)'
|
||||
# inputs:
|
||||
# command: 'restore'
|
||||
# feedsToUse: 'config'
|
||||
# nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
|
||||
# restoreSolution: '$(build.sourcesdirectory)\**\*.slnx'
|
||||
# includeNuGetOrg: false
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: 'Download WindowsAppSDK'
|
||||
inputs:
|
||||
buildType: 'specific'
|
||||
project: '55e8140e-57ac-4e5f-8f9c-c7c15b51929d'
|
||||
definition: '104083'
|
||||
buildVersionToDownload: 'latestFromBranch'
|
||||
branchName: 'refs/heads/release/${{ parameters.versionNumber }}-stable'
|
||||
artifactName: 'WindowsAppSDK_Nuget_And_MSIX'
|
||||
targetPath: '$(Build.SourcesDirectory)\localpackages'
|
||||
|
||||
- script: dir $(Build.SourcesDirectory)\localpackages\NugetPackages
|
||||
displayName: 'List downloaded packages'
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Install WindowsAppSDK'
|
||||
inputs:
|
||||
command: 'custom'
|
||||
arguments: >
|
||||
install "Microsoft.WindowsAppSDK"
|
||||
-Source "$(Build.SourcesDirectory)\localpackages\NugetPackages"
|
||||
-Version "$(WinAppSDKVersion)"
|
||||
-OutputDirectory "$(Build.SourcesDirectory)\localpackages\output"
|
||||
-FallbackSource "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Restore NuGet packages'
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Restore NuGet packages (dotnet)'
|
||||
inputs:
|
||||
command: 'restore'
|
||||
projects: '$(build.sourcesdirectory)\**\*.slnx'
|
||||
feedsToUse: 'config'
|
||||
nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
|
||||
restoreSolution: '$(build.sourcesdirectory)\**\*.sln'
|
||||
includeNuGetOrg: false
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Restore NuGet packages (slnx)'
|
||||
inputs:
|
||||
command: 'restore'
|
||||
feedsToUse: 'config'
|
||||
nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
|
||||
restoreSolution: '$(build.sourcesdirectory)\**\*.slnx'
|
||||
includeNuGetOrg: false
|
||||
workingDirectory: '$(build.sourcesdirectory)'
|
||||
|
||||
@@ -27,7 +27,8 @@ $versionExceptions = @(
|
||||
"WyHash.dll",
|
||||
"Microsoft.Recognizers.Text.DataTypes.TimexExpression.dll",
|
||||
"ObjectModelCsProjection.dll",
|
||||
"RendererCsProjection.dll") -join '|';
|
||||
"RendererCsProjection.dll",
|
||||
"Microsoft.ML.OnnxRuntime.dll") -join '|';
|
||||
$nullVersionExceptions = @(
|
||||
"SkiaSharp.Views.WinUI.Native.dll",
|
||||
"libSkiaSharp.dll",
|
||||
@@ -52,7 +53,12 @@ $nullVersionExceptions = @(
|
||||
"System.Diagnostics.EventLog.Messages.dll",
|
||||
"Microsoft.Windows.Widgets.dll",
|
||||
"AdaptiveCards.ObjectModel.WinUI3.dll",
|
||||
"AdaptiveCards.Rendering.WinUI3.dll") -join '|';
|
||||
"AdaptiveCards.Rendering.WinUI3.dll",
|
||||
"boost_regex_vc143_mt_gd_x32_1_87.dll",
|
||||
"boost_regex_vc143_mt_gd_x64_1_87.dll",
|
||||
"boost_regex_vc143_mt_x32_1_87.dll",
|
||||
"boost_regex_vc143_mt_x64_1_87.dll"
|
||||
) -join '|';
|
||||
$totalFailure = 0;
|
||||
|
||||
Write-Host $DirPath;
|
||||
|
||||
@@ -121,6 +121,9 @@ PowerToys Awake is a tool to keep your computer awake.
|
||||
|
||||
Randy contributed Registry Preview and some very early conversations about keyboard remapping.
|
||||
|
||||
### [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon
|
||||
Kayla was a former lead for PowerToys and helped create multiple utilities, maintained the GitHub repo, and collaborated with the community to improve the overall product
|
||||
|
||||
### [@oldnewthing](https://github.com/oldnewthing) - Raymond Chen
|
||||
|
||||
Find My Mouse is based on Raymond Chen's SuperSonar.
|
||||
@@ -180,7 +183,6 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter
|
||||
|
||||
## PowerToys core team
|
||||
|
||||
- [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon - Lead
|
||||
- [@craigloewen-msft](https://github.com/craigloewen-msft) - Craig Loewen - Product Manager
|
||||
- [@niels9001](https://github.com/niels9001/) - Niels Laute - Product Manager
|
||||
- [@dhowett](https://github.com/dhowett) - Dustin Howett - Dev Lead
|
||||
@@ -209,6 +211,7 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter
|
||||
## Former PowerToys core team members
|
||||
|
||||
- [@indierawk2k2](https://github.com/indierawk2k2) - Mike Harsh - Product Manager
|
||||
- [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon - Product Manager
|
||||
- [@ethanfangg](https://github.com/ethanfangg) - Ethan Fang - Product Manager
|
||||
- [@plante-msft](https://github.com/plante-msft) - Connor Plante - Product Manager
|
||||
- [@joadoumie](https://github.com/joadoumie) - Jordi Adoumie - Product Manager
|
||||
|
||||
@@ -42,6 +42,11 @@
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<!-- Make angle-bracket includes external and turn off code analysis for them -->
|
||||
<TreatAngleIncludeAsExternal>true</TreatAngleIncludeAsExternal>
|
||||
<ExternalWarningLevel>TurnOffAllWarnings</ExternalWarningLevel>
|
||||
<DisableAnalyzeExternal>true</DisableAnalyzeExternal>
|
||||
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Use</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
@@ -111,13 +116,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Debug/Release props -->
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'"
|
||||
Label="Configuration">
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'"
|
||||
Label="Configuration">
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
|
||||
@@ -262,6 +262,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Command Palette
|
||||
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
@@ -315,6 +316,14 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.CmdPalProcessStarted</td>
|
||||
<td>Triggered when the Command Palette process is started.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPal_ExtensionInvoked</td>
|
||||
<td>Tracks extension usage including extension ID, command details, success status, and execution time.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPal_SessionDuration</td>
|
||||
<td>Logs session metrics from launch to dismissal including duration, commands executed, pages visited, search queries, navigation depth, and errors.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Crop And Lock
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
<MSBuildCacheIgnoredInputPatterns>$(MSBuildCacheIgnoredInputPatterns);$(PackagesConfigFile)</MSBuildCacheIgnoredInputPatterns>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(MSBuildCacheEnabled)' == 'true' and '$(MSBuildCachePackageRoot)' == ''">
|
||||
<PropertyGroup Condition="'$(MSBuildCacheEnabled)' == 'true' AND '$(MSBuildCachePackageRoot)' == ''">
|
||||
<PackagesConfigContents>$([System.IO.File]::ReadAllText("$(PackagesConfigFile)"))</PackagesConfigContents>
|
||||
<MSBuildCachePackageVersion>$([System.Text.RegularExpressions.Regex]::Match($(PackagesConfigContents), 'Microsoft.MSBuildCache.*?version="(.*?)"').Groups[1].Value)</MSBuildCachePackageVersion>
|
||||
<MSBuildCachePackageRoot>$(MSBuildThisFileDirectory)packages\$(MSBuildCachePackageName).$(MSBuildCachePackageVersion)</MSBuildCachePackageRoot>
|
||||
|
||||
@@ -8,4 +8,24 @@
|
||||
<PropertyGroup Label="ManifestToolOverride">
|
||||
<ManifestTool Condition="Exists('$(WindowsSdkDir)bin\x64\mt.exe')">$(WindowsSdkDir)bin\x64\mt.exe</ManifestTool>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Auto-restore NuGet for native vcxproj (PackageReference) when building inside VS -->
|
||||
<Target Name="EnsureNuGetRestoreForVcxproj" BeforeTargets="PrepareForBuild" Condition="
|
||||
'$(BuildingInsideVisualStudio)' == 'true'
|
||||
and '$(DesignTimeBuild)' != 'true'
|
||||
and '$(RestoreInProgress)' != 'true'
|
||||
and '$(MSBuildProjectExtension)' == '.vcxproj'
|
||||
and '$(RestoreProjectStyle)' == 'PackageReference'
|
||||
and '$(MSBuildProjectExtensionsPath)' != ''
|
||||
and !Exists('$(MSBuildProjectExtensionsPath)project.assets.json')
|
||||
">
|
||||
|
||||
<Message Importance="normal" Text="NuGet assets missing for $(MSBuildProjectName); running Restore...; IntDir=$(IntDir); BaseIntermediateOutputPath=$(BaseIntermediateOutputPath)" />
|
||||
|
||||
<MSBuild Projects="$(MSBuildProjectFullPath)" Targets="Restore" Properties="RestoreInProgress=true" BuildInParallel="false" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup Condition="'$(IgnoreExperimentalWarnings)' == 'true'">
|
||||
<NoWarn>$(NoWarn);CS8305;SA1500;CA1852</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -7,6 +7,8 @@
|
||||
<PackageVersion Include="AdaptiveCards.ObjectModel.WinUI3" Version="2.0.0-beta" />
|
||||
<PackageVersion Include="AdaptiveCards.Rendering.WinUI3" Version="2.1.0-beta" />
|
||||
<PackageVersion Include="AdaptiveCards.Templating" Version="2.0.5" />
|
||||
<PackageVersion Include="boost" Version="1.87.0" TargetFramework="native" />
|
||||
<PackageVersion Include="boost_regex-vc143" Version="1.87.0" TargetFramework="native" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" Version="0.1.251101-build.2372" />
|
||||
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
|
||||
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
|
||||
@@ -38,6 +40,7 @@
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
|
||||
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
|
||||
<PackageVersion Include="Microsoft.Windows.CppWinRT" Version="2.0.240111.5" />
|
||||
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />
|
||||
@@ -69,10 +72,12 @@
|
||||
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.ImplementationLibrary" Version="1.0.231216.1"/>
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.37" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.250907003" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="1.8.251104000" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.39" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.251106002" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
|
||||
@@ -111,6 +116,7 @@
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Numerics.Tensors" Version="9.0.11" />
|
||||
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
||||
<PackageVersion Include="System.Reactive" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.10" />
|
||||
@@ -118,6 +124,7 @@
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageVersion Include="ToolGood.Words.Pinyin" Version="3.1.0.3" />
|
||||
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
|
||||
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||
|
||||
32
NOTICE.md
32
NOTICE.md
@@ -75,6 +75,37 @@ OTHER DEALINGS IN THE SOFTWARE.
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
```
|
||||
|
||||
### ToolGood.Words.Pinyin
|
||||
|
||||
We use the ToolGood.Words.Pinyin NuGet package for converting Chinese characters to pinyin.
|
||||
|
||||
**Source**: [https://github.com/toolgood/ToolGood.Words.Pinyin](https://github.com/toolgood/ToolGood.Words.Pinyin)
|
||||
|
||||
```
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 ToolGood
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
|
||||
## Utility: Command Palette Built-in Extensions
|
||||
|
||||
### Calculator
|
||||
@@ -1532,6 +1563,7 @@ SOFTWARE.
|
||||
- SkiaSharp.Views.WinUI
|
||||
- StreamJsonRpc
|
||||
- StyleCop.Analyzers
|
||||
- ToolGood.Words.Pinyin
|
||||
- UnicodeInformation
|
||||
- UnitsNet
|
||||
- UTF.Unknown
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
<Project Path="src/common/Telemetry/EtwTrace/EtwTrace.vcxproj" Id="8f021b46-362b-485c-bfba-ccf83e820cbd" />
|
||||
</Folder>
|
||||
<Folder Name="/common/utils/">
|
||||
<Project Path="src/common/utils/utils.vcxproj" Id="e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e" />
|
||||
<File Path="src/common/utils/appMutex.h" />
|
||||
<File Path="src/common/utils/color.h" />
|
||||
<File Path="src/common/utils/com_object_factory.h" />
|
||||
@@ -370,6 +371,10 @@
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/fancyzones/FancyZones/FancyZones.vcxproj" Id="ff1d7936-842a-4bbb-8bea-e9fe796de700" />
|
||||
<Project Path="src/modules/fancyzones/FancyZonesCLI/FancyZonesCLI.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/fancyzones/FancyZonesEditorCommon/FancyZonesEditorCommon.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
|
||||
34
doc/devdocs/commands.md
Normal file
34
doc/devdocs/commands.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Issue/PR commands
|
||||
|
||||
The PowerToys repository uses some special keywords to help manage issues and pull requests. Here is a list of the most important commands you can use in issue and PR descriptions or comments.
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/azp run` | Triggers the Azure Pipelines CI build for the current PR. Useful if you want to re-run the build without creating a new commit. |
|
||||
| `/bugreport` / `/reportbug` | Adds a comment with a manual for the Bug Report Tool, which helps users collect logs and system information for debugging purposes. It requests to upload this file and adds the `Needs-Author-Feedback` label. |
|
||||
| `/feedbackhub` | Adds a comment with a link to the Feedback Hub app on Windows, where users can submit feedback about PowerToys. Closes the issue and adds the `Resolution-Please File on Feedback Hub` label. |
|
||||
| `/dup #...` / `/duplicate #...` / `/dup https://...` / `/duplicate https://...` | Marks the current issue as a duplicate of another issue. It closes the current issue and applies the `Resolution-Duplicate` label. Replace `#...` with the issue number or a link to the issue. |
|
||||
| `/needinfo` | Adds the `Needs-Author-Feedback` label to the issue or PR, indicating that more information is needed from the author. |
|
||||
| `/helped` | Closes the issue and adds the `Resolution-Helped User` label. Furthermore a comment is added with a link to the PowerToys user documentation. |
|
||||
| `/loc` | Adds a comment informing the user that the issue was forwarded to the localization team and will soon be fixed. It adds the `Loc-Sent To Team` label. |
|
||||
|
||||
## Defining new commands
|
||||
|
||||
Most of these commands are using the [Microsoft GitHub Policy Service](https://github.com/apps/microsoft-github-policy-service) bot. Its commands are defined in the [PowerToys policy configuration file](/.github/policies/resourceManagement.yml).
|
||||
|
||||
## Other automated tasks
|
||||
|
||||
### Automatic labeling
|
||||
|
||||
The bot can automatically apply the correct `product-...` label for any opened issue.
|
||||
|
||||
> [!NOTE]
|
||||
> This feature is currently only available for the Workspaces module as a test.
|
||||
|
||||
### The `Needs-Author-Feedback` label
|
||||
|
||||
If an issue has this label and had no activity for 5 days, the bot will post a comment reminding the author to provide the needed information. It also adds the `Status-No recent activity` label. If no further activity occurs for another 5 days, the bot will close the issue.
|
||||
|
||||
### Filtering users that want to contribute
|
||||
|
||||
If a user utters their intention to contribute (e.g., by using the phrase "I want to contribute" in an issue or PR), the bot will add a comment with a link to the ["Would you like to contribute to PowerToys?" thread](https://github.com/microsoft/PowerToys/issues/28769).
|
||||
@@ -38,7 +38,7 @@ For C# modules, the settings are accessed through the `SettingsUtils` class in t
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
// Read settings
|
||||
var settings = SettingsUtils.GetSettings<ModuleSettings>("ModuleName");
|
||||
var settings = SettingsUtils.Default.GetSettings<ModuleSettings>("ModuleName");
|
||||
bool enabled = settings.Enabled;
|
||||
```
|
||||
|
||||
@@ -49,7 +49,7 @@ using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
// Write settings
|
||||
settings.Enabled = true;
|
||||
SettingsUtils.SaveSettings(settings.ToJsonString(), "ModuleName");
|
||||
SettingsUtils.Default.SaveSettings(settings.ToJsonString(), "ModuleName");
|
||||
```
|
||||
|
||||
## Settings Handling in Modules
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
# Localization
|
||||
|
||||
> **NOTE**: THIS DOCUMENT IS OUTDATED.
|
||||
> Follow [issue 15243](https://github.com/microsoft/PowerToys/issues/15243) for updates.
|
||||
|
||||
## Table of Contents
|
||||
1. [Localization on the pipeline (CDPX)](#localization-on-the-pipeline-cdpx)
|
||||
1. [UWP Special case](#uwp-special-case)
|
||||
2. [Enabling localization on a new project](#enabling-localization-on-a-new-project)
|
||||
1. [C++](#c)
|
||||
2. [C#](#c-1)
|
||||
3. [UWP](#uwp)
|
||||
3. [Lcl Files](#lcl-files)
|
||||
4. [Possible Issues in localization PRs (LEGO)](#possible-issues-in-localization-prs-lego)
|
||||
5. [Enabling localized MSI for a new project](#enabling-localized-msi-for-a-new-project)
|
||||
|
||||
## Localization on the pipeline (CDPX)
|
||||
[The localization step](https://github.com/microsoft/PowerToys/blob/86d77103e9c69686c297490acb04775d43ef8b76/.pipelines/pipeline.user.windows.yml#L45-L52) is run on the pipeline before the solution is built. This step runs the [build-localization](https://github.com/microsoft/PowerToys/blob/main/.pipelines/build-localization.cmd) script, which generates resx files for all the projects with localization enabled using the `Localization.XLoc` package.
|
||||
|
||||
The [`Localization.XLoc`](https://github.com/microsoft/PowerToys/blob/86d77103e9c69686c297490acb04775d43ef8b76/.pipelines/build-localization.cmd#L24-L25) tool is run on the repo root, and it checks for all occurrences of `LocProject.json`. Each localized project has a `LocProject.json` file in the project root, which contains the location of the English resx file, list of languages for localization, and the output path where the localized resx files are to be copied to. In addition to this, some other parameters can be set, such as whether the language ID should be added as a folder in the file path or in the file name. When the CDPX pipeline is run, the localization team is notified of changes in the English resx files. For each project with localization enabled, a `loc` folder (see [this](https://github.com/microsoft/PowerToys/tree/main/src/modules/launcher/Microsoft.Launcher/loc) for example) is created in the same directory as the `LocProject.json` file. The folder contains language specific folders which in turn have a nested folder path equivalent to `OutputPath` in the `LocProject.json`. Each of these folders contain one `lcl` file. The `lcl` files contain the English resources along with their translation for that language. These are described in more detail in the [Lcl files section](#lcl-files). Once the `.resx` files are generated, they will be used during the `Build PowerToys` step for localized versions of the modules.
|
||||
|
||||
Since the localization script requires certain nuget packages, the [`restore-localization`](https://github.com/microsoft/PowerToys/blob/main/.pipelines/restore-localization.cmd) script is run before running `build-localization` to install all the required packages. This script must [run in the `restore` step](https://github.com/microsoft/PowerToys/blob/86d77103e9c69686c297490acb04775d43ef8b76/.pipelines/pipeline.user.windows.yml#L37-L39) of pipeline because [the host is network isolated](https://onebranch.visualstudio.com/Pipeline/_wiki/wikis/Pipeline.wiki/2066/Consuming-Packages-in-a-CDPx-Pipeline?anchor=overview) at the `build` step. The [Toolset package source](https://github.com/microsoft/PowerToys/blob/86d77103e9c69686c297490acb04775d43ef8b76/.pipelines/pipeline.user.windows.yml#L23) is used for this.
|
||||
|
||||
The process and variables that can be tweaked on the pipeline are described in more detail on [onebranch (account required) under Localization](https://onebranch.visualstudio.com/Pipeline/_wiki/wikis/Pipeline.wiki/290/Localization).
|
||||
|
||||
The localized resource dlls for C# projects are added to the MSI only for build on the pipeline. This is done by checking if the [`IsPipeline` variable is defined](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/installer/PowerToysSetup/Product.wxs#L804-L805), which gets defined before [building the installer on the pipeline](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/.pipelines/build-installer.cmd#L4). This is done because the localized resx files are only present on the pipeline, and not having this check would result in the installer project failing to build locally.
|
||||
|
||||
## Enabling localization on a new project
|
||||
To enable localization on a new project, the first step is to create a file `LocProject.json` in the project root.
|
||||
|
||||
For example, for a project in the folder `src\path` where the resx file is present in `resources\Resources.resx`, the LocProject.json file will contain the following:
|
||||
```
|
||||
{
|
||||
"Projects": [
|
||||
{
|
||||
"LanguageSet": "Azure_Languages",
|
||||
"LocItems": [
|
||||
{
|
||||
"SourceFile": "src\\path\\resources\\Resources.resx",
|
||||
"CopyOption": "LangIDOnName",
|
||||
"OutputPath": "src\\path\\resources"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
The rest of the steps depend on the project type and are covered in the sections below. The steps to add the localized files to the MSI can be found in [Enabling localized MSI for a new project](#Enabling-localized-MSI-for-a-new-project).
|
||||
|
||||
### C++
|
||||
C++ projects do not support `resx` files, and instead use `rc` files along with `resource.h` files. The CDPX pipeline however doesn't support localizing `rc` files and the other alternative they support is directly translating the resources from the binary which makes it harder to maintain resources. To avoid this, a custom script has been added which expects a resx file and converts the entries to an rc file with a string table and adds resource declarations to a resource.h file so that the resources can be compiled with the C++ project.
|
||||
|
||||
If you already have a .rc file, copy the string table to a separate txt file and run the [convert-stringtable-to-resx.ps1](https://github.com/microsoft/PowerToys/blob/main/tools/build/convert-stringtable-to-resx.ps1) script on it. This script is not very robust to input, and requires the data in a specific format, where `IDS_ResName L"ResourceValue"` and any number of spaces can be present in between. The script converts this file to the format expected by [`resgen`](https://learn.microsoft.com/dotnet/framework/tools/resgen-exe-resource-file-generator#Convert), which will convert it to resx. The resource names are changed from all uppercase to title case, and the `IDS_` prefix is removed. Escape characters might have to be manually replaced, for example .rc files would have escaped double quotes as `""`, so this should be replaced with just `"` before converting to the resx files.
|
||||
|
||||
After generating the resx file, rename the existing rc and h files to ProjName.base.rc and resource.base.h. In the rc file remove the string table which is to be localized and in the .h file remove all `#define`s corresponding to localized resources. In the vcxproj of the C++ project, add the following build event:
|
||||
```
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h ProjName.base.rc ProjName.rc" />
|
||||
</Target>
|
||||
```
|
||||
|
||||
This event runs a script which generates a resource.h and ProjName.rc in the `Generated Files` folder using the strings in all the resx files along with the existing information in resource.base.h and ProjName.base.rc. The script is [convert-resx-to-rc.ps1](https://github.com/microsoft/PowerToys/blob/main/tools/build/convert-resx-to-rc.ps1). The script uses [`resgen`](https://learn.microsoft.com/dotnet/framework/tools/resgen-exe-resource-file-generator#Convert) to convert the resx file to a string table expected in the .rc file format. When the resources are added to the rc file the `IDS_` prefix is added and resource names are in upper case (as it was originally). Any occurrences of `"` in the string resource is escaped as `""` to prevent build errors. The string tables are added to the rc file in the following format:
|
||||
```
|
||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
|
||||
STRINGTABLE
|
||||
BEGIN
|
||||
strings
|
||||
END
|
||||
|
||||
#endif
|
||||
```
|
||||
Since there is no API to identify the `AFX_TARG_*`, `LANG_*` or `SUBLANG_*` values from each langId from the pipeline, these are hardcoded in the script (for each language) as done in [lines 50-77 of `convert-resx-to-rc.ps1`](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/tools/build/convert-resx-to-rc.ps1#L50-L77). **If any other languages are added in the future, this script will have to be updated.** In order to determine what are the language codes, you can open the rc file in Resource View, right click the string table and press `Insert Copy` and choose the corresponding language. This autogenerates the required code and can be used to figure out the language codes. The files also add the resource declarations to a resource.h file, starting from 101 by default(this can be changed by an optional argument). Since the output files will be generated in `Generated Files`, any includes in these two files will require an additional `..\` and wherever resource.h is used, it will have to be included as `Generated Files\resource.h`. While adding `resource.base.h` and `ProjName.base.rc` to the vcxproj, these should be modified to not participate in the build to avoid build errors:
|
||||
```
|
||||
<None Include="Resources.resx" />
|
||||
```
|
||||
|
||||
Some rc/resource.h files might be used in multiple projects (for example, KBM). To ensure the projects build for these cases, the build event can be added to the entire directory so that the rc files are generated before any project is built. See [Directory.Build.targets](https://github.com/microsoft/PowerToys/blob/main/src/modules/keyboardmanager/Directory.Build.targets) for an example.
|
||||
|
||||
Check [this PR](https://github.com/microsoft/PowerToys/pull/6104) for an example for making these changes for a C++ project.
|
||||
|
||||
### C#
|
||||
Since C# projects natively support `resx` files, the only step required here is to include all the resx files in the build. For .NET Core projects this is done automatically and the .csproj does not need to be modified. For other projects, the following line needs to be added:
|
||||
```
|
||||
<EmbeddedResource Include="Properties\Resources.*.resx" />
|
||||
```
|
||||
|
||||
**Note:** Building with localized resources may cause a build warning `Referenced assembly 'mscorlib.dll' targets a different processor` which is a VS bug. More details can be found in [PowerToys issue #7269](https://github.com/microsoft/PowerToys/issues/7269).
|
||||
|
||||
**Note:** If a project needs to be migrated from XAML resources to resx, the easiest way to convert the resources would be to change to format to `=` separates resources by either manually (by Ctrl+H on a text editor), or by a script, and then running [`resgen`](https://learn.microsoft.com/dotnet/framework/tools/resgen-exe-resource-file-generator#Convert) on `Developer Command Prompt for VS` to convert it to resx format.
|
||||
```
|
||||
<system:String x:Key="wox_plugin_calculator_plugin_name">Calculator</system:String>
|
||||
<system:String x:Key="wox_plugin_calculator_plugin_description">Allows to do mathematical calculations.(Try 5*3-2 in Wox)</system:String>
|
||||
<system:String x:Key="wox_plugin_calculator_not_a_number">Not a number (NaN)</system:String>
|
||||
```
|
||||
to
|
||||
```
|
||||
wox_plugin_calculator_plugin_name=Calculator
|
||||
wox_plugin_calculator_plugin_description=Allows to do mathematical calculations.(Try 5*3-2 in Wox)
|
||||
wox_plugin_calculator_not_a_number=Not a number (NaN)
|
||||
```
|
||||
After adding the resx file to the project along with the resource generator, references to the strings will have to be replaced with `Properties.Resources.resName` rather than the custom APIs. Check [this PR](https://github.com/microsoft/PowerToys/pull/6165) for an example of the changes required.
|
||||
|
||||
### UWP
|
||||
UWP projects expect `resw` files rather than `resx` (the format is almost the same). Unlike other C# projects, the files are expected in the format `fullLangId\Resources.resw`. To include these files in the build, replace the following line in the csproj:
|
||||
```
|
||||
<PRIResource Include="Strings\en-us\Resources.resw" />
|
||||
```
|
||||
to
|
||||
```
|
||||
<PRIResource Include="Strings\*\Resources.resw" />
|
||||
```
|
||||
|
||||
## Lcl Files
|
||||
Lcl files contain all the resources that are present in the English resx file, along with a translation if it has been added.
|
||||
|
||||
For example, an entry for a resource in the lcl file looks like this:
|
||||
```
|
||||
<Item ItemId=";EditKeyboard_WindowName" ItemType="0;.resx" PsrId="211" Leaf="true">
|
||||
<Str Cat="Text">
|
||||
<Val><![CDATA[Remap keys]]></Val>
|
||||
<Tgt Cat="Text" Stat="Loc" Orig="New">
|
||||
<Val><![CDATA[Remapper des touches]]></Val>
|
||||
</Tgt>
|
||||
</Str>
|
||||
<Disp Icon="Str" />
|
||||
</Item>
|
||||
```
|
||||
The `<Tgt>` element would not be present in the initial commits of the lcl files, as only the English version of the string would be present.
|
||||
|
||||
**Note:** The CDPX Localization system has a fail-safe check on the lcl files, where if the English string value which is present inside `<Val><![CDATA[*]]></Val>` does not match the value present in the English Resources.resx file then the translated value will not be copied to the localized resx file. This is present so that obsolete translations would not be loaded when the English resource has changed, and the English string will be used rather than the obsolete translation.
|
||||
|
||||
## Possible Issues in localization PRs (LEGO)
|
||||
Since the LEGO PRs update some of the strings in LCL files at a time, there can be multiple PRs which modify the same files, leading to merge conflicts. In most cases this would show up on GitHub as a merge conflict, but sometimes a bad git merge may occur, and the file could end up with incorrect formatting, such as two `<Tgt>` elements for a single resource. These can be fixed by ensuring the elements follow the format described in [this section](#lcl-files). To catch such errors, the build farm should be run for every LEGO PR and if any error occurs in the localization step, we should check the corresponding resx/lcl files for conflicts.
|
||||
|
||||
## Enabling localized MSI for a new project
|
||||
For C++ and UWP projects no additional files are generated with localization that need to be added to the MSI. For C++ projects all the resources are added to the dll/exe, while for UWP projects they are added to the `resources.pri` file (which is present even for an unlocalized project). To verify if the localized resources are added to the `resources.pri` file the following steps can be done:
|
||||
- Open `Developer Command Prompt for VS`
|
||||
- After navigating to the folder containing the pri file, run the following command:
|
||||
|
||||
makepri.exe dump /if .\resources.pri
|
||||
- Check the contents of the `resources.pri.xml` file that is generated from the command. The last section of the file will contain the resources with the strings in all the languages:
|
||||
```
|
||||
<NamedResource name="GeneralSettings_RunningAsAdminText" uri="ms-resource://f4f787a5-f0ae-47a9-be89-5408b1dd2b47/Resources/GeneralSettings_RunningAsAdminText">
|
||||
<Candidate qualifiers="Language-FR" type="String">
|
||||
<Value>Running as administrator</Value>
|
||||
</Candidate>
|
||||
<Candidate qualifiers="Language-EN-US" isDefault="true" type="String">
|
||||
<Value>Running as administrator</Value>
|
||||
</Candidate>
|
||||
</NamedResource>
|
||||
```
|
||||
|
||||
For C# projects, satellite dlls are generated when the project is built. For a project named `ProjName`, files are created in the format `langId\ProjName.resources.dll` where `langId` is in the same format as the lcl files. The satellite dlls need to be included with the MSI, but they must be added only if the solution is built from the build farm, as the localized resx files will not be present on local machines (and that could cause local builds of the installer to fail).
|
||||
This can be done by adding the directory name of the project to [Product.wxs near line 806](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/installer/PowerToysSetup/Product.wxs#L806) and a resource component for the project can be created in [Product.wxs near lines 845-847](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/installer/PowerToysSetup/Product.wxs#L845-L847) in this format:
|
||||
```
|
||||
<Component Id="ProjName_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)ProjNameInstallFolder">
|
||||
<File Id="ProjName_$(var.IdSafeLanguage)_File" Source="$(var.BinX64Dir)modules\ProjName\$(var.Language)\ProjName.resources.dll" />
|
||||
</Component>
|
||||
```
|
||||
|
||||
We should also ensure the new dlls are signed by the pipeline. Currently all dlls of the form [`*.resources.dll` are signed](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/.pipelines/pipeline.user.windows.yml#L68).
|
||||
|
||||
**Note:** The resource dlls should be added to the MSI project only after the initial commit with the lcl files has been done by the Localization team. Otherwise, the pipeline will fail as there wouldn't be any resx files to generate the dlls.
|
||||
@@ -38,6 +38,11 @@ Welcome to the PowerToys developer documentation. This documentation provides in
|
||||
- [Update Process](processes/update-process.md) - How PowerToys updates work
|
||||
- [GPO Implementation](processes/gpo.md) - Group Policy Objects implementation details
|
||||
|
||||
## Other Resources
|
||||
|
||||
- [aka.ms links](akaLinks.md) - List of short links
|
||||
- [Issue/PR commands](commands.md) - Special commands for managing issues and pull requests
|
||||
|
||||
## Fork, Clone, Branch and Create your PR
|
||||
|
||||
Once you've discussed your proposed feature/fix/etc. with a team member, and an approach or a spec has been written and approved, it's time to start development:
|
||||
|
||||
@@ -51,6 +51,7 @@ Contact the developers of a plugin directly for assistance with a specific plugi
|
||||
| [RandomGen](https://github.com/ruslanlap/PowerToysRun-RandomGen) | [ruslanlap](https://github.com/ruslanlap) | 🎲 Generate random data instantly with a single keystroke. Perfect for developers, testers, designers, and anyone who needs quick access to random data. Features include secure passwords, PINs, names, business data, dates, numbers, GUIDs, color codes, and more. Especially useful for designers who need random color codes and placeholder content. |
|
||||
| [Open With Cursor](https://github.com/VictorNoxx/PowerToys-Run-Cursor/) | [VictorNoxx](https://github.com/VictorNoxx) | Open Visual Studio, VS Code recents with Cursor AI |
|
||||
| [CheatSheets](https://github.com/ruslanlap/PowerToysRun-CheatSheets) | [ruslanlap](https://github.com/ruslanlap) | 📚 Find cheat sheets and command examples instantly from tldr pages, cheat.sh, and devhints.io. Features include favorites system, categories, offline mode, and smart caching. |
|
||||
| [QuickAI](https://github.com/ruslanlap/PowerToysRun-QuickAi) | [ruslanlap](https://github.com/ruslanlap) | AI-powered assistance with instant, smart responses from multiple providers (Groq, Together, Fireworks, OpenRouter, Cohere) |
|
||||
|
||||
## Extending software plugins
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
</Project>
|
||||
<Project Path="../src/common/Telemetry/EtwTrace/EtwTrace.vcxproj" Id="8f021b46-362b-485c-bfba-ccf83e820cbd" />
|
||||
<Project Path="../src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
|
||||
<Project Path="../src/common/utils/utils.vcxproj" Id="e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e" />
|
||||
<Project Path="../src/logging/logging.vcxproj" Id="7e1e3f13-2bd6-3f75-a6a7-873a2b55c60f">
|
||||
<Build Solution="Debug|ARM64" Project="false" />
|
||||
</Project>
|
||||
|
||||
@@ -162,6 +162,9 @@
|
||||
<ProjectReference Include="..\..\src\common\Telemetry\EtwTrace\EtwTrace.vcxproj">
|
||||
<Project>{8f021b46-362b-485c-bfba-ccf83e820cbd}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\src\common\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
|
||||
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallCmdPalPackage" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed AND WINDOWSBUILDNUMBER >= 22000" />
|
||||
<Custom Action="override Wix4CloseApplications_$(sys.BUILDARCHSHORT)" Before="RemoveFiles" />
|
||||
<Custom Action="RemovePowerToysSchTasks" After="RemoveFiles" />
|
||||
<!-- TODO: Use to activate embedded MSIX -->
|
||||
|
||||
@@ -47,6 +47,9 @@
|
||||
<ProjectReference Include="..\common\SettingsAPI\SettingsAPI.vcxproj">
|
||||
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\common\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="resource.h" />
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
<ProjectReference Include="..\common\updating\updating.vcxproj">
|
||||
<Project>{17da04df-e393-4397-9cf0-84dabe11032e}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\common\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="resource.h" />
|
||||
|
||||
@@ -113,6 +113,9 @@
|
||||
<ProjectReference Include="..\..\common\version\version.vcxproj">
|
||||
<Project>{cc6e41ac-8174-4e8a-8d22-85dd7f4851df}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\common\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
@@ -125,4 +128,4 @@
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -16,4 +16,5 @@ public static partial class CLSID
|
||||
public static readonly Guid CollatorDataSource = new Guid("9E175B8B-F52A-11D8-B9A5-505054503030");
|
||||
public static readonly Guid ApplicationActivationManager = new Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C");
|
||||
public static readonly Guid VirtualDesktopManager = new("aa509086-5ca9-4c25-8f95-589d3c07b48a");
|
||||
public static readonly Guid DesktopWallpaper = new("C2CF3110-460E-4FC1-B9D0-8A1C0C9CC4BD");
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@ public static partial class Ole32
|
||||
CLSCTX dwClsContext,
|
||||
ref Guid riid,
|
||||
out IntPtr rReturnedComObject);
|
||||
|
||||
[LibraryImport("ole32.dll")]
|
||||
internal static partial int CoInitializeEx(nint pvReserved, uint dwCoInit);
|
||||
|
||||
[LibraryImport("ole32.dll")]
|
||||
internal static partial void CoUninitialize();
|
||||
}
|
||||
|
||||
[Flags]
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<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')" />
|
||||
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<ProjectGuid>{6955446D-23F7-4023-9BB3-8657F904AF99}</ProjectGuid>
|
||||
@@ -50,6 +67,9 @@
|
||||
<ProjectReference Include="..\logger\logger.vcxproj">
|
||||
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Import Project="..\..\..\deps\spdlog.props" />
|
||||
@@ -65,4 +85,4 @@
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
399
src/common/UITestAutomation/ScreenRecording.cs
Normal file
399
src/common/UITestAutomation/ScreenRecording.cs
Normal file
@@ -0,0 +1,399 @@
|
||||
// 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.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods for recording the screen during UI tests.
|
||||
/// Requires FFmpeg to be installed and available in PATH.
|
||||
/// </summary>
|
||||
internal class ScreenRecording : IDisposable
|
||||
{
|
||||
private readonly string outputDirectory;
|
||||
private readonly string framesDirectory;
|
||||
private readonly string outputFilePath;
|
||||
private readonly List<string> capturedFrames;
|
||||
private readonly SemaphoreSlim recordingLock = new(1, 1);
|
||||
private readonly Stopwatch recordingStopwatch = new();
|
||||
private readonly string? ffmpegPath;
|
||||
private CancellationTokenSource? recordingCancellation;
|
||||
private Task? recordingTask;
|
||||
private bool isRecording;
|
||||
private int frameCount;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetDC(IntPtr hWnd);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetCursorInfo(out ScreenCapture.CURSORINFO pci);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool DrawIconEx(IntPtr hdc, int x, int y, IntPtr hIcon, int cx, int cy, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, int diFlags);
|
||||
|
||||
private const int CURSORSHOWING = 0x00000001;
|
||||
private const int DESKTOPHORZRES = 118;
|
||||
private const int DESKTOPVERTRES = 117;
|
||||
private const int DINORMAL = 0x0003;
|
||||
private const int TargetFps = 15; // 15 FPS for good balance of quality and size
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScreenRecording"/> class.
|
||||
/// </summary>
|
||||
/// <param name="outputDirectory">Directory where the recording will be saved.</param>
|
||||
public ScreenRecording(string outputDirectory)
|
||||
{
|
||||
this.outputDirectory = outputDirectory;
|
||||
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
framesDirectory = Path.Combine(outputDirectory, $"frames_{timestamp}");
|
||||
outputFilePath = Path.Combine(outputDirectory, $"recording_{timestamp}.mp4");
|
||||
capturedFrames = new List<string>();
|
||||
frameCount = 0;
|
||||
|
||||
// Check if FFmpeg is available
|
||||
ffmpegPath = FindFfmpeg();
|
||||
if (ffmpegPath == null)
|
||||
{
|
||||
Console.WriteLine("FFmpeg not found. Screen recording will be disabled.");
|
||||
Console.WriteLine("To enable video recording, install FFmpeg: https://ffmpeg.org/download.html");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether screen recording is available (FFmpeg found).
|
||||
/// </summary>
|
||||
public bool IsAvailable => ffmpegPath != null;
|
||||
|
||||
/// <summary>
|
||||
/// Starts recording the screen.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task StartRecordingAsync()
|
||||
{
|
||||
await recordingLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (isRecording || !IsAvailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Create frames directory
|
||||
Directory.CreateDirectory(framesDirectory);
|
||||
|
||||
recordingCancellation = new CancellationTokenSource();
|
||||
isRecording = true;
|
||||
recordingStopwatch.Start();
|
||||
|
||||
// Start the recording task
|
||||
recordingTask = Task.Run(() => RecordFrames(recordingCancellation.Token));
|
||||
|
||||
Console.WriteLine($"Started screen recording at {TargetFps} FPS");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to start recording: {ex.Message}");
|
||||
isRecording = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
recordingLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops recording and encodes video.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task StopRecordingAsync()
|
||||
{
|
||||
await recordingLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (!isRecording || recordingCancellation == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Signal cancellation
|
||||
recordingCancellation.Cancel();
|
||||
|
||||
// Wait for recording task to complete
|
||||
if (recordingTask != null)
|
||||
{
|
||||
await recordingTask;
|
||||
}
|
||||
|
||||
recordingStopwatch.Stop();
|
||||
isRecording = false;
|
||||
|
||||
double duration = recordingStopwatch.Elapsed.TotalSeconds;
|
||||
Console.WriteLine($"Recording stopped. Captured {capturedFrames.Count} frames in {duration:F2} seconds");
|
||||
|
||||
// Encode to video
|
||||
await EncodeToVideoAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error stopping recording: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Cleanup();
|
||||
recordingLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records frames from the screen.
|
||||
/// </summary>
|
||||
private void RecordFrames(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
int frameInterval = 1000 / TargetFps;
|
||||
var frameTimer = Stopwatch.StartNew();
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var frameStart = frameTimer.ElapsedMilliseconds;
|
||||
|
||||
try
|
||||
{
|
||||
CaptureFrame();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error capturing frame: {ex.Message}");
|
||||
}
|
||||
|
||||
// Sleep for remaining time to maintain target FPS
|
||||
var frameTime = frameTimer.ElapsedMilliseconds - frameStart;
|
||||
var sleepTime = Math.Max(0, frameInterval - (int)frameTime);
|
||||
|
||||
if (sleepTime > 0)
|
||||
{
|
||||
Thread.Sleep(sleepTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected when stopping
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error during recording: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures a single frame.
|
||||
/// </summary>
|
||||
private void CaptureFrame()
|
||||
{
|
||||
IntPtr hdc = GetDC(IntPtr.Zero);
|
||||
int screenWidth = GetDeviceCaps(hdc, DESKTOPHORZRES);
|
||||
int screenHeight = GetDeviceCaps(hdc, DESKTOPVERTRES);
|
||||
ReleaseDC(IntPtr.Zero, hdc);
|
||||
|
||||
Rectangle bounds = new Rectangle(0, 0, screenWidth, screenHeight);
|
||||
using (Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format24bppRgb))
|
||||
{
|
||||
using (Graphics g = Graphics.FromImage(bitmap))
|
||||
{
|
||||
g.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);
|
||||
|
||||
ScreenCapture.CURSORINFO cursorInfo;
|
||||
cursorInfo.CbSize = Marshal.SizeOf<ScreenCapture.CURSORINFO>();
|
||||
if (GetCursorInfo(out cursorInfo) && cursorInfo.Flags == CURSORSHOWING)
|
||||
{
|
||||
IntPtr hdcDest = g.GetHdc();
|
||||
DrawIconEx(hdcDest, cursorInfo.PTScreenPos.X, cursorInfo.PTScreenPos.Y, cursorInfo.HCursor, 0, 0, 0, IntPtr.Zero, DINORMAL);
|
||||
g.ReleaseHdc(hdcDest);
|
||||
}
|
||||
}
|
||||
|
||||
string framePath = Path.Combine(framesDirectory, $"frame_{frameCount:D6}.jpg");
|
||||
bitmap.Save(framePath, ImageFormat.Jpeg);
|
||||
capturedFrames.Add(framePath);
|
||||
frameCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes captured frames to video using ffmpeg.
|
||||
/// </summary>
|
||||
private async Task EncodeToVideoAsync()
|
||||
{
|
||||
if (capturedFrames.Count == 0)
|
||||
{
|
||||
Console.WriteLine("No frames captured");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Build ffmpeg command with proper non-interactive flags
|
||||
string inputPattern = Path.Combine(framesDirectory, "frame_%06d.jpg");
|
||||
|
||||
// -y: overwrite without asking
|
||||
// -nostdin: disable interaction
|
||||
// -loglevel error: only show errors
|
||||
// -stats: show encoding progress
|
||||
string args = $"-y -nostdin -loglevel error -stats -framerate {TargetFps} -i \"{inputPattern}\" -c:v libx264 -pix_fmt yuv420p -crf 23 \"{outputFilePath}\"";
|
||||
|
||||
Console.WriteLine($"Encoding {capturedFrames.Count} frames to video...");
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = ffmpegPath!,
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true, // Important: redirect stdin to prevent hanging
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process != null)
|
||||
{
|
||||
// Close stdin immediately to ensure FFmpeg doesn't wait for input
|
||||
process.StandardInput.Close();
|
||||
|
||||
// Read output streams asynchronously to prevent deadlock
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||
var errorTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
// Wait for process to exit
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
// Get the output
|
||||
string stdout = await outputTask;
|
||||
string stderr = await errorTask;
|
||||
|
||||
if (process.ExitCode == 0 && File.Exists(outputFilePath))
|
||||
{
|
||||
var fileInfo = new FileInfo(outputFilePath);
|
||||
Console.WriteLine($"Video created: {outputFilePath} ({fileInfo.Length / 1024 / 1024:F1} MB)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"FFmpeg encoding failed with exit code {process.ExitCode}");
|
||||
if (!string.IsNullOrWhiteSpace(stderr))
|
||||
{
|
||||
Console.WriteLine($"FFmpeg error: {stderr}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error encoding video: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds ffmpeg executable.
|
||||
/// </summary>
|
||||
private static string? FindFfmpeg()
|
||||
{
|
||||
// Check if ffmpeg is in PATH
|
||||
var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty<string>();
|
||||
|
||||
foreach (var dir in pathDirs)
|
||||
{
|
||||
var ffmpegPath = Path.Combine(dir, "ffmpeg.exe");
|
||||
if (File.Exists(ffmpegPath))
|
||||
{
|
||||
return ffmpegPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Check common installation locations
|
||||
var commonPaths = new[]
|
||||
{
|
||||
@"C:\.tools\ffmpeg\bin\ffmpeg.exe",
|
||||
@"C:\ffmpeg\bin\ffmpeg.exe",
|
||||
@"C:\Program Files\ffmpeg\bin\ffmpeg.exe",
|
||||
@"C:\Program Files (x86)\ffmpeg\bin\ffmpeg.exe",
|
||||
@$"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\WinGet\Links\ffmpeg.exe",
|
||||
};
|
||||
|
||||
foreach (var path in commonPaths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the recorded video file.
|
||||
/// </summary>
|
||||
public string OutputFilePath => outputFilePath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directory containing recordings.
|
||||
/// </summary>
|
||||
public string OutputDirectory => outputDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up resources.
|
||||
/// </summary>
|
||||
private void Cleanup()
|
||||
{
|
||||
recordingCancellation?.Dispose();
|
||||
recordingCancellation = null;
|
||||
recordingTask = null;
|
||||
|
||||
// Clean up frames directory if it exists
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(framesDirectory))
|
||||
{
|
||||
Directory.Delete(framesDirectory, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to cleanup frames directory: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (isRecording)
|
||||
{
|
||||
StopRecordingAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
Cleanup();
|
||||
recordingLock.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,9 +130,13 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// </summary>
|
||||
/// <param name="appPath">The path to the application executable.</param>
|
||||
/// <param name="args">Optional command line arguments to pass to the application.</param>
|
||||
public void StartExe(string appPath, string[]? args = null)
|
||||
public void StartExe(string appPath, string[]? args = null, string? enableModules = null)
|
||||
{
|
||||
var opts = new AppiumOptions();
|
||||
if (!string.IsNullOrEmpty(enableModules))
|
||||
{
|
||||
opts.AddAdditionalCapability("enableModules", enableModules);
|
||||
}
|
||||
|
||||
if (scope == PowerToysModule.PowerToysSettings)
|
||||
{
|
||||
@@ -169,27 +173,66 @@ namespace Microsoft.PowerToys.UITest
|
||||
|
||||
private void TryLaunchPowerToysSettings(AppiumOptions opts)
|
||||
{
|
||||
try
|
||||
if (opts.ToCapabilities().HasCapability("enableModules"))
|
||||
{
|
||||
var runnerProcessInfo = new ProcessStartInfo
|
||||
var modulesString = (string)opts.ToCapabilities().GetCapability("enableModules");
|
||||
var modulesArray = modulesString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
SettingsConfigHelper.ConfigureGlobalModuleSettings(modulesArray);
|
||||
}
|
||||
else
|
||||
{
|
||||
SettingsConfigHelper.ConfigureGlobalModuleSettings();
|
||||
}
|
||||
|
||||
const int maxTries = 3;
|
||||
const int delayMs = 5000;
|
||||
const int maxRetries = 3;
|
||||
|
||||
for (int tryCount = 1; tryCount <= maxTries; tryCount++)
|
||||
{
|
||||
try
|
||||
{
|
||||
FileName = locationPath + runnerPath,
|
||||
Verb = "runas",
|
||||
Arguments = "--open-settings",
|
||||
};
|
||||
var runnerProcessInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = locationPath + runnerPath,
|
||||
Verb = "runas",
|
||||
Arguments = "--open-settings",
|
||||
};
|
||||
|
||||
ExitExe(runnerProcessInfo.FileName);
|
||||
runner = Process.Start(runnerProcessInfo);
|
||||
ExitExe(runnerProcessInfo.FileName);
|
||||
|
||||
WaitForWindowAndSetCapability(opts, "PowerToys Settings", 5000, 5);
|
||||
// Verify process was killed
|
||||
string exeName = Path.GetFileNameWithoutExtension(runnerProcessInfo.FileName);
|
||||
var remainingProcesses = Process.GetProcessesByName(exeName);
|
||||
|
||||
// Exit CmdPal UI before launching new process if use installer for test
|
||||
ExitExeByName("Microsoft.CmdPal.UI");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to launch PowerToys Settings: {ex.Message}", ex);
|
||||
runner = Process.Start(runnerProcessInfo);
|
||||
|
||||
if (WaitForWindowAndSetCapability(opts, "PowerToys Settings", delayMs, maxRetries))
|
||||
{
|
||||
// Exit CmdPal UI before launching new process if use installer for test
|
||||
ExitExeByName("Microsoft.CmdPal.UI");
|
||||
return;
|
||||
}
|
||||
|
||||
// Window not found, kill all PowerToys processes and retry
|
||||
if (tryCount < maxTries)
|
||||
{
|
||||
KillPowerToysProcesses();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (tryCount == maxTries)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to launch PowerToys Settings after {maxTries} attempts: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
// Kill processes and retry
|
||||
KillPowerToysProcesses();
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Failed to launch PowerToys Settings: Window not found after {maxTries} attempts.");
|
||||
}
|
||||
|
||||
private void TryLaunchCommandPalette(AppiumOptions opts)
|
||||
@@ -211,7 +254,10 @@ namespace Microsoft.PowerToys.UITest
|
||||
var process = Process.Start(processStartInfo);
|
||||
process?.WaitForExit();
|
||||
|
||||
WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10);
|
||||
if (!WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10))
|
||||
{
|
||||
throw new TimeoutException("Failed to find Command Palette window after multiple attempts.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -219,7 +265,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
}
|
||||
|
||||
private void WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
|
||||
private bool WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
|
||||
{
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
@@ -230,18 +276,16 @@ namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
var hexHwnd = window[0].HWnd.ToString("x");
|
||||
opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries)
|
||||
{
|
||||
Thread.Sleep(delayMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TimeoutException($"Failed to find {windowName} window after multiple attempts.");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -292,17 +336,17 @@ namespace Microsoft.PowerToys.UITest
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle exceptions if needed
|
||||
Debug.WriteLine($"Exception during Cleanup: {ex.Message}");
|
||||
Console.WriteLine($"Exception during Cleanup: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restarts now exe and takes control of it.
|
||||
/// </summary>
|
||||
public void RestartScopeExe()
|
||||
public void RestartScopeExe(string? enableModules = null)
|
||||
{
|
||||
ExitScopeExe();
|
||||
StartExe(locationPath + sessionPath, this.commandLineArgs);
|
||||
StartExe(locationPath + sessionPath, commandLineArgs, enableModules);
|
||||
}
|
||||
|
||||
public WindowsDriver<WindowsElement> GetRoot()
|
||||
@@ -327,5 +371,31 @@ namespace Microsoft.PowerToys.UITest
|
||||
this.ExitExe(winAppDriverProcessInfo.FileName);
|
||||
SessionHelper.appDriver = Process.Start(winAppDriverProcessInfo);
|
||||
}
|
||||
|
||||
private void KillPowerToysProcesses()
|
||||
{
|
||||
var powerToysProcessNames = new[] { "PowerToys", "Microsoft.CmdPal.UI" };
|
||||
|
||||
foreach (var processName in powerToysProcessNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
var processes = Process.GetProcessesByName(processName);
|
||||
|
||||
foreach (var process in processes)
|
||||
{
|
||||
process.Kill();
|
||||
process.WaitForExit();
|
||||
}
|
||||
|
||||
// Verify processes are actually gone
|
||||
var remainingProcesses = Process.GetProcessesByName(processName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[KillPowerToysProcesses] Failed to kill process {processName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,19 +21,18 @@ namespace Microsoft.PowerToys.UITest
|
||||
public class SettingsConfigHelper
|
||||
{
|
||||
private static readonly JsonSerializerOptions IndentedJsonOptions = new() { WriteIndented = true };
|
||||
private static readonly SettingsUtils SettingsUtils = new SettingsUtils();
|
||||
private static readonly SettingsUtils SettingsUtils = SettingsUtils.Default;
|
||||
|
||||
/// <summary>
|
||||
/// Configures global PowerToys settings to enable only specified modules and disable all others.
|
||||
/// </summary>
|
||||
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when modulesToEnable is null.</exception>
|
||||
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled. If null or empty, all modules will be disabled.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when settings file operations fail.</exception>
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")]
|
||||
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")]
|
||||
public static void ConfigureGlobalModuleSettings(params string[] modulesToEnable)
|
||||
public static void ConfigureGlobalModuleSettings(params string[]? modulesToEnable)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(modulesToEnable);
|
||||
modulesToEnable ??= Array.Empty<string>();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace Microsoft.PowerToys.UITest
|
||||
|
||||
public string? ScreenshotDirectory { get; set; }
|
||||
|
||||
public string? RecordingDirectory { get; set; }
|
||||
|
||||
public static MonitorInfoData.ParamsWrapper MonitorInfoData { get; set; } = new MonitorInfoData.ParamsWrapper() { Monitors = new List<MonitorInfoData.MonitorInfoDataWrapper>() };
|
||||
|
||||
private readonly PowerToysModule scope;
|
||||
@@ -36,6 +38,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
private readonly string[]? commandLineArgs;
|
||||
private SessionHelper? sessionHelper;
|
||||
private System.Threading.Timer? screenshotTimer;
|
||||
private ScreenRecording? screenRecording;
|
||||
|
||||
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified, string[]? commandLineArgs = null)
|
||||
{
|
||||
@@ -65,12 +68,35 @@ namespace Microsoft.PowerToys.UITest
|
||||
CloseOtherApplications();
|
||||
if (IsInPipeline)
|
||||
{
|
||||
ScreenshotDirectory = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "UITestScreenshots_" + Guid.NewGuid().ToString());
|
||||
string baseDirectory = this.TestContext.TestResultsDirectory ?? string.Empty;
|
||||
ScreenshotDirectory = Path.Combine(baseDirectory, "UITestScreenshots_" + Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(ScreenshotDirectory);
|
||||
|
||||
RecordingDirectory = Path.Combine(baseDirectory, "UITestRecordings_" + Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(RecordingDirectory);
|
||||
|
||||
// Take screenshot every 1 second
|
||||
screenshotTimer = new System.Threading.Timer(ScreenCapture.TimerCallback, ScreenshotDirectory, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000));
|
||||
|
||||
// Start screen recording (requires FFmpeg)
|
||||
try
|
||||
{
|
||||
screenRecording = new ScreenRecording(RecordingDirectory);
|
||||
if (screenRecording.IsAvailable)
|
||||
{
|
||||
_ = screenRecording.StartRecordingAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
screenRecording = null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to start screen recording: {ex.Message}");
|
||||
screenRecording = null;
|
||||
}
|
||||
|
||||
// Escape Popups before starting
|
||||
System.Windows.Forms.SendKeys.SendWait("{ESC}");
|
||||
}
|
||||
@@ -88,15 +114,36 @@ namespace Microsoft.PowerToys.UITest
|
||||
if (IsInPipeline)
|
||||
{
|
||||
screenshotTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
Dispose();
|
||||
|
||||
// Stop screen recording
|
||||
if (screenRecording != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
screenRecording.StopRecordingAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to stop screen recording: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (TestContext.CurrentTestOutcome is UnitTestOutcome.Failed
|
||||
or UnitTestOutcome.Error
|
||||
or UnitTestOutcome.Unknown)
|
||||
{
|
||||
Task.Delay(1000).Wait();
|
||||
AddScreenShotsToTestResultsDirectory();
|
||||
AddRecordingsToTestResultsDirectory();
|
||||
AddLogFilesToTestResultsDirectory();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clean up recording if test passed
|
||||
CleanupRecordingDirectory();
|
||||
}
|
||||
|
||||
Dispose();
|
||||
}
|
||||
|
||||
this.Session.Cleanup();
|
||||
@@ -106,6 +153,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
public void Dispose()
|
||||
{
|
||||
screenshotTimer?.Dispose();
|
||||
screenRecording?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
@@ -600,6 +648,47 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds screen recordings to test results directory when test fails.
|
||||
/// </summary>
|
||||
protected void AddRecordingsToTestResultsDirectory()
|
||||
{
|
||||
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
|
||||
{
|
||||
// Add video files (MP4)
|
||||
var videoFiles = Directory.GetFiles(RecordingDirectory, "*.mp4");
|
||||
foreach (string file in videoFiles)
|
||||
{
|
||||
this.TestContext.AddResultFile(file);
|
||||
var fileInfo = new FileInfo(file);
|
||||
Console.WriteLine($"Added video recording: {Path.GetFileName(file)} ({fileInfo.Length / 1024 / 1024:F1} MB)");
|
||||
}
|
||||
|
||||
if (videoFiles.Length == 0)
|
||||
{
|
||||
Console.WriteLine("No video recording available (FFmpeg not found). Screenshots are still captured.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up recording directory when test passes.
|
||||
/// </summary>
|
||||
private void CleanupRecordingDirectory()
|
||||
{
|
||||
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(RecordingDirectory, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to cleanup recording directory: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies PowerToys log files to test results directory when test fails.
|
||||
/// Renames files to include the directory structure after \PowerToys.
|
||||
@@ -689,11 +778,11 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// <summary>
|
||||
/// Restart scope exe.
|
||||
/// </summary>
|
||||
public void RestartScopeExe()
|
||||
public Session RestartScopeExe(string? enableModules = null)
|
||||
{
|
||||
this.sessionHelper!.RestartScopeExe();
|
||||
this.sessionHelper!.RestartScopeExe(enableModules);
|
||||
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), this.scope, this.size);
|
||||
return;
|
||||
return Session;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -168,6 +168,9 @@
|
||||
<ProjectReference Include="..\version\version.vcxproj">
|
||||
<Project>{cc6e41ac-8174-4e8a-8d22-85dd7f4851df}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
|
||||
62
src/common/utils/EventLocker.cpp
Normal file
62
src/common/utils/EventLocker.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#include "pch.h"
|
||||
#include "EventLocker.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
EventLocker::EventLocker(HANDLE handle) :
|
||||
eventHandle(handle)
|
||||
{
|
||||
if (eventHandle)
|
||||
{
|
||||
SetEvent(eventHandle);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<EventLocker> EventLocker::Get(std::wstring eventName)
|
||||
{
|
||||
EventLocker locker(std::move(eventName));
|
||||
if (!locker.eventHandle)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return std::optional<EventLocker>(std::move(locker));
|
||||
}
|
||||
|
||||
EventLocker::EventLocker(EventLocker&& other) noexcept :
|
||||
eventHandle(other.eventHandle)
|
||||
{
|
||||
other.eventHandle = nullptr;
|
||||
}
|
||||
|
||||
EventLocker& EventLocker::operator=(EventLocker&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
eventHandle = other.eventHandle;
|
||||
other.eventHandle = nullptr;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
EventLocker::~EventLocker()
|
||||
{
|
||||
if (eventHandle)
|
||||
{
|
||||
ResetEvent(eventHandle);
|
||||
CloseHandle(eventHandle);
|
||||
eventHandle = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
EventLocker::EventLocker(std::wstring eventName)
|
||||
{
|
||||
eventHandle = CreateEvent(nullptr, true, false, eventName.c_str());
|
||||
if (!eventHandle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetEvent(eventHandle);
|
||||
}
|
||||
@@ -1,61 +1,26 @@
|
||||
#include <windows.h>
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <windows.h>
|
||||
|
||||
class EventLocker
|
||||
{
|
||||
public:
|
||||
EventLocker(HANDLE h)
|
||||
{
|
||||
eventHandle = h;
|
||||
SetEvent(eventHandle);
|
||||
}
|
||||
explicit EventLocker(HANDLE handle);
|
||||
|
||||
static std::optional<EventLocker> Get(std::wstring eventName)
|
||||
{
|
||||
EventLocker locker(eventName);
|
||||
if (!locker.eventHandle)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
static std::optional<EventLocker> Get(std::wstring eventName);
|
||||
|
||||
return locker;
|
||||
}
|
||||
EventLocker(EventLocker&) = delete;
|
||||
EventLocker& operator=(EventLocker&) = delete;
|
||||
|
||||
EventLocker(EventLocker& e) = delete;
|
||||
EventLocker& operator=(EventLocker& e) = delete;
|
||||
EventLocker(EventLocker&& other) noexcept;
|
||||
EventLocker& operator=(EventLocker&& other) noexcept;
|
||||
|
||||
EventLocker(EventLocker&& e) noexcept
|
||||
{
|
||||
this->eventHandle = e.eventHandle;
|
||||
e.eventHandle = nullptr;
|
||||
}
|
||||
|
||||
EventLocker& operator=(EventLocker&& e) noexcept
|
||||
{
|
||||
this->eventHandle = e.eventHandle;
|
||||
e.eventHandle = nullptr;
|
||||
}
|
||||
~EventLocker();
|
||||
|
||||
~EventLocker()
|
||||
{
|
||||
if (eventHandle)
|
||||
{
|
||||
ResetEvent(eventHandle);
|
||||
CloseHandle(eventHandle);
|
||||
eventHandle = nullptr;
|
||||
}
|
||||
}
|
||||
private:
|
||||
EventLocker(std::wstring eventName)
|
||||
{
|
||||
eventHandle = CreateEvent(nullptr, true, false, eventName.c_str());
|
||||
if (!eventHandle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
explicit EventLocker(std::wstring eventName);
|
||||
|
||||
SetEvent(eventHandle);
|
||||
}
|
||||
|
||||
HANDLE eventHandle;
|
||||
HANDLE eventHandle = nullptr;
|
||||
};
|
||||
|
||||
71
src/common/utils/EventWaiter.cpp
Normal file
71
src/common/utils/EventWaiter.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
#include "pch.h"
|
||||
#include "EventWaiter.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
EventWaiter::EventWaiter(const std::wstring& name, std::function<void(DWORD)> callback)
|
||||
{
|
||||
// Create localExitThreadEvent and localWaitingEvent for capturing. We cannot capture 'this' as we implement move constructor.
|
||||
const auto localExitThreadEvent = exitThreadEvent = CreateEvent(nullptr, false, false, nullptr);
|
||||
const HANDLE localWaitingEvent = waitingEvent = CreateEvent(nullptr, false, false, name.c_str());
|
||||
|
||||
std::thread([=]() {
|
||||
HANDLE events[2] = { localWaitingEvent, localExitThreadEvent };
|
||||
while (true)
|
||||
{
|
||||
const auto waitResult = WaitForMultipleObjects(2, events, false, INFINITE);
|
||||
if (waitResult == WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (waitResult == WAIT_FAILED)
|
||||
{
|
||||
callback(GetLastError());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (waitResult == WAIT_OBJECT_0)
|
||||
{
|
||||
callback(ERROR_SUCCESS);
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
EventWaiter::EventWaiter(EventWaiter&& other) noexcept :
|
||||
exitThreadEvent(other.exitThreadEvent),
|
||||
waitingEvent(other.waitingEvent)
|
||||
{
|
||||
other.exitThreadEvent = nullptr;
|
||||
other.waitingEvent = nullptr;
|
||||
}
|
||||
|
||||
EventWaiter& EventWaiter::operator=(EventWaiter&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
exitThreadEvent = other.exitThreadEvent;
|
||||
waitingEvent = other.waitingEvent;
|
||||
other.exitThreadEvent = nullptr;
|
||||
other.waitingEvent = nullptr;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
EventWaiter::~EventWaiter()
|
||||
{
|
||||
if (exitThreadEvent)
|
||||
{
|
||||
SetEvent(exitThreadEvent);
|
||||
CloseHandle(exitThreadEvent);
|
||||
exitThreadEvent = nullptr;
|
||||
}
|
||||
|
||||
if (waitingEvent)
|
||||
{
|
||||
CloseHandle(waitingEvent);
|
||||
waitingEvent = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -8,73 +8,15 @@
|
||||
class EventWaiter
|
||||
{
|
||||
public:
|
||||
EventWaiter() {}
|
||||
EventWaiter(const std::wstring& name, std::function<void(DWORD)> callback)
|
||||
{
|
||||
// Create localExitThreadEvent and localWaitingEvent for capturing. We cannot capture 'this' as we implement move constructor.
|
||||
auto localExitThreadEvent = exitThreadEvent = CreateEvent(nullptr, false, false, nullptr);
|
||||
HANDLE localWaitingEvent = waitingEvent = CreateEvent(nullptr, false, false, name.c_str());
|
||||
std::thread([=]() {
|
||||
HANDLE events[2] = { localWaitingEvent, localExitThreadEvent };
|
||||
while (true)
|
||||
{
|
||||
auto waitResult = WaitForMultipleObjects(2, events, false, INFINITE);
|
||||
if (waitResult == WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (waitResult == WAIT_FAILED)
|
||||
{
|
||||
callback(GetLastError());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (waitResult == WAIT_OBJECT_0)
|
||||
{
|
||||
callback(ERROR_SUCCESS);
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
EventWaiter() = default;
|
||||
EventWaiter(const std::wstring& name, std::function<void(DWORD)> callback);
|
||||
EventWaiter(EventWaiter&) = delete;
|
||||
EventWaiter& operator=(EventWaiter&) = delete;
|
||||
|
||||
EventWaiter(EventWaiter&& a) noexcept
|
||||
{
|
||||
this->exitThreadEvent = a.exitThreadEvent;
|
||||
this->waitingEvent = a.waitingEvent;
|
||||
|
||||
a.exitThreadEvent = nullptr;
|
||||
a.waitingEvent = nullptr;
|
||||
}
|
||||
|
||||
EventWaiter& operator=(EventWaiter&& a) noexcept
|
||||
{
|
||||
this->exitThreadEvent = a.exitThreadEvent;
|
||||
this->waitingEvent = a.waitingEvent;
|
||||
|
||||
a.exitThreadEvent = nullptr;
|
||||
a.waitingEvent = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
~EventWaiter()
|
||||
{
|
||||
if (exitThreadEvent)
|
||||
{
|
||||
SetEvent(exitThreadEvent);
|
||||
CloseHandle(exitThreadEvent);
|
||||
}
|
||||
|
||||
if (waitingEvent)
|
||||
{
|
||||
CloseHandle(waitingEvent);
|
||||
}
|
||||
}
|
||||
EventWaiter(EventWaiter&& other) noexcept;
|
||||
EventWaiter& operator=(EventWaiter&& other) noexcept;
|
||||
~EventWaiter();
|
||||
|
||||
private:
|
||||
HANDLE exitThreadEvent = nullptr;
|
||||
HANDLE waitingEvent = nullptr;
|
||||
};
|
||||
};
|
||||
|
||||
60
src/common/utils/HDropIterator.cpp
Normal file
60
src/common/utils/HDropIterator.cpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#include "pch.h"
|
||||
#include "HDropIterator.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
HDropIterator::HDropIterator(IDataObject* dataObject)
|
||||
{
|
||||
FORMATETC formatetc{
|
||||
CF_HDROP,
|
||||
nullptr,
|
||||
DVASPECT_CONTENT,
|
||||
-1,
|
||||
TYMED_HGLOBAL
|
||||
};
|
||||
|
||||
if (dataObject && SUCCEEDED(dataObject->GetData(&formatetc, &m_medium)))
|
||||
{
|
||||
_listCount = DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), 0xFFFFFFFF, nullptr, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_medium = {};
|
||||
}
|
||||
}
|
||||
|
||||
HDropIterator::~HDropIterator()
|
||||
{
|
||||
if (m_medium.tymed)
|
||||
{
|
||||
ReleaseStgMedium(&m_medium);
|
||||
}
|
||||
}
|
||||
|
||||
void HDropIterator::First()
|
||||
{
|
||||
_current = 0;
|
||||
}
|
||||
|
||||
void HDropIterator::Next()
|
||||
{
|
||||
++_current;
|
||||
}
|
||||
|
||||
bool HDropIterator::IsDone() const
|
||||
{
|
||||
return _current >= _listCount;
|
||||
}
|
||||
|
||||
LPTSTR HDropIterator::CurrentItem() const
|
||||
{
|
||||
const UINT cch = DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), _current, nullptr, 0) + 1;
|
||||
LPTSTR path = static_cast<LPTSTR>(malloc(sizeof(TCHAR) * cch));
|
||||
if (!path)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), _current, path, cch);
|
||||
return path;
|
||||
}
|
||||
@@ -1,67 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <objidl.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
class HDropIterator
|
||||
{
|
||||
public:
|
||||
HDropIterator(IDataObject* pDataObject)
|
||||
{
|
||||
_current = 0;
|
||||
_listCount = 0;
|
||||
explicit HDropIterator(IDataObject* dataObject);
|
||||
~HDropIterator();
|
||||
|
||||
FORMATETC formatetc = {
|
||||
CF_HDROP,
|
||||
NULL,
|
||||
DVASPECT_CONTENT,
|
||||
-1,
|
||||
TYMED_HGLOBAL
|
||||
};
|
||||
|
||||
if (SUCCEEDED(pDataObject->GetData(&formatetc, &m_medium)))
|
||||
{
|
||||
_listCount = DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), 0xFFFFFFFF, NULL, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_medium = {};
|
||||
}
|
||||
}
|
||||
|
||||
~HDropIterator()
|
||||
{
|
||||
if (m_medium.tymed)
|
||||
{
|
||||
ReleaseStgMedium(&m_medium);
|
||||
}
|
||||
}
|
||||
|
||||
void First()
|
||||
{
|
||||
_current = 0;
|
||||
}
|
||||
|
||||
void Next()
|
||||
{
|
||||
_current++;
|
||||
}
|
||||
|
||||
bool IsDone() const
|
||||
{
|
||||
return _current >= _listCount;
|
||||
}
|
||||
|
||||
LPTSTR CurrentItem() const
|
||||
{
|
||||
UINT cch = DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), _current, NULL, 0) + 1;
|
||||
LPTSTR pszPath = static_cast<LPTSTR>(malloc(sizeof(TCHAR) * cch));
|
||||
|
||||
DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), _current, pszPath, cch);
|
||||
|
||||
return pszPath;
|
||||
}
|
||||
void First();
|
||||
void Next();
|
||||
[[nodiscard]] bool IsDone() const;
|
||||
[[nodiscard]] LPTSTR CurrentItem() const;
|
||||
|
||||
private:
|
||||
UINT _listCount;
|
||||
STGMEDIUM m_medium;
|
||||
UINT _current;
|
||||
UINT _listCount = 0;
|
||||
STGMEDIUM m_medium{};
|
||||
UINT _current = 0;
|
||||
};
|
||||
|
||||
73
src/common/utils/HttpClient.cpp
Normal file
73
src/common/utils/HttpClient.cpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#include "pch.h"
|
||||
#include "HttpClient.h"
|
||||
|
||||
namespace http
|
||||
{
|
||||
HttpClient::HttpClient()
|
||||
{
|
||||
auto headers = m_client.DefaultRequestHeaders();
|
||||
headers.UserAgent().TryParseAdd(USER_AGENT);
|
||||
}
|
||||
|
||||
std::future<std::wstring> HttpClient::request(const winrt::Windows::Foundation::Uri& url)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url);
|
||||
(void)response.EnsureSuccessStatusCode();
|
||||
auto body = co_await response.Content().ReadAsStringAsync();
|
||||
co_return std::wstring(body);
|
||||
}
|
||||
|
||||
std::future<void> HttpClient::download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url);
|
||||
(void)response.EnsureSuccessStatusCode();
|
||||
auto fileStream = co_await storage::Streams::FileRandomAccessStream::OpenAsync(
|
||||
dstFilePath.c_str(),
|
||||
storage::FileAccessMode::ReadWrite,
|
||||
storage::StorageOpenOptions::AllowReadersAndWriters,
|
||||
storage::Streams::FileOpenDisposition::CreateAlways);
|
||||
co_await response.Content().WriteToStreamAsync(fileStream);
|
||||
fileStream.Close();
|
||||
}
|
||||
|
||||
std::future<void> HttpClient::download(const winrt::Windows::Foundation::Uri& url,
|
||||
const std::wstring& dstFilePath,
|
||||
const std::function<void(float)>& progressUpdateCallback)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url, winrt::Windows::Web::Http::HttpCompletionOption::ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
const uint64_t totalBytes = response.Content().Headers().ContentLength().GetUInt64();
|
||||
auto contentStream = co_await response.Content().ReadAsInputStreamAsync();
|
||||
|
||||
uint64_t totalBytesRead = 0;
|
||||
storage::Streams::Buffer buffer(8192);
|
||||
auto fileStream = co_await storage::Streams::FileRandomAccessStream::OpenAsync(
|
||||
dstFilePath.c_str(),
|
||||
storage::FileAccessMode::ReadWrite,
|
||||
storage::StorageOpenOptions::AllowReadersAndWriters,
|
||||
storage::Streams::FileOpenDisposition::CreateAlways);
|
||||
|
||||
co_await contentStream.ReadAsync(buffer, buffer.Capacity(), storage::Streams::InputStreamOptions::None);
|
||||
while (buffer.Length() > 0)
|
||||
{
|
||||
co_await fileStream.WriteAsync(buffer);
|
||||
totalBytesRead += buffer.Length();
|
||||
if (progressUpdateCallback)
|
||||
{
|
||||
const float percentage = static_cast<float>(totalBytesRead) / totalBytes;
|
||||
progressUpdateCallback(percentage);
|
||||
}
|
||||
|
||||
co_await contentStream.ReadAsync(buffer, buffer.Capacity(), storage::Streams::InputStreamOptions::None);
|
||||
}
|
||||
|
||||
if (progressUpdateCallback)
|
||||
{
|
||||
progressUpdateCallback(1);
|
||||
}
|
||||
|
||||
fileStream.Close();
|
||||
contentStream.Close();
|
||||
}
|
||||
}
|
||||
@@ -15,63 +15,13 @@ namespace http
|
||||
class HttpClient
|
||||
{
|
||||
public:
|
||||
HttpClient()
|
||||
{
|
||||
auto headers = m_client.DefaultRequestHeaders();
|
||||
headers.UserAgent().TryParseAdd(USER_AGENT);
|
||||
}
|
||||
HttpClient();
|
||||
|
||||
std::future<std::wstring> request(const winrt::Windows::Foundation::Uri& url)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url);
|
||||
(void)response.EnsureSuccessStatusCode();
|
||||
auto body = co_await response.Content().ReadAsStringAsync();
|
||||
co_return std::wstring(body);
|
||||
}
|
||||
|
||||
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url);
|
||||
(void)response.EnsureSuccessStatusCode();
|
||||
auto file_stream = co_await storage::Streams::FileRandomAccessStream::OpenAsync(dstFilePath.c_str(), storage::FileAccessMode::ReadWrite, storage::StorageOpenOptions::AllowReadersAndWriters, storage::Streams::FileOpenDisposition::CreateAlways);
|
||||
co_await response.Content().WriteToStreamAsync(file_stream);
|
||||
file_stream.Close();
|
||||
}
|
||||
|
||||
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath, const std::function<void(float)>& progressUpdateCallback)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url, HttpCompletionOption::ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
uint64_t totalBytes = response.Content().Headers().ContentLength().GetUInt64();
|
||||
auto contentStream = co_await response.Content().ReadAsInputStreamAsync();
|
||||
|
||||
uint64_t totalBytesRead = 0;
|
||||
storage::Streams::Buffer buffer(8192);
|
||||
auto fileStream = co_await storage::Streams::FileRandomAccessStream::OpenAsync(dstFilePath.c_str(), storage::FileAccessMode::ReadWrite, storage::StorageOpenOptions::AllowReadersAndWriters, storage::Streams::FileOpenDisposition::CreateAlways);
|
||||
|
||||
co_await contentStream.ReadAsync(buffer, buffer.Capacity(), storage::Streams::InputStreamOptions::None);
|
||||
while (buffer.Length() > 0)
|
||||
{
|
||||
co_await fileStream.WriteAsync(buffer);
|
||||
totalBytesRead += buffer.Length();
|
||||
if (progressUpdateCallback)
|
||||
{
|
||||
float percentage = static_cast<float>(totalBytesRead) / totalBytes;
|
||||
progressUpdateCallback(percentage);
|
||||
}
|
||||
|
||||
co_await contentStream.ReadAsync(buffer, buffer.Capacity(), storage::Streams::InputStreamOptions::None);
|
||||
}
|
||||
|
||||
if (progressUpdateCallback)
|
||||
{
|
||||
progressUpdateCallback(1);
|
||||
}
|
||||
|
||||
fileStream.Close();
|
||||
contentStream.Close();
|
||||
}
|
||||
std::future<std::wstring> request(const winrt::Windows::Foundation::Uri& url);
|
||||
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath);
|
||||
std::future<void> download(const winrt::Windows::Foundation::Uri& url,
|
||||
const std::wstring& dstFilePath,
|
||||
const std::function<void(float)>& progressUpdateCallback);
|
||||
|
||||
private:
|
||||
winrt::Windows::Web::Http::HttpClient m_client;
|
||||
|
||||
13
src/common/utils/MsWindowsSettings.cpp
Normal file
13
src/common/utils/MsWindowsSettings.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#include "pch.h"
|
||||
#include "MsWindowsSettings.h"
|
||||
|
||||
bool GetAnimationsEnabled()
|
||||
{
|
||||
BOOL enabled = 0;
|
||||
const auto result = SystemParametersInfo(SPI_GETCLIENTAREAANIMATION, 0, &enabled, 0);
|
||||
if (!result)
|
||||
{
|
||||
Logger::error("SystemParametersInfo SPI_GETCLIENTAREAANIMATION failed.");
|
||||
}
|
||||
return enabled != 0;
|
||||
}
|
||||
@@ -1,13 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
inline bool GetAnimationsEnabled()
|
||||
{
|
||||
BOOL enabled = 0;
|
||||
BOOL fResult;
|
||||
fResult = SystemParametersInfo(SPI_GETCLIENTAREAANIMATION, 0, &enabled, 0);
|
||||
if (!fResult)
|
||||
{
|
||||
Logger::error("SystemParametersInfo SPI_GETCLIENTAREAANIMATION failed.");
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
#include <common/logger/logger.h>
|
||||
#include <windows.h>
|
||||
|
||||
bool GetAnimationsEnabled();
|
||||
|
||||
83
src/common/utils/MsiUtils.cpp
Normal file
83
src/common/utils/MsiUtils.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "pch.h"
|
||||
#include "MsiUtils.h"
|
||||
|
||||
std::optional<std::wstring> GetMsiPackageInstalledPath(bool perUser)
|
||||
{
|
||||
constexpr size_t guid_length = 39;
|
||||
wchar_t productId[guid_length];
|
||||
const std::wstring upgradeCode = perUser ? POWER_TOYS_UPGRADE_CODE_USER : POWER_TOYS_UPGRADE_CODE;
|
||||
if (const bool found = ERROR_SUCCESS == MsiEnumRelatedProductsW(upgradeCode.c_str(), 0, 0, productId); !found)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (const bool installed = INSTALLSTATE_DEFAULT == MsiQueryProductStateW(productId); !installed)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
DWORD bufferSize = MAX_PATH;
|
||||
wchar_t buffer[MAX_PATH];
|
||||
if (ERROR_SUCCESS == MsiGetProductInfoW(productId, INSTALLPROPERTY_INSTALLLOCATION, buffer, &bufferSize) && bufferSize)
|
||||
{
|
||||
return buffer;
|
||||
}
|
||||
|
||||
DWORD packagePathSize = 0;
|
||||
|
||||
if (ERROR_SUCCESS != MsiGetProductInfoW(productId, INSTALLPROPERTY_LOCALPACKAGE, nullptr, &packagePathSize))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::wstring packagePath(++packagePathSize, L'\0');
|
||||
|
||||
if (ERROR_SUCCESS != MsiGetProductInfoW(productId, INSTALLPROPERTY_LOCALPACKAGE, packagePath.data(), &packagePathSize))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
packagePath.resize(size(packagePath) - 1); // trim additional \0 which we got from MsiGetProductInfoW
|
||||
|
||||
wchar_t path[MAX_PATH];
|
||||
DWORD pathSize = MAX_PATH;
|
||||
MsiGetComponentPathW(productId, POWERTOYS_EXE_COMPONENT, path, &pathSize);
|
||||
if (!pathSize)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
PathCchRemoveFileSpec(path, pathSize);
|
||||
return path;
|
||||
}
|
||||
|
||||
std::wstring GetMsiPackagePath()
|
||||
{
|
||||
std::wstring packagePath;
|
||||
wchar_t productString[39];
|
||||
if (const bool found = ERROR_SUCCESS == MsiEnumRelatedProductsW(POWER_TOYS_UPGRADE_CODE, 0, 0, productString); !found)
|
||||
{
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
if (const bool installed = INSTALLSTATE_DEFAULT == MsiQueryProductStateW(productString); !installed)
|
||||
{
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
DWORD packagePathSize = 0;
|
||||
|
||||
if (const bool hasPackagePath = ERROR_SUCCESS == MsiGetProductInfoW(productString, INSTALLPROPERTY_LOCALPACKAGE, nullptr, &packagePathSize); !hasPackagePath)
|
||||
{
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
packagePath = std::wstring(++packagePathSize, L'\0');
|
||||
if (const bool gotPackagePath = ERROR_SUCCESS == MsiGetProductInfoW(productString, INSTALLPROPERTY_LOCALPACKAGE, packagePath.data(), &packagePathSize); !gotPackagePath)
|
||||
{
|
||||
packagePath.clear();
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
packagePath.resize(size(packagePath) - 1); // trim additional \0 which we got from MsiGetProductInfoW
|
||||
|
||||
return packagePath;
|
||||
}
|
||||
@@ -16,82 +16,5 @@ namespace // Strings in this namespace should not be localized
|
||||
const inline wchar_t POWERTOYS_EXE_COMPONENT[] = L"{A2C66D91-3485-4D00-B04D-91844E6B345B}";
|
||||
}
|
||||
|
||||
std::optional<std::wstring> GetMsiPackageInstalledPath(bool perUser)
|
||||
{
|
||||
constexpr size_t guid_length = 39;
|
||||
wchar_t product_ID[guid_length];
|
||||
std::wstring upgradeCode = (perUser ? POWER_TOYS_UPGRADE_CODE_USER : POWER_TOYS_UPGRADE_CODE);
|
||||
if (const bool found = ERROR_SUCCESS == MsiEnumRelatedProductsW(upgradeCode.c_str(), 0, 0, product_ID); !found)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (const bool installed = INSTALLSTATE_DEFAULT == MsiQueryProductStateW(product_ID); !installed)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
DWORD buf_size = MAX_PATH;
|
||||
wchar_t buf[MAX_PATH];
|
||||
if (ERROR_SUCCESS == MsiGetProductInfoW(product_ID, INSTALLPROPERTY_INSTALLLOCATION, buf, &buf_size) && buf_size)
|
||||
{
|
||||
return buf;
|
||||
}
|
||||
|
||||
DWORD package_path_size = 0;
|
||||
|
||||
if (ERROR_SUCCESS != MsiGetProductInfoW(product_ID, INSTALLPROPERTY_LOCALPACKAGE, nullptr, &package_path_size))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
std::wstring package_path(++package_path_size, L'\0');
|
||||
|
||||
if (ERROR_SUCCESS != MsiGetProductInfoW(product_ID, INSTALLPROPERTY_LOCALPACKAGE, package_path.data(), &package_path_size))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
package_path.resize(size(package_path) - 1); // trim additional \0 which we got from MsiGetProductInfoW
|
||||
|
||||
wchar_t path[MAX_PATH];
|
||||
DWORD path_size = MAX_PATH;
|
||||
MsiGetComponentPathW(product_ID, POWERTOYS_EXE_COMPONENT, path, &path_size);
|
||||
if (!path_size)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
PathCchRemoveFileSpec(path, path_size);
|
||||
return path;
|
||||
}
|
||||
|
||||
std::wstring GetMsiPackagePath()
|
||||
{
|
||||
std::wstring package_path;
|
||||
wchar_t GUID_product_string[39];
|
||||
if (const bool found = ERROR_SUCCESS == MsiEnumRelatedProductsW(POWER_TOYS_UPGRADE_CODE, 0, 0, GUID_product_string); !found)
|
||||
{
|
||||
return package_path;
|
||||
}
|
||||
|
||||
if (const bool installed = INSTALLSTATE_DEFAULT == MsiQueryProductStateW(GUID_product_string); !installed)
|
||||
{
|
||||
return package_path;
|
||||
}
|
||||
|
||||
DWORD package_path_size = 0;
|
||||
|
||||
if (const bool has_package_path = ERROR_SUCCESS == MsiGetProductInfoW(GUID_product_string, INSTALLPROPERTY_LOCALPACKAGE, nullptr, &package_path_size); !has_package_path)
|
||||
{
|
||||
return package_path;
|
||||
}
|
||||
|
||||
package_path = std::wstring(++package_path_size, L'\0');
|
||||
if (const bool got_package_path = ERROR_SUCCESS == MsiGetProductInfoW(GUID_product_string, INSTALLPROPERTY_LOCALPACKAGE, package_path.data(), &package_path_size); !got_package_path)
|
||||
{
|
||||
package_path = {};
|
||||
return package_path;
|
||||
}
|
||||
|
||||
package_path.resize(size(package_path) - 1); // trim additional \0 which we got from MsiGetProductInfoW
|
||||
|
||||
return package_path;
|
||||
}
|
||||
std::optional<std::wstring> GetMsiPackageInstalledPath(bool perUser);
|
||||
std::wstring GetMsiPackagePath();
|
||||
|
||||
57
src/common/utils/OnThreadExecutor.cpp
Normal file
57
src/common/utils/OnThreadExecutor.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "pch.h"
|
||||
#include "OnThreadExecutor.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
OnThreadExecutor::OnThreadExecutor() :
|
||||
_worker_thread([this] { worker_thread(); })
|
||||
{
|
||||
}
|
||||
|
||||
OnThreadExecutor::~OnThreadExecutor()
|
||||
{
|
||||
_shutdown_request = true;
|
||||
_task_cv.notify_one();
|
||||
if (_worker_thread.joinable())
|
||||
{
|
||||
_worker_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
std::future<void> OnThreadExecutor::submit(task_t task)
|
||||
{
|
||||
auto future = task.get_future();
|
||||
{
|
||||
std::lock_guard lock{ _task_mutex };
|
||||
_task_queue.emplace(std::move(task));
|
||||
}
|
||||
_task_cv.notify_one();
|
||||
return future;
|
||||
}
|
||||
|
||||
void OnThreadExecutor::cancel()
|
||||
{
|
||||
std::lock_guard lock{ _task_mutex };
|
||||
std::queue<task_t> emptyQueue;
|
||||
std::swap(_task_queue, emptyQueue);
|
||||
_task_cv.notify_one();
|
||||
}
|
||||
|
||||
void OnThreadExecutor::worker_thread()
|
||||
{
|
||||
while (!_shutdown_request)
|
||||
{
|
||||
task_t task;
|
||||
{
|
||||
std::unique_lock task_lock{ _task_mutex };
|
||||
_task_cv.wait(task_lock, [this] { return !_task_queue.empty() || _shutdown_request; });
|
||||
if (_shutdown_request)
|
||||
{
|
||||
return;
|
||||
}
|
||||
task = std::move(_task_queue.front());
|
||||
_task_queue.pop();
|
||||
}
|
||||
task();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <functional>
|
||||
#include <queue>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
|
||||
// OnThreadExecutor allows its caller to off-load some work to a persistently running background thread.
|
||||
// This might come in handy if you use the API which sets thread-wide global state and the state needs
|
||||
@@ -15,58 +17,18 @@ class OnThreadExecutor final
|
||||
public:
|
||||
using task_t = std::packaged_task<void()>;
|
||||
|
||||
OnThreadExecutor() :
|
||||
_shutdown_request{ false },
|
||||
_worker_thread{ [this] { worker_thread(); } }
|
||||
{
|
||||
}
|
||||
OnThreadExecutor();
|
||||
~OnThreadExecutor();
|
||||
|
||||
~OnThreadExecutor()
|
||||
{
|
||||
_shutdown_request = true;
|
||||
_task_cv.notify_one();
|
||||
_worker_thread.join();
|
||||
}
|
||||
|
||||
std::future<void> submit(task_t task)
|
||||
{
|
||||
auto future = task.get_future();
|
||||
std::lock_guard lock{ _task_mutex };
|
||||
_task_queue.emplace(std::move(task));
|
||||
_task_cv.notify_one();
|
||||
return future;
|
||||
}
|
||||
|
||||
void cancel()
|
||||
{
|
||||
std::lock_guard lock{ _task_mutex };
|
||||
_task_queue = {};
|
||||
_task_cv.notify_one();
|
||||
}
|
||||
std::future<void> submit(task_t task);
|
||||
void cancel();
|
||||
|
||||
private:
|
||||
void worker_thread()
|
||||
{
|
||||
while (!_shutdown_request)
|
||||
{
|
||||
task_t task;
|
||||
{
|
||||
std::unique_lock task_lock{ _task_mutex };
|
||||
_task_cv.wait(task_lock, [this] { return !_task_queue.empty() || _shutdown_request; });
|
||||
if (_shutdown_request)
|
||||
{
|
||||
return;
|
||||
}
|
||||
task = std::move(_task_queue.front());
|
||||
_task_queue.pop();
|
||||
}
|
||||
task();
|
||||
}
|
||||
}
|
||||
void worker_thread();
|
||||
|
||||
std::mutex _task_mutex;
|
||||
std::condition_variable _task_cv;
|
||||
std::atomic_bool _shutdown_request;
|
||||
std::atomic_bool _shutdown_request{ false };
|
||||
std::queue<std::packaged_task<void()>> _task_queue;
|
||||
std::thread _worker_thread;
|
||||
};
|
||||
|
||||
40
src/common/utils/ProcessWaiter.cpp
Normal file
40
src/common/utils/ProcessWaiter.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include "pch.h"
|
||||
#include "ProcessWaiter.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace ProcessWaiter
|
||||
{
|
||||
void OnProcessTerminate(std::wstring parent_pid, std::function<void(DWORD)> callback)
|
||||
{
|
||||
DWORD pid = 0;
|
||||
try
|
||||
{
|
||||
pid = std::stoul(parent_pid);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
callback(ERROR_INVALID_PARAMETER);
|
||||
return;
|
||||
}
|
||||
|
||||
std::thread([pid, callback = std::move(callback)]() mutable {
|
||||
wil::unique_handle process{ OpenProcess(SYNCHRONIZE, FALSE, pid) };
|
||||
if (process)
|
||||
{
|
||||
if (WaitForSingleObject(process.get(), INFINITE) == WAIT_OBJECT_0)
|
||||
{
|
||||
callback(ERROR_SUCCESS);
|
||||
}
|
||||
else
|
||||
{
|
||||
callback(GetLastError());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
callback(GetLastError());
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <Windows.h>
|
||||
#include <thread>
|
||||
#include <Windows.h>
|
||||
|
||||
namespace ProcessWaiter
|
||||
{
|
||||
void OnProcessTerminate(std::wstring parent_pid, std::function<void(DWORD)> callback)
|
||||
{
|
||||
DWORD pid = std::stol(parent_pid);
|
||||
std::thread([=]() {
|
||||
HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, pid);
|
||||
if (process != nullptr)
|
||||
{
|
||||
if (WaitForSingleObject(process, INFINITE) == WAIT_OBJECT_0)
|
||||
{
|
||||
CloseHandle(process);
|
||||
callback(ERROR_SUCCESS);
|
||||
}
|
||||
else
|
||||
{
|
||||
CloseHandle(process);
|
||||
callback(GetLastError());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
callback(GetLastError());
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
void OnProcessTerminate(std::wstring parent_pid, std::function<void(DWORD)> callback);
|
||||
}
|
||||
|
||||
277
src/common/utils/UnhandledExceptionHandler.cpp
Normal file
277
src/common/utils/UnhandledExceptionHandler.cpp
Normal file
@@ -0,0 +1,277 @@
|
||||
#include "pch.h"
|
||||
#include "UnhandledExceptionHandler.h"
|
||||
|
||||
#include <DbgHelp.h>
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
#include <sstream>
|
||||
|
||||
#include "winapi_error.h"
|
||||
#include "../logger/logger.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::atomic_bool processingException{ false };
|
||||
|
||||
const char* exceptionDescription(const DWORD code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case EXCEPTION_ACCESS_VIOLATION:
|
||||
return "EXCEPTION_ACCESS_VIOLATION";
|
||||
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
|
||||
return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED";
|
||||
case EXCEPTION_BREAKPOINT:
|
||||
return "EXCEPTION_BREAKPOINT";
|
||||
case EXCEPTION_DATATYPE_MISALIGNMENT:
|
||||
return "EXCEPTION_DATATYPE_MISALIGNMENT";
|
||||
case EXCEPTION_FLT_DENORMAL_OPERAND:
|
||||
return "EXCEPTION_FLT_DENORMAL_OPERAND";
|
||||
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_FLT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_FLT_INEXACT_RESULT:
|
||||
return "EXCEPTION_FLT_INEXACT_RESULT";
|
||||
case EXCEPTION_FLT_INVALID_OPERATION:
|
||||
return "EXCEPTION_FLT_INVALID_OPERATION";
|
||||
case EXCEPTION_FLT_OVERFLOW:
|
||||
return "EXCEPTION_FLT_OVERFLOW";
|
||||
case EXCEPTION_FLT_STACK_CHECK:
|
||||
return "EXCEPTION_FLT_STACK_CHECK";
|
||||
case EXCEPTION_FLT_UNDERFLOW:
|
||||
return "EXCEPTION_FLT_UNDERFLOW";
|
||||
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
||||
return "EXCEPTION_ILLEGAL_INSTRUCTION";
|
||||
case EXCEPTION_IN_PAGE_ERROR:
|
||||
return "EXCEPTION_IN_PAGE_ERROR";
|
||||
case EXCEPTION_INT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_INT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_INT_OVERFLOW:
|
||||
return "EXCEPTION_INT_OVERFLOW";
|
||||
case EXCEPTION_INVALID_DISPOSITION:
|
||||
return "EXCEPTION_INVALID_DISPOSITION";
|
||||
case EXCEPTION_NONCONTINUABLE_EXCEPTION:
|
||||
return "EXCEPTION_NONCONTINUABLE_EXCEPTION";
|
||||
case EXCEPTION_PRIV_INSTRUCTION:
|
||||
return "EXCEPTION_PRIV_INSTRUCTION";
|
||||
case EXCEPTION_SINGLE_STEP:
|
||||
return "EXCEPTION_SINGLE_STEP";
|
||||
case EXCEPTION_STACK_OVERFLOW:
|
||||
return "EXCEPTION_STACK_OVERFLOW";
|
||||
default:
|
||||
return "UNKNOWN EXCEPTION";
|
||||
}
|
||||
}
|
||||
|
||||
int GetFilenameStart(wchar_t* path)
|
||||
{
|
||||
int pos = 0;
|
||||
int found = 0;
|
||||
if (path != nullptr)
|
||||
{
|
||||
while (path[pos] != L'\0' && pos < MAX_PATH)
|
||||
{
|
||||
if (path[pos] == L'\\')
|
||||
{
|
||||
found = pos + 1;
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
std::wstring GetModuleName(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static wchar_t modulePath[MAX_PATH]{};
|
||||
memset(&modulePath[0], '\0', sizeof(modulePath));
|
||||
|
||||
const DWORD64 moduleBase = SymGetModuleBase64(process, stack.AddrPC.Offset);
|
||||
if (!moduleBase)
|
||||
{
|
||||
Logger::error(L"Failed to get a module. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
if (!GetModuleFileNameW(reinterpret_cast<HINSTANCE>(moduleBase), modulePath, MAX_PATH))
|
||||
{
|
||||
Logger::error(L"Failed to get a module path. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
const int start = GetFilenameStart(modulePath);
|
||||
return std::wstring(modulePath, start);
|
||||
}
|
||||
|
||||
std::wstring GetName(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static IMAGEHLP_SYMBOL64* pSymbol = static_cast<IMAGEHLP_SYMBOL64*>(malloc(sizeof(IMAGEHLP_SYMBOL64) + MAX_PATH * sizeof(TCHAR)));
|
||||
if (!pSymbol)
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
memset(pSymbol, '\0', sizeof(*pSymbol) + MAX_PATH);
|
||||
pSymbol->MaxNameLength = MAX_PATH;
|
||||
pSymbol->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL64);
|
||||
|
||||
DWORD64 displacement = 0;
|
||||
if (!SymGetSymFromAddr64(process, stack.AddrPC.Offset, &displacement, pSymbol))
|
||||
{
|
||||
Logger::error(L"Failed to get a symbol. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
std::string str = pSymbol->Name;
|
||||
return std::wstring(str.begin(), str.end());
|
||||
}
|
||||
|
||||
std::wstring GetLine(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static IMAGEHLP_LINE64 line{};
|
||||
|
||||
memset(&line, '\0', sizeof(IMAGEHLP_LINE64));
|
||||
line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
|
||||
line.LineNumber = 0;
|
||||
|
||||
DWORD displacement = 0;
|
||||
if (!SymGetLineFromAddr64(process, stack.AddrPC.Offset, &displacement, &line))
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
std::string fileName(line.FileName);
|
||||
return L"(" + std::wstring(fileName.begin(), fileName.end()) + L":" + std::to_wstring(line.LineNumber) + L")";
|
||||
}
|
||||
|
||||
void LogStackTrace()
|
||||
{
|
||||
CONTEXT context;
|
||||
try
|
||||
{
|
||||
RtlCaptureContext(&context);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Failed to capture context. {}", get_last_error_or_default(GetLastError()));
|
||||
return;
|
||||
}
|
||||
|
||||
STACKFRAME64 stack;
|
||||
memset(&stack, 0, sizeof(STACKFRAME64));
|
||||
|
||||
HANDLE process = GetCurrentProcess();
|
||||
HANDLE thread = GetCurrentThread();
|
||||
|
||||
#ifdef _M_ARM64
|
||||
stack.AddrPC.Offset = context.Pc;
|
||||
stack.AddrStack.Offset = context.Sp;
|
||||
stack.AddrFrame.Offset = context.Fp;
|
||||
#else
|
||||
stack.AddrPC.Offset = context.Rip;
|
||||
stack.AddrStack.Offset = context.Rsp;
|
||||
stack.AddrFrame.Offset = context.Rbp;
|
||||
#endif
|
||||
stack.AddrPC.Mode = AddrModeFlat;
|
||||
stack.AddrStack.Mode = AddrModeFlat;
|
||||
stack.AddrFrame.Mode = AddrModeFlat;
|
||||
|
||||
std::wstringstream ss;
|
||||
for (;;)
|
||||
{
|
||||
const BOOL result = StackWalk64(
|
||||
#ifdef _M_ARM64
|
||||
IMAGE_FILE_MACHINE_ARM64,
|
||||
#else
|
||||
IMAGE_FILE_MACHINE_AMD64,
|
||||
#endif
|
||||
process,
|
||||
thread,
|
||||
&stack,
|
||||
&context,
|
||||
NULL,
|
||||
SymFunctionTableAccess64,
|
||||
SymGetModuleBase64,
|
||||
NULL);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ss << GetModuleName(process, stack) << "!" << GetName(process, stack) << GetLine(process, stack) << std::endl;
|
||||
}
|
||||
|
||||
Logger::error(L"STACK TRACE\r\n{}", ss.str());
|
||||
Logger::flush();
|
||||
}
|
||||
}
|
||||
|
||||
LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info)
|
||||
{
|
||||
bool expected = false;
|
||||
if (!processingException.compare_exchange_strong(expected, true))
|
||||
{
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
auto guard = wil::scope_exit([]() noexcept { processingException = false; });
|
||||
|
||||
try
|
||||
{
|
||||
const char* description = "Exception code not available";
|
||||
if (info != nullptr && info->ExceptionRecord != nullptr && info->ExceptionRecord->ExceptionCode != 0)
|
||||
{
|
||||
description = exceptionDescription(info->ExceptionRecord->ExceptionCode);
|
||||
}
|
||||
|
||||
Logger::error(description);
|
||||
LogStackTrace();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to log stack trace");
|
||||
Logger::flush();
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
void AbortHandler(int /*signal_number*/)
|
||||
{
|
||||
Logger::error("--- ABORT");
|
||||
try
|
||||
{
|
||||
LogStackTrace();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to log stack trace on abort");
|
||||
Logger::flush();
|
||||
}
|
||||
}
|
||||
|
||||
void InitSymbols()
|
||||
{
|
||||
// Preload symbols so they will be available in case of out-of-memory exception
|
||||
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME);
|
||||
HANDLE process = GetCurrentProcess();
|
||||
if (!SymInitialize(process, NULL, TRUE))
|
||||
{
|
||||
Logger::error(L"Failed to initialize symbol handler. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
}
|
||||
|
||||
void InitUnhandledExceptionHandler()
|
||||
{
|
||||
try
|
||||
{
|
||||
InitSymbols();
|
||||
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||
signal(SIGABRT, &AbortHandler);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to init global unhandled exception handler");
|
||||
}
|
||||
}
|
||||
@@ -1,280 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include <DbgHelp.h>
|
||||
#include <signal.h>
|
||||
#include <sstream>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "winapi_error.h"
|
||||
#include "../logger/logger.h"
|
||||
|
||||
static BOOLEAN processingException = FALSE;
|
||||
|
||||
static inline const char* exceptionDescription(const DWORD& code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case EXCEPTION_ACCESS_VIOLATION:
|
||||
return "EXCEPTION_ACCESS_VIOLATION";
|
||||
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
|
||||
return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED";
|
||||
case EXCEPTION_BREAKPOINT:
|
||||
return "EXCEPTION_BREAKPOINT";
|
||||
case EXCEPTION_DATATYPE_MISALIGNMENT:
|
||||
return "EXCEPTION_DATATYPE_MISALIGNMENT";
|
||||
case EXCEPTION_FLT_DENORMAL_OPERAND:
|
||||
return "EXCEPTION_FLT_DENORMAL_OPERAND";
|
||||
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_FLT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_FLT_INEXACT_RESULT:
|
||||
return "EXCEPTION_FLT_INEXACT_RESULT";
|
||||
case EXCEPTION_FLT_INVALID_OPERATION:
|
||||
return "EXCEPTION_FLT_INVALID_OPERATION";
|
||||
case EXCEPTION_FLT_OVERFLOW:
|
||||
return "EXCEPTION_FLT_OVERFLOW";
|
||||
case EXCEPTION_FLT_STACK_CHECK:
|
||||
return "EXCEPTION_FLT_STACK_CHECK";
|
||||
case EXCEPTION_FLT_UNDERFLOW:
|
||||
return "EXCEPTION_FLT_UNDERFLOW";
|
||||
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
||||
return "EXCEPTION_ILLEGAL_INSTRUCTION";
|
||||
case EXCEPTION_IN_PAGE_ERROR:
|
||||
return "EXCEPTION_IN_PAGE_ERROR";
|
||||
case EXCEPTION_INT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_INT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_INT_OVERFLOW:
|
||||
return "EXCEPTION_INT_OVERFLOW";
|
||||
case EXCEPTION_INVALID_DISPOSITION:
|
||||
return "EXCEPTION_INVALID_DISPOSITION";
|
||||
case EXCEPTION_NONCONTINUABLE_EXCEPTION:
|
||||
return "EXCEPTION_NONCONTINUABLE_EXCEPTION";
|
||||
case EXCEPTION_PRIV_INSTRUCTION:
|
||||
return "EXCEPTION_PRIV_INSTRUCTION";
|
||||
case EXCEPTION_SINGLE_STEP:
|
||||
return "EXCEPTION_SINGLE_STEP";
|
||||
case EXCEPTION_STACK_OVERFLOW:
|
||||
return "EXCEPTION_STACK_OVERFLOW";
|
||||
default:
|
||||
return "UNKNOWN EXCEPTION";
|
||||
}
|
||||
}
|
||||
|
||||
/* Returns the index of the last backslash in the file path */
|
||||
inline int GetFilenameStart(wchar_t* path)
|
||||
{
|
||||
int pos = 0;
|
||||
int found = 0;
|
||||
if (path != NULL)
|
||||
{
|
||||
while (path[pos] != L'\0' && pos < MAX_PATH)
|
||||
{
|
||||
if (path[pos] == L'\\')
|
||||
{
|
||||
found = pos + 1;
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
inline std::wstring GetModuleName(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static wchar_t modulePath[MAX_PATH]{};
|
||||
const size_t size = sizeof(modulePath);
|
||||
memset(&modulePath[0], '\0', size);
|
||||
|
||||
DWORD64 moduleBase = SymGetModuleBase64(process, stack.AddrPC.Offset);
|
||||
if (!moduleBase)
|
||||
{
|
||||
Logger::error(L"Failed to get a module. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
if (!GetModuleFileNameW(reinterpret_cast<HINSTANCE>(moduleBase), modulePath, MAX_PATH))
|
||||
{
|
||||
Logger::error(L"Failed to get a module path. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
const int start = GetFilenameStart(modulePath);
|
||||
return std::wstring(modulePath, start);
|
||||
}
|
||||
|
||||
inline std::wstring GetName(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static IMAGEHLP_SYMBOL64* pSymbol = static_cast<IMAGEHLP_SYMBOL64*>(malloc(sizeof(IMAGEHLP_SYMBOL64) + MAX_PATH * sizeof(TCHAR)));
|
||||
if (!pSymbol)
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
memset(pSymbol, '\0', sizeof(*pSymbol) + MAX_PATH);
|
||||
pSymbol->MaxNameLength = MAX_PATH;
|
||||
pSymbol->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL64);
|
||||
|
||||
DWORD64 dw64Displacement = 0;
|
||||
if (!SymGetSymFromAddr64(process, stack.AddrPC.Offset, &dw64Displacement, pSymbol))
|
||||
{
|
||||
Logger::error(L"Failed to get a symbol. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
std::string str = pSymbol->Name;
|
||||
return std::wstring(str.begin(), str.end());
|
||||
}
|
||||
|
||||
inline std::wstring GetLine(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static IMAGEHLP_LINE64 line{};
|
||||
|
||||
memset(&line, '\0', sizeof(IMAGEHLP_LINE64));
|
||||
line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
|
||||
line.LineNumber = 0;
|
||||
|
||||
DWORD dwDisplacement = 0;
|
||||
if (!SymGetLineFromAddr64(process, stack.AddrPC.Offset, &dwDisplacement, &line))
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
std::string fileName(line.FileName);
|
||||
return L"(" + std::wstring(fileName.begin(), fileName.end()) + L":" + std::to_wstring(line.LineNumber) + L")";
|
||||
}
|
||||
|
||||
inline void LogStackTrace()
|
||||
{
|
||||
CONTEXT context;
|
||||
try
|
||||
{
|
||||
RtlCaptureContext(&context);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Failed to capture context. {}", get_last_error_or_default(GetLastError()));
|
||||
return;
|
||||
}
|
||||
|
||||
STACKFRAME64 stack;
|
||||
memset(&stack, 0, sizeof(STACKFRAME64));
|
||||
|
||||
HANDLE process = GetCurrentProcess();
|
||||
HANDLE thread = GetCurrentThread();
|
||||
|
||||
#ifdef _M_ARM64
|
||||
stack.AddrPC.Offset = context.Pc;
|
||||
stack.AddrStack.Offset = context.Sp;
|
||||
stack.AddrFrame.Offset = context.Fp;
|
||||
#else
|
||||
stack.AddrPC.Offset = context.Rip;
|
||||
stack.AddrStack.Offset = context.Rsp;
|
||||
stack.AddrFrame.Offset = context.Rbp;
|
||||
#endif
|
||||
stack.AddrPC.Mode = AddrModeFlat;
|
||||
stack.AddrStack.Mode = AddrModeFlat;
|
||||
stack.AddrFrame.Mode = AddrModeFlat;
|
||||
|
||||
BOOL result = false;
|
||||
std::wstringstream ss;
|
||||
for (;;)
|
||||
{
|
||||
result = StackWalk64(
|
||||
#ifdef _M_ARM64
|
||||
IMAGE_FILE_MACHINE_ARM64,
|
||||
#else
|
||||
IMAGE_FILE_MACHINE_AMD64,
|
||||
#endif
|
||||
process,
|
||||
thread,
|
||||
&stack,
|
||||
&context,
|
||||
NULL,
|
||||
SymFunctionTableAccess64,
|
||||
SymGetModuleBase64,
|
||||
NULL);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ss << GetModuleName(process, stack) << "!" << GetName(process, stack) << GetLine(process, stack) << std::endl;
|
||||
}
|
||||
|
||||
Logger::error(L"STACK TRACE\r\n{}", ss.str());
|
||||
Logger::flush();
|
||||
}
|
||||
|
||||
inline LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info)
|
||||
{
|
||||
if (!processingException)
|
||||
{
|
||||
bool headerLogged = false;
|
||||
try
|
||||
{
|
||||
const char* exDescription = "Exception code not available";
|
||||
processingException = true;
|
||||
if (info != NULL && info->ExceptionRecord != NULL && info->ExceptionRecord->ExceptionCode != NULL)
|
||||
{
|
||||
exDescription = exceptionDescription(info->ExceptionRecord->ExceptionCode);
|
||||
}
|
||||
|
||||
headerLogged = true;
|
||||
Logger::error(exDescription);
|
||||
LogStackTrace();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to log stack trace");
|
||||
Logger::flush();
|
||||
}
|
||||
|
||||
processingException = false;
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
/* Handler to trap abort() calls */
|
||||
inline void AbortHandler(int /*signal_number*/)
|
||||
{
|
||||
Logger::error("--- ABORT");
|
||||
try
|
||||
{
|
||||
LogStackTrace();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to log stack trace on abort");
|
||||
Logger::flush();
|
||||
}
|
||||
}
|
||||
|
||||
inline void InitSymbols()
|
||||
{
|
||||
// Preload symbols so they will be available in case of out-of-memory exception
|
||||
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME);
|
||||
HANDLE process = GetCurrentProcess();
|
||||
if (!SymInitialize(process, NULL, TRUE))
|
||||
{
|
||||
Logger::error(L"Failed to initialize symbol handler. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
}
|
||||
|
||||
inline void InitUnhandledExceptionHandler(void)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitSymbols();
|
||||
// Global handler for unhandled exceptions
|
||||
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||
// Handler for abort()
|
||||
signal(SIGABRT, &AbortHandler);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to init global unhandled exception handler");
|
||||
}
|
||||
}
|
||||
LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info);
|
||||
void AbortHandler(int signal_number);
|
||||
void InitSymbols();
|
||||
void InitUnhandledExceptionHandler();
|
||||
|
||||
14
src/common/utils/appMutex.cpp
Normal file
14
src/common/utils/appMutex.cpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#include "pch.h"
|
||||
#include "appMutex.h"
|
||||
|
||||
wil::unique_mutex_nothrow createAppMutex(const std::wstring& mutexName)
|
||||
{
|
||||
wil::unique_mutex_nothrow result{ CreateMutexW(nullptr, TRUE, mutexName.c_str()) };
|
||||
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -14,9 +14,4 @@ namespace
|
||||
constexpr inline wchar_t POWERTOYS_BOOTSTRAPPER_MUTEX_NAME[] = L"Local\\PowerToys_Bootstrapper_InstanceMutex";
|
||||
}
|
||||
|
||||
inline wil::unique_mutex_nothrow createAppMutex(const std::wstring& mutexName)
|
||||
{
|
||||
wil::unique_mutex_nothrow result{ CreateMutexW(nullptr, TRUE, mutexName.c_str()) };
|
||||
|
||||
return GetLastError() == ERROR_ALREADY_EXISTS ? wil::unique_mutex_nothrow{} : std::move(result);
|
||||
}
|
||||
wil::unique_mutex_nothrow createAppMutex(const std::wstring& mutexName);
|
||||
|
||||
18
src/common/utils/clean_video_conference.cpp
Normal file
18
src/common/utils/clean_video_conference.cpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#include "pch.h"
|
||||
#include "clean_video_conference.h"
|
||||
|
||||
void clean_video_conference()
|
||||
{
|
||||
// 31AD75E9-8C3A-49C8-B9ED-5880D6B4A764 is the CLSID GUID for the 64 video conference mute driver.
|
||||
// 31AD75E9-8C3A-49C8-B9ED-5880D6B4A732 is the CLSID GUID for the 32 video conference mute driver.
|
||||
// 860BB310-5D01-11D0-BD3B-00A0C911CE86 is the CLSID GUID for CLSID_VideoInputDeviceCategory.
|
||||
|
||||
// Unregister the 64 bit driver CLSID:
|
||||
RegDeleteTreeW(HKEY_CLASSES_ROOT, L"CLSID\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A764}");
|
||||
// Unregister the 64 bit driver CLSID from Video Input Devices:
|
||||
RegDeleteTreeW(HKEY_CLASSES_ROOT, L"CLSID\\{860BB310-5D01-11D0-BD3B-00A0C911CE86}\\Instance\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A764}");
|
||||
// Unregister the 32 bit driver CLSID:
|
||||
RegDeleteTreeW(HKEY_LOCAL_MACHINE, L"Software\\WOW6432Node\\Classes\\CLSID\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A732}");
|
||||
// Unregister the 32 bit driver CLSID from Video Input Devices:
|
||||
RegDeleteTreeW(HKEY_LOCAL_MACHINE, L"Software\\WOW6432Node\\Classes\\CLSID\\{860BB310-5D01-11D0-BD3B-00A0C911CE86}\\Instance\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A732}");
|
||||
}
|
||||
@@ -1,18 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
// Video Conference Mute was a utility we deprecated. However, this required a manual user disable of the module to remove the camera registration, so we include the disable code here to be able to clean up.
|
||||
void clean_video_conference()
|
||||
{
|
||||
// 31AD75E9-8C3A-49C8-B9ED-5880D6B4A764 is the CLSID GUID for the 64 video conference mute driver.
|
||||
// 31AD75E9-8C3A-49C8-B9ED-5880D6B4A732 is the CLSID GUID for the 32 video conference mute driver.
|
||||
// 860BB310-5D01-11D0-BD3B-00A0C911CE86 is the CLSID GUID for CLSID_VideoInputDeviceCategory.
|
||||
#include <windows.h>
|
||||
|
||||
// Unregister the 64 bit driver CLSID:
|
||||
RegDeleteTreeW(HKEY_CLASSES_ROOT, L"CLSID\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A764}");
|
||||
// Unregister the 64 bit driver CLSID from Video Input Devices:
|
||||
RegDeleteTreeW(HKEY_CLASSES_ROOT, L"CLSID\\{860BB310-5D01-11D0-BD3B-00A0C911CE86}\\Instance\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A764}");
|
||||
// Unregister the 32 bit driver CLSID:
|
||||
RegDeleteTreeW(HKEY_LOCAL_MACHINE, L"Software\\WOW6432Node\\Classes\\CLSID\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A732}");
|
||||
// Unregister the 32 bit driver CLSID from Video Input Devices:
|
||||
RegDeleteTreeW(HKEY_LOCAL_MACHINE, L"Software\\WOW6432Node\\Classes\\CLSID\\{860BB310-5D01-11D0-BD3B-00A0C911CE86}\\Instance\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A732}");
|
||||
}
|
||||
// Video Conference Mute was a utility we deprecated. However, this required a manual user disable of the module to remove the camera registration, so we include the disable code here to be able to clean up.
|
||||
void clean_video_conference();
|
||||
|
||||
50
src/common/utils/color.cpp
Normal file
50
src/common/utils/color.cpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#include "pch.h"
|
||||
#include "color.h"
|
||||
|
||||
bool checkValidRGB(std::wstring_view hex, uint8_t* R, uint8_t* G, uint8_t* B)
|
||||
{
|
||||
if (hex.length() != 7)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
hex = hex.substr(1, 6); // remove #
|
||||
for (const auto& c : hex)
|
||||
{
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (swscanf_s(hex.data(), L"%2hhx%2hhx%2hhx", R, G, B) != 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool checkValidARGB(std::wstring_view hex, uint8_t* A, uint8_t* R, uint8_t* G, uint8_t* B)
|
||||
{
|
||||
if (hex.length() != 9)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
hex = hex.substr(1, 8); // remove #
|
||||
for (const auto& c : hex)
|
||||
{
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (swscanf_s(hex.data(), L"%2hhx%2hhx%2hhx%2hhx", A, R, G, B) != 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,41 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
// helper function to get the RGB from a #FFFFFF string.
|
||||
inline bool checkValidRGB(std::wstring_view hex, uint8_t* R, uint8_t* G, uint8_t* B)
|
||||
{
|
||||
if (hex.length() != 7)
|
||||
return false;
|
||||
hex = hex.substr(1, 6); // remove #
|
||||
for (auto& c : hex)
|
||||
{
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (swscanf_s(hex.data(), L"%2hhx%2hhx%2hhx", R, G, B) != 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool checkValidRGB(std::wstring_view hex, uint8_t* R, uint8_t* G, uint8_t* B);
|
||||
|
||||
// helper function to get the ARGB from a #FFFFFFFF string.
|
||||
inline bool checkValidARGB(std::wstring_view hex, uint8_t* A, uint8_t* R, uint8_t* G, uint8_t* B)
|
||||
{
|
||||
if (hex.length() != 9)
|
||||
return false;
|
||||
hex = hex.substr(1, 8); // remove #
|
||||
for (auto& c : hex)
|
||||
{
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (swscanf_s(hex.data(), L"%2hhx%2hhx%2hhx%2hhx", A, R, G, B) != 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool checkValidARGB(std::wstring_view hex, uint8_t* A, uint8_t* R, uint8_t* G, uint8_t* B);
|
||||
|
||||
506
src/common/utils/elevation.cpp
Normal file
506
src/common/utils/elevation.cpp
Normal file
@@ -0,0 +1,506 @@
|
||||
#include "pch.h"
|
||||
#include "elevation.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::wstring GetErrorString(HRESULT handle)
|
||||
{
|
||||
_com_error err(handle);
|
||||
return err.ErrorMessage();
|
||||
}
|
||||
|
||||
bool FindDesktopFolderView(REFIID riid, void** ppv)
|
||||
{
|
||||
CComPtr<IShellWindows> spShellWindows;
|
||||
auto result = spShellWindows.CoCreateInstance(CLSID_ShellWindows);
|
||||
if (result != S_OK || spShellWindows == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to create instance. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComVariant vtLoc(CSIDL_DESKTOP);
|
||||
CComVariant vtEmpty;
|
||||
long lhwnd;
|
||||
CComPtr<IDispatch> spdisp;
|
||||
result = spShellWindows->FindWindowSW(&vtLoc, &vtEmpty, SWC_DESKTOP, &lhwnd, SWFO_NEEDDISPATCH, &spdisp);
|
||||
if (result != S_OK || spdisp == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to find the window. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IShellBrowser> spBrowser;
|
||||
result = CComQIPtr<IServiceProvider>(spdisp)->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&spBrowser));
|
||||
if (result != S_OK || spBrowser == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query service. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IShellView> spView;
|
||||
result = spBrowser->QueryActiveShellView(&spView);
|
||||
if (result != S_OK || spView == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query active shell window. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
result = spView->QueryInterface(riid, ppv);
|
||||
if (result != S_OK || ppv == nullptr || *ppv == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query interface. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GetDesktopAutomationObject(REFIID riid, void** ppv)
|
||||
{
|
||||
CComPtr<IShellView> spsv;
|
||||
|
||||
// Desktop may not be available on startup
|
||||
auto attempts = 5;
|
||||
for (auto i = 1; i <= attempts; i++)
|
||||
{
|
||||
if (FindDesktopFolderView(IID_PPV_ARGS(&spsv)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Logger::warn(L"FindDesktopFolderView() failed attempt {}", i);
|
||||
|
||||
if (i == attempts)
|
||||
{
|
||||
Logger::warn(L"FindDesktopFolderView() max attempts reached");
|
||||
return false;
|
||||
}
|
||||
Sleep(3000);
|
||||
}
|
||||
|
||||
CComPtr<IDispatch> spdispView;
|
||||
auto result = spsv->GetItemObject(SVGIO_BACKGROUND, IID_PPV_ARGS(&spdispView));
|
||||
if (result != S_OK || spdispView == nullptr)
|
||||
{
|
||||
Logger::warn(L"spsv->GetItemObject() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
return SUCCEEDED(spdispView->QueryInterface(riid, ppv));
|
||||
}
|
||||
|
||||
bool ShellExecuteFromExplorer(LPCWSTR file, LPCWSTR parameters, LPCWSTR directory)
|
||||
{
|
||||
CComPtr<IShellFolderViewDual> spFolderView;
|
||||
if (!GetDesktopAutomationObject(IID_PPV_ARGS(&spFolderView)))
|
||||
{
|
||||
Logger::warn(L"GetDesktopAutomationObject() failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IDispatch> spdispShell;
|
||||
auto result = spFolderView->get_Application(&spdispShell);
|
||||
if (result != S_OK || spdispShell == nullptr)
|
||||
{
|
||||
Logger::warn(L"spFolderView->get_Application() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComQIPtr<IShellDispatch2> shell(spdispShell);
|
||||
if (shell == nullptr)
|
||||
{
|
||||
Logger::warn(L"IShellDispatch2 is nullptr");
|
||||
return false;
|
||||
}
|
||||
|
||||
CComVariant args(parameters ? parameters : L"");
|
||||
CComVariant dir(directory ? directory : L"");
|
||||
CComVariant operation(L"open");
|
||||
CComVariant show(SW_SHOWNORMAL);
|
||||
result = shell->ShellExecute(CComBSTR(file), args, dir, operation, show);
|
||||
if (result != S_OK)
|
||||
{
|
||||
Logger::warn(L"IShellDispatch2::ShellExecute() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_process_elevated(const bool use_cached_value)
|
||||
{
|
||||
auto detection_func = []() {
|
||||
HANDLE token = nullptr;
|
||||
bool elevated = false;
|
||||
|
||||
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token))
|
||||
{
|
||||
TOKEN_ELEVATION elevation;
|
||||
DWORD size;
|
||||
if (GetTokenInformation(token, TokenElevation, &elevation, sizeof(elevation), &size))
|
||||
{
|
||||
elevated = (elevation.TokenIsElevated != 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (token)
|
||||
{
|
||||
CloseHandle(token);
|
||||
}
|
||||
|
||||
return elevated;
|
||||
};
|
||||
static const bool cached_value = detection_func();
|
||||
return use_cached_value ? cached_value : detection_func();
|
||||
}
|
||||
|
||||
bool drop_elevated_privileges()
|
||||
{
|
||||
HANDLE token = nullptr;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_DEFAULT | WRITE_OWNER, &token))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PSID medium_sid = NULL;
|
||||
if (!::ConvertStringSidToSid(SDDL_ML_MEDIUM, &medium_sid))
|
||||
{
|
||||
CloseHandle(token);
|
||||
return false;
|
||||
}
|
||||
|
||||
TOKEN_MANDATORY_LABEL tml = {};
|
||||
tml.Label.Attributes = SE_GROUP_INTEGRITY;
|
||||
tml.Label.Sid = medium_sid;
|
||||
DWORD dwLen = sizeof(TOKEN_MANDATORY_LABEL) + ::GetLengthSid(medium_sid);
|
||||
BOOL res = ::SetTokenInformation(token, TokenIntegrityLevel, &tml, dwLen);
|
||||
LocalFree(medium_sid);
|
||||
CloseHandle(token);
|
||||
return res == TRUE;
|
||||
}
|
||||
|
||||
HANDLE run_as_different_user(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir, const bool showWindow)
|
||||
{
|
||||
Logger::info(L"run_as_different_user with params={}", params);
|
||||
SHELLEXECUTEINFOW exec_info = { 0 };
|
||||
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
|
||||
exec_info.lpVerb = L"runAsUser";
|
||||
exec_info.lpFile = file.c_str();
|
||||
exec_info.lpParameters = params.c_str();
|
||||
exec_info.hwnd = 0;
|
||||
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
exec_info.lpDirectory = workingDir;
|
||||
exec_info.hInstApp = 0;
|
||||
exec_info.nShow = showWindow ? SW_SHOWDEFAULT : SW_HIDE; // may have limited effect
|
||||
return ShellExecuteExW(&exec_info) ? exec_info.hProcess : nullptr;
|
||||
}
|
||||
|
||||
HANDLE run_elevated(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir, const bool showWindow)
|
||||
{
|
||||
Logger::info(L"run_elevated with params={}", params);
|
||||
SHELLEXECUTEINFOW exec_info = { 0 };
|
||||
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
|
||||
exec_info.lpVerb = L"runas";
|
||||
exec_info.lpFile = file.c_str();
|
||||
exec_info.lpParameters = params.c_str();
|
||||
exec_info.hwnd = 0;
|
||||
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
exec_info.lpDirectory = workingDir;
|
||||
exec_info.hInstApp = 0;
|
||||
exec_info.nShow = showWindow ? SW_SHOWDEFAULT : SW_HIDE; // may have limited effect
|
||||
BOOL result = ShellExecuteExW(&exec_info);
|
||||
return result ? exec_info.hProcess : nullptr;
|
||||
}
|
||||
|
||||
bool run_non_elevated(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir, const bool showWindow)
|
||||
{
|
||||
Logger::info(L"run_non_elevated with params={}", params);
|
||||
auto executable_args = L"\"" + file + L"\"";
|
||||
if (!params.empty())
|
||||
{
|
||||
executable_args += L" " + params;
|
||||
}
|
||||
|
||||
HWND hwnd = GetShellWindow();
|
||||
if (!hwnd)
|
||||
{
|
||||
if (GetLastError() == ERROR_SUCCESS)
|
||||
{
|
||||
Logger::warn(L"GetShellWindow() returned null. Shell window is not available");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"GetShellWindow() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
DWORD pid;
|
||||
GetWindowThreadProcessId(hwnd, &pid);
|
||||
|
||||
winrt::handle process{ OpenProcess(PROCESS_CREATE_PROCESS, FALSE, pid) };
|
||||
if (!process)
|
||||
{
|
||||
Logger::error(L"OpenProcess() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
SIZE_T size = 0;
|
||||
InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
|
||||
auto pproc_buffer = std::make_unique<char[]>(size);
|
||||
auto pptal = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(pproc_buffer.get());
|
||||
if (!pptal)
|
||||
{
|
||||
Logger::error(L"pptal failed to initialize. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!InitializeProcThreadAttributeList(pptal, 1, 0, &size))
|
||||
{
|
||||
Logger::error(L"InitializeProcThreadAttributeList() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
HANDLE process_handle = process.get();
|
||||
if (!UpdateProcThreadAttribute(pptal,
|
||||
0,
|
||||
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
|
||||
&process_handle,
|
||||
sizeof(process_handle),
|
||||
nullptr,
|
||||
nullptr))
|
||||
{
|
||||
Logger::error(L"UpdateProcThreadAttribute() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
STARTUPINFOEX siex = { 0 };
|
||||
siex.lpAttributeList = pptal;
|
||||
siex.StartupInfo.cb = sizeof(siex);
|
||||
PROCESS_INFORMATION pi = { 0 };
|
||||
auto dwCreationFlags = EXTENDED_STARTUPINFO_PRESENT;
|
||||
|
||||
if (!showWindow)
|
||||
{
|
||||
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
|
||||
siex.StartupInfo.wShowWindow = SW_HIDE;
|
||||
dwCreationFlags = CREATE_NO_WINDOW;
|
||||
}
|
||||
|
||||
std::wstring cmdLine = executable_args;
|
||||
auto succeeded = CreateProcessW(file.c_str(),
|
||||
&cmdLine[0],
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
dwCreationFlags,
|
||||
nullptr,
|
||||
workingDir,
|
||||
&siex.StartupInfo,
|
||||
&pi);
|
||||
|
||||
if (succeeded)
|
||||
{
|
||||
if (returnPid)
|
||||
{
|
||||
*returnPid = pi.dwProcessId;
|
||||
}
|
||||
if (pi.hProcess)
|
||||
{
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
if (pi.hThread)
|
||||
{
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
}
|
||||
|
||||
DeleteProcThreadAttributeList(pptal);
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
bool RunNonElevatedEx(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir)
|
||||
{
|
||||
bool success = false;
|
||||
HRESULT co_init = E_FAIL;
|
||||
try
|
||||
{
|
||||
co_init = CoInitialize(nullptr);
|
||||
success = ShellExecuteFromExplorer(file.c_str(), params.c_str(), working_dir.c_str());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
if (SUCCEEDED(co_init))
|
||||
{
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
std::optional<ProcessInfo> RunNonElevatedFailsafe(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir, DWORD handleAccess)
|
||||
{
|
||||
bool launched = RunNonElevatedEx(file, params, working_dir);
|
||||
if (!launched)
|
||||
{
|
||||
Logger::warn(L"RunNonElevatedEx() failed. Trying fallback");
|
||||
std::wstring action_runner_path = get_module_folderpath() + L"\\PowerToys.ActionRunner.exe";
|
||||
std::wstring newParams = L"-run-non-elevated -target \"" + file + L"\" " + params;
|
||||
launched = run_non_elevated(action_runner_path, newParams, nullptr, working_dir.c_str());
|
||||
if (launched)
|
||||
{
|
||||
Logger::trace(L"Started {}", file);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Failed to start {}", file);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
auto handles = getProcessHandlesByName(std::filesystem::path{ file }.filename().wstring(), PROCESS_QUERY_INFORMATION | SYNCHRONIZE | handleAccess);
|
||||
if (handles.empty())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ProcessInfo result;
|
||||
result.processID = GetProcessId(handles[0].get());
|
||||
result.processHandle = std::move(handles[0]);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool run_same_elevation(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir)
|
||||
{
|
||||
auto executable_args = L"\"" + file + L"\"";
|
||||
if (!params.empty())
|
||||
{
|
||||
executable_args += L" " + params;
|
||||
}
|
||||
|
||||
STARTUPINFO si = { sizeof(STARTUPINFO) };
|
||||
PROCESS_INFORMATION pi = { 0 };
|
||||
|
||||
auto succeeded = CreateProcessW(file.c_str(),
|
||||
&executable_args[0],
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
0,
|
||||
nullptr,
|
||||
workingDir,
|
||||
&si,
|
||||
&pi);
|
||||
|
||||
if (succeeded)
|
||||
{
|
||||
if (pi.hProcess)
|
||||
{
|
||||
if (returnPid)
|
||||
{
|
||||
*returnPid = GetProcessId(pi.hProcess);
|
||||
}
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
if (pi.hThread)
|
||||
{
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
bool check_user_is_admin()
|
||||
{
|
||||
auto freeMemory = [](PSID pSID, PTOKEN_GROUPS pGroupInfo) {
|
||||
if (pSID)
|
||||
{
|
||||
FreeSid(pSID);
|
||||
}
|
||||
if (pGroupInfo)
|
||||
{
|
||||
GlobalFree(pGroupInfo);
|
||||
}
|
||||
};
|
||||
|
||||
HANDLE hToken;
|
||||
DWORD dwSize = 0;
|
||||
PTOKEN_GROUPS pGroupInfo;
|
||||
SID_IDENTIFIER_AUTHORITY SIDAuth = SECURITY_NT_AUTHORITY;
|
||||
PSID pSID = NULL;
|
||||
|
||||
// Open a handle to the access token for the calling process.
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Call GetTokenInformation to get the buffer size.
|
||||
if (!GetTokenInformation(hToken, TokenGroups, NULL, dwSize, &dwSize))
|
||||
{
|
||||
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate the buffer.
|
||||
pGroupInfo = static_cast<PTOKEN_GROUPS>(GlobalAlloc(GPTR, dwSize));
|
||||
|
||||
// Call GetTokenInformation again to get the group information.
|
||||
if (!GetTokenInformation(hToken, TokenGroups, pGroupInfo, dwSize, &dwSize))
|
||||
{
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create a SID for the BUILTIN\\Administrators group.
|
||||
if (!AllocateAndInitializeSid(&SIDAuth, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &pSID))
|
||||
{
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Loop through the group SIDs looking for the administrator SID.
|
||||
for (DWORD i = 0; i < pGroupInfo->GroupCount; ++i)
|
||||
{
|
||||
if (EqualSid(pSID, pGroupInfo->Groups[i].Sid))
|
||||
{
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsProcessOfWindowElevated(HWND window)
|
||||
{
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
if (!pid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
wil::unique_handle hProcess{ OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
FALSE,
|
||||
pid) };
|
||||
|
||||
wil::unique_handle token;
|
||||
|
||||
if (OpenProcessToken(hProcess.get(), TOKEN_QUERY, &token))
|
||||
{
|
||||
TOKEN_ELEVATION elevation;
|
||||
DWORD size;
|
||||
if (GetTokenInformation(token.get(), TokenElevation, &elevation, sizeof(elevation), &size))
|
||||
{
|
||||
return elevation.TokenIsElevated != 0;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
@@ -14,6 +14,8 @@
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
|
||||
@@ -22,376 +24,23 @@
|
||||
#include <common/utils/process_path.h>
|
||||
#include <common/utils/processApi.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
inline std::wstring GetErrorString(HRESULT handle)
|
||||
{
|
||||
_com_error err(handle);
|
||||
return err.ErrorMessage();
|
||||
}
|
||||
|
||||
inline bool FindDesktopFolderView(REFIID riid, void** ppv)
|
||||
{
|
||||
CComPtr<IShellWindows> spShellWindows;
|
||||
auto result = spShellWindows.CoCreateInstance(CLSID_ShellWindows);
|
||||
if (result != S_OK || spShellWindows == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to create instance. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComVariant vtLoc(CSIDL_DESKTOP);
|
||||
CComVariant vtEmpty;
|
||||
long lhwnd;
|
||||
CComPtr<IDispatch> spdisp;
|
||||
result = spShellWindows->FindWindowSW(
|
||||
&vtLoc, &vtEmpty, SWC_DESKTOP, &lhwnd, SWFO_NEEDDISPATCH, &spdisp);
|
||||
|
||||
if (result != S_OK || spdisp == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to find the window. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IShellBrowser> spBrowser;
|
||||
result = CComQIPtr<IServiceProvider>(spdisp)->QueryService(SID_STopLevelBrowser,
|
||||
IID_PPV_ARGS(&spBrowser));
|
||||
if (result != S_OK || spBrowser == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query service. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IShellView> spView;
|
||||
result = spBrowser->QueryActiveShellView(&spView);
|
||||
if (result != S_OK || spView == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query active shell window. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
result = spView->QueryInterface(riid, ppv);
|
||||
if (result != S_OK || ppv == nullptr || *ppv == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query interface. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool GetDesktopAutomationObject(REFIID riid, void** ppv)
|
||||
{
|
||||
CComPtr<IShellView> spsv;
|
||||
|
||||
// Desktop may not be available on startup
|
||||
auto attempts = 5;
|
||||
for (auto i = 1; i <= attempts; i++)
|
||||
{
|
||||
if (FindDesktopFolderView(IID_PPV_ARGS(&spsv)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Logger::warn(L"FindDesktopFolderView() failed attempt {}", i);
|
||||
|
||||
if (i == attempts)
|
||||
{
|
||||
Logger::warn(L"FindDesktopFolderView() max attempts reached");
|
||||
return false;
|
||||
}
|
||||
|
||||
Sleep(3000);
|
||||
}
|
||||
|
||||
CComPtr<IDispatch> spdispView;
|
||||
auto result = spsv->GetItemObject(SVGIO_BACKGROUND, IID_PPV_ARGS(&spdispView));
|
||||
if (result != S_OK)
|
||||
{
|
||||
Logger::warn(L"GetItemObject() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
result = spdispView->QueryInterface(riid, ppv);
|
||||
if (result != S_OK)
|
||||
{
|
||||
Logger::warn(L"QueryInterface() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ShellExecuteFromExplorer(
|
||||
PCWSTR pszFile,
|
||||
PCWSTR pszParameters = nullptr,
|
||||
PCWSTR workingDir = L"")
|
||||
{
|
||||
CComPtr<IShellFolderViewDual> spFolderView;
|
||||
if (!GetDesktopAutomationObject(IID_PPV_ARGS(&spFolderView)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IDispatch> spdispShell;
|
||||
auto result = spFolderView->get_Application(&spdispShell);
|
||||
if (result != S_OK)
|
||||
{
|
||||
Logger::warn(L"get_Application() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComQIPtr<IShellDispatch2>(spdispShell)
|
||||
->ShellExecuteW(CComBSTR(pszFile),
|
||||
CComVariant(pszParameters ? pszParameters : L""),
|
||||
CComVariant(workingDir),
|
||||
CComVariant(L""),
|
||||
CComVariant(SW_SHOWNORMAL));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if the current process is running with elevated privileges
|
||||
inline bool is_process_elevated(const bool use_cached_value = true)
|
||||
{
|
||||
auto detection_func = []() {
|
||||
HANDLE token = nullptr;
|
||||
bool elevated = false;
|
||||
|
||||
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token))
|
||||
{
|
||||
TOKEN_ELEVATION elevation;
|
||||
DWORD size;
|
||||
if (GetTokenInformation(token, TokenElevation, &elevation, sizeof(elevation), &size))
|
||||
{
|
||||
elevated = (elevation.TokenIsElevated != 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (token)
|
||||
{
|
||||
CloseHandle(token);
|
||||
}
|
||||
|
||||
return elevated;
|
||||
};
|
||||
static const bool cached_value = detection_func();
|
||||
return use_cached_value ? cached_value : detection_func();
|
||||
}
|
||||
bool is_process_elevated(const bool use_cached_value = true);
|
||||
|
||||
// Drops the elevated privileges if present
|
||||
inline bool drop_elevated_privileges()
|
||||
{
|
||||
HANDLE token = nullptr;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_DEFAULT | WRITE_OWNER, &token))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool drop_elevated_privileges();
|
||||
|
||||
PSID medium_sid = NULL;
|
||||
if (!::ConvertStringSidToSid(SDDL_ML_MEDIUM, &medium_sid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Run command as different user, returns process handle or null on failure
|
||||
HANDLE run_as_different_user(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true);
|
||||
|
||||
TOKEN_MANDATORY_LABEL label = { 0 };
|
||||
label.Label.Attributes = SE_GROUP_INTEGRITY;
|
||||
label.Label.Sid = medium_sid;
|
||||
DWORD size = static_cast<DWORD>(sizeof(TOKEN_MANDATORY_LABEL) + ::GetLengthSid(medium_sid));
|
||||
|
||||
BOOL result = SetTokenInformation(token, TokenIntegrityLevel, &label, size);
|
||||
LocalFree(medium_sid);
|
||||
CloseHandle(token);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Run command as different user, returns true if succeeded
|
||||
inline HANDLE run_as_different_user(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true)
|
||||
{
|
||||
Logger::info(L"run_elevated with params={}", params);
|
||||
SHELLEXECUTEINFOW exec_info = { 0 };
|
||||
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
|
||||
exec_info.lpVerb = L"runAsUser";
|
||||
exec_info.lpFile = file.c_str();
|
||||
exec_info.lpParameters = params.c_str();
|
||||
exec_info.hwnd = 0;
|
||||
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
exec_info.lpDirectory = workingDir;
|
||||
exec_info.hInstApp = 0;
|
||||
if (showWindow)
|
||||
{
|
||||
exec_info.nShow = SW_SHOWDEFAULT;
|
||||
}
|
||||
else
|
||||
{
|
||||
// might have limited success, but only option with ShellExecuteExW
|
||||
exec_info.nShow = SW_HIDE;
|
||||
}
|
||||
|
||||
return ShellExecuteExW(&exec_info) ? exec_info.hProcess : nullptr;
|
||||
}
|
||||
|
||||
// Run command as elevated user, returns true if succeeded
|
||||
inline HANDLE run_elevated(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true)
|
||||
{
|
||||
Logger::info(L"run_elevated with params={}", params);
|
||||
SHELLEXECUTEINFOW exec_info = { 0 };
|
||||
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
|
||||
exec_info.lpVerb = L"runas";
|
||||
exec_info.lpFile = file.c_str();
|
||||
exec_info.lpParameters = params.c_str();
|
||||
exec_info.hwnd = 0;
|
||||
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
exec_info.lpDirectory = workingDir;
|
||||
exec_info.hInstApp = 0;
|
||||
|
||||
if (showWindow)
|
||||
{
|
||||
exec_info.nShow = SW_SHOWDEFAULT;
|
||||
}
|
||||
else
|
||||
{
|
||||
// might have limited success, but only option with ShellExecuteExW
|
||||
exec_info.nShow = SW_HIDE;
|
||||
}
|
||||
|
||||
BOOL result = ShellExecuteExW(&exec_info);
|
||||
|
||||
return result ? exec_info.hProcess : nullptr;
|
||||
}
|
||||
// Run command as elevated user, returns process handle or null on failure
|
||||
HANDLE run_elevated(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true);
|
||||
|
||||
// Run command as non-elevated user, returns true if succeeded, puts the process id into returnPid if returnPid != NULL
|
||||
inline bool run_non_elevated(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr, const bool showWindow = true)
|
||||
{
|
||||
Logger::info(L"run_non_elevated with params={}", params);
|
||||
auto executable_args = L"\"" + file + L"\"";
|
||||
if (!params.empty())
|
||||
{
|
||||
executable_args += L" " + params;
|
||||
}
|
||||
bool run_non_elevated(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr, const bool showWindow = true);
|
||||
|
||||
HWND hwnd = GetShellWindow();
|
||||
if (!hwnd)
|
||||
{
|
||||
if (GetLastError() == ERROR_SUCCESS)
|
||||
{
|
||||
Logger::warn(L"GetShellWindow() returned null. Shell window is not available");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"GetShellWindow() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
DWORD pid;
|
||||
GetWindowThreadProcessId(hwnd, &pid);
|
||||
|
||||
winrt::handle process{ OpenProcess(PROCESS_CREATE_PROCESS, FALSE, pid) };
|
||||
if (!process)
|
||||
{
|
||||
Logger::error(L"OpenProcess() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
SIZE_T size = 0;
|
||||
|
||||
InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
|
||||
auto pproc_buffer = std::make_unique<char[]>(size);
|
||||
auto pptal = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(pproc_buffer.get());
|
||||
if (!pptal)
|
||||
{
|
||||
Logger::error(L"pptal failed to initialize. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!InitializeProcThreadAttributeList(pptal, 1, 0, &size))
|
||||
{
|
||||
Logger::error(L"InitializeProcThreadAttributeList() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
HANDLE process_handle = process.get();
|
||||
if (!UpdateProcThreadAttribute(pptal,
|
||||
0,
|
||||
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
|
||||
&process_handle,
|
||||
sizeof(process_handle),
|
||||
nullptr,
|
||||
nullptr))
|
||||
{
|
||||
Logger::error(L"UpdateProcThreadAttribute() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
STARTUPINFOEX siex = { 0 };
|
||||
siex.lpAttributeList = pptal;
|
||||
siex.StartupInfo.cb = sizeof(siex);
|
||||
PROCESS_INFORMATION pi = { 0 };
|
||||
auto dwCreationFlags = EXTENDED_STARTUPINFO_PRESENT;
|
||||
|
||||
if (!showWindow)
|
||||
{
|
||||
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
|
||||
siex.StartupInfo.wShowWindow = SW_HIDE;
|
||||
dwCreationFlags = CREATE_NO_WINDOW;
|
||||
}
|
||||
|
||||
auto succeeded = CreateProcessW(file.c_str(),
|
||||
&executable_args[0],
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
dwCreationFlags,
|
||||
nullptr,
|
||||
workingDir,
|
||||
&siex.StartupInfo,
|
||||
&pi);
|
||||
if (succeeded)
|
||||
{
|
||||
if (pi.hProcess)
|
||||
{
|
||||
if (returnPid)
|
||||
{
|
||||
*returnPid = GetProcessId(pi.hProcess);
|
||||
}
|
||||
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
if (pi.hThread)
|
||||
{
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"CreateProcessW() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
inline bool RunNonElevatedEx(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir)
|
||||
{
|
||||
bool success = false;
|
||||
HRESULT co_init = E_FAIL;
|
||||
try
|
||||
{
|
||||
co_init = CoInitialize(nullptr);
|
||||
success = ShellExecuteFromExplorer(file.c_str(), params.c_str(), working_dir.c_str());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
if (SUCCEEDED(co_init))
|
||||
{
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
// Try running through the shell's automation object
|
||||
bool RunNonElevatedEx(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir);
|
||||
|
||||
struct ProcessInfo
|
||||
{
|
||||
@@ -399,172 +48,16 @@ struct ProcessInfo
|
||||
DWORD processID = {};
|
||||
};
|
||||
|
||||
inline std::optional<ProcessInfo> RunNonElevatedFailsafe(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir, DWORD handleAccess = 0)
|
||||
{
|
||||
bool launched = RunNonElevatedEx(file, params, working_dir);
|
||||
if (!launched)
|
||||
{
|
||||
Logger::warn(L"RunNonElevatedEx() failed. Trying fallback");
|
||||
std::wstring action_runner_path = get_module_folderpath() + L"\\PowerToys.ActionRunner.exe";
|
||||
std::wstring newParams = fmt::format(L"-run-non-elevated -target \"{}\" {}", file, params);
|
||||
launched = run_non_elevated(action_runner_path, newParams, nullptr, working_dir.c_str());
|
||||
if (launched)
|
||||
{
|
||||
Logger::trace(L"Started {}", file);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Failed to start {}", file);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
auto handles = getProcessHandlesByName(std::filesystem::path{ file }.filename().wstring(), PROCESS_QUERY_INFORMATION | SYNCHRONIZE | handleAccess);
|
||||
|
||||
if (handles.empty())
|
||||
return std::nullopt;
|
||||
|
||||
ProcessInfo result;
|
||||
result.processID = GetProcessId(handles[0].get());
|
||||
result.processHandle = std::move(handles[0]);
|
||||
|
||||
return result;
|
||||
}
|
||||
// Fallback to ActionRunner when shell route fails and return process info if available
|
||||
std::optional<ProcessInfo> RunNonElevatedFailsafe(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir, DWORD handleAccess = 0);
|
||||
|
||||
// Run command with the same elevation, returns true if succeeded
|
||||
inline bool run_same_elevation(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr)
|
||||
{
|
||||
auto executable_args = L"\"" + file + L"\"";
|
||||
if (!params.empty())
|
||||
{
|
||||
executable_args += L" " + params;
|
||||
}
|
||||
|
||||
STARTUPINFO si = { sizeof(STARTUPINFO) };
|
||||
PROCESS_INFORMATION pi = { 0 };
|
||||
|
||||
auto succeeded = CreateProcessW(file.c_str(),
|
||||
&executable_args[0],
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
0,
|
||||
nullptr,
|
||||
workingDir,
|
||||
&si,
|
||||
&pi);
|
||||
|
||||
if (succeeded)
|
||||
{
|
||||
if (pi.hProcess)
|
||||
{
|
||||
if (returnPid)
|
||||
{
|
||||
*returnPid = GetProcessId(pi.hProcess);
|
||||
}
|
||||
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
|
||||
if (pi.hThread)
|
||||
{
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
bool run_same_elevation(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr);
|
||||
|
||||
// Returns true if the current process is running from administrator account
|
||||
// The function returns true in case of error since we want to return false
|
||||
// only in case of a positive verification that the user is not an admin.
|
||||
inline bool check_user_is_admin()
|
||||
{
|
||||
auto freeMemory = [](PSID pSID, PTOKEN_GROUPS pGroupInfo) {
|
||||
if (pSID)
|
||||
{
|
||||
FreeSid(pSID);
|
||||
}
|
||||
if (pGroupInfo)
|
||||
{
|
||||
GlobalFree(pGroupInfo);
|
||||
}
|
||||
};
|
||||
bool check_user_is_admin();
|
||||
|
||||
HANDLE hToken;
|
||||
DWORD dwSize = 0;
|
||||
PTOKEN_GROUPS pGroupInfo;
|
||||
SID_IDENTIFIER_AUTHORITY SIDAuth = SECURITY_NT_AUTHORITY;
|
||||
PSID pSID = NULL;
|
||||
bool IsProcessOfWindowElevated(HWND window);
|
||||
|
||||
// Open a handle to the access token for the calling process.
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Call GetTokenInformation to get the buffer size.
|
||||
if (!GetTokenInformation(hToken, TokenGroups, NULL, dwSize, &dwSize))
|
||||
{
|
||||
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate the buffer.
|
||||
pGroupInfo = static_cast<PTOKEN_GROUPS>(GlobalAlloc(GPTR, dwSize));
|
||||
|
||||
// Call GetTokenInformation again to get the group information.
|
||||
if (!GetTokenInformation(hToken, TokenGroups, pGroupInfo, dwSize, &dwSize))
|
||||
{
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create a SID for the BUILTIN\Administrators group.
|
||||
if (!AllocateAndInitializeSid(&SIDAuth, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &pSID))
|
||||
{
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Loop through the group SIDs looking for the administrator SID.
|
||||
for (DWORD i = 0; i < pGroupInfo->GroupCount; ++i)
|
||||
{
|
||||
if (EqualSid(pSID, pGroupInfo->Groups[i].Sid))
|
||||
{
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool IsProcessOfWindowElevated(HWND window)
|
||||
{
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
if (!pid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
wil::unique_handle hProcess{ OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
FALSE,
|
||||
pid) };
|
||||
|
||||
wil::unique_handle token;
|
||||
|
||||
if (OpenProcessToken(hProcess.get(), TOKEN_QUERY, &token))
|
||||
{
|
||||
TOKEN_ELEVATION elevation;
|
||||
DWORD size;
|
||||
if (GetTokenInformation(token.get(), TokenElevation, &elevation, sizeof(elevation), &size))
|
||||
{
|
||||
return elevation.TokenIsElevated != 0;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
63
src/common/utils/excluded_apps.cpp
Normal file
63
src/common/utils/excluded_apps.cpp
Normal file
@@ -0,0 +1,63 @@
|
||||
#include "pch.h"
|
||||
#include "excluded_apps.h"
|
||||
|
||||
bool find_app_name_in_path(const std::wstring& where, const std::vector<std::wstring>& what)
|
||||
{
|
||||
for (const auto& row : what)
|
||||
{
|
||||
const auto pos = where.rfind(row);
|
||||
const auto last_slash = where.rfind('\\');
|
||||
// Check that row occurs in where, and its last occurrence contains the first character after the last backslash.
|
||||
if (pos != std::wstring::npos && pos <= last_slash + 1 && pos + row.length() > last_slash)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool find_folder_in_path(const std::wstring& where, const std::vector<std::wstring>& what)
|
||||
{
|
||||
for (const auto& row : what)
|
||||
{
|
||||
if (where.rfind(row) != std::wstring::npos)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool check_excluded_app_with_title(const HWND& hwnd, const std::vector<std::wstring>& excludedApps)
|
||||
{
|
||||
WCHAR title[MAX_TITLE_LENGTH];
|
||||
const int len = GetWindowTextW(hwnd, title, MAX_TITLE_LENGTH);
|
||||
if (len <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring titleStr(title);
|
||||
CharUpperBuffW(titleStr.data(), static_cast<DWORD>(titleStr.length()));
|
||||
|
||||
for (const auto& app : excludedApps)
|
||||
{
|
||||
if (titleStr.contains(app))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool check_excluded_app(const HWND& hwnd, const std::wstring& processPath, const std::vector<std::wstring>& excludedApps)
|
||||
{
|
||||
bool res = find_app_name_in_path(processPath, excludedApps);
|
||||
|
||||
if (!res)
|
||||
{
|
||||
res = check_excluded_app_with_title(hwnd, excludedApps);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -1,67 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <windows.h>
|
||||
|
||||
// Checks if a process path is included in a list of strings.
|
||||
inline bool find_app_name_in_path(const std::wstring& where, const std::vector<std::wstring>& what)
|
||||
{
|
||||
for (const auto& row : what)
|
||||
{
|
||||
const auto pos = where.rfind(row);
|
||||
const auto last_slash = where.rfind('\\');
|
||||
//Check that row occurs in where, and its last occurrence contains in itself the first character after the last backslash.
|
||||
if (pos != std::wstring::npos && pos <= last_slash + 1 && pos + row.length() > last_slash)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool find_app_name_in_path(const std::wstring& where, const std::vector<std::wstring>& what);
|
||||
|
||||
inline bool find_folder_in_path(const std::wstring& where, const std::vector<std::wstring>& what)
|
||||
{
|
||||
for (const auto& row : what)
|
||||
{
|
||||
const auto pos = where.rfind(row);
|
||||
if (pos != std::wstring::npos)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool find_folder_in_path(const std::wstring& where, const std::vector<std::wstring>& what);
|
||||
|
||||
#define MAX_TITLE_LENGTH 255
|
||||
inline bool check_excluded_app_with_title(const HWND& hwnd, const std::vector<std::wstring>& excludedApps)
|
||||
{
|
||||
WCHAR title[MAX_TITLE_LENGTH];
|
||||
int len = GetWindowTextW(hwnd, title, MAX_TITLE_LENGTH);
|
||||
if (len <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool check_excluded_app_with_title(const HWND& hwnd, const std::vector<std::wstring>& excludedApps);
|
||||
|
||||
std::wstring titleStr(title);
|
||||
CharUpperBuffW(titleStr.data(), static_cast<DWORD>(titleStr.length()));
|
||||
|
||||
for (const auto& app : excludedApps)
|
||||
{
|
||||
if (titleStr.contains(app))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool check_excluded_app(const HWND& hwnd, const std::wstring& processPath, const std::vector<std::wstring>& excludedApps)
|
||||
{
|
||||
bool res = find_app_name_in_path(processPath, excludedApps);
|
||||
|
||||
if (!res)
|
||||
{
|
||||
res = check_excluded_app_with_title(hwnd, excludedApps);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
bool check_excluded_app(const HWND& hwnd, const std::wstring& processPath, const std::vector<std::wstring>& excludedApps);
|
||||
|
||||
100
src/common/utils/exec.cpp
Normal file
100
src/common/utils/exec.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
#include "pch.h"
|
||||
#include "exec.h"
|
||||
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
|
||||
std::optional<std::string> exec_and_read_output(const std::wstring_view command, DWORD timeout_ms)
|
||||
{
|
||||
SECURITY_ATTRIBUTES saAttr{ sizeof(saAttr) };
|
||||
saAttr.bInheritHandle = false;
|
||||
|
||||
constexpr size_t bufferSize = 4096;
|
||||
// We must use a named pipe for async I/O
|
||||
char pipename[MAX_PATH + 1];
|
||||
if (!GetTempFileNameA(R"(\\.\pipe\)", "tmp", 1, pipename))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
wil::unique_handle readPipe{
|
||||
CreateNamedPipeA(pipename, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, PIPE_UNLIMITED_INSTANCES, bufferSize, bufferSize, 0, &saAttr)
|
||||
};
|
||||
|
||||
saAttr.bInheritHandle = true;
|
||||
wil::unique_handle writePipe{
|
||||
CreateFileA(pipename, GENERIC_WRITE, 0, &saAttr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)
|
||||
};
|
||||
|
||||
if (!readPipe || !writePipe)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
PROCESS_INFORMATION piProcInfo{};
|
||||
STARTUPINFOW siStartInfo{ sizeof(siStartInfo) };
|
||||
|
||||
siStartInfo.hStdError = writePipe.get();
|
||||
siStartInfo.hStdOutput = writePipe.get();
|
||||
siStartInfo.dwFlags |= STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
|
||||
siStartInfo.wShowWindow = SW_HIDE;
|
||||
|
||||
std::wstring cmdLine{ command };
|
||||
if (!CreateProcessW(nullptr,
|
||||
cmdLine.data(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
true,
|
||||
NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE,
|
||||
nullptr,
|
||||
nullptr,
|
||||
&siStartInfo,
|
||||
&piProcInfo))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
// Child process inherited the write end of the pipe, we can close it now
|
||||
writePipe.reset();
|
||||
|
||||
auto closeProcessHandles = wil::scope_exit([&] {
|
||||
CloseHandle(piProcInfo.hThread);
|
||||
CloseHandle(piProcInfo.hProcess);
|
||||
});
|
||||
|
||||
std::string childOutput;
|
||||
bool processExited = false;
|
||||
for (;;)
|
||||
{
|
||||
char buffer[bufferSize];
|
||||
DWORD gotBytes = 0;
|
||||
wil::unique_handle ioEvent{ CreateEventW(nullptr, true, false, nullptr) };
|
||||
OVERLAPPED overlapped{ .hEvent = ioEvent.get() };
|
||||
ReadFile(readPipe.get(), buffer, sizeof(buffer), nullptr, &overlapped);
|
||||
|
||||
const std::array<HANDLE, 2> handlesToWait = { overlapped.hEvent, piProcInfo.hProcess };
|
||||
switch (WaitForMultipleObjects(1 + !processExited, handlesToWait.data(), false, timeout_ms))
|
||||
{
|
||||
case WAIT_OBJECT_0 + 1:
|
||||
if (!processExited)
|
||||
{
|
||||
// When the process exits, we can reduce timeout and read the rest of the output w/o possibly big timeout
|
||||
timeout_ms = 1000;
|
||||
processExited = true;
|
||||
closeProcessHandles.reset();
|
||||
}
|
||||
[[fallthrough]];
|
||||
case WAIT_OBJECT_0:
|
||||
if (GetOverlappedResultEx(readPipe.get(), &overlapped, &gotBytes, timeout_ms, true))
|
||||
{
|
||||
childOutput += std::string_view{ buffer, gotBytes };
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
default:
|
||||
goto exit;
|
||||
}
|
||||
}
|
||||
exit:
|
||||
CancelIo(readPipe.get());
|
||||
return childOutput;
|
||||
}
|
||||
@@ -15,94 +15,4 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
inline std::optional<std::string> exec_and_read_output(const std::wstring_view command, DWORD timeout_ms = 30000)
|
||||
{
|
||||
SECURITY_ATTRIBUTES saAttr{ sizeof(saAttr) };
|
||||
saAttr.bInheritHandle = false;
|
||||
|
||||
constexpr size_t bufferSize = 4096;
|
||||
// We must use a named pipe for async I/O
|
||||
char pipename[MAX_PATH + 1];
|
||||
if (!GetTempFileNameA(R"(\\.\pipe\)", "tmp", 1, pipename))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
wil::unique_handle readPipe{ CreateNamedPipeA(pipename, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, PIPE_UNLIMITED_INSTANCES, bufferSize, bufferSize, 0, &saAttr) };
|
||||
|
||||
saAttr.bInheritHandle = true;
|
||||
wil::unique_handle writePipe{ CreateFileA(pipename, GENERIC_WRITE, 0, &saAttr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) };
|
||||
|
||||
if (!readPipe || !writePipe)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
PROCESS_INFORMATION piProcInfo{};
|
||||
STARTUPINFOW siStartInfo{ sizeof(siStartInfo) };
|
||||
|
||||
siStartInfo.hStdError = writePipe.get();
|
||||
siStartInfo.hStdOutput = writePipe.get();
|
||||
siStartInfo.dwFlags |= STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
|
||||
siStartInfo.wShowWindow = SW_HIDE;
|
||||
|
||||
std::wstring cmdLine{ command };
|
||||
if (!CreateProcessW(nullptr,
|
||||
cmdLine.data(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
true,
|
||||
NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE,
|
||||
nullptr,
|
||||
nullptr,
|
||||
&siStartInfo,
|
||||
&piProcInfo))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
// Child process inherited the write end of the pipe, we can close it now
|
||||
writePipe.reset();
|
||||
|
||||
auto closeProcessHandles = wil::scope_exit([&] {
|
||||
CloseHandle(piProcInfo.hThread);
|
||||
CloseHandle(piProcInfo.hProcess);
|
||||
});
|
||||
|
||||
std::string childOutput;
|
||||
bool processExited = false;
|
||||
for (;;)
|
||||
{
|
||||
char buffer[bufferSize];
|
||||
DWORD gotBytes = 0;
|
||||
wil::unique_handle IOEvent{ CreateEventW(nullptr, true, false, nullptr) };
|
||||
OVERLAPPED overlapped{ .hEvent = IOEvent.get() };
|
||||
ReadFile(readPipe.get(), buffer, sizeof(buffer), nullptr, &overlapped);
|
||||
|
||||
const std::array<HANDLE, 2> handlesToWait = { overlapped.hEvent, piProcInfo.hProcess };
|
||||
switch (WaitForMultipleObjects(1 + !processExited, handlesToWait.data(), false, timeout_ms))
|
||||
{
|
||||
case WAIT_OBJECT_0 + 1:
|
||||
if (!processExited)
|
||||
{
|
||||
// When the process exits, we can reduce timeout and read the rest of the output w/o possibly big timeout
|
||||
timeout_ms = 1000;
|
||||
processExited = true;
|
||||
closeProcessHandles.reset();
|
||||
}
|
||||
[[fallthrough]];
|
||||
case WAIT_OBJECT_0:
|
||||
if (GetOverlappedResultEx(readPipe.get(), &overlapped, &gotBytes, timeout_ms, true))
|
||||
{
|
||||
childOutput += std::string_view{ buffer, gotBytes };
|
||||
break;
|
||||
}
|
||||
// Timeout
|
||||
[[fallthrough]];
|
||||
default:
|
||||
goto exit;
|
||||
}
|
||||
}
|
||||
exit:
|
||||
CancelIo(readPipe.get());
|
||||
return childOutput;
|
||||
}
|
||||
std::optional<std::string> exec_and_read_output(const std::wstring_view command, DWORD timeout_ms = 30000);
|
||||
|
||||
12
src/common/utils/game_mode.cpp
Normal file
12
src/common/utils/game_mode.cpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#include "pch.h"
|
||||
#include "game_mode.h"
|
||||
|
||||
bool detect_game_mode()
|
||||
{
|
||||
QUERY_USER_NOTIFICATION_STATE notification_state;
|
||||
if (SHQueryUserNotificationState(¬ification_state) != S_OK)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return notification_state == QUNS_RUNNING_D3D_FULL_SCREEN;
|
||||
}
|
||||
@@ -1,12 +1,4 @@
|
||||
#pragma once
|
||||
#include <shellapi.h>
|
||||
|
||||
inline bool detect_game_mode()
|
||||
{
|
||||
QUERY_USER_NOTIFICATION_STATE notification_state;
|
||||
if (SHQueryUserNotificationState(¬ification_state) != S_OK)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return (notification_state == QUNS_RUNNING_D3D_FULL_SCREEN);
|
||||
}
|
||||
bool detect_game_mode();
|
||||
|
||||
236
src/common/utils/gpo.cpp
Normal file
236
src/common/utils/gpo.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
#include "pch.h"
|
||||
#include "gpo.h"
|
||||
|
||||
namespace powertoys_gpo
|
||||
{
|
||||
std::optional<std::wstring> readRegistryStringValue(HKEY hRootKey, const std::wstring& subKey, const std::wstring& value_name, const bool is_multi_line_text)
|
||||
{
|
||||
// Set value type
|
||||
DWORD reg_value_type = REG_SZ;
|
||||
DWORD reg_flags = RRF_RT_REG_SZ;
|
||||
if (is_multi_line_text)
|
||||
{
|
||||
reg_value_type = REG_MULTI_SZ;
|
||||
reg_flags = RRF_RT_REG_MULTI_SZ;
|
||||
}
|
||||
|
||||
DWORD string_buffer_capacity;
|
||||
// Request required buffer capacity / string length
|
||||
if (RegGetValueW(hRootKey, subKey.c_str(), value_name.c_str(), reg_flags, ®_value_type, NULL, &string_buffer_capacity) != ERROR_SUCCESS)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
else if (string_buffer_capacity == 0)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// RegGetValueW overshoots sometimes. Use a buffer first to not have characters past the string end.
|
||||
wchar_t* temp_buffer = new wchar_t[string_buffer_capacity / sizeof(wchar_t) + 1];
|
||||
// Read string
|
||||
if (RegGetValueW(hRootKey, subKey.c_str(), value_name.c_str(), reg_flags, ®_value_type, temp_buffer, &string_buffer_capacity) != ERROR_SUCCESS)
|
||||
{
|
||||
delete[] temp_buffer;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Convert buffer to std::wstring
|
||||
std::wstring string_value = L"";
|
||||
if (reg_value_type == REG_MULTI_SZ)
|
||||
{
|
||||
// If it is REG_MULTI_SZ handle this way
|
||||
wchar_t* currentString = temp_buffer;
|
||||
while (*currentString != L'\0')
|
||||
{
|
||||
// If first entry then assign the string, else add to the string
|
||||
string_value = (string_value == L"") ? currentString : (string_value + L"\r\n" + currentString);
|
||||
currentString += wcslen(currentString) + 1; // Move to the next string
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If it is REG_SZ handle this way
|
||||
string_value = temp_buffer;
|
||||
}
|
||||
|
||||
// delete buffer, return string value
|
||||
delete[] temp_buffer;
|
||||
return string_value;
|
||||
}
|
||||
|
||||
gpo_rule_configured_t getConfiguredValue(const std::wstring& registry_value_name)
|
||||
{
|
||||
HKEY key{};
|
||||
DWORD value = 0xFFFFFFFE;
|
||||
DWORD valueSize = sizeof(value);
|
||||
|
||||
bool machine_key_found = true;
|
||||
if (auto res = RegOpenKeyExW(POLICIES_SCOPE_MACHINE, POLICIES_PATH.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
machine_key_found = false;
|
||||
}
|
||||
|
||||
if (machine_key_found)
|
||||
{
|
||||
// If the path was found in the machine, we need to check if the value for the policy exists.
|
||||
auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &valueSize);
|
||||
|
||||
RegCloseKey(key);
|
||||
|
||||
if (res != ERROR_SUCCESS)
|
||||
{
|
||||
// Value not found on the path.
|
||||
machine_key_found = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!machine_key_found)
|
||||
{
|
||||
// If there's no value found on the machine scope, try to get it from the user scope.
|
||||
if (auto res = RegOpenKeyExW(POLICIES_SCOPE_USER, POLICIES_PATH.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
if (res == ERROR_FILE_NOT_FOUND)
|
||||
{
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
return gpo_rule_configured_unavailable;
|
||||
}
|
||||
auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &valueSize);
|
||||
RegCloseKey(key);
|
||||
|
||||
if (res != ERROR_SUCCESS)
|
||||
{
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case 0:
|
||||
return gpo_rule_configured_disabled;
|
||||
case 1:
|
||||
return gpo_rule_configured_enabled;
|
||||
default:
|
||||
return gpo_rule_configured_wrong_value;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::wstring> getPolicyListValue(const std::wstring& registry_list_path, const std::wstring& registry_list_value_name)
|
||||
{
|
||||
// This function returns the value of an entry of a policy list. The user scope is only checked, if the list is not enabled for the machine to not mix the lists.
|
||||
|
||||
HKEY key{};
|
||||
|
||||
// Try to read from the machine list.
|
||||
bool machine_list_found = false;
|
||||
if (RegOpenKeyExW(POLICIES_SCOPE_MACHINE, registry_list_path.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
machine_list_found = true;
|
||||
RegCloseKey(key);
|
||||
|
||||
// If the path exists in the machine registry, we try to read the value.
|
||||
auto regValueData = readRegistryStringValue(POLICIES_SCOPE_MACHINE, registry_list_path, registry_list_value_name);
|
||||
|
||||
if (regValueData.has_value())
|
||||
{
|
||||
// Return the value from the machine list.
|
||||
return *regValueData;
|
||||
}
|
||||
}
|
||||
|
||||
// If no list exists for machine, we try to read from the user list.
|
||||
if (!machine_list_found)
|
||||
{
|
||||
if (RegOpenKeyExW(POLICIES_SCOPE_USER, registry_list_path.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
RegCloseKey(key);
|
||||
|
||||
// If the path exists in the user registry, we try to read the value.
|
||||
auto regValueData = readRegistryStringValue(POLICIES_SCOPE_USER, registry_list_path, registry_list_value_name);
|
||||
|
||||
if (regValueData.has_value())
|
||||
{
|
||||
// Return the value from the user list.
|
||||
return *regValueData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No list exists for machine and user, or no value was found in the list, or an error ocurred while reading the value.
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
gpo_rule_configured_t getUtilityEnabledValue(const std::wstring& utility_name)
|
||||
{
|
||||
auto individual_value = getConfiguredValue(utility_name);
|
||||
|
||||
if (individual_value == gpo_rule_configured_disabled || individual_value == gpo_rule_configured_enabled)
|
||||
{
|
||||
return individual_value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return getConfiguredValue(POLICY_CONFIGURE_ENABLED_GLOBAL_ALL_UTILITIES);
|
||||
}
|
||||
}
|
||||
|
||||
gpo_rule_configured_t getRunPluginEnabledValue(std::string pluginID)
|
||||
{
|
||||
if (pluginID == "" || pluginID == " ")
|
||||
{
|
||||
// this plugin id can't exist in the registry
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
|
||||
std::wstring plugin_id(pluginID.begin(), pluginID.end());
|
||||
auto individual_plugin_setting = getPolicyListValue(POWER_LAUNCHER_INDIVIDUAL_PLUGIN_ENABLED_LIST_PATH, plugin_id);
|
||||
|
||||
if (individual_plugin_setting.has_value())
|
||||
{
|
||||
if (*individual_plugin_setting == L"0")
|
||||
{
|
||||
// force disabled
|
||||
return gpo_rule_configured_disabled;
|
||||
}
|
||||
else if (*individual_plugin_setting == L"1")
|
||||
{
|
||||
// force enabled
|
||||
return gpo_rule_configured_enabled;
|
||||
}
|
||||
else if (*individual_plugin_setting == L"2")
|
||||
{
|
||||
// user takes control
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
else
|
||||
{
|
||||
return gpo_rule_configured_wrong_value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no individual plugin policy exists, we check the policy with the setting for all plugins.
|
||||
return getConfiguredValue(POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER_ALL_PLUGINS);
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring getConfiguredMwbPolicyDefinedIpMappingRules()
|
||||
{
|
||||
// Important: HKLM has priority over HKCU
|
||||
auto mapping_rules = readRegistryStringValue(HKEY_LOCAL_MACHINE, POLICIES_PATH, POLICY_MWB_POLICY_DEFINED_IP_MAPPING_RULES, true);
|
||||
if (!mapping_rules.has_value())
|
||||
{
|
||||
mapping_rules = readRegistryStringValue(HKEY_CURRENT_USER, POLICIES_PATH, POLICY_MWB_POLICY_DEFINED_IP_MAPPING_RULES, true);
|
||||
}
|
||||
|
||||
// return value
|
||||
if (mapping_rules.has_value())
|
||||
{
|
||||
return mapping_rules.value();
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,176 +105,10 @@ namespace powertoys_gpo
|
||||
|
||||
// Methods used for reading the registry
|
||||
#pragma region ReadRegistryMethods
|
||||
inline std::optional<std::wstring> readRegistryStringValue(HKEY hRootKey, const std::wstring& subKey, const std::wstring& value_name, const bool is_multi_line_text = false)
|
||||
{
|
||||
// Set value type
|
||||
DWORD reg_value_type = REG_SZ;
|
||||
DWORD reg_flags = RRF_RT_REG_SZ;
|
||||
if (is_multi_line_text)
|
||||
{
|
||||
reg_value_type = REG_MULTI_SZ;
|
||||
reg_flags = RRF_RT_REG_MULTI_SZ;
|
||||
}
|
||||
|
||||
DWORD string_buffer_capacity;
|
||||
// Request required buffer capacity / string length
|
||||
if (RegGetValueW(hRootKey, subKey.c_str(), value_name.c_str(), reg_flags, ®_value_type, NULL, &string_buffer_capacity) != ERROR_SUCCESS)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
else if (string_buffer_capacity == 0)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// RegGetValueW overshoots sometimes. Use a buffer first to not have characters past the string end.
|
||||
wchar_t* temp_buffer = new wchar_t[string_buffer_capacity / sizeof(wchar_t) + 1];
|
||||
// Read string
|
||||
if (RegGetValueW(hRootKey, subKey.c_str(), value_name.c_str(), reg_flags, ®_value_type, temp_buffer, &string_buffer_capacity) != ERROR_SUCCESS)
|
||||
{
|
||||
delete temp_buffer;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Convert buffer to std::wstring
|
||||
std::wstring string_value = L"";
|
||||
if (reg_value_type == REG_MULTI_SZ)
|
||||
{
|
||||
// If it is REG_MULTI_SZ handle this way
|
||||
wchar_t* currentString = temp_buffer;
|
||||
while (*currentString != L'\0')
|
||||
{
|
||||
// If first entry then assign the string, else add to the string
|
||||
string_value = (string_value == L"") ? currentString : (string_value + L"\r\n" + currentString);
|
||||
currentString += wcslen(currentString) + 1; // Move to the next string
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If it is REG_SZ handle this way
|
||||
string_value = temp_buffer;
|
||||
}
|
||||
|
||||
// delete buffer, return string value
|
||||
delete temp_buffer;
|
||||
return string_value;
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getConfiguredValue(const std::wstring& registry_value_name)
|
||||
{
|
||||
HKEY key{};
|
||||
DWORD value = 0xFFFFFFFE;
|
||||
DWORD valueSize = sizeof(value);
|
||||
|
||||
bool machine_key_found = true;
|
||||
if (auto res = RegOpenKeyExW(POLICIES_SCOPE_MACHINE, POLICIES_PATH.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
machine_key_found = false;
|
||||
}
|
||||
|
||||
if (machine_key_found)
|
||||
{
|
||||
// If the path was found in the machine, we need to check if the value for the policy exists.
|
||||
auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &valueSize);
|
||||
|
||||
RegCloseKey(key);
|
||||
|
||||
if (res != ERROR_SUCCESS)
|
||||
{
|
||||
// Value not found on the path.
|
||||
machine_key_found = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!machine_key_found)
|
||||
{
|
||||
// If there's no value found on the machine scope, try to get it from the user scope.
|
||||
if (auto res = RegOpenKeyExW(POLICIES_SCOPE_USER, POLICIES_PATH.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
if (res == ERROR_FILE_NOT_FOUND)
|
||||
{
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
return gpo_rule_configured_unavailable;
|
||||
}
|
||||
auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &valueSize);
|
||||
RegCloseKey(key);
|
||||
|
||||
if (res != ERROR_SUCCESS)
|
||||
{
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case 0:
|
||||
return gpo_rule_configured_disabled;
|
||||
case 1:
|
||||
return gpo_rule_configured_enabled;
|
||||
default:
|
||||
return gpo_rule_configured_wrong_value;
|
||||
}
|
||||
}
|
||||
|
||||
inline std::optional<std::wstring> getPolicyListValue(const std::wstring& registry_list_path, const std::wstring& registry_list_value_name)
|
||||
{
|
||||
// This function returns the value of an entry of a policy list. The user scope is only checked, if the list is not enabled for the machine to not mix the lists.
|
||||
|
||||
HKEY key{};
|
||||
|
||||
// Try to read from the machine list.
|
||||
bool machine_list_found = false;
|
||||
if (RegOpenKeyExW(POLICIES_SCOPE_MACHINE, registry_list_path.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
machine_list_found = true;
|
||||
RegCloseKey(key);
|
||||
|
||||
// If the path exists in the machine registry, we try to read the value.
|
||||
auto regValueData = readRegistryStringValue(POLICIES_SCOPE_MACHINE, registry_list_path, registry_list_value_name);
|
||||
|
||||
if (regValueData.has_value())
|
||||
{
|
||||
// Return the value from the machine list.
|
||||
return *regValueData;
|
||||
}
|
||||
}
|
||||
|
||||
// If no list exists for machine, we try to read from the user list.
|
||||
if (!machine_list_found)
|
||||
{
|
||||
if (RegOpenKeyExW(POLICIES_SCOPE_USER, registry_list_path.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
RegCloseKey(key);
|
||||
|
||||
// If the path exists in the user registry, we try to read the value.
|
||||
auto regValueData = readRegistryStringValue(POLICIES_SCOPE_USER, registry_list_path, registry_list_value_name);
|
||||
|
||||
if (regValueData.has_value())
|
||||
{
|
||||
// Return the value from the user list.
|
||||
return *regValueData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No list exists for machine and user, or no value was found in the list, or an error ocurred while reading the value.
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getUtilityEnabledValue(const std::wstring& utility_name)
|
||||
{
|
||||
auto individual_value = getConfiguredValue(utility_name);
|
||||
|
||||
if (individual_value == gpo_rule_configured_disabled || individual_value == gpo_rule_configured_enabled)
|
||||
{
|
||||
return individual_value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return getConfiguredValue(POLICY_CONFIGURE_ENABLED_GLOBAL_ALL_UTILITIES);
|
||||
}
|
||||
}
|
||||
std::optional<std::wstring> readRegistryStringValue(HKEY hRootKey, const std::wstring& subKey, const std::wstring& value_name, const bool is_multi_line_text = false);
|
||||
gpo_rule_configured_t getConfiguredValue(const std::wstring& registry_value_name);
|
||||
std::optional<std::wstring> getPolicyListValue(const std::wstring& registry_list_path, const std::wstring& registry_list_value_name);
|
||||
gpo_rule_configured_t getUtilityEnabledValue(const std::wstring& utility_name);
|
||||
#pragma endregion ReadRegistryMethods
|
||||
|
||||
// Utility enabled state policies
|
||||
@@ -544,45 +378,7 @@ namespace powertoys_gpo
|
||||
return getConfiguredValue(POLICY_CONFIGURE_RUN_AT_STARTUP);
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getRunPluginEnabledValue(std::string pluginID)
|
||||
{
|
||||
if (pluginID == "" || pluginID == " ")
|
||||
{
|
||||
// this plugin id can't exist in the registry
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
|
||||
std::wstring plugin_id(pluginID.begin(), pluginID.end());
|
||||
auto individual_plugin_setting = getPolicyListValue(POWER_LAUNCHER_INDIVIDUAL_PLUGIN_ENABLED_LIST_PATH, plugin_id);
|
||||
|
||||
if (individual_plugin_setting.has_value())
|
||||
{
|
||||
if (*individual_plugin_setting == L"0")
|
||||
{
|
||||
// force disabled
|
||||
return gpo_rule_configured_disabled;
|
||||
}
|
||||
else if (*individual_plugin_setting == L"1")
|
||||
{
|
||||
// force enabled
|
||||
return gpo_rule_configured_enabled;
|
||||
}
|
||||
else if (*individual_plugin_setting == L"2")
|
||||
{
|
||||
// user takes control
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
else
|
||||
{
|
||||
return gpo_rule_configured_wrong_value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no individual plugin policy exists, we check the policy with the setting for all plugins.
|
||||
return getConfiguredValue(POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER_ALL_PLUGINS);
|
||||
}
|
||||
}
|
||||
gpo_rule_configured_t getRunPluginEnabledValue(std::string pluginID);
|
||||
|
||||
inline gpo_rule_configured_t getAllowedAdvancedPasteOnlineAIModelsValue()
|
||||
{
|
||||
@@ -664,25 +460,7 @@ namespace powertoys_gpo
|
||||
return getConfiguredValue(POLICY_MWB_DISABLE_USER_DEFINED_IP_MAPPING_RULES);
|
||||
}
|
||||
|
||||
inline std::wstring getConfiguredMwbPolicyDefinedIpMappingRules()
|
||||
{
|
||||
// Important: HKLM has priority over HKCU
|
||||
auto mapping_rules = readRegistryStringValue(HKEY_LOCAL_MACHINE, POLICIES_PATH, POLICY_MWB_POLICY_DEFINED_IP_MAPPING_RULES, true);
|
||||
if (!mapping_rules.has_value())
|
||||
{
|
||||
mapping_rules = readRegistryStringValue(HKEY_CURRENT_USER, POLICIES_PATH, POLICY_MWB_POLICY_DEFINED_IP_MAPPING_RULES, true);
|
||||
}
|
||||
|
||||
// return value
|
||||
if (mapping_rules.has_value())
|
||||
{
|
||||
return mapping_rules.value();
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
}
|
||||
std::wstring getConfiguredMwbPolicyDefinedIpMappingRules();
|
||||
|
||||
inline gpo_rule_configured_t getConfiguredNewPlusHideTemplateFilenameExtensionValue()
|
||||
{
|
||||
|
||||
50
src/common/utils/json.cpp
Normal file
50
src/common/utils/json.cpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#include "pch.h"
|
||||
#include "json.h"
|
||||
|
||||
namespace json
|
||||
{
|
||||
std::optional<JsonObject> from_file(std::wstring_view file_name)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::ifstream file(file_name.data(), std::ios::binary);
|
||||
if (file.is_open())
|
||||
{
|
||||
using iterator = std::istreambuf_iterator<char>;
|
||||
std::string objStr{ iterator{ file }, iterator{} };
|
||||
return JsonValue::Parse(winrt::to_hstring(objStr)).GetObjectW();
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
void to_file(std::wstring_view file_name, const JsonObject& obj)
|
||||
{
|
||||
std::wstring objStr{ obj.Stringify().c_str() };
|
||||
std::ofstream{ file_name.data(), std::ios::binary } << winrt::to_string(objStr);
|
||||
}
|
||||
|
||||
bool has(const json::JsonObject& o, std::wstring_view name, const json::JsonValueType type)
|
||||
{
|
||||
return o.HasKey(name) && o.GetNamedValue(name).ValueType() == type;
|
||||
}
|
||||
|
||||
JsonValue value(const bool boolean)
|
||||
{
|
||||
return json::JsonValue::CreateBooleanValue(boolean);
|
||||
}
|
||||
|
||||
JsonValue value(JsonObject valueObject)
|
||||
{
|
||||
return valueObject.as<JsonValue>();
|
||||
}
|
||||
|
||||
JsonValue value(JsonValue valueObject)
|
||||
{
|
||||
return valueObject; // identity function overload for convenience
|
||||
}
|
||||
}
|
||||
@@ -11,38 +11,11 @@ namespace json
|
||||
{
|
||||
using namespace winrt::Windows::Data::Json;
|
||||
|
||||
inline std::optional<JsonObject> from_file(std::wstring_view file_name)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::ifstream file(file_name.data(), std::ios::binary);
|
||||
if (file.is_open())
|
||||
{
|
||||
using isbi = std::istreambuf_iterator<char>;
|
||||
std::string obj_str{ isbi{ file }, isbi{} };
|
||||
return JsonValue::Parse(winrt::to_hstring(obj_str)).GetObjectW();
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
std::optional<JsonObject> from_file(std::wstring_view file_name);
|
||||
|
||||
inline void to_file(std::wstring_view file_name, const JsonObject& obj)
|
||||
{
|
||||
std::wstring obj_str{ obj.Stringify().c_str() };
|
||||
std::ofstream{ file_name.data(), std::ios::binary } << winrt::to_string(obj_str);
|
||||
}
|
||||
void to_file(std::wstring_view file_name, const JsonObject& obj);
|
||||
|
||||
inline bool has(
|
||||
const json::JsonObject& o,
|
||||
std::wstring_view name,
|
||||
const json::JsonValueType type = JsonValueType::Object)
|
||||
{
|
||||
return o.HasKey(name) && o.GetNamedValue(name).ValueType() == type;
|
||||
}
|
||||
bool has(const json::JsonObject& o, std::wstring_view name, const json::JsonValueType type = JsonValueType::Object);
|
||||
|
||||
template<typename T>
|
||||
inline std::enable_if_t<std::is_arithmetic_v<T>, JsonValue> value(const T arithmetic)
|
||||
@@ -56,20 +29,11 @@ namespace json
|
||||
return json::JsonValue::CreateStringValue(s);
|
||||
}
|
||||
|
||||
inline JsonValue value(const bool boolean)
|
||||
{
|
||||
return json::JsonValue::CreateBooleanValue(boolean);
|
||||
}
|
||||
JsonValue value(const bool boolean);
|
||||
|
||||
inline JsonValue value(JsonObject value)
|
||||
{
|
||||
return value.as<JsonValue>();
|
||||
}
|
||||
JsonValue value(JsonObject value);
|
||||
|
||||
inline JsonValue value(JsonValue value)
|
||||
{
|
||||
return value; // identity function overload for convenience
|
||||
}
|
||||
JsonValue value(JsonValue value);
|
||||
|
||||
template<typename T, typename D = std::optional<T>>
|
||||
requires std::constructible_from<std::optional<T>, D>
|
||||
|
||||
19
src/common/utils/language_helper.cpp
Normal file
19
src/common/utils/language_helper.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "pch.h"
|
||||
#include "language_helper.h"
|
||||
|
||||
namespace LanguageHelpers
|
||||
{
|
||||
std::wstring load_language()
|
||||
{
|
||||
std::filesystem::path languageJsonFilePath(PTSettingsHelper::get_root_save_folder_location() + L"\\language.json");
|
||||
|
||||
auto langJson = json::from_file(languageJsonFilePath.c_str());
|
||||
if (!langJson.has_value())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring language = langJson->GetNamedString(L"language", L"").c_str();
|
||||
return language;
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,5 @@
|
||||
|
||||
namespace LanguageHelpers
|
||||
{
|
||||
inline std::wstring load_language()
|
||||
{
|
||||
std::filesystem::path languageJsonFilePath(PTSettingsHelper::get_root_save_folder_location() + L"\\language.json");
|
||||
|
||||
auto langJson = json::from_file(languageJsonFilePath.c_str());
|
||||
if (!langJson.has_value())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring language = langJson->GetNamedString(L"language", L"").c_str();
|
||||
return language;
|
||||
}
|
||||
std::wstring load_language();
|
||||
}
|
||||
|
||||
96
src/common/utils/logger_helper.cpp
Normal file
96
src/common/utils/logger_helper.cpp
Normal file
@@ -0,0 +1,96 @@
|
||||
#include "pch.h"
|
||||
#include "logger_helper.h"
|
||||
|
||||
namespace LoggerHelpers
|
||||
{
|
||||
std::filesystem::path get_log_folder_path(std::wstring_view appPath)
|
||||
{
|
||||
std::filesystem::path logFolderPath(appPath);
|
||||
logFolderPath.append(LogSettings::logPath);
|
||||
logFolderPath.append(get_product_version());
|
||||
return logFolderPath;
|
||||
}
|
||||
|
||||
bool delete_old_log_folder(const std::filesystem::path& logFolderPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::filesystem::remove_all(logFolderPath);
|
||||
return true;
|
||||
}
|
||||
catch (std::filesystem::filesystem_error& e)
|
||||
{
|
||||
Logger::error("Failed to delete old log folder: {}", e.what());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool dir_exists(std::filesystem::path dir)
|
||||
{
|
||||
std::error_code err;
|
||||
auto entry = std::filesystem::directory_entry(dir, err);
|
||||
if (err.value())
|
||||
{
|
||||
Logger::error("Failed to create directory entry. {}", err.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
return entry.exists();
|
||||
}
|
||||
|
||||
bool delete_other_versions_log_folders(std::wstring_view appPath, const std::filesystem::path& currentVersionLogFolder)
|
||||
{
|
||||
bool result = true;
|
||||
std::filesystem::path logFolderPath(appPath);
|
||||
logFolderPath.append(LogSettings::logPath);
|
||||
|
||||
if (!dir_exists(logFolderPath))
|
||||
{
|
||||
Logger::trace("Directory {} does not exist", logFolderPath.string());
|
||||
return true;
|
||||
}
|
||||
|
||||
std::error_code err;
|
||||
auto folders = std::filesystem::directory_iterator(logFolderPath, err);
|
||||
if (err.value())
|
||||
{
|
||||
Logger::error("Failed to create directory iterator for {}. {}", logFolderPath.string(), err.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& dir : folders)
|
||||
{
|
||||
if (dir != currentVersionLogFolder)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::filesystem::remove_all(dir);
|
||||
}
|
||||
catch (std::filesystem::filesystem_error& e)
|
||||
{
|
||||
Logger::error("Failed to delete previous version log folder: {}", e.what());
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void init_logger(std::wstring moduleName, std::wstring internalPath, std::string loggerName)
|
||||
{
|
||||
std::filesystem::path rootFolder(PTSettingsHelper::get_module_save_folder_location(moduleName));
|
||||
rootFolder.append(internalPath);
|
||||
|
||||
auto currentFolder = rootFolder;
|
||||
currentFolder.append(LogSettings::logPath);
|
||||
currentFolder.append(get_product_version());
|
||||
|
||||
auto logsPath = currentFolder;
|
||||
logsPath.append(L"log.log");
|
||||
Logger::init(loggerName, logsPath.wstring(), PTSettingsHelper::get_log_settings_file_location());
|
||||
|
||||
delete_other_versions_log_folders(rootFolder.wstring(), currentFolder);
|
||||
}
|
||||
}
|
||||
@@ -1,99 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <common/version/version.h>
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
|
||||
namespace LoggerHelpers
|
||||
{
|
||||
inline std::filesystem::path get_log_folder_path(std::wstring_view appPath)
|
||||
{
|
||||
std::filesystem::path logFolderPath(appPath);
|
||||
logFolderPath.append(LogSettings::logPath);
|
||||
logFolderPath.append(get_product_version());
|
||||
return logFolderPath;
|
||||
}
|
||||
std::filesystem::path get_log_folder_path(std::wstring_view appPath);
|
||||
|
||||
inline bool delete_old_log_folder(const std::filesystem::path& logFolderPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::filesystem::remove_all(logFolderPath);
|
||||
return true;
|
||||
}
|
||||
catch (std::filesystem::filesystem_error& e)
|
||||
{
|
||||
Logger::error("Failed to delete old log folder: {}", e.what());
|
||||
}
|
||||
bool delete_old_log_folder(const std::filesystem::path& logFolderPath);
|
||||
|
||||
return false;
|
||||
}
|
||||
bool dir_exists(std::filesystem::path dir);
|
||||
|
||||
inline bool dir_exists(std::filesystem::path dir)
|
||||
{
|
||||
std::error_code err;
|
||||
auto entry = std::filesystem::directory_entry(dir, err);
|
||||
if (err.value())
|
||||
{
|
||||
Logger::error("Failed to create directory entry. {}", err.message());
|
||||
return false;
|
||||
}
|
||||
bool delete_other_versions_log_folders(std::wstring_view appPath, const std::filesystem::path& currentVersionLogFolder);
|
||||
|
||||
return entry.exists();
|
||||
}
|
||||
|
||||
inline bool delete_other_versions_log_folders(std::wstring_view appPath, const std::filesystem::path& currentVersionLogFolder)
|
||||
{
|
||||
bool result = true;
|
||||
std::filesystem::path logFolderPath(appPath);
|
||||
logFolderPath.append(LogSettings::logPath);
|
||||
|
||||
if (!dir_exists(logFolderPath))
|
||||
{
|
||||
Logger::trace("Directory {} does not exist", logFolderPath.string());
|
||||
return true;
|
||||
}
|
||||
|
||||
std::error_code err;
|
||||
auto folders = std::filesystem::directory_iterator(logFolderPath, err);
|
||||
if (err.value())
|
||||
{
|
||||
Logger::error("Failed to create directory iterator for {}. {}", logFolderPath.string(), err.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& dir : folders)
|
||||
{
|
||||
if (dir != currentVersionLogFolder)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::filesystem::remove_all(dir);
|
||||
}
|
||||
catch (std::filesystem::filesystem_error& e)
|
||||
{
|
||||
Logger::error("Failed to delete previous version log folder: {}", e.what());
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
inline void init_logger(std::wstring moduleName, std::wstring internalPath, std::string loggerName)
|
||||
{
|
||||
std::filesystem::path rootFolder(PTSettingsHelper::get_module_save_folder_location(moduleName));
|
||||
rootFolder.append(internalPath);
|
||||
|
||||
auto currentFolder = rootFolder;
|
||||
currentFolder.append(LogSettings::logPath);
|
||||
currentFolder.append(get_product_version());
|
||||
|
||||
auto logsPath = currentFolder;
|
||||
logsPath.append(L"log.log");
|
||||
Logger::init(loggerName, logsPath.wstring(), PTSettingsHelper::get_log_settings_file_location());
|
||||
|
||||
delete_other_versions_log_folders(rootFolder.wstring(), currentFolder);
|
||||
}
|
||||
void init_logger(std::wstring moduleName, std::wstring internalPath, std::string loggerName);
|
||||
}
|
||||
|
||||
318
src/common/utils/modulesRegistry.cpp
Normal file
318
src/common/utils/modulesRegistry.cpp
Normal file
@@ -0,0 +1,318 @@
|
||||
#include "pch.h"
|
||||
#include "modulesRegistry.h"
|
||||
|
||||
#include <utility>
|
||||
namespace
|
||||
{
|
||||
registry::ChangeSet createPreviewChangeSet(registry::shellex::PreviewHandlerType type,
|
||||
bool perUser,
|
||||
const wchar_t* clsid,
|
||||
std::wstring dllName,
|
||||
std::wstring handlerClsid,
|
||||
std::wstring handlerDisplayName,
|
||||
std::vector<std::wstring> extensions,
|
||||
std::wstring perceivedType = L"",
|
||||
std::wstring fileKindType = L"")
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
|
||||
return generatePreviewHandler(type,
|
||||
perUser,
|
||||
clsid,
|
||||
get_std_product_version(),
|
||||
std::move(dllName),
|
||||
std::move(handlerClsid),
|
||||
std::move(handlerDisplayName),
|
||||
std::move(extensions),
|
||||
std::move(perceivedType),
|
||||
std::move(fileKindType));
|
||||
}
|
||||
}
|
||||
|
||||
registry::ChangeSet getSvgPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{FCDD4EED-41AA-492F-8A84-31A1546226E0}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.SvgPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"SvgPreviewHandler",
|
||||
L"Svg Preview Handler",
|
||||
NonLocalizable::ExtSVG);
|
||||
}
|
||||
|
||||
registry::ChangeSet getMdPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{60789D87-9C3C-44AF-B18C-3DE2C2820ED3}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.MarkdownPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"MarkdownPreviewHandler",
|
||||
L"Markdown Preview Handler",
|
||||
NonLocalizable::ExtMarkdown);
|
||||
}
|
||||
|
||||
registry::ChangeSet getMonacoPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
std::vector<std::wstring> extensions;
|
||||
|
||||
std::vector<std::wstring> exclusions;
|
||||
exclusions.insert(exclusions.end(), NonLocalizable::ExtMarkdown.begin(), NonLocalizable::ExtMarkdown.end());
|
||||
exclusions.insert(exclusions.end(), NonLocalizable::ExtSVG.begin(), NonLocalizable::ExtSVG.end());
|
||||
exclusions.insert(exclusions.end(), NonLocalizable::ExtNoNoNo.begin(), NonLocalizable::ExtNoNoNo.end());
|
||||
|
||||
std::wstring languagesFilePath = fs::path{ installationDir } / NonLocalizable::MONACO_LANGUAGES_FILE_NAME;
|
||||
auto jsonValue = json::from_file(languagesFilePath);
|
||||
|
||||
if (jsonValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto list = jsonValue->GetNamedArray(NonLocalizable::ListID);
|
||||
for (uint32_t i = 0; i < list.Size(); ++i)
|
||||
{
|
||||
auto entry = list.GetObjectAt(i);
|
||||
if (entry.HasKey(NonLocalizable::ExtensionsID))
|
||||
{
|
||||
auto extensionsList = entry.GetNamedArray(NonLocalizable::ExtensionsID);
|
||||
|
||||
for (uint32_t j = 0; j < extensionsList.Size(); ++j)
|
||||
{
|
||||
auto extension = extensionsList.GetStringAt(j);
|
||||
|
||||
bool isExcluded = false;
|
||||
for (const auto& excluded : exclusions)
|
||||
{
|
||||
if (std::wstring{ extension } == excluded)
|
||||
{
|
||||
isExcluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isExcluded)
|
||||
{
|
||||
extensions.emplace_back(extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{D8034CFA-F34B-41FE-AD45-62FCBB52A6DA}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.MonacoPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"MonacoPreviewHandler",
|
||||
L"Monaco Preview Handler",
|
||||
std::move(extensions));
|
||||
}
|
||||
|
||||
registry::ChangeSet getPdfPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{A5A41CC7-02CB-41D4-8C9B-9087040D6098}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.PdfPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"PdfPreviewHandler",
|
||||
L"Pdf Preview Handler",
|
||||
NonLocalizable::ExtPDF);
|
||||
}
|
||||
|
||||
registry::ChangeSet getGcodePreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{A0257634-8812-4CE8-AF11-FA69ACAEAFAE}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.GcodePreviewHandlerCpp.dll)d").wstring(),
|
||||
L"GcodePreviewHandler",
|
||||
L"G-code Preview Handler",
|
||||
NonLocalizable::ExtGCode);
|
||||
}
|
||||
|
||||
registry::ChangeSet getBgcodePreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{0e6d5bdd-d5f8-4692-a089-8bb88cdd37f4}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.BgcodePreviewHandlerCpp.dll)d").wstring(),
|
||||
L"BgcodePreviewHandler",
|
||||
L"Binary G-code Preview Handler",
|
||||
NonLocalizable::ExtBGCode);
|
||||
}
|
||||
|
||||
registry::ChangeSet getQoiPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{729B72CD-B72E-4FE9-BCBF-E954B33FE699}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.QoiPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"QoiPreviewHandler",
|
||||
L"Qoi Preview Handler",
|
||||
NonLocalizable::ExtQOI);
|
||||
}
|
||||
|
||||
registry::ChangeSet getSvgThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{10144713-1526-46C9-88DA-1FB52807A9FF}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.SvgThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"SvgThumbnailProvider",
|
||||
L"Svg Thumbnail Provider",
|
||||
NonLocalizable::ExtSVG,
|
||||
L"image",
|
||||
L"Picture");
|
||||
}
|
||||
|
||||
registry::ChangeSet getPdfThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{D8BB9942-93BD-412D-87E4-33FAB214DC1A}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.PdfThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"PdfThumbnailProvider",
|
||||
L"Pdf Thumbnail Provider",
|
||||
NonLocalizable::ExtPDF);
|
||||
}
|
||||
|
||||
registry::ChangeSet getGcodeThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{F2847CBE-CD03-4C83-A359-1A8052C1B9D5}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.GcodeThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"GcodeThumbnailProvider",
|
||||
L"G-code Thumbnail Provider",
|
||||
NonLocalizable::ExtGCode);
|
||||
}
|
||||
|
||||
registry::ChangeSet getBgcodeThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{5c93a1e4-99d0-4fb3-991c-6c296a27be21}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.BgcodeThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"BgcodeThumbnailProvider",
|
||||
L"Binary G-code Thumbnail Provider",
|
||||
NonLocalizable::ExtBGCode);
|
||||
}
|
||||
|
||||
registry::ChangeSet getStlThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{77257004-6F25-4521-B602-50ECC6EC62A6}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.StlThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"StlThumbnailProvider",
|
||||
L"Stl Thumbnail Provider",
|
||||
NonLocalizable::ExtSTL);
|
||||
}
|
||||
|
||||
registry::ChangeSet getQoiThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{AD856B15-D25E-4008-AFB7-AFAA55586188}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.QoiThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"QoiThumbnailProvider",
|
||||
L"Qoi Thumbnail Provider",
|
||||
NonLocalizable::ExtQOI,
|
||||
L"image",
|
||||
L"Picture");
|
||||
}
|
||||
|
||||
registry::ChangeSet getRegistryPreviewSetDefaultAppChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
|
||||
|
||||
std::vector<registry::ValueChange> changes;
|
||||
|
||||
std::wstring appName = L"Registry Preview";
|
||||
std::wstring fullAppName = L"PowerToys.RegistryPreview";
|
||||
std::wstring registryKeyPrefix = L"Software\\Classes\\";
|
||||
|
||||
std::wstring appPath = installationDir + L"\\WinUI3Apps\\PowerToys.RegistryPreview.exe";
|
||||
std::wstring command = appPath + L" \"----ms-protocol:ms-encodedlaunch:App?ContractId=Windows.File&Verb=open&File=%1\"";
|
||||
|
||||
changes.push_back({ scope, registryKeyPrefix + fullAppName + L"\\Application", L"ApplicationName", appName });
|
||||
changes.push_back({ scope, registryKeyPrefix + fullAppName + L"\\DefaultIcon", std::nullopt, appPath });
|
||||
changes.push_back({ scope, registryKeyPrefix + fullAppName + L"\\shell\\open\\command", std::nullopt, command });
|
||||
changes.push_back({ scope, registryKeyPrefix + L".reg\\OpenWithProgIDs", fullAppName, L"" });
|
||||
|
||||
return { changes };
|
||||
}
|
||||
|
||||
registry::ChangeSet getRegistryPreviewChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
|
||||
|
||||
std::vector<registry::ValueChange> changes;
|
||||
|
||||
std::wstring command = installationDir;
|
||||
command.append(L"\\WinUI3Apps\\PowerToys.RegistryPreview.exe \"%1\"");
|
||||
changes.push_back({ scope, L"Software\\Classes\\regfile\\shell\\preview\\command", std::nullopt, command });
|
||||
|
||||
std::wstring iconPath = installationDir;
|
||||
iconPath.append(L"\\WinUI3Apps\\Assets\\RegistryPreview\\RegistryPreview.ico");
|
||||
changes.push_back({ scope, L"Software\\Classes\\regfile\\shell\\preview", L"icon", iconPath });
|
||||
|
||||
return { changes };
|
||||
}
|
||||
|
||||
std::vector<registry::ChangeSet> getAllOnByDefaultModulesChangeSets(const std::wstring& installationDir)
|
||||
{
|
||||
constexpr bool perUser = true;
|
||||
return {
|
||||
getSvgPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getMdPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getMonacoPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getGcodePreviewHandlerChangeSet(installationDir, perUser),
|
||||
getBgcodePreviewHandlerChangeSet(installationDir, perUser),
|
||||
getQoiPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getSvgThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getGcodeThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getBgcodeThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getStlThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getQoiThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getRegistryPreviewChangeSet(installationDir, perUser)
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<registry::ChangeSet> getAllModulesChangeSets(const std::wstring& installationDir)
|
||||
{
|
||||
constexpr bool perUser = true;
|
||||
return {
|
||||
getSvgPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getMdPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getMonacoPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getPdfPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getGcodePreviewHandlerChangeSet(installationDir, perUser),
|
||||
getBgcodePreviewHandlerChangeSet(installationDir, perUser),
|
||||
getQoiPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getSvgThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getPdfThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getGcodeThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getBgcodeThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getStlThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getQoiThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getRegistryPreviewChangeSet(installationDir, perUser),
|
||||
getRegistryPreviewSetDefaultAppChangeSet(installationDir, perUser)
|
||||
};
|
||||
}
|
||||
@@ -25,309 +25,36 @@ namespace NonLocalizable
|
||||
};
|
||||
}
|
||||
|
||||
inline registry::ChangeSet getSvgPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{FCDD4EED-41AA-492F-8A84-31A1546226E0}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } /
|
||||
LR"d(PowerToys.SvgPreviewHandlerCpp.dll)d")
|
||||
.wstring(),
|
||||
L"SvgPreviewHandler",
|
||||
L"Svg Preview Handler",
|
||||
NonLocalizable::ExtSVG);
|
||||
}
|
||||
registry::ChangeSet getSvgPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
inline registry::ChangeSet getMdPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{60789D87-9C3C-44AF-B18C-3DE2C2820ED3}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.MarkdownPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"MarkdownPreviewHandler",
|
||||
L"Markdown Preview Handler",
|
||||
NonLocalizable::ExtMarkdown);
|
||||
}
|
||||
registry::ChangeSet getMdPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
inline registry::ChangeSet getMonacoPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
registry::ChangeSet getMonacoPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
// Set up a list of extensions for the preview handler to take over
|
||||
std::vector<std::wstring> extensions;
|
||||
registry::ChangeSet getPdfPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
// Set up a list of extensions that Monaco support but the preview handler shouldn't take over
|
||||
std::vector<std::wstring> ExtExclusions;
|
||||
ExtExclusions.insert(ExtExclusions.end(), NonLocalizable::ExtMarkdown.begin(), NonLocalizable::ExtMarkdown.end());
|
||||
ExtExclusions.insert(ExtExclusions.end(), NonLocalizable::ExtSVG.begin(), NonLocalizable::ExtSVG.end());
|
||||
ExtExclusions.insert(ExtExclusions.end(), NonLocalizable::ExtNoNoNo.begin(), NonLocalizable::ExtNoNoNo.end());
|
||||
bool IsExcluded = false;
|
||||
registry::ChangeSet getGcodePreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
std::wstring languagesFilePath = fs::path{ installationDir } / NonLocalizable::MONACO_LANGUAGES_FILE_NAME;
|
||||
auto json = json::from_file(languagesFilePath);
|
||||
registry::ChangeSet getBgcodePreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
if (json)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto list = json->GetNamedArray(NonLocalizable::ListID);
|
||||
for (uint32_t i = 0; i < list.Size(); ++i)
|
||||
{
|
||||
auto entry = list.GetObjectAt(i);
|
||||
if (entry.HasKey(NonLocalizable::ExtensionsID))
|
||||
{
|
||||
auto extensionsList = entry.GetNamedArray(NonLocalizable::ExtensionsID);
|
||||
registry::ChangeSet getQoiPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
for (uint32_t j = 0; j < extensionsList.Size(); ++j)
|
||||
{
|
||||
auto extension = extensionsList.GetStringAt(j);
|
||||
registry::ChangeSet getSvgThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
// Ignore extensions in the exclusion list
|
||||
IsExcluded = false;
|
||||
registry::ChangeSet getPdfThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
for (std::wstring k : ExtExclusions)
|
||||
{
|
||||
if (std::wstring{ extension } == k)
|
||||
{
|
||||
IsExcluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (IsExcluded)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
extensions.push_back(std::wstring{ extension });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
registry::ChangeSet getGcodeThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
return generatePreviewHandler(PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{D8034CFA-F34B-41FE-AD45-62FCBB52A6DA}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.MonacoPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"MonacoPreviewHandler",
|
||||
L"Monaco Preview Handler",
|
||||
extensions);
|
||||
}
|
||||
registry::ChangeSet getBgcodeThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
inline registry::ChangeSet getPdfPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{A5A41CC7-02CB-41D4-8C9B-9087040D6098}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.PdfPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"PdfPreviewHandler",
|
||||
L"Pdf Preview Handler",
|
||||
NonLocalizable::ExtPDF);
|
||||
}
|
||||
registry::ChangeSet getStlThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
inline registry::ChangeSet getGcodePreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{A0257634-8812-4CE8-AF11-FA69ACAEAFAE}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.GcodePreviewHandlerCpp.dll)d").wstring(),
|
||||
L"GcodePreviewHandler",
|
||||
L"G-code Preview Handler",
|
||||
NonLocalizable::ExtGCode);
|
||||
}
|
||||
registry::ChangeSet getQoiThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
inline registry::ChangeSet getBgcodePreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{0e6d5bdd-d5f8-4692-a089-8bb88cdd37f4}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.BgcodePreviewHandlerCpp.dll)d").wstring(),
|
||||
L"BgcodePreviewHandler",
|
||||
L"Binary G-code Preview Handler",
|
||||
NonLocalizable::ExtBGCode);
|
||||
}
|
||||
registry::ChangeSet getRegistryPreviewSetDefaultAppChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
inline registry::ChangeSet getQoiPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{729B72CD-B72E-4FE9-BCBF-E954B33FE699}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.QoiPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"QoiPreviewHandler",
|
||||
L"Qoi Preview Handler",
|
||||
NonLocalizable::ExtQOI);
|
||||
}
|
||||
registry::ChangeSet getRegistryPreviewChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
inline registry::ChangeSet getSvgThumbnailHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{10144713-1526-46C9-88DA-1FB52807A9FF}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.SvgThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"SvgThumbnailProvider",
|
||||
L"Svg Thumbnail Provider",
|
||||
NonLocalizable::ExtSVG,
|
||||
L"image",
|
||||
L"Picture");
|
||||
}
|
||||
std::vector<registry::ChangeSet> getAllOnByDefaultModulesChangeSets(const std::wstring& installationDir);
|
||||
|
||||
inline registry::ChangeSet getPdfThumbnailHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{D8BB9942-93BD-412D-87E4-33FAB214DC1A}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.PdfThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"PdfThumbnailProvider",
|
||||
L"Pdf Thumbnail Provider",
|
||||
NonLocalizable::ExtPDF);
|
||||
}
|
||||
|
||||
inline registry::ChangeSet getGcodeThumbnailHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{F2847CBE-CD03-4C83-A359-1A8052C1B9D5}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.GcodeThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"GcodeThumbnailProvider",
|
||||
L"G-code Thumbnail Provider",
|
||||
NonLocalizable::ExtGCode);
|
||||
}
|
||||
|
||||
inline registry::ChangeSet getBgcodeThumbnailHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{5c93a1e4-99d0-4fb3-991c-6c296a27be21}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.BgcodeThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"BgcodeThumbnailProvider",
|
||||
L"Binary G-code Thumbnail Provider",
|
||||
NonLocalizable::ExtBGCode);
|
||||
}
|
||||
|
||||
inline registry::ChangeSet getStlThumbnailHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{77257004-6F25-4521-B602-50ECC6EC62A6}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.StlThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"StlThumbnailProvider",
|
||||
L"Stl Thumbnail Provider",
|
||||
NonLocalizable::ExtSTL);
|
||||
}
|
||||
|
||||
inline registry::ChangeSet getQoiThumbnailHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{AD856B15-D25E-4008-AFB7-AFAA55586188}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.QoiThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"QoiThumbnailProvider",
|
||||
L"Qoi Thumbnail Provider",
|
||||
NonLocalizable::ExtQOI,
|
||||
L"image",
|
||||
L"Picture");
|
||||
}
|
||||
|
||||
inline registry::ChangeSet getRegistryPreviewSetDefaultAppChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
|
||||
|
||||
using vec_t = std::vector<registry::ValueChange>;
|
||||
vec_t changes;
|
||||
|
||||
std::wstring appName = L"Registry Preview";
|
||||
std::wstring fullAppName = L"PowerToys.RegistryPreview";
|
||||
std::wstring registryKeyPrefix = L"Software\\Classes\\";
|
||||
|
||||
std::wstring appPath = installationDir + L"\\WinUI3Apps\\PowerToys.RegistryPreview.exe";
|
||||
std::wstring command = appPath + L" \"----ms-protocol:ms-encodedlaunch:App?ContractId=Windows.File&Verb=open&File=%1\"";
|
||||
|
||||
changes.push_back({ scope, registryKeyPrefix + fullAppName + L"\\" + L"Application", L"ApplicationName", appName });
|
||||
changes.push_back({ scope, registryKeyPrefix + fullAppName + L"\\" + L"DefaultIcon", std::nullopt, appPath });
|
||||
changes.push_back({ scope, registryKeyPrefix + fullAppName + L"\\" + L"shell\\open\\command", std::nullopt, command });
|
||||
changes.push_back({ scope, registryKeyPrefix + L".reg\\OpenWithProgIDs", fullAppName, L"" });
|
||||
|
||||
return { changes };
|
||||
}
|
||||
|
||||
inline registry::ChangeSet getRegistryPreviewChangeSet(const std::wstring installationDir,const bool perUser)
|
||||
{
|
||||
const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
|
||||
|
||||
using vec_t = std::vector<registry::ValueChange>;
|
||||
vec_t changes;
|
||||
|
||||
std::wstring command = installationDir;
|
||||
command.append(L"\\WinUI3Apps\\PowerToys.RegistryPreview.exe \"%1\"");
|
||||
changes.push_back({ scope, L"Software\\Classes\\regfile\\shell\\preview\\command", std::nullopt, command });
|
||||
|
||||
std::wstring icon_path = installationDir;
|
||||
icon_path.append(L"\\WinUI3Apps\\Assets\\RegistryPreview\\RegistryPreview.ico");
|
||||
changes.push_back({ scope, L"Software\\Classes\\regfile\\shell\\preview", L"icon", icon_path });
|
||||
|
||||
return { changes };
|
||||
}
|
||||
|
||||
inline std::vector<registry::ChangeSet> getAllOnByDefaultModulesChangeSets(const std::wstring installationDir)
|
||||
{
|
||||
constexpr bool PER_USER = true;
|
||||
return { getSvgPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getMdPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getMonacoPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getGcodePreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getBgcodePreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getQoiPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getSvgThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getGcodeThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getBgcodeThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getStlThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getQoiThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getRegistryPreviewChangeSet(installationDir, PER_USER) };
|
||||
}
|
||||
|
||||
inline std::vector<registry::ChangeSet> getAllModulesChangeSets(const std::wstring installationDir)
|
||||
{
|
||||
constexpr bool PER_USER = true;
|
||||
return { getSvgPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getMdPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getMonacoPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getPdfPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getGcodePreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getBgcodePreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getQoiPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getSvgThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getPdfThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getGcodeThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getBgcodeThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getStlThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getQoiThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getRegistryPreviewChangeSet(installationDir, PER_USER),
|
||||
getRegistryPreviewSetDefaultAppChangeSet(installationDir, PER_USER) };
|
||||
}
|
||||
std::vector<registry::ChangeSet> getAllModulesChangeSets(const std::wstring& installationDir);
|
||||
|
||||
417
src/common/utils/package.cpp
Normal file
417
src/common/utils/package.cpp
Normal file
@@ -0,0 +1,417 @@
|
||||
#include "pch.h"
|
||||
#include "package.h"
|
||||
#include <common/utils/winapi_error.h>
|
||||
|
||||
namespace package
|
||||
{
|
||||
using namespace winrt::Windows::Foundation;
|
||||
using namespace winrt::Windows::ApplicationModel;
|
||||
using namespace winrt::Windows::Management::Deployment;
|
||||
|
||||
BOOL IsWin11OrGreater()
|
||||
{
|
||||
OSVERSIONINFOEX osvi{};
|
||||
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
|
||||
osvi.dwMajorVersion = HIBYTE(_WIN32_WINNT_WINTHRESHOLD); // 10
|
||||
osvi.dwMinorVersion = LOBYTE(_WIN32_WINNT_WINTHRESHOLD); // 0
|
||||
osvi.dwBuildNumber = 22000; // Windows 11 RTM build
|
||||
|
||||
DWORDLONG mask = 0;
|
||||
BYTE op = VER_GREATER_EQUAL;
|
||||
VER_SET_CONDITION(mask, VER_MAJORVERSION, op);
|
||||
VER_SET_CONDITION(mask, VER_MINORVERSION, op);
|
||||
VER_SET_CONDITION(mask, VER_BUILDNUMBER, op);
|
||||
|
||||
return VerifyVersionInfo(&osvi,
|
||||
VER_MAJORVERSION | VER_MINORVERSION | VER_BUILDNUMBER,
|
||||
mask);
|
||||
}
|
||||
|
||||
ComInitializer::ComInitializer(DWORD coInitFlags) : _initialized(false)
|
||||
{
|
||||
const HRESULT hr = CoInitializeEx(nullptr, coInitFlags);
|
||||
_initialized = SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
ComInitializer::~ComInitializer()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
CoUninitialize();
|
||||
}
|
||||
}
|
||||
|
||||
bool ComInitializer::Succeeded() const
|
||||
{
|
||||
return _initialized;
|
||||
}
|
||||
|
||||
static inline int compare_versions(const PackageVersion& a, const PACKAGE_VERSION& b)
|
||||
{
|
||||
if (a.Major != b.Major) return (a.Major < b.Major) ? -1 : 1;
|
||||
if (a.Minor != b.Minor) return (a.Minor < b.Minor) ? -1 : 1;
|
||||
if (a.Build != b.Build) return (a.Build < b.Build) ? -1 : 1;
|
||||
if (a.Revision != b.Revision) return (a.Revision < b.Revision) ? -1 : 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool GetPackageNameAndVersionFromAppx(const std::wstring& appxPath,
|
||||
std::wstring& outName,
|
||||
PACKAGE_VERSION& outVersion)
|
||||
{
|
||||
try
|
||||
{
|
||||
ComInitializer comInit;
|
||||
if (!comInit.Succeeded())
|
||||
{
|
||||
Logger::error(L"COM initialization failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IAppxFactory> factory;
|
||||
Microsoft::WRL::ComPtr<IStream> stream;
|
||||
Microsoft::WRL::ComPtr<IAppxPackageReader> reader;
|
||||
Microsoft::WRL::ComPtr<IAppxManifestReader> manifest;
|
||||
Microsoft::WRL::ComPtr<IAppxManifestPackageId> packageId;
|
||||
|
||||
HRESULT hr = CoCreateInstance(__uuidof(AppxFactory), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&factory));
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Logger::error(L"CoCreateInstance(AppxFactory) failed. {}", get_last_error_or_default(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = SHCreateStreamOnFileEx(appxPath.c_str(), STGM_READ | STGM_SHARE_DENY_WRITE, FILE_ATTRIBUTE_NORMAL, FALSE, nullptr, &stream);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Logger::error(L"SHCreateStreamOnFileEx failed. {}", get_last_error_or_default(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = factory->CreatePackageReader(stream.Get(), &reader);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Logger::error(L"CreatePackageReader failed. {}", get_last_error_or_default(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = reader->GetManifest(&manifest);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Logger::error(L"GetManifest failed. {}", get_last_error_or_default(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = manifest->GetPackageId(&packageId);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Logger::error(L"GetPackageId failed. {}", get_last_error_or_default(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
LPWSTR name = nullptr;
|
||||
hr = packageId->GetName(&name);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Logger::error(L"GetName failed. {}", get_last_error_or_default(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
UINT64 ver64 = 0;
|
||||
hr = packageId->GetVersion(&ver64);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
CoTaskMemFree(name);
|
||||
Logger::error(L"GetVersion failed. {}", get_last_error_or_default(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
outName = std::wstring(name);
|
||||
CoTaskMemFree(name);
|
||||
|
||||
outVersion.Major = static_cast<UINT16>((ver64 >> 48) & 0xFFFF);
|
||||
outVersion.Minor = static_cast<UINT16>((ver64 >> 32) & 0xFFFF);
|
||||
outVersion.Build = static_cast<UINT16>((ver64 >> 16) & 0xFFFF);
|
||||
outVersion.Revision = static_cast<UINT16>(ver64 & 0xFFFF);
|
||||
|
||||
Logger::info(L"Package name: {}, version: {}.{}.{}.{}, appxPath: {}",
|
||||
outName,
|
||||
outVersion.Major,
|
||||
outVersion.Minor,
|
||||
outVersion.Build,
|
||||
outVersion.Revision,
|
||||
appxPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
Logger::error(L"Standard exception: {}", winrt::to_hstring(ex.what()));
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Unknown or non-standard exception occurred.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Package> GetRegisteredPackage(std::wstring packageDisplayName, bool checkVersion)
|
||||
{
|
||||
PackageManager packageManager;
|
||||
for (const auto& pkg : packageManager.FindPackagesForUser({}))
|
||||
{
|
||||
const auto& fullName = std::wstring{ pkg.Id().FullName() };
|
||||
if (fullName.find(packageDisplayName) != std::wstring::npos)
|
||||
{
|
||||
if (!checkVersion)
|
||||
{
|
||||
return pkg;
|
||||
}
|
||||
const auto& ver = pkg.Id().Version();
|
||||
if (ver.Major == VERSION_MAJOR && ver.Minor == VERSION_MINOR && ver.Revision == VERSION_REVISION)
|
||||
{
|
||||
return pkg;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool IsPackageRegisteredWithPowerToysVersion(std::wstring packageDisplayName)
|
||||
{
|
||||
return GetRegisteredPackage(packageDisplayName, true).has_value();
|
||||
}
|
||||
|
||||
bool RegisterSparsePackage(const std::wstring& externalLocation, const std::wstring& sparsePkgPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
Uri externalUri{ externalLocation };
|
||||
Uri packageUri{ sparsePkgPath };
|
||||
|
||||
PackageManager packageManager;
|
||||
AddPackageOptions options;
|
||||
options.ExternalLocationUri(externalUri);
|
||||
options.ForceUpdateFromAnyVersion(true);
|
||||
|
||||
auto deploymentOperation = packageManager.AddPackageByUriAsync(packageUri, options);
|
||||
deploymentOperation.get();
|
||||
|
||||
if (deploymentOperation.Status() == AsyncStatus::Error)
|
||||
{
|
||||
auto deploymentResult{ deploymentOperation.GetResults() };
|
||||
auto errorCode = deploymentOperation.ErrorCode();
|
||||
auto errorText = deploymentResult.ErrorText();
|
||||
Logger::error(L"Register {} package failed. ErrorCode: {}, ErrorText: {}", sparsePkgPath, std::to_wstring(errorCode), errorText);
|
||||
return false;
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Canceled)
|
||||
{
|
||||
Logger::error(L"Register {} package canceled.", sparsePkgPath);
|
||||
return false;
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Completed)
|
||||
{
|
||||
Logger::info(L"Register {} package completed.", sparsePkgPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"Register {} package started.", sparsePkgPath);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Logger::error("Exception thrown while trying to register package: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool UnRegisterPackage(const std::wstring& pkgDisplayName)
|
||||
{
|
||||
try
|
||||
{
|
||||
PackageManager packageManager;
|
||||
const auto packages = packageManager.FindPackagesForUser({});
|
||||
|
||||
bool any = false;
|
||||
for (auto const& pkg : packages)
|
||||
{
|
||||
const auto& fullName = std::wstring{ pkg.Id().FullName() };
|
||||
if (fullName.find(pkgDisplayName) != std::wstring::npos)
|
||||
{
|
||||
any = true;
|
||||
auto op = packageManager.RemovePackageAsync(fullName);
|
||||
op.get();
|
||||
|
||||
if (op.Status() == AsyncStatus::Error)
|
||||
{
|
||||
auto res = op.GetResults();
|
||||
auto code = op.ErrorCode();
|
||||
auto text = res.ErrorText();
|
||||
Logger::error(L"Unregister {} package failed. ErrorCode: {}, ErrorText: {}", fullName, std::to_wstring(code), text);
|
||||
return false;
|
||||
}
|
||||
else if (op.Status() == AsyncStatus::Canceled)
|
||||
{
|
||||
Logger::error(L"Unregister {} package canceled.", fullName);
|
||||
return false;
|
||||
}
|
||||
else if (op.Status() == AsyncStatus::Completed)
|
||||
{
|
||||
Logger::info(L"Unregister {} package completed.", fullName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"Unregister {} package started.", fullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If nothing matched, treat as success (nothing to remove)
|
||||
return true;
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Logger::error("Exception thrown while trying to unregister package: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::wstring> FindMsixFile(const std::wstring& directoryPath, bool recursive)
|
||||
{
|
||||
std::vector<std::wstring> results;
|
||||
try
|
||||
{
|
||||
if (recursive)
|
||||
{
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(directoryPath))
|
||||
{
|
||||
if (!entry.is_regular_file()) continue;
|
||||
auto ext = entry.path().extension().wstring();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), ::towlower);
|
||||
if (ext == L".msix" || ext == L".msixbundle")
|
||||
{
|
||||
results.push_back(entry.path().wstring());
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const auto& entry : std::filesystem::directory_iterator(directoryPath))
|
||||
{
|
||||
if (!entry.is_regular_file()) continue;
|
||||
auto ext = entry.path().extension().wstring();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), ::towlower);
|
||||
if (ext == L".msix" || ext == L".msixbundle")
|
||||
{
|
||||
results.push_back(entry.path().wstring());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
Logger::error(L"FindMsixFile error: {}", winrt::to_hstring(e.what()));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
bool IsPackageSatisfied(const std::wstring& appxPath)
|
||||
{
|
||||
std::wstring targetName;
|
||||
PACKAGE_VERSION targetVersion{};
|
||||
if (!GetPackageNameAndVersionFromAppx(appxPath, targetName, targetVersion))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PackageManager pm;
|
||||
for (const auto& pkg : pm.FindPackagesForUser({}))
|
||||
{
|
||||
if (std::wstring{ pkg.Id().Name() } == targetName)
|
||||
{
|
||||
auto v = pkg.Id().Version();
|
||||
if (compare_versions(v, targetVersion) >= 0)
|
||||
{
|
||||
Logger::info(L"Package {} is satisfied. Installed version {}.{}.{}.{} >= target {}.{}.{}.{}, appxPath: {}",
|
||||
targetName,
|
||||
v.Major, v.Minor, v.Build, v.Revision,
|
||||
targetVersion.Major, targetVersion.Minor, targetVersion.Build, targetVersion.Revision,
|
||||
appxPath);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Logger::info(L"Package {} is not satisfied. Target version: {}.{}.{}.{}; appxPath: {}",
|
||||
targetName,
|
||||
targetVersion.Major, targetVersion.Minor, targetVersion.Build, targetVersion.Revision,
|
||||
appxPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RegisterPackage(std::wstring pkgPath, std::vector<std::wstring> dependencies)
|
||||
{
|
||||
try
|
||||
{
|
||||
Uri packageUri{ pkgPath };
|
||||
PackageManager packageManager;
|
||||
|
||||
DeploymentOptions options = DeploymentOptions::ForceTargetApplicationShutdown;
|
||||
Collections::IVector<Uri> uris = winrt::single_threaded_vector<Uri>();
|
||||
|
||||
for (const auto& dep : dependencies)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsPackageSatisfied(dep))
|
||||
{
|
||||
Logger::info(L"Dependency already satisfied: {}", dep);
|
||||
}
|
||||
else
|
||||
{
|
||||
uris.Append(Uri(dep));
|
||||
}
|
||||
}
|
||||
catch (const winrt::hresult_error& ex)
|
||||
{
|
||||
Logger::error(L"Error creating Uri for dependency: %s", ex.message().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
auto op = packageManager.AddPackageAsync(packageUri, uris, options);
|
||||
op.get();
|
||||
|
||||
if (op.Status() == AsyncStatus::Error)
|
||||
{
|
||||
auto res = op.GetResults();
|
||||
auto code = op.ErrorCode();
|
||||
auto text = res.ErrorText();
|
||||
Logger::error(L"Register {} package failed. ErrorCode: {}, ErrorText: {}", pkgPath, std::to_wstring(code), text);
|
||||
return false;
|
||||
}
|
||||
else if (op.Status() == AsyncStatus::Canceled)
|
||||
{
|
||||
Logger::error(L"Register {} package canceled.", pkgPath);
|
||||
return false;
|
||||
}
|
||||
else if (op.Status() == AsyncStatus::Completed)
|
||||
{
|
||||
Logger::info(L"Register {} package completed.", pkgPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"Register {} package started.", pkgPath);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Logger::error("Exception thrown while trying to register package: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Management.Deployment.h>
|
||||
|
||||
#include "../logger/logger.h"
|
||||
#include "../version/version.h"
|
||||
|
||||
namespace package
|
||||
{
|
||||
using namespace winrt::Windows::Foundation;
|
||||
@@ -25,30 +22,7 @@ namespace package
|
||||
using namespace winrt::Windows::Management::Deployment;
|
||||
using Microsoft::WRL::ComPtr;
|
||||
|
||||
inline BOOL IsWin11OrGreater()
|
||||
{
|
||||
OSVERSIONINFOEX osvi{};
|
||||
DWORDLONG dwlConditionMask = 0;
|
||||
byte op = VER_GREATER_EQUAL;
|
||||
|
||||
// Initialize the OSVERSIONINFOEX structure.
|
||||
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
|
||||
osvi.dwMajorVersion = HIBYTE(_WIN32_WINNT_WINTHRESHOLD);
|
||||
osvi.dwMinorVersion = LOBYTE(_WIN32_WINNT_WINTHRESHOLD);
|
||||
// Windows 11 build number
|
||||
osvi.dwBuildNumber = 22000;
|
||||
|
||||
// Initialize the condition mask.
|
||||
VER_SET_CONDITION(dwlConditionMask, VER_MAJORVERSION, op);
|
||||
VER_SET_CONDITION(dwlConditionMask, VER_MINORVERSION, op);
|
||||
VER_SET_CONDITION(dwlConditionMask, VER_BUILDNUMBER, op);
|
||||
|
||||
// Perform the test.
|
||||
return VerifyVersionInfo(
|
||||
&osvi,
|
||||
VER_MAJORVERSION | VER_MINORVERSION | VER_BUILDNUMBER,
|
||||
dwlConditionMask);
|
||||
}
|
||||
BOOL IsWin11OrGreater();
|
||||
|
||||
struct PACKAGE_VERSION
|
||||
{
|
||||
@@ -61,412 +35,30 @@ namespace package
|
||||
class ComInitializer
|
||||
{
|
||||
public:
|
||||
explicit ComInitializer(DWORD coInitFlags = COINIT_MULTITHREADED) :
|
||||
_initialized(false)
|
||||
{
|
||||
const HRESULT hr = CoInitializeEx(nullptr, coInitFlags);
|
||||
_initialized = SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
~ComInitializer()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
CoUninitialize();
|
||||
}
|
||||
}
|
||||
|
||||
bool Succeeded() const { return _initialized; }
|
||||
explicit ComInitializer(DWORD coInitFlags = COINIT_MULTITHREADED);
|
||||
~ComInitializer();
|
||||
bool Succeeded() const;
|
||||
|
||||
private:
|
||||
bool _initialized;
|
||||
};
|
||||
|
||||
inline bool GetPackageNameAndVersionFromAppx(
|
||||
bool GetPackageNameAndVersionFromAppx(
|
||||
const std::wstring& appxPath,
|
||||
std::wstring& outName,
|
||||
PACKAGE_VERSION& outVersion)
|
||||
{
|
||||
try
|
||||
{
|
||||
ComInitializer comInit;
|
||||
if (!comInit.Succeeded())
|
||||
{
|
||||
Logger::error(L"COM initialization failed.");
|
||||
return false;
|
||||
}
|
||||
PACKAGE_VERSION& outVersion);
|
||||
|
||||
ComPtr<IAppxFactory> factory;
|
||||
ComPtr<IStream> stream;
|
||||
ComPtr<IAppxPackageReader> reader;
|
||||
ComPtr<IAppxManifestReader> manifest;
|
||||
ComPtr<IAppxManifestPackageId> packageId;
|
||||
std::optional<Package> GetRegisteredPackage(std::wstring packageDisplayName, bool checkVersion);
|
||||
|
||||
HRESULT hr = CoCreateInstance(__uuidof(AppxFactory), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&factory));
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
bool IsPackageRegisteredWithPowerToysVersion(std::wstring packageDisplayName);
|
||||
|
||||
hr = SHCreateStreamOnFileEx(appxPath.c_str(), STGM_READ | STGM_SHARE_DENY_WRITE, FILE_ATTRIBUTE_NORMAL, FALSE, nullptr, &stream);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
bool RegisterSparsePackage(const std::wstring& externalLocation, const std::wstring& sparsePkgPath);
|
||||
|
||||
hr = factory->CreatePackageReader(stream.Get(), &reader);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
bool UnRegisterPackage(const std::wstring& pkgDisplayName);
|
||||
|
||||
hr = reader->GetManifest(&manifest);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
std::vector<std::wstring> FindMsixFile(const std::wstring& directoryPath, bool recursive);
|
||||
|
||||
hr = manifest->GetPackageId(&packageId);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
bool IsPackageSatisfied(const std::wstring& appxPath);
|
||||
|
||||
LPWSTR name = nullptr;
|
||||
hr = packageId->GetName(&name);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
UINT64 version = 0;
|
||||
hr = packageId->GetVersion(&version);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
outName = std::wstring(name);
|
||||
CoTaskMemFree(name);
|
||||
|
||||
outVersion.Major = static_cast<UINT16>((version >> 48) & 0xFFFF);
|
||||
outVersion.Minor = static_cast<UINT16>((version >> 32) & 0xFFFF);
|
||||
outVersion.Build = static_cast<UINT16>((version >> 16) & 0xFFFF);
|
||||
outVersion.Revision = static_cast<UINT16>(version & 0xFFFF);
|
||||
|
||||
Logger::info(L"Package name: {}, version: {}.{}.{}.{}, appxPath: {}",
|
||||
outName,
|
||||
outVersion.Major,
|
||||
outVersion.Minor,
|
||||
outVersion.Build,
|
||||
outVersion.Revision,
|
||||
appxPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
Logger::error(L"Standard exception: {}", winrt::to_hstring(ex.what()));
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Unknown or non-standard exception occurred.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
inline std::optional<Package> GetRegisteredPackage(std::wstring packageDisplayName, bool checkVersion)
|
||||
{
|
||||
PackageManager packageManager;
|
||||
|
||||
for (const auto& package : packageManager.FindPackagesForUser({}))
|
||||
{
|
||||
const auto& packageFullName = std::wstring{ package.Id().FullName() };
|
||||
const auto& packageVersion = package.Id().Version();
|
||||
|
||||
if (packageFullName.contains(packageDisplayName))
|
||||
{
|
||||
// If checkVersion is true, verify if the package has the same version as PowerToys.
|
||||
if ((!checkVersion) || (packageVersion.Major == VERSION_MAJOR && packageVersion.Minor == VERSION_MINOR && packageVersion.Revision == VERSION_REVISION))
|
||||
{
|
||||
return { package };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
inline bool IsPackageRegisteredWithPowerToysVersion(std::wstring packageDisplayName)
|
||||
{
|
||||
return GetRegisteredPackage(packageDisplayName, true).has_value();
|
||||
}
|
||||
|
||||
inline bool RegisterSparsePackage(const std::wstring& externalLocation, const std::wstring& sparsePkgPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
Uri externalUri{ externalLocation };
|
||||
Uri packageUri{ sparsePkgPath };
|
||||
|
||||
PackageManager packageManager;
|
||||
|
||||
// Declare use of an external location
|
||||
AddPackageOptions options;
|
||||
options.ExternalLocationUri(externalUri);
|
||||
options.ForceUpdateFromAnyVersion(true);
|
||||
|
||||
IAsyncOperationWithProgress<DeploymentResult, DeploymentProgress> deploymentOperation = packageManager.AddPackageByUriAsync(packageUri, options);
|
||||
deploymentOperation.get();
|
||||
|
||||
// Check the status of the operation
|
||||
if (deploymentOperation.Status() == AsyncStatus::Error)
|
||||
{
|
||||
auto deploymentResult{ deploymentOperation.GetResults() };
|
||||
auto errorCode = deploymentOperation.ErrorCode();
|
||||
auto errorText = deploymentResult.ErrorText();
|
||||
|
||||
Logger::error(L"Register {} package failed. ErrorCode: {}, ErrorText: {}", sparsePkgPath, std::to_wstring(errorCode), errorText);
|
||||
return false;
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Canceled)
|
||||
{
|
||||
Logger::error(L"Register {} package canceled.", sparsePkgPath);
|
||||
return false;
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Completed)
|
||||
{
|
||||
Logger::info(L"Register {} package completed.", sparsePkgPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"Register {} package started.", sparsePkgPath);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Logger::error("Exception thrown while trying to register package: {}", e.what());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool UnRegisterPackage(const std::wstring& pkgDisplayName)
|
||||
{
|
||||
try
|
||||
{
|
||||
PackageManager packageManager;
|
||||
const static auto packages = packageManager.FindPackagesForUser({});
|
||||
|
||||
for (auto const& package : packages)
|
||||
{
|
||||
const auto& packageFullName = std::wstring{ package.Id().FullName() };
|
||||
|
||||
if (packageFullName.contains(pkgDisplayName))
|
||||
{
|
||||
auto deploymentOperation{ packageManager.RemovePackageAsync(packageFullName) };
|
||||
deploymentOperation.get();
|
||||
|
||||
// Check the status of the operation
|
||||
if (deploymentOperation.Status() == AsyncStatus::Error)
|
||||
{
|
||||
auto deploymentResult{ deploymentOperation.GetResults() };
|
||||
auto errorCode = deploymentOperation.ErrorCode();
|
||||
auto errorText = deploymentResult.ErrorText();
|
||||
|
||||
Logger::error(L"Unregister {} package failed. ErrorCode: {}, ErrorText: {}", packageFullName, std::to_wstring(errorCode), errorText);
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Canceled)
|
||||
{
|
||||
Logger::error(L"Unregister {} package canceled.", packageFullName);
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Completed)
|
||||
{
|
||||
Logger::info(L"Unregister {} package completed.", packageFullName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"Unregister {} package started.", packageFullName);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Logger::error("Exception thrown while trying to unregister package: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::vector<std::wstring> FindMsixFile(const std::wstring& directoryPath, bool recursive)
|
||||
{
|
||||
if (directoryPath.empty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!std::filesystem::exists(directoryPath))
|
||||
{
|
||||
Logger::error(L"The directory '" + directoryPath + L"' does not exist.");
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::regex pattern(R"(^.+\.(appx|msix|msixbundle)$)", std::regex_constants::icase);
|
||||
std::vector<std::wstring> matchedFiles;
|
||||
|
||||
try
|
||||
{
|
||||
if (recursive)
|
||||
{
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(directoryPath))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
const auto& fileName = entry.path().filename().string();
|
||||
if (std::regex_match(fileName, pattern))
|
||||
{
|
||||
matchedFiles.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const auto& entry : std::filesystem::directory_iterator(directoryPath))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
const auto& fileName = entry.path().filename().string();
|
||||
if (std::regex_match(fileName, pattern))
|
||||
{
|
||||
matchedFiles.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
Logger::error("An error occurred while searching for MSIX files: " + std::string(ex.what()));
|
||||
}
|
||||
|
||||
return matchedFiles;
|
||||
}
|
||||
|
||||
inline bool IsPackageSatisfied(const std::wstring& appxPath)
|
||||
{
|
||||
std::wstring targetName;
|
||||
PACKAGE_VERSION targetVersion{};
|
||||
|
||||
if (!GetPackageNameAndVersionFromAppx(appxPath, targetName, targetVersion))
|
||||
{
|
||||
Logger::error(L"Failed to get package name and version from appx: " + appxPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
PackageManager pm;
|
||||
|
||||
for (const auto& package : pm.FindPackagesForUser({}))
|
||||
{
|
||||
const auto& id = package.Id();
|
||||
if (std::wstring(id.Name()) == targetName)
|
||||
{
|
||||
const auto& version = id.Version();
|
||||
|
||||
if (version.Major > targetVersion.Major ||
|
||||
(version.Major == targetVersion.Major && version.Minor > targetVersion.Minor) ||
|
||||
(version.Major == targetVersion.Major && version.Minor == targetVersion.Minor && version.Build > targetVersion.Build) ||
|
||||
(version.Major == targetVersion.Major && version.Minor == targetVersion.Minor && version.Build == targetVersion.Build && version.Revision >= targetVersion.Revision))
|
||||
{
|
||||
Logger::info(
|
||||
L"Package {} is already satisfied with version {}.{}.{}.{}; target version {}.{}.{}.{}; appxPath: {}",
|
||||
id.Name(),
|
||||
version.Major,
|
||||
version.Minor,
|
||||
version.Build,
|
||||
version.Revision,
|
||||
targetVersion.Major,
|
||||
targetVersion.Minor,
|
||||
targetVersion.Build,
|
||||
targetVersion.Revision,
|
||||
appxPath);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger::info(
|
||||
L"Package {} is not satisfied. Target version: {}.{}.{}.{}; appxPath: {}",
|
||||
targetName,
|
||||
targetVersion.Major,
|
||||
targetVersion.Minor,
|
||||
targetVersion.Build,
|
||||
targetVersion.Revision,
|
||||
appxPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool RegisterPackage(std::wstring pkgPath, std::vector<std::wstring> dependencies)
|
||||
{
|
||||
try
|
||||
{
|
||||
Uri packageUri{ pkgPath };
|
||||
|
||||
PackageManager packageManager;
|
||||
|
||||
// Declare use of an external location
|
||||
DeploymentOptions options = DeploymentOptions::ForceTargetApplicationShutdown;
|
||||
|
||||
Collections::IVector<Uri> uris = winrt::single_threaded_vector<Uri>();
|
||||
if (!dependencies.empty())
|
||||
{
|
||||
for (const auto& dependency : dependencies)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsPackageSatisfied(dependency))
|
||||
{
|
||||
Logger::info(L"Dependency already satisfied: {}", dependency);
|
||||
}
|
||||
else
|
||||
{
|
||||
uris.Append(Uri(dependency));
|
||||
}
|
||||
}
|
||||
catch (const winrt::hresult_error& ex)
|
||||
{
|
||||
Logger::error(L"Error creating Uri for dependency: %s", ex.message().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IAsyncOperationWithProgress<DeploymentResult, DeploymentProgress> deploymentOperation = packageManager.AddPackageAsync(packageUri, uris, options);
|
||||
deploymentOperation.get();
|
||||
|
||||
// Check the status of the operation
|
||||
if (deploymentOperation.Status() == AsyncStatus::Error)
|
||||
{
|
||||
auto deploymentResult{ deploymentOperation.GetResults() };
|
||||
auto errorCode = deploymentOperation.ErrorCode();
|
||||
auto errorText = deploymentResult.ErrorText();
|
||||
|
||||
Logger::error(L"Register {} package failed. ErrorCode: {}, ErrorText: {}", pkgPath, std::to_wstring(errorCode), errorText);
|
||||
return false;
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Canceled)
|
||||
{
|
||||
Logger::error(L"Register {} package canceled.", pkgPath);
|
||||
return false;
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Completed)
|
||||
{
|
||||
Logger::info(L"Register {} package completed.", pkgPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"Register {} package started.", pkgPath);
|
||||
}
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Logger::error("Exception thrown while trying to register package: {}", e.what());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
bool RegisterPackage(std::wstring pkgPath, std::vector<std::wstring> dependencies);
|
||||
}
|
||||
|
||||
5
src/common/utils/packages.config
Normal file
5
src/common/utils/packages.config
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
|
||||
</packages>
|
||||
1
src/common/utils/pch.cpp
Normal file
1
src/common/utils/pch.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "pch.h"
|
||||
38
src/common/utils/pch.h
Normal file
38
src/common/utils/pch.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
#include <sddl.h>
|
||||
#include <shldisp.h>
|
||||
#include <shlobj.h>
|
||||
#include <Shlwapi.h>
|
||||
#include <exdisp.h>
|
||||
#include <atlbase.h>
|
||||
#include <comdef.h>
|
||||
#include <appxpackaging.h>
|
||||
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
#include <winrt/Windows.ApplicationModel.h>
|
||||
#include <winrt/Windows.Management.Deployment.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
#include <regex>
|
||||
#include <fstream>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
|
||||
#include <wil/result.h>
|
||||
#include <wil/com.h>
|
||||
#include <wil/resource.h>
|
||||
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/version/version.h>
|
||||
44
src/common/utils/processApi.cpp
Normal file
44
src/common/utils/processApi.cpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#include "pch.h"
|
||||
#include "processApi.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
std::vector<wil::unique_process_handle> getProcessHandlesByName(std::wstring_view processName, DWORD handleAccess)
|
||||
{
|
||||
std::vector<wil::unique_process_handle> result;
|
||||
DWORD bytesRequired = 0;
|
||||
std::vector<DWORD> processIds;
|
||||
processIds.resize(4096 / sizeof(processIds[0]));
|
||||
auto processIdSize = static_cast<DWORD>(processIds.size() * sizeof(processIds[0]));
|
||||
EnumProcesses(processIds.data(), processIdSize, &bytesRequired);
|
||||
while (bytesRequired == processIdSize)
|
||||
{
|
||||
processIdSize *= 2;
|
||||
processIds.resize(processIdSize / sizeof(processIds[0]));
|
||||
EnumProcesses(processIds.data(), processIdSize, &bytesRequired);
|
||||
}
|
||||
processIds.resize(bytesRequired / sizeof(processIds[0]));
|
||||
|
||||
handleAccess |= PROCESS_QUERY_LIMITED_INFORMATION;
|
||||
for (const DWORD processId : processIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
wil::unique_process_handle hProcess{ OpenProcess(handleAccess, FALSE, processId) };
|
||||
wchar_t name[MAX_PATH + 1];
|
||||
DWORD length = MAX_PATH;
|
||||
if (!hProcess || !QueryFullProcessImageNameW(hProcess.get(), 0, name, &length))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (processName == PathFindFileNameW(name))
|
||||
{
|
||||
result.push_back(std::move(hProcess));
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -6,42 +6,4 @@
|
||||
#include <Psapi.h>
|
||||
#include <string_view>
|
||||
|
||||
inline std::vector<wil::unique_process_handle> getProcessHandlesByName(const std::wstring_view processName, DWORD handleAccess)
|
||||
{
|
||||
std::vector<wil::unique_process_handle> result;
|
||||
DWORD bytesRequired;
|
||||
std::vector<DWORD> processIds;
|
||||
processIds.resize(4096 / sizeof(processIds[0]));
|
||||
auto processIdSize = static_cast<DWORD>(size(processIds) * sizeof(processIds[0]));
|
||||
EnumProcesses(processIds.data(), processIdSize, &bytesRequired);
|
||||
while (bytesRequired == processIdSize)
|
||||
{
|
||||
processIdSize *= 2;
|
||||
processIds.resize(processIdSize / sizeof(processIds[0]));
|
||||
EnumProcesses(processIds.data(), processIdSize, &bytesRequired);
|
||||
}
|
||||
processIds.resize(bytesRequired / sizeof(processIds[0]));
|
||||
|
||||
handleAccess |= PROCESS_QUERY_LIMITED_INFORMATION;
|
||||
for (const DWORD processId : processIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
wil::unique_process_handle hProcess{ OpenProcess(handleAccess, FALSE, processId) };
|
||||
wchar_t name[MAX_PATH + 1];
|
||||
DWORD length = MAX_PATH;
|
||||
if (!hProcess || !QueryFullProcessImageNameW(hProcess.get(), 0, name, &length))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (processName == PathFindFileNameW(name))
|
||||
{
|
||||
result.push_back(std::move(hProcess));
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
std::vector<wil::unique_process_handle> getProcessHandlesByName(std::wstring_view processName, DWORD handleAccess);
|
||||
|
||||
111
src/common/utils/process_path.cpp
Normal file
111
src/common/utils/process_path.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#include "pch.h"
|
||||
#include "process_path.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
std::wstring get_process_path(DWORD pid) noexcept
|
||||
{
|
||||
wil::unique_handle process{ OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, TRUE, pid) };
|
||||
std::wstring name;
|
||||
if (process)
|
||||
{
|
||||
name.resize(MAX_PATH);
|
||||
DWORD nameLength = static_cast<DWORD>(name.length());
|
||||
if (QueryFullProcessImageNameW(process.get(), 0, name.data(), &nameLength) == 0)
|
||||
{
|
||||
nameLength = 0;
|
||||
}
|
||||
name.resize(nameLength);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
std::wstring get_process_path(HWND window) noexcept
|
||||
{
|
||||
const static std::wstring appFrameHost = L"ApplicationFrameHost.exe";
|
||||
|
||||
DWORD pid{};
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
auto name = get_process_path(pid);
|
||||
|
||||
if (name.length() >= appFrameHost.length() &&
|
||||
name.compare(name.length() - appFrameHost.length(), appFrameHost.length(), appFrameHost) == 0)
|
||||
{
|
||||
DWORD newPid = pid;
|
||||
|
||||
EnumChildWindows(
|
||||
window,
|
||||
[](HWND hwnd, LPARAM param) -> BOOL {
|
||||
auto newPidPtr = reinterpret_cast<DWORD*>(param);
|
||||
DWORD childPid;
|
||||
GetWindowThreadProcessId(hwnd, &childPid);
|
||||
if (childPid != *newPidPtr)
|
||||
{
|
||||
*newPidPtr = childPid;
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
},
|
||||
reinterpret_cast<LPARAM>(&newPid));
|
||||
|
||||
if (newPid != pid)
|
||||
{
|
||||
return get_process_path(newPid);
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
std::wstring get_process_path_waiting_uwp(HWND window)
|
||||
{
|
||||
const static std::wstring appFrameHost = L"ApplicationFrameHost.exe";
|
||||
|
||||
int attempt = 0;
|
||||
auto processPath = get_process_path(window);
|
||||
|
||||
while (++attempt < 30 && processPath.length() >= appFrameHost.length() &&
|
||||
processPath.compare(processPath.length() - appFrameHost.length(), appFrameHost.length(), appFrameHost) == 0)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
processPath = get_process_path(window);
|
||||
}
|
||||
|
||||
return processPath;
|
||||
}
|
||||
|
||||
std::wstring get_module_filename(HMODULE mod)
|
||||
{
|
||||
wchar_t buffer[MAX_PATH + 1];
|
||||
DWORD actualLength = GetModuleFileNameW(mod, buffer, MAX_PATH);
|
||||
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
const DWORD longPathLength = 0xFFFF;
|
||||
std::wstring longFilename(longPathLength, L'\0');
|
||||
actualLength = GetModuleFileNameW(mod, longFilename.data(), longPathLength);
|
||||
return longFilename.substr(0, actualLength);
|
||||
}
|
||||
return { buffer, actualLength };
|
||||
}
|
||||
|
||||
std::wstring get_module_folderpath(HMODULE mod, bool removeFilename)
|
||||
{
|
||||
wchar_t buffer[MAX_PATH + 1];
|
||||
DWORD actualLength = GetModuleFileNameW(mod, buffer, MAX_PATH);
|
||||
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
const DWORD longPathLength = 0xFFFF;
|
||||
std::wstring longFilename(longPathLength, L'\0');
|
||||
actualLength = GetModuleFileNameW(mod, longFilename.data(), longPathLength);
|
||||
PathRemoveFileSpecW(longFilename.data());
|
||||
longFilename.resize(std::wcslen(longFilename.data()));
|
||||
longFilename.shrink_to_fit();
|
||||
return longFilename;
|
||||
}
|
||||
|
||||
if (removeFilename)
|
||||
{
|
||||
PathRemoveFileSpecW(buffer);
|
||||
}
|
||||
return { buffer, static_cast<uint64_t>(lstrlenW(buffer)) };
|
||||
}
|
||||
@@ -7,116 +7,13 @@
|
||||
#include <thread>
|
||||
|
||||
// Get the executable path or module name for modern apps
|
||||
inline std::wstring get_process_path(DWORD pid) noexcept
|
||||
{
|
||||
auto process = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, TRUE, pid);
|
||||
std::wstring name;
|
||||
if (process != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
name.resize(MAX_PATH);
|
||||
DWORD name_length = static_cast<DWORD>(name.length());
|
||||
if (QueryFullProcessImageNameW(process, 0, name.data(), &name_length) == 0)
|
||||
{
|
||||
name_length = 0;
|
||||
}
|
||||
name.resize(name_length);
|
||||
CloseHandle(process);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
std::wstring get_process_path(DWORD pid) noexcept;
|
||||
|
||||
// Get the executable path or module name for modern apps
|
||||
inline std::wstring get_process_path(HWND window) noexcept
|
||||
{
|
||||
const static std::wstring app_frame_host = L"ApplicationFrameHost.exe";
|
||||
std::wstring get_process_path(HWND window) noexcept;
|
||||
|
||||
DWORD pid{};
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
auto name = get_process_path(pid);
|
||||
std::wstring get_process_path_waiting_uwp(HWND window);
|
||||
|
||||
if (name.length() >= app_frame_host.length() &&
|
||||
name.compare(name.length() - app_frame_host.length(), app_frame_host.length(), app_frame_host) == 0)
|
||||
{
|
||||
// It is a UWP app. We will enumerate the windows and look for one created
|
||||
// by something with a different PID
|
||||
DWORD new_pid = pid;
|
||||
std::wstring get_module_filename(HMODULE mod = nullptr);
|
||||
|
||||
EnumChildWindows(
|
||||
window, [](HWND hwnd, LPARAM param) -> BOOL {
|
||||
auto new_pid_ptr = reinterpret_cast<DWORD*>(param);
|
||||
DWORD pid;
|
||||
GetWindowThreadProcessId(hwnd, &pid);
|
||||
if (pid != *new_pid_ptr)
|
||||
{
|
||||
*new_pid_ptr = pid;
|
||||
return FALSE;
|
||||
}
|
||||
else
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
},
|
||||
reinterpret_cast<LPARAM>(&new_pid));
|
||||
|
||||
// If we have a new pid, get the new name.
|
||||
if (new_pid != pid)
|
||||
{
|
||||
return get_process_path(new_pid);
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
inline std::wstring get_process_path_waiting_uwp(HWND window)
|
||||
{
|
||||
const static std::wstring appFrameHost = L"ApplicationFrameHost.exe";
|
||||
|
||||
int attempt = 0;
|
||||
auto processPath = get_process_path(window);
|
||||
|
||||
while (++attempt < 30 && processPath.length() >= appFrameHost.length() &&
|
||||
processPath.compare(processPath.length() - appFrameHost.length(), appFrameHost.length(), appFrameHost) == 0)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
processPath = get_process_path(window);
|
||||
}
|
||||
|
||||
return processPath;
|
||||
}
|
||||
|
||||
inline std::wstring get_module_filename(HMODULE mod = nullptr)
|
||||
{
|
||||
wchar_t buffer[MAX_PATH + 1];
|
||||
DWORD actual_length = GetModuleFileNameW(mod, buffer, MAX_PATH);
|
||||
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
const DWORD long_path_length = 0xFFFF; // should be always enough
|
||||
std::wstring long_filename(long_path_length, L'\0');
|
||||
actual_length = GetModuleFileNameW(mod, long_filename.data(), long_path_length);
|
||||
return long_filename.substr(0, actual_length);
|
||||
}
|
||||
return { buffer, actual_length };
|
||||
}
|
||||
|
||||
inline std::wstring get_module_folderpath(HMODULE mod = nullptr, const bool removeFilename = true)
|
||||
{
|
||||
wchar_t buffer[MAX_PATH + 1];
|
||||
DWORD actual_length = GetModuleFileNameW(mod, buffer, MAX_PATH);
|
||||
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
const DWORD long_path_length = 0xFFFF; // should be always enough
|
||||
std::wstring long_filename(long_path_length, L'\0');
|
||||
actual_length = GetModuleFileNameW(mod, long_filename.data(), long_path_length);
|
||||
PathRemoveFileSpecW(long_filename.data());
|
||||
long_filename.resize(std::wcslen(long_filename.data()));
|
||||
long_filename.shrink_to_fit();
|
||||
return long_filename;
|
||||
}
|
||||
|
||||
if (removeFilename)
|
||||
{
|
||||
PathRemoveFileSpecW(buffer);
|
||||
}
|
||||
return { buffer, static_cast<uint64_t>(lstrlenW(buffer))};
|
||||
}
|
||||
std::wstring get_module_folderpath(HMODULE mod = nullptr, bool removeFilename = true);
|
||||
|
||||
398
src/common/utils/registry.cpp
Normal file
398
src/common/utils/registry.cpp
Normal file
@@ -0,0 +1,398 @@
|
||||
#include "pch.h"
|
||||
#include "registry.h"
|
||||
|
||||
namespace registry
|
||||
{
|
||||
namespace install_scope
|
||||
{
|
||||
const InstallScope get_current_install_scope()
|
||||
{
|
||||
// Open HKLM key
|
||||
HKEY perMachineKey{};
|
||||
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
|
||||
INSTALL_SCOPE_REG_KEY,
|
||||
0,
|
||||
KEY_READ,
|
||||
&perMachineKey) != ERROR_SUCCESS)
|
||||
{
|
||||
// Open HKCU key
|
||||
HKEY perUserKey{};
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER,
|
||||
INSTALL_SCOPE_REG_KEY,
|
||||
0,
|
||||
KEY_READ,
|
||||
&perUserKey) != ERROR_SUCCESS)
|
||||
{
|
||||
// both keys are missing
|
||||
return InstallScope::PerMachine;
|
||||
}
|
||||
else
|
||||
{
|
||||
DWORD dataSize{};
|
||||
if (RegGetValueW(
|
||||
perUserKey,
|
||||
nullptr,
|
||||
L"InstallScope",
|
||||
RRF_RT_REG_SZ,
|
||||
nullptr,
|
||||
nullptr,
|
||||
&dataSize) != ERROR_SUCCESS)
|
||||
{
|
||||
// HKCU key is missing
|
||||
RegCloseKey(perUserKey);
|
||||
return InstallScope::PerMachine;
|
||||
}
|
||||
|
||||
std::wstring data;
|
||||
data.resize(dataSize / sizeof(wchar_t));
|
||||
|
||||
if (RegGetValueW(
|
||||
perUserKey,
|
||||
nullptr,
|
||||
L"InstallScope",
|
||||
RRF_RT_REG_SZ,
|
||||
nullptr,
|
||||
&data[0],
|
||||
&dataSize) != ERROR_SUCCESS)
|
||||
{
|
||||
// HKCU key is missing
|
||||
RegCloseKey(perUserKey);
|
||||
return InstallScope::PerMachine;
|
||||
}
|
||||
RegCloseKey(perUserKey);
|
||||
|
||||
if (data.contains(L"perUser"))
|
||||
{
|
||||
return InstallScope::PerUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return InstallScope::PerMachine;
|
||||
}
|
||||
}
|
||||
|
||||
namespace detail
|
||||
{
|
||||
const wchar_t* getScopeName(HKEY scope)
|
||||
{
|
||||
if (scope == HKEY_LOCAL_MACHINE)
|
||||
{
|
||||
return L"HKLM";
|
||||
}
|
||||
else if (scope == HKEY_CURRENT_USER)
|
||||
{
|
||||
return L"HKCU";
|
||||
}
|
||||
else if (scope == HKEY_CLASSES_ROOT)
|
||||
{
|
||||
return L"HKCR";
|
||||
}
|
||||
else
|
||||
{
|
||||
return L"HK??";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring ValueChange::toString() const
|
||||
{
|
||||
using namespace detail;
|
||||
|
||||
std::wstring value_str;
|
||||
std::visit(overloaded{ [&](DWORD value) {
|
||||
std::wostringstream oss;
|
||||
oss << value;
|
||||
value_str = oss.str();
|
||||
},
|
||||
[&](const std::wstring& value) { value_str = value; } },
|
||||
value);
|
||||
|
||||
return fmt::format(L"{}\\{}\\{}:{}", detail::getScopeName(scope), path, name ? *name : L"Default", value_str);
|
||||
}
|
||||
|
||||
bool ValueChange::isApplied() const
|
||||
{
|
||||
HKEY key{};
|
||||
if (auto res = RegOpenKeyExW(scope, path.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::info(L"isApplied of {}: RegOpenKeyExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
detail::on_exit closeKey{ [key] { RegCloseKey(key); } };
|
||||
|
||||
const DWORD expectedType = valueTypeToWinapiType(value);
|
||||
|
||||
DWORD retrievedType{};
|
||||
wchar_t buffer[VALUE_BUFFER_SIZE];
|
||||
DWORD valueSize = sizeof(buffer);
|
||||
if (auto res = RegQueryValueExW(key,
|
||||
name.has_value() ? name->c_str() : nullptr,
|
||||
0,
|
||||
&retrievedType,
|
||||
reinterpret_cast<LPBYTE>(&buffer),
|
||||
&valueSize);
|
||||
res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::info(L"isApplied of {}: RegQueryValueExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expectedType != retrievedType)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (const auto retrievedValue = bufferToValue(buffer, valueSize, retrievedType))
|
||||
{
|
||||
return value == retrievedValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ValueChange::apply() const
|
||||
{
|
||||
HKEY key{};
|
||||
|
||||
if (auto res = RegCreateKeyExW(scope, path.c_str(), 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &key, nullptr); res !=
|
||||
ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"apply of {}: RegCreateKeyExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
detail::on_exit closeKey{ [key] { RegCloseKey(key); } };
|
||||
|
||||
wchar_t buffer[VALUE_BUFFER_SIZE];
|
||||
DWORD valueSize;
|
||||
DWORD valueType;
|
||||
|
||||
valueToBuffer(value, buffer, valueSize, valueType);
|
||||
if (auto res = RegSetValueExW(key,
|
||||
name.has_value() ? name->c_str() : nullptr,
|
||||
0,
|
||||
valueType,
|
||||
reinterpret_cast<BYTE*>(buffer),
|
||||
valueSize);
|
||||
res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"apply of {}: RegSetValueExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ValueChange::unApply() const
|
||||
{
|
||||
HKEY key{};
|
||||
if (auto res = RegOpenKeyExW(scope, path.c_str(), 0, KEY_ALL_ACCESS, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"unApply of {}: RegOpenKeyExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
detail::on_exit closeKey{ [key] { RegCloseKey(key); } };
|
||||
|
||||
// delete the value itself
|
||||
if (auto res = RegDeleteKeyValueW(scope, path.c_str(), name.has_value() ? name->c_str() : nullptr); res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"unApply of {}: RegDeleteKeyValueW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the path doesn't contain anything and delete it if so
|
||||
DWORD nValues = 0;
|
||||
DWORD maxValueLen = 0;
|
||||
const auto ok =
|
||||
RegQueryInfoKeyW(
|
||||
key, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, &nValues, nullptr, &maxValueLen, nullptr, nullptr) ==
|
||||
ERROR_SUCCESS;
|
||||
|
||||
if (ok && (!nValues || !maxValueLen))
|
||||
{
|
||||
RegDeleteTreeW(scope, path.c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
DWORD ValueChange::valueTypeToWinapiType(const value_t& v)
|
||||
{
|
||||
return std::visit(
|
||||
[](auto&& arg) {
|
||||
using T = std::decay_t<decltype(arg)>;
|
||||
if constexpr (std::is_same_v<T, DWORD>)
|
||||
return REG_DWORD;
|
||||
else if constexpr (std::is_same_v<T, std::wstring>)
|
||||
return REG_SZ;
|
||||
else
|
||||
static_assert(always_false_v<T>, "support for this registry type is not implemented");
|
||||
},
|
||||
v);
|
||||
}
|
||||
|
||||
void ValueChange::valueToBuffer(const value_t& value, wchar_t buffer[VALUE_BUFFER_SIZE], DWORD& valueSize, DWORD& type)
|
||||
{
|
||||
using detail::overloaded;
|
||||
|
||||
std::visit(overloaded{ [&](DWORD value) {
|
||||
*reinterpret_cast<DWORD*>(buffer) = value;
|
||||
type = REG_DWORD;
|
||||
valueSize = sizeof(value);
|
||||
},
|
||||
[&](const std::wstring& value) {
|
||||
assert(value.size() < VALUE_BUFFER_SIZE);
|
||||
value.copy(buffer, value.size());
|
||||
type = REG_SZ;
|
||||
valueSize = static_cast<DWORD>(sizeof(wchar_t) * value.size());
|
||||
} },
|
||||
value);
|
||||
}
|
||||
|
||||
std::optional<ValueChange::value_t> ValueChange::bufferToValue(const wchar_t buffer[VALUE_BUFFER_SIZE],
|
||||
const DWORD valueSize,
|
||||
const DWORD type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case REG_DWORD:
|
||||
return *reinterpret_cast<const DWORD*>(buffer);
|
||||
case REG_SZ:
|
||||
{
|
||||
if (!valueSize)
|
||||
{
|
||||
return std::wstring{};
|
||||
}
|
||||
std::wstring result{ buffer, valueSize / sizeof(wchar_t) };
|
||||
while (result[result.size() - 1] == L'\0')
|
||||
{
|
||||
result.resize(result.size() - 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
bool ChangeSet::isApplied() const
|
||||
{
|
||||
for (const auto& c : changes)
|
||||
{
|
||||
if (c.required && !c.isApplied())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChangeSet::apply() const
|
||||
{
|
||||
bool ok = true;
|
||||
for (const auto& c : changes)
|
||||
{
|
||||
ok = (c.apply()||!c.required) && ok;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool ChangeSet::unApply() const
|
||||
{
|
||||
bool ok = true;
|
||||
for (const auto& c : changes)
|
||||
{
|
||||
ok = (c.unApply()||!c.required) && ok;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
namespace shellex
|
||||
{
|
||||
registry::ChangeSet generatePreviewHandler(const PreviewHandlerType handlerType,
|
||||
const bool perUser,
|
||||
std::wstring handlerClsid,
|
||||
std::wstring powertoysVersion,
|
||||
std::wstring fullPathToHandler,
|
||||
std::wstring className,
|
||||
std::wstring displayName,
|
||||
std::vector<std::wstring> fileTypes,
|
||||
std::wstring perceivedType,
|
||||
std::wstring fileKindType)
|
||||
{
|
||||
const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
|
||||
|
||||
std::wstring clsidPath = L"Software\\Classes\\CLSID";
|
||||
clsidPath += L'\\';
|
||||
clsidPath += handlerClsid;
|
||||
|
||||
std::wstring inprocServerPath = clsidPath;
|
||||
inprocServerPath += L'\\';
|
||||
inprocServerPath += L"InprocServer32";
|
||||
|
||||
std::wstring assemblyKeyValue;
|
||||
if (const auto lastDotPos = className.rfind(L'.'); lastDotPos != std::wstring::npos)
|
||||
{
|
||||
assemblyKeyValue = L"PowerToys." + className.substr(lastDotPos + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
assemblyKeyValue = L"PowerToys." + className;
|
||||
}
|
||||
|
||||
assemblyKeyValue += L", Version=";
|
||||
assemblyKeyValue += powertoysVersion;
|
||||
assemblyKeyValue += L", Culture=neutral";
|
||||
|
||||
std::wstring versionPath = inprocServerPath;
|
||||
versionPath += L'\\';
|
||||
versionPath += powertoysVersion;
|
||||
|
||||
using vec_t = std::vector<registry::ValueChange>;
|
||||
// TODO: verify that we actually need all of those
|
||||
vec_t changes = { { scope, clsidPath, L"DisplayName", displayName },
|
||||
{ scope, clsidPath, std::nullopt, className },
|
||||
{ scope, inprocServerPath, std::nullopt, fullPathToHandler },
|
||||
{ scope, inprocServerPath, L"Assembly", assemblyKeyValue },
|
||||
{ scope, inprocServerPath, L"Class", className },
|
||||
{ scope, inprocServerPath, L"ThreadingModel", L"Apartment" } };
|
||||
|
||||
for (const auto& fileType : fileTypes)
|
||||
{
|
||||
std::wstring fileTypePath = L"Software\\Classes\\" + fileType;
|
||||
std::wstring fileAssociationPath = fileTypePath + L"\\shellex\\";
|
||||
fileAssociationPath += handlerType == PreviewHandlerType::preview ? IPREVIEW_HANDLER_CLSID : ITHUMBNAIL_PROVIDER_CLSID;
|
||||
changes.push_back({ scope, fileAssociationPath, std::nullopt, handlerClsid });
|
||||
if (!fileKindType.empty())
|
||||
{
|
||||
// Registering a file type as a kind needs to be done at the HKEY_LOCAL_MACHINE level.
|
||||
// Make it optional as well so that we don't fail registering the handler if we can't write to HKEY_LOCAL_MACHINE.
|
||||
std::wstring kindMapPath = L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\KindMap";
|
||||
changes.push_back({ HKEY_LOCAL_MACHINE, kindMapPath, fileType, fileKindType, false});
|
||||
}
|
||||
if (!perceivedType.empty())
|
||||
{
|
||||
changes.push_back({ scope, fileTypePath, L"PerceivedType", perceivedType });
|
||||
}
|
||||
if (handlerType == PreviewHandlerType::preview && fileType == L".reg")
|
||||
{
|
||||
// this regfile registry key has precedence over Software\Classes\.reg for .reg files
|
||||
std::wstring regfilePath = L"Software\\Classes\\regfile\\shellex\\" + IPREVIEW_HANDLER_CLSID + L"\\";
|
||||
changes.push_back({ scope, regfilePath, std::nullopt, handlerClsid });
|
||||
}
|
||||
}
|
||||
|
||||
if (handlerType == PreviewHandlerType::preview)
|
||||
{
|
||||
const std::wstring previewHostClsid = L"{6d2b5079-2f0b-48dd-ab7f-97cec514d30b}";
|
||||
const std::wstring previewHandlerListPath = LR"(Software\Microsoft\Windows\CurrentVersion\PreviewHandlers)";
|
||||
|
||||
changes.push_back({ scope, clsidPath, L"AppID", previewHostClsid });
|
||||
changes.push_back({ scope, previewHandlerListPath, handlerClsid, displayName });
|
||||
}
|
||||
|
||||
return registry::ChangeSet{ .changes = std::move(changes) };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,54 @@
|
||||
|
||||
namespace registry
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
struct on_exit
|
||||
{
|
||||
std::function<void()> f;
|
||||
|
||||
on_exit(std::function<void()> f) :
|
||||
f{ std::move(f) } {}
|
||||
~on_exit() { f(); }
|
||||
};
|
||||
|
||||
template<class... Ts>
|
||||
struct overloaded : Ts...
|
||||
{
|
||||
using Ts::operator()...;
|
||||
};
|
||||
|
||||
template<class... Ts>
|
||||
overloaded(Ts...) -> overloaded<Ts...>;
|
||||
|
||||
inline const wchar_t* getScopeName(HKEY scope)
|
||||
{
|
||||
if (scope == HKEY_LOCAL_MACHINE)
|
||||
{
|
||||
return L"HKLM";
|
||||
}
|
||||
else if (scope == HKEY_CURRENT_USER)
|
||||
{
|
||||
return L"HKCU";
|
||||
}
|
||||
else if (scope == HKEY_CLASSES_ROOT)
|
||||
{
|
||||
return L"HKCR";
|
||||
}
|
||||
else
|
||||
{
|
||||
return L"HK??";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace install_scope
|
||||
{
|
||||
const wchar_t INSTALL_SCOPE_REG_KEY[] = L"Software\\Classes\\powertoys\\";
|
||||
const wchar_t UNINSTALL_REG_KEY[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
|
||||
|
||||
// Bundle UpgradeCode from PowerToys.wxs (with braces as stored in registry)
|
||||
const wchar_t BUNDLE_UPGRADE_CODE[] = L"{6341382D-C0A9-4238-9188-BE9607E3FAB2}";
|
||||
|
||||
enum class InstallScope
|
||||
{
|
||||
@@ -26,8 +71,67 @@ namespace registry
|
||||
PerUser,
|
||||
};
|
||||
|
||||
// Helper function to find PowerToys bundle in Windows Uninstall registry by BundleUpgradeCode
|
||||
inline bool find_powertoys_bundle_in_uninstall_registry(HKEY rootKey)
|
||||
{
|
||||
HKEY uninstallKey{};
|
||||
if (RegOpenKeyExW(rootKey, UNINSTALL_REG_KEY, 0, KEY_READ, &uninstallKey) != ERROR_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
detail::on_exit closeUninstallKey{ [uninstallKey] { RegCloseKey(uninstallKey); } };
|
||||
|
||||
DWORD index = 0;
|
||||
wchar_t subKeyName[256];
|
||||
|
||||
// Enumerate all subkeys under Uninstall
|
||||
while (RegEnumKeyW(uninstallKey, index++, subKeyName, 256) == ERROR_SUCCESS)
|
||||
{
|
||||
HKEY productKey{};
|
||||
if (RegOpenKeyExW(uninstallKey, subKeyName, 0, KEY_READ, &productKey) != ERROR_SUCCESS)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
detail::on_exit closeProductKey{ [productKey] { RegCloseKey(productKey); } };
|
||||
|
||||
// Check BundleUpgradeCode value (specific to WiX Bundle installations)
|
||||
wchar_t bundleUpgradeCode[256]{};
|
||||
DWORD bundleUpgradeCodeSize = sizeof(bundleUpgradeCode);
|
||||
|
||||
if (RegQueryValueExW(productKey, L"BundleUpgradeCode", nullptr, nullptr,
|
||||
reinterpret_cast<LPBYTE>(bundleUpgradeCode), &bundleUpgradeCodeSize) == ERROR_SUCCESS)
|
||||
{
|
||||
if (_wcsicmp(bundleUpgradeCode, BUNDLE_UPGRADE_CODE) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
inline const InstallScope get_current_install_scope()
|
||||
{
|
||||
// 1. Check HKCU Uninstall registry first (user-level bundle)
|
||||
// Note: MSI components are always in HKLM regardless of install scope,
|
||||
// but the Bundle entry will be in HKCU for per-user installations
|
||||
if (find_powertoys_bundle_in_uninstall_registry(HKEY_CURRENT_USER))
|
||||
{
|
||||
Logger::info(L"Found user-level PowerToys bundle via BundleUpgradeCode in HKCU");
|
||||
return InstallScope::PerUser;
|
||||
}
|
||||
|
||||
// 2. Check HKLM Uninstall registry (machine-level bundle)
|
||||
if (find_powertoys_bundle_in_uninstall_registry(HKEY_LOCAL_MACHINE))
|
||||
{
|
||||
Logger::info(L"Found machine-level PowerToys bundle via BundleUpgradeCode in HKLM");
|
||||
return InstallScope::PerMachine;
|
||||
}
|
||||
|
||||
// 3. Fallback to legacy custom registry key detection
|
||||
Logger::info(L"PowerToys bundle not found in Uninstall registry, falling back to legacy detection");
|
||||
|
||||
// Open HKLM key
|
||||
HKEY perMachineKey{};
|
||||
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
|
||||
@@ -45,6 +149,7 @@ namespace registry
|
||||
&perUserKey) != ERROR_SUCCESS)
|
||||
{
|
||||
// both keys are missing
|
||||
Logger::warn(L"No PowerToys installation detected, defaulting to PerMachine");
|
||||
return InstallScope::PerMachine;
|
||||
}
|
||||
else
|
||||
@@ -96,47 +201,6 @@ namespace registry
|
||||
template<class>
|
||||
inline constexpr bool always_false_v = false;
|
||||
|
||||
namespace detail
|
||||
{
|
||||
struct on_exit
|
||||
{
|
||||
std::function<void()> f;
|
||||
|
||||
on_exit(std::function<void()> f) :
|
||||
f{ std::move(f) } {}
|
||||
~on_exit() { f(); }
|
||||
};
|
||||
|
||||
template<class... Ts>
|
||||
struct overloaded : Ts...
|
||||
{
|
||||
using Ts::operator()...;
|
||||
};
|
||||
|
||||
template<class... Ts>
|
||||
overloaded(Ts...) -> overloaded<Ts...>;
|
||||
|
||||
inline const wchar_t* getScopeName(HKEY scope)
|
||||
{
|
||||
if (scope == HKEY_LOCAL_MACHINE)
|
||||
{
|
||||
return L"HKLM";
|
||||
}
|
||||
else if (scope == HKEY_CURRENT_USER)
|
||||
{
|
||||
return L"HKCU";
|
||||
}
|
||||
else if (scope == HKEY_CLASSES_ROOT)
|
||||
{
|
||||
return L"HKCR";
|
||||
}
|
||||
else
|
||||
{
|
||||
return L"HK??";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ValueChange
|
||||
{
|
||||
using value_t = std::variant<DWORD, std::wstring>;
|
||||
@@ -153,226 +217,28 @@ namespace registry
|
||||
{
|
||||
}
|
||||
|
||||
std::wstring toString() const
|
||||
{
|
||||
using namespace detail;
|
||||
|
||||
std::wstring value_str;
|
||||
std::visit(overloaded{ [&](DWORD value) {
|
||||
std::wostringstream oss;
|
||||
oss << value;
|
||||
value_str = oss.str();
|
||||
},
|
||||
[&](const std::wstring& value) { value_str = value; } },
|
||||
value);
|
||||
|
||||
return fmt::format(L"{}\\{}\\{}:{}", detail::getScopeName(scope), path, name ? *name : L"Default", value_str);
|
||||
}
|
||||
|
||||
bool isApplied() const
|
||||
{
|
||||
HKEY key{};
|
||||
if (auto res = RegOpenKeyExW(scope, path.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::info(L"isApplied of {}: RegOpenKeyExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
detail::on_exit closeKey{ [key] { RegCloseKey(key); } };
|
||||
|
||||
const DWORD expectedType = valueTypeToWinapiType(value);
|
||||
|
||||
DWORD retrievedType{};
|
||||
wchar_t buffer[VALUE_BUFFER_SIZE];
|
||||
DWORD valueSize = sizeof(buffer);
|
||||
if (auto res = RegQueryValueExW(key,
|
||||
name.has_value() ? name->c_str() : nullptr,
|
||||
0,
|
||||
&retrievedType,
|
||||
reinterpret_cast<LPBYTE>(&buffer),
|
||||
&valueSize);
|
||||
res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::info(L"isApplied of {}: RegQueryValueExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expectedType != retrievedType)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (const auto retrievedValue = bufferToValue(buffer, valueSize, retrievedType))
|
||||
{
|
||||
return value == retrievedValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool apply() const
|
||||
{
|
||||
HKEY key{};
|
||||
|
||||
if (auto res = RegCreateKeyExW(scope, path.c_str(), 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &key, nullptr); res !=
|
||||
ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"apply of {}: RegCreateKeyExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
detail::on_exit closeKey{ [key] { RegCloseKey(key); } };
|
||||
|
||||
wchar_t buffer[VALUE_BUFFER_SIZE];
|
||||
DWORD valueSize;
|
||||
DWORD valueType;
|
||||
|
||||
valueToBuffer(value, buffer, valueSize, valueType);
|
||||
if (auto res = RegSetValueExW(key,
|
||||
name.has_value() ? name->c_str() : nullptr,
|
||||
0,
|
||||
valueType,
|
||||
reinterpret_cast<BYTE*>(buffer),
|
||||
valueSize);
|
||||
res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"apply of {}: RegSetValueExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unApply() const
|
||||
{
|
||||
HKEY key{};
|
||||
if (auto res = RegOpenKeyExW(scope, path.c_str(), 0, KEY_ALL_ACCESS, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"unApply of {}: RegOpenKeyExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
detail::on_exit closeKey{ [key] { RegCloseKey(key); } };
|
||||
|
||||
// delete the value itself
|
||||
if (auto res = RegDeleteKeyValueW(scope, path.c_str(), name.has_value() ? name->c_str() : nullptr); res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"unApply of {}: RegDeleteKeyValueW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the path doesn't contain anything and delete it if so
|
||||
DWORD nValues = 0;
|
||||
DWORD maxValueLen = 0;
|
||||
const auto ok =
|
||||
RegQueryInfoKeyW(
|
||||
key, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, &nValues, nullptr, &maxValueLen, nullptr, nullptr) ==
|
||||
ERROR_SUCCESS;
|
||||
|
||||
if (ok && (!nValues || !maxValueLen))
|
||||
{
|
||||
RegDeleteTreeW(scope, path.c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::wstring toString() const;
|
||||
bool isApplied() const;
|
||||
bool apply() const;
|
||||
bool unApply() const;
|
||||
|
||||
bool requiresElevation() const { return scope == HKEY_LOCAL_MACHINE; }
|
||||
|
||||
private:
|
||||
static DWORD valueTypeToWinapiType(const value_t& v)
|
||||
{
|
||||
return std::visit(
|
||||
[](auto&& arg) {
|
||||
using T = std::decay_t<decltype(arg)>;
|
||||
if constexpr (std::is_same_v<T, DWORD>)
|
||||
return REG_DWORD;
|
||||
else if constexpr (std::is_same_v<T, std::wstring>)
|
||||
return REG_SZ;
|
||||
else
|
||||
static_assert(always_false_v<T>, "support for this registry type is not implemented");
|
||||
},
|
||||
v);
|
||||
}
|
||||
|
||||
static void valueToBuffer(const value_t& value, wchar_t buffer[VALUE_BUFFER_SIZE], DWORD& valueSize, DWORD& type)
|
||||
{
|
||||
using detail::overloaded;
|
||||
|
||||
std::visit(overloaded{ [&](DWORD value) {
|
||||
*reinterpret_cast<DWORD*>(buffer) = value;
|
||||
type = REG_DWORD;
|
||||
valueSize = sizeof(value);
|
||||
},
|
||||
[&](const std::wstring& value) {
|
||||
assert(value.size() < VALUE_BUFFER_SIZE);
|
||||
value.copy(buffer, value.size());
|
||||
type = REG_SZ;
|
||||
valueSize = static_cast<DWORD>(sizeof(wchar_t) * value.size());
|
||||
} },
|
||||
value);
|
||||
}
|
||||
|
||||
static DWORD valueTypeToWinapiType(const value_t& v);
|
||||
static void valueToBuffer(const value_t& value, wchar_t buffer[VALUE_BUFFER_SIZE], DWORD& valueSize, DWORD& type);
|
||||
static std::optional<value_t> bufferToValue(const wchar_t buffer[VALUE_BUFFER_SIZE],
|
||||
const DWORD valueSize,
|
||||
const DWORD type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case REG_DWORD:
|
||||
return *reinterpret_cast<const DWORD*>(buffer);
|
||||
case REG_SZ:
|
||||
{
|
||||
if (!valueSize)
|
||||
{
|
||||
return std::wstring{};
|
||||
}
|
||||
std::wstring result{ buffer, valueSize / sizeof(wchar_t) };
|
||||
while (result[result.size() - 1] == L'\0')
|
||||
{
|
||||
result.resize(result.size() - 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
const DWORD type);
|
||||
};
|
||||
|
||||
struct ChangeSet
|
||||
{
|
||||
std::vector<ValueChange> changes;
|
||||
|
||||
bool isApplied() const
|
||||
{
|
||||
for (const auto& c : changes)
|
||||
{
|
||||
if (c.required && !c.isApplied())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool apply() const
|
||||
{
|
||||
bool ok = true;
|
||||
for (const auto& c : changes)
|
||||
{
|
||||
ok = (c.apply()||!c.required) && ok;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool unApply() const
|
||||
{
|
||||
bool ok = true;
|
||||
for (const auto& c : changes)
|
||||
{
|
||||
ok = (c.unApply()||!c.required) && ok;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
bool isApplied() const;
|
||||
bool apply() const;
|
||||
bool unApply() const;
|
||||
};
|
||||
|
||||
const inline std::wstring DOTNET_COMPONENT_CATEGORY_CLSID = L"{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}";
|
||||
@@ -387,7 +253,7 @@ namespace registry
|
||||
thumbnail
|
||||
};
|
||||
|
||||
inline registry::ChangeSet generatePreviewHandler(const PreviewHandlerType handlerType,
|
||||
registry::ChangeSet generatePreviewHandler(const PreviewHandlerType handlerType,
|
||||
const bool perUser,
|
||||
std::wstring handlerClsid,
|
||||
std::wstring powertoysVersion,
|
||||
@@ -396,80 +262,6 @@ namespace registry
|
||||
std::wstring displayName,
|
||||
std::vector<std::wstring> fileTypes,
|
||||
std::wstring perceivedType = L"",
|
||||
std::wstring fileKindType = L"")
|
||||
{
|
||||
const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
|
||||
|
||||
std::wstring clsidPath = L"Software\\Classes\\CLSID";
|
||||
clsidPath += L'\\';
|
||||
clsidPath += handlerClsid;
|
||||
|
||||
std::wstring inprocServerPath = clsidPath;
|
||||
inprocServerPath += L'\\';
|
||||
inprocServerPath += L"InprocServer32";
|
||||
|
||||
std::wstring assemblyKeyValue;
|
||||
if (const auto lastDotPos = className.rfind(L'.'); lastDotPos != std::wstring::npos)
|
||||
{
|
||||
assemblyKeyValue = L"PowerToys." + className.substr(lastDotPos + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
assemblyKeyValue = L"PowerToys." + className;
|
||||
}
|
||||
|
||||
assemblyKeyValue += L", Version=";
|
||||
assemblyKeyValue += powertoysVersion;
|
||||
assemblyKeyValue += L", Culture=neutral";
|
||||
|
||||
std::wstring versionPath = inprocServerPath;
|
||||
versionPath += L'\\';
|
||||
versionPath += powertoysVersion;
|
||||
|
||||
using vec_t = std::vector<registry::ValueChange>;
|
||||
// TODO: verify that we actually need all of those
|
||||
vec_t changes = { { scope, clsidPath, L"DisplayName", displayName },
|
||||
{ scope, clsidPath, std::nullopt, className },
|
||||
{ scope, inprocServerPath, std::nullopt, fullPathToHandler },
|
||||
{ scope, inprocServerPath, L"Assembly", assemblyKeyValue },
|
||||
{ scope, inprocServerPath, L"Class", className },
|
||||
{ scope, inprocServerPath, L"ThreadingModel", L"Apartment" } };
|
||||
|
||||
for (const auto& fileType : fileTypes)
|
||||
{
|
||||
std::wstring fileTypePath = L"Software\\Classes\\" + fileType;
|
||||
std::wstring fileAssociationPath = fileTypePath + L"\\shellex\\";
|
||||
fileAssociationPath += handlerType == PreviewHandlerType::preview ? IPREVIEW_HANDLER_CLSID : ITHUMBNAIL_PROVIDER_CLSID;
|
||||
changes.push_back({ scope, fileAssociationPath, std::nullopt, handlerClsid });
|
||||
if (!fileKindType.empty())
|
||||
{
|
||||
// Registering a file type as a kind needs to be done at the HKEY_LOCAL_MACHINE level.
|
||||
// Make it optional as well so that we don't fail registering the handler if we can't write to HKEY_LOCAL_MACHINE.
|
||||
std::wstring kindMapPath = L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\KindMap";
|
||||
changes.push_back({ HKEY_LOCAL_MACHINE, kindMapPath, fileType, fileKindType, false});
|
||||
}
|
||||
if (!perceivedType.empty())
|
||||
{
|
||||
changes.push_back({ scope, fileTypePath, L"PerceivedType", perceivedType });
|
||||
}
|
||||
if (handlerType == PreviewHandlerType::preview && fileType == L".reg")
|
||||
{
|
||||
// this regfile registry key has precedence over Software\Classes\.reg for .reg files
|
||||
std::wstring regfilePath = L"Software\\Classes\\regfile\\shellex\\" + IPREVIEW_HANDLER_CLSID + L"\\";
|
||||
changes.push_back({ scope, regfilePath, std::nullopt, handlerClsid });
|
||||
}
|
||||
}
|
||||
|
||||
if (handlerType == PreviewHandlerType::preview)
|
||||
{
|
||||
const std::wstring previewHostClsid = L"{6d2b5079-2f0b-48dd-ab7f-97cec514d30b}";
|
||||
const std::wstring previewHandlerListPath = LR"(Software\Microsoft\Windows\CurrentVersion\PreviewHandlers)";
|
||||
|
||||
changes.push_back({ scope, clsidPath, L"AppID", previewHostClsid });
|
||||
changes.push_back({ scope, previewHandlerListPath, handlerClsid, displayName });
|
||||
}
|
||||
|
||||
return registry::ChangeSet{ .changes = std::move(changes) };
|
||||
}
|
||||
std::wstring fileKindType = L"");
|
||||
}
|
||||
}
|
||||
|
||||
201
src/common/utils/resources.cpp
Normal file
201
src/common/utils/resources.cpp
Normal file
@@ -0,0 +1,201 @@
|
||||
#include "pch.h"
|
||||
#include "resources.h"
|
||||
#include <atlstr.h>
|
||||
#include <common/utils/language_helper.h>
|
||||
|
||||
std::wstring get_english_fallback_string(UINT resource_id, HINSTANCE instance)
|
||||
{
|
||||
// Try to load en-us string as the first fallback.
|
||||
WORD english_language = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
|
||||
|
||||
ATL::CStringW english_string;
|
||||
try
|
||||
{
|
||||
if (!english_string.LoadStringW(instance, resource_id, english_language))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::wstring(english_string);
|
||||
}
|
||||
|
||||
std::wstring get_resource_string_language_override(UINT resource_id, HINSTANCE instance)
|
||||
{
|
||||
static std::wstring language = LanguageHelpers::load_language();
|
||||
unsigned lang = LANG_ENGLISH;
|
||||
unsigned sublang = SUBLANG_ENGLISH_US;
|
||||
|
||||
if (!language.empty())
|
||||
{
|
||||
// Language list taken from Resources.wxs
|
||||
if (language == L"ar-SA")
|
||||
{
|
||||
lang = LANG_ARABIC;
|
||||
sublang = SUBLANG_ARABIC_SAUDI_ARABIA;
|
||||
}
|
||||
else if (language == L"cs-CZ")
|
||||
{
|
||||
lang = LANG_CZECH;
|
||||
sublang = SUBLANG_CZECH_CZECH_REPUBLIC;
|
||||
}
|
||||
else if (language == L"de-DE")
|
||||
{
|
||||
lang = LANG_GERMAN;
|
||||
sublang = SUBLANG_GERMAN;
|
||||
}
|
||||
else if (language == L"en-US")
|
||||
{
|
||||
lang = LANG_ENGLISH;
|
||||
sublang = SUBLANG_ENGLISH_US;
|
||||
}
|
||||
else if (language == L"es-ES")
|
||||
{
|
||||
lang = LANG_SPANISH;
|
||||
sublang = SUBLANG_SPANISH;
|
||||
}
|
||||
else if (language == L"fa-IR")
|
||||
{
|
||||
lang = LANG_PERSIAN;
|
||||
sublang = SUBLANG_PERSIAN_IRAN;
|
||||
}
|
||||
else if (language == L"fr-FR")
|
||||
{
|
||||
lang = LANG_FRENCH;
|
||||
sublang = SUBLANG_FRENCH;
|
||||
}
|
||||
else if (language == L"he-IL")
|
||||
{
|
||||
lang = LANG_HEBREW;
|
||||
sublang = SUBLANG_HEBREW_ISRAEL;
|
||||
}
|
||||
else if (language == L"hu-HU")
|
||||
{
|
||||
lang = LANG_HUNGARIAN;
|
||||
sublang = SUBLANG_HUNGARIAN_HUNGARY;
|
||||
}
|
||||
else if (language == L"it-IT")
|
||||
{
|
||||
lang = LANG_ITALIAN;
|
||||
sublang = SUBLANG_ITALIAN;
|
||||
}
|
||||
else if (language == L"ja-JP")
|
||||
{
|
||||
lang = LANG_JAPANESE;
|
||||
sublang = SUBLANG_JAPANESE_JAPAN;
|
||||
}
|
||||
else if (language == L"ko-KR")
|
||||
{
|
||||
lang = LANG_KOREAN;
|
||||
sublang = SUBLANG_KOREAN;
|
||||
}
|
||||
else if (language == L"nl-NL")
|
||||
{
|
||||
lang = LANG_DUTCH;
|
||||
sublang = SUBLANG_DUTCH;
|
||||
}
|
||||
else if (language == L"pl-PL")
|
||||
{
|
||||
lang = LANG_POLISH;
|
||||
sublang = SUBLANG_POLISH_POLAND;
|
||||
}
|
||||
else if (language == L"pt-BR")
|
||||
{
|
||||
lang = LANG_PORTUGUESE;
|
||||
sublang = SUBLANG_PORTUGUESE_BRAZILIAN;
|
||||
}
|
||||
else if (language == L"pt-PT")
|
||||
{
|
||||
lang = LANG_PORTUGUESE;
|
||||
sublang = SUBLANG_PORTUGUESE;
|
||||
}
|
||||
else if (language == L"ru-RU")
|
||||
{
|
||||
lang = LANG_RUSSIAN;
|
||||
sublang = SUBLANG_RUSSIAN_RUSSIA;
|
||||
}
|
||||
else if (language == L"sv-SE")
|
||||
{
|
||||
lang = LANG_SWEDISH;
|
||||
sublang = SUBLANG_SWEDISH;
|
||||
}
|
||||
else if (language == L"tr-TR")
|
||||
{
|
||||
lang = LANG_TURKISH;
|
||||
sublang = SUBLANG_TURKISH_TURKEY;
|
||||
}
|
||||
else if (language == L"uk-UA")
|
||||
{
|
||||
lang = LANG_UKRAINIAN;
|
||||
sublang = SUBLANG_UKRAINIAN_UKRAINE;
|
||||
}
|
||||
else if (language == L"zh-CN")
|
||||
{
|
||||
lang = LANG_CHINESE_SIMPLIFIED;
|
||||
sublang = SUBLANG_CHINESE_SIMPLIFIED;
|
||||
}
|
||||
else if (language == L"zh-TW")
|
||||
{
|
||||
lang = LANG_CHINESE_TRADITIONAL;
|
||||
sublang = SUBLANG_CHINESE_TRADITIONAL;
|
||||
}
|
||||
|
||||
WORD languageID = MAKELANGID(lang, sublang);
|
||||
ATL::CStringW result;
|
||||
try
|
||||
{
|
||||
if (!result.LoadStringW(instance, resource_id, languageID))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!result.IsEmpty())
|
||||
{
|
||||
return std::wstring(result);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring get_resource_string(UINT resource_id, HINSTANCE instance, const wchar_t* fallback)
|
||||
{
|
||||
// Try to load en-us string as the first fallback.
|
||||
std::wstring english_string = get_english_fallback_string(resource_id, instance);
|
||||
|
||||
std::wstring language_override_resource = get_resource_string_language_override(resource_id, instance);
|
||||
|
||||
if (!language_override_resource.empty())
|
||||
{
|
||||
return language_override_resource;
|
||||
}
|
||||
else
|
||||
{
|
||||
wchar_t* text_ptr;
|
||||
auto length = LoadStringW(instance, resource_id, reinterpret_cast<wchar_t*>(&text_ptr), 0);
|
||||
if (length == 0)
|
||||
{
|
||||
if (!english_string.empty())
|
||||
{
|
||||
return std::wstring(english_string);
|
||||
}
|
||||
else
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return { text_ptr, static_cast<std::size_t>(length) };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,210 +2,20 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
#include <atlstr.h>
|
||||
|
||||
#include <common/utils/language_helper.h>
|
||||
|
||||
|
||||
inline std::wstring get_english_fallback_string(UINT resource_id, HINSTANCE instance)
|
||||
{
|
||||
// Try to load en-us string as the first fallback.
|
||||
WORD english_language = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
|
||||
|
||||
ATL::CStringW english_string;
|
||||
try
|
||||
{
|
||||
if (!english_string.LoadStringW(instance, resource_id, english_language))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::wstring(english_string);
|
||||
}
|
||||
|
||||
inline std::wstring get_resource_string_language_override(UINT resource_id, HINSTANCE instance)
|
||||
{
|
||||
static std::wstring language = LanguageHelpers::load_language();
|
||||
unsigned lang = LANG_ENGLISH;
|
||||
unsigned sublang = SUBLANG_ENGLISH_US;
|
||||
|
||||
if (!language.empty())
|
||||
{
|
||||
// Language list taken from Resources.wxs
|
||||
if (language == L"ar-SA")
|
||||
{
|
||||
lang = LANG_ARABIC;
|
||||
sublang = SUBLANG_ARABIC_SAUDI_ARABIA;
|
||||
}
|
||||
else if (language == L"cs-CZ")
|
||||
{
|
||||
lang = LANG_CZECH;
|
||||
sublang = SUBLANG_CZECH_CZECH_REPUBLIC;
|
||||
}
|
||||
else if (language == L"de-DE")
|
||||
{
|
||||
lang = LANG_GERMAN;
|
||||
sublang = SUBLANG_GERMAN;
|
||||
}
|
||||
else if (language == L"en-US")
|
||||
{
|
||||
lang = LANG_ENGLISH;
|
||||
sublang = SUBLANG_ENGLISH_US;
|
||||
}
|
||||
else if (language == L"es-ES")
|
||||
{
|
||||
lang = LANG_SPANISH;
|
||||
sublang = SUBLANG_SPANISH;
|
||||
}
|
||||
else if (language == L"fa-IR")
|
||||
{
|
||||
lang = LANG_PERSIAN;
|
||||
sublang = SUBLANG_PERSIAN_IRAN;
|
||||
}
|
||||
else if (language == L"fr-FR")
|
||||
{
|
||||
lang = LANG_FRENCH;
|
||||
sublang = SUBLANG_FRENCH;
|
||||
}
|
||||
else if (language == L"he-IL")
|
||||
{
|
||||
lang = LANG_HEBREW;
|
||||
sublang = SUBLANG_HEBREW_ISRAEL;
|
||||
}
|
||||
else if (language == L"hu-HU")
|
||||
{
|
||||
lang = LANG_HUNGARIAN;
|
||||
sublang = SUBLANG_HUNGARIAN_HUNGARY;
|
||||
}
|
||||
else if (language == L"it-IT")
|
||||
{
|
||||
lang = LANG_ITALIAN;
|
||||
sublang = SUBLANG_ITALIAN;
|
||||
}
|
||||
else if (language == L"ja-JP")
|
||||
{
|
||||
lang = LANG_JAPANESE;
|
||||
sublang = SUBLANG_JAPANESE_JAPAN;
|
||||
}
|
||||
else if (language == L"ko-KR")
|
||||
{
|
||||
lang = LANG_KOREAN;
|
||||
sublang = SUBLANG_KOREAN;
|
||||
}
|
||||
else if (language == L"nl-NL")
|
||||
{
|
||||
lang = LANG_DUTCH;
|
||||
sublang = SUBLANG_DUTCH;
|
||||
}
|
||||
else if (language == L"pl-PL")
|
||||
{
|
||||
lang = LANG_POLISH;
|
||||
sublang = SUBLANG_POLISH_POLAND;
|
||||
}
|
||||
else if (language == L"pt-BR")
|
||||
{
|
||||
lang = LANG_PORTUGUESE;
|
||||
sublang = SUBLANG_PORTUGUESE_BRAZILIAN;
|
||||
}
|
||||
else if (language == L"pt-PT")
|
||||
{
|
||||
lang = LANG_PORTUGUESE;
|
||||
sublang = SUBLANG_PORTUGUESE;
|
||||
}
|
||||
else if (language == L"ru-RU")
|
||||
{
|
||||
lang = LANG_RUSSIAN;
|
||||
sublang = SUBLANG_RUSSIAN_RUSSIA;
|
||||
}
|
||||
else if (language == L"sv-SE")
|
||||
{
|
||||
lang = LANG_SWEDISH;
|
||||
sublang = SUBLANG_SWEDISH;
|
||||
}
|
||||
else if (language == L"tr-TR")
|
||||
{
|
||||
lang = LANG_TURKISH;
|
||||
sublang = SUBLANG_TURKISH_TURKEY;
|
||||
}
|
||||
else if (language == L"uk-UA")
|
||||
{
|
||||
lang = LANG_UKRAINIAN;
|
||||
sublang = SUBLANG_UKRAINIAN_UKRAINE;
|
||||
}
|
||||
else if (language == L"zh-CN")
|
||||
{
|
||||
lang = LANG_CHINESE_SIMPLIFIED;
|
||||
sublang = SUBLANG_CHINESE_SIMPLIFIED;
|
||||
}
|
||||
else if (language == L"zh-TW")
|
||||
{
|
||||
lang = LANG_CHINESE_TRADITIONAL;
|
||||
sublang = SUBLANG_CHINESE_TRADITIONAL;
|
||||
}
|
||||
|
||||
WORD languageID = MAKELANGID(lang, sublang);
|
||||
ATL::CStringW result;
|
||||
try
|
||||
{
|
||||
if (!result.LoadStringW(instance, resource_id, languageID))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!result.IsEmpty())
|
||||
{
|
||||
return std::wstring(result);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// Get a string from the resource file
|
||||
inline std::wstring get_resource_string(UINT resource_id, HINSTANCE instance, const wchar_t* fallback)
|
||||
{
|
||||
// Try to load en-us string as the first fallback.
|
||||
std::wstring english_string = get_english_fallback_string(resource_id, instance);
|
||||
|
||||
std::wstring language_override_resource = get_resource_string_language_override(resource_id, instance);
|
||||
|
||||
if (!language_override_resource.empty())
|
||||
{
|
||||
return language_override_resource;
|
||||
}
|
||||
else
|
||||
{
|
||||
wchar_t* text_ptr;
|
||||
auto length = LoadStringW(instance, resource_id, reinterpret_cast<wchar_t*>(&text_ptr), 0);
|
||||
if (length == 0)
|
||||
{
|
||||
if (!english_string.empty())
|
||||
{
|
||||
return std::wstring(english_string);
|
||||
}
|
||||
else
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return { text_ptr, static_cast<std::size_t>(length) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" IMAGE_DOS_HEADER __ImageBase;
|
||||
|
||||
// Non-localizable - Load English fallback string
|
||||
std::wstring get_english_fallback_string(UINT resource_id, HINSTANCE instance);
|
||||
|
||||
// Non-localizable - Load string with language override
|
||||
std::wstring get_resource_string_language_override(UINT resource_id, HINSTANCE instance);
|
||||
|
||||
// Localizable - Load resource string with fallback support
|
||||
std::wstring get_resource_string(UINT resource_id, HINSTANCE instance, const wchar_t* fallback);
|
||||
|
||||
// Wrapper for getting a string from the resource file. Returns the resource id text when fails.
|
||||
#define GET_RESOURCE_STRING(resource_id) get_resource_string(resource_id, reinterpret_cast<HINSTANCE>(&__ImageBase), L#resource_id)
|
||||
#define GET_RESOURCE_STRING_FALLBACK(resource_id, fallback) get_resource_string(resource_id, reinterpret_cast<HINSTANCE>(&__ImageBase), fallback)
|
||||
|
||||
|
||||
|
||||
270
src/common/utils/shell_ext_registration.cpp
Normal file
270
src/common/utils/shell_ext_registration.cpp
Normal file
@@ -0,0 +1,270 @@
|
||||
#include "pch.h"
|
||||
#include "shell_ext_registration.h"
|
||||
|
||||
#include "../logger/logger.h"
|
||||
|
||||
namespace runtime_shell_ext
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
struct unique_hkey
|
||||
{
|
||||
HKEY h{ nullptr };
|
||||
unique_hkey() = default;
|
||||
explicit unique_hkey(HKEY handle) : h(handle) {}
|
||||
~unique_hkey() { if (h) RegCloseKey(h); }
|
||||
unique_hkey(const unique_hkey&) = delete;
|
||||
unique_hkey& operator=(const unique_hkey&) = delete;
|
||||
unique_hkey(unique_hkey&& other) noexcept : h(other.h) { other.h = nullptr; }
|
||||
unique_hkey& operator=(unique_hkey&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
if (h)
|
||||
{
|
||||
RegCloseKey(h);
|
||||
}
|
||||
h = other.h;
|
||||
other.h = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
HKEY get() const { return h; }
|
||||
HKEY* put()
|
||||
{
|
||||
if (h)
|
||||
{
|
||||
RegCloseKey(h);
|
||||
h = nullptr;
|
||||
}
|
||||
return &h;
|
||||
}
|
||||
};
|
||||
|
||||
std::wstring base_dir_from_module(HMODULE moduleInstance)
|
||||
{
|
||||
wchar_t buf[MAX_PATH];
|
||||
if (GetModuleFileNameW(moduleInstance, buf, MAX_PATH))
|
||||
{
|
||||
PathRemoveFileSpecW(buf);
|
||||
return buf;
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
std::wstring pick_existing_dll(const std::wstring& base, const std::vector<std::wstring>& candidates)
|
||||
{
|
||||
for (const auto& rel : candidates)
|
||||
{
|
||||
std::wstring full = base + L"\\" + rel;
|
||||
if (GetFileAttributesW(full.c_str()) != INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
return full;
|
||||
}
|
||||
}
|
||||
if (!candidates.empty())
|
||||
{
|
||||
return base + L"\\" + candidates.front();
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
bool sentinel_exists(const Spec& spec)
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, KEY_READ, key.put()) != ERROR_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
DWORD value = 0;
|
||||
DWORD size = sizeof(value);
|
||||
return RegQueryValueExW(key.get(), spec.sentinelValue.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size) == ERROR_SUCCESS && value == 1;
|
||||
}
|
||||
|
||||
void write_sentinel(const Spec& spec)
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD one = 1;
|
||||
RegSetValueExW(key.get(), spec.sentinelValue.c_str(), 0, REG_DWORD, reinterpret_cast<const BYTE*>(&one), sizeof(one));
|
||||
}
|
||||
}
|
||||
|
||||
void write_inproc_server(const Spec& spec, const std::wstring& dllPath)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
std::wstring clsidRoot = L"Software\\Classes\\CLSID\\"s + spec.clsid;
|
||||
std::wstring inprocKey = clsidRoot + L"\\InprocServer32";
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, clsidRoot.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
if (!spec.friendlyName.empty())
|
||||
{
|
||||
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(spec.friendlyName.c_str()), static_cast<DWORD>((spec.friendlyName.size() + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
if (spec.writeOptInEmptyValue)
|
||||
{
|
||||
const wchar_t* optIn = L"ContextMenuOptIn";
|
||||
const wchar_t empty = L'\0';
|
||||
RegSetValueExW(key.get(), optIn, 0, REG_SZ, reinterpret_cast<const BYTE*>(&empty), sizeof(empty));
|
||||
}
|
||||
}
|
||||
}
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, inprocKey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(dllPath.c_str()), static_cast<DWORD>((dllPath.size() + 1) * sizeof(wchar_t)));
|
||||
if (spec.writeThreadingModel)
|
||||
{
|
||||
const wchar_t* tm = L"Apartment";
|
||||
RegSetValueExW(key.get(), L"ThreadingModel", 0, REG_SZ, reinterpret_cast<const BYTE*>(tm), static_cast<DWORD>((wcslen(tm) + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring read_inproc_server(const Spec& spec)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
std::wstring inprocKey = L"Software\\Classes\\CLSID\\"s + spec.clsid + L"\\InprocServer32";
|
||||
unique_hkey key;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, inprocKey.c_str(), 0, KEY_READ, key.put()) != ERROR_SUCCESS)
|
||||
{
|
||||
return L"";
|
||||
}
|
||||
wchar_t buf[MAX_PATH];
|
||||
DWORD size = sizeof(buf);
|
||||
if (RegQueryValueExW(key.get(), nullptr, nullptr, nullptr, reinterpret_cast<LPBYTE>(buf), &size) == ERROR_SUCCESS)
|
||||
{
|
||||
return std::wstring(buf);
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
void write_default_value_key(const std::wstring& keyPath, const std::wstring& value)
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(value.c_str()), static_cast<DWORD>((value.size() + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
}
|
||||
|
||||
bool representative_association_exists(const Spec& spec)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
if (spec.representativeSystemExt.empty() || spec.systemFileAssocHandlerName.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
std::wstring keyPath = L"Software\\Classes\\SystemFileAssociations\\"s + spec.representativeSystemExt + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
|
||||
unique_hkey key;
|
||||
return RegOpenKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, KEY_READ, key.put()) == ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
bool EnsureRegistered(const Spec& spec, HMODULE moduleInstance)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
auto base = detail::base_dir_from_module(moduleInstance);
|
||||
auto dllPath = detail::pick_existing_dll(base, spec.dllFileCandidates);
|
||||
if (dllPath.empty())
|
||||
{
|
||||
Logger::error(L"Runtime registration: cannot locate dll path for CLSID {}", spec.clsid);
|
||||
return false;
|
||||
}
|
||||
bool exists = detail::sentinel_exists(spec);
|
||||
bool repaired = false;
|
||||
if (exists)
|
||||
{
|
||||
auto current = detail::read_inproc_server(spec);
|
||||
if (_wcsicmp(current.c_str(), dllPath.c_str()) != 0)
|
||||
{
|
||||
detail::write_inproc_server(spec, dllPath);
|
||||
repaired = true;
|
||||
}
|
||||
if (!detail::representative_association_exists(spec))
|
||||
{
|
||||
repaired = true;
|
||||
}
|
||||
}
|
||||
if (!exists)
|
||||
{
|
||||
detail::write_inproc_server(spec, dllPath);
|
||||
}
|
||||
if (!exists || repaired)
|
||||
{
|
||||
for (const auto& path : spec.contextMenuHandlerKeyPaths)
|
||||
{
|
||||
detail::write_default_value_key(path, spec.clsid);
|
||||
}
|
||||
for (const auto& path : spec.extraAssociationPaths)
|
||||
{
|
||||
detail::write_default_value_key(path, spec.clsid);
|
||||
}
|
||||
for (const auto& ext : spec.systemFileAssocExtensions)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
auto baseKey = L"Software\\Classes\\SystemFileAssociations\\"s + ext + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
|
||||
detail::write_default_value_key(baseKey, spec.clsid);
|
||||
}
|
||||
if (spec.logRepairs)
|
||||
{
|
||||
Logger::info(L"Runtime shell extension registration repaired {}", spec.clsid);
|
||||
}
|
||||
detail::write_sentinel(spec);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::trace(L"Runtime shell extension registration already up to date for {}", spec.clsid);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Unregister(const Spec& spec)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
|
||||
for (const auto& path : spec.contextMenuHandlerKeyPaths)
|
||||
{
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
|
||||
}
|
||||
|
||||
for (const auto& path : spec.extraAssociationPaths)
|
||||
{
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
|
||||
}
|
||||
|
||||
if (!spec.systemFileAssocExtensions.empty() && !spec.systemFileAssocHandlerName.empty())
|
||||
{
|
||||
for (const auto& ext : spec.systemFileAssocExtensions)
|
||||
{
|
||||
std::wstring keyPath = L"Software\\Classes\\SystemFileAssociations\\";
|
||||
keyPath += ext;
|
||||
keyPath += L"\\ShellEx\\ContextMenuHandlers\\";
|
||||
keyPath += spec.systemFileAssocHandlerName;
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, keyPath.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (!spec.clsid.empty())
|
||||
{
|
||||
std::wstring clsidRoot = L"Software\\Classes\\CLSID\\"s + spec.clsid;
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, clsidRoot.c_str());
|
||||
}
|
||||
|
||||
if (!spec.sentinelKey.empty() && !spec.sentinelValue.empty())
|
||||
{
|
||||
HKEY key{};
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, KEY_SET_VALUE, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
RegDeleteValueW(key, spec.sentinelValue.c_str());
|
||||
RegCloseKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
Logger::info(L"Runtime shell extension unregistered for {}", spec.clsid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
// Shared runtime shell extension registration utility for PowerToys modules.
|
||||
// Provides a generic EnsureRegistered function so individual modules only need
|
||||
// to supply a specification (CLSID, sentinel, handler key paths, etc.).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
@@ -9,258 +5,28 @@
|
||||
#include <windows.h>
|
||||
#include <shlwapi.h>
|
||||
|
||||
#include "../logger/logger.h"
|
||||
|
||||
namespace runtime_shell_ext
|
||||
{
|
||||
struct Spec
|
||||
{
|
||||
// Mandatory
|
||||
std::wstring clsid; // e.g. {GUID}
|
||||
std::wstring sentinelKey; // e.g. Software\\Microsoft\\PowerToys\\ModuleName
|
||||
std::wstring sentinelValue; // e.g. ContextMenuRegistered
|
||||
std::wstring clsid; // e.g. {GUID}
|
||||
std::wstring sentinelKey; // e.g. Software\\Microsoft\\PowerToys\\ModuleName
|
||||
std::wstring sentinelValue; // e.g. ContextMenuRegistered
|
||||
std::vector<std::wstring> dllFileCandidates; // relative filenames (pick first existing)
|
||||
std::vector<std::wstring> contextMenuHandlerKeyPaths; // full HKCU relative paths where default value = CLSID
|
||||
|
||||
// Optional
|
||||
std::wstring friendlyName; // if non-empty written as default under CLSID root
|
||||
bool writeOptInEmptyValue = true; // write ContextMenuOptIn="" under CLSID root (legacy pattern)
|
||||
bool writeThreadingModel = true; // write Apartment threading model
|
||||
std::wstring friendlyName; // if non-empty written as default under CLSID root
|
||||
bool writeOptInEmptyValue = true; // write ContextMenuOptIn="" under CLSID root (legacy pattern)
|
||||
bool writeThreadingModel = true; // write Apartment threading model
|
||||
std::vector<std::wstring> extraAssociationPaths; // additional key paths (DragDropHandlers etc.) default=CLSID
|
||||
std::vector<std::wstring> systemFileAssocExtensions; // e.g. .png -> Software\\Classes\\SystemFileAssociations\\.png\\ShellEx\\ContextMenuHandlers\\<HandlerName>
|
||||
std::wstring systemFileAssocHandlerName; // e.g. ImageResizer
|
||||
std::wstring representativeSystemExt; // used to decide if associations need repair (.png)
|
||||
std::wstring representativeSystemExt; // used to decide if associations need repair (.png)
|
||||
bool logRepairs = true;
|
||||
};
|
||||
|
||||
namespace detail
|
||||
{
|
||||
// Minimal RAII wrapper for HKEY
|
||||
struct unique_hkey
|
||||
{
|
||||
HKEY h{ nullptr };
|
||||
unique_hkey() = default;
|
||||
explicit unique_hkey(HKEY handle) : h(handle) {}
|
||||
~unique_hkey() { if (h) RegCloseKey(h); }
|
||||
unique_hkey(const unique_hkey&) = delete;
|
||||
unique_hkey& operator=(const unique_hkey&) = delete;
|
||||
unique_hkey(unique_hkey&& other) noexcept : h(other.h) { other.h = nullptr; }
|
||||
unique_hkey& operator=(unique_hkey&& other) noexcept { if (this != &other) { if (h) RegCloseKey(h); h = other.h; other.h = nullptr; } return *this; }
|
||||
HKEY get() const { return h; }
|
||||
HKEY* put() { if (h) { RegCloseKey(h); h = nullptr; } return &h; }
|
||||
};
|
||||
inline std::wstring base_dir_from_module(HMODULE h)
|
||||
{
|
||||
wchar_t buf[MAX_PATH];
|
||||
if (GetModuleFileNameW(h, buf, MAX_PATH))
|
||||
{
|
||||
PathRemoveFileSpecW(buf);
|
||||
return buf;
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
inline std::wstring pick_existing_dll(const std::wstring& base, const std::vector<std::wstring>& candidates)
|
||||
{
|
||||
for (const auto& rel : candidates)
|
||||
{
|
||||
std::wstring full = base + L"\\" + rel;
|
||||
if (GetFileAttributesW(full.c_str()) != INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
return full;
|
||||
}
|
||||
}
|
||||
if (!candidates.empty())
|
||||
{
|
||||
return base + L"\\" + candidates.front();
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
inline bool sentinel_exists(const Spec& spec)
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, KEY_READ, key.put()) != ERROR_SUCCESS)
|
||||
return false;
|
||||
DWORD v = 0; DWORD sz = sizeof(v);
|
||||
return RegQueryValueExW(key.get(), spec.sentinelValue.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&v), &sz) == ERROR_SUCCESS && v == 1;
|
||||
}
|
||||
|
||||
inline void write_sentinel(const Spec& spec)
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD one = 1;
|
||||
RegSetValueExW(key.get(), spec.sentinelValue.c_str(), 0, REG_DWORD, reinterpret_cast<const BYTE*>(&one), sizeof(one));
|
||||
}
|
||||
}
|
||||
|
||||
inline void write_inproc_server(const Spec& spec, const std::wstring& dllPath)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
std::wstring clsidRoot = L"Software\\Classes\\CLSID\\"s + spec.clsid;
|
||||
std::wstring inprocKey = clsidRoot + L"\\InprocServer32";
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, clsidRoot.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
if (!spec.friendlyName.empty())
|
||||
{
|
||||
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(spec.friendlyName.c_str()), static_cast<DWORD>((spec.friendlyName.size() + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
if (spec.writeOptInEmptyValue)
|
||||
{
|
||||
const wchar_t* optIn = L"ContextMenuOptIn";
|
||||
const wchar_t empty = L'\0';
|
||||
RegSetValueExW(key.get(), optIn, 0, REG_SZ, reinterpret_cast<const BYTE*>(&empty), sizeof(empty));
|
||||
}
|
||||
}
|
||||
}
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, inprocKey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(dllPath.c_str()), static_cast<DWORD>((dllPath.size() + 1) * sizeof(wchar_t)));
|
||||
if (spec.writeThreadingModel)
|
||||
{
|
||||
const wchar_t* tm = L"Apartment";
|
||||
RegSetValueExW(key.get(), L"ThreadingModel", 0, REG_SZ, reinterpret_cast<const BYTE*>(tm), static_cast<DWORD>((wcslen(tm) + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline std::wstring read_inproc_server(const Spec& spec)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
std::wstring inprocKey = L"Software\\Classes\\CLSID\\"s + spec.clsid + L"\\InprocServer32";
|
||||
unique_hkey key;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, inprocKey.c_str(), 0, KEY_READ, key.put()) != ERROR_SUCCESS)
|
||||
return L"";
|
||||
wchar_t buf[MAX_PATH]; DWORD sz = sizeof(buf);
|
||||
if (RegQueryValueExW(key.get(), nullptr, nullptr, nullptr, reinterpret_cast<LPBYTE>(buf), &sz) == ERROR_SUCCESS)
|
||||
return std::wstring(buf);
|
||||
return L"";
|
||||
}
|
||||
|
||||
inline void write_default_value_key(const std::wstring& keyPath, const std::wstring& value)
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(value.c_str()), static_cast<DWORD>((value.size() + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
}
|
||||
|
||||
inline bool representative_association_exists(const Spec& spec)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
if (spec.representativeSystemExt.empty() || spec.systemFileAssocHandlerName.empty())
|
||||
return true;
|
||||
std::wstring keyPath = L"Software\\Classes\\SystemFileAssociations\\"s + spec.representativeSystemExt + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
|
||||
unique_hkey key;
|
||||
return RegOpenKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, KEY_READ, key.put()) == ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool EnsureRegistered(const Spec& spec, HMODULE moduleInstance)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
auto base = detail::base_dir_from_module(moduleInstance);
|
||||
auto dllPath = detail::pick_existing_dll(base, spec.dllFileCandidates);
|
||||
if (dllPath.empty())
|
||||
{
|
||||
Logger::error(L"Runtime registration: cannot locate dll path for CLSID {}", spec.clsid);
|
||||
return false;
|
||||
}
|
||||
bool exists = detail::sentinel_exists(spec);
|
||||
bool repaired = false;
|
||||
if (exists)
|
||||
{
|
||||
auto current = detail::read_inproc_server(spec);
|
||||
if (_wcsicmp(current.c_str(), dllPath.c_str()) != 0)
|
||||
{
|
||||
detail::write_inproc_server(spec, dllPath);
|
||||
repaired = true;
|
||||
}
|
||||
if (!detail::representative_association_exists(spec))
|
||||
{
|
||||
repaired = true;
|
||||
}
|
||||
}
|
||||
if (!exists)
|
||||
{
|
||||
detail::write_inproc_server(spec, dllPath);
|
||||
}
|
||||
if (!exists || repaired)
|
||||
{
|
||||
for (const auto& path : spec.contextMenuHandlerKeyPaths)
|
||||
{
|
||||
detail::write_default_value_key(path, spec.clsid);
|
||||
}
|
||||
for (const auto& path : spec.extraAssociationPaths)
|
||||
{
|
||||
detail::write_default_value_key(path, spec.clsid);
|
||||
}
|
||||
if (!spec.systemFileAssocExtensions.empty() && !spec.systemFileAssocHandlerName.empty())
|
||||
{
|
||||
for (const auto& ext : spec.systemFileAssocExtensions)
|
||||
{
|
||||
std::wstring path = L"Software\\Classes\\SystemFileAssociations\\"s + ext + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
|
||||
detail::write_default_value_key(path, spec.clsid);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!exists)
|
||||
{
|
||||
detail::write_sentinel(spec);
|
||||
Logger::info(L"Runtime registration completed for CLSID {}", spec.clsid);
|
||||
}
|
||||
else if (repaired && spec.logRepairs)
|
||||
{
|
||||
Logger::info(L"Runtime registration repaired for CLSID {}", spec.clsid);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool Unregister(const Spec& spec)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
// Remove handler key paths
|
||||
for (const auto& path : spec.contextMenuHandlerKeyPaths)
|
||||
{
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
|
||||
}
|
||||
// Remove extra association paths (e.g., drag & drop handlers)
|
||||
for (const auto& path : spec.extraAssociationPaths)
|
||||
{
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
|
||||
}
|
||||
// Remove per-extension system file association handler keys
|
||||
if (!spec.systemFileAssocExtensions.empty() && !spec.systemFileAssocHandlerName.empty())
|
||||
{
|
||||
for (const auto& ext : spec.systemFileAssocExtensions)
|
||||
{
|
||||
std::wstring keyPath = L"Software\\Classes\\SystemFileAssociations\\"s + ext + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, keyPath.c_str());
|
||||
}
|
||||
}
|
||||
// Remove CLSID branch
|
||||
if (!spec.clsid.empty())
|
||||
{
|
||||
std::wstring clsidRoot = L"Software\\Classes\\CLSID\\"s + spec.clsid;
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, clsidRoot.c_str());
|
||||
}
|
||||
// Remove sentinel value (not deleting entire key to avoid disturbing other values)
|
||||
if (!spec.sentinelKey.empty() && !spec.sentinelValue.empty())
|
||||
{
|
||||
HKEY hKey{};
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
RegDeleteValueW(hKey, spec.sentinelValue.c_str());
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
}
|
||||
Logger::info(L"Successfully unregistered CLSID {}", spec.clsid);
|
||||
return true;
|
||||
}
|
||||
bool EnsureRegistered(const Spec& spec, HMODULE moduleInstance);
|
||||
bool Unregister(const Spec& spec);
|
||||
}
|
||||
|
||||
11
src/common/utils/string_utils.cpp
Normal file
11
src/common/utils/string_utils.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#include "pch.h"
|
||||
#include "string_utils.h"
|
||||
|
||||
std::string unwide(const std::wstring& wide)
|
||||
{
|
||||
std::string result(wide.length(), 0);
|
||||
std::transform(begin(wide), end(wide), result.begin(), [](const wchar_t c) {
|
||||
return static_cast<char>(c);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -55,11 +55,4 @@ inline void replace_chars(std::basic_string<CharT>& s,
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string unwide(const std::wstring& wide)
|
||||
{
|
||||
std::string result(wide.length(), 0);
|
||||
std::transform(begin(wide), end(wide), result.begin(), [](const wchar_t c) {
|
||||
return static_cast<char>(c);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
std::string unwide(const std::wstring& wide);
|
||||
|
||||
60
src/common/utils/timeutil.cpp
Normal file
60
src/common/utils/timeutil.cpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#include "pch.h"
|
||||
#include "timeutil.h"
|
||||
|
||||
namespace timeutil
|
||||
{
|
||||
std::string format_as_local(const char* format_string, const time_t time)
|
||||
{
|
||||
char timeBuf[256] = {};
|
||||
tm localtime{};
|
||||
localtime_s(&localtime, &time);
|
||||
std::strftime(timeBuf, sizeof(timeBuf), format_string, &localtime);
|
||||
return timeBuf;
|
||||
}
|
||||
|
||||
std::wstring to_string(const time_t time)
|
||||
{
|
||||
return std::to_wstring(static_cast<uint64_t>(time));
|
||||
}
|
||||
|
||||
std::optional<std::time_t> from_string(const std::wstring& s)
|
||||
{
|
||||
try
|
||||
{
|
||||
uint64_t value = std::stoull(s);
|
||||
return static_cast<std::time_t>(value);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
std::time_t now()
|
||||
{
|
||||
return winrt::clock::to_time_t(winrt::clock::now());
|
||||
}
|
||||
|
||||
namespace diff
|
||||
{
|
||||
int64_t in_seconds(const std::time_t to, const std::time_t from)
|
||||
{
|
||||
return static_cast<int64_t>(std::difftime(to, from));
|
||||
}
|
||||
|
||||
int64_t in_minutes(const std::time_t to, const std::time_t from)
|
||||
{
|
||||
return static_cast<int64_t>(std::difftime(to, from) / 60);
|
||||
}
|
||||
|
||||
int64_t in_hours(const std::time_t to, const std::time_t from)
|
||||
{
|
||||
return static_cast<int64_t>(std::difftime(to, from) / 3600);
|
||||
}
|
||||
|
||||
int64_t in_days(const std::time_t to, const std::time_t from)
|
||||
{
|
||||
return static_cast<int64_t>(std::difftime(to, from) / (3600 * 24LL));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,58 +9,22 @@
|
||||
|
||||
namespace timeutil
|
||||
{
|
||||
inline std::string format_as_local(const char* format_string, const time_t time)
|
||||
{
|
||||
char timeBuf[256] = {};
|
||||
tm localtime{};
|
||||
localtime_s(&localtime, &time);
|
||||
std::strftime(timeBuf, sizeof(timeBuf), format_string, &localtime);
|
||||
return timeBuf;
|
||||
}
|
||||
std::string format_as_local(const char* format_string, const time_t time);
|
||||
|
||||
inline std::wstring to_string(const time_t time)
|
||||
{
|
||||
return std::to_wstring(static_cast<uint64_t>(time));
|
||||
}
|
||||
std::wstring to_string(const time_t time);
|
||||
|
||||
inline std::optional<std::time_t> from_string(const std::wstring& s)
|
||||
{
|
||||
try
|
||||
{
|
||||
uint64_t i = std::stoull(s);
|
||||
return static_cast<std::time_t>(i);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
std::optional<std::time_t> from_string(const std::wstring& s);
|
||||
|
||||
inline std::time_t now()
|
||||
{
|
||||
return winrt::clock::to_time_t(winrt::clock::now());
|
||||
}
|
||||
std::time_t now();
|
||||
|
||||
namespace diff
|
||||
{
|
||||
inline int64_t in_seconds(const std::time_t to, const std::time_t from)
|
||||
{
|
||||
return static_cast<int64_t>(std::difftime(to, from));
|
||||
}
|
||||
int64_t in_seconds(std::time_t to, std::time_t from);
|
||||
|
||||
inline int64_t in_minutes(const std::time_t to, const std::time_t from)
|
||||
{
|
||||
return static_cast<int64_t>(std::difftime(to, from) / 60);
|
||||
}
|
||||
int64_t in_minutes(std::time_t to, std::time_t from);
|
||||
|
||||
inline int64_t in_hours(const std::time_t to, const std::time_t from)
|
||||
{
|
||||
return static_cast<int64_t>(std::difftime(to, from) / 3600);
|
||||
}
|
||||
int64_t in_hours(std::time_t to, std::time_t from);
|
||||
|
||||
inline int64_t in_days(const std::time_t to, const std::time_t from)
|
||||
{
|
||||
return static_cast<int64_t>(std::difftime(to, from) / (3600 * 24LL));
|
||||
}
|
||||
int64_t in_days(std::time_t to, std::time_t from);
|
||||
}
|
||||
}
|
||||
|
||||
152
src/common/utils/utils.vcxproj
Normal file
152
src/common/utils/utils.vcxproj
Normal file
@@ -0,0 +1,152 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<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')" />
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</ProjectGuid>
|
||||
<RootNamespace>utils</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<OutDir>..\..\..\$(Platform)\$(Configuration)\</OutDir>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<Import Project="..\..\..\deps\spdlog.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\\..\\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="appMutex.h" />
|
||||
<ClInclude Include="clean_video_conference.h" />
|
||||
<ClInclude Include="color.h" />
|
||||
<ClInclude Include="com_object_factory.h" />
|
||||
<ClInclude Include="elevation.h" />
|
||||
<ClInclude Include="EventLocker.h" />
|
||||
<ClInclude Include="EventWaiter.h" />
|
||||
<ClInclude Include="excluded_apps.h" />
|
||||
<ClInclude Include="exec.h" />
|
||||
<ClInclude Include="game_mode.h" />
|
||||
<ClInclude Include="gpo.h" />
|
||||
<ClInclude Include="HDropIterator.h" />
|
||||
<ClInclude Include="HttpClient.h" />
|
||||
<ClInclude Include="json.h" />
|
||||
<ClInclude Include="language_helper.h" />
|
||||
<ClInclude Include="logger_helper.h" />
|
||||
<ClInclude Include="modulesRegistry.h" />
|
||||
<ClInclude Include="MsiUtils.h" />
|
||||
<ClInclude Include="MsWindowsSettings.h" />
|
||||
<ClInclude Include="OnThreadExecutor.h" />
|
||||
<ClInclude Include="os-detect.h" />
|
||||
<ClInclude Include="package.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="process_path.h" />
|
||||
<ClInclude Include="processApi.h" />
|
||||
<ClInclude Include="ProcessWaiter.h" />
|
||||
<ClInclude Include="registry.h" />
|
||||
<ClInclude Include="resources.h" />
|
||||
<ClInclude Include="serialized.h" />
|
||||
<ClInclude Include="shell_ext_registration.h" />
|
||||
<ClInclude Include="string_utils.h" />
|
||||
<ClInclude Include="timeutil.h" />
|
||||
<ClInclude Include="UnhandledExceptionHandler.h" />
|
||||
<ClInclude Include="winapi_error.h" />
|
||||
<ClInclude Include="window.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' == 'false'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="EventLocker.cpp" />
|
||||
<ClCompile Include="EventWaiter.cpp" />
|
||||
<ClCompile Include="HDropIterator.cpp" />
|
||||
<ClCompile Include="HttpClient.cpp" />
|
||||
<ClCompile Include="MsWindowsSettings.cpp" />
|
||||
<ClCompile Include="MsiUtils.cpp" />
|
||||
<ClCompile Include="OnThreadExecutor.cpp" />
|
||||
<ClCompile Include="ProcessWaiter.cpp" />
|
||||
<ClCompile Include="UnhandledExceptionHandler.cpp" />
|
||||
<ClCompile Include="appMutex.cpp" />
|
||||
<ClCompile Include="clean_video_conference.cpp" />
|
||||
<ClCompile Include="color.cpp" />
|
||||
<ClCompile Include="excluded_apps.cpp" />
|
||||
<ClCompile Include="exec.cpp" />
|
||||
<ClCompile Include="game_mode.cpp" />
|
||||
<ClCompile Include="package.cpp" />
|
||||
<ClCompile Include="elevation.cpp" />
|
||||
<ClCompile Include="resources.cpp" />
|
||||
<ClCompile Include="gpo.cpp" />
|
||||
<ClCompile Include="json.cpp" />
|
||||
<ClCompile Include="language_helper.cpp" />
|
||||
<ClCompile Include="logger_helper.cpp" />
|
||||
<ClCompile Include="modulesRegistry.cpp" />
|
||||
<ClCompile Include="processApi.cpp" />
|
||||
<ClCompile Include="process_path.cpp" />
|
||||
<ClCompile Include="registry.cpp" />
|
||||
<ClCompile Include="shell_ext_registration.cpp" />
|
||||
<ClCompile Include="string_utils.cpp" />
|
||||
<ClCompile Include="timeutil.cpp" />
|
||||
<ClCompile Include="winapi_error.cpp" />
|
||||
<ClCompile Include="window.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\logging\logging.vcxproj">
|
||||
<Project>{7e1e3f13-2bd6-3f75-a6a7-873a2b55c60f}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\logger\logger.vcxproj">
|
||||
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\version\version.vcxproj">
|
||||
<Project>{cc6e41ac-8174-4e8a-8d22-85dd7f4851df}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
<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')" />
|
||||
</ImportGroup>
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
43
src/common/utils/winapi_error.cpp
Normal file
43
src/common/utils/winapi_error.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "pch.h"
|
||||
#include "winapi_error.h"
|
||||
|
||||
std::optional<std::wstring> get_last_error_message(DWORD dw)
|
||||
{
|
||||
std::optional<std::wstring> message;
|
||||
try
|
||||
{
|
||||
const auto msg = std::system_category().message(dw);
|
||||
message.emplace(begin(msg), end(msg));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
std::wstring get_last_error_or_default(DWORD dw)
|
||||
{
|
||||
auto message = get_last_error_message(dw);
|
||||
return message.has_value() ? *message : L"";
|
||||
}
|
||||
|
||||
void show_last_error_message(const wchar_t* functionName, DWORD dw, const wchar_t* errorTitle)
|
||||
{
|
||||
const auto systemMessage = get_last_error_message(dw);
|
||||
if (!systemMessage.has_value())
|
||||
{
|
||||
return;
|
||||
}
|
||||
LPWSTR lpDisplayBuf = static_cast<LPWSTR>(LocalAlloc(LMEM_ZEROINIT, (systemMessage->size() + lstrlenW(functionName) + 40) * sizeof(WCHAR)));
|
||||
if (lpDisplayBuf != nullptr)
|
||||
{
|
||||
StringCchPrintfW(lpDisplayBuf,
|
||||
LocalSize(lpDisplayBuf) / sizeof(WCHAR),
|
||||
L"%s: %s (%d)",
|
||||
functionName,
|
||||
systemMessage->c_str(),
|
||||
dw);
|
||||
MessageBoxW(nullptr, lpDisplayBuf, errorTitle, MB_OK | MB_ICONERROR);
|
||||
LocalFree(lpDisplayBuf);
|
||||
}
|
||||
}
|
||||
@@ -8,43 +8,8 @@
|
||||
#include <system_error>
|
||||
#include <strsafe.h>
|
||||
|
||||
inline std::optional<std::wstring> get_last_error_message(const DWORD dw)
|
||||
{
|
||||
std::optional<std::wstring> message;
|
||||
try
|
||||
{
|
||||
const auto msg = std::system_category().message(dw);
|
||||
message.emplace(begin(msg), end(msg));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
return message;
|
||||
}
|
||||
std::optional<std::wstring> get_last_error_message(DWORD dw);
|
||||
|
||||
inline std::wstring get_last_error_or_default(const DWORD dw)
|
||||
{
|
||||
auto message = get_last_error_message(dw);
|
||||
return message.has_value() ? *message : L"";
|
||||
}
|
||||
std::wstring get_last_error_or_default(DWORD dw);
|
||||
|
||||
inline void show_last_error_message(const wchar_t* functionName, DWORD dw, const wchar_t* errorTitle)
|
||||
{
|
||||
const auto system_message = get_last_error_message(dw);
|
||||
if (!system_message.has_value())
|
||||
{
|
||||
return;
|
||||
}
|
||||
LPWSTR lpDisplayBuf = static_cast<LPWSTR>(LocalAlloc(LMEM_ZEROINIT, (system_message->size() + lstrlenW(functionName) + 40) * sizeof(WCHAR)));
|
||||
if (lpDisplayBuf != NULL)
|
||||
{
|
||||
StringCchPrintfW(lpDisplayBuf,
|
||||
LocalSize(lpDisplayBuf) / sizeof(WCHAR),
|
||||
L"%s: %s (%d)",
|
||||
functionName,
|
||||
system_message->c_str(),
|
||||
dw);
|
||||
MessageBoxW(NULL, lpDisplayBuf, errorTitle, MB_OK | MB_ICONERROR);
|
||||
LocalFree(lpDisplayBuf);
|
||||
}
|
||||
}
|
||||
void show_last_error_message(const wchar_t* functionName, DWORD dw, const wchar_t* errorTitle);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user