mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-03 18:00:25 +01:00
Compare commits
18 Commits
user/yeela
...
dev/vanzue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
761dd2f75a | ||
|
|
27ba536872 | ||
|
|
18efa0559c | ||
|
|
b3e7c9d227 | ||
|
|
49cc504d94 | ||
|
|
18c6d6b0f3 | ||
|
|
4d1f92199c | ||
|
|
dca532cf4b | ||
|
|
b5991642f8 | ||
|
|
84b39a9edc | ||
|
|
67d96b0a13 | ||
|
|
c5d4f992c1 | ||
|
|
11b406feee | ||
|
|
256af8f6e0 | ||
|
|
87c65f9eec | ||
|
|
971c7e9fba | ||
|
|
055c3011cc | ||
|
|
2f7fc91956 |
@@ -1 +0,0 @@
|
||||
../.github/copilot-instructions.md
|
||||
@@ -1 +0,0 @@
|
||||
../.github/agents
|
||||
@@ -1 +0,0 @@
|
||||
../.github/prompts
|
||||
@@ -1 +0,0 @@
|
||||
../.github/instructions
|
||||
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(grep:*)",
|
||||
"Bash(ls:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
../.github/skills
|
||||
1
.github/actions/spell-check/excludes.txt
vendored
1
.github/actions/spell-check/excludes.txt
vendored
@@ -101,6 +101,7 @@
|
||||
^doc/devdocs/akaLinks\.md$
|
||||
^NOTICE\.md$
|
||||
^src/common/CalculatorEngineCommon/exprtk\.hpp$
|
||||
^src/common/UnitTests-CommonUtils/
|
||||
^src/common/ManagedCommon/ColorFormatHelper\.cs$
|
||||
^src/common/notifications/BackgroundActivatorDLL/cpp\.hint$
|
||||
^src/common/sysinternals/Eula/
|
||||
|
||||
106
.github/actions/spell-check/expect.txt
vendored
106
.github/actions/spell-check/expect.txt
vendored
@@ -11,6 +11,7 @@ ACCESSDENIED
|
||||
ACCESSTOKEN
|
||||
acfs
|
||||
ACIE
|
||||
ACR
|
||||
AClient
|
||||
AColumn
|
||||
acrt
|
||||
@@ -44,6 +45,7 @@ ALLCHILDREN
|
||||
ALLINPUT
|
||||
Allman
|
||||
Allmodule
|
||||
ALLNOISE
|
||||
ALLOWUNDO
|
||||
ALLVIEW
|
||||
ALPHATYPE
|
||||
@@ -57,7 +59,6 @@ AOC
|
||||
aocfnapldcnfbofgmbbllojgocaelgdd
|
||||
AOklab
|
||||
aot
|
||||
APARTMENTTHREADED
|
||||
APeriod
|
||||
apicontract
|
||||
apidl
|
||||
@@ -95,6 +96,7 @@ asf
|
||||
Ashcraft
|
||||
AShortcut
|
||||
ASingle
|
||||
ASUS
|
||||
ASSOCCHANGED
|
||||
ASSOCF
|
||||
ASSOCSTR
|
||||
@@ -104,6 +106,7 @@ atl
|
||||
ATRIOX
|
||||
aumid
|
||||
authenticode
|
||||
AUO
|
||||
AUTOBUDDY
|
||||
AUTOCHECKBOX
|
||||
AUTOHIDE
|
||||
@@ -121,6 +124,10 @@ azureaiinference
|
||||
azureinference
|
||||
azureopenai
|
||||
backticks
|
||||
Backlight
|
||||
Badflags
|
||||
Badmode
|
||||
Badparam
|
||||
bbwe
|
||||
BCIE
|
||||
bck
|
||||
@@ -129,6 +136,7 @@ bezelled
|
||||
bhid
|
||||
BIF
|
||||
bigbar
|
||||
BIGGERSIZEOK
|
||||
bigobj
|
||||
binlog
|
||||
binres
|
||||
@@ -193,6 +201,7 @@ Carlseibert
|
||||
CAtl
|
||||
caub
|
||||
CBN
|
||||
Cds
|
||||
cch
|
||||
CCHDEVICENAME
|
||||
CCHFORMNAME
|
||||
@@ -212,6 +221,7 @@ checkmarks
|
||||
CHILDACTIVATE
|
||||
CHILDWINDOW
|
||||
CHOOSEFONT
|
||||
Chunghwa
|
||||
CIBUILD
|
||||
cidl
|
||||
CIELCh
|
||||
@@ -226,7 +236,7 @@ claude
|
||||
CLEARTYPE
|
||||
clickable
|
||||
clickonce
|
||||
CLIENTEDGE
|
||||
clientedge
|
||||
clientid
|
||||
clientside
|
||||
CLIPBOARDUPDATE
|
||||
@@ -238,6 +248,7 @@ CLSCTX
|
||||
clsids
|
||||
Clusion
|
||||
cmder
|
||||
CMN
|
||||
CMDNOTFOUNDMODULEINTERFACE
|
||||
cmdpal
|
||||
CMIC
|
||||
@@ -292,6 +303,7 @@ Corpor
|
||||
cotaskmem
|
||||
COULDNOT
|
||||
countof
|
||||
Cowait
|
||||
covrun
|
||||
cpcontrols
|
||||
cph
|
||||
@@ -310,11 +322,14 @@ CRECT
|
||||
CRH
|
||||
critsec
|
||||
cropandlock
|
||||
crt
|
||||
CROPTOSQUARE
|
||||
Crossdevice
|
||||
csdevkit
|
||||
CSearch
|
||||
CSettings
|
||||
cso
|
||||
CSOT
|
||||
CSRW
|
||||
CStyle
|
||||
cswin
|
||||
@@ -357,11 +372,14 @@ DBPROPIDSET
|
||||
DBPROPSET
|
||||
DBT
|
||||
DCBA
|
||||
DCapabilities
|
||||
DCOM
|
||||
DComposition
|
||||
DCR
|
||||
ddc
|
||||
DDEIf
|
||||
Deact
|
||||
debouncer
|
||||
debugbreak
|
||||
decryptor
|
||||
Dedup
|
||||
@@ -379,6 +397,7 @@ DEFAULTTOPRIMARY
|
||||
DEFERERASE
|
||||
DEFPUSHBUTTON
|
||||
deinitialization
|
||||
DELA
|
||||
DELETEDKEYIMAGE
|
||||
DELETESCANS
|
||||
DEMOTYPE
|
||||
@@ -413,18 +432,20 @@ DISABLEASACTIONKEY
|
||||
DISABLENOSCROLL
|
||||
diskmgmt
|
||||
DISPLAYCHANGE
|
||||
DISPLAYCONFIG
|
||||
displayconfig
|
||||
DISPLAYFLAGS
|
||||
DISPLAYFREQUENCY
|
||||
displayname
|
||||
DISPLAYORIENTATION
|
||||
diu
|
||||
divyan
|
||||
Dlg
|
||||
DLGFRAME
|
||||
DLGMODALFRAME
|
||||
dlgmodalframe
|
||||
dlib
|
||||
dllhost
|
||||
dllmain
|
||||
Dmdo
|
||||
DNLEN
|
||||
DONOTROUND
|
||||
DONTVALIDATEPATH
|
||||
@@ -434,6 +455,7 @@ downsampling
|
||||
downscale
|
||||
DPICHANGED
|
||||
DPIs
|
||||
DPMS
|
||||
DPSAPI
|
||||
DQTAT
|
||||
DQTYPE
|
||||
@@ -471,15 +493,19 @@ DWMWINDOWMAXIMIZEDCHANGE
|
||||
DWORDLONG
|
||||
dworigin
|
||||
dwrite
|
||||
Dxva
|
||||
dxgi
|
||||
eab
|
||||
EAccess
|
||||
easeofaccess
|
||||
ecount
|
||||
Edid
|
||||
edid
|
||||
EDITKEYBOARD
|
||||
EDITSHORTCUTS
|
||||
EDITTEXT
|
||||
EFile
|
||||
EInvalid
|
||||
eep
|
||||
eku
|
||||
emojis
|
||||
ENABLEDELAYEDEXPANSION
|
||||
@@ -489,14 +515,15 @@ ENABLETEMPLATE
|
||||
encodedlaunch
|
||||
encryptor
|
||||
ENDSESSION
|
||||
ENot
|
||||
ENSUREVISIBLE
|
||||
ENTERSIZEMOVE
|
||||
ENTRYW
|
||||
ENU
|
||||
environmentvariables
|
||||
EOAC
|
||||
EPO
|
||||
epu
|
||||
EProvider
|
||||
ERASEBKGND
|
||||
EREOF
|
||||
EResize
|
||||
@@ -550,6 +577,7 @@ fdx
|
||||
FErase
|
||||
fesf
|
||||
FFFF
|
||||
FFh
|
||||
Figma
|
||||
FILEEXPLORER
|
||||
fileexploreraddons
|
||||
@@ -592,6 +620,7 @@ formatetc
|
||||
FORPARSING
|
||||
foundrylocal
|
||||
FRAMECHANGED
|
||||
Framechanged
|
||||
FRestore
|
||||
frm
|
||||
FROMTOUCH
|
||||
@@ -635,6 +664,7 @@ GMEM
|
||||
GNumber
|
||||
googleai
|
||||
googlegemini
|
||||
Gotchas
|
||||
gpedit
|
||||
gpo
|
||||
GPOCA
|
||||
@@ -652,6 +682,8 @@ gwl
|
||||
GWLP
|
||||
GWLSTYLE
|
||||
hangeul
|
||||
Hann
|
||||
Hantai
|
||||
Hanzi
|
||||
Hardlines
|
||||
hardlinks
|
||||
@@ -710,6 +742,7 @@ HKPD
|
||||
HKU
|
||||
HMD
|
||||
hmenu
|
||||
HMON
|
||||
hmodule
|
||||
hmonitor
|
||||
homies
|
||||
@@ -727,6 +760,7 @@ hotkeys
|
||||
hotlight
|
||||
hotspot
|
||||
HPAINTBUFFER
|
||||
HPhysical
|
||||
HRAWINPUT
|
||||
hredraw
|
||||
hres
|
||||
@@ -737,6 +771,7 @@ hsb
|
||||
HSCROLL
|
||||
hsi
|
||||
HSpeed
|
||||
HSync
|
||||
HTCLIENT
|
||||
hthumbnail
|
||||
HTOUCHINPUT
|
||||
@@ -746,6 +781,7 @@ HVal
|
||||
HValue
|
||||
Hvci
|
||||
hwb
|
||||
HWP
|
||||
HWHEEL
|
||||
HWINEVENTHOOK
|
||||
hwnd
|
||||
@@ -759,6 +795,7 @@ IAI
|
||||
icf
|
||||
ICONERROR
|
||||
ICONLOCATION
|
||||
ICONONLY
|
||||
IDCANCEL
|
||||
IDD
|
||||
idk
|
||||
@@ -802,6 +839,7 @@ INITTOLOGFONTSTRUCT
|
||||
INLINEPREFIX
|
||||
inlines
|
||||
Inno
|
||||
Innolux
|
||||
INPC
|
||||
inproc
|
||||
INPUTHARDWARE
|
||||
@@ -843,6 +881,7 @@ istep
|
||||
ith
|
||||
ITHUMBNAIL
|
||||
IUI
|
||||
IVO
|
||||
IUWP
|
||||
IWIC
|
||||
jeli
|
||||
@@ -856,6 +895,7 @@ jpnime
|
||||
Jsons
|
||||
jsonval
|
||||
jxr
|
||||
Kantai
|
||||
keybd
|
||||
KEYBDDATA
|
||||
KEYBDINPUT
|
||||
@@ -877,6 +917,7 @@ KILLFOCUS
|
||||
killrunner
|
||||
kmph
|
||||
kvp
|
||||
KVM
|
||||
Kybd
|
||||
LARGEICON
|
||||
lastcodeanalysissucceeded
|
||||
@@ -892,12 +933,15 @@ Lclean
|
||||
Ldone
|
||||
Ldr
|
||||
LEFTALIGN
|
||||
leftclick
|
||||
LEFTSCROLLBAR
|
||||
LEFTTEXT
|
||||
leftclick
|
||||
LError
|
||||
LEVELID
|
||||
LExit
|
||||
Lenovo
|
||||
LGD
|
||||
LFU
|
||||
lhwnd
|
||||
LIBFUZZER
|
||||
LIBID
|
||||
@@ -1002,6 +1046,7 @@ MAPTOSAMESHORTCUT
|
||||
MAPVK
|
||||
MARKDOWNPREVIEWHANDLERCPP
|
||||
MAXIMIZEBOX
|
||||
Maximizebox
|
||||
MAXSHORTCUTSIZE
|
||||
maxversiontested
|
||||
mber
|
||||
@@ -1014,12 +1059,14 @@ MDL
|
||||
mdtext
|
||||
mdtxt
|
||||
mdwn
|
||||
mccs
|
||||
meme
|
||||
memicmp
|
||||
MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
MERGECOPY
|
||||
MERGEPAINT
|
||||
Metacharacter
|
||||
metadatamatters
|
||||
Metadatas
|
||||
metafile
|
||||
@@ -1034,6 +1081,7 @@ mikeclayton
|
||||
mindaro
|
||||
Minimizable
|
||||
MINIMIZEBOX
|
||||
Minimizebox
|
||||
MINIMIZEEND
|
||||
MINIMIZESTART
|
||||
MINMAXINFO
|
||||
@@ -1069,7 +1117,8 @@ mouseutils
|
||||
MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
MRM
|
||||
MRT
|
||||
Mrt
|
||||
mrt
|
||||
mru
|
||||
MSAL
|
||||
msc
|
||||
@@ -1095,6 +1144,7 @@ Mso
|
||||
msrc
|
||||
msstore
|
||||
mstsc
|
||||
mswhql
|
||||
msvcp
|
||||
MT
|
||||
MTND
|
||||
@@ -1112,6 +1162,7 @@ MYICON
|
||||
myorg
|
||||
myrepo
|
||||
NAMECHANGE
|
||||
Nanjing
|
||||
namespaceanddescendants
|
||||
nao
|
||||
NCACTIVATE
|
||||
@@ -1180,6 +1231,7 @@ NOMCX
|
||||
NOMINMAX
|
||||
NOMIRRORBITMAP
|
||||
NOMOVE
|
||||
Nomove
|
||||
NONANTIALIASED
|
||||
nonclient
|
||||
NONCLIENTMETRICSW
|
||||
@@ -1201,6 +1253,7 @@ NORMALUSER
|
||||
NOSEARCH
|
||||
NOSENDCHANGING
|
||||
NOSIZE
|
||||
Nosize
|
||||
NOTHOUSANDS
|
||||
NOTICKS
|
||||
NOTIFICATIONSDLL
|
||||
@@ -1208,9 +1261,11 @@ NOTIFYICONDATA
|
||||
NOTIFYICONDATAW
|
||||
NOTIMPL
|
||||
NOTOPMOST
|
||||
Notopmost
|
||||
NOTRACK
|
||||
NOTSRCCOPY
|
||||
NOTSRCERASE
|
||||
Notupdated
|
||||
notwindows
|
||||
NOTXORPEN
|
||||
nowarn
|
||||
@@ -1254,6 +1309,7 @@ opensource
|
||||
openurl
|
||||
openxmlformats
|
||||
OPTIMIZEFORINVOKE
|
||||
Optronics
|
||||
ORPHANEDDIALOGTITLE
|
||||
ORSCANS
|
||||
oss
|
||||
@@ -1289,6 +1345,7 @@ PATINVERT
|
||||
PATPAINT
|
||||
pbc
|
||||
pbi
|
||||
PBP
|
||||
PBlob
|
||||
pbrush
|
||||
pcb
|
||||
@@ -1303,6 +1360,7 @@ PDBs
|
||||
PDEVMODE
|
||||
pdisp
|
||||
PDLL
|
||||
pdmodels
|
||||
pdo
|
||||
pdto
|
||||
pdtobj
|
||||
@@ -1325,6 +1383,7 @@ pguid
|
||||
phbm
|
||||
phbmp
|
||||
phicon
|
||||
PHL
|
||||
Photoshop
|
||||
phwnd
|
||||
pici
|
||||
@@ -1357,6 +1416,8 @@ Popups
|
||||
POPUPWINDOW
|
||||
POSITIONITEM
|
||||
POWERBROADCAST
|
||||
powerdisplay
|
||||
POWERDISPLAYMODULEINTERFACE
|
||||
POWERRENAMECONTEXTMENU
|
||||
powerrenameinput
|
||||
POWERRENAMETEST
|
||||
@@ -1411,6 +1472,7 @@ projectname
|
||||
PROPERTYKEY
|
||||
Propset
|
||||
PROPVARIANT
|
||||
prot
|
||||
PRTL
|
||||
prvpane
|
||||
psapi
|
||||
@@ -1438,12 +1500,16 @@ PTOKEN
|
||||
PToy
|
||||
ptstr
|
||||
pui
|
||||
pvct
|
||||
PWAs
|
||||
pwcs
|
||||
PWSTR
|
||||
pwsz
|
||||
pwtd
|
||||
Qdc
|
||||
QDC
|
||||
qdc
|
||||
QDS
|
||||
qit
|
||||
QITAB
|
||||
QITABENT
|
||||
@@ -1487,7 +1553,9 @@ regfile
|
||||
REGISTERCLASSFAILED
|
||||
REGISTRYHEADER
|
||||
REGISTRYPREVIEWEXT
|
||||
registryroot
|
||||
regkey
|
||||
regroot
|
||||
regsvr
|
||||
REINSTALLMODE
|
||||
releaseblog
|
||||
@@ -1532,7 +1600,6 @@ riid
|
||||
RKey
|
||||
RNumber
|
||||
rollups
|
||||
ROOTOWNER
|
||||
rop
|
||||
ROUNDSMALL
|
||||
ROWSETEXT
|
||||
@@ -1665,6 +1732,7 @@ sigdn
|
||||
Signedness
|
||||
SIGNINGSCENARIO
|
||||
signtool
|
||||
SIIGBF
|
||||
SINGLEKEY
|
||||
sipolicy
|
||||
SIZEBOX
|
||||
@@ -1729,6 +1797,7 @@ STARTUPINFOW
|
||||
startupscreen
|
||||
STATFLAG
|
||||
STATICEDGE
|
||||
Staticedge
|
||||
staticmethod
|
||||
STATSTG
|
||||
stdafx
|
||||
@@ -1765,6 +1834,7 @@ subkeys
|
||||
sublang
|
||||
SUBMODULEUPDATE
|
||||
subresource
|
||||
swp
|
||||
Superbar
|
||||
sut
|
||||
svchost
|
||||
@@ -1773,7 +1843,8 @@ SVGIO
|
||||
svgz
|
||||
SVSI
|
||||
SWFO
|
||||
swp
|
||||
SWP
|
||||
Swp
|
||||
SWPNOSIZE
|
||||
SWPNOZORDER
|
||||
SWRESTORE
|
||||
@@ -1833,7 +1904,9 @@ THEMECHANGED
|
||||
themeresources
|
||||
THH
|
||||
THICKFRAME
|
||||
Thickframe
|
||||
THISCOMPONENT
|
||||
Tianma
|
||||
throughs
|
||||
TILEDWINDOW
|
||||
TILLSON
|
||||
@@ -1914,13 +1987,13 @@ UNLEN
|
||||
UNORM
|
||||
unremapped
|
||||
Unsubscribes
|
||||
unsubscribes
|
||||
unvirtualized
|
||||
unwide
|
||||
unzoom
|
||||
UOffset
|
||||
UOI
|
||||
UPDATENOW
|
||||
UPDATEREGISTRY
|
||||
updown
|
||||
UPGRADINGPRODUCTCODE
|
||||
upscaling
|
||||
@@ -1947,6 +2020,8 @@ vcamp
|
||||
vcenter
|
||||
vcgtq
|
||||
VCINSTALLDIR
|
||||
vcp
|
||||
vcpname
|
||||
Vcpkg
|
||||
VCRT
|
||||
vcruntime
|
||||
@@ -1959,6 +2034,8 @@ VERIFYCONTEXT
|
||||
VERSIONINFO
|
||||
VERTRES
|
||||
VERTSIZE
|
||||
VESA
|
||||
vesa
|
||||
VFT
|
||||
vget
|
||||
vgetq
|
||||
@@ -1990,6 +2067,7 @@ VSM
|
||||
vso
|
||||
vsonline
|
||||
VSpeed
|
||||
VSync
|
||||
vstemplate
|
||||
vstest
|
||||
VSTHRD
|
||||
@@ -2031,7 +2109,7 @@ winapi
|
||||
winappsdk
|
||||
windir
|
||||
WINDOWCREATED
|
||||
WINDOWEDGE
|
||||
windowedge
|
||||
WINDOWINFO
|
||||
WINDOWNAME
|
||||
WINDOWPLACEMENT
|
||||
@@ -2055,12 +2133,12 @@ WINL
|
||||
winlogon
|
||||
winmd
|
||||
winml
|
||||
WINNT
|
||||
winres
|
||||
winrt
|
||||
winsdk
|
||||
winsta
|
||||
WINTHRESHOLD
|
||||
WINNT
|
||||
WINVER
|
||||
winxamlmanager
|
||||
withinrafael
|
||||
@@ -2072,6 +2150,7 @@ WKSG
|
||||
Wlkr
|
||||
wmain
|
||||
Wman
|
||||
wmi
|
||||
WMI
|
||||
WMICIM
|
||||
wmimgmt
|
||||
@@ -2084,6 +2163,7 @@ WNDCLASSEX
|
||||
WNDCLASSEXW
|
||||
WNDCLASSW
|
||||
WNDPROC
|
||||
Wndproc
|
||||
wnode
|
||||
wom
|
||||
WORKSPACESEDITOR
|
||||
|
||||
13
.github/actions/spell-check/patterns.txt
vendored
13
.github/actions/spell-check/patterns.txt
vendored
@@ -274,5 +274,18 @@ St&yle
|
||||
# 0x6f677548 is user name but user folder causes a flag
|
||||
\bx6f677548\b
|
||||
|
||||
# Windows API constants and hardware interface terms
|
||||
\bCOINIT[_A-Z]*\b
|
||||
\bEOAC[_A-Z]*\b
|
||||
\b(?:RPC_C_AUTHN_)?WINNT\b
|
||||
\bUPDATEREGISTRY\b
|
||||
\b(?:CDS_)?UPDATEREGISTRY\b
|
||||
|
||||
# Display interface terms (HDMI, DVI, DisplayPort)
|
||||
\b(?:HDMI|DVI|DisplayPort)(?:-\d+)?\b
|
||||
|
||||
# 2D Region struct names
|
||||
\bDisplayConfig2?D?Region\b
|
||||
|
||||
# Microsoft Store URLs and product IDs
|
||||
ms-windows-store://\S+
|
||||
|
||||
@@ -210,6 +210,11 @@
|
||||
"PowerToys.PowerAccentModuleInterface.dll",
|
||||
"PowerToys.PowerAccentKeyboardService.dll",
|
||||
|
||||
"PowerToys.PowerDisplayModuleInterface.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerDisplay.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerDisplay.exe",
|
||||
"PowerDisplay.Lib.dll",
|
||||
|
||||
"WinUI3Apps\\PowerToys.PowerRenameExt.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerRename.exe",
|
||||
"WinUI3Apps\\PowerToys.PowerRenameContextMenu.dll",
|
||||
@@ -378,6 +383,8 @@
|
||||
"UnitsNet.dll",
|
||||
"UtfUnknown.dll",
|
||||
"Wpf.Ui.dll",
|
||||
"WmiLight.dll",
|
||||
"WmiLight.Native.dll",
|
||||
"Shmuelie.WinRTServer.dll",
|
||||
"ToolGood.Words.Pinyin.dll"
|
||||
],
|
||||
|
||||
@@ -91,6 +91,7 @@ extends:
|
||||
official: true
|
||||
codeSign: true
|
||||
runTests: false
|
||||
buildTests: false
|
||||
signingIdentity:
|
||||
serviceName: $(SigningServiceName)
|
||||
appId: $(SigningAppId)
|
||||
|
||||
@@ -258,6 +258,7 @@ jobs:
|
||||
-restore -graph
|
||||
/p:RestorePackagesConfig=true
|
||||
/p:CIBuild=true
|
||||
/p:BuildTests=${{ parameters.buildTests }}
|
||||
/bl:$(LogOutputDirectory)\build-0-main.binlog
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
$(MSBuildCacheParameters)
|
||||
|
||||
@@ -59,6 +59,7 @@ stages:
|
||||
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
|
||||
msBuildCacheIsReadOnly: ${{ parameters.msBuildCacheIsReadOnly }}
|
||||
runTests: ${{ parameters.runTests }}
|
||||
buildTests: true
|
||||
useVSPreview: ${{ parameters.useVSPreview }}
|
||||
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
|
||||
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
|
||||
@@ -78,7 +79,9 @@ stages:
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
demands: ImageOverride -equals SHINE-VS18-Preview
|
||||
${{ else }}:
|
||||
demands: ImageOverride -equals SHINE-VS18-Latest
|
||||
buildConfigurations: [Release]
|
||||
official: false
|
||||
codeSign: false
|
||||
|
||||
@@ -90,9 +90,15 @@ if ($noticeMatch.Success) {
|
||||
$currentNoticePackageList = ""
|
||||
}
|
||||
|
||||
# Test-only packages that are allowed to be in NOTICE.md but not in the build
|
||||
# (e.g., when BuildTests=false, these packages won't appear in the NuGet list)
|
||||
$allowedExtraPackages = @(
|
||||
"- Moq"
|
||||
)
|
||||
|
||||
if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
{
|
||||
Write-Host -ForegroundColor Red "Notice.md does not match NuGet list."
|
||||
Write-Host -ForegroundColor Yellow "Notice.md does not exactly match NuGet list. Analyzing differences..."
|
||||
|
||||
# Show detailed differences
|
||||
$generatedPackages = $returnList -split "`r`n|`n" | Where-Object { $_.Trim() -ne "" } | Sort-Object
|
||||
@@ -105,7 +111,7 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
# Find packages in proj file list but not in NOTICE.md
|
||||
$missingFromNotice = $generatedPackages | Where-Object { $noticePackages -notcontains $_ }
|
||||
if ($missingFromNotice.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Red "MissingFromNotice:"
|
||||
Write-Host -ForegroundColor Red "MissingFromNotice (ERROR - these must be added to NOTICE.md):"
|
||||
foreach ($pkg in $missingFromNotice) {
|
||||
Write-Host -ForegroundColor Red " $pkg"
|
||||
}
|
||||
@@ -114,10 +120,23 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
|
||||
# Find packages in NOTICE.md but not in proj file list
|
||||
$extraInNotice = $noticePackages | Where-Object { $generatedPackages -notcontains $_ }
|
||||
if ($extraInNotice.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Yellow "ExtraInNotice:"
|
||||
foreach ($pkg in $extraInNotice) {
|
||||
Write-Host -ForegroundColor Yellow " $pkg"
|
||||
|
||||
# Filter out allowed extra packages (test-only dependencies)
|
||||
$unexpectedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -notcontains $_ }
|
||||
$allowedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -contains $_ }
|
||||
|
||||
if ($allowedExtra.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Green "ExtraInNotice (OK - allowed test-only packages):"
|
||||
foreach ($pkg in $allowedExtra) {
|
||||
Write-Host -ForegroundColor Green " $pkg"
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
if ($unexpectedExtra.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Red "ExtraInNotice (ERROR - unexpected packages in NOTICE.md):"
|
||||
foreach ($pkg in $unexpectedExtra) {
|
||||
Write-Host -ForegroundColor Red " $pkg"
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
@@ -127,10 +146,17 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
Write-Host " Proj file list has $($generatedPackages.Count) packages"
|
||||
Write-Host " NOTICE.md has $($noticePackages.Count) packages"
|
||||
Write-Host " MissingFromNotice: $($missingFromNotice.Count) packages"
|
||||
Write-Host " ExtraInNotice: $($extraInNotice.Count) packages"
|
||||
Write-Host " ExtraInNotice (allowed): $($allowedExtra.Count) packages"
|
||||
Write-Host " ExtraInNotice (unexpected): $($unexpectedExtra.Count) packages"
|
||||
Write-Host ""
|
||||
|
||||
exit 1
|
||||
# Fail if there are missing packages OR unexpected extra packages
|
||||
if ($missingFromNotice.Count -gt 0 -or $unexpectedExtra.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Red "FAILED: NOTICE.md mismatch detected."
|
||||
exit 1
|
||||
} else {
|
||||
Write-Host -ForegroundColor Green "PASSED: NOTICE.md matches (with allowed test-only packages)."
|
||||
}
|
||||
}
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
<Project ToolsVersion="4.0"
|
||||
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<!-- Skip building C++ test projects when BuildTests=false -->
|
||||
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
|
||||
<UsePrecompiledHeaders>false</UsePrecompiledHeaders>
|
||||
<RunCodeAnalysis>false</RunCodeAnalysis>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Project configurations -->
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
|
||||
@@ -19,6 +19,39 @@
|
||||
<PlatformTarget>$(Platform)</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Completely skip building test projects when BuildTests=false (e.g., Release pipeline).
|
||||
This avoids InternalsVisibleTo/signing issues by not compiling test code at all.
|
||||
Match: projects ending in Test, Tests, UnitTests, UITests, FuzzTests, or in a folder named Tests.
|
||||
Also matches projects starting with UnitTests- (e.g., UnitTests-CommonLib).
|
||||
Also removes all PackageReference/ProjectReference to prevent NuGet restore and dependency builds.
|
||||
Note: Checking both 'false' and 'False' to handle YAML boolean serialization.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(BuildTests)' == 'false' or '$(BuildTests)' == 'False'">
|
||||
<_ProjectName>$(MSBuildProjectName)</_ProjectName>
|
||||
<!-- Match any project ending with "Test" or "Tests" (covers UnitTests, UITests, FuzzTests, etc.) -->
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Test'))">true</_IsSkippedTestProject>
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Tests'))">true</_IsSkippedTestProject>
|
||||
<!-- Match projects starting with UnitTests- or UITest- prefix -->
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UnitTests-'))">true</_IsSkippedTestProject>
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UITest-'))">true</_IsSkippedTestProject>
|
||||
<!-- Match projects in a Tests folder -->
|
||||
<_IsSkippedTestProject Condition="$(MSBuildProjectDirectory.Contains('\Tests\'))">true</_IsSkippedTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
|
||||
<EnableDefaultItems>false</EnableDefaultItems>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateGlobalUsings>false</GenerateGlobalUsings>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<!-- Disable all code analysis for skipped test projects -->
|
||||
<EnableNETAnalyzers>false</EnableNETAnalyzers>
|
||||
<RunAnalyzers>false</RunAnalyzers>
|
||||
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
|
||||
<Version>$(Version).0</Version>
|
||||
<RepositoryUrl>https://github.com/microsoft/PowerToys</RepositoryUrl>
|
||||
@@ -30,7 +63,9 @@
|
||||
<_PropertySheetDisplayName>PowerToys.Root.Props</_PropertySheetDisplayName>
|
||||
<ForceImportBeforeCppProps>$(MsbuildThisFileDirectory)\Cpp.Build.props</ForceImportBeforeCppProps>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
|
||||
|
||||
|
||||
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj' and '$(_IsSkippedTestProject)' != 'true'">
|
||||
<PackageReference Include="StyleCop.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -28,4 +28,41 @@
|
||||
<PropertyGroup Condition="'$(IgnoreExperimentalWarnings)' == 'true'">
|
||||
<NoWarn>$(NoWarn);CS8305;SA1500;CA1852</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
<!-- Skipped test projects when BuildTests=false: no-op build and remove references.
|
||||
This must be in targets (not props) so it runs AFTER the project file adds its items. -->
|
||||
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
|
||||
<BuildDependsOn />
|
||||
<CoreBuildDependsOn />
|
||||
<RebuildDependsOn />
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- For C# projects: remove all items -->
|
||||
<ItemGroup Condition="'$(_IsSkippedTestProject)' == 'true' and '$(MSBuildProjectExtension)' == '.csproj'">
|
||||
<PackageReference Remove="@(PackageReference)" />
|
||||
<ProjectReference Remove="@(ProjectReference)" />
|
||||
<Reference Remove="@(Reference)" />
|
||||
<Compile Remove="@(Compile)" />
|
||||
<Content Remove="@(Content)" />
|
||||
<EmbeddedResource Remove="@(EmbeddedResource)" />
|
||||
<None Remove="@(None)" />
|
||||
<Using Remove="@(Using)" />
|
||||
<GlobalUsing Remove="@(GlobalUsing)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For C++ projects (vcxproj): remove all compile/link items to prevent build -->
|
||||
<ItemGroup Condition="'$(_IsSkippedTestProject)' == 'true' and '$(MSBuildProjectExtension)' == '.vcxproj'">
|
||||
<ClCompile Remove="@(ClCompile)" />
|
||||
<ClInclude Remove="@(ClInclude)" />
|
||||
<Link Remove="@(Link)" />
|
||||
<Lib Remove="@(Lib)" />
|
||||
<ProjectReference Remove="@(ProjectReference)" />
|
||||
<None Remove="@(None)" />
|
||||
<ResourceCompile Remove="@(ResourceCompile)" />
|
||||
<Midl Remove="@(Midl)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Note: For C++ skipped test projects, build is effectively skipped by removing all compile items above.
|
||||
We don't define empty Build/Rebuild/Clean targets here because MSBuild Target definitions with Condition
|
||||
on the Target element still override the default targets even when condition is false. -->
|
||||
</Project>
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
|
||||
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.5.0" />
|
||||
<PackageVersion Include="Polly.Core" Version="8.6.5" />
|
||||
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
|
||||
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
||||
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
||||
@@ -104,6 +105,7 @@
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<!-- Package System.CodeDom added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Management but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="9.0.0" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.10" />
|
||||
@@ -133,6 +135,7 @@
|
||||
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||
<PackageVersion Include="WinUIEx" Version="2.8.0" />
|
||||
<PackageVersion Include="WmiLight" Version="6.14.0" />
|
||||
<PackageVersion Include="WPF-UI" Version="3.0.5" />
|
||||
<PackageVersion Include="WyHash" Version="1.0.5" />
|
||||
<PackageVersion Include="WixToolset.Heat" Version="5.0.2" />
|
||||
|
||||
32
NOTICE.md
32
NOTICE.md
@@ -10,6 +10,7 @@ This software incorporates material from third parties.
|
||||
- Installer/Runner
|
||||
- Measure tool
|
||||
- Peek
|
||||
- PowerDisplay
|
||||
- Registry Preview
|
||||
|
||||
## Utility: Color Picker
|
||||
@@ -1519,6 +1520,35 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
## Utility: PowerDisplay
|
||||
|
||||
### Twinkle Tray
|
||||
|
||||
PowerDisplay's DDC/CI implementation references techniques from Twinkle Tray.
|
||||
|
||||
**Source**: https://github.com/xanderfrangos/twinkle-tray
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright © 2020 Xander Frangos
|
||||
|
||||
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.
|
||||
|
||||
## NuGet Packages used by PowerToys
|
||||
|
||||
@@ -1557,6 +1587,7 @@ SOFTWARE.
|
||||
- NLog.Extensions.Logging
|
||||
- NLog.Schema
|
||||
- OpenAI
|
||||
- Polly.Core
|
||||
- ReverseMarkdown
|
||||
- ScipBe.Common.Office.OneNote
|
||||
- SharpCompress
|
||||
@@ -1569,5 +1600,6 @@ SOFTWARE.
|
||||
- UnitsNet
|
||||
- UTF.Unknown
|
||||
- WinUIEx
|
||||
- WmiLight
|
||||
- WPF-UI
|
||||
- WyHash
|
||||
@@ -55,6 +55,7 @@
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj" Id="1a066c63-64b3-45f8-92fe-664e1cce8077" />
|
||||
<Project Path="src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.vcxproj" Id="8b5cfb38-ccba-40a8-ad7a-89c57b070884" />
|
||||
<Project Path="src/common/updating/updating.vcxproj" Id="17da04df-e393-4397-9cf0-84dabe11032e" />
|
||||
<Project Path="src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
|
||||
</Folder>
|
||||
@@ -684,6 +685,23 @@
|
||||
<Deploy />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/PowerDisplay/">
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib/PowerDisplay.Lib.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj" Id="d1234567-8901-2345-6789-abcdef012345" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/PowerDisplay/Tests/">
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/PowerDisplay.Lib.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/MeasureTool/">
|
||||
<Project Path="src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj" Id="54a93af7-60c7-4f6c-99d2-fbb1f75f853a">
|
||||
<BuildDependency Project="src/common/Display/Display.vcxproj" />
|
||||
|
||||
311
doc/devdocs/development/new-powertoy.md
Normal file
311
doc/devdocs/development/new-powertoy.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# 🧭 Creating a new PowerToy: end-to-end developer guide
|
||||
|
||||
First of all, thank you for wanting to contribute to PowerToys. The work we do would not be possible without the support of community supporters like you.
|
||||
|
||||
This guide documents the process of building a new PowerToys utility from scratch, including architecture decisions, integration steps, and common pitfalls.
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview and prerequisites
|
||||
|
||||
A PowerToy module is a self-contained utility integrated into the PowerToys ecosystem. It can be UI-based, service-based, or both.
|
||||
|
||||
### Requirements
|
||||
|
||||
- [Visual Studio 2026](https://visualstudio.microsoft.com/downloads/) and the following workloads/individual components:
|
||||
- Desktop Development with C++
|
||||
- WinUI application development
|
||||
- .NET desktop development
|
||||
- Windows 10 SDK (10.0.22621.0)
|
||||
- Windows 11 SDK (10.0.26100.3916)
|
||||
- .NET 8 SDK
|
||||
- Fork the [PowerToys repository](https://github.com/microsoft/PowerToys/tree/main) locally
|
||||
- [Validate that you are able to build and run](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/development/debugging.md) `PowerToys.slnx`.
|
||||
|
||||
Optional:
|
||||
- [WiX v5 toolset](https://github.com/microsoft/PowerToys/tree/main) for the installer
|
||||
|
||||
> [!NOTE]
|
||||
> To ensure all the correct VS Workloads are installed, use [the WinGet configuration files](https://github.com/microsoft/PowerToys/tree/e13d6a78aafbcf32a4bb5f8581d041e1d057c3f1/.config) in the project repository. (Use the one that matches your VS distribution. ie: VS Community would use `configuration.winget`)
|
||||
|
||||
### Folder structure
|
||||
|
||||
```
|
||||
src/
|
||||
modules/
|
||||
your_module/
|
||||
YourModule.sln
|
||||
YourModuleInterface/
|
||||
YourModuleUI/ (if needed)
|
||||
YourModuleService/ (if needed)
|
||||
```
|
||||
|
||||
---
|
||||
## 2. Design and planning
|
||||
|
||||
### Decide the type of module
|
||||
|
||||
Think about how your module works and which existing modules behave similarly. You are going to want to think about the UI needed for the application, the lifecycle, whether it is a service that is always running or event based. Below are some basic scenarios with some modules to explore. You can write your application in C++ or C#.
|
||||
- **UI-only:** e.g., ColorPicker
|
||||
- **Background service:** e.g., LightSwitch, Awake
|
||||
- **Hybrid (UI + background logic):** e.g., ShortcutGuide
|
||||
- **C++/C# interop:** e.g., PowerRename
|
||||
|
||||
### Write your module interface
|
||||
|
||||
Begin by setting up the [PowerToy module template project](https://github.com/microsoft/PowerToys/tree/main/tools/project_template). This will generate boilerplate for you to begin your new module. Below are the key headers in the Module Interface (`dllmain.cpp`) and an explanation of their purpose:
|
||||
1. This is where module settings are defined. These can be anything from strings, bools, ints, and even custom Enums.
|
||||
```c++
|
||||
struct ModuleSettings {};
|
||||
```
|
||||
|
||||
2. This is the header for the full class. It inherits the PowerToyModuleIface
|
||||
```c++
|
||||
class ModuleInterface : public PowertoyModuleIface
|
||||
{
|
||||
private:
|
||||
// the private members of the class
|
||||
// Can include the enabled variable, logic for event handlers, or hotkeys.
|
||||
public:
|
||||
// the public members of the class
|
||||
// Will include the constructor and initialization logic.
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Many of the class functions are boilerplate and need simple string replacements with your module name. The rest of the functions below will require bigger changes.
|
||||
|
||||
3. GPO stands for "Group Policy Object" and allows for administrators to configure settings across a network of machines. It is required that your module is on this list of settings. You can right click the `powertoys_gpo` object to go to the definition and set up the `getConfiguredModuleEnabledValue` for your module.
|
||||
```c++
|
||||
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
|
||||
{
|
||||
return powertoys_gpo::getConfiguredModuleEnabledValue();
|
||||
}
|
||||
```
|
||||
|
||||
4. `init_settings()` initializes the settings for the interface. Will either pull from existing settings.json or use defaults.
|
||||
```c++
|
||||
void ModuleInterface::init_settings()
|
||||
```
|
||||
|
||||
5. `get_config` retrieves the settings from the settings.json file.
|
||||
```c++
|
||||
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
|
||||
```
|
||||
|
||||
6. `set_config` sets the new settings to the settings.json file.
|
||||
```c++
|
||||
virtual void set_config(const wchar_t* config) override
|
||||
```
|
||||
|
||||
7. `call_custom_action` allows custom actions to be called based on signals from the settings app.
|
||||
```c++
|
||||
void call_custom_action(const wchar_t* action) override
|
||||
```
|
||||
|
||||
8. Lifecycle events control whether the module is enabled or not, as well as the default status of the module.
|
||||
```c++
|
||||
virtual void enable() // starts the module
|
||||
virtual void disable() // terminates the module and performs any cleanup
|
||||
virtual bool is_enabled() // returns if the module is currently enabled
|
||||
virtual bool is_enabled_by_default() const override // allows the module to dictate whether it should be enabled by default in the PowerToys app.
|
||||
```
|
||||
|
||||
9. Hotkey functions control the status of the hotkey.
|
||||
```c++
|
||||
// takes the hotkey from settings into a format that the interface can understand
|
||||
void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
|
||||
|
||||
// returns the hotkeys from settings
|
||||
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
|
||||
|
||||
// performs logic when the hotkey event is fired
|
||||
virtual bool on_hotkey(size_t hotkeyId) override
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Keep module logic isolated under `/modules/<YourModule>`
|
||||
- Use shared utilities from [`common`](https://github.com/microsoft/PowerToys/tree/main/src/common) instead of cross-module dependencies
|
||||
- init/set/get config use preset functions to access the settings. Check out the [`settings_objects.h`](https://github.com/microsoft/PowerToys/blob/main/src/common/SettingsAPI/settings_helpers.h) in `src\common\SettingsAPI`
|
||||
|
||||
---
|
||||
## 3. Bootstrapping your module
|
||||
|
||||
1. Use the [template](https://github.com/microsoft/PowerToys/tree/main/tools/project_template) to generate the module interface starter code.
|
||||
2. Update all projects and namespaces with your module name.
|
||||
3. Update GUIDs in `.vcxproj` and solution files.
|
||||
4. Update the functions mentioned in the above section with your custom logic.
|
||||
5. In order for your module to be detected by the runner you are required to add references to various lists. In order to register your module, add the corresponding module reference to the lists that can be found in the following files. (Hint: search other modules names to find the lists quicker)
|
||||
- `src/runner/modules.h`
|
||||
- `src/runner/modules.cpp`
|
||||
- `src/runner/resource.h`
|
||||
- `src/runner/settings_window.h`
|
||||
- `src/runner/settings_window.cpp`
|
||||
- `src/runner/main.cpp`
|
||||
- `src/common/logger.h` (for logging)
|
||||
6. ModuleInterface should build your `ModuleInterface.dll`. This will allow the runner to interact with your service.
|
||||
|
||||
> [!TIP]
|
||||
> Mismatched module IDs are one of the most common causes of load failures. Keep your ID consistent across manifest, registry, and service.
|
||||
|
||||
---
|
||||
## 4. Write your service
|
||||
|
||||
This is going to look different for every PowerToy. It may be easier to develop the application independently, and then link in the PowerToys settings logic later. But you have to write the service first, before connecting it to the runner.
|
||||
|
||||
### Notes
|
||||
|
||||
- This is a separate project from the Module Interface.
|
||||
- You can develop this project using C# or C++.
|
||||
- Set the service icon using the `.rc` file.
|
||||
- Set the service name in the `.vcxproj` by setting the `<TargetName>`
|
||||
```
|
||||
<PropertyGroup>
|
||||
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
|
||||
<TargetName>PowerToys.LightSwitchService</TargetName>
|
||||
</PropertyGroup>
|
||||
```
|
||||
- To view the code of the `.vcxproj`, right click the item and select **Unload project**
|
||||
- Use the following functions to interact with settings from your service
|
||||
```
|
||||
ModuleSettings::instance().InitFileWatcher();
|
||||
ModuleSettings::instance().LoadSettings();
|
||||
auto& settings = ModuleSettings::instance().settings();
|
||||
```
|
||||
These come from the `ModuleSettings.h` file that lives with the Service. You can copy this from another module (e.g., Light Switch) and adjust to fit your needs.
|
||||
|
||||
If your module has a user interface:
|
||||
- Use the **WinUI Blank App** template when setting up your project
|
||||
- Use [Windows design best practices](https://learn.microsoft.com/windows/apps/design/basics/)
|
||||
- Use the [WinUI 3 Gallery](https://apps.microsoft.com/detail/9p3jfpwwdzrc) for help with your UI code, and additional guidance.
|
||||
|
||||
## 5. Settings integration
|
||||
|
||||
PowerToys settings are stored per-module as JSON under:
|
||||
|
||||
```
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\<module>\settings.json
|
||||
```
|
||||
|
||||
### Implementation steps
|
||||
|
||||
- In `src\settings-ui\Settings.UI.Library\` create `<module>Properties.cs` and `<module>Settings.cs`
|
||||
- `<module>Properties.cs` is where you will define your defaults. Every setting needs to be represented here. This should match what was set in the Module Interface.
|
||||
- `<module>Settings.cs`is where your settings.json will be built from. The structure should match the following
|
||||
```cs
|
||||
public ModuleSettings()
|
||||
{
|
||||
Name = ModuleName;
|
||||
Version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
|
||||
Properties = new ModuleProperties(); // settings properties you set above.
|
||||
}
|
||||
```
|
||||
|
||||
- In `src\settings-ui\Settings.UI\ViewModels` create `<module>ViewModel.cs` this is where the interaction happens between your settings page in the PowerToys app and the settings file that is stored on the device. Changes here will trigger the settings watcher via a `NotifyPropertyChanged` event.
|
||||
- Create a `SettingsPage.xaml` at `src\settings-ui\Settings.UI\SettingsXAML\Views`. This will be the page where the user interacts with the settings of your module.
|
||||
- Be sure to use resource strings for user facing strings so they can be localized. (`x:Uid` connects to Resources.resw)
|
||||
```xaml
|
||||
// LightSwitch.xaml
|
||||
<ComboBoxItem
|
||||
x:Uid="LightSwitch_ModeOff"
|
||||
AutomationProperties.AutomationId="OffCBItem_LightSwitch"
|
||||
Tag="Off" />
|
||||
|
||||
// Resources.resw
|
||||
<data name="LightSwitch_ModeOff.Content" xml:space="preserve">
|
||||
<value>Off</value>
|
||||
</data>
|
||||
```
|
||||
> [!IMPORTANT]
|
||||
> In the above example we use `.Content` to target the content of the Combobox. This can change per UI element (e.g., `.Text`, `.Header`, etc.)
|
||||
|
||||
> **Reminder:** Manual changes via external editors (VS Code, Notepad) do **not** trigger the settings watcher. Only changes written through PowerToys trigger reloads.
|
||||
|
||||
---
|
||||
|
||||
### Gotchas:
|
||||
|
||||
- Only use the WinUI 3 framework, _not_ UWP.
|
||||
- Use [`DispatcherQueue`](https://learn.microsoft.com/windows/apps/develop/dispatcherqueue) when updating UI from non-UI threads.
|
||||
|
||||
---
|
||||
## 6. Building and debugging
|
||||
|
||||
### Debugging steps
|
||||
|
||||
1. If this is your first time debugging PowerToys, be sure to follow [these steps first](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/development/debugging.md#pre-debugging-setup).
|
||||
2. Set "runner" as the start up project and ensure your build configuration is set to match your system (ARM64/x64)
|
||||
3. Select <kbd>F5</kbd> or the **Local Windows Debugger** button to begin debugging. This should start the PowerToys runner.
|
||||
4. To set breakpoints in your service, select Ctrl+Alt+P and search for your service to attach to the runner.
|
||||
5. Use logs to document changes. The logs live at `%LOCALAPPDATA%\Microsoft\PowerToys\RunnerLogs` and `%LOCALAPPDATA%\Microsoft\PowerToys\Module\Service\<version>` for the specific module.
|
||||
|
||||
> [!TIP]
|
||||
> PowerToys caches `.nuget` artifacts aggressively. Use `git clean -xfd` when builds behave unexpectedly.
|
||||
|
||||
---
|
||||
## 7. Installer and packaging (WiX)
|
||||
|
||||
### Add your module to installer
|
||||
|
||||
1. Install [`WixToolset.Heat`](https://www.nuget.org/packages/WixToolset.Heat/) for Wix5 via nuget
|
||||
2. Inside `installer\PowerToysInstallerVNext` add a new file for your module: `Module.wxs`
|
||||
3. Inside of this file you will need copy the format from another module (ie: Light Switch) and replace the strings and GUID values.
|
||||
4. The key part will be `<!--ModuleNameFiles_Component_Def-->` which is a placeholder for code that will be generated by `generateFileComponents.ps1`.
|
||||
5. Inside `Product.wxs` add a line item in the `<Feature Id="CoreFeature" ... >` section. It will look like a list of ` <ComponentGroupRef Id="ModuleComponentGroup" />` items.
|
||||
6. Inside `generateFileComponents.ps1` you will need to add an entry to the bottom for your new module. It will follow the following format. `-fileListName <Module>Files` will match the string you set in `Module.wxs`, `<ModuleServiceName>` will match the name of your exe.
|
||||
```bash
|
||||
# Module Name
|
||||
Generate-FileList -fileDepsJson "" -fileListName <Module>Files -wxsFilePath $PSScriptRoot\<Module>.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\<ModuleServiceName>"
|
||||
Generate-FileComponents -fileListName "<Module>Files" -wxsFilePath $PSScriptRoot\<Module>.wxs -regroot $registryroot
|
||||
```
|
||||
---
|
||||
## 8. Testing and validation
|
||||
|
||||
### UI tests
|
||||
|
||||
- Place under `/modules/<YourModule>/Tests`
|
||||
- Create a new [WinUI Unit Test App](https://learn.microsoft.com/windows/apps/winui/winui3/testing/create-winui-unit-test-project)
|
||||
- Write unit tests following the format from previous modules (ie: Light Switch). This can be to test your standalone UI (if you're a module like Color Picker) or to verify that the Settings UI in the PowerToys app is controlling your service.
|
||||
|
||||
### Manual validation
|
||||
|
||||
- Enable/disable in PowerToys Settings
|
||||
- Check initialization in logs
|
||||
- Confirm icons, tooltips, and OOBE page appear correctly
|
||||
|
||||
### Pro tips
|
||||
|
||||
1. Validate wake/sleep and elevation states. Background modules often fail silently after resume if event handles aren’t recreated.
|
||||
2. Use Windows Sandbox to simulate clean install environments
|
||||
3. To simulate a "new user" you can delete the PowerToys folder from `%LOCALAPPDATA%\Microsoft`
|
||||
|
||||
### Shortcut conflict detection
|
||||
|
||||
If your module has a shortcut, ensure that it is properly registered following [the steps listed in the documentation](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/core/settings/settings-implementation.md#shortcut-conflict-detection) for conflict detection.
|
||||
|
||||
---
|
||||
## 9. The final touches
|
||||
|
||||
### Out-of-Box experience (OOBE) page
|
||||
|
||||
The OOBE page is a custom settings page that gives the user at a glance information about each module. This window opens before the Settings application for new users and after updates. Create `OOBE<ModuleName>.xaml` at `src\settings-ui\Settings.UI\SettingsXAML\OOBE\Views`. You will also need to add your module name to the enum at `src\settings-ui\Settings.UI\OOBE\Enums\PowerToysModules.cs`.
|
||||
|
||||
### Module assets
|
||||
|
||||
Now that your PowerToy is _done_ you can start to think about the assets that will represent your module.
|
||||
- Module Icon: This will be displayed in a number of places: OOBE page, in the README, on the home screen of PowerToys, on your individual module settings page, etc.
|
||||
- Module Image: This is the image you see at the top of each individual settings page.
|
||||
- OOBE Image: This is the header you see on the OOBE page for each module
|
||||
|
||||
> [!NOTE]
|
||||
> This step is something that the Design team will handle internally to ensure consistency throughout the application. If you have ideas or recommendations on what the icon or screenshots should be for your module feel free to leave it in the "Additional Comments" section of the PR and the team will take it into consideration.
|
||||
|
||||
### Documentation
|
||||
|
||||
There are two types of documentation that will be required when submitting a new PowerToy:
|
||||
1. Developer documentation: This will live in the [PowerToys repo](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/modules) at `/doc/devdocs/modules/` and should tell a developer how to work on your app. It should outline the module architecture, key files, testing, and tips on debugging if necessary.
|
||||
2. Microsoft Learn documentation: When your new Module is ready to be merged into the PowerToys repository, an internal team member will create Microsoft Learn documentation so that users will understand how to use your module. There is not much work on your end as the developer for this step, but keep an eye on your PR in case we need more information about your PowerToy for this step.
|
||||
|
||||
---
|
||||
Thank you again for contributing! If you need help, feel free to [open an issue](https://github.com/microsoft/PowerToys/issues/new/choose) and use the `Needs-Team-Response` label so we know you need attention.
|
||||
@@ -18,13 +18,28 @@ Advanced Paste is a PowerToys module that provides enhanced clipboard pasting wi
|
||||
|
||||
TODO: Add implementation details
|
||||
|
||||
### Paste with AI Preview
|
||||
|
||||
The "Show preview" setting (`ShowCustomPreview`) controls whether AI-generated results are displayed in a preview window before pasting. **The preview feature does not consume additional AI credits**—the preview displays the same AI response that was already generated, cached locally from a single API call.
|
||||
|
||||
The implementation flow:
|
||||
1. User initiates "Paste with AI" action
|
||||
2. A single AI API call is made via `ExecutePasteFormatAsync`
|
||||
3. The result is cached in `GeneratedResponses`
|
||||
4. If preview is enabled, the cached result is displayed in the preview UI
|
||||
5. User can paste the cached result without any additional API calls
|
||||
|
||||
See the `ExecutePasteFormatAsync(PasteFormat, PasteActionSource)` method in `OptionsViewModel.cs` for the implementation.
|
||||
|
||||
## Debugging
|
||||
|
||||
TODO: Add debugging information
|
||||
|
||||
## Settings
|
||||
|
||||
TODO: Add settings documentation
|
||||
| Setting | Description |
|
||||
|---------|-------------|
|
||||
| `ShowCustomPreview` | When enabled, shows AI-generated results in a preview window before pasting. Does not affect AI credit consumption. |
|
||||
|
||||
## Future Improvements
|
||||
|
||||
|
||||
1616
doc/devdocs/modules/powerdisplay/design.md
Normal file
1616
doc/devdocs/modules/powerdisplay/design.md
Normal file
File diff suppressed because it is too large
Load Diff
223
doc/devdocs/modules/powerdisplay/mccsParserDesign.md
Normal file
223
doc/devdocs/modules/powerdisplay/mccsParserDesign.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# MCCS Capabilities String Parser - Recursive Descent Design
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the recursive descent parser implementation for DDC/CI MCCS (Monitor Control Command Set) capabilities strings.
|
||||
|
||||
### Attention!
|
||||
This document and the code implement are generated by Copilot.
|
||||
|
||||
## Grammar Definition (BNF)
|
||||
|
||||
```bnf
|
||||
capabilities ::= ['('] segment* [')']
|
||||
segment ::= identifier '(' segment_content ')'
|
||||
segment_content ::= text | vcp_entries | hex_list
|
||||
vcp_entries ::= vcp_entry*
|
||||
vcp_entry ::= hex_byte [ '(' hex_list ')' ]
|
||||
hex_list ::= hex_byte*
|
||||
hex_byte ::= [0-9A-Fa-f]{2}
|
||||
identifier ::= [a-z_A-Z]+
|
||||
text ::= [^()]+
|
||||
```
|
||||
|
||||
## Example Input
|
||||
|
||||
```
|
||||
(prot(monitor)type(lcd)model(PD3220U)cmds(01 02 03 07)vcp(10 12 14(04 05 06) 16 60(11 12 0F) DC DF)mccs_ver(2.2)vcpname(F0(Custom Setting)))
|
||||
```
|
||||
|
||||
## Parser Architecture
|
||||
|
||||
### Component Hierarchy
|
||||
|
||||
```
|
||||
MccsCapabilitiesParser (main parser)
|
||||
├── ParseCapabilities() → MccsParseResult
|
||||
├── ParseSegment() → ParsedSegment?
|
||||
├── ParseBalancedContent() → string
|
||||
├── ParseIdentifier() → ReadOnlySpan<char>
|
||||
├── ApplySegment() → void
|
||||
│ ├── ParseHexList() → List<byte>
|
||||
│ ├── ParseVcpEntries() → Dictionary<byte, VcpCodeInfo>
|
||||
│ └── ParseVcpNames() → void
|
||||
│
|
||||
├── VcpEntryParser (sub-parser for vcp() content)
|
||||
│ └── TryParseEntry() → VcpEntry
|
||||
│
|
||||
├── VcpNameParser (sub-parser for vcpname() content)
|
||||
│ └── TryParseEntry() → (byte code, string name)
|
||||
│
|
||||
└── WindowParser (sub-parser for windowN() content)
|
||||
├── Parse() → WindowCapability
|
||||
└── ParseSubSegment() → (name, content)?
|
||||
```
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **ref struct for Zero Allocation**
|
||||
- Main parser uses `ref struct` to avoid heap allocation
|
||||
- Works with `ReadOnlySpan<char>` for efficient string slicing
|
||||
- No intermediate string allocations during parsing
|
||||
|
||||
2. **Recursive Descent Pattern**
|
||||
- Each grammar rule has a corresponding parse method
|
||||
- Methods call each other recursively for nested structures
|
||||
- Single-character lookahead via `Peek()`
|
||||
|
||||
3. **Error Recovery**
|
||||
- Errors are accumulated, not thrown
|
||||
- Parser attempts to continue after errors
|
||||
- Returns partial results when possible
|
||||
|
||||
4. **Sub-parsers for Specialized Content**
|
||||
- `VcpEntryParser` for VCP code entries
|
||||
- `VcpNameParser` for custom VCP names
|
||||
- Each sub-parser handles its own grammar subset
|
||||
|
||||
## Parse Methods Detail
|
||||
|
||||
### ParseCapabilities()
|
||||
Entry point. Handles optional outer parentheses and iterates through segments.
|
||||
|
||||
```csharp
|
||||
private MccsParseResult ParseCapabilities()
|
||||
{
|
||||
// Handle optional outer parens
|
||||
// while (!IsAtEnd()) { ParseSegment() }
|
||||
// Return result with accumulated errors
|
||||
}
|
||||
```
|
||||
|
||||
### ParseSegment()
|
||||
Parses a single `identifier(content)` segment.
|
||||
|
||||
```csharp
|
||||
private ParsedSegment? ParseSegment()
|
||||
{
|
||||
// 1. ParseIdentifier()
|
||||
// 2. Expect '('
|
||||
// 3. ParseBalancedContent()
|
||||
// 4. Expect ')'
|
||||
}
|
||||
```
|
||||
|
||||
### ParseBalancedContent()
|
||||
Extracts content between balanced parentheses, handling nested parens.
|
||||
|
||||
```csharp
|
||||
private string ParseBalancedContent()
|
||||
{
|
||||
int depth = 1;
|
||||
while (depth > 0) {
|
||||
if (char == '(') depth++;
|
||||
if (char == ')') depth--;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ParseVcpEntries()
|
||||
Delegates to `VcpEntryParser` for the specialized VCP entry grammar.
|
||||
|
||||
```csharp
|
||||
vcp_entry ::= hex_byte [ '(' hex_list ')' ]
|
||||
|
||||
Examples:
|
||||
- "10" → code=0x10, values=[]
|
||||
- "14(04 05 06)" → code=0x14, values=[4, 5, 6]
|
||||
- "60(11 12 0F)" → code=0x60, values=[0x11, 0x12, 0x0F]
|
||||
```
|
||||
|
||||
## Comparison with Other Approaches
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
|----------|------|------|
|
||||
| **Recursive Descent** (this) | Clear structure, handles nesting, extensible | More code |
|
||||
| **Regex** (DDCSharp) | Concise | Hard to debug, limited nesting |
|
||||
| **Mixed** (original) | Pragmatic | Inconsistent, hard to maintain |
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
- **Time Complexity**: O(n) where n = input length
|
||||
- **Space Complexity**: O(1) for parsing + O(m) for output where m = number of VCP codes
|
||||
- **Allocations**: Minimal - only for output structures
|
||||
|
||||
## Supported Segments
|
||||
|
||||
| Segment | Description | Parser |
|
||||
|---------|-------------|--------|
|
||||
| `prot(...)` | Protocol type | Direct assignment |
|
||||
| `type(...)` | Display type (lcd/crt) | Direct assignment |
|
||||
| `model(...)` | Model name | Direct assignment |
|
||||
| `cmds(...)` | Supported commands | ParseHexList |
|
||||
| `vcp(...)` | VCP code entries | VcpEntryParser |
|
||||
| `mccs_ver(...)` | MCCS version | Direct assignment |
|
||||
| `vcpname(...)` | Custom VCP names | VcpNameParser |
|
||||
| `windowN(...)` | PIP/PBP window capabilities | WindowParser |
|
||||
|
||||
### Window Segment Format
|
||||
|
||||
The `windowN` segment (where N is 1, 2, 3, etc.) describes PIP/PBP window capabilities:
|
||||
|
||||
```
|
||||
window1(type(PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10))
|
||||
```
|
||||
|
||||
| Sub-field | Format | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `type` | `type(PIP)` or `type(PBP)` | Window type (Picture-in-Picture or Picture-by-Picture) |
|
||||
| `area` | `area(x1 y1 x2 y2)` | Window area coordinates in pixels |
|
||||
| `max` | `max(width height)` | Maximum window dimensions |
|
||||
| `min` | `min(width height)` | Minimum window dimensions |
|
||||
| `window` | `window(id)` | Window identifier |
|
||||
|
||||
All sub-fields are optional; missing fields default to zero values.
|
||||
|
||||
## Error Handling
|
||||
|
||||
```csharp
|
||||
public readonly struct ParseError
|
||||
{
|
||||
public int Position { get; } // Character position
|
||||
public string Message { get; } // Human-readable error
|
||||
}
|
||||
|
||||
public sealed class MccsParseResult
|
||||
{
|
||||
public VcpCapabilities Capabilities { get; }
|
||||
public IReadOnlyList<ParseError> Errors { get; }
|
||||
public bool HasErrors => Errors.Count > 0;
|
||||
public bool IsValid => !HasErrors && Capabilities.SupportedVcpCodes.Count > 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Example
|
||||
|
||||
```csharp
|
||||
// Parse capabilities string
|
||||
var result = MccsCapabilitiesParser.Parse(capabilitiesString);
|
||||
|
||||
if (result.IsValid)
|
||||
{
|
||||
var caps = result.Capabilities;
|
||||
Console.WriteLine($"Model: {caps.Model}");
|
||||
Console.WriteLine($"MCCS Version: {caps.MccsVersion}");
|
||||
Console.WriteLine($"VCP Codes: {caps.SupportedVcpCodes.Count}");
|
||||
}
|
||||
|
||||
if (result.HasErrors)
|
||||
{
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
Console.WriteLine($"Parse error at {error.Position}: {error.Message}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Edge Cases Handled
|
||||
|
||||
1. **Missing outer parentheses** (Apple Cinema Display)
|
||||
2. **No spaces between hex bytes** (`010203` vs `01 02 03`)
|
||||
3. **Nested parentheses** in VCP values
|
||||
4. **Unknown segments** (logged but not fatal)
|
||||
5. **Malformed input** (partial results returned)
|
||||
@@ -1549,7 +1549,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
}
|
||||
processes.resize(bytes / sizeof(processes[0]));
|
||||
|
||||
std::array<std::wstring_view, 44> processesToTerminate = {
|
||||
std::array<std::wstring_view, 45> processesToTerminate = {
|
||||
L"PowerToys.PowerLauncher.exe",
|
||||
L"PowerToys.Settings.exe",
|
||||
L"PowerToys.AdvancedPaste.exe",
|
||||
@@ -1565,6 +1565,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
L"PowerToys.PowerRename.exe",
|
||||
L"PowerToys.ImageResizer.exe",
|
||||
L"PowerToys.LightSwitchService.exe",
|
||||
L"PowerToys.PowerDisplay.exe",
|
||||
L"PowerToys.GcodeThumbnailProvider.exe",
|
||||
L"PowerToys.BgcodeThumbnailProvider.exe",
|
||||
L"PowerToys.PdfThumbnailProvider.exe",
|
||||
|
||||
29
installer/PowerToysSetupVNext/PowerDisplay.wxs
Normal file
29
installer/PowerToysSetupVNext/PowerDisplay.wxs
Normal file
@@ -0,0 +1,29 @@
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
|
||||
xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util" >
|
||||
|
||||
<?include $(sys.CURRENTDIR)\Common.wxi?>
|
||||
|
||||
<?define PowerDisplayAssetsFiles=?>
|
||||
<?define PowerDisplayAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\PowerDisplay?>
|
||||
|
||||
<Fragment>
|
||||
<!-- Power Display -->
|
||||
<DirectoryRef Id="WinUI3AppsAssetsFolder">
|
||||
<Directory Id="PowerDisplayAssetsInstallFolder" Name="PowerDisplay" />
|
||||
</DirectoryRef>
|
||||
<DirectoryRef Id="PowerDisplayAssetsInstallFolder" FileSource="$(var.PowerDisplayAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--PowerDisplayAssetsFiles_Component_Def-->
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="PowerDisplayComponentGroup">
|
||||
<Component Id="RemovePowerDisplayFolder" Guid="B8F2E3A5-72C1-4A2D-9B3F-8E5D7C6A4F9B" Directory="PowerDisplayAssetsInstallFolder" >
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="RemovePowerDisplayFolder" Value="" KeyPath="yes"/>
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderPowerDisplayAssetsFolder" Directory="PowerDisplayAssetsInstallFolder" On="uninstall"/>
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
</Wix>
|
||||
@@ -47,6 +47,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
|
||||
call move /Y ..\..\..\NewPlus.wxs.bk ..\..\..\NewPlus.wxs
|
||||
call move /Y ..\..\..\Peek.wxs.bk ..\..\..\Peek.wxs
|
||||
call move /Y ..\..\..\PowerRename.wxs.bk ..\..\..\PowerRename.wxs
|
||||
call move /Y ..\..\..\PowerDisplay.wxs.bk ..\..\..\PowerDisplay.wxs
|
||||
call move /Y ..\..\..\Product.wxs.bk ..\..\..\Product.wxs
|
||||
call move /Y ..\..\..\RegistryPreview.wxs.bk ..\..\..\RegistryPreview.wxs
|
||||
call move /Y ..\..\..\Resources.wxs.bk ..\..\..\Resources.wxs
|
||||
@@ -123,6 +124,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
|
||||
<Compile Include="KeyboardManager.wxs" />
|
||||
<Compile Include="Peek.wxs" />
|
||||
<Compile Include="PowerRename.wxs" />
|
||||
<Compile Include="PowerDisplay.wxs" />
|
||||
<Compile Include="DscResources.wxs" />
|
||||
<Compile Include="RegistryPreview.wxs" />
|
||||
<Compile Include="Run.wxs" />
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
<ComponentGroupRef Id="LightSwitchComponentGroup" />
|
||||
<ComponentGroupRef Id="PeekComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerRenameComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
|
||||
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
|
||||
<ComponentGroupRef Id="RunComponentGroup" />
|
||||
<ComponentGroupRef Id="SettingsComponentGroup" />
|
||||
|
||||
@@ -367,6 +367,12 @@
|
||||
</RegistryKey>
|
||||
<File Id="BgcodePreviewHandler_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.BgcodePreviewHandler.resources.dll" />
|
||||
</Component>
|
||||
<Component Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER" Guid="$(var.CompGUIDPrefix)23">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<File Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\Microsoft.CmdPal.Ext.PowerToys.resources.dll" />
|
||||
</Component>
|
||||
<?undef IdSafeLanguage?>
|
||||
<?undef CompGUIDPrefix?>
|
||||
<?endforeach?>
|
||||
|
||||
@@ -176,6 +176,10 @@ Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PS
|
||||
Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService"
|
||||
Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs
|
||||
|
||||
#PowerDisplay
|
||||
Generate-FileList -fileDepsJson "" -fileListName PowerDisplayAssetsFiles -wxsFilePath $PSScriptRoot\PowerDisplay.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerDisplay"
|
||||
Generate-FileComponents -fileListName "PowerDisplayAssetsFiles" -wxsFilePath $PSScriptRoot\PowerDisplay.wxs
|
||||
|
||||
#New+
|
||||
Generate-FileList -fileDepsJson "" -fileListName NewPlusAssetsFiles -wxsFilePath $PSScriptRoot\NewPlus.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\NewPlus"
|
||||
Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs
|
||||
|
||||
@@ -45,6 +45,7 @@ namespace Common.UI
|
||||
NewPlus,
|
||||
CmdPal,
|
||||
ZoomIt,
|
||||
PowerDisplay,
|
||||
}
|
||||
|
||||
private static string SettingsWindowNameToString(SettingsWindow value)
|
||||
@@ -115,6 +116,8 @@ namespace Common.UI
|
||||
return "CmdPal";
|
||||
case SettingsWindow.ZoomIt:
|
||||
return "ZoomIt";
|
||||
case SettingsWindow.PowerDisplay:
|
||||
return "PowerDisplay";
|
||||
default:
|
||||
{
|
||||
return string.Empty;
|
||||
|
||||
@@ -32,6 +32,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredLightSwitchEnabledValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetConfiguredPowerDisplayEnabledValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredPowerDisplayEnabledValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetConfiguredFancyZonesEnabledValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredFancyZonesEnabledValue());
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredPowerDisplayEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace PowerToys
|
||||
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredPowerDisplayEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace ManagedCommon
|
||||
PowerRename,
|
||||
PowerLauncher,
|
||||
PowerAccent,
|
||||
PowerDisplay,
|
||||
RegistryPreview,
|
||||
MeasureTool,
|
||||
ShortcutGuide,
|
||||
|
||||
120
src/common/UnitTests-CommonUtils/AppMutex.Tests.cpp
Normal file
120
src/common/UnitTests-CommonUtils/AppMutex.Tests.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <appMutex.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(AppMutexTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(CreateAppMutex_ValidName_ReturnsHandle)
|
||||
{
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_1";
|
||||
auto handle = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle.get());
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_SameName_ReturnsExistingHandle)
|
||||
{
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_2";
|
||||
|
||||
auto handle1 = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle1.get());
|
||||
|
||||
auto handle2 = createAppMutex(mutexName);
|
||||
Assert::IsNull(handle2.get());
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_DifferentNames_ReturnsDifferentHandles)
|
||||
{
|
||||
std::wstring mutexName1 = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_A";
|
||||
std::wstring mutexName2 = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_B";
|
||||
|
||||
auto handle1 = createAppMutex(mutexName1);
|
||||
auto handle2 = createAppMutex(mutexName2);
|
||||
|
||||
Assert::IsNotNull(handle1.get());
|
||||
Assert::IsNotNull(handle2.get());
|
||||
Assert::AreNotEqual(handle1.get(), handle2.get());
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_EmptyName_ReturnsHandle)
|
||||
{
|
||||
// Empty name creates unnamed mutex
|
||||
auto handle = createAppMutex(L"");
|
||||
// CreateMutexW with empty string should still work
|
||||
Assert::IsTrue(true);
|
||||
// Test passes regardless - just checking it doesn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_LongName_ReturnsHandle)
|
||||
{
|
||||
// Create a long mutex name
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_";
|
||||
for (int i = 0; i < 50; ++i)
|
||||
{
|
||||
mutexName += L"LongNameSegment";
|
||||
}
|
||||
|
||||
auto handle = createAppMutex(mutexName);
|
||||
// Long names might fail, but shouldn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_SpecialCharacters_ReturnsHandle)
|
||||
{
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_Special!@#$%";
|
||||
|
||||
auto handle = createAppMutex(mutexName);
|
||||
// Some special characters might not be valid in mutex names
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_GlobalPrefix_ReturnsHandle)
|
||||
{
|
||||
// Global prefix for cross-session mutex
|
||||
std::wstring mutexName = L"Global\\TestMutex_" + std::to_wstring(GetCurrentProcessId());
|
||||
|
||||
auto handle = createAppMutex(mutexName);
|
||||
// Might require elevation, but shouldn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_LocalPrefix_ReturnsHandle)
|
||||
{
|
||||
std::wstring mutexName = L"Local\\TestMutex_" + std::to_wstring(GetCurrentProcessId());
|
||||
|
||||
auto handle = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle.get());
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_MultipleCalls_AllSucceed)
|
||||
{
|
||||
std::vector<wil::unique_mutex_nothrow> handles;
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) +
|
||||
L"_Multi_" + std::to_wstring(i);
|
||||
auto handle = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle.get());
|
||||
handles.push_back(std::move(handle));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_ReleaseAndRecreate_Works)
|
||||
{
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_Recreate";
|
||||
|
||||
auto handle1 = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle1.get());
|
||||
handle1.reset();
|
||||
|
||||
// After closing, should be able to create again
|
||||
auto handle2 = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle2.get());
|
||||
}
|
||||
};
|
||||
}
|
||||
220
src/common/UnitTests-CommonUtils/ColorUtils.Tests.cpp
Normal file
220
src/common/UnitTests-CommonUtils/ColorUtils.Tests.cpp
Normal file
@@ -0,0 +1,220 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <color.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ColorUtilsTests)
|
||||
{
|
||||
public:
|
||||
// checkValidRGB tests
|
||||
TEST_METHOD(CheckValidRGB_ValidBlack_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#000000", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_ValidWhite_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#FFFFFF", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_ValidRed_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#FF0000", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_ValidGreen_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#00FF00", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_ValidBlue_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#0000FF", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_ValidMixed_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#AB12CD", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0xAB), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x12), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0xCD), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_MissingHash_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"FFFFFF", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_TooShort_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#FFF", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_TooLong_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#FFFFFFFF", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_InvalidChars_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#GGHHII", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_LowercaseInvalid_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#ffffff", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_EmptyString_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_OnlyHash_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
// checkValidARGB tests
|
||||
TEST_METHOD(CheckValidARGB_ValidBlackOpaque_ReturnsTrue)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#FF000000", &a, &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), a);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_ValidWhiteOpaque_ReturnsTrue)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#FFFFFFFF", &a, &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), a);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_ValidTransparent_ReturnsTrue)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#00FFFFFF", &a, &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), a);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_ValidSemiTransparent_ReturnsTrue)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#80FF0000", &a, &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x80), a);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_ValidMixed_ReturnsTrue)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#12345678", &a, &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x12), a);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x34), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x56), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x78), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_MissingHash_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"FFFFFFFF", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_TooShort_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#FFFFFF", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_TooLong_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#FFFFFFFFFF", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_InvalidChars_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#GGHHIIJJ", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_LowercaseInvalid_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#ffffffff", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_EmptyString_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
};
|
||||
}
|
||||
228
src/common/UnitTests-CommonUtils/ComObjectFactory.Tests.cpp
Normal file
228
src/common/UnitTests-CommonUtils/ComObjectFactory.Tests.cpp
Normal file
@@ -0,0 +1,228 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <com_object_factory.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
// Test COM object for testing the factory
|
||||
class TestComObject : public IUnknown
|
||||
{
|
||||
public:
|
||||
TestComObject() : m_refCount(1) {}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override
|
||||
{
|
||||
if (riid == IID_IUnknown)
|
||||
{
|
||||
*ppvObject = static_cast<IUnknown*>(this);
|
||||
AddRef();
|
||||
return S_OK;
|
||||
}
|
||||
*ppvObject = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE AddRef() override
|
||||
{
|
||||
return InterlockedIncrement(&m_refCount);
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE Release() override
|
||||
{
|
||||
ULONG count = InterlockedDecrement(&m_refCount);
|
||||
if (count == 0)
|
||||
{
|
||||
delete this;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private:
|
||||
LONG m_refCount;
|
||||
};
|
||||
|
||||
TEST_CLASS(ComObjectFactoryTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ComObjectFactory_Construction_DoesNotCrash)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_QueryInterface_IUnknown_Succeeds)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
IUnknown* pUnknown = nullptr;
|
||||
|
||||
HRESULT hr = factory.QueryInterface(IID_IUnknown, reinterpret_cast<void**>(&pUnknown));
|
||||
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
Assert::IsNotNull(pUnknown);
|
||||
|
||||
if (pUnknown)
|
||||
{
|
||||
pUnknown->Release();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_QueryInterface_IClassFactory_Succeeds)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
IClassFactory* pFactory = nullptr;
|
||||
|
||||
HRESULT hr = factory.QueryInterface(IID_IClassFactory, reinterpret_cast<void**>(&pFactory));
|
||||
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
Assert::IsNotNull(pFactory);
|
||||
|
||||
if (pFactory)
|
||||
{
|
||||
pFactory->Release();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_QueryInterface_InvalidInterface_Fails)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
void* pInterface = nullptr;
|
||||
|
||||
// Random GUID that we don't support
|
||||
GUID randomGuid = { 0x12345678, 0x1234, 0x1234, { 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0 } };
|
||||
HRESULT hr = factory.QueryInterface(randomGuid, &pInterface);
|
||||
|
||||
Assert::AreEqual(E_NOINTERFACE, hr);
|
||||
Assert::IsNull(pInterface);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_AddRef_IncreasesRefCount)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
ULONG count1 = factory.AddRef();
|
||||
ULONG count2 = factory.AddRef();
|
||||
|
||||
Assert::IsTrue(count2 > count1);
|
||||
|
||||
// Clean up
|
||||
factory.Release();
|
||||
factory.Release();
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_Release_DecreasesRefCount)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
factory.AddRef();
|
||||
factory.AddRef();
|
||||
ULONG count1 = factory.Release();
|
||||
ULONG count2 = factory.Release();
|
||||
|
||||
Assert::IsTrue(count2 < count1);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_CreateInstance_NoAggregation_Succeeds)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
IUnknown* pObj = nullptr;
|
||||
|
||||
HRESULT hr = factory.CreateInstance(nullptr, IID_IUnknown, reinterpret_cast<void**>(&pObj));
|
||||
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
Assert::IsNotNull(pObj);
|
||||
|
||||
if (pObj)
|
||||
{
|
||||
pObj->Release();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_CreateInstance_WithAggregation_Fails)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
TestComObject outer;
|
||||
IUnknown* pObj = nullptr;
|
||||
|
||||
// Aggregation should fail for our simple test object
|
||||
HRESULT hr = factory.CreateInstance(&outer, IID_IUnknown, reinterpret_cast<void**>(&pObj));
|
||||
|
||||
Assert::AreEqual(CLASS_E_NOAGGREGATION, hr);
|
||||
Assert::IsNull(pObj);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_CreateInstance_NullOutput_Fails)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
HRESULT hr = factory.CreateInstance(nullptr, IID_IUnknown, nullptr);
|
||||
|
||||
Assert::AreEqual(E_POINTER, hr);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_LockServer_Lock_Succeeds)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
HRESULT hr = factory.LockServer(TRUE);
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
|
||||
// Unlock
|
||||
factory.LockServer(FALSE);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_LockServer_Unlock_Succeeds)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
factory.LockServer(TRUE);
|
||||
HRESULT hr = factory.LockServer(FALSE);
|
||||
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_LockServer_MultipleLocks_Work)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
factory.LockServer(TRUE);
|
||||
factory.LockServer(TRUE);
|
||||
factory.LockServer(TRUE);
|
||||
|
||||
factory.LockServer(FALSE);
|
||||
factory.LockServer(FALSE);
|
||||
HRESULT hr = factory.LockServer(FALSE);
|
||||
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(ComObjectFactory_ConcurrentCreateInstance_Works)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&factory, &successCount]() {
|
||||
IUnknown* pObj = nullptr;
|
||||
HRESULT hr = factory.CreateInstance(nullptr, IID_IUnknown, reinterpret_cast<void**>(&pObj));
|
||||
if (SUCCEEDED(hr) && pObj)
|
||||
{
|
||||
successCount++;
|
||||
pObj->Release();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(10, successCount.load());
|
||||
}
|
||||
};
|
||||
}
|
||||
146
src/common/UnitTests-CommonUtils/Elevation.Tests.cpp
Normal file
146
src/common/UnitTests-CommonUtils/Elevation.Tests.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <elevation.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ElevationTests)
|
||||
{
|
||||
public:
|
||||
// is_process_elevated tests
|
||||
TEST_METHOD(IsProcessElevated_ReturnsBoolean)
|
||||
{
|
||||
bool result = is_process_elevated(false);
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsProcessElevated_CachedValue_ReturnsSameResult)
|
||||
{
|
||||
bool result1 = is_process_elevated(true);
|
||||
bool result2 = is_process_elevated(true);
|
||||
|
||||
// Cached value should be consistent
|
||||
Assert::AreEqual(result1, result2);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsProcessElevated_UncachedValue_ReturnsBoolean)
|
||||
{
|
||||
bool result = is_process_elevated(false);
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsProcessElevated_CachedAndUncached_AreConsistent)
|
||||
{
|
||||
// Both should return the same value for the same process
|
||||
bool cached = is_process_elevated(true);
|
||||
bool uncached = is_process_elevated(false);
|
||||
|
||||
Assert::AreEqual(cached, uncached);
|
||||
}
|
||||
|
||||
// check_user_is_admin tests
|
||||
TEST_METHOD(CheckUserIsAdmin_ReturnsBoolean)
|
||||
{
|
||||
bool result = check_user_is_admin();
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckUserIsAdmin_ConsistentResults)
|
||||
{
|
||||
bool result1 = check_user_is_admin();
|
||||
bool result2 = check_user_is_admin();
|
||||
bool result3 = check_user_is_admin();
|
||||
|
||||
Assert::AreEqual(result1, result2);
|
||||
Assert::AreEqual(result2, result3);
|
||||
}
|
||||
|
||||
// Relationship between elevation and admin
|
||||
TEST_METHOD(ElevationAndAdmin_Relationship)
|
||||
{
|
||||
bool elevated = is_process_elevated(false);
|
||||
bool admin = check_user_is_admin();
|
||||
(void)admin;
|
||||
|
||||
// If elevated, user should typically be admin
|
||||
// But user can be admin without process being elevated
|
||||
if (elevated)
|
||||
{
|
||||
// Elevated process usually means admin user
|
||||
// (though there are edge cases)
|
||||
}
|
||||
// Just verify both functions return without crashing
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// IsProcessOfWindowElevated tests
|
||||
TEST_METHOD(IsProcessOfWindowElevated_DesktopWindow_ReturnsBoolean)
|
||||
{
|
||||
HWND desktop = GetDesktopWindow();
|
||||
if (desktop)
|
||||
{
|
||||
bool result = IsProcessOfWindowElevated(desktop);
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsProcessOfWindowElevated_InvalidHwnd_DoesNotCrash)
|
||||
{
|
||||
bool result = IsProcessOfWindowElevated(nullptr);
|
||||
// Should handle null HWND gracefully
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
// ProcessInfo struct tests
|
||||
TEST_METHOD(ProcessInfo_DefaultConstruction)
|
||||
{
|
||||
ProcessInfo info{};
|
||||
Assert::AreEqual(static_cast<DWORD>(0), info.processID);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(IsProcessElevated_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
is_process_elevated(j % 2 == 0);
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
|
||||
// Performance of cached value
|
||||
TEST_METHOD(IsProcessElevated_CachedPerformance)
|
||||
{
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (int i = 0; i < 10000; ++i)
|
||||
{
|
||||
is_process_elevated(true);
|
||||
}
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
|
||||
// Cached calls should be very fast
|
||||
Assert::IsTrue(duration.count() < 1000);
|
||||
}
|
||||
};
|
||||
}
|
||||
182
src/common/UnitTests-CommonUtils/ExcludedApps.Tests.cpp
Normal file
182
src/common/UnitTests-CommonUtils/ExcludedApps.Tests.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <excluded_apps.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ExcludedAppsTests)
|
||||
{
|
||||
public:
|
||||
// find_app_name_in_path tests
|
||||
TEST_METHOD(FindAppNameInPath_ExactMatch_ReturnsTrue)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsTrue(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_NoMatch_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"calc.exe" };
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_MultipleApps_FindsMatch)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"calc.exe", L"notepad.exe", L"word.exe" };
|
||||
Assert::IsTrue(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_EmptyPath_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_EmptyApps_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\notepad.exe";
|
||||
std::vector<std::wstring> apps = {};
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_PartialMatchInFolder_ReturnsFalse)
|
||||
{
|
||||
// "notepad" appears in folder name but not as the exe name
|
||||
std::wstring path = L"C:\\notepad\\other.exe";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_CaseSensitive_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\NOTEPAD.EXE";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
// The function does rfind which is case-sensitive
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_MatchWithDifferentExtension_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\notepad.com";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_MatchAtEndOfPath_ReturnsTrue)
|
||||
{
|
||||
std::wstring path = L"C:\\Windows\\System32\\notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsTrue(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_UNCPath_Works)
|
||||
{
|
||||
std::wstring path = L"\\\\server\\share\\folder\\app.exe";
|
||||
std::vector<std::wstring> apps = { L"app.exe" };
|
||||
Assert::IsTrue(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
// find_folder_in_path tests
|
||||
TEST_METHOD(FindFolderInPath_FolderExists_ReturnsTrue)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\MyApp\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"Program Files" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_FolderNotExists_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Windows\\System32\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"Program Files" };
|
||||
Assert::IsFalse(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_MultipleFolders_FindsMatch)
|
||||
{
|
||||
std::wstring path = L"C:\\Windows\\System32\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"Program Files", L"System32", L"Users" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_EmptyPath_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"";
|
||||
std::vector<std::wstring> folders = { L"Windows" };
|
||||
Assert::IsFalse(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_EmptyFolders_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Windows\\app.exe";
|
||||
std::vector<std::wstring> folders = {};
|
||||
Assert::IsFalse(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_PartialMatch_ReturnsTrue)
|
||||
{
|
||||
// find_folder_in_path uses rfind which finds substrings
|
||||
std::wstring path = L"C:\\Windows\\System32\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"System" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_NestedFolder_ReturnsTrue)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\Company\\Product\\bin\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"Product" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_RootDrive_ReturnsTrue)
|
||||
{
|
||||
std::wstring path = L"C:\\folder\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"C:\\" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_UNCPath_Works)
|
||||
{
|
||||
std::wstring path = L"\\\\server\\share\\folder\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"share" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_CaseSensitive_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\WINDOWS\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"windows" };
|
||||
// rfind is case-sensitive
|
||||
Assert::IsFalse(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
// Edge case tests
|
||||
TEST_METHOD(FindAppNameInPath_AppNameInMiddleOfPath_HandlesCorrectly)
|
||||
{
|
||||
// The app name appears both in folder and as filename
|
||||
std::wstring path = L"C:\\notepad\\bin\\notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsTrue(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_JustFilename_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
// find_app_name_in_path expects a path separator to validate the executable segment
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_JustFilename_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"app.exe";
|
||||
std::vector<std::wstring> folders = { L"Windows" };
|
||||
Assert::IsFalse(find_folder_in_path(path, folders));
|
||||
}
|
||||
};
|
||||
}
|
||||
148
src/common/UnitTests-CommonUtils/Exec.Tests.cpp
Normal file
148
src/common/UnitTests-CommonUtils/Exec.Tests.cpp
Normal file
@@ -0,0 +1,148 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <exec.h>
|
||||
#include <cctype>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ExecTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ExecAndReadOutput_EchoCommand_ReturnsOutput)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c echo hello", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
// Output should contain "hello"
|
||||
Assert::IsTrue(result->find("hello") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_WhereCommand_ReturnsPath)
|
||||
{
|
||||
auto result = exec_and_read_output(L"where cmd", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
// Should contain path to cmd.exe
|
||||
Assert::IsTrue(result->find("cmd") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_DirCommand_ReturnsListing)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c dir /b C:\\Windows", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
// Should contain some common Windows folder names
|
||||
std::string output = *result;
|
||||
std::transform(output.begin(), output.end(), output.begin(), [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
||||
Assert::IsTrue(output.find("system32") != std::string::npos ||
|
||||
output.find("system") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_InvalidCommand_ReturnsEmptyOrError)
|
||||
{
|
||||
auto result = exec_and_read_output(L"nonexistentcommand12345", 5000);
|
||||
|
||||
// Invalid command should either return nullopt or an error message
|
||||
Assert::IsTrue(!result.has_value() || result->empty() ||
|
||||
result->find("not recognized") != std::string::npos ||
|
||||
result->find("error") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_EmptyCommand_DoesNotCrash)
|
||||
{
|
||||
auto result = exec_and_read_output(L"", 5000);
|
||||
// Should handle empty command gracefully
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_TimeoutExpires_ReturnsAvailableOutput)
|
||||
{
|
||||
// Use a command that produces output slowly
|
||||
// ping localhost will run for a while
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
// Very short timeout
|
||||
auto result = exec_and_read_output(L"ping localhost -n 10", 100);
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start);
|
||||
|
||||
// Should return within reasonable time
|
||||
Assert::IsTrue(elapsed.count() < 5000);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_MultilineOutput_PreservesLines)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c \"echo line1 & echo line2 & echo line3\"", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
// Should contain multiple lines
|
||||
Assert::IsTrue(result->find("line1") != std::string::npos);
|
||||
Assert::IsTrue(result->find("line2") != std::string::npos);
|
||||
Assert::IsTrue(result->find("line3") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_UnicodeOutput_Works)
|
||||
{
|
||||
// Echo a simple ASCII string (Unicode test depends on system codepage)
|
||||
auto result = exec_and_read_output(L"cmd /c echo test123", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsTrue(result->find("test123") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_LongTimeout_Works)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c echo test", 60000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsTrue(result->find("test") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_QuotedArguments_Work)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c echo \"hello world\"", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsTrue(result->find("hello") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_EnvironmentVariable_Expanded)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c echo %USERNAME%", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
// Should not contain the literal %USERNAME% but the actual username
|
||||
// Or if not expanded, still should not crash
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_ExitCode_CommandFails)
|
||||
{
|
||||
// Command that exits with error
|
||||
auto result = exec_and_read_output(L"cmd /c exit 1", 5000);
|
||||
|
||||
// Should still return something (possibly empty)
|
||||
// Just verify it doesn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_ZeroTimeout_DoesNotHang)
|
||||
{
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
auto result = exec_and_read_output(L"cmd /c echo test", 0);
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start);
|
||||
|
||||
// Should complete quickly with zero timeout
|
||||
Assert::IsTrue(elapsed.count() < 5000);
|
||||
}
|
||||
};
|
||||
}
|
||||
68
src/common/UnitTests-CommonUtils/GameMode.Tests.cpp
Normal file
68
src/common/UnitTests-CommonUtils/GameMode.Tests.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <game_mode.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(GameModeTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(DetectGameMode_ReturnsBoolean)
|
||||
{
|
||||
// This function queries Windows game mode status
|
||||
bool result = detect_game_mode();
|
||||
|
||||
// Result depends on current system state, but should be a valid boolean
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(DetectGameMode_ConsistentResults)
|
||||
{
|
||||
// Multiple calls should return consistent results (unless game mode changes)
|
||||
bool result1 = detect_game_mode();
|
||||
bool result2 = detect_game_mode();
|
||||
bool result3 = detect_game_mode();
|
||||
|
||||
// Results should be consistent across rapid calls
|
||||
Assert::AreEqual(result1, result2);
|
||||
Assert::AreEqual(result2, result3);
|
||||
}
|
||||
|
||||
TEST_METHOD(DetectGameMode_DoesNotCrash)
|
||||
{
|
||||
// Call multiple times to ensure no crash or memory leak
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
detect_game_mode();
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(DetectGameMode_ThreadSafe)
|
||||
{
|
||||
// Test that calling from multiple threads is safe
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
detect_game_mode();
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
};
|
||||
}
|
||||
218
src/common/UnitTests-CommonUtils/Gpo.Tests.cpp
Normal file
218
src/common/UnitTests-CommonUtils/Gpo.Tests.cpp
Normal file
@@ -0,0 +1,218 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <gpo.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace powertoys_gpo;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(GpoTests)
|
||||
{
|
||||
public:
|
||||
// Helper to check if result is a valid gpo_rule_configured_t value
|
||||
static constexpr bool IsValidGpoResult(gpo_rule_configured_t result)
|
||||
{
|
||||
return result == gpo_rule_configured_wrong_value ||
|
||||
result == gpo_rule_configured_unavailable ||
|
||||
result == gpo_rule_configured_not_configured ||
|
||||
result == gpo_rule_configured_disabled ||
|
||||
result == gpo_rule_configured_enabled;
|
||||
}
|
||||
|
||||
// gpo_rule_configured_t enum tests
|
||||
TEST_METHOD(GpoRuleConfigured_EnumValues_AreDistinct)
|
||||
{
|
||||
Assert::AreNotEqual(static_cast<int>(gpo_rule_configured_not_configured),
|
||||
static_cast<int>(gpo_rule_configured_enabled));
|
||||
Assert::AreNotEqual(static_cast<int>(gpo_rule_configured_enabled),
|
||||
static_cast<int>(gpo_rule_configured_disabled));
|
||||
Assert::AreNotEqual(static_cast<int>(gpo_rule_configured_not_configured),
|
||||
static_cast<int>(gpo_rule_configured_disabled));
|
||||
}
|
||||
|
||||
// getConfiguredValue tests
|
||||
TEST_METHOD(GetConfiguredValue_NonExistentKey_ReturnsNotConfigured)
|
||||
{
|
||||
auto result = getConfiguredValue(L"NonExistentPolicyValue12345");
|
||||
Assert::IsTrue(result == gpo_rule_configured_not_configured ||
|
||||
result == gpo_rule_configured_unavailable);
|
||||
}
|
||||
|
||||
// Utility enabled getters - these all follow the same pattern
|
||||
TEST_METHOD(GetAllowExperimentationValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getAllowExperimentationValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredAlwaysOnTopEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredAlwaysOnTopEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredAwakeEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredAwakeEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredColorPickerEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredColorPickerEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredFancyZonesEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredFancyZonesEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredFileLocksmithEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredFileLocksmithEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredImageResizerEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredImageResizerEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredKeyboardManagerEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredKeyboardManagerEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredPowerRenameEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredPowerRenameEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredPowerLauncherEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredPowerLauncherEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredShortcutGuideEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredShortcutGuideEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredTextExtractorEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredTextExtractorEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredHostsFileEditorEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredHostsFileEditorEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredMousePointerCrosshairsEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredMousePointerCrosshairsEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredMouseHighlighterEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredMouseHighlighterEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredMouseJumpEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredMouseJumpEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredFindMyMouseEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredFindMyMouseEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredMouseWithoutBordersEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredMouseWithoutBordersEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredAdvancedPasteEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredAdvancedPasteEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredPeekEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredPeekEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredRegistryPreviewEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredRegistryPreviewEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredScreenRulerEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredScreenRulerEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredCropAndLockEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredCropAndLockEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredEnvironmentVariablesEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredEnvironmentVariablesEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
// All GPO functions should not crash
|
||||
TEST_METHOD(AllGpoFunctions_DoNotCrash)
|
||||
{
|
||||
getAllowExperimentationValue();
|
||||
getConfiguredAlwaysOnTopEnabledValue();
|
||||
getConfiguredAwakeEnabledValue();
|
||||
getConfiguredColorPickerEnabledValue();
|
||||
getConfiguredFancyZonesEnabledValue();
|
||||
getConfiguredFileLocksmithEnabledValue();
|
||||
getConfiguredImageResizerEnabledValue();
|
||||
getConfiguredKeyboardManagerEnabledValue();
|
||||
getConfiguredPowerRenameEnabledValue();
|
||||
getConfiguredPowerLauncherEnabledValue();
|
||||
getConfiguredShortcutGuideEnabledValue();
|
||||
getConfiguredTextExtractorEnabledValue();
|
||||
getConfiguredHostsFileEditorEnabledValue();
|
||||
getConfiguredMousePointerCrosshairsEnabledValue();
|
||||
getConfiguredMouseHighlighterEnabledValue();
|
||||
getConfiguredMouseJumpEnabledValue();
|
||||
getConfiguredFindMyMouseEnabledValue();
|
||||
getConfiguredMouseWithoutBordersEnabledValue();
|
||||
getConfiguredAdvancedPasteEnabledValue();
|
||||
getConfiguredPeekEnabledValue();
|
||||
getConfiguredRegistryPreviewEnabledValue();
|
||||
getConfiguredScreenRulerEnabledValue();
|
||||
getConfiguredCropAndLockEnabledValue();
|
||||
getConfiguredEnvironmentVariablesEnabledValue();
|
||||
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
200
src/common/UnitTests-CommonUtils/HDropIterator.Tests.cpp
Normal file
200
src/common/UnitTests-CommonUtils/HDropIterator.Tests.cpp
Normal file
@@ -0,0 +1,200 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <HDropIterator.h>
|
||||
#include <shlobj.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(HDropIteratorTests)
|
||||
{
|
||||
public:
|
||||
// Helper to create a test HDROP structure
|
||||
static HGLOBAL CreateTestHDrop(const std::vector<std::wstring>& files)
|
||||
{
|
||||
// Calculate required size
|
||||
size_t size = sizeof(DROPFILES);
|
||||
for (const auto& file : files)
|
||||
{
|
||||
size += (file.length() + 1) * sizeof(wchar_t);
|
||||
}
|
||||
size += sizeof(wchar_t); // Double null terminator
|
||||
|
||||
HGLOBAL hGlobal = GlobalAlloc(GHND, size);
|
||||
if (!hGlobal) return nullptr;
|
||||
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
if (!pDropFiles)
|
||||
{
|
||||
GlobalFree(hGlobal);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
pDropFiles->pFiles = sizeof(DROPFILES);
|
||||
pDropFiles->fWide = TRUE;
|
||||
|
||||
wchar_t* pData = reinterpret_cast<wchar_t*>(reinterpret_cast<BYTE*>(pDropFiles) + sizeof(DROPFILES));
|
||||
for (const auto& file : files)
|
||||
{
|
||||
wcscpy_s(pData, file.length() + 1, file.c_str());
|
||||
pData += file.length() + 1;
|
||||
}
|
||||
*pData = L'\0'; // Double null terminator
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
return hGlobal;
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_EmptyDrop_IsDoneImmediately)
|
||||
{
|
||||
HGLOBAL hGlobal = CreateTestHDrop({});
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true); // Skip if allocation failed
|
||||
return;
|
||||
}
|
||||
|
||||
STGMEDIUM medium = {};
|
||||
medium.tymed = TYMED_HGLOBAL;
|
||||
medium.hGlobal = hGlobal;
|
||||
|
||||
// Without a proper IDataObject, we can't fully test
|
||||
// Just verify the class can be instantiated conceptually
|
||||
GlobalFree(hGlobal);
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_Iteration_Conceptual)
|
||||
{
|
||||
// This test verifies the concept of iteration
|
||||
// Full integration testing requires a proper IDataObject
|
||||
|
||||
std::vector<std::wstring> testFiles = {
|
||||
L"C:\\test\\file1.txt",
|
||||
L"C:\\test\\file2.txt",
|
||||
L"C:\\test\\file3.txt"
|
||||
};
|
||||
|
||||
HGLOBAL hGlobal = CreateTestHDrop(testFiles);
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify we can create the HDROP structure
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
Assert::IsNotNull(pDropFiles);
|
||||
Assert::IsTrue(pDropFiles->fWide);
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
GlobalFree(hGlobal);
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_SingleFile_Works)
|
||||
{
|
||||
std::vector<std::wstring> testFiles = { L"C:\\test\\single.txt" };
|
||||
|
||||
HGLOBAL hGlobal = CreateTestHDrop(testFiles);
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify structure
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
Assert::IsNotNull(pDropFiles);
|
||||
|
||||
// Read back the file name
|
||||
wchar_t* pData = reinterpret_cast<wchar_t*>(reinterpret_cast<BYTE*>(pDropFiles) + pDropFiles->pFiles);
|
||||
Assert::AreEqual(std::wstring(L"C:\\test\\single.txt"), std::wstring(pData));
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
GlobalFree(hGlobal);
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_MultipleFiles_Structure)
|
||||
{
|
||||
std::vector<std::wstring> testFiles = {
|
||||
L"C:\\file1.txt",
|
||||
L"C:\\file2.txt",
|
||||
L"C:\\file3.txt"
|
||||
};
|
||||
|
||||
HGLOBAL hGlobal = CreateTestHDrop(testFiles);
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
Assert::IsNotNull(pDropFiles);
|
||||
|
||||
// Count files by iterating through null-terminated strings
|
||||
wchar_t* pData = reinterpret_cast<wchar_t*>(reinterpret_cast<BYTE*>(pDropFiles) + pDropFiles->pFiles);
|
||||
int count = 0;
|
||||
while (*pData)
|
||||
{
|
||||
count++;
|
||||
pData += wcslen(pData) + 1;
|
||||
}
|
||||
|
||||
Assert::AreEqual(3, count);
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
GlobalFree(hGlobal);
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_UnicodeFilenames_Work)
|
||||
{
|
||||
std::vector<std::wstring> testFiles = {
|
||||
L"C:\\test\\file.txt"
|
||||
};
|
||||
|
||||
HGLOBAL hGlobal = CreateTestHDrop(testFiles);
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
Assert::IsTrue(pDropFiles->fWide == TRUE);
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
GlobalFree(hGlobal);
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_LongFilenames_Work)
|
||||
{
|
||||
std::wstring longPath = L"C:\\";
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
longPath += L"LongFolderName\\";
|
||||
}
|
||||
longPath += L"file.txt";
|
||||
|
||||
std::vector<std::wstring> testFiles = { longPath };
|
||||
|
||||
HGLOBAL hGlobal = CreateTestHDrop(testFiles);
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
Assert::IsNotNull(pDropFiles);
|
||||
|
||||
wchar_t* pData = reinterpret_cast<wchar_t*>(reinterpret_cast<BYTE*>(pDropFiles) + pDropFiles->pFiles);
|
||||
Assert::AreEqual(longPath, std::wstring(pData));
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
GlobalFree(hGlobal);
|
||||
}
|
||||
};
|
||||
}
|
||||
152
src/common/UnitTests-CommonUtils/HttpClient.Tests.cpp
Normal file
152
src/common/UnitTests-CommonUtils/HttpClient.Tests.cpp
Normal file
@@ -0,0 +1,152 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <HttpClient.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(HttpClientTests)
|
||||
{
|
||||
public:
|
||||
// Note: Network tests may fail in offline environments
|
||||
// These tests are designed to verify the API doesn't crash
|
||||
|
||||
TEST_METHOD(HttpClient_DefaultConstruction)
|
||||
{
|
||||
http::HttpClient client;
|
||||
// Should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HttpClient_Request_InvalidUri_ReturnsEmpty)
|
||||
{
|
||||
http::HttpClient client;
|
||||
|
||||
try
|
||||
{
|
||||
// Invalid URI should not crash, may throw or return empty
|
||||
auto result = client.request(winrt::Windows::Foundation::Uri(L"invalid://not-a-valid-uri"));
|
||||
// If we get here, result may be empty or contain error
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Exception is acceptable for invalid URI
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HttpClient_Download_InvalidUri_DoesNotCrash)
|
||||
{
|
||||
http::HttpClient client;
|
||||
TestHelpers::TempFile tempFile;
|
||||
|
||||
try
|
||||
{
|
||||
auto result = client.download(
|
||||
winrt::Windows::Foundation::Uri(L"https://invalid.invalid.invalid"),
|
||||
tempFile.path());
|
||||
// May return false or throw
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Exception is acceptable for invalid/unreachable URI
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HttpClient_Download_WithCallback_DoesNotCrash)
|
||||
{
|
||||
http::HttpClient client;
|
||||
TestHelpers::TempFile tempFile;
|
||||
std::atomic<int> callbackCount{ 0 };
|
||||
|
||||
try
|
||||
{
|
||||
auto result = client.download(
|
||||
winrt::Windows::Foundation::Uri(L"https://invalid.invalid.invalid"),
|
||||
tempFile.path(),
|
||||
[&callbackCount]([[maybe_unused]] float progress) {
|
||||
callbackCount++;
|
||||
});
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Exception is acceptable
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HttpClient_Download_EmptyPath_DoesNotCrash)
|
||||
{
|
||||
http::HttpClient client;
|
||||
|
||||
try
|
||||
{
|
||||
auto result = client.download(
|
||||
winrt::Windows::Foundation::Uri(L"https://example.com"),
|
||||
L"");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Exception is acceptable for empty path
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// These tests require network access and may be skipped in offline environments
|
||||
TEST_METHOD(HttpClient_Request_ValidUri_ReturnsResult)
|
||||
{
|
||||
// Skip this test in most CI environments
|
||||
// Only run manually to verify network functionality
|
||||
http::HttpClient client;
|
||||
|
||||
try
|
||||
{
|
||||
// Use a reliable, fast-responding URL
|
||||
auto result = client.request(winrt::Windows::Foundation::Uri(L"https://www.microsoft.com"));
|
||||
// Result may or may not be successful depending on network
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Network errors are acceptable in test environment
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// Thread safety test (doesn't require network)
|
||||
TEST_METHOD(HttpClient_MultipleInstances_DoNotCrash)
|
||||
{
|
||||
std::vector<std::unique_ptr<http::HttpClient>> clients;
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
clients.push_back(std::make_unique<http::HttpClient>());
|
||||
}
|
||||
|
||||
// All clients should coexist without crashing
|
||||
Assert::AreEqual(static_cast<size_t>(10), clients.size());
|
||||
}
|
||||
|
||||
TEST_METHOD(HttpClient_ConcurrentConstruction_DoesNotCrash)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
http::HttpClient client;
|
||||
successCount++;
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(5, successCount.load());
|
||||
}
|
||||
};
|
||||
}
|
||||
283
src/common/UnitTests-CommonUtils/Json.Tests.cpp
Normal file
283
src/common/UnitTests-CommonUtils/Json.Tests.cpp
Normal file
@@ -0,0 +1,283 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <json.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace winrt::Windows::Data::Json;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(JsonTests)
|
||||
{
|
||||
public:
|
||||
// from_file tests
|
||||
TEST_METHOD(FromFile_NonExistentFile_ReturnsNullopt)
|
||||
{
|
||||
auto result = json::from_file(L"C:\\NonExistent\\File\\Path.json");
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromFile_ValidJsonFile_ReturnsJsonObject)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
tempFile.write("{\"key\": \"value\"}");
|
||||
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsTrue(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromFile_InvalidJson_ReturnsNullopt)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
tempFile.write("not valid json {{{");
|
||||
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromFile_EmptyFile_ReturnsNullopt)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
// File is empty
|
||||
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromFile_ValidComplexJson_ParsesCorrectly)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
tempFile.write("{\"name\": \"test\", \"value\": 42, \"enabled\": true}");
|
||||
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsTrue(result.has_value());
|
||||
|
||||
auto& obj = *result;
|
||||
Assert::IsTrue(obj.HasKey(L"name"));
|
||||
Assert::IsTrue(obj.HasKey(L"value"));
|
||||
Assert::IsTrue(obj.HasKey(L"enabled"));
|
||||
}
|
||||
|
||||
// to_file tests
|
||||
TEST_METHOD(ToFile_ValidObject_WritesFile)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateStringValue(L"value"));
|
||||
json::to_file(tempFile.path(), obj);
|
||||
|
||||
// Read back and verify
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsTrue(result->HasKey(L"key"));
|
||||
}
|
||||
|
||||
TEST_METHOD(ToFile_ComplexObject_WritesFile)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"name", JsonValue::CreateStringValue(L"test"));
|
||||
obj.SetNamedValue(L"value", JsonValue::CreateNumberValue(42));
|
||||
obj.SetNamedValue(L"enabled", JsonValue::CreateBooleanValue(true));
|
||||
json::to_file(tempFile.path(), obj);
|
||||
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::AreEqual(std::wstring(L"test"), std::wstring(result->GetNamedString(L"name")));
|
||||
Assert::AreEqual(42.0, result->GetNamedNumber(L"value"));
|
||||
Assert::IsTrue(result->GetNamedBoolean(L"enabled"));
|
||||
}
|
||||
|
||||
// has tests
|
||||
TEST_METHOD(Has_ExistingKey_ReturnsTrue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateStringValue(L"value"));
|
||||
Assert::IsTrue(json::has(obj, L"key", JsonValueType::String));
|
||||
}
|
||||
|
||||
TEST_METHOD(Has_NonExistingKey_ReturnsFalse)
|
||||
{
|
||||
JsonObject obj;
|
||||
Assert::IsFalse(json::has(obj, L"key", JsonValueType::String));
|
||||
}
|
||||
|
||||
TEST_METHOD(Has_WrongType_ReturnsFalse)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateStringValue(L"value"));
|
||||
Assert::IsFalse(json::has(obj, L"key", JsonValueType::Number));
|
||||
}
|
||||
|
||||
TEST_METHOD(Has_NumberType_ReturnsTrue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateNumberValue(42));
|
||||
Assert::IsTrue(json::has(obj, L"key", JsonValueType::Number));
|
||||
}
|
||||
|
||||
TEST_METHOD(Has_BooleanType_ReturnsTrue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateBooleanValue(true));
|
||||
Assert::IsTrue(json::has(obj, L"key", JsonValueType::Boolean));
|
||||
}
|
||||
|
||||
TEST_METHOD(Has_ObjectType_ReturnsTrue)
|
||||
{
|
||||
JsonObject obj;
|
||||
JsonObject nested;
|
||||
obj.SetNamedValue(L"key", nested);
|
||||
Assert::IsTrue(json::has(obj, L"key", JsonValueType::Object));
|
||||
}
|
||||
|
||||
// value function tests
|
||||
TEST_METHOD(Value_IntegerType_CreatesNumberValue)
|
||||
{
|
||||
auto val = json::value(42);
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::Number);
|
||||
Assert::AreEqual(42.0, val.GetNumber());
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_DoubleType_CreatesNumberValue)
|
||||
{
|
||||
auto val = json::value(3.14);
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::Number);
|
||||
Assert::AreEqual(3.14, val.GetNumber());
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_BooleanTrue_CreatesBooleanValue)
|
||||
{
|
||||
auto val = json::value(true);
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::Boolean);
|
||||
Assert::IsTrue(val.GetBoolean());
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_BooleanFalse_CreatesBooleanValue)
|
||||
{
|
||||
auto val = json::value(false);
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::Boolean);
|
||||
Assert::IsFalse(val.GetBoolean());
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_String_CreatesStringValue)
|
||||
{
|
||||
auto val = json::value(L"hello");
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::String);
|
||||
Assert::AreEqual(std::wstring(L"hello"), std::wstring(val.GetString()));
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_JsonObject_ReturnsJsonValue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateStringValue(L"value"));
|
||||
auto val = json::value(obj);
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::Object);
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_JsonValue_ReturnsIdentity)
|
||||
{
|
||||
auto original = JsonValue::CreateStringValue(L"test");
|
||||
auto result = json::value(original);
|
||||
Assert::AreEqual(std::wstring(L"test"), std::wstring(result.GetString()));
|
||||
}
|
||||
|
||||
// get function tests
|
||||
TEST_METHOD(Get_BooleanValue_ReturnsValue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"enabled", JsonValue::CreateBooleanValue(true));
|
||||
|
||||
bool result = false;
|
||||
json::get(obj, L"enabled", result);
|
||||
Assert::IsTrue(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_IntValue_ReturnsValue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"count", JsonValue::CreateNumberValue(42));
|
||||
|
||||
int result = 0;
|
||||
json::get(obj, L"count", result);
|
||||
Assert::AreEqual(42, result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_DoubleValue_ReturnsValue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"ratio", JsonValue::CreateNumberValue(3.14));
|
||||
|
||||
double result = 0.0;
|
||||
json::get(obj, L"ratio", result);
|
||||
Assert::AreEqual(3.14, result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_StringValue_ReturnsValue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"name", JsonValue::CreateStringValue(L"test"));
|
||||
|
||||
std::wstring result;
|
||||
json::get(obj, L"name", result);
|
||||
Assert::AreEqual(std::wstring(L"test"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_MissingKey_UsesDefault)
|
||||
{
|
||||
JsonObject obj;
|
||||
|
||||
int result = 0;
|
||||
json::get(obj, L"missing", result, 99);
|
||||
Assert::AreEqual(99, result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_MissingKeyNoDefault_PreservesOriginal)
|
||||
{
|
||||
JsonObject obj;
|
||||
|
||||
int result = 42;
|
||||
json::get(obj, L"missing", result);
|
||||
// When key is missing and no default, original value is preserved
|
||||
Assert::AreEqual(42, result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_JsonObject_ReturnsObject)
|
||||
{
|
||||
JsonObject obj;
|
||||
JsonObject nested;
|
||||
nested.SetNamedValue(L"inner", JsonValue::CreateStringValue(L"value"));
|
||||
obj.SetNamedValue(L"nested", nested);
|
||||
|
||||
JsonObject result;
|
||||
json::get(obj, L"nested", result);
|
||||
Assert::IsTrue(result.HasKey(L"inner"));
|
||||
}
|
||||
|
||||
// Roundtrip tests
|
||||
TEST_METHOD(Roundtrip_ComplexObject_PreservesData)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
|
||||
JsonObject original;
|
||||
original.SetNamedValue(L"string", JsonValue::CreateStringValue(L"hello"));
|
||||
original.SetNamedValue(L"number", JsonValue::CreateNumberValue(42));
|
||||
original.SetNamedValue(L"boolean", JsonValue::CreateBooleanValue(true));
|
||||
|
||||
JsonObject nested;
|
||||
nested.SetNamedValue(L"inner", JsonValue::CreateStringValue(L"world"));
|
||||
original.SetNamedValue(L"object", nested);
|
||||
|
||||
json::to_file(tempFile.path(), original);
|
||||
auto loaded = json::from_file(tempFile.path());
|
||||
|
||||
Assert::IsTrue(loaded.has_value());
|
||||
Assert::AreEqual(std::wstring(L"hello"), std::wstring(loaded->GetNamedString(L"string")));
|
||||
Assert::AreEqual(42.0, loaded->GetNamedNumber(L"number"));
|
||||
Assert::IsTrue(loaded->GetNamedBoolean(L"boolean"));
|
||||
Assert::AreEqual(std::wstring(L"world"), std::wstring(loaded->GetNamedObject(L"object").GetNamedString(L"inner")));
|
||||
}
|
||||
};
|
||||
}
|
||||
180
src/common/UnitTests-CommonUtils/LoggerHelper.Tests.cpp
Normal file
180
src/common/UnitTests-CommonUtils/LoggerHelper.Tests.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <logger_helper.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace LoggerHelpers;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(LoggerHelperTests)
|
||||
{
|
||||
public:
|
||||
// get_log_folder_path tests
|
||||
TEST_METHOD(GetLogFolderPath_ValidAppPath_ReturnsPath)
|
||||
{
|
||||
auto result = get_log_folder_path(L"TestApp");
|
||||
|
||||
Assert::IsFalse(result.empty());
|
||||
// Should contain the app name or be a valid path
|
||||
auto pathStr = result.wstring();
|
||||
Assert::IsTrue(pathStr.length() > 0);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLogFolderPath_EmptyAppPath_ReturnsPath)
|
||||
{
|
||||
auto result = get_log_folder_path(L"");
|
||||
|
||||
// Should still return a base path
|
||||
Assert::IsTrue(true); // Just verify no crash
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLogFolderPath_SpecialCharacters_Works)
|
||||
{
|
||||
auto result = get_log_folder_path(L"Test App With Spaces");
|
||||
|
||||
// Should handle spaces in path
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLogFolderPath_ConsistentResults)
|
||||
{
|
||||
auto result1 = get_log_folder_path(L"TestApp");
|
||||
auto result2 = get_log_folder_path(L"TestApp");
|
||||
|
||||
Assert::AreEqual(result1.wstring(), result2.wstring());
|
||||
}
|
||||
|
||||
// dir_exists tests
|
||||
TEST_METHOD(DirExists_WindowsDirectory_ReturnsTrue)
|
||||
{
|
||||
bool result = dir_exists(std::filesystem::path(L"C:\\Windows"));
|
||||
Assert::IsTrue(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DirExists_NonExistentDirectory_ReturnsFalse)
|
||||
{
|
||||
bool result = dir_exists(std::filesystem::path(L"C:\\NonExistentDir12345"));
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DirExists_FileInsteadOfDir_ReturnsTrue)
|
||||
{
|
||||
// notepad.exe is a file, not a directory
|
||||
bool result = dir_exists(std::filesystem::path(L"C:\\Windows\\notepad.exe"));
|
||||
Assert::IsTrue(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DirExists_EmptyPath_ReturnsFalse)
|
||||
{
|
||||
bool result = dir_exists(std::filesystem::path(L""));
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DirExists_TempDirectory_ReturnsTrue)
|
||||
{
|
||||
wchar_t tempPath[MAX_PATH];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
|
||||
bool result = dir_exists(std::filesystem::path(tempPath));
|
||||
Assert::IsTrue(result);
|
||||
}
|
||||
|
||||
// delete_old_log_folder tests
|
||||
TEST_METHOD(DeleteOldLogFolder_NonExistentFolder_DoesNotCrash)
|
||||
{
|
||||
delete_old_log_folder(std::filesystem::path(L"C:\\NonExistentLogFolder12345"));
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(DeleteOldLogFolder_ValidEmptyFolder_Works)
|
||||
{
|
||||
TestHelpers::TempDirectory tempDir;
|
||||
|
||||
// Create a subfolder structure
|
||||
auto logFolder = std::filesystem::path(tempDir.path()) / L"logs";
|
||||
std::filesystem::create_directories(logFolder);
|
||||
|
||||
Assert::IsTrue(std::filesystem::exists(logFolder));
|
||||
|
||||
delete_old_log_folder(logFolder);
|
||||
|
||||
// Folder may or may not be deleted depending on implementation
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// delete_other_versions_log_folders tests
|
||||
TEST_METHOD(DeleteOtherVersionsLogFolders_NonExistentPath_DoesNotCrash)
|
||||
{
|
||||
delete_other_versions_log_folders(L"C:\\NonExistent\\Path", L"1.0.0");
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(DeleteOtherVersionsLogFolders_EmptyVersion_DoesNotCrash)
|
||||
{
|
||||
wchar_t tempPath[MAX_PATH];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
|
||||
delete_other_versions_log_folders(tempPath, L"");
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(GetLogFolderPath_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount, i]() {
|
||||
auto path = get_log_folder_path(L"TestApp" + std::to_wstring(i));
|
||||
if (!path.empty())
|
||||
{
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(10, successCount.load());
|
||||
}
|
||||
|
||||
TEST_METHOD(DirExists_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
dir_exists(std::filesystem::path(L"C:\\Windows"));
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
|
||||
// Path construction tests
|
||||
TEST_METHOD(GetLogFolderPath_ReturnsValidFilesystemPath)
|
||||
{
|
||||
auto result = get_log_folder_path(L"TestApp");
|
||||
|
||||
// Should be a valid path that we can use with filesystem operations
|
||||
Assert::IsTrue(result.is_absolute() || result.has_root_name() || !result.empty());
|
||||
}
|
||||
};
|
||||
}
|
||||
173
src/common/UnitTests-CommonUtils/ModulesRegistry.Tests.cpp
Normal file
173
src/common/UnitTests-CommonUtils/ModulesRegistry.Tests.cpp
Normal file
@@ -0,0 +1,173 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <modulesRegistry.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
static std::wstring GetInstallDir()
|
||||
{
|
||||
wchar_t path[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
return std::filesystem::path{ path }.parent_path().wstring();
|
||||
}
|
||||
|
||||
TEST_CLASS(ModulesRegistryTests)
|
||||
{
|
||||
public:
|
||||
// Test that all changeset generator functions return valid changesets
|
||||
TEST_METHOD(GetSvgPreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getSvgPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetSvgThumbnailProviderChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getSvgThumbnailHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMarkdownPreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getMdPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMonacoPreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getMonacoPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetPdfPreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getPdfPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetPdfThumbnailProviderChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getPdfThumbnailHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetGcodePreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getGcodePreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetGcodeThumbnailProviderChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getGcodeThumbnailHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetStlThumbnailProviderChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getStlThumbnailHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetQoiPreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getQoiPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetQoiThumbnailProviderChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getQoiThumbnailHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
// Test enabled vs disabled state
|
||||
TEST_METHOD(ChangeSet_EnabledVsDisabled_MayDiffer)
|
||||
{
|
||||
auto enabledSet = getSvgPreviewHandlerChangeSet(GetInstallDir(), true);
|
||||
auto disabledSet = getSvgPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
// Both should be valid change sets
|
||||
Assert::IsFalse(enabledSet.changes.empty());
|
||||
Assert::IsFalse(disabledSet.changes.empty());
|
||||
}
|
||||
|
||||
// Test getAllOnByDefaultModulesChangeSets
|
||||
TEST_METHOD(GetAllOnByDefaultModulesChangeSets_ReturnsMultipleChangeSets)
|
||||
{
|
||||
auto changeSets = getAllOnByDefaultModulesChangeSets(GetInstallDir());
|
||||
|
||||
// Should return multiple changesets for all default-enabled modules
|
||||
Assert::IsTrue(changeSets.size() > 0);
|
||||
}
|
||||
|
||||
// Test getAllModulesChangeSets
|
||||
TEST_METHOD(GetAllModulesChangeSets_ReturnsChangeSets)
|
||||
{
|
||||
auto changeSets = getAllModulesChangeSets(GetInstallDir());
|
||||
|
||||
// Should return changesets for all modules
|
||||
Assert::IsTrue(changeSets.size() > 0);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetAllModulesChangeSets_ContainsMoreThanOnByDefault)
|
||||
{
|
||||
auto allSets = getAllModulesChangeSets(GetInstallDir());
|
||||
auto defaultSets = getAllOnByDefaultModulesChangeSets(GetInstallDir());
|
||||
|
||||
// All modules should be >= on-by-default modules
|
||||
Assert::IsTrue(allSets.size() >= defaultSets.size());
|
||||
}
|
||||
|
||||
// Test that changesets have valid structure
|
||||
TEST_METHOD(ChangeSet_HasValidKeyPath)
|
||||
{
|
||||
auto changeSet = getSvgPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
// Test all changeset functions don't crash
|
||||
TEST_METHOD(AllChangeSetFunctions_DoNotCrash)
|
||||
{
|
||||
auto installDir = GetInstallDir();
|
||||
getSvgPreviewHandlerChangeSet(installDir, true);
|
||||
getSvgPreviewHandlerChangeSet(installDir, false);
|
||||
getSvgThumbnailHandlerChangeSet(installDir, true);
|
||||
getSvgThumbnailHandlerChangeSet(installDir, false);
|
||||
getMdPreviewHandlerChangeSet(installDir, true);
|
||||
getMdPreviewHandlerChangeSet(installDir, false);
|
||||
getMonacoPreviewHandlerChangeSet(installDir, true);
|
||||
getMonacoPreviewHandlerChangeSet(installDir, false);
|
||||
getPdfPreviewHandlerChangeSet(installDir, true);
|
||||
getPdfPreviewHandlerChangeSet(installDir, false);
|
||||
getPdfThumbnailHandlerChangeSet(installDir, true);
|
||||
getPdfThumbnailHandlerChangeSet(installDir, false);
|
||||
getGcodePreviewHandlerChangeSet(installDir, true);
|
||||
getGcodePreviewHandlerChangeSet(installDir, false);
|
||||
getGcodeThumbnailHandlerChangeSet(installDir, true);
|
||||
getGcodeThumbnailHandlerChangeSet(installDir, false);
|
||||
getStlThumbnailHandlerChangeSet(installDir, true);
|
||||
getStlThumbnailHandlerChangeSet(installDir, false);
|
||||
getQoiPreviewHandlerChangeSet(installDir, true);
|
||||
getQoiPreviewHandlerChangeSet(installDir, false);
|
||||
getQoiThumbnailHandlerChangeSet(installDir, true);
|
||||
getQoiThumbnailHandlerChangeSet(installDir, false);
|
||||
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
65
src/common/UnitTests-CommonUtils/MsWindowsSettings.Tests.cpp
Normal file
65
src/common/UnitTests-CommonUtils/MsWindowsSettings.Tests.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <MsWindowsSettings.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(MsWindowsSettingsTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(GetAnimationsEnabled_ReturnsBoolean)
|
||||
{
|
||||
bool result = GetAnimationsEnabled();
|
||||
|
||||
// Should return a valid boolean
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetAnimationsEnabled_ConsistentResults)
|
||||
{
|
||||
// Multiple calls should return consistent results
|
||||
bool result1 = GetAnimationsEnabled();
|
||||
bool result2 = GetAnimationsEnabled();
|
||||
bool result3 = GetAnimationsEnabled();
|
||||
|
||||
Assert::AreEqual(result1, result2);
|
||||
Assert::AreEqual(result2, result3);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetAnimationsEnabled_DoesNotCrash)
|
||||
{
|
||||
// Call multiple times to ensure stability
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
GetAnimationsEnabled();
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetAnimationsEnabled_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
GetAnimationsEnabled();
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
};
|
||||
}
|
||||
146
src/common/UnitTests-CommonUtils/MsiUtils.Tests.cpp
Normal file
146
src/common/UnitTests-CommonUtils/MsiUtils.Tests.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <MsiUtils.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(MsiUtilsTests)
|
||||
{
|
||||
public:
|
||||
// GetMsiPackageInstalledPath tests
|
||||
TEST_METHOD(GetMsiPackageInstalledPath_PerUser_DoesNotCrash)
|
||||
{
|
||||
auto result = GetMsiPackageInstalledPath(true);
|
||||
// Result depends on installation state, but should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMsiPackageInstalledPath_PerMachine_DoesNotCrash)
|
||||
{
|
||||
auto result = GetMsiPackageInstalledPath(false);
|
||||
// Result depends on installation state, but should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMsiPackageInstalledPath_ConsistentResults)
|
||||
{
|
||||
auto result1 = GetMsiPackageInstalledPath(true);
|
||||
auto result2 = GetMsiPackageInstalledPath(true);
|
||||
|
||||
// Results should be consistent
|
||||
Assert::AreEqual(result1.has_value(), result2.has_value());
|
||||
if (result1.has_value() && result2.has_value())
|
||||
{
|
||||
Assert::AreEqual(*result1, *result2);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMsiPackageInstalledPath_PerUserVsPerMachine_MayDiffer)
|
||||
{
|
||||
auto perUser = GetMsiPackageInstalledPath(true);
|
||||
auto perMachine = GetMsiPackageInstalledPath(false);
|
||||
|
||||
// These may or may not be equal depending on installation
|
||||
// Just verify they don't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// GetMsiPackagePath tests
|
||||
TEST_METHOD(GetMsiPackagePath_DoesNotCrash)
|
||||
{
|
||||
auto result = GetMsiPackagePath();
|
||||
// Result depends on installation state, but should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMsiPackagePath_ConsistentResults)
|
||||
{
|
||||
auto result1 = GetMsiPackagePath();
|
||||
auto result2 = GetMsiPackagePath();
|
||||
|
||||
// Results should be consistent
|
||||
Assert::AreEqual(result1, result2);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(GetMsiPackageInstalledPath_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 5; ++j)
|
||||
{
|
||||
GetMsiPackageInstalledPath(j % 2 == 0);
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(25, successCount.load());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMsiPackagePath_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 5; ++j)
|
||||
{
|
||||
GetMsiPackagePath();
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(25, successCount.load());
|
||||
}
|
||||
|
||||
// Return value format tests
|
||||
TEST_METHOD(GetMsiPackageInstalledPath_ReturnsValidPathOrEmpty)
|
||||
{
|
||||
auto path = GetMsiPackageInstalledPath(true);
|
||||
|
||||
if (path.has_value() && !path->empty())
|
||||
{
|
||||
// If a path is returned, it should contain backslash or be a valid path format
|
||||
Assert::IsTrue(path->find(L'\\') != std::wstring::npos ||
|
||||
path->find(L'/') != std::wstring::npos ||
|
||||
path->length() >= 2); // At minimum drive letter + colon
|
||||
}
|
||||
// No value or empty is also valid (not installed)
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMsiPackagePath_ReturnsValidPathOrEmpty)
|
||||
{
|
||||
auto path = GetMsiPackagePath();
|
||||
|
||||
if (!path.empty())
|
||||
{
|
||||
// If a path is returned, it should be a valid path format
|
||||
Assert::IsTrue(path.find(L'\\') != std::wstring::npos ||
|
||||
path.find(L'/') != std::wstring::npos ||
|
||||
path.length() >= 2);
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
107
src/common/UnitTests-CommonUtils/OsDetect.Tests.cpp
Normal file
107
src/common/UnitTests-CommonUtils/OsDetect.Tests.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <os-detect.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(OsDetectTests)
|
||||
{
|
||||
public:
|
||||
// IsAPIContractVxAvailable tests
|
||||
TEST_METHOD(IsAPIContractV8Available_ReturnsBoolean)
|
||||
{
|
||||
// This test verifies the function runs without crashing
|
||||
// The actual result depends on the OS version
|
||||
bool result = IsAPIContractV8Available();
|
||||
// Result is either true or false, both are valid
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsAPIContractVxAvailable_V1_ReturnsTrue)
|
||||
{
|
||||
// API contract v1 should be available on any modern Windows
|
||||
bool result = IsAPIContractVxAvailable<1>();
|
||||
Assert::IsTrue(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsAPIContractVxAvailable_V5_ReturnsBooleanConsistently)
|
||||
{
|
||||
// Call multiple times to verify caching works correctly
|
||||
bool result1 = IsAPIContractVxAvailable<5>();
|
||||
bool result2 = IsAPIContractVxAvailable<5>();
|
||||
bool result3 = IsAPIContractVxAvailable<5>();
|
||||
Assert::AreEqual(result1, result2);
|
||||
Assert::AreEqual(result2, result3);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsAPIContractVxAvailable_V10_ReturnsBoolean)
|
||||
{
|
||||
bool result = IsAPIContractVxAvailable<10>();
|
||||
// Result depends on Windows version, but should not crash
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsAPIContractVxAvailable_V15_ReturnsBoolean)
|
||||
{
|
||||
bool result = IsAPIContractVxAvailable<15>();
|
||||
// Higher API versions, may or may not be available
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
// Is19H1OrHigher tests
|
||||
TEST_METHOD(Is19H1OrHigher_ReturnsBoolean)
|
||||
{
|
||||
bool result = Is19H1OrHigher();
|
||||
// Result depends on OS version, but should not crash
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(Is19H1OrHigher_ReturnsSameAsV8Contract)
|
||||
{
|
||||
// Is19H1OrHigher is implemented as IsAPIContractV8Available
|
||||
bool is19H1 = Is19H1OrHigher();
|
||||
bool isV8 = IsAPIContractV8Available();
|
||||
Assert::AreEqual(is19H1, isV8);
|
||||
}
|
||||
|
||||
TEST_METHOD(Is19H1OrHigher_ConsistentAcrossMultipleCalls)
|
||||
{
|
||||
bool result1 = Is19H1OrHigher();
|
||||
bool result2 = Is19H1OrHigher();
|
||||
bool result3 = Is19H1OrHigher();
|
||||
Assert::AreEqual(result1, result2);
|
||||
Assert::AreEqual(result2, result3);
|
||||
}
|
||||
|
||||
// Static caching behavior tests
|
||||
TEST_METHOD(StaticCaching_DifferentContractVersions_IndependentResults)
|
||||
{
|
||||
// Each template instantiation has its own static variable
|
||||
bool v1 = IsAPIContractVxAvailable<1>();
|
||||
(void)v1; // Suppress unused variable warning
|
||||
|
||||
// v1 should be true on any modern Windows
|
||||
Assert::IsTrue(v1);
|
||||
}
|
||||
|
||||
// Performance test (optional - verifies caching)
|
||||
TEST_METHOD(Performance_MultipleCallsAreFast)
|
||||
{
|
||||
// The static caching should make subsequent calls very fast
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (int i = 0; i < 10000; ++i)
|
||||
{
|
||||
Is19H1OrHigher();
|
||||
}
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
|
||||
// 10000 calls should complete in well under 1 second due to caching
|
||||
Assert::IsTrue(duration.count() < 1000);
|
||||
}
|
||||
};
|
||||
}
|
||||
180
src/common/UnitTests-CommonUtils/Package.Tests.cpp
Normal file
180
src/common/UnitTests-CommonUtils/Package.Tests.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <package.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace package;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(PackageTests)
|
||||
{
|
||||
public:
|
||||
// IsWin11OrGreater tests
|
||||
TEST_METHOD(IsWin11OrGreater_ReturnsBoolean)
|
||||
{
|
||||
bool result = IsWin11OrGreater();
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsWin11OrGreater_ConsistentResults)
|
||||
{
|
||||
bool result1 = IsWin11OrGreater();
|
||||
bool result2 = IsWin11OrGreater();
|
||||
bool result3 = IsWin11OrGreater();
|
||||
|
||||
Assert::AreEqual(result1, result2);
|
||||
Assert::AreEqual(result2, result3);
|
||||
}
|
||||
|
||||
// PACKAGE_VERSION struct tests
|
||||
TEST_METHOD(PackageVersion_DefaultConstruction)
|
||||
{
|
||||
PACKAGE_VERSION version{};
|
||||
Assert::AreEqual(static_cast<UINT16>(0), version.Major);
|
||||
Assert::AreEqual(static_cast<UINT16>(0), version.Minor);
|
||||
Assert::AreEqual(static_cast<UINT16>(0), version.Build);
|
||||
Assert::AreEqual(static_cast<UINT16>(0), version.Revision);
|
||||
}
|
||||
|
||||
TEST_METHOD(PackageVersion_Assignment)
|
||||
{
|
||||
PACKAGE_VERSION version{};
|
||||
version.Major = 1;
|
||||
version.Minor = 2;
|
||||
version.Build = 3;
|
||||
version.Revision = 4;
|
||||
|
||||
Assert::AreEqual(static_cast<UINT16>(1), version.Major);
|
||||
Assert::AreEqual(static_cast<UINT16>(2), version.Minor);
|
||||
Assert::AreEqual(static_cast<UINT16>(3), version.Build);
|
||||
Assert::AreEqual(static_cast<UINT16>(4), version.Revision);
|
||||
}
|
||||
|
||||
// ComInitializer tests
|
||||
TEST_METHOD(ComInitializer_InitializesAndUninitializesCom)
|
||||
{
|
||||
{
|
||||
ComInitializer comInit;
|
||||
// COM should be initialized within this scope
|
||||
}
|
||||
// COM should be uninitialized after scope
|
||||
|
||||
// Verify we can initialize again
|
||||
{
|
||||
ComInitializer comInit2;
|
||||
}
|
||||
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComInitializer_MultipleInstances)
|
||||
{
|
||||
ComInitializer init1;
|
||||
ComInitializer init2;
|
||||
ComInitializer init3;
|
||||
|
||||
// Multiple initializations should work (COM uses reference counting)
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// GetRegisteredPackage tests
|
||||
TEST_METHOD(GetRegisteredPackage_NonExistentPackage_ReturnsEmpty)
|
||||
{
|
||||
auto result = GetRegisteredPackage(L"NonExistentPackage12345", false);
|
||||
|
||||
// Should return empty for non-existent package
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetRegisteredPackage_EmptyName_DoesNotCrash)
|
||||
{
|
||||
auto result = GetRegisteredPackage(L"", false);
|
||||
// Behavior may vary based on package enumeration; just ensure it doesn't crash.
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// IsPackageRegisteredWithPowerToysVersion tests
|
||||
TEST_METHOD(IsPackageRegisteredWithPowerToysVersion_NonExistentPackage_ReturnsFalse)
|
||||
{
|
||||
bool result = IsPackageRegisteredWithPowerToysVersion(L"NonExistentPackage12345");
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsPackageRegisteredWithPowerToysVersion_EmptyName_ReturnsFalse)
|
||||
{
|
||||
bool result = IsPackageRegisteredWithPowerToysVersion(L"");
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
// FindMsixFile tests
|
||||
TEST_METHOD(FindMsixFile_NonExistentDirectory_ReturnsEmpty)
|
||||
{
|
||||
auto result = FindMsixFile(L"C:\\NonExistentDirectory12345", false);
|
||||
Assert::IsTrue(result.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(FindMsixFile_SystemDirectory_DoesNotCrash)
|
||||
{
|
||||
// System32 probably doesn't have MSIX files, but shouldn't crash
|
||||
auto result = FindMsixFile(L"C:\\Windows\\System32", false);
|
||||
// May or may not find files, but should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(FindMsixFile_RecursiveSearch_DoesNotCrash)
|
||||
{
|
||||
// Use temp directory which should exist
|
||||
wchar_t tempPath[MAX_PATH];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
|
||||
auto result = FindMsixFile(tempPath, true);
|
||||
// May or may not find files, but should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// GetPackageNameAndVersionFromAppx tests
|
||||
TEST_METHOD(GetPackageNameAndVersionFromAppx_NonExistentFile_ReturnsFalse)
|
||||
{
|
||||
std::wstring name;
|
||||
PACKAGE_VERSION version{};
|
||||
|
||||
bool result = GetPackageNameAndVersionFromAppx(L"C:\\NonExistent\\file.msix", name, version);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetPackageNameAndVersionFromAppx_EmptyPath_ReturnsFalse)
|
||||
{
|
||||
std::wstring name;
|
||||
PACKAGE_VERSION version{};
|
||||
|
||||
bool result = GetPackageNameAndVersionFromAppx(L"", name, version);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
// Thread safety
|
||||
TEST_METHOD(IsWin11OrGreater_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
IsWin11OrGreater();
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
};
|
||||
}
|
||||
136
src/common/UnitTests-CommonUtils/ProcessApi.Tests.cpp
Normal file
136
src/common/UnitTests-CommonUtils/ProcessApi.Tests.cpp
Normal file
@@ -0,0 +1,136 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <processApi.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ProcessApiTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(GetProcessHandlesByName_CurrentProcess_ReturnsHandles)
|
||||
{
|
||||
// Get current process executable name
|
||||
wchar_t path[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
|
||||
// Extract just the filename
|
||||
std::wstring fullPath(path);
|
||||
auto lastSlash = fullPath.rfind(L'\\');
|
||||
std::wstring exeName = (lastSlash != std::wstring::npos) ?
|
||||
fullPath.substr(lastSlash + 1) : fullPath;
|
||||
|
||||
auto handles = getProcessHandlesByName(exeName, PROCESS_QUERY_LIMITED_INFORMATION);
|
||||
|
||||
// Should find at least our own process
|
||||
Assert::IsFalse(handles.empty());
|
||||
|
||||
// Handles are RAII-managed
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessHandlesByName_NonExistentProcess_ReturnsEmpty)
|
||||
{
|
||||
auto handles = getProcessHandlesByName(L"NonExistentProcess12345.exe", PROCESS_QUERY_LIMITED_INFORMATION);
|
||||
Assert::IsTrue(handles.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessHandlesByName_EmptyName_ReturnsEmpty)
|
||||
{
|
||||
auto handles = getProcessHandlesByName(L"", PROCESS_QUERY_LIMITED_INFORMATION);
|
||||
Assert::IsTrue(handles.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessHandlesByName_Explorer_ReturnsHandles)
|
||||
{
|
||||
// Explorer.exe should typically be running
|
||||
auto handles = getProcessHandlesByName(L"explorer.exe", PROCESS_QUERY_LIMITED_INFORMATION);
|
||||
|
||||
// Handles are RAII-managed
|
||||
|
||||
// May or may not find explorer depending on system state
|
||||
// Just verify it doesn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessHandlesByName_CaseInsensitive_Works)
|
||||
{
|
||||
// Get current process name in uppercase
|
||||
wchar_t path[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
|
||||
std::wstring fullPath(path);
|
||||
auto lastSlash = fullPath.rfind(L'\\');
|
||||
std::wstring exeName = (lastSlash != std::wstring::npos) ?
|
||||
fullPath.substr(lastSlash + 1) : fullPath;
|
||||
|
||||
// Convert to uppercase
|
||||
std::wstring upperName = exeName;
|
||||
std::transform(upperName.begin(), upperName.end(), upperName.begin(), ::towupper);
|
||||
|
||||
auto handles = getProcessHandlesByName(upperName, PROCESS_QUERY_LIMITED_INFORMATION);
|
||||
|
||||
// Handles are RAII-managed
|
||||
|
||||
// The function may or may not be case insensitive - just don't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessHandlesByName_DifferentAccessRights_Works)
|
||||
{
|
||||
wchar_t path[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
|
||||
std::wstring fullPath(path);
|
||||
auto lastSlash = fullPath.rfind(L'\\');
|
||||
std::wstring exeName = (lastSlash != std::wstring::npos) ?
|
||||
fullPath.substr(lastSlash + 1) : fullPath;
|
||||
|
||||
// Try with different access rights
|
||||
auto handles1 = getProcessHandlesByName(exeName, PROCESS_QUERY_INFORMATION);
|
||||
auto handles2 = getProcessHandlesByName(exeName, PROCESS_VM_READ);
|
||||
|
||||
// Handles are RAII-managed
|
||||
|
||||
// Just verify no crashes
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessHandlesByName_SystemProcess_MayRequireElevation)
|
||||
{
|
||||
// System processes might require elevation
|
||||
auto handles = getProcessHandlesByName(L"System", PROCESS_QUERY_LIMITED_INFORMATION);
|
||||
|
||||
// Handles are RAII-managed
|
||||
|
||||
// Just verify no crashes
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessHandlesByName_ValidHandles_AreUsable)
|
||||
{
|
||||
wchar_t path[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
|
||||
std::wstring fullPath(path);
|
||||
auto lastSlash = fullPath.rfind(L'\\');
|
||||
std::wstring exeName = (lastSlash != std::wstring::npos) ?
|
||||
fullPath.substr(lastSlash + 1) : fullPath;
|
||||
|
||||
auto handles = getProcessHandlesByName(exeName, PROCESS_QUERY_LIMITED_INFORMATION);
|
||||
|
||||
bool foundValidHandle = false;
|
||||
for (auto& handle : handles)
|
||||
{
|
||||
// Try to use the handle
|
||||
DWORD exitCode;
|
||||
if (GetExitCodeProcess(handle.get(), &exitCode))
|
||||
{
|
||||
foundValidHandle = true;
|
||||
}
|
||||
}
|
||||
|
||||
Assert::IsTrue(foundValidHandle || handles.empty());
|
||||
}
|
||||
};
|
||||
}
|
||||
153
src/common/UnitTests-CommonUtils/ProcessPath.Tests.cpp
Normal file
153
src/common/UnitTests-CommonUtils/ProcessPath.Tests.cpp
Normal file
@@ -0,0 +1,153 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <process_path.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ProcessPathTests)
|
||||
{
|
||||
public:
|
||||
// get_process_path (by PID) tests
|
||||
TEST_METHOD(GetProcessPath_CurrentProcess_ReturnsPath)
|
||||
{
|
||||
DWORD pid = GetCurrentProcessId();
|
||||
auto path = get_process_path(pid);
|
||||
|
||||
Assert::IsFalse(path.empty());
|
||||
Assert::IsTrue(path.find(L".exe") != std::wstring::npos ||
|
||||
path.find(L".dll") != std::wstring::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessPath_InvalidPid_ReturnsEmpty)
|
||||
{
|
||||
DWORD invalidPid = 0xFFFFFFFF;
|
||||
auto path = get_process_path(invalidPid);
|
||||
|
||||
// Should return empty for invalid PID
|
||||
Assert::IsTrue(path.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessPath_ZeroPid_ReturnsEmpty)
|
||||
{
|
||||
auto path = get_process_path(static_cast<DWORD>(0));
|
||||
// PID 0 is the System Idle Process, might return empty or a path
|
||||
// Just verify it doesn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessPath_SystemPid_DoesNotCrash)
|
||||
{
|
||||
// PID 4 is typically the System process
|
||||
auto path = get_process_path(static_cast<DWORD>(4));
|
||||
// May return empty due to access rights, but shouldn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// get_module_filename tests
|
||||
TEST_METHOD(GetModuleFilename_NullModule_ReturnsExePath)
|
||||
{
|
||||
auto path = get_module_filename(nullptr);
|
||||
|
||||
Assert::IsFalse(path.empty());
|
||||
Assert::IsTrue(path.find(L".exe") != std::wstring::npos ||
|
||||
path.find(L".dll") != std::wstring::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetModuleFilename_Kernel32_ReturnsPath)
|
||||
{
|
||||
HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");
|
||||
Assert::IsNotNull(kernel32);
|
||||
|
||||
auto path = get_module_filename(kernel32);
|
||||
|
||||
Assert::IsFalse(path.empty());
|
||||
// Should contain kernel32 (case insensitive check)
|
||||
std::wstring lowerPath = path;
|
||||
std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(), ::towlower);
|
||||
Assert::IsTrue(lowerPath.find(L"kernel32") != std::wstring::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetModuleFilename_InvalidModule_ReturnsEmpty)
|
||||
{
|
||||
auto path = get_module_filename(reinterpret_cast<HMODULE>(0x12345678));
|
||||
// Invalid module should return empty
|
||||
Assert::IsTrue(path.empty());
|
||||
}
|
||||
|
||||
// get_module_folderpath tests
|
||||
TEST_METHOD(GetModuleFolderpath_NullModule_ReturnsFolder)
|
||||
{
|
||||
auto folder = get_module_folderpath(nullptr, true);
|
||||
|
||||
Assert::IsFalse(folder.empty());
|
||||
// Should not end with .exe when removeFilename is true
|
||||
Assert::IsTrue(folder.find(L".exe") == std::wstring::npos);
|
||||
// Should end with backslash or be a valid folder path
|
||||
Assert::IsTrue(folder.back() == L'\\' || folder.find(L"\\") != std::wstring::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetModuleFolderpath_KeepFilename_ReturnsFullPath)
|
||||
{
|
||||
auto fullPath = get_module_folderpath(nullptr, false);
|
||||
|
||||
Assert::IsFalse(fullPath.empty());
|
||||
// Should contain .exe or .dll when not removing filename
|
||||
Assert::IsTrue(fullPath.find(L".exe") != std::wstring::npos ||
|
||||
fullPath.find(L".dll") != std::wstring::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetModuleFolderpath_Kernel32_ReturnsSystem32)
|
||||
{
|
||||
HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");
|
||||
Assert::IsNotNull(kernel32);
|
||||
|
||||
auto folder = get_module_folderpath(kernel32, true);
|
||||
|
||||
Assert::IsFalse(folder.empty());
|
||||
// Should be in system32 folder
|
||||
std::wstring lowerPath = folder;
|
||||
std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(), ::towlower);
|
||||
Assert::IsTrue(lowerPath.find(L"system32") != std::wstring::npos ||
|
||||
lowerPath.find(L"syswow64") != std::wstring::npos);
|
||||
}
|
||||
|
||||
// get_process_path (by HWND) tests
|
||||
TEST_METHOD(GetProcessPath_DesktopWindow_ReturnsPath)
|
||||
{
|
||||
HWND desktop = GetDesktopWindow();
|
||||
Assert::IsNotNull(desktop);
|
||||
|
||||
auto path = get_process_path(desktop);
|
||||
// Desktop window should return a path
|
||||
// (could be explorer.exe or empty depending on system)
|
||||
Assert::IsTrue(true); // Just verify it doesn't crash
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessPath_InvalidHwnd_ReturnsEmpty)
|
||||
{
|
||||
auto path = get_process_path(reinterpret_cast<HWND>(0x12345678));
|
||||
Assert::IsTrue(path.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetProcessPath_NullHwnd_ReturnsEmpty)
|
||||
{
|
||||
auto path = get_process_path(static_cast<HWND>(nullptr));
|
||||
Assert::IsTrue(path.empty());
|
||||
}
|
||||
|
||||
// Consistency tests
|
||||
TEST_METHOD(Consistency_ModuleFilenameAndFolderpath_AreRelated)
|
||||
{
|
||||
auto fullPath = get_module_filename(nullptr);
|
||||
auto folder = get_module_folderpath(nullptr, true);
|
||||
|
||||
Assert::IsFalse(fullPath.empty());
|
||||
Assert::IsFalse(folder.empty());
|
||||
|
||||
// Full path should start with the folder
|
||||
Assert::IsTrue(fullPath.find(folder) == 0 || folder.find(fullPath.substr(0, folder.length())) == 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
127
src/common/UnitTests-CommonUtils/ProcessWaiter.Tests.cpp
Normal file
127
src/common/UnitTests-CommonUtils/ProcessWaiter.Tests.cpp
Normal file
@@ -0,0 +1,127 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <ProcessWaiter.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace ProcessWaiter;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ProcessWaiterTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(OnProcessTerminate_InvalidPid_DoesNotCrash)
|
||||
{
|
||||
std::atomic<bool> called{ false };
|
||||
|
||||
// Use a very unlikely PID (negative value as string will fail conversion)
|
||||
OnProcessTerminate(L"invalid", [&called](DWORD) {
|
||||
called = true;
|
||||
});
|
||||
|
||||
// Wait briefly
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
// Should not crash, callback may or may not be called depending on implementation
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnProcessTerminate_NonExistentPid_DoesNotCrash)
|
||||
{
|
||||
std::atomic<bool> called{ false };
|
||||
|
||||
// Use a PID that likely doesn't exist
|
||||
OnProcessTerminate(L"999999999", [&called](DWORD) {
|
||||
called = true;
|
||||
});
|
||||
|
||||
// Wait briefly
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
// Should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnProcessTerminate_ZeroPid_DoesNotCrash)
|
||||
{
|
||||
std::atomic<bool> called{ false };
|
||||
|
||||
OnProcessTerminate(L"0", [&called](DWORD) {
|
||||
called = true;
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnProcessTerminate_CurrentProcessPid_DoesNotTerminate)
|
||||
{
|
||||
std::atomic<bool> called{ false };
|
||||
|
||||
// Use current process PID - it shouldn't terminate during test
|
||||
std::wstring pid = std::to_wstring(GetCurrentProcessId());
|
||||
|
||||
OnProcessTerminate(pid, [&called](DWORD) {
|
||||
called = true;
|
||||
});
|
||||
|
||||
// Wait briefly - current process should not terminate
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
// Callback should not have been called since process is still running
|
||||
Assert::IsFalse(called);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnProcessTerminate_EmptyCallback_DoesNotCrash)
|
||||
{
|
||||
// Test with an empty function
|
||||
OnProcessTerminate(L"999999999", std::function<void(DWORD)>());
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnProcessTerminate_MultipleCallsForSamePid_DoesNotCrash)
|
||||
{
|
||||
std::atomic<int> counter{ 0 };
|
||||
std::wstring pid = std::to_wstring(GetCurrentProcessId());
|
||||
|
||||
// Multiple waits on same (running) process
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
OnProcessTerminate(pid, [&counter](DWORD) {
|
||||
counter++;
|
||||
});
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
// None should have been called since process is running
|
||||
Assert::AreEqual(0, counter.load());
|
||||
}
|
||||
|
||||
TEST_METHOD(OnProcessTerminate_NegativeNumberString_DoesNotCrash)
|
||||
{
|
||||
std::atomic<bool> called{ false };
|
||||
|
||||
OnProcessTerminate(L"-1", [&called](DWORD) {
|
||||
called = true;
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnProcessTerminate_LargeNumber_DoesNotCrash)
|
||||
{
|
||||
std::atomic<bool> called{ false };
|
||||
|
||||
OnProcessTerminate(L"18446744073709551615", [&called](DWORD) {
|
||||
called = true;
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
61
src/common/UnitTests-CommonUtils/Registry.Tests.cpp
Normal file
61
src/common/UnitTests-CommonUtils/Registry.Tests.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <registry.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(RegistryTests)
|
||||
{
|
||||
public:
|
||||
// Note: These tests use HKCU which doesn't require elevation
|
||||
|
||||
TEST_METHOD(InstallScope_Registry_CanReadAndWrite)
|
||||
{
|
||||
TestHelpers::TestRegistryKey testKey(L"RegistryTest");
|
||||
Assert::IsTrue(testKey.isValid());
|
||||
|
||||
// Write a test value
|
||||
Assert::IsTrue(testKey.setStringValue(L"TestValue", L"TestData"));
|
||||
Assert::IsTrue(testKey.setDwordValue(L"TestDword", 42));
|
||||
}
|
||||
|
||||
TEST_METHOD(Registry_ValueChange_StringValue)
|
||||
{
|
||||
registry::ValueChange change{ HKEY_CURRENT_USER, L"Software\\PowerToys\\Test", L"TestValue", std::wstring{ L"TestData" } };
|
||||
|
||||
Assert::AreEqual(std::wstring(L"Software\\PowerToys\\Test"), change.path);
|
||||
Assert::IsTrue(change.name.has_value());
|
||||
Assert::AreEqual(std::wstring(L"TestValue"), *change.name);
|
||||
Assert::AreEqual(std::wstring(L"TestData"), std::get<std::wstring>(change.value));
|
||||
}
|
||||
|
||||
TEST_METHOD(Registry_ValueChange_DwordValue)
|
||||
{
|
||||
registry::ValueChange change{ HKEY_CURRENT_USER, L"Software\\PowerToys\\Test", L"TestDword", static_cast<DWORD>(42) };
|
||||
|
||||
Assert::AreEqual(std::wstring(L"Software\\PowerToys\\Test"), change.path);
|
||||
Assert::IsTrue(change.name.has_value());
|
||||
Assert::AreEqual(std::wstring(L"TestDword"), *change.name);
|
||||
Assert::AreEqual(static_cast<DWORD>(42), std::get<DWORD>(change.value));
|
||||
}
|
||||
|
||||
TEST_METHOD(Registry_ChangeSet_AddChanges)
|
||||
{
|
||||
registry::ChangeSet changeSet;
|
||||
|
||||
changeSet.changes.push_back({ HKEY_CURRENT_USER, L"Software\\PowerToys\\Test", L"Value1", std::wstring{ L"Data1" } });
|
||||
changeSet.changes.push_back({ HKEY_CURRENT_USER, L"Software\\PowerToys\\Test", L"Value2", static_cast<DWORD>(123) });
|
||||
|
||||
Assert::AreEqual(static_cast<size_t>(2), changeSet.changes.size());
|
||||
}
|
||||
|
||||
TEST_METHOD(InstallScope_GetCurrentInstallScope_ReturnsValidValue)
|
||||
{
|
||||
auto scope = registry::install_scope::get_current_install_scope();
|
||||
Assert::IsTrue(scope == registry::install_scope::InstallScope::PerMachine ||
|
||||
scope == registry::install_scope::InstallScope::PerUser);
|
||||
}
|
||||
};
|
||||
}
|
||||
144
src/common/UnitTests-CommonUtils/Resources.Tests.cpp
Normal file
144
src/common/UnitTests-CommonUtils/Resources.Tests.cpp
Normal file
@@ -0,0 +1,144 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <resources.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ResourcesTests)
|
||||
{
|
||||
public:
|
||||
// get_resource_string tests with current module
|
||||
TEST_METHOD(GetResourceString_NonExistentId_ReturnsFallback)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
|
||||
auto result = get_resource_string(99999, instance, L"fallback");
|
||||
Assert::AreEqual(std::wstring(L"fallback"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetResourceString_NullInstance_UsesFallback)
|
||||
{
|
||||
auto result = get_resource_string(99999, nullptr, L"fallback");
|
||||
// Should return fallback or empty string
|
||||
Assert::IsTrue(result == L"fallback" || result.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetResourceString_EmptyFallback_ReturnsEmpty)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
|
||||
auto result = get_resource_string(99999, instance, L"");
|
||||
Assert::IsTrue(result.empty());
|
||||
}
|
||||
|
||||
// get_english_fallback_string tests
|
||||
TEST_METHOD(GetEnglishFallbackString_NonExistentId_ReturnsEmpty)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
|
||||
auto result = get_english_fallback_string(99999, instance);
|
||||
// Should return empty or the resource if it exists
|
||||
Assert::IsTrue(true); // Just verify no crash
|
||||
}
|
||||
|
||||
TEST_METHOD(GetEnglishFallbackString_NullInstance_DoesNotCrash)
|
||||
{
|
||||
auto result = get_english_fallback_string(99999, nullptr);
|
||||
Assert::IsTrue(true); // Just verify no crash
|
||||
}
|
||||
|
||||
// get_resource_string_language_override tests
|
||||
TEST_METHOD(GetResourceStringLanguageOverride_NonExistentId_ReturnsEmpty)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
|
||||
auto result = get_resource_string_language_override(99999, instance);
|
||||
// Should return empty for non-existent resource
|
||||
Assert::IsTrue(result.empty() || !result.empty()); // Valid either way
|
||||
}
|
||||
|
||||
TEST_METHOD(GetResourceStringLanguageOverride_NullInstance_DoesNotCrash)
|
||||
{
|
||||
auto result = get_resource_string_language_override(99999, nullptr);
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(GetResourceString_ThreadSafe)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount, instance]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
get_resource_string(99999, instance, L"fallback");
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
|
||||
// Kernel32 resource tests (has known resources)
|
||||
TEST_METHOD(GetResourceString_Kernel32_DoesNotCrash)
|
||||
{
|
||||
HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");
|
||||
if (kernel32)
|
||||
{
|
||||
// Kernel32 has resources, but we don't know exact IDs
|
||||
// Just verify it doesn't crash
|
||||
get_resource_string(1, kernel32, L"fallback");
|
||||
get_resource_string(100, kernel32, L"fallback");
|
||||
get_resource_string(1000, kernel32, L"fallback");
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// Performance test
|
||||
TEST_METHOD(GetResourceString_Performance_Acceptable)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
{
|
||||
get_resource_string(99999, instance, L"fallback");
|
||||
}
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
|
||||
// 1000 lookups should complete in under 1 second
|
||||
Assert::IsTrue(duration.count() < 1000);
|
||||
}
|
||||
|
||||
// Edge case tests
|
||||
TEST_METHOD(GetResourceString_ZeroId_DoesNotCrash)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
auto result = get_resource_string(0, instance, L"fallback");
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetResourceString_MaxUintId_DoesNotCrash)
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
auto result = get_resource_string(UINT_MAX, instance, L"fallback");
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
286
src/common/UnitTests-CommonUtils/Serialized.Tests.cpp
Normal file
286
src/common/UnitTests-CommonUtils/Serialized.Tests.cpp
Normal file
@@ -0,0 +1,286 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <serialized.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(SerializedTests)
|
||||
{
|
||||
public:
|
||||
// Basic Read tests
|
||||
TEST_METHOD(Read_DefaultState_ReturnsDefaultValue)
|
||||
{
|
||||
Serialized<int> s;
|
||||
int value = -1;
|
||||
s.Read([&value](const int& v) {
|
||||
value = v;
|
||||
});
|
||||
Assert::AreEqual(0, value); // Default constructed int is 0
|
||||
}
|
||||
|
||||
TEST_METHOD(Read_StringType_ReturnsEmpty)
|
||||
{
|
||||
Serialized<std::string> s;
|
||||
std::string value = "initial";
|
||||
s.Read([&value](const std::string& v) {
|
||||
value = v;
|
||||
});
|
||||
Assert::AreEqual(std::string(""), value);
|
||||
}
|
||||
|
||||
// Basic Access tests
|
||||
TEST_METHOD(Access_ModifyValue_ValueIsModified)
|
||||
{
|
||||
Serialized<int> s;
|
||||
s.Access([](int& v) {
|
||||
v = 42;
|
||||
});
|
||||
|
||||
int value = 0;
|
||||
s.Read([&value](const int& v) {
|
||||
value = v;
|
||||
});
|
||||
Assert::AreEqual(42, value);
|
||||
}
|
||||
|
||||
TEST_METHOD(Access_ModifyString_StringIsModified)
|
||||
{
|
||||
Serialized<std::string> s;
|
||||
s.Access([](std::string& v) {
|
||||
v = "hello";
|
||||
});
|
||||
|
||||
std::string value;
|
||||
s.Read([&value](const std::string& v) {
|
||||
value = v;
|
||||
});
|
||||
Assert::AreEqual(std::string("hello"), value);
|
||||
}
|
||||
|
||||
TEST_METHOD(Access_MultipleModifications_LastValuePersists)
|
||||
{
|
||||
Serialized<int> s;
|
||||
s.Access([](int& v) { v = 1; });
|
||||
s.Access([](int& v) { v = 2; });
|
||||
s.Access([](int& v) { v = 3; });
|
||||
|
||||
int value = 0;
|
||||
s.Read([&value](const int& v) {
|
||||
value = v;
|
||||
});
|
||||
Assert::AreEqual(3, value);
|
||||
}
|
||||
|
||||
// Reset tests
|
||||
TEST_METHOD(Reset_AfterModification_ReturnsDefault)
|
||||
{
|
||||
Serialized<int> s;
|
||||
s.Access([](int& v) { v = 42; });
|
||||
s.Reset();
|
||||
|
||||
int value = -1;
|
||||
s.Read([&value](const int& v) {
|
||||
value = v;
|
||||
});
|
||||
Assert::AreEqual(0, value);
|
||||
}
|
||||
|
||||
TEST_METHOD(Reset_String_ReturnsEmpty)
|
||||
{
|
||||
Serialized<std::string> s;
|
||||
s.Access([](std::string& v) { v = "hello"; });
|
||||
s.Reset();
|
||||
|
||||
std::string value = "initial";
|
||||
s.Read([&value](const std::string& v) {
|
||||
value = v;
|
||||
});
|
||||
Assert::AreEqual(std::string(""), value);
|
||||
}
|
||||
|
||||
// Complex type tests
|
||||
TEST_METHOD(Serialized_VectorType_Works)
|
||||
{
|
||||
Serialized<std::vector<int>> s;
|
||||
s.Access([](std::vector<int>& v) {
|
||||
v.push_back(1);
|
||||
v.push_back(2);
|
||||
v.push_back(3);
|
||||
});
|
||||
|
||||
size_t size = 0;
|
||||
int sum = 0;
|
||||
s.Read([&size, &sum](const std::vector<int>& v) {
|
||||
size = v.size();
|
||||
for (int i : v) sum += i;
|
||||
});
|
||||
|
||||
Assert::AreEqual(static_cast<size_t>(3), size);
|
||||
Assert::AreEqual(6, sum);
|
||||
}
|
||||
|
||||
TEST_METHOD(Serialized_MapType_Works)
|
||||
{
|
||||
Serialized<std::map<std::string, int>> s;
|
||||
s.Access([](std::map<std::string, int>& v) {
|
||||
v["one"] = 1;
|
||||
v["two"] = 2;
|
||||
});
|
||||
|
||||
int value = 0;
|
||||
s.Read([&value](const std::map<std::string, int>& v) {
|
||||
auto it = v.find("two");
|
||||
if (it != v.end()) {
|
||||
value = it->second;
|
||||
}
|
||||
});
|
||||
|
||||
Assert::AreEqual(2, value);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(ThreadSafety_ConcurrentReads_NoDataRace)
|
||||
{
|
||||
Serialized<int> s;
|
||||
s.Access([](int& v) { v = 42; });
|
||||
|
||||
std::atomic<int> readCount{ 0 };
|
||||
std::vector<std::thread> threads;
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&s, &readCount]() {
|
||||
for (int j = 0; j < 100; ++j)
|
||||
{
|
||||
s.Read([&readCount](const int& v) {
|
||||
if (v == 42) {
|
||||
readCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(1000, readCount.load());
|
||||
}
|
||||
|
||||
TEST_METHOD(ThreadSafety_ConcurrentAccessAndRead_NoDataRace)
|
||||
{
|
||||
Serialized<int> s;
|
||||
std::atomic<bool> done{ false };
|
||||
std::atomic<int> accessCount{ 0 };
|
||||
std::atomic<int> readersReady{ 0 };
|
||||
std::atomic<bool> start{ false };
|
||||
|
||||
// Writer thread
|
||||
std::thread writer([&s, &done, &accessCount, &readersReady, &start]() {
|
||||
while (readersReady.load() < 5)
|
||||
{
|
||||
std::this_thread::yield();
|
||||
}
|
||||
start = true;
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
s.Access([i](int& v) {
|
||||
v = i;
|
||||
});
|
||||
accessCount++;
|
||||
}
|
||||
done = true;
|
||||
});
|
||||
|
||||
// Reader threads
|
||||
std::vector<std::thread> readers;
|
||||
std::atomic<int> readAttempts{ 0 };
|
||||
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
readers.emplace_back([&s, &done, &readAttempts, &readersReady, &start]() {
|
||||
readersReady++;
|
||||
while (!start)
|
||||
{
|
||||
std::this_thread::yield();
|
||||
}
|
||||
while (!done)
|
||||
{
|
||||
s.Read([](const int& v) {
|
||||
// Just read the value
|
||||
(void)v;
|
||||
});
|
||||
readAttempts++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
writer.join();
|
||||
for (auto& t : readers)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
// Verify all access calls completed
|
||||
Assert::AreEqual(100, accessCount.load());
|
||||
// Verify reads happened
|
||||
Assert::IsTrue(readAttempts > 0);
|
||||
}
|
||||
|
||||
// Struct type test
|
||||
TEST_METHOD(Serialized_StructType_Works)
|
||||
{
|
||||
struct TestStruct
|
||||
{
|
||||
int x = 0;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
Serialized<TestStruct> s;
|
||||
s.Access([](TestStruct& v) {
|
||||
v.x = 10;
|
||||
v.name = "test";
|
||||
});
|
||||
|
||||
int x = 0;
|
||||
std::string name;
|
||||
s.Read([&x, &name](const TestStruct& v) {
|
||||
x = v.x;
|
||||
name = v.name;
|
||||
});
|
||||
|
||||
Assert::AreEqual(10, x);
|
||||
Assert::AreEqual(std::string("test"), name);
|
||||
}
|
||||
|
||||
TEST_METHOD(Reset_StructType_ResetsToDefault)
|
||||
{
|
||||
struct TestStruct
|
||||
{
|
||||
int x = 0;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
Serialized<TestStruct> s;
|
||||
s.Access([](TestStruct& v) {
|
||||
v.x = 10;
|
||||
v.name = "test";
|
||||
});
|
||||
s.Reset();
|
||||
|
||||
int x = -1;
|
||||
std::string name = "not empty";
|
||||
s.Read([&x, &name](const TestStruct& v) {
|
||||
x = v.x;
|
||||
name = v.name;
|
||||
});
|
||||
|
||||
Assert::AreEqual(0, x);
|
||||
Assert::AreEqual(std::string(""), name);
|
||||
}
|
||||
};
|
||||
}
|
||||
283
src/common/UnitTests-CommonUtils/StringUtils.Tests.cpp
Normal file
283
src/common/UnitTests-CommonUtils/StringUtils.Tests.cpp
Normal file
@@ -0,0 +1,283 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <string_utils.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(StringUtilsTests)
|
||||
{
|
||||
public:
|
||||
// left_trim tests
|
||||
TEST_METHOD(LeftTrim_EmptyString_ReturnsEmpty)
|
||||
{
|
||||
std::string_view input = "";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view(""), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_NoWhitespace_ReturnsOriginal)
|
||||
{
|
||||
std::string_view input = "hello";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_LeadingSpaces_TrimsSpaces)
|
||||
{
|
||||
std::string_view input = " hello";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_LeadingTabs_TrimsTabs)
|
||||
{
|
||||
std::string_view input = "\t\thello";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_LeadingNewlines_TrimsNewlines)
|
||||
{
|
||||
std::string_view input = "\r\n\nhello";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_MixedWhitespace_TrimsAll)
|
||||
{
|
||||
std::string_view input = " \t\r\nhello";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_TrailingWhitespace_PreservesTrailing)
|
||||
{
|
||||
std::string_view input = " hello ";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello "), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_OnlyWhitespace_ReturnsEmpty)
|
||||
{
|
||||
std::string_view input = " \t\r\n";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::string_view(""), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_CustomChars_TrimsSpecified)
|
||||
{
|
||||
std::string_view input = "xxxhello";
|
||||
auto result = left_trim(input, std::string_view("x"));
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LeftTrim_WideString_Works)
|
||||
{
|
||||
std::wstring_view input = L" hello";
|
||||
auto result = left_trim(input);
|
||||
Assert::AreEqual(std::wstring_view(L"hello"), result);
|
||||
}
|
||||
|
||||
// right_trim tests
|
||||
TEST_METHOD(RightTrim_EmptyString_ReturnsEmpty)
|
||||
{
|
||||
std::string_view input = "";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::string_view(""), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_NoWhitespace_ReturnsOriginal)
|
||||
{
|
||||
std::string_view input = "hello";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_TrailingSpaces_TrimsSpaces)
|
||||
{
|
||||
std::string_view input = "hello ";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_TrailingTabs_TrimsTabs)
|
||||
{
|
||||
std::string_view input = "hello\t\t";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_TrailingNewlines_TrimsNewlines)
|
||||
{
|
||||
std::string_view input = "hello\r\n\n";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_LeadingWhitespace_PreservesLeading)
|
||||
{
|
||||
std::string_view input = " hello ";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::string_view(" hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_OnlyWhitespace_ReturnsEmpty)
|
||||
{
|
||||
std::string_view input = " \t\r\n";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::string_view(""), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_CustomChars_TrimsSpecified)
|
||||
{
|
||||
std::string_view input = "helloxxx";
|
||||
auto result = right_trim(input, std::string_view("x"));
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(RightTrim_WideString_Works)
|
||||
{
|
||||
std::wstring_view input = L"hello ";
|
||||
auto result = right_trim(input);
|
||||
Assert::AreEqual(std::wstring_view(L"hello"), result);
|
||||
}
|
||||
|
||||
// trim tests
|
||||
TEST_METHOD(Trim_EmptyString_ReturnsEmpty)
|
||||
{
|
||||
std::string_view input = "";
|
||||
auto result = trim(input);
|
||||
Assert::AreEqual(std::string_view(""), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Trim_NoWhitespace_ReturnsOriginal)
|
||||
{
|
||||
std::string_view input = "hello";
|
||||
auto result = trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Trim_BothSides_TrimsBoth)
|
||||
{
|
||||
std::string_view input = " hello ";
|
||||
auto result = trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Trim_MixedWhitespace_TrimsAll)
|
||||
{
|
||||
std::string_view input = " \t\r\nhello \t\r\n";
|
||||
auto result = trim(input);
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Trim_InternalWhitespace_Preserved)
|
||||
{
|
||||
std::string_view input = " hello world ";
|
||||
auto result = trim(input);
|
||||
Assert::AreEqual(std::string_view("hello world"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Trim_OnlyWhitespace_ReturnsEmpty)
|
||||
{
|
||||
std::string_view input = " \t\r\n ";
|
||||
auto result = trim(input);
|
||||
Assert::AreEqual(std::string_view(""), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Trim_CustomChars_TrimsSpecified)
|
||||
{
|
||||
std::string_view input = "xxxhelloxxx";
|
||||
auto result = trim(input, std::string_view("x"));
|
||||
Assert::AreEqual(std::string_view("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Trim_WideString_Works)
|
||||
{
|
||||
std::wstring_view input = L" hello ";
|
||||
auto result = trim(input);
|
||||
Assert::AreEqual(std::wstring_view(L"hello"), result);
|
||||
}
|
||||
|
||||
// replace_chars tests
|
||||
TEST_METHOD(ReplaceChars_EmptyString_NoChange)
|
||||
{
|
||||
std::string s = "";
|
||||
replace_chars(s, std::string_view("abc"), 'x');
|
||||
Assert::AreEqual(std::string(""), s);
|
||||
}
|
||||
|
||||
TEST_METHOD(ReplaceChars_NoMatchingChars_NoChange)
|
||||
{
|
||||
std::string s = "hello";
|
||||
replace_chars(s, std::string_view("xyz"), '_');
|
||||
Assert::AreEqual(std::string("hello"), s);
|
||||
}
|
||||
|
||||
TEST_METHOD(ReplaceChars_SingleChar_Replaces)
|
||||
{
|
||||
std::string s = "hello";
|
||||
replace_chars(s, std::string_view("l"), '_');
|
||||
Assert::AreEqual(std::string("he__o"), s);
|
||||
}
|
||||
|
||||
TEST_METHOD(ReplaceChars_MultipleChars_ReplacesAll)
|
||||
{
|
||||
std::string s = "hello world";
|
||||
replace_chars(s, std::string_view("lo"), '_');
|
||||
Assert::AreEqual(std::string("he___ w_r_d"), s);
|
||||
}
|
||||
|
||||
TEST_METHOD(ReplaceChars_WideString_Works)
|
||||
{
|
||||
std::wstring s = L"hello";
|
||||
replace_chars(s, std::wstring_view(L"l"), L'_');
|
||||
Assert::AreEqual(std::wstring(L"he__o"), s);
|
||||
}
|
||||
|
||||
// unwide tests
|
||||
TEST_METHOD(Unwide_EmptyString_ReturnsEmpty)
|
||||
{
|
||||
std::wstring input = L"";
|
||||
auto result = unwide(input);
|
||||
Assert::AreEqual(std::string(""), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Unwide_AsciiString_Converts)
|
||||
{
|
||||
std::wstring input = L"hello";
|
||||
auto result = unwide(input);
|
||||
Assert::AreEqual(std::string("hello"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Unwide_WithNumbers_Converts)
|
||||
{
|
||||
std::wstring input = L"test123";
|
||||
auto result = unwide(input);
|
||||
Assert::AreEqual(std::string("test123"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Unwide_WithSpecialChars_Converts)
|
||||
{
|
||||
std::wstring input = L"test!@#$%";
|
||||
auto result = unwide(input);
|
||||
Assert::AreEqual(std::string("test!@#$%"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Unwide_MixedCase_PreservesCase)
|
||||
{
|
||||
std::wstring input = L"HeLLo WoRLd";
|
||||
auto result = unwide(input);
|
||||
Assert::AreEqual(std::string("HeLLo WoRLd"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Unwide_LongString_Works)
|
||||
{
|
||||
std::wstring input = L"This is a longer string with multiple words and punctuation!";
|
||||
auto result = unwide(input);
|
||||
Assert::AreEqual(std::string("This is a longer string with multiple words and punctuation!"), result);
|
||||
}
|
||||
};
|
||||
}
|
||||
192
src/common/UnitTests-CommonUtils/TestHelpers.h
Normal file
192
src/common/UnitTests-CommonUtils/TestHelpers.h
Normal file
@@ -0,0 +1,192 @@
|
||||
#pragma once
|
||||
|
||||
#include "pch.h"
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
|
||||
namespace TestHelpers
|
||||
{
|
||||
// RAII helper for creating and cleaning up temporary files
|
||||
class TempFile
|
||||
{
|
||||
public:
|
||||
TempFile(const std::wstring& content = L"", const std::wstring& extension = L".txt")
|
||||
{
|
||||
wchar_t tempPath[MAX_PATH];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
|
||||
// Generate a unique filename
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<> dis(10000, 99999);
|
||||
|
||||
m_path = std::wstring(tempPath) + L"test_" + std::to_wstring(dis(gen)) + extension;
|
||||
|
||||
if (!content.empty())
|
||||
{
|
||||
std::wofstream file(m_path);
|
||||
file << content;
|
||||
}
|
||||
}
|
||||
|
||||
~TempFile()
|
||||
{
|
||||
if (std::filesystem::exists(m_path))
|
||||
{
|
||||
std::filesystem::remove(m_path);
|
||||
}
|
||||
}
|
||||
|
||||
TempFile(const TempFile&) = delete;
|
||||
TempFile& operator=(const TempFile&) = delete;
|
||||
|
||||
const std::wstring& path() const { return m_path; }
|
||||
|
||||
void write(const std::string& content)
|
||||
{
|
||||
std::ofstream file(m_path, std::ios::binary);
|
||||
file << content;
|
||||
}
|
||||
|
||||
void write(const std::wstring& content)
|
||||
{
|
||||
std::wofstream file(m_path);
|
||||
file << content;
|
||||
}
|
||||
|
||||
std::wstring read()
|
||||
{
|
||||
std::wifstream file(m_path);
|
||||
return std::wstring((std::istreambuf_iterator<wchar_t>(file)),
|
||||
std::istreambuf_iterator<wchar_t>());
|
||||
}
|
||||
|
||||
private:
|
||||
std::wstring m_path;
|
||||
};
|
||||
|
||||
// RAII helper for creating and cleaning up temporary directories
|
||||
class TempDirectory
|
||||
{
|
||||
public:
|
||||
TempDirectory()
|
||||
{
|
||||
wchar_t tempPath[MAX_PATH];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<> dis(10000, 99999);
|
||||
|
||||
m_path = std::wstring(tempPath) + L"testdir_" + std::to_wstring(dis(gen));
|
||||
std::filesystem::create_directories(m_path);
|
||||
}
|
||||
|
||||
~TempDirectory()
|
||||
{
|
||||
if (std::filesystem::exists(m_path))
|
||||
{
|
||||
std::filesystem::remove_all(m_path);
|
||||
}
|
||||
}
|
||||
|
||||
TempDirectory(const TempDirectory&) = delete;
|
||||
TempDirectory& operator=(const TempDirectory&) = delete;
|
||||
|
||||
const std::wstring& path() const { return m_path; }
|
||||
|
||||
private:
|
||||
std::wstring m_path;
|
||||
};
|
||||
|
||||
// Registry test key path - use HKCU for non-elevated tests
|
||||
inline const std::wstring TestRegistryPath = L"Software\\PowerToys\\UnitTests";
|
||||
|
||||
// RAII helper for registry key creation/cleanup
|
||||
class TestRegistryKey
|
||||
{
|
||||
public:
|
||||
TestRegistryKey(const std::wstring& subKey = L"")
|
||||
{
|
||||
m_path = TestRegistryPath;
|
||||
if (!subKey.empty())
|
||||
{
|
||||
m_path += L"\\" + subKey;
|
||||
}
|
||||
|
||||
HKEY key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, m_path.c_str(), 0, nullptr,
|
||||
REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, nullptr,
|
||||
&key, nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
RegCloseKey(key);
|
||||
m_created = true;
|
||||
}
|
||||
}
|
||||
|
||||
~TestRegistryKey()
|
||||
{
|
||||
if (m_created)
|
||||
{
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, m_path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
TestRegistryKey(const TestRegistryKey&) = delete;
|
||||
TestRegistryKey& operator=(const TestRegistryKey&) = delete;
|
||||
|
||||
bool isValid() const { return m_created; }
|
||||
const std::wstring& path() const { return m_path; }
|
||||
|
||||
bool setStringValue(const std::wstring& name, const std::wstring& value)
|
||||
{
|
||||
HKEY key;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, m_path.c_str(), 0, KEY_SET_VALUE, &key) != ERROR_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto result = RegSetValueExW(key, name.c_str(), 0, REG_SZ,
|
||||
reinterpret_cast<const BYTE*>(value.c_str()),
|
||||
static_cast<DWORD>((value.length() + 1) * sizeof(wchar_t)));
|
||||
RegCloseKey(key);
|
||||
return result == ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
bool setDwordValue(const std::wstring& name, DWORD value)
|
||||
{
|
||||
HKEY key;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, m_path.c_str(), 0, KEY_SET_VALUE, &key) != ERROR_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto result = RegSetValueExW(key, name.c_str(), 0, REG_DWORD,
|
||||
reinterpret_cast<const BYTE*>(&value), sizeof(DWORD));
|
||||
RegCloseKey(key);
|
||||
return result == ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
private:
|
||||
std::wstring m_path;
|
||||
bool m_created = false;
|
||||
};
|
||||
|
||||
// Helper to wait for a condition with timeout
|
||||
template<typename Predicate>
|
||||
bool WaitFor(Predicate pred, std::chrono::milliseconds timeout = std::chrono::milliseconds(5000))
|
||||
{
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (!pred())
|
||||
{
|
||||
if (std::chrono::steady_clock::now() - start > timeout)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
14
src/common/UnitTests-CommonUtils/TestStubs.cpp
Normal file
14
src/common/UnitTests-CommonUtils/TestStubs.cpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#include "pch.h"
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
#include <spdlog/sinks/null_sink.h>
|
||||
|
||||
std::shared_ptr<spdlog::logger> Logger::logger = spdlog::null_logger_mt("Common.Utils.UnitTests");
|
||||
|
||||
namespace PTSettingsHelper
|
||||
{
|
||||
std::wstring get_root_save_folder_location()
|
||||
{
|
||||
return L"";
|
||||
}
|
||||
}
|
||||
336
src/common/UnitTests-CommonUtils/Threading.Tests.cpp
Normal file
336
src/common/UnitTests-CommonUtils/Threading.Tests.cpp
Normal file
@@ -0,0 +1,336 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <OnThreadExecutor.h>
|
||||
#include <EventWaiter.h>
|
||||
#include <EventLocker.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(OnThreadExecutorTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(Constructor_CreatesInstance)
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
// Should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(Submit_SingleTask_Executes)
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
std::atomic<bool> executed{ false };
|
||||
|
||||
auto future = executor.submit(OnThreadExecutor::task_t([&executed]() {
|
||||
executed = true;
|
||||
}));
|
||||
|
||||
future.wait();
|
||||
Assert::IsTrue(executed);
|
||||
}
|
||||
|
||||
TEST_METHOD(Submit_MultipleTasks_ExecutesAll)
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
std::atomic<int> counter{ 0 };
|
||||
|
||||
std::vector<std::future<void>> futures;
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
futures.push_back(executor.submit(OnThreadExecutor::task_t([&counter]() {
|
||||
counter++;
|
||||
})));
|
||||
}
|
||||
|
||||
for (auto& f : futures)
|
||||
{
|
||||
f.wait();
|
||||
}
|
||||
|
||||
Assert::AreEqual(10, counter.load());
|
||||
}
|
||||
|
||||
TEST_METHOD(Submit_TasksExecuteInOrder)
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
std::vector<int> order;
|
||||
std::mutex orderMutex;
|
||||
|
||||
std::vector<std::future<void>> futures;
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
futures.push_back(executor.submit(OnThreadExecutor::task_t([&order, &orderMutex, i]() {
|
||||
std::lock_guard lock(orderMutex);
|
||||
order.push_back(i);
|
||||
})));
|
||||
}
|
||||
|
||||
for (auto& f : futures)
|
||||
{
|
||||
f.wait();
|
||||
}
|
||||
|
||||
Assert::AreEqual(static_cast<size_t>(5), order.size());
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
Assert::AreEqual(i, order[i]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(Submit_TaskReturnsResult)
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
std::atomic<int> result{ 0 };
|
||||
|
||||
auto future = executor.submit(OnThreadExecutor::task_t([&result]() {
|
||||
result = 42;
|
||||
}));
|
||||
|
||||
future.wait();
|
||||
Assert::AreEqual(42, result.load());
|
||||
}
|
||||
|
||||
TEST_METHOD(Cancel_ClearsPendingTasks)
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
std::atomic<int> counter{ 0 };
|
||||
|
||||
// Submit a slow task first
|
||||
executor.submit(OnThreadExecutor::task_t([&counter]() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
counter++;
|
||||
}));
|
||||
|
||||
// Submit more tasks
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
executor.submit(OnThreadExecutor::task_t([&counter]() {
|
||||
counter++;
|
||||
}));
|
||||
}
|
||||
|
||||
// Cancel pending tasks
|
||||
executor.cancel();
|
||||
|
||||
// Wait a bit for any running task to complete
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
// Not all tasks should have executed
|
||||
Assert::IsTrue(counter < 6);
|
||||
}
|
||||
|
||||
TEST_METHOD(Destructor_WaitsForCompletion)
|
||||
{
|
||||
std::atomic<bool> completed{ false };
|
||||
std::future<void> future;
|
||||
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
future = executor.submit(OnThreadExecutor::task_t([&completed]() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
completed = true;
|
||||
}));
|
||||
future.wait();
|
||||
} // Destructor no longer required to wait for completion
|
||||
|
||||
Assert::IsTrue(completed);
|
||||
}
|
||||
|
||||
TEST_METHOD(Submit_AfterCancel_StillWorks)
|
||||
{
|
||||
OnThreadExecutor executor;
|
||||
std::atomic<int> counter{ 0 };
|
||||
|
||||
executor.submit(OnThreadExecutor::task_t([&counter]() {
|
||||
counter++;
|
||||
}));
|
||||
executor.cancel();
|
||||
|
||||
auto future = executor.submit(OnThreadExecutor::task_t([&counter]() {
|
||||
counter = 42;
|
||||
}));
|
||||
future.wait();
|
||||
|
||||
Assert::AreEqual(42, counter.load());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(EventWaiterTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(Constructor_CreatesInstance)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
Assert::IsFalse(waiter.is_listening());
|
||||
}
|
||||
|
||||
TEST_METHOD(Start_ValidEvent_ReturnsTrue)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
bool result = waiter.start(L"TestEvent_Start", [](DWORD) {});
|
||||
Assert::IsTrue(result);
|
||||
Assert::IsTrue(waiter.is_listening());
|
||||
waiter.stop();
|
||||
}
|
||||
|
||||
TEST_METHOD(Start_AlreadyListening_ReturnsFalse)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
waiter.start(L"TestEvent_Double1", [](DWORD) {});
|
||||
bool result = waiter.start(L"TestEvent_Double2", [](DWORD) {});
|
||||
Assert::IsFalse(result);
|
||||
waiter.stop();
|
||||
}
|
||||
|
||||
TEST_METHOD(Stop_WhileListening_StopsListening)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
waiter.start(L"TestEvent_Stop", [](DWORD) {});
|
||||
Assert::IsTrue(waiter.is_listening());
|
||||
|
||||
waiter.stop();
|
||||
Assert::IsFalse(waiter.is_listening());
|
||||
}
|
||||
|
||||
TEST_METHOD(Stop_WhenNotListening_DoesNotCrash)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
waiter.stop(); // Should not crash
|
||||
Assert::IsFalse(waiter.is_listening());
|
||||
}
|
||||
|
||||
TEST_METHOD(Stop_CalledMultipleTimes_DoesNotCrash)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
waiter.start(L"TestEvent_MultiStop", [](DWORD) {});
|
||||
waiter.stop();
|
||||
waiter.stop();
|
||||
waiter.stop();
|
||||
Assert::IsFalse(waiter.is_listening());
|
||||
}
|
||||
|
||||
TEST_METHOD(Callback_EventSignaled_CallsCallback)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
std::atomic<bool> called{ false };
|
||||
std::atomic<DWORD> errorCode{ 0xFFFFFFFF };
|
||||
|
||||
// Create a named event we can signal
|
||||
std::wstring eventName = L"TestEvent_Callback_" + std::to_wstring(GetCurrentProcessId());
|
||||
HANDLE signalEvent = CreateEventW(nullptr, FALSE, FALSE, eventName.c_str());
|
||||
Assert::IsNotNull(signalEvent);
|
||||
|
||||
waiter.start(eventName, [&called, &errorCode](DWORD err) {
|
||||
errorCode = err;
|
||||
called = true;
|
||||
});
|
||||
|
||||
// Signal the event
|
||||
SetEvent(signalEvent);
|
||||
|
||||
// Wait for callback
|
||||
bool waitResult = TestHelpers::WaitFor([&called]() { return called.load(); }, std::chrono::milliseconds(1000));
|
||||
|
||||
waiter.stop();
|
||||
CloseHandle(signalEvent);
|
||||
|
||||
Assert::IsTrue(waitResult);
|
||||
Assert::AreEqual(static_cast<DWORD>(ERROR_SUCCESS), errorCode.load());
|
||||
}
|
||||
|
||||
TEST_METHOD(Destructor_StopsListening)
|
||||
{
|
||||
std::atomic<bool> isListening{ false };
|
||||
{
|
||||
EventWaiter waiter;
|
||||
waiter.start(L"TestEvent_Destructor", [](DWORD) {});
|
||||
isListening = waiter.is_listening();
|
||||
}
|
||||
// After destruction, the waiter should have stopped
|
||||
Assert::IsTrue(isListening);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsListening_InitialState_ReturnsFalse)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
Assert::IsFalse(waiter.is_listening());
|
||||
}
|
||||
|
||||
TEST_METHOD(IsListening_AfterStart_ReturnsTrue)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
waiter.start(L"TestEvent_IsListening", [](DWORD) {});
|
||||
Assert::IsTrue(waiter.is_listening());
|
||||
waiter.stop();
|
||||
}
|
||||
|
||||
TEST_METHOD(IsListening_AfterStop_ReturnsFalse)
|
||||
{
|
||||
EventWaiter waiter;
|
||||
waiter.start(L"TestEvent_AfterStop", [](DWORD) {});
|
||||
waiter.stop();
|
||||
Assert::IsFalse(waiter.is_listening());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(EventLockerTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(Get_ValidEventName_ReturnsLocker)
|
||||
{
|
||||
std::wstring eventName = L"TestEventLocker_" + std::to_wstring(GetCurrentProcessId());
|
||||
auto locker = EventLocker::Get(eventName);
|
||||
Assert::IsTrue(locker.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_UniqueNames_CreatesSeparateLockers)
|
||||
{
|
||||
auto locker1 = EventLocker::Get(L"TestEventLocker1_" + std::to_wstring(GetCurrentProcessId()));
|
||||
auto locker2 = EventLocker::Get(L"TestEventLocker2_" + std::to_wstring(GetCurrentProcessId()));
|
||||
Assert::IsTrue(locker1.has_value());
|
||||
Assert::IsTrue(locker2.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(Destructor_CleansUpHandle)
|
||||
{
|
||||
std::wstring eventName = L"TestEventLockerCleanup_" + std::to_wstring(GetCurrentProcessId());
|
||||
{
|
||||
auto locker = EventLocker::Get(eventName);
|
||||
Assert::IsTrue(locker.has_value());
|
||||
}
|
||||
// After destruction, the event should be cleaned up
|
||||
// Creating a new one should succeed
|
||||
auto newLocker = EventLocker::Get(eventName);
|
||||
Assert::IsTrue(newLocker.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(MoveConstructor_TransfersOwnership)
|
||||
{
|
||||
std::wstring eventName = L"TestEventLockerMove_" + std::to_wstring(GetCurrentProcessId());
|
||||
auto locker1 = EventLocker::Get(eventName);
|
||||
Assert::IsTrue(locker1.has_value());
|
||||
|
||||
EventLocker locker2 = std::move(*locker1);
|
||||
// Move should transfer ownership without crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(MoveAssignment_TransfersOwnership)
|
||||
{
|
||||
std::wstring eventName1 = L"TestEventLockerMoveAssign1_" + std::to_wstring(GetCurrentProcessId());
|
||||
std::wstring eventName2 = L"TestEventLockerMoveAssign2_" + std::to_wstring(GetCurrentProcessId());
|
||||
|
||||
auto locker1 = EventLocker::Get(eventName1);
|
||||
auto locker2 = EventLocker::Get(eventName2);
|
||||
|
||||
Assert::IsTrue(locker1.has_value());
|
||||
Assert::IsTrue(locker2.has_value());
|
||||
|
||||
*locker1 = std::move(*locker2);
|
||||
// Should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
248
src/common/UnitTests-CommonUtils/TimeUtils.Tests.cpp
Normal file
248
src/common/UnitTests-CommonUtils/TimeUtils.Tests.cpp
Normal file
@@ -0,0 +1,248 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <timeutil.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(TimeUtilsTests)
|
||||
{
|
||||
public:
|
||||
// to_string tests
|
||||
TEST_METHOD(ToString_ZeroTime_ReturnsZero)
|
||||
{
|
||||
time_t t = 0;
|
||||
auto result = timeutil::to_string(t);
|
||||
Assert::AreEqual(std::wstring(L"0"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ToString_PositiveTime_ReturnsString)
|
||||
{
|
||||
time_t t = 1234567890;
|
||||
auto result = timeutil::to_string(t);
|
||||
Assert::AreEqual(std::wstring(L"1234567890"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ToString_LargeTime_ReturnsString)
|
||||
{
|
||||
time_t t = 1700000000;
|
||||
auto result = timeutil::to_string(t);
|
||||
Assert::AreEqual(std::wstring(L"1700000000"), result);
|
||||
}
|
||||
|
||||
// from_string tests
|
||||
TEST_METHOD(FromString_ZeroString_ReturnsZero)
|
||||
{
|
||||
auto result = timeutil::from_string(L"0");
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::AreEqual(static_cast<time_t>(0), result.value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromString_ValidNumber_ReturnsTime)
|
||||
{
|
||||
auto result = timeutil::from_string(L"1234567890");
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::AreEqual(static_cast<time_t>(1234567890), result.value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromString_InvalidString_ReturnsNullopt)
|
||||
{
|
||||
auto result = timeutil::from_string(L"invalid");
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromString_EmptyString_ReturnsNullopt)
|
||||
{
|
||||
auto result = timeutil::from_string(L"");
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromString_MixedAlphaNumeric_ReturnsNullopt)
|
||||
{
|
||||
auto result = timeutil::from_string(L"123abc");
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromString_NegativeNumber_ReturnsNullopt)
|
||||
{
|
||||
auto result = timeutil::from_string(L"-1");
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
// Roundtrip test
|
||||
TEST_METHOD(ToStringFromString_Roundtrip_Works)
|
||||
{
|
||||
time_t original = 1609459200; // 2021-01-01 00:00:00 UTC
|
||||
auto str = timeutil::to_string(original);
|
||||
auto result = timeutil::from_string(str);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::AreEqual(original, result.value());
|
||||
}
|
||||
|
||||
// now tests
|
||||
TEST_METHOD(Now_ReturnsReasonableTime)
|
||||
{
|
||||
auto result = timeutil::now();
|
||||
// Should be after 2020 and before 2100
|
||||
Assert::IsTrue(result > 1577836800); // 2020-01-01
|
||||
Assert::IsTrue(result < 4102444800); // 2100-01-01
|
||||
}
|
||||
|
||||
TEST_METHOD(Now_TwoCallsAreCloseInTime)
|
||||
{
|
||||
auto first = timeutil::now();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
auto second = timeutil::now();
|
||||
// Difference should be less than 2 seconds
|
||||
Assert::IsTrue(second >= first);
|
||||
Assert::IsTrue(second - first < 2);
|
||||
}
|
||||
|
||||
// diff::in_seconds tests
|
||||
TEST_METHOD(DiffInSeconds_SameTime_ReturnsZero)
|
||||
{
|
||||
time_t t = 1000000;
|
||||
auto result = timeutil::diff::in_seconds(t, t);
|
||||
Assert::AreEqual(static_cast<int64_t>(0), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInSeconds_OneDifference_ReturnsOne)
|
||||
{
|
||||
time_t to = 1000001;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_seconds(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(1), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInSeconds_60Seconds_Returns60)
|
||||
{
|
||||
time_t to = 1000060;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_seconds(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(60), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInSeconds_NegativeDiff_ReturnsNegative)
|
||||
{
|
||||
time_t to = 1000000;
|
||||
time_t from = 1000060;
|
||||
auto result = timeutil::diff::in_seconds(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(-60), result);
|
||||
}
|
||||
|
||||
// diff::in_minutes tests
|
||||
TEST_METHOD(DiffInMinutes_SameTime_ReturnsZero)
|
||||
{
|
||||
time_t t = 1000000;
|
||||
auto result = timeutil::diff::in_minutes(t, t);
|
||||
Assert::AreEqual(static_cast<int64_t>(0), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInMinutes_OneMinute_ReturnsOne)
|
||||
{
|
||||
time_t to = 1000060;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_minutes(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(1), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInMinutes_60Minutes_Returns60)
|
||||
{
|
||||
time_t to = 1003600;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_minutes(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(60), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInMinutes_LessThanMinute_ReturnsZero)
|
||||
{
|
||||
time_t to = 1000059;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_minutes(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(0), result);
|
||||
}
|
||||
|
||||
// diff::in_hours tests
|
||||
TEST_METHOD(DiffInHours_SameTime_ReturnsZero)
|
||||
{
|
||||
time_t t = 1000000;
|
||||
auto result = timeutil::diff::in_hours(t, t);
|
||||
Assert::AreEqual(static_cast<int64_t>(0), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInHours_OneHour_ReturnsOne)
|
||||
{
|
||||
time_t to = 1003600;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_hours(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(1), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInHours_24Hours_Returns24)
|
||||
{
|
||||
time_t to = 1086400;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_hours(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(24), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInHours_LessThanHour_ReturnsZero)
|
||||
{
|
||||
time_t to = 1003599;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_hours(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(0), result);
|
||||
}
|
||||
|
||||
// diff::in_days tests
|
||||
TEST_METHOD(DiffInDays_SameTime_ReturnsZero)
|
||||
{
|
||||
time_t t = 1000000;
|
||||
auto result = timeutil::diff::in_days(t, t);
|
||||
Assert::AreEqual(static_cast<int64_t>(0), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInDays_OneDay_ReturnsOne)
|
||||
{
|
||||
time_t to = 1086400;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_days(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(1), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInDays_7Days_Returns7)
|
||||
{
|
||||
time_t to = 1604800;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_days(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(7), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DiffInDays_LessThanDay_ReturnsZero)
|
||||
{
|
||||
time_t to = 1086399;
|
||||
time_t from = 1000000;
|
||||
auto result = timeutil::diff::in_days(to, from);
|
||||
Assert::AreEqual(static_cast<int64_t>(0), result);
|
||||
}
|
||||
|
||||
// format_as_local tests
|
||||
TEST_METHOD(FormatAsLocal_YearFormat_ReturnsYear)
|
||||
{
|
||||
time_t t = 1609459200; // 2021-01-01 00:00:00 UTC
|
||||
auto result = timeutil::format_as_local("%Y", t);
|
||||
// Result depends on local timezone, but year should be 2020 or 2021
|
||||
Assert::IsTrue(result == "2020" || result == "2021");
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatAsLocal_DateFormat_ReturnsDate)
|
||||
{
|
||||
time_t t = 0; // 1970-01-01 00:00:00 UTC
|
||||
auto result = timeutil::format_as_local("%Y-%m-%d", t);
|
||||
// Result should be a date around 1970-01-01 depending on timezone
|
||||
Assert::IsTrue(result.length() == 10); // YYYY-MM-DD format
|
||||
Assert::IsTrue(result.substr(0, 4) == "1969" || result.substr(0, 4) == "1970");
|
||||
}
|
||||
};
|
||||
}
|
||||
210
src/common/UnitTests-CommonUtils/UnhandledException.Tests.cpp
Normal file
210
src/common/UnitTests-CommonUtils/UnhandledException.Tests.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <UnhandledExceptionHandler.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(UnhandledExceptionTests)
|
||||
{
|
||||
public:
|
||||
// exceptionDescription tests
|
||||
TEST_METHOD(ExceptionDescription_AccessViolation_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_ACCESS_VIOLATION);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
// Should contain meaningful description
|
||||
std::string desc{ result };
|
||||
Assert::IsTrue(desc.find("ACCESS") != std::string::npos ||
|
||||
desc.find("access") != std::string::npos ||
|
||||
desc.find("violation") != std::string::npos ||
|
||||
desc.length() > 0);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_StackOverflow_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_STACK_OVERFLOW);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_DivideByZero_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_INT_DIVIDE_BY_ZERO);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_IllegalInstruction_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_ILLEGAL_INSTRUCTION);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_ArrayBoundsExceeded_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_ARRAY_BOUNDS_EXCEEDED);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_Breakpoint_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_BREAKPOINT);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_SingleStep_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_SINGLE_STEP);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_FloatDivideByZero_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_FLT_DIVIDE_BY_ZERO);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_FloatOverflow_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_FLT_OVERFLOW);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_FloatUnderflow_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_FLT_UNDERFLOW);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_FloatInvalidOperation_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_FLT_INVALID_OPERATION);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_PrivilegedInstruction_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_PRIV_INSTRUCTION);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_InPageError_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(EXCEPTION_IN_PAGE_ERROR);
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_UnknownCode_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(0x12345678);
|
||||
// Should return something (possibly "Unknown exception" or similar)
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
TEST_METHOD(ExceptionDescription_ZeroCode_ReturnsDescription)
|
||||
{
|
||||
auto result = exceptionDescription(0);
|
||||
// Should handle zero gracefully
|
||||
Assert::IsTrue(result && *result != '\0');
|
||||
}
|
||||
|
||||
// GetFilenameStart tests (if accessible)
|
||||
TEST_METHOD(GetFilenameStart_ValidPath_ReturnsFilename)
|
||||
{
|
||||
wchar_t path[] = L"C:\\folder\\subfolder\\file.exe";
|
||||
int start = GetFilenameStart(path);
|
||||
|
||||
Assert::IsTrue(start >= 0);
|
||||
Assert::AreEqual(std::wstring(L"file.exe"), std::wstring(path + start));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetFilenameStart_NoPath_ReturnsOriginal)
|
||||
{
|
||||
wchar_t path[] = L"file.exe";
|
||||
int start = GetFilenameStart(path);
|
||||
|
||||
Assert::IsTrue(start >= 0);
|
||||
Assert::AreEqual(std::wstring(L"file.exe"), std::wstring(path + start));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetFilenameStart_TrailingBackslash_ReturnsEmpty)
|
||||
{
|
||||
wchar_t path[] = L"C:\\folder\\";
|
||||
int start = GetFilenameStart(path);
|
||||
|
||||
// Should point to empty string after last backslash
|
||||
Assert::IsTrue(start >= 0);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetFilenameStart_NullPath_HandlesGracefully)
|
||||
{
|
||||
// This might crash or return null depending on implementation
|
||||
// Just document the behavior
|
||||
int start = GetFilenameStart(nullptr);
|
||||
(void)start;
|
||||
// Result is implementation-defined for null input
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(ExceptionDescription_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
auto desc = exceptionDescription(EXCEPTION_ACCESS_VIOLATION);
|
||||
if (desc && *desc != '\0')
|
||||
{
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
|
||||
// All exception codes test
|
||||
TEST_METHOD(ExceptionDescription_AllCommonCodes_ReturnDescriptions)
|
||||
{
|
||||
std::vector<DWORD> codes = {
|
||||
EXCEPTION_ACCESS_VIOLATION,
|
||||
EXCEPTION_ARRAY_BOUNDS_EXCEEDED,
|
||||
EXCEPTION_BREAKPOINT,
|
||||
EXCEPTION_DATATYPE_MISALIGNMENT,
|
||||
EXCEPTION_FLT_DENORMAL_OPERAND,
|
||||
EXCEPTION_FLT_DIVIDE_BY_ZERO,
|
||||
EXCEPTION_FLT_INEXACT_RESULT,
|
||||
EXCEPTION_FLT_INVALID_OPERATION,
|
||||
EXCEPTION_FLT_OVERFLOW,
|
||||
EXCEPTION_FLT_STACK_CHECK,
|
||||
EXCEPTION_FLT_UNDERFLOW,
|
||||
EXCEPTION_ILLEGAL_INSTRUCTION,
|
||||
EXCEPTION_IN_PAGE_ERROR,
|
||||
EXCEPTION_INT_DIVIDE_BY_ZERO,
|
||||
EXCEPTION_INT_OVERFLOW,
|
||||
EXCEPTION_INVALID_DISPOSITION,
|
||||
EXCEPTION_NONCONTINUABLE_EXCEPTION,
|
||||
EXCEPTION_PRIV_INSTRUCTION,
|
||||
EXCEPTION_SINGLE_STEP,
|
||||
EXCEPTION_STACK_OVERFLOW
|
||||
};
|
||||
|
||||
for (DWORD code : codes)
|
||||
{
|
||||
auto desc = exceptionDescription(code);
|
||||
Assert::IsTrue(desc && *desc != '\0', (L"Empty description for code: " + std::to_wstring(code)).c_str());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
36
src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.rc
Normal file
36
src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.rc
Normal file
@@ -0,0 +1,36 @@
|
||||
#include <windows.h>
|
||||
#include "resource.h"
|
||||
#include "../version/version.h"
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION FILE_VERSION
|
||||
PRODUCTVERSION PRODUCT_VERSION
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_DLL
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
|
||||
BEGIN
|
||||
VALUE "CompanyName", COMPANY_NAME
|
||||
VALUE "FileDescription", FILE_DESCRIPTION
|
||||
VALUE "FileVersion", FILE_VERSION_STRING
|
||||
VALUE "InternalName", INTERNAL_NAME
|
||||
VALUE "LegalCopyright", COPYRIGHT_NOTE
|
||||
VALUE "OriginalFilename", ORIGINAL_FILENAME
|
||||
VALUE "ProductName", PRODUCT_NAME
|
||||
VALUE "ProductVersion", PRODUCT_VERSION_STRING
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
|
||||
END
|
||||
END
|
||||
101
src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.vcxproj
Normal file
101
src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.vcxproj
Normal file
@@ -0,0 +1,101 @@
|
||||
<?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')" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<ProjectGuid>{8B5CFB38-CCBA-40A8-AD7A-89C57B070884}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>UnitTestsCommonUtils</RootNamespace>
|
||||
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
|
||||
<ProjectName>Common.Utils.UnitTests</ProjectName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseOfMfc>false</UseOfMfc>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\tests\UnitTestsCommonUtils\</OutDir>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.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>..\;..\utils;..\Telemetry;..\..\;..\..\..\deps\;..\..\..\deps\spdlog\include;..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\include;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<LanguageStandard>stdcpp23</LanguageStandard>
|
||||
<PreprocessorDefinitions>SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_HEADER_ONLY;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>RuntimeObject.lib;Msi.lib;Shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="StringUtils.Tests.cpp" />
|
||||
<ClCompile Include="ColorUtils.Tests.cpp" />
|
||||
<ClCompile Include="TimeUtils.Tests.cpp" />
|
||||
<ClCompile Include="WinApiError.Tests.cpp" />
|
||||
<ClCompile Include="Serialized.Tests.cpp" />
|
||||
<ClCompile Include="Json.Tests.cpp" />
|
||||
<ClCompile Include="OsDetect.Tests.cpp" />
|
||||
<ClCompile Include="Threading.Tests.cpp" />
|
||||
<ClCompile Include="ProcessPath.Tests.cpp" />
|
||||
<ClCompile Include="Window.Tests.cpp" />
|
||||
<ClCompile Include="GameMode.Tests.cpp" />
|
||||
<ClCompile Include="Gpo.Tests.cpp" />
|
||||
<ClCompile Include="MsiUtils.Tests.cpp" />
|
||||
<ClCompile Include="HttpClient.Tests.cpp" />
|
||||
<ClCompile Include="ComObjectFactory.Tests.cpp" />
|
||||
<ClCompile Include="AppMutex.Tests.cpp" />
|
||||
<ClCompile Include="Elevation.Tests.cpp" />
|
||||
<ClCompile Include="Exec.Tests.cpp" />
|
||||
<ClCompile Include="ExcludedApps.Tests.cpp" />
|
||||
<ClCompile Include="HDropIterator.Tests.cpp" />
|
||||
<ClCompile Include="LoggerHelper.Tests.cpp" />
|
||||
<ClCompile Include="ModulesRegistry.Tests.cpp" />
|
||||
<ClCompile Include="MsWindowsSettings.Tests.cpp" />
|
||||
<ClCompile Include="Package.Tests.cpp" />
|
||||
<ClCompile Include="ProcessApi.Tests.cpp" />
|
||||
<ClCompile Include="ProcessWaiter.Tests.cpp" />
|
||||
<ClCompile Include="Registry.Tests.cpp" />
|
||||
<ClCompile Include="Resources.Tests.cpp" />
|
||||
<ClCompile Include="TestStubs.cpp" />
|
||||
<ClCompile Include="UnhandledException.Tests.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="TestHelpers.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="UnitTests-CommonUtils.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\utils\CommonUtils.vcxproj">
|
||||
<Project>{74485049-C722-400F-ABE5-86AC41736D21}</Project>
|
||||
</ProjectReference>
|
||||
</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')" />
|
||||
</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'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -0,0 +1,143 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\Pure Functions">
|
||||
<UniqueIdentifier>{A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\Threading">
|
||||
<UniqueIdentifier>{B2C3D4E5-F6A7-4B6C-9D0E-1F2A3B4C5D6E}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\Process">
|
||||
<UniqueIdentifier>{C3D4E5F6-A7B8-4C7D-0E1F-2A3B4C5D6E7F}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\Registry">
|
||||
<UniqueIdentifier>{D4E5F6A7-B8C9-4D8E-1F2A-3B4C5D6E7F8A}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\Integration">
|
||||
<UniqueIdentifier>{E5F6A7B8-C9D0-4E9F-2A3B-4C5D6E7F8A9B}</UniqueIdentifier>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="StringUtils.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ColorUtils.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="TimeUtils.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="WinApiError.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Serialized.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Json.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ExcludedApps.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="OsDetect.Tests.cpp">
|
||||
<Filter>Source Files\Pure Functions</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Threading.Tests.cpp">
|
||||
<Filter>Source Files\Threading</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="AppMutex.Tests.cpp">
|
||||
<Filter>Source Files\Threading</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ProcessWaiter.Tests.cpp">
|
||||
<Filter>Source Files\Threading</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ProcessPath.Tests.cpp">
|
||||
<Filter>Source Files\Process</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ProcessApi.Tests.cpp">
|
||||
<Filter>Source Files\Process</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Window.Tests.cpp">
|
||||
<Filter>Source Files\Process</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Exec.Tests.cpp">
|
||||
<Filter>Source Files\Process</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="GameMode.Tests.cpp">
|
||||
<Filter>Source Files\Process</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="MsWindowsSettings.Tests.cpp">
|
||||
<Filter>Source Files\Process</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Registry.Tests.cpp">
|
||||
<Filter>Source Files\Registry</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Gpo.Tests.cpp">
|
||||
<Filter>Source Files\Registry</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ModulesRegistry.Tests.cpp">
|
||||
<Filter>Source Files\Registry</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Elevation.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Package.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="MsiUtils.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="HttpClient.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Resources.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="LoggerHelper.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ComObjectFactory.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="HDropIterator.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="UnhandledException.Tests.cpp">
|
||||
<Filter>Source Files\Integration</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="resource.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="TestHelpers.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="UnitTests-CommonUtils.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
130
src/common/UnitTests-CommonUtils/WinApiError.Tests.cpp
Normal file
130
src/common/UnitTests-CommonUtils/WinApiError.Tests.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <winapi_error.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(WinApiErrorTests)
|
||||
{
|
||||
public:
|
||||
// get_last_error_message tests
|
||||
TEST_METHOD(GetLastErrorMessage_Success_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_SUCCESS);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_FileNotFound_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_FILE_NOT_FOUND);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_AccessDenied_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_ACCESS_DENIED);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_PathNotFound_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_PATH_NOT_FOUND);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_InvalidHandle_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_INVALID_HANDLE);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_NotEnoughMemory_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_NOT_ENOUGH_MEMORY);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_InvalidParameter_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_INVALID_PARAMETER);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
// get_last_error_or_default tests
|
||||
TEST_METHOD(GetLastErrorOrDefault_Success_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_or_default(ERROR_SUCCESS);
|
||||
Assert::IsFalse(result.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorOrDefault_FileNotFound_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_or_default(ERROR_FILE_NOT_FOUND);
|
||||
Assert::IsFalse(result.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorOrDefault_AccessDenied_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_or_default(ERROR_ACCESS_DENIED);
|
||||
Assert::IsFalse(result.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorOrDefault_UnknownError_ReturnsEmptyOrMessage)
|
||||
{
|
||||
// For an unknown error code, should return empty string or a default message
|
||||
auto result = get_last_error_or_default(0xFFFFFFFF);
|
||||
// Either empty or has content, both are valid
|
||||
Assert::IsTrue(result.empty() || !result.empty());
|
||||
}
|
||||
|
||||
// Comparison tests
|
||||
TEST_METHOD(BothFunctions_SameError_ProduceSameContent)
|
||||
{
|
||||
auto message = get_last_error_message(ERROR_FILE_NOT_FOUND);
|
||||
auto defaultMessage = get_last_error_or_default(ERROR_FILE_NOT_FOUND);
|
||||
|
||||
Assert::IsTrue(message.has_value());
|
||||
Assert::AreEqual(*message, defaultMessage);
|
||||
}
|
||||
|
||||
TEST_METHOD(BothFunctions_SuccessError_ProduceSameContent)
|
||||
{
|
||||
auto message = get_last_error_message(ERROR_SUCCESS);
|
||||
auto defaultMessage = get_last_error_or_default(ERROR_SUCCESS);
|
||||
|
||||
Assert::IsTrue(message.has_value());
|
||||
Assert::AreEqual(*message, defaultMessage);
|
||||
}
|
||||
|
||||
// Error code specific tests
|
||||
TEST_METHOD(GetLastErrorMessage_SharingViolation_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_SHARING_VIOLATION);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_FileExists_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_FILE_EXISTS);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLastErrorMessage_DirNotEmpty_ReturnsMessage)
|
||||
{
|
||||
auto result = get_last_error_message(ERROR_DIR_NOT_EMPTY);
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
};
|
||||
}
|
||||
159
src/common/UnitTests-CommonUtils/Window.Tests.cpp
Normal file
159
src/common/UnitTests-CommonUtils/Window.Tests.cpp
Normal file
@@ -0,0 +1,159 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <window.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(WindowTests)
|
||||
{
|
||||
public:
|
||||
// is_system_window tests
|
||||
TEST_METHOD(IsSystemWindow_DesktopWindow_ReturnsResult)
|
||||
{
|
||||
HWND desktop = GetDesktopWindow();
|
||||
Assert::IsNotNull(desktop);
|
||||
|
||||
// Get class name
|
||||
char className[256] = {};
|
||||
GetClassNameA(desktop, className, sizeof(className));
|
||||
|
||||
bool result = is_system_window(desktop, className);
|
||||
// Just verify it doesn't crash and returns a boolean
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsSystemWindow_NullHwnd_ReturnsFalse)
|
||||
{
|
||||
auto shell = GetShellWindow();
|
||||
auto desktop = GetDesktopWindow();
|
||||
bool result = is_system_window(nullptr, "ClassName");
|
||||
bool expected = (shell == nullptr) || (desktop == nullptr);
|
||||
Assert::AreEqual(expected, result);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsSystemWindow_InvalidHwnd_ReturnsFalse)
|
||||
{
|
||||
bool result = is_system_window(reinterpret_cast<HWND>(0x12345678), "ClassName");
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsSystemWindow_EmptyClassName_DoesNotCrash)
|
||||
{
|
||||
HWND desktop = GetDesktopWindow();
|
||||
bool result = is_system_window(desktop, "");
|
||||
// Just verify it doesn't crash
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsSystemWindow_NullClassName_DoesNotCrash)
|
||||
{
|
||||
HWND desktop = GetDesktopWindow();
|
||||
bool result = is_system_window(desktop, nullptr);
|
||||
// Should handle null className gracefully
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
// GetWindowCreateParam tests
|
||||
TEST_METHOD(GetWindowCreateParam_ValidLparam_ReturnsValue)
|
||||
{
|
||||
struct TestData
|
||||
{
|
||||
int value;
|
||||
};
|
||||
|
||||
TestData data{ 42 };
|
||||
CREATESTRUCT cs{};
|
||||
cs.lpCreateParams = &data;
|
||||
|
||||
auto result = GetWindowCreateParam<TestData*>(reinterpret_cast<LPARAM>(&cs));
|
||||
Assert::IsNotNull(result);
|
||||
Assert::AreEqual(42, result->value);
|
||||
}
|
||||
|
||||
// Window data storage tests
|
||||
TEST_METHOD(WindowData_StoreAndRetrieve_Works)
|
||||
{
|
||||
// Create a simple message-only window for testing
|
||||
WNDCLASSW wc = {};
|
||||
wc.lpfnWndProc = DefWindowProcW;
|
||||
wc.hInstance = GetModuleHandleW(nullptr);
|
||||
wc.lpszClassName = L"TestWindowClass_DataTest";
|
||||
RegisterClassW(&wc);
|
||||
|
||||
HWND hwnd = CreateWindowExW(0, L"TestWindowClass_DataTest", L"Test",
|
||||
0, 0, 0, 0, 0, HWND_MESSAGE, nullptr,
|
||||
GetModuleHandleW(nullptr), nullptr);
|
||||
|
||||
if (hwnd)
|
||||
{
|
||||
int value = 42;
|
||||
int* testValue = &value;
|
||||
StoreWindowParam(hwnd, testValue);
|
||||
|
||||
auto retrieved = GetWindowParam<int*>(hwnd);
|
||||
Assert::AreEqual(testValue, retrieved);
|
||||
|
||||
DestroyWindow(hwnd);
|
||||
}
|
||||
|
||||
UnregisterClassW(L"TestWindowClass_DataTest", GetModuleHandleW(nullptr));
|
||||
Assert::IsTrue(true); // Window creation might fail in test environment
|
||||
}
|
||||
|
||||
// run_message_loop tests
|
||||
TEST_METHOD(RunMessageLoop_UntilIdle_Completes)
|
||||
{
|
||||
// Run message loop until idle with a timeout
|
||||
// This should complete quickly since there are no messages
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
run_message_loop(true, 100);
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start);
|
||||
|
||||
// Should complete within reasonable time
|
||||
Assert::IsTrue(elapsed.count() < 500);
|
||||
}
|
||||
|
||||
TEST_METHOD(RunMessageLoop_WithTimeout_RespectsTimeout)
|
||||
{
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
run_message_loop(false, 50);
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start);
|
||||
|
||||
// Should take at least the timeout duration
|
||||
// Allow some tolerance for timing
|
||||
Assert::IsTrue(elapsed.count() >= 40 && elapsed.count() < 500);
|
||||
}
|
||||
|
||||
TEST_METHOD(RunMessageLoop_ZeroTimeout_CompletesImmediately)
|
||||
{
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
run_message_loop(false, 0);
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start);
|
||||
|
||||
// Should complete very quickly
|
||||
Assert::IsTrue(elapsed.count() < 100);
|
||||
}
|
||||
|
||||
TEST_METHOD(RunMessageLoop_NoTimeout_ProcessesMessages)
|
||||
{
|
||||
// Post a quit message before starting the loop
|
||||
PostQuitMessage(0);
|
||||
|
||||
// Should process the quit message and exit
|
||||
run_message_loop(false, std::nullopt);
|
||||
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
4
src/common/UnitTests-CommonUtils/packages.config
Normal file
4
src/common/UnitTests-CommonUtils/packages.config
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
|
||||
</packages>
|
||||
5
src/common/UnitTests-CommonUtils/pch.cpp
Normal file
5
src/common/UnitTests-CommonUtils/pch.cpp
Normal file
@@ -0,0 +1,5 @@
|
||||
// pch.cpp: source file corresponding to the pre-compiled header
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.
|
||||
39
src/common/UnitTests-CommonUtils/pch.h
Normal file
39
src/common/UnitTests-CommonUtils/pch.h
Normal file
@@ -0,0 +1,39 @@
|
||||
// pch.h: This is a precompiled header file.
|
||||
// Files listed below are compiled only once, improving build performance for future builds.
|
||||
// This also affects IntelliSense performance, including code completion and many code browsing features.
|
||||
// However, files listed here are ALL re-compiled if any one of them is updated between builds.
|
||||
// Do not add files here that you will be updating frequently as this negates the performance advantage.
|
||||
|
||||
#ifndef PCH_H
|
||||
#define PCH_H
|
||||
|
||||
// add headers that you want to pre-compile here
|
||||
#include <Windows.h>
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
#include <winrt/Windows.Foundation.Metadata.h>
|
||||
#include <winrt/Windows.Data.Json.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <future>
|
||||
#include <queue>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
|
||||
// Suppressing 26466 - Don't use static_cast downcasts - in CppUnitTest.h
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 26466)
|
||||
#include "CppUnitTest.h"
|
||||
#pragma warning(pop)
|
||||
|
||||
#endif //PCH_H
|
||||
13
src/common/UnitTests-CommonUtils/resource.h
Normal file
13
src/common/UnitTests-CommonUtils/resource.h
Normal file
@@ -0,0 +1,13 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by UnitTests-CommonUtils.rc
|
||||
|
||||
//////////////////////////////
|
||||
// Non-localizable
|
||||
|
||||
#define FILE_DESCRIPTION "PowerToys UnitTests-CommonUtils"
|
||||
#define INTERNAL_NAME "UnitTests-CommonUtils"
|
||||
#define ORIGINAL_FILENAME "UnitTests-CommonUtils.dll"
|
||||
|
||||
// Non-localizable
|
||||
//////////////////////////////
|
||||
@@ -251,4 +251,40 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
{
|
||||
return CommonSharedConstants::CMDPAL_SHOW_EVENT;
|
||||
}
|
||||
hstring Constants::TogglePowerDisplayEvent()
|
||||
{
|
||||
return CommonSharedConstants::TOGGLE_POWER_DISPLAY_EVENT;
|
||||
}
|
||||
hstring Constants::TerminatePowerDisplayEvent()
|
||||
{
|
||||
return CommonSharedConstants::TERMINATE_POWER_DISPLAY_EVENT;
|
||||
}
|
||||
hstring Constants::RefreshPowerDisplayMonitorsEvent()
|
||||
{
|
||||
return CommonSharedConstants::REFRESH_POWER_DISPLAY_MONITORS_EVENT;
|
||||
}
|
||||
hstring Constants::SettingsUpdatedPowerDisplayEvent()
|
||||
{
|
||||
return CommonSharedConstants::SETTINGS_UPDATED_POWER_DISPLAY_EVENT;
|
||||
}
|
||||
hstring Constants::PowerDisplaySendSettingsTelemetryEvent()
|
||||
{
|
||||
return CommonSharedConstants::POWER_DISPLAY_SEND_SETTINGS_TELEMETRY_EVENT;
|
||||
}
|
||||
hstring Constants::HotkeyUpdatedPowerDisplayEvent()
|
||||
{
|
||||
return CommonSharedConstants::HOTKEY_UPDATED_POWER_DISPLAY_EVENT;
|
||||
}
|
||||
hstring Constants::PowerDisplayToggleMessage()
|
||||
{
|
||||
return CommonSharedConstants::POWER_DISPLAY_TOGGLE_MESSAGE;
|
||||
}
|
||||
hstring Constants::PowerDisplayApplyProfileMessage()
|
||||
{
|
||||
return CommonSharedConstants::POWER_DISPLAY_APPLY_PROFILE_MESSAGE;
|
||||
}
|
||||
hstring Constants::PowerDisplayTerminateAppMessage()
|
||||
{
|
||||
return CommonSharedConstants::POWER_DISPLAY_TERMINATE_APP_MESSAGE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,15 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring WorkspacesHotkeyEvent();
|
||||
static hstring PowerToysRunnerTerminateSettingsEvent();
|
||||
static hstring ShowCmdPalEvent();
|
||||
static hstring TogglePowerDisplayEvent();
|
||||
static hstring TerminatePowerDisplayEvent();
|
||||
static hstring RefreshPowerDisplayMonitorsEvent();
|
||||
static hstring SettingsUpdatedPowerDisplayEvent();
|
||||
static hstring PowerDisplaySendSettingsTelemetryEvent();
|
||||
static hstring HotkeyUpdatedPowerDisplayEvent();
|
||||
static hstring PowerDisplayToggleMessage();
|
||||
static hstring PowerDisplayApplyProfileMessage();
|
||||
static hstring PowerDisplayTerminateAppMessage();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,15 @@ namespace PowerToys
|
||||
static String WorkspacesHotkeyEvent();
|
||||
static String PowerToysRunnerTerminateSettingsEvent();
|
||||
static String ShowCmdPalEvent();
|
||||
static String TogglePowerDisplayEvent();
|
||||
static String TerminatePowerDisplayEvent();
|
||||
static String RefreshPowerDisplayMonitorsEvent();
|
||||
static String SettingsUpdatedPowerDisplayEvent();
|
||||
static String PowerDisplaySendSettingsTelemetryEvent();
|
||||
static String HotkeyUpdatedPowerDisplayEvent();
|
||||
static String PowerDisplayToggleMessage();
|
||||
static String PowerDisplayApplyProfileMessage();
|
||||
static String PowerDisplayTerminateAppMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,23 @@ namespace CommonSharedConstants
|
||||
const wchar_t ZOOMIT_SNIP_EVENT[] = L"Local\\PowerToysZoomIt-SnipEvent-2fd9c211-436d-4f17-a902-2528aaae3e30";
|
||||
const wchar_t ZOOMIT_RECORD_EVENT[] = L"Local\\PowerToysZoomIt-RecordEvent-74539344-eaad-4711-8e83-23946e424512";
|
||||
|
||||
// Path to the events used by PowerDisplay
|
||||
const wchar_t TOGGLE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ToggleEvent-5f1a9c3e-7d2b-4e8f-9a6c-3b5d7e9f1a2c";
|
||||
const wchar_t TERMINATE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a";
|
||||
const wchar_t REFRESH_POWER_DISPLAY_MONITORS_EVENT[] = L"Local\\PowerToysPowerDisplay-RefreshMonitorsEvent-a3f5c8e7-9d1b-4e2f-8c6a-3b5d7e9f1a2c";
|
||||
const wchar_t SETTINGS_UPDATED_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-SettingsUpdatedEvent-2e4d6f8a-1c3b-5e7f-9a1d-4c6e8f0b2d3e";
|
||||
const wchar_t POWER_DISPLAY_SEND_SETTINGS_TELEMETRY_EVENT[] = L"Local\\PowerToysPowerDisplay-SettingsTelemetryEvent-8c4f2a1d-5e3b-7f9c-1a6d-3b8e5f2c9a7d";
|
||||
const wchar_t HOTKEY_UPDATED_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-HotkeyUpdatedEvent-9d5f3a2b-7e1c-4b8a-6f3d-2a9e5c7b1d4f";
|
||||
|
||||
// IPC Messages used in PowerDisplay (Named Pipe communication)
|
||||
const wchar_t POWER_DISPLAY_TOGGLE_MESSAGE[] = L"Toggle";
|
||||
const wchar_t POWER_DISPLAY_APPLY_PROFILE_MESSAGE[] = L"ApplyProfile";
|
||||
const wchar_t POWER_DISPLAY_TERMINATE_APP_MESSAGE[] = L"TerminateApp";
|
||||
|
||||
// Path to the events used by LightSwitch to notify PowerDisplay of theme changes
|
||||
const wchar_t LIGHT_SWITCH_LIGHT_THEME_EVENT[] = L"Local\\PowerToysLightSwitch-LightThemeEvent-50077121-2ffc-4841-9c86-ab1bd3f9baca";
|
||||
const wchar_t LIGHT_SWITCH_DARK_THEME_EVENT[] = L"Local\\PowerToysLightSwitch-DarkThemeEvent-b3a835c0-eaa2-49b0-b8eb-f793e3df3368";
|
||||
|
||||
// used from quick access window
|
||||
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
|
||||
const wchar_t CMDPAL_EXIT_EVENT[] = L"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd";
|
||||
|
||||
@@ -83,6 +83,7 @@ struct LogSettings
|
||||
inline const static std::wstring workspacesSnapshotToolLogPath = L"workspaces-snapshot-tool-log.log";
|
||||
inline const static std::string zoomItLoggerName = "zoom-it";
|
||||
inline const static std::string lightSwitchLoggerName = "light-switch";
|
||||
inline const static std::string powerDisplayLoggerName = "powerdisplay";
|
||||
inline const static int retention = 30;
|
||||
std::wstring logLevel;
|
||||
LogSettings();
|
||||
|
||||
135
src/common/utils/CommonUtils.vcxproj
Normal file
135
src/common/utils/CommonUtils.vcxproj
Normal file
@@ -0,0 +1,135 @@
|
||||
<?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|Win32">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</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>{74485049-C722-400F-ABE5-86AC41736D21}</ProjectGuid>
|
||||
<RootNamespace>CommonUtils</RootNamespace>
|
||||
<ProjectName>CommonUtils</ProjectName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<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>..\;..\..\;..\..\..\;..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>DbgHelp.lib;Msi.lib;Shlwapi.lib;pathcch.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</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="string_utils.h" />
|
||||
<ClInclude Include="timeutil.h" />
|
||||
<ClInclude Include="UnhandledExceptionHandler.h" />
|
||||
<ClInclude Include="winapi_error.h" />
|
||||
<ClInclude Include="window.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="elevation.cpp" />
|
||||
<ClCompile Include="exec.cpp" />
|
||||
<ClCompile Include="gpo.cpp" />
|
||||
<ClCompile Include="modulesRegistry.cpp" />
|
||||
<ClCompile Include="MsiUtils.cpp" />
|
||||
<ClCompile Include="package.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="registry.cpp" />
|
||||
<ClCompile Include="resources.cpp" />
|
||||
<ClCompile Include="UnhandledExceptionHandler.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<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')" />
|
||||
</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'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -34,6 +34,7 @@ public:
|
||||
{
|
||||
this->eventHandle = e.eventHandle;
|
||||
e.eventHandle = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
~EventLocker()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include "../logger/logger.h"
|
||||
|
||||
inline bool GetAnimationsEnabled()
|
||||
{
|
||||
BOOL enabled = 0;
|
||||
@@ -10,4 +13,4 @@ inline bool GetAnimationsEnabled()
|
||||
Logger::error("SystemParametersInfo SPI_GETCLIENTAREAANIMATION failed.");
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
|
||||
92
src/common/utils/MsiUtils.cpp
Normal file
92
src/common/utils/MsiUtils.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#include "pch.h"
|
||||
#include "MsiUtils.h"
|
||||
|
||||
#include <Msi.h>
|
||||
#include <pathcch.h>
|
||||
|
||||
namespace // Strings in this namespace should not be localized
|
||||
{
|
||||
const inline wchar_t POWER_TOYS_UPGRADE_CODE[] = L"{42B84BF7-5FBF-473B-9C8B-049DC16F7708}";
|
||||
const inline wchar_t POWER_TOYS_UPGRADE_CODE_USER[] = L"{D8B559DB-4C98-487A-A33F-50A8EEE42726}";
|
||||
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;
|
||||
}
|
||||
@@ -3,95 +3,10 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <Windows.h>
|
||||
#include <pathcch.h>
|
||||
#include <Msi.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace // Strings in this namespace should not be localized
|
||||
{
|
||||
const inline wchar_t POWER_TOYS_UPGRADE_CODE[] = L"{42B84BF7-5FBF-473B-9C8B-049DC16F7708}";
|
||||
const inline wchar_t POWER_TOYS_UPGRADE_CODE_USER[] = L"{D8B559DB-4C98-487A-A33F-50A8EEE42726}";
|
||||
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;
|
||||
}
|
||||
// Implementations in MsiUtils.cpp
|
||||
std::optional<std::wstring> GetMsiPackageInstalledPath(bool perUser);
|
||||
std::wstring GetMsiPackagePath();
|
||||
|
||||
@@ -7,7 +7,19 @@ namespace ProcessWaiter
|
||||
{
|
||||
void OnProcessTerminate(std::wstring parent_pid, std::function<void(DWORD)> callback)
|
||||
{
|
||||
DWORD pid = std::stol(parent_pid);
|
||||
DWORD pid = 0;
|
||||
try
|
||||
{
|
||||
pid = std::stol(parent_pid);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
if (callback)
|
||||
{
|
||||
callback(ERROR_INVALID_PARAMETER);
|
||||
}
|
||||
return;
|
||||
}
|
||||
std::thread([=]() {
|
||||
HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, pid);
|
||||
if (process != nullptr)
|
||||
@@ -15,17 +27,26 @@ namespace ProcessWaiter
|
||||
if (WaitForSingleObject(process, INFINITE) == WAIT_OBJECT_0)
|
||||
{
|
||||
CloseHandle(process);
|
||||
callback(ERROR_SUCCESS);
|
||||
if (callback)
|
||||
{
|
||||
callback(ERROR_SUCCESS);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CloseHandle(process);
|
||||
callback(GetLastError());
|
||||
if (callback)
|
||||
{
|
||||
callback(GetLastError());
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
callback(GetLastError());
|
||||
if (callback)
|
||||
{
|
||||
callback(GetLastError());
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
209
src/common/utils/UnhandledExceptionHandler.cpp
Normal file
209
src/common/utils/UnhandledExceptionHandler.cpp
Normal file
@@ -0,0 +1,209 @@
|
||||
#include "pch.h"
|
||||
#include "UnhandledExceptionHandler.h"
|
||||
|
||||
#include <DbgHelp.h>
|
||||
#include <signal.h>
|
||||
#include <sstream>
|
||||
|
||||
#include "winapi_error.h"
|
||||
#include "../logger/logger.h"
|
||||
|
||||
static BOOLEAN processingException = FALSE;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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")";
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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(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");
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,9 @@
|
||||
|
||||
#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;
|
||||
#include <string>
|
||||
|
||||
// Small inline functions that should stay in the header
|
||||
static inline const char* exceptionDescription(const DWORD& code)
|
||||
{
|
||||
switch (code)
|
||||
@@ -80,201 +74,12 @@ inline int GetFilenameStart(wchar_t* path)
|
||||
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");
|
||||
}
|
||||
}
|
||||
// Implementations in UnhandledExceptionHandler.cpp
|
||||
std::wstring GetModuleName(HANDLE process, const STACKFRAME64& stack);
|
||||
std::wstring GetName(HANDLE process, const STACKFRAME64& stack);
|
||||
std::wstring GetLine(HANDLE process, const STACKFRAME64& stack);
|
||||
void LogStackTrace();
|
||||
LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info);
|
||||
void AbortHandler(int signal_number);
|
||||
void InitSymbols();
|
||||
void InitUnhandledExceptionHandler(void);
|
||||
|
||||
@@ -31,6 +31,10 @@ public:
|
||||
|
||||
HRESULT __stdcall CreateInstance(IUnknown* punkOuter, const IID& riid, void** ppv)
|
||||
{
|
||||
if (!ppv)
|
||||
{
|
||||
return E_POINTER;
|
||||
}
|
||||
*ppv = nullptr;
|
||||
|
||||
if (punkOuter)
|
||||
@@ -55,4 +59,4 @@ public:
|
||||
|
||||
private:
|
||||
std::atomic<long> _refCount;
|
||||
};
|
||||
};
|
||||
|
||||
491
src/common/utils/elevation.cpp
Normal file
491
src/common/utils/elevation.cpp
Normal file
@@ -0,0 +1,491 @@
|
||||
#include "pch.h"
|
||||
#include "elevation.h"
|
||||
|
||||
#include <shldisp.h>
|
||||
#include <exdisp.h>
|
||||
#include <comdef.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/utils/process_path.h>
|
||||
#include <common/utils/processApi.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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
bool ShellExecuteFromExplorer(
|
||||
PCWSTR pszFile,
|
||||
PCWSTR pszParameters,
|
||||
PCWSTR workingDir)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
HANDLE run_as_different_user(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"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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 = 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -4,153 +4,25 @@
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
#include <sddl.h>
|
||||
#include <shldisp.h>
|
||||
#include <shlobj.h>
|
||||
#include <exdisp.h>
|
||||
#include <atlbase.h>
|
||||
#include <stdlib.h>
|
||||
#include <comdef.h>
|
||||
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/utils/process_path.h>
|
||||
#include <common/utils/processApi.h>
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 26471 26492 26493 26497)
|
||||
#include <wil/resource.h>
|
||||
#pragma warning(pop)
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Forward declarations - implementations in elevation.cpp
|
||||
bool FindDesktopFolderView(REFIID riid, void** ppv);
|
||||
bool GetDesktopAutomationObject(REFIID riid, void** ppv);
|
||||
bool ShellExecuteFromExplorer(
|
||||
PCWSTR pszFile,
|
||||
PCWSTR pszParameters = nullptr,
|
||||
PCWSTR workingDir = L"");
|
||||
|
||||
// Returns true if the current process is running with elevated privileges
|
||||
inline bool is_process_elevated(const bool use_cached_value = true)
|
||||
@@ -207,191 +79,16 @@ inline bool drop_elevated_privileges()
|
||||
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;
|
||||
}
|
||||
// Run command as different user, returns process handle if succeeded
|
||||
HANDLE run_as_different_user(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true);
|
||||
|
||||
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 if succeeded
|
||||
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;
|
||||
}
|
||||
bool RunNonElevatedEx(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir);
|
||||
|
||||
struct ProcessInfo
|
||||
{
|
||||
@@ -399,172 +96,14 @@ 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;
|
||||
}
|
||||
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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
bool IsProcessOfWindowElevated(HWND window);
|
||||
|
||||
101
src/common/utils/exec.cpp
Normal file
101
src/common/utils/exec.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#include "pch.h"
|
||||
#include "exec.h"
|
||||
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 26471 26492 26493 26497)
|
||||
#include <wil/resource.h>
|
||||
#pragma warning(pop)
|
||||
|
||||
#include <array>
|
||||
|
||||
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;
|
||||
}
|
||||
// Timeout
|
||||
[[fallthrough]];
|
||||
default:
|
||||
goto exit;
|
||||
}
|
||||
}
|
||||
exit:
|
||||
CancelIo(readPipe.get());
|
||||
return childOutput;
|
||||
}
|
||||
@@ -3,106 +3,8 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
|
||||
// disable warning 26471 - Don't use reinterpret_cast. A cast from void* can use static_cast
|
||||
// disable warning 26492 - Don't use const_cast to cast away const
|
||||
// disable warning 26493 - Don't use C-style casts
|
||||
// Disable 26497 for winrt - This function function-name could be marked constexpr if compile-time evaluation is desired.
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 26471 26492 26493 26497)
|
||||
#include <wil/resource.h>
|
||||
#pragma warning(pop)
|
||||
|
||||
#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;
|
||||
}
|
||||
// Implementation in exec.cpp
|
||||
std::optional<std::string> exec_and_read_output(const std::wstring_view command, DWORD timeout_ms = 30000);
|
||||
|
||||
176
src/common/utils/gpo.cpp
Normal file
176
src/common/utils/gpo.cpp
Normal file
@@ -0,0 +1,176 @@
|
||||
#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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ namespace powertoys_gpo
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_COLOR_PICKER = L"ConfigureEnabledUtilityColorPicker";
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_CROP_AND_LOCK = L"ConfigureEnabledUtilityCropAndLock";
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_LIGHT_SWITCH = L"ConfigureEnabledUtilityLightSwitch";
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_POWER_DISPLAY = L"ConfigureEnabledUtilityPowerDisplay";
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_FANCYZONES = L"ConfigureEnabledUtilityFancyZones";
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_FILE_LOCKSMITH = L"ConfigureEnabledUtilityFileLocksmith";
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_SVG_PREVIEW = L"ConfigureEnabledUtilityFileExplorerSVGPreview";
|
||||
@@ -103,179 +104,12 @@ namespace powertoys_gpo
|
||||
const std::wstring POLICY_NEW_PLUS_HIDE_TEMPLATE_FILENAME_EXTENSION = L"NewPlusHideTemplateFilenameExtension";
|
||||
const std::wstring POLICY_NEW_PLUS_REPLACE_VARIABLES = L"NewPlusReplaceVariablesInTemplateFilenames";
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
#pragma endregion ReadRegistryMethods
|
||||
// Methods used for reading the registry - declarations
|
||||
// Implementations are in gpo.cpp
|
||||
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);
|
||||
|
||||
// Utility enabled state policies
|
||||
// (Always use 'getUtilityEnabledValue()'.)
|
||||
@@ -310,6 +144,11 @@ namespace powertoys_gpo
|
||||
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_LIGHT_SWITCH);
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getConfiguredPowerDisplayEnabledValue()
|
||||
{
|
||||
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_POWER_DISPLAY);
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getConfiguredFancyZonesEnabledValue()
|
||||
{
|
||||
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_FANCYZONES);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <filesystem>
|
||||
#include <common/version/version.h>
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
#include "../logger/logger.h"
|
||||
|
||||
namespace LoggerHelpers
|
||||
{
|
||||
|
||||
83
src/common/utils/modulesRegistry.cpp
Normal file
83
src/common/utils/modulesRegistry.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "pch.h"
|
||||
#include "modulesRegistry.h"
|
||||
|
||||
#include <common/utils/json.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace NonLocalizable
|
||||
{
|
||||
const static wchar_t* MONACO_LANGUAGES_FILE_NAME = L"Assets\\Monaco\\monaco_languages.json";
|
||||
const static wchar_t* ListID = L"list";
|
||||
const static wchar_t* ExtensionsID = L"extensions";
|
||||
}
|
||||
|
||||
registry::ChangeSet getMonacoPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
|
||||
// Set up a list of extensions for the preview handler to take over
|
||||
std::vector<std::wstring> extensions;
|
||||
|
||||
// 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;
|
||||
|
||||
std::wstring languagesFilePath = fs::path{ installationDir } / NonLocalizable::MONACO_LANGUAGES_FILE_NAME;
|
||||
auto json = json::from_file(languagesFilePath);
|
||||
|
||||
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);
|
||||
|
||||
for (uint32_t j = 0; j < extensionsList.Size(); ++j)
|
||||
{
|
||||
auto extension = extensionsList.GetStringAt(j);
|
||||
|
||||
// Ignore extensions in the exclusion list
|
||||
IsExcluded = false;
|
||||
|
||||
for (std::wstring k : ExtExclusions)
|
||||
{
|
||||
if (std::wstring{ extension } == k)
|
||||
{
|
||||
IsExcluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (IsExcluded)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
extensions.push_back(std::wstring{ extension });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -2,17 +2,12 @@
|
||||
|
||||
#include "registry.h"
|
||||
|
||||
#include <common/utils/json.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace NonLocalizable
|
||||
{
|
||||
const static wchar_t* MONACO_LANGUAGES_FILE_NAME = L"Assets\\Monaco\\monaco_languages.json";
|
||||
const static wchar_t* ListID = L"list";
|
||||
const static wchar_t* ExtensionsID = L"extensions";
|
||||
const static std::vector<std::wstring> ExtSVG = { L".svg" };
|
||||
const static std::vector<std::wstring> ExtMarkdown = { L".md", L".markdown", L".mdown", L".mkdn", L".mkd", L".mdwn", L".mdtxt", L".mdtext" };
|
||||
const static std::vector<std::wstring> ExtPDF = { L".pdf" };
|
||||
@@ -53,73 +48,8 @@ inline registry::ChangeSet getMdPreviewHandlerChangeSet(const std::wstring insta
|
||||
NonLocalizable::ExtMarkdown);
|
||||
}
|
||||
|
||||
inline registry::ChangeSet getMonacoPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
|
||||
// Set up a list of extensions for the preview handler to take over
|
||||
std::vector<std::wstring> extensions;
|
||||
|
||||
// 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;
|
||||
|
||||
std::wstring languagesFilePath = fs::path{ installationDir } / NonLocalizable::MONACO_LANGUAGES_FILE_NAME;
|
||||
auto json = json::from_file(languagesFilePath);
|
||||
|
||||
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);
|
||||
|
||||
for (uint32_t j = 0; j < extensionsList.Size(); ++j)
|
||||
{
|
||||
auto extension = extensionsList.GetStringAt(j);
|
||||
|
||||
// Ignore extensions in the exclusion list
|
||||
IsExcluded = false;
|
||||
|
||||
for (std::wstring k : ExtExclusions)
|
||||
{
|
||||
if (std::wstring{ extension } == k)
|
||||
{
|
||||
IsExcluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (IsExcluded)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
extensions.push_back(std::wstring{ extension });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
// Implementation in modulesRegistry.cpp
|
||||
registry::ChangeSet getMonacoPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser);
|
||||
|
||||
inline registry::ChangeSet getPdfPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
|
||||
397
src/common/utils/package.cpp
Normal file
397
src/common/utils/package.cpp
Normal file
@@ -0,0 +1,397 @@
|
||||
#include "pch.h"
|
||||
#include "package.h"
|
||||
|
||||
#include <appxpackaging.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include "../logger/logger.h"
|
||||
|
||||
namespace package
|
||||
{
|
||||
using Microsoft::WRL::ComPtr;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
ComPtr<IAppxFactory> factory;
|
||||
ComPtr<IStream> stream;
|
||||
ComPtr<IAppxPackageReader> reader;
|
||||
ComPtr<IAppxManifestReader> manifest;
|
||||
ComPtr<IAppxManifestPackageId> packageId;
|
||||
|
||||
HRESULT hr = CoCreateInstance(__uuidof(AppxFactory), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&factory));
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
hr = SHCreateStreamOnFileEx(appxPath.c_str(), STGM_READ | STGM_SHARE_DENY_WRITE, FILE_ATTRIBUTE_NORMAL, FALSE, nullptr, &stream);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
hr = factory->CreatePackageReader(stream.Get(), &reader);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
hr = reader->GetManifest(&manifest);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
hr = manifest->GetPackageId(&packageId);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by package version in descending order (newest first)
|
||||
std::sort(matchedFiles.begin(), matchedFiles.end(), [](const std::wstring& a, const std::wstring& b) {
|
||||
std::wstring nameA, nameB;
|
||||
PACKAGE_VERSION versionA{}, versionB{};
|
||||
|
||||
bool gotA = GetPackageNameAndVersionFromAppx(a, nameA, versionA);
|
||||
bool gotB = GetPackageNameAndVersionFromAppx(b, nameB, versionB);
|
||||
|
||||
// Files that failed to parse go to the end
|
||||
if (!gotA)
|
||||
return false;
|
||||
if (!gotB)
|
||||
return true;
|
||||
|
||||
// Compare versions: Major, Minor, Build, Revision (descending)
|
||||
if (versionA.Major != versionB.Major)
|
||||
return versionA.Major > versionB.Major;
|
||||
if (versionA.Minor != versionB.Minor)
|
||||
return versionA.Minor > versionB.Minor;
|
||||
if (versionA.Build != versionB.Build)
|
||||
return versionA.Build > versionB.Build;
|
||||
return versionA.Revision > versionB.Revision;
|
||||
});
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
Logger::error("An error occurred while searching for MSIX files: " + std::string(ex.what()));
|
||||
}
|
||||
|
||||
return matchedFiles;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,12 @@
|
||||
#include <Windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <appxpackaging.h>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <Shlwapi.h>
|
||||
#include <wrl/client.h>
|
||||
#include <vector>
|
||||
|
||||
#include <winrt/Windows.ApplicationModel.h>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
@@ -21,10 +19,16 @@
|
||||
|
||||
namespace package
|
||||
{
|
||||
using namespace winrt::Windows::Foundation;
|
||||
using namespace winrt::Windows::ApplicationModel;
|
||||
using namespace winrt::Windows::Management::Deployment;
|
||||
using Microsoft::WRL::ComPtr;
|
||||
using winrt::Windows::ApplicationModel::Package;
|
||||
using winrt::Windows::Foundation::IAsyncOperationWithProgress;
|
||||
using winrt::Windows::Foundation::AsyncStatus;
|
||||
using winrt::Windows::Foundation::Uri;
|
||||
using winrt::Windows::Foundation::Collections::IVector;
|
||||
using winrt::Windows::Management::Deployment::AddPackageOptions;
|
||||
using winrt::Windows::Management::Deployment::DeploymentOptions;
|
||||
using winrt::Windows::Management::Deployment::DeploymentProgress;
|
||||
using winrt::Windows::Management::Deployment::DeploymentResult;
|
||||
using winrt::Windows::Management::Deployment::PackageManager;
|
||||
|
||||
inline BOOL IsWin11OrGreater()
|
||||
{
|
||||
@@ -83,85 +87,11 @@ namespace package
|
||||
bool _initialized;
|
||||
};
|
||||
|
||||
inline bool GetPackageNameAndVersionFromAppx(
|
||||
// Implementations in package.cpp
|
||||
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;
|
||||
}
|
||||
|
||||
ComPtr<IAppxFactory> factory;
|
||||
ComPtr<IStream> stream;
|
||||
ComPtr<IAppxPackageReader> reader;
|
||||
ComPtr<IAppxManifestReader> manifest;
|
||||
ComPtr<IAppxManifestPackageId> packageId;
|
||||
|
||||
HRESULT hr = CoCreateInstance(__uuidof(AppxFactory), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&factory));
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
hr = SHCreateStreamOnFileEx(appxPath.c_str(), STGM_READ | STGM_SHARE_DENY_WRITE, FILE_ATTRIBUTE_NORMAL, FALSE, nullptr, &stream);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
hr = factory->CreatePackageReader(stream.Get(), &reader);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
hr = reader->GetManifest(&manifest);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
hr = manifest->GetPackageId(&packageId);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
PACKAGE_VERSION& outVersion);
|
||||
|
||||
inline std::optional<Package> GetRegisteredPackage(std::wstring packageDisplayName, bool checkVersion)
|
||||
{
|
||||
@@ -190,308 +120,10 @@ namespace package
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by package version in descending order (newest first)
|
||||
std::sort(matchedFiles.begin(), matchedFiles.end(), [](const std::wstring& a, const std::wstring& b) {
|
||||
std::wstring nameA, nameB;
|
||||
PACKAGE_VERSION versionA{}, versionB{};
|
||||
|
||||
bool gotA = GetPackageNameAndVersionFromAppx(a, nameA, versionA);
|
||||
bool gotB = GetPackageNameAndVersionFromAppx(b, nameB, versionB);
|
||||
|
||||
// Files that failed to parse go to the end
|
||||
if (!gotA)
|
||||
return false;
|
||||
if (!gotB)
|
||||
return true;
|
||||
|
||||
// Compare versions: Major, Minor, Build, Revision (descending)
|
||||
if (versionA.Major != versionB.Major)
|
||||
return versionA.Major > versionB.Major;
|
||||
if (versionA.Minor != versionB.Minor)
|
||||
return versionA.Minor > versionB.Minor;
|
||||
if (versionA.Build != versionB.Build)
|
||||
return versionA.Build > versionB.Build;
|
||||
return versionA.Revision > versionB.Revision;
|
||||
});
|
||||
}
|
||||
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;
|
||||
}
|
||||
// Implementations in package.cpp
|
||||
bool RegisterSparsePackage(const std::wstring& externalLocation, const std::wstring& sparsePkgPath);
|
||||
bool UnRegisterPackage(const std::wstring& pkgDisplayName);
|
||||
std::vector<std::wstring> FindMsixFile(const std::wstring& directoryPath, bool recursive);
|
||||
bool IsPackageSatisfied(const std::wstring& appxPath);
|
||||
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"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user