mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-11 06:47:00 +01:00
Compare commits
51 Commits
dev/vanzue
...
shawn/impr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca4b52e8c2 | ||
|
|
be06f081d1 | ||
|
|
7cb41d8fe3 | ||
|
|
73dda5a08d | ||
|
|
d48338bad3 | ||
|
|
fd19168883 | ||
|
|
db9f8d555e | ||
|
|
be90b587da | ||
|
|
a2cd47f36c | ||
|
|
3167145d42 | ||
|
|
0899961e56 | ||
|
|
8a7503e7dc | ||
|
|
4704e3edb8 | ||
|
|
4aec8f9d0e | ||
|
|
961a65f470 | ||
|
|
876130c3cd | ||
|
|
082ba75ec7 | ||
|
|
8ed090d38e | ||
|
|
b9de59ce64 | ||
|
|
72fc8288eb | ||
|
|
9c58574484 | ||
|
|
08d4689ec5 | ||
|
|
03d1dfca2d | ||
|
|
9086995eeb | ||
|
|
19c9b4e1fd | ||
|
|
d9709b2b91 | ||
|
|
03aec1a9a7 | ||
|
|
335dcbaae8 | ||
|
|
6515985823 | ||
|
|
36a64cae90 | ||
|
|
30f832ac65 | ||
|
|
e0c0e7bb76 | ||
|
|
dfede67993 | ||
|
|
1438a628ad | ||
|
|
1b6b446915 | ||
|
|
78b0139bc3 | ||
|
|
709c4bbf6b | ||
|
|
bb3435322f | ||
|
|
d43e1d8cb2 | ||
|
|
1a145fd136 | ||
|
|
673cd5aba3 | ||
|
|
97997035f7 | ||
|
|
59962ffd3a | ||
|
|
3f106344b3 | ||
|
|
ab531b2620 | ||
|
|
48e95caf39 | ||
|
|
d87dde132d | ||
|
|
534c411fd8 | ||
|
|
afeeea671f | ||
|
|
f1e045751a | ||
|
|
8682d0f54d |
3
.github/actions/spell-check/allow/code.txt
vendored
3
.github/actions/spell-check/allow/code.txt
vendored
@@ -330,6 +330,9 @@ HHH
|
||||
riday
|
||||
YYY
|
||||
|
||||
# Unicode
|
||||
precomposed
|
||||
|
||||
# GitHub issue/PR commands
|
||||
azp
|
||||
feedbackhub
|
||||
|
||||
47
.github/actions/spell-check/expect.txt
vendored
47
.github/actions/spell-check/expect.txt
vendored
@@ -215,7 +215,9 @@ cim
|
||||
CImage
|
||||
cla
|
||||
CLASSDC
|
||||
classmethod
|
||||
CLASSNOTAVAILABLE
|
||||
CLEARTYPE
|
||||
clickable
|
||||
clickonce
|
||||
CLIENTEDGE
|
||||
@@ -253,6 +255,7 @@ colorhistory
|
||||
colorhistorylimit
|
||||
COLORKEY
|
||||
colorref
|
||||
Convs
|
||||
comctl
|
||||
comdlg
|
||||
comexp
|
||||
@@ -362,7 +365,9 @@ DEFAULTFLAGS
|
||||
DEFAULTICON
|
||||
defaultlib
|
||||
DEFAULTONLY
|
||||
DEFAULTSIZE
|
||||
DEFAULTTONEAREST
|
||||
Defaulttonearest
|
||||
DEFAULTTONULL
|
||||
DEFAULTTOPRIMARY
|
||||
DEFERERASE
|
||||
@@ -391,6 +396,7 @@ devpal
|
||||
dfx
|
||||
DIALOGEX
|
||||
digicert
|
||||
diffs
|
||||
DINORMAL
|
||||
DISABLEASACTIONKEY
|
||||
DISABLENOSCROLL
|
||||
@@ -528,9 +534,12 @@ eyetracker
|
||||
FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
FARPROC
|
||||
fdw
|
||||
fdx
|
||||
FErase
|
||||
fesf
|
||||
FFFF
|
||||
FInc
|
||||
Figma
|
||||
FILEEXPLORER
|
||||
fileexploreraddons
|
||||
@@ -572,6 +581,7 @@ formatetc
|
||||
FORPARSING
|
||||
foundrylocal
|
||||
FRAMECHANGED
|
||||
FRestore
|
||||
frm
|
||||
FROMTOUCH
|
||||
fsanitize
|
||||
@@ -606,6 +616,7 @@ GETSCREENSAVERRUNNING
|
||||
GETSECKEY
|
||||
GETSTICKYKEYS
|
||||
GETTEXTLENGTH
|
||||
gfx
|
||||
GHND
|
||||
gitmodules
|
||||
GMEM
|
||||
@@ -656,6 +667,7 @@ hdwwiz
|
||||
Helpline
|
||||
helptext
|
||||
HGFE
|
||||
hgdiobj
|
||||
hglobal
|
||||
hhk
|
||||
HHmmssfff
|
||||
@@ -703,7 +715,7 @@ hotlight
|
||||
hotspot
|
||||
HPAINTBUFFER
|
||||
HRAWINPUT
|
||||
HREDRAW
|
||||
hredraw
|
||||
hres
|
||||
hresult
|
||||
hrgn
|
||||
@@ -820,9 +832,11 @@ ITHUMBNAIL
|
||||
IUI
|
||||
IUWP
|
||||
IWIC
|
||||
jeli
|
||||
jfif
|
||||
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
||||
jjw
|
||||
JOBOBJECT
|
||||
jobject
|
||||
jpe
|
||||
jpnime
|
||||
@@ -856,6 +870,7 @@ lastcodeanalysissucceeded
|
||||
LASTEXITCODE
|
||||
LAYOUTRTL
|
||||
lbl
|
||||
Lbuttondown
|
||||
LCh
|
||||
lcid
|
||||
LCIDTo
|
||||
@@ -881,7 +896,7 @@ LINKOVERLAY
|
||||
LINQTo
|
||||
listview
|
||||
LIVEDRAW
|
||||
LIVEZOOM
|
||||
livezoom
|
||||
LLKH
|
||||
llkhf
|
||||
LMEM
|
||||
@@ -910,6 +925,7 @@ LPBITMAPINFOHEADER
|
||||
LPCFHOOKPROC
|
||||
LPCITEMIDLIST
|
||||
LPCLSID
|
||||
lpch
|
||||
lpcmi
|
||||
LPCMINVOKECOMMANDINFO
|
||||
LPCREATESTRUCT
|
||||
@@ -934,6 +950,7 @@ lptpm
|
||||
LPTR
|
||||
LPTSTR
|
||||
lpv
|
||||
LPrivate
|
||||
LPW
|
||||
lpwcx
|
||||
lpwndpl
|
||||
@@ -976,12 +993,14 @@ maxversiontested
|
||||
mber
|
||||
MBM
|
||||
MBR
|
||||
Mbuttondown
|
||||
MDICHILD
|
||||
MDL
|
||||
mdtext
|
||||
mdtxt
|
||||
mdwn
|
||||
meme
|
||||
mcp
|
||||
memicmp
|
||||
MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
@@ -1072,6 +1091,8 @@ muxxc
|
||||
muxxh
|
||||
MVPs
|
||||
mvvm
|
||||
myorg
|
||||
myrepo
|
||||
MVVMTK
|
||||
MWBEx
|
||||
MYICON
|
||||
@@ -1218,6 +1239,7 @@ opensource
|
||||
openxmlformats
|
||||
ollama
|
||||
onnx
|
||||
openurl
|
||||
OPTIMIZEFORINVOKE
|
||||
ORPHANEDDIALOGTITLE
|
||||
ORSCANS
|
||||
@@ -1295,6 +1317,7 @@ phwnd
|
||||
pici
|
||||
pidl
|
||||
PIDLIST
|
||||
PII
|
||||
pinfo
|
||||
pinvoke
|
||||
pipename
|
||||
@@ -1343,6 +1366,7 @@ ppv
|
||||
ppwsz
|
||||
prc
|
||||
Prefixer
|
||||
Premul
|
||||
prependpath
|
||||
prepopulate
|
||||
prevhost
|
||||
@@ -1428,6 +1452,7 @@ RAWINPUTHEADER
|
||||
RAWMODE
|
||||
RAWPATH
|
||||
rbhid
|
||||
Rbuttondown
|
||||
rclsid
|
||||
RCZOOMIT
|
||||
remotedesktop
|
||||
@@ -1463,6 +1488,7 @@ remoteip
|
||||
Removelnk
|
||||
renamable
|
||||
RENAMEONCOLLISION
|
||||
RENDERFULLCONTENT
|
||||
reparented
|
||||
reparenting
|
||||
reportfileaccesses
|
||||
@@ -1492,6 +1518,7 @@ riid
|
||||
RKey
|
||||
RNumber
|
||||
rop
|
||||
rollups
|
||||
ROUNDSMALL
|
||||
ROWSETEXT
|
||||
rpcrt
|
||||
@@ -1576,7 +1603,7 @@ sharpfuzz
|
||||
SHCNE
|
||||
SHCNF
|
||||
SHCONTF
|
||||
Shcore
|
||||
shcore
|
||||
shellapi
|
||||
SHELLDETAILS
|
||||
SHELLDLL
|
||||
@@ -1636,6 +1663,7 @@ SKIPOWNPROCESS
|
||||
sku
|
||||
SLGP
|
||||
sln
|
||||
slnf
|
||||
slnx
|
||||
SMALLICON
|
||||
smartphone
|
||||
@@ -1674,6 +1702,7 @@ srw
|
||||
srwlock
|
||||
sse
|
||||
ssf
|
||||
sszzz
|
||||
STACKFRAME
|
||||
stackoverflow
|
||||
STARTF
|
||||
@@ -1684,6 +1713,7 @@ STARTUPINFOW
|
||||
startupscreen
|
||||
STATFLAG
|
||||
STATICEDGE
|
||||
staticmethod
|
||||
STATSTG
|
||||
stdafx
|
||||
STDAPI
|
||||
@@ -1727,6 +1757,7 @@ svgz
|
||||
SVSI
|
||||
SWFO
|
||||
SWP
|
||||
Swp
|
||||
SWPNOSIZE
|
||||
SWPNOZORDER
|
||||
SWRESTORE
|
||||
@@ -1746,6 +1777,7 @@ syskeydown
|
||||
SYSKEYUP
|
||||
SYSLIB
|
||||
SYSMENU
|
||||
Sysmenu
|
||||
systemai
|
||||
SYSTEMAPPS
|
||||
SYSTEMMODAL
|
||||
@@ -1791,7 +1823,6 @@ TILEDWINDOW
|
||||
TILLSON
|
||||
timedate
|
||||
timediff
|
||||
timeunion
|
||||
timeutil
|
||||
TITLEBARINFO
|
||||
Titlecase
|
||||
@@ -1801,7 +1832,6 @@ tlb
|
||||
tlbimp
|
||||
tlc
|
||||
tmain
|
||||
tml
|
||||
TNP
|
||||
Toolhelp
|
||||
toolwindow
|
||||
@@ -1850,6 +1880,7 @@ uild
|
||||
uitests
|
||||
UITo
|
||||
ULONGLONG
|
||||
Ultrawide
|
||||
ums
|
||||
UMax
|
||||
UMin
|
||||
@@ -1897,7 +1928,7 @@ valuegenerator
|
||||
variantassignment
|
||||
VARTYPE
|
||||
vcamp
|
||||
VCENTER
|
||||
vcenter
|
||||
vcgtq
|
||||
VCINSTALLDIR
|
||||
Vcpkg
|
||||
@@ -1929,7 +1960,7 @@ vorrq
|
||||
VOS
|
||||
vpaddlq
|
||||
vqsubq
|
||||
VREDRAW
|
||||
vredraw
|
||||
vreinterpretq
|
||||
VSC
|
||||
VSCBD
|
||||
@@ -2072,6 +2103,7 @@ Wwanpp
|
||||
xap
|
||||
XAxis
|
||||
XButton
|
||||
Xbuttondown
|
||||
xclip
|
||||
xcopy
|
||||
XDeployment
|
||||
@@ -2097,6 +2129,7 @@ xstyler
|
||||
XTimer
|
||||
XUP
|
||||
XVIRTUALSCREEN
|
||||
XXL
|
||||
xxxxxx
|
||||
YAxis
|
||||
ycombinator
|
||||
|
||||
3
.github/actions/spell-check/patterns.txt
vendored
3
.github/actions/spell-check/patterns.txt
vendored
@@ -273,3 +273,6 @@ St&yle
|
||||
# Usernames with numbers
|
||||
# 0x6f677548 is user name but user folder causes a flag
|
||||
\bx6f677548\b
|
||||
|
||||
# Microsoft Store URLs and product IDs
|
||||
ms-windows-store://\S+
|
||||
|
||||
80
.github/copilot-instructions.md
vendored
80
.github/copilot-instructions.md
vendored
@@ -1,59 +1,45 @@
|
||||
---
|
||||
description: PowerToys AI contributor guidance.
|
||||
applyTo: pullRequests
|
||||
description: 'PowerToys AI contributor guidance'
|
||||
---
|
||||
|
||||
# PowerToys - Copilot guide (concise)
|
||||
# PowerToys – Copilot Instructions
|
||||
|
||||
This is the top-level guide for AI changes. Keep edits small, follow existing patterns, and cite exact paths in PRs.
|
||||
Concise guidance for AI contributions. For complete details, see [AGENTS.md](../AGENTS.md).
|
||||
|
||||
# Repo map (1-line per area)
|
||||
- Core apps: `src/runner/**` (tray/loader), `src/settings-ui/**` (Settings app)
|
||||
- Shared libs: `src/common/**`
|
||||
- Modules: `src/modules/*` (one per utility; Command Palette in `src/modules/cmdpal/**`)
|
||||
- Build tools/docs: `tools/**`, `doc/devdocs/**`
|
||||
## Quick Reference
|
||||
|
||||
# Build and test (defaults)
|
||||
- Prerequisites: Visual Studio 2022 17.4+, minimal Windows 10 1803+.
|
||||
- Build discipline:
|
||||
- One terminal per operation (build -> test). Do not switch or open new ones mid-flow.
|
||||
- After making changes, `cd` to the project folder that changed (`.csproj`/`.vcxproj`).
|
||||
- Use scripts to build, synchronously block and wait in foreground for completion: `tools/build/build.ps1|.cmd` (current folder), `build-essentials.*` (once per brand new build for missing nuget packages).
|
||||
- Treat build exit code 0 as success; any non-zero exit code is a failure. Read the errors log in the build folder (such as `build.*.*.errors.log`) and surface problems.
|
||||
- Do not start tests or launch Runner until the previous step succeeded.
|
||||
- Tests (fast and targeted):
|
||||
- Find the test project by product code prefix (for example FancyZones, AdvancedPaste). Look for a sibling folder or one to two levels up named like `<Product>*UnitTests` or `<Product>*UITests`.
|
||||
- Build the test project, wait for exit, then run only those tests via VS Test Explorer or `vstest.console.exe` with filters. Avoid `dotnet test` in this repo.
|
||||
- Add or adjust tests when changing behavior; if skipped, state why (for example comment-only or string rename).
|
||||
- **Build**: `tools\build\build-essentials.cmd` (first time), then `tools\build\build.cmd`
|
||||
- **Tests**: Find `<Product>*UnitTests` project, build it, run via VS Test Explorer
|
||||
- **Exit code 0 = success** – do not proceed if build fails
|
||||
|
||||
# Pull requests (expectations)
|
||||
- Atomic: one logical change; no drive-by refactors.
|
||||
- Describe: problem, approach, risk, test evidence.
|
||||
- List: touched paths if not obvious.
|
||||
## Key Rules
|
||||
|
||||
# When to ask for clarification
|
||||
- Ambiguous spec after scanning relevant docs (see below).
|
||||
- Cross-module impact (shared enum or struct) not clear.
|
||||
- Security, elevation, or installer changes.
|
||||
- One terminal per operation (build → test)
|
||||
- Atomic PRs: one logical change, no drive-by refactors
|
||||
- Add tests when changing behavior
|
||||
- Keep hot paths quiet (no logging in hooks/tight loops)
|
||||
|
||||
# Logging (use existing stacks)
|
||||
- C++ logging lives in `src/common/logger/**` (`Logger::info`, `Logger::warn`, `Logger::error`, `Logger::debug`). Keep hot paths quiet (hooks, tight loops).
|
||||
- C# logging goes through `ManagedCommon.Logger` (`LogInfo`, `LogWarning`, `LogError`, `LogDebug`, `LogTrace`). Some UIs use injected `ILogger` via `LoggerInstance.Logger`.
|
||||
## Style Enforcement
|
||||
|
||||
# Docs to consult
|
||||
- `tools/build/BUILD-GUIDELINES.md`
|
||||
- `doc/devdocs/core/architecture.md`
|
||||
- `doc/devdocs/core/runner.md`
|
||||
- `doc/devdocs/core/settings/readme.md`
|
||||
- `doc/devdocs/modules/readme.md`
|
||||
- C#: `src/.editorconfig`, StyleCop.Analyzers
|
||||
- C++: `src/.clang-format`
|
||||
- XAML: XamlStyler
|
||||
|
||||
# Language style rules
|
||||
- Always enforce repo analyzers: root `.editorconfig` plus any `stylecop.json`.
|
||||
- C# code follows StyleCop.Analyzers and Microsoft.CodeAnalysis.NetAnalyzers.
|
||||
- C++ code honors `.clang-format` plus `.clang-tidy` (modernize/cppcoreguidelines/readability).
|
||||
- Markdown files wrap at 80 characters and use ATX headers with fenced code blocks that include language tags.
|
||||
- YAML files indent two spaces and add comments for complex settings while keeping keys clear.
|
||||
- PowerShell scripts use Verb-Noun names and prefer single-quoted literals while documenting parameters and satisfying PSScriptAnalyzer.
|
||||
## When to Ask for Clarification
|
||||
|
||||
# Done checklist (self review before finishing)
|
||||
- Build clean? Tests updated or passed? No unintended formatting? Any new dependency? Documented skips?
|
||||
- Ambiguous spec after scanning docs
|
||||
- Cross-module impact unclear
|
||||
- Security, elevation, or installer changes
|
||||
|
||||
## Component-Specific Instructions
|
||||
|
||||
These are auto-applied based on file location:
|
||||
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md)
|
||||
- [Common Libraries](.github/instructions/common-libraries.instructions.md)
|
||||
|
||||
## Detailed Documentation
|
||||
|
||||
- [AGENTS.md](../AGENTS.md) – Full contributor guide
|
||||
- [Build Guidelines](../tools/build/BUILD-GUIDELINES.md)
|
||||
- [Architecture](../doc/devdocs/core/architecture.md)
|
||||
- [Coding Style](../doc/devdocs/development/style.md)
|
||||
|
||||
791
.github/instructions/agents.instructions.md
vendored
Normal file
791
.github/instructions/agents.instructions.md
vendored
Normal file
@@ -0,0 +1,791 @@
|
||||
---
|
||||
description: 'Guidelines for creating custom agent files for GitHub Copilot'
|
||||
applyTo: '**/*.agent.md'
|
||||
---
|
||||
|
||||
# Custom Agent File Guidelines
|
||||
|
||||
Instructions for creating effective and maintainable custom agent files that provide specialized expertise for specific development tasks in GitHub Copilot.
|
||||
|
||||
## Project Context
|
||||
|
||||
- Target audience: Developers creating custom agents for GitHub Copilot
|
||||
- File format: Markdown with YAML frontmatter
|
||||
- File naming convention: lowercase with hyphens (e.g., `test-specialist.agent.md`)
|
||||
- Location: `.github/agents/` directory (repository-level) or `agents/` directory (organization/enterprise-level)
|
||||
- Purpose: Define specialized agents with tailored expertise, tools, and instructions for specific tasks
|
||||
- Official documentation: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-custom-agents
|
||||
|
||||
## Required Frontmatter
|
||||
|
||||
Every agent file must include YAML frontmatter with the following fields:
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: 'Brief description of the agent purpose and capabilities'
|
||||
name: 'Agent Display Name'
|
||||
tools: ['read', 'edit', 'search']
|
||||
model: 'Claude Sonnet 4.5'
|
||||
target: 'vscode'
|
||||
infer: true
|
||||
---
|
||||
```
|
||||
|
||||
### Core Frontmatter Properties
|
||||
|
||||
#### **description** (REQUIRED)
|
||||
- Single-quoted string, clearly stating the agent's purpose and domain expertise
|
||||
- Should be concise (50-150 characters) and actionable
|
||||
- Example: `'Focuses on test coverage, quality, and testing best practices'`
|
||||
|
||||
#### **name** (OPTIONAL)
|
||||
- Display name for the agent in the UI
|
||||
- If omitted, defaults to filename (without `.md` or `.agent.md`)
|
||||
- Use title case and be descriptive
|
||||
- Example: `'Testing Specialist'`
|
||||
|
||||
#### **tools** (OPTIONAL)
|
||||
- List of tool names or aliases the agent can use
|
||||
- Supports comma-separated string or YAML array format
|
||||
- If omitted, agent has access to all available tools
|
||||
- See "Tool Configuration" section below for details
|
||||
|
||||
#### **model** (STRONGLY RECOMMENDED)
|
||||
- Specifies which AI model the agent should use
|
||||
- Supported in VS Code, JetBrains IDEs, Eclipse, and Xcode
|
||||
- Example: `'Claude Sonnet 4.5'`, `'gpt-4'`, `'gpt-4o'`
|
||||
- Choose based on agent complexity and required capabilities
|
||||
|
||||
#### **target** (OPTIONAL)
|
||||
- Specifies target environment: `'vscode'` or `'github-copilot'`
|
||||
- If omitted, agent is available in both environments
|
||||
- Use when agent has environment-specific features
|
||||
|
||||
#### **infer** (OPTIONAL)
|
||||
- Boolean controlling whether Copilot can automatically use this agent based on context
|
||||
- Default: `true` if omitted
|
||||
- Set to `false` to require manual agent selection
|
||||
|
||||
#### **metadata** (OPTIONAL, GitHub.com only)
|
||||
- Object with name-value pairs for agent annotation
|
||||
- Example: `metadata: { category: 'testing', version: '1.0' }`
|
||||
- Not supported in VS Code
|
||||
|
||||
#### **mcp-servers** (OPTIONAL, Organization/Enterprise only)
|
||||
- Configure MCP servers available only to this agent
|
||||
- Only supported for organization/enterprise level agents
|
||||
- See "MCP Server Configuration" section below
|
||||
|
||||
## Tool Configuration
|
||||
|
||||
### Tool Specification Strategies
|
||||
|
||||
**Enable all tools** (default):
|
||||
```yaml
|
||||
# Omit tools property entirely, or use:
|
||||
tools: ['*']
|
||||
```
|
||||
|
||||
**Enable specific tools**:
|
||||
```yaml
|
||||
tools: ['read', 'edit', 'search', 'execute']
|
||||
```
|
||||
|
||||
**Enable MCP server tools**:
|
||||
```yaml
|
||||
tools: ['read', 'edit', 'github/*', 'playwright/navigate']
|
||||
```
|
||||
|
||||
**Disable all tools**:
|
||||
```yaml
|
||||
tools: []
|
||||
```
|
||||
|
||||
### Standard Tool Aliases
|
||||
|
||||
All aliases are case-insensitive:
|
||||
|
||||
| Alias | Alternative Names | Category | Description |
|
||||
|-------|------------------|----------|-------------|
|
||||
| `execute` | shell, Bash, powershell | Shell execution | Execute commands in appropriate shell |
|
||||
| `read` | Read, NotebookRead, view | File reading | Read file contents |
|
||||
| `edit` | Edit, MultiEdit, Write, NotebookEdit | File editing | Edit and modify files |
|
||||
| `search` | Grep, Glob, search | Code search | Search for files or text in files |
|
||||
| `agent` | custom-agent, Task | Agent invocation | Invoke other custom agents |
|
||||
| `web` | WebSearch, WebFetch | Web access | Fetch web content and search |
|
||||
| `todo` | TodoWrite | Task management | Create and manage task lists (VS Code only) |
|
||||
|
||||
### Built-in MCP Server Tools
|
||||
|
||||
**GitHub MCP Server**:
|
||||
```yaml
|
||||
tools: ['github/*'] # All GitHub tools
|
||||
tools: ['github/get_file_contents', 'github/search_repositories'] # Specific tools
|
||||
```
|
||||
- All read-only tools available by default
|
||||
- Token scoped to source repository
|
||||
|
||||
**Playwright MCP Server**:
|
||||
```yaml
|
||||
tools: ['playwright/*'] # All Playwright tools
|
||||
tools: ['playwright/navigate', 'playwright/screenshot'] # Specific tools
|
||||
```
|
||||
- Configured to access localhost only
|
||||
- Useful for browser automation and testing
|
||||
|
||||
### Tool Selection Best Practices
|
||||
|
||||
- **Principle of Least Privilege**: Only enable tools necessary for the agent's purpose
|
||||
- **Security**: Limit `execute` access unless explicitly required
|
||||
- **Focus**: Fewer tools = clearer agent purpose and better performance
|
||||
- **Documentation**: Comment why specific tools are required for complex configurations
|
||||
|
||||
## Sub-Agent Invocation (Agent Orchestration)
|
||||
|
||||
Agents can invoke other agents using `runSubagent` to orchestrate multi-step workflows.
|
||||
|
||||
### How It Works
|
||||
|
||||
Include `agent` in tools list to enable sub-agent invocation:
|
||||
|
||||
```yaml
|
||||
tools: ['read', 'edit', 'search', 'agent']
|
||||
```
|
||||
|
||||
Then invoke other agents with `runSubagent`:
|
||||
|
||||
```javascript
|
||||
const result = await runSubagent({
|
||||
description: 'What this step does',
|
||||
prompt: `You are the [Specialist] specialist.
|
||||
|
||||
Context:
|
||||
- Parameter: ${parameterValue}
|
||||
- Input: ${inputPath}
|
||||
- Output: ${outputPath}
|
||||
|
||||
Task:
|
||||
1. Do the specific work
|
||||
2. Write results to output location
|
||||
3. Return summary of completion`
|
||||
});
|
||||
```
|
||||
|
||||
### Basic Pattern
|
||||
|
||||
Structure each sub-agent call with:
|
||||
|
||||
1. **description**: Clear one-line purpose of the sub-agent invocation
|
||||
2. **prompt**: Detailed instructions with substituted variables
|
||||
|
||||
The prompt should include:
|
||||
- Who the sub-agent is (specialist role)
|
||||
- What context it needs (parameters, paths)
|
||||
- What to do (concrete tasks)
|
||||
- Where to write output
|
||||
- What to return (summary)
|
||||
|
||||
### Example: Multi-Step Processing
|
||||
|
||||
```javascript
|
||||
// Step 1: Process data
|
||||
const processing = await runSubagent({
|
||||
description: 'Transform raw input data',
|
||||
prompt: `You are the Data Processor specialist.
|
||||
|
||||
Project: ${projectName}
|
||||
Input: ${basePath}/raw/
|
||||
Output: ${basePath}/processed/
|
||||
|
||||
Task:
|
||||
1. Read all files from input directory
|
||||
2. Apply transformations
|
||||
3. Write processed files to output
|
||||
4. Create summary: ${basePath}/processed/summary.md
|
||||
|
||||
Return: Number of files processed and any issues found`
|
||||
});
|
||||
|
||||
// Step 2: Analyze (depends on Step 1)
|
||||
const analysis = await runSubagent({
|
||||
description: 'Analyze processed data',
|
||||
prompt: `You are the Data Analyst specialist.
|
||||
|
||||
Project: ${projectName}
|
||||
Input: ${basePath}/processed/
|
||||
Output: ${basePath}/analysis/
|
||||
|
||||
Task:
|
||||
1. Read processed files from input
|
||||
2. Generate analysis report
|
||||
3. Write to: ${basePath}/analysis/report.md
|
||||
|
||||
Return: Key findings and identified patterns`
|
||||
});
|
||||
```
|
||||
|
||||
### Key Points
|
||||
|
||||
- **Pass variables in prompts**: Use `${variableName}` for all dynamic values
|
||||
- **Keep prompts focused**: Clear, specific tasks for each sub-agent
|
||||
- **Return summaries**: Each sub-agent should report what it accomplished
|
||||
- **Sequential execution**: Use `await` to maintain order when steps depend on each other
|
||||
- **Error handling**: Check results before proceeding to dependent steps
|
||||
|
||||
### ⚠️ Tool Availability Requirement
|
||||
|
||||
**Critical**: If a sub-agent requires specific tools (e.g., `edit`, `execute`, `search`), the orchestrator must include those tools in its own `tools` list. Sub-agents cannot access tools that aren't available to their parent orchestrator.
|
||||
|
||||
**Example**:
|
||||
```yaml
|
||||
# If your sub-agents need to edit files, execute commands, or search code
|
||||
tools: ['read', 'edit', 'search', 'execute', 'agent']
|
||||
```
|
||||
|
||||
The orchestrator's tool permissions act as a ceiling for all invoked sub-agents. Plan your tool list carefully to ensure all sub-agents have the tools they need.
|
||||
|
||||
### ⚠️ Important Limitation
|
||||
|
||||
**Sub-agent orchestration is NOT suitable for large-scale data processing.** Avoid using `runSubagent` when:
|
||||
- Processing hundreds or thousands of files
|
||||
- Handling large datasets
|
||||
- Performing bulk transformations on big codebases
|
||||
- Orchestrating more than 5-10 sequential steps
|
||||
|
||||
Each sub-agent call adds latency and context overhead. For high-volume processing, implement logic directly in a single agent instead. Use orchestration only for coordinating specialized tasks on focused, manageable datasets.
|
||||
|
||||
## Agent Prompt Structure
|
||||
|
||||
The markdown content below the frontmatter defines the agent's behavior, expertise, and instructions. Well-structured prompts typically include:
|
||||
|
||||
1. **Agent Identity and Role**: Who the agent is and its primary role
|
||||
2. **Core Responsibilities**: What specific tasks the agent performs
|
||||
3. **Approach and Methodology**: How the agent works to accomplish tasks
|
||||
4. **Guidelines and Constraints**: What to do/avoid and quality standards
|
||||
5. **Output Expectations**: Expected output format and quality
|
||||
|
||||
### Prompt Writing Best Practices
|
||||
|
||||
- **Be Specific and Direct**: Use imperative mood ("Analyze", "Generate"); avoid vague terms
|
||||
- **Define Boundaries**: Clearly state scope limits and constraints
|
||||
- **Include Context**: Explain domain expertise and reference relevant frameworks
|
||||
- **Focus on Behavior**: Describe how the agent should think and work
|
||||
- **Use Structured Format**: Headers, bullets, and lists make prompts scannable
|
||||
|
||||
## Variable Definition and Extraction
|
||||
|
||||
Agents can define dynamic parameters to extract values from user input and use them throughout the agent's behavior and sub-agent communications. This enables flexible, context-aware agents that adapt to user-provided data.
|
||||
|
||||
### When to Use Variables
|
||||
|
||||
**Use variables when**:
|
||||
- Agent behavior depends on user input
|
||||
- Need to pass dynamic values to sub-agents
|
||||
- Want to make agents reusable across different contexts
|
||||
- Require parameterized workflows
|
||||
- Need to track or reference user-provided context
|
||||
|
||||
**Examples**:
|
||||
- Extract project name from user prompt
|
||||
- Capture certification name for pipeline processing
|
||||
- Identify file paths or directories
|
||||
- Extract configuration options
|
||||
- Parse feature names or module identifiers
|
||||
|
||||
### Variable Declaration Pattern
|
||||
|
||||
Define variables section early in the agent prompt to document expected parameters:
|
||||
|
||||
```markdown
|
||||
# Agent Name
|
||||
|
||||
## Dynamic Parameters
|
||||
|
||||
- **Parameter Name**: Description and usage
|
||||
- **Another Parameter**: How it's extracted and used
|
||||
|
||||
## Your Mission
|
||||
|
||||
Process [PARAMETER_NAME] to accomplish [task].
|
||||
```
|
||||
|
||||
### Variable Extraction Methods
|
||||
|
||||
#### 1. **Explicit User Input**
|
||||
Ask the user to provide the variable if not detected in the prompt:
|
||||
|
||||
```markdown
|
||||
## Your Mission
|
||||
|
||||
Process the project by analyzing your codebase.
|
||||
|
||||
### Step 1: Identify Project
|
||||
If no project name is provided, **ASK THE USER** for:
|
||||
- Project name or identifier
|
||||
- Base path or directory location
|
||||
- Configuration type (if applicable)
|
||||
|
||||
Use this information to contextualize all subsequent tasks.
|
||||
```
|
||||
|
||||
#### 2. **Implicit Extraction from Prompt**
|
||||
Automatically extract variables from the user's natural language input:
|
||||
|
||||
```javascript
|
||||
// Example: Extract certification name from user input
|
||||
const userInput = "Process My Certification";
|
||||
|
||||
// Extract key information
|
||||
const certificationName = extractCertificationName(userInput);
|
||||
// Result: "My Certification"
|
||||
|
||||
const basePath = `certifications/${certificationName}`;
|
||||
// Result: "certifications/My Certification"
|
||||
```
|
||||
|
||||
#### 3. **Contextual Variable Resolution**
|
||||
Use file context or workspace information to derive variables:
|
||||
|
||||
```markdown
|
||||
## Variable Resolution Strategy
|
||||
|
||||
1. **From User Prompt**: First, look for explicit mentions in user input
|
||||
2. **From File Context**: Check current file name or path
|
||||
3. **From Workspace**: Use workspace folder or active project
|
||||
4. **From Settings**: Reference configuration files
|
||||
5. **Ask User**: If all else fails, request missing information
|
||||
```
|
||||
|
||||
### Using Variables in Agent Prompts
|
||||
|
||||
#### Variable Substitution in Instructions
|
||||
|
||||
Use template variables in agent prompts to make them dynamic:
|
||||
|
||||
```markdown
|
||||
# Agent Name
|
||||
|
||||
## Dynamic Parameters
|
||||
- **Project Name**: ${projectName}
|
||||
- **Base Path**: ${basePath}
|
||||
- **Output Directory**: ${outputDir}
|
||||
|
||||
## Your Mission
|
||||
|
||||
Process the **${projectName}** project located at `${basePath}`.
|
||||
|
||||
## Process Steps
|
||||
|
||||
1. Read input from: `${basePath}/input/`
|
||||
2. Process files according to project configuration
|
||||
3. Write results to: `${outputDir}/`
|
||||
4. Generate summary report
|
||||
|
||||
## Quality Standards
|
||||
|
||||
- Maintain project-specific coding standards for **${projectName}**
|
||||
- Follow directory structure: `${basePath}/[structure]`
|
||||
```
|
||||
|
||||
#### Passing Variables to Sub-Agents
|
||||
|
||||
When invoking a sub-agent, pass all context through template variables in the prompt:
|
||||
|
||||
```javascript
|
||||
// Extract and prepare variables
|
||||
const basePath = `projects/${projectName}`;
|
||||
const inputPath = `${basePath}/src/`;
|
||||
const outputPath = `${basePath}/docs/`;
|
||||
|
||||
// Pass to sub-agent with all variables substituted
|
||||
const result = await runSubagent({
|
||||
description: 'Generate project documentation',
|
||||
prompt: `You are the Documentation specialist.
|
||||
|
||||
Project: ${projectName}
|
||||
Input: ${inputPath}
|
||||
Output: ${outputPath}
|
||||
|
||||
Task:
|
||||
1. Read source files from ${inputPath}
|
||||
2. Generate comprehensive documentation
|
||||
3. Write to ${outputPath}/index.md
|
||||
4. Include code examples and usage guides
|
||||
|
||||
Return: Summary of documentation generated (file count, word count)`
|
||||
});
|
||||
```
|
||||
|
||||
The sub-agent receives all necessary context embedded in the prompt. Variables are resolved before sending the prompt, so the sub-agent works with concrete paths and values, not variable placeholders.
|
||||
|
||||
### Real-World Example: Code Review Orchestrator
|
||||
|
||||
Example of a simple orchestrator that validates code through multiple specialized agents:
|
||||
|
||||
```javascript
|
||||
async function reviewCodePipeline(repositoryName, prNumber) {
|
||||
const basePath = `projects/${repositoryName}/pr-${prNumber}`;
|
||||
|
||||
// Step 1: Security Review
|
||||
const security = await runSubagent({
|
||||
description: 'Scan for security vulnerabilities',
|
||||
prompt: `You are the Security Reviewer specialist.
|
||||
|
||||
Repository: ${repositoryName}
|
||||
PR: ${prNumber}
|
||||
Code: ${basePath}/changes/
|
||||
|
||||
Task:
|
||||
1. Scan code for OWASP Top 10 vulnerabilities
|
||||
2. Check for injection attacks, auth flaws
|
||||
3. Write findings to ${basePath}/security-review.md
|
||||
|
||||
Return: List of critical, high, and medium issues found`
|
||||
});
|
||||
|
||||
// Step 2: Test Coverage Check
|
||||
const coverage = await runSubagent({
|
||||
description: 'Verify test coverage for changes',
|
||||
prompt: `You are the Test Coverage specialist.
|
||||
|
||||
Repository: ${repositoryName}
|
||||
PR: ${prNumber}
|
||||
Changes: ${basePath}/changes/
|
||||
|
||||
Task:
|
||||
1. Analyze code coverage for modified files
|
||||
2. Identify untested critical paths
|
||||
3. Write report to ${basePath}/coverage-report.md
|
||||
|
||||
Return: Current coverage percentage and gaps`
|
||||
});
|
||||
|
||||
// Step 3: Aggregate Results
|
||||
const finalReport = await runSubagent({
|
||||
description: 'Compile all review findings',
|
||||
prompt: `You are the Review Aggregator specialist.
|
||||
|
||||
Repository: ${repositoryName}
|
||||
Reports: ${basePath}/*.md
|
||||
|
||||
Task:
|
||||
1. Read all review reports from ${basePath}/
|
||||
2. Synthesize findings into single report
|
||||
3. Determine overall verdict (APPROVE/NEEDS_FIXES/BLOCK)
|
||||
4. Write to ${basePath}/final-review.md
|
||||
|
||||
Return: Final verdict and executive summary`
|
||||
});
|
||||
|
||||
return finalReport;
|
||||
}
|
||||
```
|
||||
|
||||
This pattern applies to any orchestration scenario: extract variables, call sub-agents with clear context, await results.
|
||||
|
||||
|
||||
### Variable Best Practices
|
||||
|
||||
#### 1. **Clear Documentation**
|
||||
Always document what variables are expected:
|
||||
|
||||
```markdown
|
||||
## Required Variables
|
||||
- **projectName**: The name of the project (string, required)
|
||||
- **basePath**: Root directory for project files (path, required)
|
||||
|
||||
## Optional Variables
|
||||
- **mode**: Processing mode - quick/standard/detailed (enum, default: standard)
|
||||
- **outputFormat**: Output format - markdown/json/html (enum, default: markdown)
|
||||
|
||||
## Derived Variables
|
||||
- **outputDir**: Automatically set to ${basePath}/output
|
||||
- **logFile**: Automatically set to ${basePath}/.log.md
|
||||
```
|
||||
|
||||
#### 2. **Consistent Naming**
|
||||
Use consistent variable naming conventions:
|
||||
|
||||
```javascript
|
||||
// Good: Clear, descriptive naming
|
||||
const variables = {
|
||||
projectName, // What project to work on
|
||||
basePath, // Where project files are located
|
||||
outputDirectory, // Where to save results
|
||||
processingMode, // How to process (detail level)
|
||||
configurationPath // Where config files are
|
||||
};
|
||||
|
||||
// Avoid: Ambiguous or inconsistent
|
||||
const bad_variables = {
|
||||
name, // Too generic
|
||||
path, // Unclear which path
|
||||
mode, // Too short
|
||||
config // Too vague
|
||||
};
|
||||
```
|
||||
|
||||
#### 3. **Validation and Constraints**
|
||||
Document valid values and constraints:
|
||||
|
||||
```markdown
|
||||
## Variable Constraints
|
||||
|
||||
**projectName**:
|
||||
- Type: string (alphanumeric, hyphens, underscores allowed)
|
||||
- Length: 1-100 characters
|
||||
- Required: yes
|
||||
- Pattern: `/^[a-zA-Z0-9_-]+$/`
|
||||
|
||||
**processingMode**:
|
||||
- Type: enum
|
||||
- Valid values: "quick" (< 5min), "standard" (5-15min), "detailed" (15+ min)
|
||||
- Default: "standard"
|
||||
- Required: no
|
||||
```
|
||||
|
||||
## MCP Server Configuration (Organization/Enterprise Only)
|
||||
|
||||
MCP servers extend agent capabilities with additional tools. Only supported for organization and enterprise-level agents.
|
||||
|
||||
### Configuration Format
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: my-custom-agent
|
||||
description: 'Agent with MCP integration'
|
||||
tools: ['read', 'edit', 'custom-mcp/tool-1']
|
||||
mcp-servers:
|
||||
custom-mcp:
|
||||
type: 'local'
|
||||
command: 'some-command'
|
||||
args: ['--arg1', '--arg2']
|
||||
tools: ["*"]
|
||||
env:
|
||||
ENV_VAR_NAME: ${{ secrets.API_KEY }}
|
||||
---
|
||||
```
|
||||
|
||||
### MCP Server Properties
|
||||
|
||||
- **type**: Server type (`'local'` or `'stdio'`)
|
||||
- **command**: Command to start the MCP server
|
||||
- **args**: Array of command arguments
|
||||
- **tools**: Tools to enable from this server (`["*"]` for all)
|
||||
- **env**: Environment variables (supports secrets)
|
||||
|
||||
### Environment Variables and Secrets
|
||||
|
||||
Secrets must be configured in repository settings under "copilot" environment.
|
||||
|
||||
**Supported syntax**:
|
||||
```yaml
|
||||
env:
|
||||
# Environment variable only
|
||||
VAR_NAME: COPILOT_MCP_ENV_VAR_VALUE
|
||||
|
||||
# Variable with header
|
||||
VAR_NAME: $COPILOT_MCP_ENV_VAR_VALUE
|
||||
VAR_NAME: ${COPILOT_MCP_ENV_VAR_VALUE}
|
||||
|
||||
# GitHub Actions-style (YAML only)
|
||||
VAR_NAME: ${{ secrets.COPILOT_MCP_ENV_VAR_VALUE }}
|
||||
VAR_NAME: ${{ var.COPILOT_MCP_ENV_VAR_VALUE }}
|
||||
```
|
||||
|
||||
## File Organization and Naming
|
||||
|
||||
### Repository-Level Agents
|
||||
- Location: `.github/agents/`
|
||||
- Scope: Available only in the specific repository
|
||||
- Access: Uses repository-configured MCP servers
|
||||
|
||||
### Organization/Enterprise-Level Agents
|
||||
- Location: `.github-private/agents/` (then move to `agents/` root)
|
||||
- Scope: Available across all repositories in org/enterprise
|
||||
- Access: Can configure dedicated MCP servers
|
||||
|
||||
### Naming Conventions
|
||||
- Use lowercase with hyphens: `test-specialist.agent.md`
|
||||
- Name should reflect agent purpose
|
||||
- Filename becomes default agent name (if `name` not specified)
|
||||
- Allowed characters: `.`, `-`, `_`, `a-z`, `A-Z`, `0-9`
|
||||
|
||||
## Agent Processing and Behavior
|
||||
|
||||
### Versioning
|
||||
- Based on Git commit SHAs for the agent file
|
||||
- Create branches/tags for different agent versions
|
||||
- Instantiated using latest version for repository/branch
|
||||
- PR interactions use same agent version for consistency
|
||||
|
||||
### Name Conflicts
|
||||
Priority (highest to lowest):
|
||||
1. Repository-level agent
|
||||
2. Organization-level agent
|
||||
3. Enterprise-level agent
|
||||
|
||||
Lower-level configurations override higher-level ones with the same name.
|
||||
|
||||
### Tool Processing
|
||||
- `tools` list filters available tools (built-in and MCP)
|
||||
- No tools specified = all tools enabled
|
||||
- Empty list (`[]`) = all tools disabled
|
||||
- Specific list = only those tools enabled
|
||||
- Unrecognized tool names are ignored (allows environment-specific tools)
|
||||
|
||||
### MCP Server Processing Order
|
||||
1. Out-of-the-box MCP servers (e.g., GitHub MCP)
|
||||
2. Custom agent MCP configuration (org/enterprise only)
|
||||
3. Repository-level MCP configurations
|
||||
|
||||
Each level can override settings from previous levels.
|
||||
|
||||
## Agent Creation Checklist
|
||||
|
||||
### Frontmatter
|
||||
- [ ] `description` field present and descriptive (50-150 chars)
|
||||
- [ ] `description` wrapped in single quotes
|
||||
- [ ] `name` specified (optional but recommended)
|
||||
- [ ] `tools` configured appropriately (or intentionally omitted)
|
||||
- [ ] `model` specified for optimal performance
|
||||
- [ ] `target` set if environment-specific
|
||||
- [ ] `infer` set to `false` if manual selection required
|
||||
|
||||
### Prompt Content
|
||||
- [ ] Clear agent identity and role defined
|
||||
- [ ] Core responsibilities listed explicitly
|
||||
- [ ] Approach and methodology explained
|
||||
- [ ] Guidelines and constraints specified
|
||||
- [ ] Output expectations documented
|
||||
- [ ] Examples provided where helpful
|
||||
- [ ] Instructions are specific and actionable
|
||||
- [ ] Scope and boundaries clearly defined
|
||||
- [ ] Total content under 30,000 characters
|
||||
|
||||
### File Structure
|
||||
- [ ] Filename follows lowercase-with-hyphens convention
|
||||
- [ ] File placed in correct directory (`.github/agents/` or `agents/`)
|
||||
- [ ] Filename uses only allowed characters
|
||||
- [ ] File extension is `.agent.md`
|
||||
|
||||
### Quality Assurance
|
||||
- [ ] Agent purpose is unique and not duplicative
|
||||
- [ ] Tools are minimal and necessary
|
||||
- [ ] Instructions are clear and unambiguous
|
||||
- [ ] Agent has been tested with representative tasks
|
||||
- [ ] Documentation references are current
|
||||
- [ ] Security considerations addressed (if applicable)
|
||||
|
||||
## Common Agent Patterns
|
||||
|
||||
### Testing Specialist
|
||||
**Purpose**: Focus on test coverage and quality
|
||||
**Tools**: All tools (for comprehensive test creation)
|
||||
**Approach**: Analyze, identify gaps, write tests, avoid production code changes
|
||||
|
||||
### Implementation Planner
|
||||
**Purpose**: Create detailed technical plans and specifications
|
||||
**Tools**: Limited to `['read', 'search', 'edit']`
|
||||
**Approach**: Analyze requirements, create documentation, avoid implementation
|
||||
|
||||
### Code Reviewer
|
||||
**Purpose**: Review code quality and provide feedback
|
||||
**Tools**: `['read', 'search']` only
|
||||
**Approach**: Analyze, suggest improvements, no direct modifications
|
||||
|
||||
### Refactoring Specialist
|
||||
**Purpose**: Improve code structure and maintainability
|
||||
**Tools**: `['read', 'search', 'edit']`
|
||||
**Approach**: Analyze patterns, propose refactorings, implement safely
|
||||
|
||||
### Security Auditor
|
||||
**Purpose**: Identify security issues and vulnerabilities
|
||||
**Tools**: `['read', 'search', 'web']`
|
||||
**Approach**: Scan code, check against OWASP, report findings
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
### Frontmatter Errors
|
||||
- ❌ Missing `description` field
|
||||
- ❌ Description not wrapped in quotes
|
||||
- ❌ Invalid tool names without checking documentation
|
||||
- ❌ Incorrect YAML syntax (indentation, quotes)
|
||||
|
||||
### Tool Configuration Issues
|
||||
- ❌ Granting excessive tool access unnecessarily
|
||||
- ❌ Missing required tools for agent's purpose
|
||||
- ❌ Not using tool aliases consistently
|
||||
- ❌ Forgetting MCP server namespace (`server-name/tool`)
|
||||
|
||||
### Prompt Content Problems
|
||||
- ❌ Vague, ambiguous instructions
|
||||
- ❌ Conflicting or contradictory guidelines
|
||||
- ❌ Lack of clear scope definition
|
||||
- ❌ Missing output expectations
|
||||
- ❌ Overly verbose instructions (exceeding character limits)
|
||||
- ❌ No examples or context for complex tasks
|
||||
|
||||
### Organizational Issues
|
||||
- ❌ Filename doesn't reflect agent purpose
|
||||
- ❌ Wrong directory (confusing repo vs org level)
|
||||
- ❌ Using spaces or special characters in filename
|
||||
- ❌ Duplicate agent names causing conflicts
|
||||
|
||||
## Testing and Validation
|
||||
|
||||
### Manual Testing
|
||||
1. Create the agent file with proper frontmatter
|
||||
2. Reload VS Code or refresh GitHub.com
|
||||
3. Select the agent from the dropdown in Copilot Chat
|
||||
4. Test with representative user queries
|
||||
5. Verify tool access works as expected
|
||||
6. Confirm output meets expectations
|
||||
|
||||
### Integration Testing
|
||||
- Test agent with different file types in scope
|
||||
- Verify MCP server connectivity (if configured)
|
||||
- Check agent behavior with missing context
|
||||
- Test error handling and edge cases
|
||||
- Validate agent switching and handoffs
|
||||
|
||||
### Quality Checks
|
||||
- Run through agent creation checklist
|
||||
- Review against common mistakes list
|
||||
- Compare with example agents in repository
|
||||
- Get peer review for complex agents
|
||||
- Document any special configuration needs
|
||||
|
||||
## Additional Resources
|
||||
|
||||
### Official Documentation
|
||||
- [Creating Custom Agents](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-custom-agents)
|
||||
- [Custom Agents Configuration](https://docs.github.com/en/copilot/reference/custom-agents-configuration)
|
||||
- [Custom Agents in VS Code](https://code.visualstudio.com/docs/copilot/customization/custom-agents)
|
||||
- [MCP Integration](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/extend-coding-agent-with-mcp)
|
||||
|
||||
### Community Resources
|
||||
- [Awesome Copilot Agents Collection](https://github.com/github/awesome-copilot/tree/main/agents)
|
||||
- [Customization Library Examples](https://docs.github.com/en/copilot/tutorials/customization-library/custom-agents)
|
||||
- [Your First Custom Agent Tutorial](https://docs.github.com/en/copilot/tutorials/customization-library/custom-agents/your-first-custom-agent)
|
||||
|
||||
### Related Files
|
||||
- [Prompt Files Guidelines](./prompt.instructions.md) - For creating prompt files
|
||||
- [Instructions Guidelines](./instructions.instructions.md) - For creating instruction files
|
||||
|
||||
## Version Compatibility Notes
|
||||
|
||||
### GitHub.com (Coding Agent)
|
||||
- ✅ Fully supports all standard frontmatter properties
|
||||
- ✅ Repository and org/enterprise level agents
|
||||
- ✅ MCP server configuration (org/enterprise)
|
||||
- ❌ Does not support `model`, `argument-hint`, `handoffs` properties
|
||||
|
||||
### VS Code / JetBrains / Eclipse / Xcode
|
||||
- ✅ Supports `model` property for AI model selection
|
||||
- ✅ Supports `argument-hint` and `handoffs` properties
|
||||
- ✅ User profile and workspace-level agents
|
||||
- ❌ Cannot configure MCP servers at repository level
|
||||
- ⚠️ Some properties may behave differently
|
||||
|
||||
When creating agents for multiple environments, focus on common properties and test in all target environments. Use `target` property to create environment-specific agents when necessary.
|
||||
187
.github/instructions/azure-devops-pipelines.instructions.md
vendored
Normal file
187
.github/instructions/azure-devops-pipelines.instructions.md
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
---
|
||||
description: 'Best practices for Azure DevOps Pipeline YAML files'
|
||||
applyTo: '**/azure-pipelines.yml, **/azure-pipelines*.yml, **/*.pipeline.yml'
|
||||
---
|
||||
|
||||
# Azure DevOps Pipeline YAML Best Practices
|
||||
|
||||
Guidelines for creating maintainable, secure, and efficient Azure DevOps pipelines in PowerToys.
|
||||
|
||||
## General Guidelines
|
||||
|
||||
- Use YAML syntax consistently with proper indentation (2 spaces)
|
||||
- Always include meaningful names and display names for pipelines, stages, jobs, and steps
|
||||
- Implement proper error handling and conditional execution
|
||||
- Use variables and parameters to make pipelines reusable and maintainable
|
||||
- Follow the principle of least privilege for service connections and permissions
|
||||
- Include comprehensive logging and diagnostics for troubleshooting
|
||||
|
||||
## Pipeline Structure
|
||||
|
||||
- Organize complex pipelines using stages for better visualization and control
|
||||
- Use jobs to group related steps and enable parallel execution when possible
|
||||
- Implement proper dependencies between stages and jobs
|
||||
- Use templates for reusable pipeline components
|
||||
- Keep pipeline files focused and modular - split large pipelines into multiple files
|
||||
|
||||
## Build Best Practices
|
||||
|
||||
- Use specific agent pool versions and VM images for consistency
|
||||
- Cache dependencies (npm, NuGet, Maven, etc.) to improve build performance
|
||||
- Implement proper artifact management with meaningful names and retention policies
|
||||
- Use build variables for version numbers and build metadata
|
||||
- Include code quality gates (lint checks, testing, security scans)
|
||||
- Ensure builds are reproducible and environment-independent
|
||||
|
||||
## Testing Integration
|
||||
|
||||
- Run unit tests as part of the build process
|
||||
- Publish test results in standard formats (JUnit, VSTest, etc.)
|
||||
- Include code coverage reporting and quality gates
|
||||
- Implement integration and end-to-end tests in appropriate stages
|
||||
- Use test impact analysis when available to optimize test execution
|
||||
- Fail fast on test failures to provide quick feedback
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Use Azure Key Vault for sensitive configuration and secrets
|
||||
- Implement proper secret management with variable groups
|
||||
- Use service connections with minimal required permissions
|
||||
- Enable security scans (dependency vulnerabilities, static analysis)
|
||||
- Implement approval gates for production deployments
|
||||
- Use managed identities when possible instead of service principals
|
||||
|
||||
## Deployment Strategies
|
||||
|
||||
- Implement proper environment promotion (dev → staging → production)
|
||||
- Use deployment jobs with proper environment targeting
|
||||
- Implement blue-green or canary deployment strategies when appropriate
|
||||
- Include rollback mechanisms and health checks
|
||||
- Use infrastructure as code (ARM, Bicep, Terraform) for consistent deployments
|
||||
- Implement proper configuration management per environment
|
||||
|
||||
## Variable and Parameter Management
|
||||
|
||||
- Use variable groups for shared configuration across pipelines
|
||||
- Implement runtime parameters for flexible pipeline execution
|
||||
- Use conditional variables based on branches or environments
|
||||
- Secure sensitive variables and mark them as secrets
|
||||
- Document variable purposes and expected values
|
||||
- Use variable templates for complex variable logic
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
- Use parallel jobs and matrix strategies when appropriate
|
||||
- Implement proper caching strategies for dependencies and build outputs
|
||||
- Use shallow clone for Git operations when full history isn't needed
|
||||
- Optimize Docker image builds with multi-stage builds and layer caching
|
||||
- Monitor pipeline performance and optimize bottlenecks
|
||||
- Use pipeline resource triggers efficiently
|
||||
|
||||
## Monitoring and Observability
|
||||
|
||||
- Include comprehensive logging throughout the pipeline
|
||||
- Use Azure Monitor and Application Insights for deployment tracking
|
||||
- Implement proper notification strategies for failures and successes
|
||||
- Include deployment health checks and automated rollback triggers
|
||||
- Use pipeline analytics to identify improvement opportunities
|
||||
- Document pipeline behavior and troubleshooting steps
|
||||
|
||||
## Template and Reusability
|
||||
|
||||
- Create pipeline templates for common patterns
|
||||
- Use extends templates for complete pipeline inheritance
|
||||
- Implement step templates for reusable task sequences
|
||||
- Use variable templates for complex variable logic
|
||||
- Version templates appropriately for stability
|
||||
- Document template parameters and usage examples
|
||||
|
||||
## Branch and Trigger Strategy
|
||||
|
||||
- Implement appropriate triggers for different branch types
|
||||
- Use path filters to trigger builds only when relevant files change
|
||||
- Configure proper CI/CD triggers for main/master branches
|
||||
- Use pull request triggers for code validation
|
||||
- Implement scheduled triggers for maintenance tasks
|
||||
- Consider resource triggers for multi-repository scenarios
|
||||
|
||||
## Example Structure
|
||||
|
||||
```yaml
|
||||
# azure-pipelines.yml
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
- develop
|
||||
paths:
|
||||
exclude:
|
||||
- docs/*
|
||||
- README.md
|
||||
|
||||
variables:
|
||||
- group: shared-variables
|
||||
- name: buildConfiguration
|
||||
value: 'Release'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: 'Build and Test'
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: 'Build Application'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Use .NET SDK'
|
||||
inputs:
|
||||
version: '8.x'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Restore dependencies'
|
||||
inputs:
|
||||
command: 'restore'
|
||||
projects: '**/*.csproj'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Build application'
|
||||
inputs:
|
||||
command: 'build'
|
||||
projects: '**/*.csproj'
|
||||
arguments: '--configuration $(buildConfiguration) --no-restore'
|
||||
|
||||
- stage: Deploy
|
||||
displayName: 'Deploy to Staging'
|
||||
dependsOn: Build
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
||||
jobs:
|
||||
- deployment: DeployToStaging
|
||||
displayName: 'Deploy to Staging Environment'
|
||||
environment: 'staging'
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- download: current
|
||||
displayName: 'Download drop artifact'
|
||||
artifact: drop
|
||||
- task: AzureWebApp@1
|
||||
displayName: 'Deploy to Azure Web App'
|
||||
inputs:
|
||||
azureSubscription: 'staging-service-connection'
|
||||
appType: 'webApp'
|
||||
appName: 'myapp-staging'
|
||||
package: '$(Pipeline.Workspace)/drop/**/*.zip'
|
||||
```
|
||||
|
||||
## Common Anti-Patterns to Avoid
|
||||
|
||||
- Hardcoding sensitive values directly in YAML files
|
||||
- Using overly broad triggers that cause unnecessary builds
|
||||
- Mixing build and deployment logic in a single stage
|
||||
- Not implementing proper error handling and cleanup
|
||||
- Using deprecated task versions without upgrade plans
|
||||
- Creating monolithic pipelines that are difficult to maintain
|
||||
- Not using proper naming conventions for clarity
|
||||
- Ignoring pipeline security best practices
|
||||
61
.github/instructions/common-libraries.instructions.md
vendored
Normal file
61
.github/instructions/common-libraries.instructions.md
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
description: 'Guidelines for shared libraries including logging, IPC, settings, DPI, telemetry, and utilities consumed by multiple modules'
|
||||
applyTo: 'src/common/**'
|
||||
---
|
||||
|
||||
# Common Libraries – Shared Code Guidance
|
||||
|
||||
Guidelines for modifying shared code in `src/common/`. Changes here can have wide-reaching impact across the entire PowerToys codebase.
|
||||
|
||||
## Scope
|
||||
|
||||
- Logging infrastructure (`src/common/logger/`)
|
||||
- IPC primitives and named pipe utilities
|
||||
- Settings serialization and management
|
||||
- DPI awareness and scaling utilities
|
||||
- Telemetry helpers
|
||||
- General utilities (JSON parsing, string helpers, etc.)
|
||||
|
||||
## Guidelines
|
||||
|
||||
### API Stability
|
||||
|
||||
- Avoid breaking public headers/APIs; if changed, search & update all callers
|
||||
- Coordinate ABI-impacting struct/class layout changes; keep binary compatibility
|
||||
- When modifying public interfaces, grep the entire codebase for usages
|
||||
|
||||
### Performance
|
||||
|
||||
- Watch perf in hot paths (hooks, timers, serialization)
|
||||
- Avoid avoidable allocations in frequently called code
|
||||
- Profile changes that touch performance-sensitive areas
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Ask before adding third-party deps or changing serialization formats
|
||||
- New dependencies must be MIT-licensed or approved by PM team
|
||||
- Add any new external packages to `NOTICE.md`
|
||||
|
||||
### Logging
|
||||
|
||||
- C++ logging uses spdlog (`Logger::info`, `Logger::warn`, `Logger::error`, `Logger::debug`)
|
||||
- Initialize with `init_logger()` early in startup
|
||||
- Keep hot paths quiet – no logging in tight loops or hooks
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- No unintended ABI breaks
|
||||
- No noisy logs in hot paths
|
||||
- New non-obvious symbols briefly commented
|
||||
- All callers updated when interfaces change
|
||||
|
||||
## Code Style
|
||||
|
||||
- **C++**: Follow `.clang-format` in `src/`; use Modern C++ patterns per C++ Core Guidelines
|
||||
- **C#**: Follow `src/.editorconfig`; enforce StyleCop.Analyzers
|
||||
|
||||
## Validation
|
||||
|
||||
- Build: `tools\build\build.cmd` from `src/common/` folder
|
||||
- Verify no ABI breaks: grep for changed function/struct names across codebase
|
||||
- Check logs: ensure no new logging in performance-critical paths
|
||||
256
.github/instructions/instructions.instructions.md
vendored
Normal file
256
.github/instructions/instructions.instructions.md
vendored
Normal file
@@ -0,0 +1,256 @@
|
||||
---
|
||||
description: 'Guidelines for creating high-quality custom instruction files for GitHub Copilot'
|
||||
applyTo: '**/*.instructions.md'
|
||||
---
|
||||
|
||||
# Custom Instructions File Guidelines
|
||||
|
||||
Instructions for creating effective and maintainable custom instruction files that guide GitHub Copilot in generating domain-specific code and following project conventions.
|
||||
|
||||
## Project Context
|
||||
|
||||
- Target audience: Developers and GitHub Copilot working with domain-specific code
|
||||
- File format: Markdown with YAML frontmatter
|
||||
- File naming convention: lowercase with hyphens (e.g., `react-best-practices.instructions.md`)
|
||||
- Location: `.github/instructions/` directory
|
||||
- Purpose: Provide context-aware guidance for code generation, review, and documentation
|
||||
|
||||
## Required Frontmatter
|
||||
|
||||
Every instruction file must include YAML frontmatter with the following fields:
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: 'Brief description of the instruction purpose and scope'
|
||||
applyTo: 'glob pattern for target files (e.g., **/*.ts, **/*.py)'
|
||||
---
|
||||
```
|
||||
|
||||
### Frontmatter Guidelines
|
||||
|
||||
- **description**: Single-quoted string, 1-500 characters, clearly stating the purpose
|
||||
- **applyTo**: Glob pattern(s) specifying which files these instructions apply to
|
||||
- Single pattern: `'**/*.ts'`
|
||||
- Multiple patterns: `'**/*.ts, **/*.tsx, **/*.js'`
|
||||
- Specific files: `'src/**/*.py'`
|
||||
- All files: `'**'`
|
||||
|
||||
## File Structure
|
||||
|
||||
A well-structured instruction file should include the following sections:
|
||||
|
||||
### 1. Title and Overview
|
||||
|
||||
- Clear, descriptive title using `#` heading
|
||||
- Brief introduction explaining the purpose and scope
|
||||
- Optional: Project context section with key technologies and versions
|
||||
|
||||
### 2. Core Sections
|
||||
|
||||
Organize content into logical sections based on the domain:
|
||||
|
||||
- **General Instructions**: High-level guidelines and principles
|
||||
- **Best Practices**: Recommended patterns and approaches
|
||||
- **Code Standards**: Naming conventions, formatting, style rules
|
||||
- **Architecture/Structure**: Project organization and design patterns
|
||||
- **Common Patterns**: Frequently used implementations
|
||||
- **Security**: Security considerations (if applicable)
|
||||
- **Performance**: Optimization guidelines (if applicable)
|
||||
- **Testing**: Testing standards and approaches (if applicable)
|
||||
|
||||
### 3. Examples and Code Snippets
|
||||
|
||||
Provide concrete examples with clear labels:
|
||||
|
||||
```markdown
|
||||
### Good Example
|
||||
\`\`\`language
|
||||
// Recommended approach
|
||||
code example here
|
||||
\`\`\`
|
||||
|
||||
### Bad Example
|
||||
\`\`\`language
|
||||
// Avoid this pattern
|
||||
code example here
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### 4. Validation and Verification (Optional but Recommended)
|
||||
|
||||
- Build commands to verify code
|
||||
- Lint checks and formatting tools
|
||||
- Testing requirements
|
||||
- Verification steps
|
||||
|
||||
## Content Guidelines
|
||||
|
||||
### Writing Style
|
||||
|
||||
- Use clear, concise language
|
||||
- Write in imperative mood ("Use", "Implement", "Avoid")
|
||||
- Be specific and actionable
|
||||
- Avoid ambiguous terms like "should", "might", "possibly"
|
||||
- Use bullet points and lists for readability
|
||||
- Keep sections focused and scannable
|
||||
|
||||
### Best Practices
|
||||
|
||||
- **Be Specific**: Provide concrete examples rather than abstract concepts
|
||||
- **Show Why**: Explain the reasoning behind recommendations when it adds value
|
||||
- **Use Tables**: For comparing options, listing rules, or showing patterns
|
||||
- **Include Examples**: Real code snippets are more effective than descriptions
|
||||
- **Stay Current**: Reference current versions and best practices
|
||||
- **Link Resources**: Include official documentation and authoritative sources
|
||||
|
||||
### Common Patterns to Include
|
||||
|
||||
1. **Naming Conventions**: How to name variables, functions, classes, files
|
||||
2. **Code Organization**: File structure, module organization, import order
|
||||
3. **Error Handling**: Preferred error handling patterns
|
||||
4. **Dependencies**: How to manage and document dependencies
|
||||
5. **Comments and Documentation**: When and how to document code
|
||||
6. **Version Information**: Target language/framework versions
|
||||
|
||||
## Patterns to Follow
|
||||
|
||||
### Bullet Points and Lists
|
||||
|
||||
```markdown
|
||||
## Security Best Practices
|
||||
|
||||
- Always validate user input before processing
|
||||
- Use parameterized queries to prevent SQL injection
|
||||
- Store secrets in environment variables, never in code
|
||||
- Implement proper authentication and authorization
|
||||
- Enable HTTPS for all production endpoints
|
||||
```
|
||||
|
||||
### Tables for Structured Information
|
||||
|
||||
```markdown
|
||||
## Common Issues
|
||||
|
||||
| Issue | Solution | Example |
|
||||
| ---------------- | ------------------- | ----------------------------- |
|
||||
| Magic numbers | Use named constants | `const MAX_RETRIES = 3` |
|
||||
| Deep nesting | Extract functions | Refactor nested if statements |
|
||||
| Hardcoded values | Use configuration | Store API URLs in config |
|
||||
```
|
||||
|
||||
### Code Comparison
|
||||
|
||||
```markdown
|
||||
### Good Example - Using TypeScript interfaces
|
||||
\`\`\`typescript
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
function getUser(id: string): User {
|
||||
// Implementation
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Bad Example - Using any type
|
||||
\`\`\`typescript
|
||||
function getUser(id: any): any {
|
||||
// Loses type safety
|
||||
}
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### Conditional Guidance
|
||||
|
||||
```markdown
|
||||
## Framework Selection
|
||||
|
||||
- **For small projects**: Use Minimal API approach
|
||||
- **For large projects**: Use controller-based architecture with clear separation
|
||||
- **For microservices**: Consider domain-driven design patterns
|
||||
```
|
||||
|
||||
## Patterns to Avoid
|
||||
|
||||
- **Overly verbose explanations**: Keep it concise and scannable
|
||||
- **Outdated information**: Always reference current versions and practices
|
||||
- **Ambiguous guidelines**: Be specific about what to do or avoid
|
||||
- **Missing examples**: Abstract rules without concrete code examples
|
||||
- **Contradictory advice**: Ensure consistency throughout the file
|
||||
- **Copy-paste from documentation**: Add value by distilling and providing context
|
||||
|
||||
## Testing Your Instructions
|
||||
|
||||
Before finalizing instruction files:
|
||||
|
||||
1. **Test with Copilot**: Try the instructions with actual prompts in VS Code
|
||||
2. **Verify Examples**: Ensure code examples are correct and run without errors
|
||||
3. **Check Glob Patterns**: Confirm `applyTo` patterns match intended files
|
||||
|
||||
## Example Structure
|
||||
|
||||
Here's a minimal example structure for a new instruction file:
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: 'Brief description of purpose'
|
||||
applyTo: '**/*.ext'
|
||||
---
|
||||
|
||||
# Technology Name Development
|
||||
|
||||
Brief introduction and context.
|
||||
|
||||
## General Instructions
|
||||
|
||||
- High-level guideline 1
|
||||
- High-level guideline 2
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Specific practice 1
|
||||
- Specific practice 2
|
||||
|
||||
## Code Standards
|
||||
|
||||
### Naming Conventions
|
||||
- Rule 1
|
||||
- Rule 2
|
||||
|
||||
### File Organization
|
||||
- Structure 1
|
||||
- Structure 2
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1
|
||||
Description and example
|
||||
|
||||
\`\`\`language
|
||||
code example
|
||||
\`\`\`
|
||||
|
||||
### Pattern 2
|
||||
Description and example
|
||||
|
||||
## Validation
|
||||
|
||||
- Build command: `command to verify`
|
||||
- Lint checks: `command to lint`
|
||||
- Testing: `command to test`
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
- Review instructions when dependencies or frameworks are updated
|
||||
- Update examples to reflect current best practices
|
||||
- Remove outdated patterns or deprecated features
|
||||
- Add new patterns as they emerge in the community
|
||||
- Keep glob patterns accurate as project structure evolves
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Custom Instructions Documentation](https://code.visualstudio.com/docs/copilot/customization/custom-instructions)
|
||||
- [Awesome Copilot Instructions](https://github.com/github/awesome-copilot/tree/main/instructions)
|
||||
88
.github/instructions/prompt.instructions.md
vendored
Normal file
88
.github/instructions/prompt.instructions.md
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
description: 'Guidelines for creating high-quality prompt files for GitHub Copilot'
|
||||
applyTo: '**/*.prompt.md'
|
||||
---
|
||||
|
||||
# Copilot Prompt Files Guidelines
|
||||
|
||||
Instructions for creating effective and maintainable prompt files that guide GitHub Copilot in delivering consistent, high-quality outcomes across any repository.
|
||||
|
||||
## Scope and Principles
|
||||
- Target audience: maintainers and contributors authoring reusable prompts for Copilot Chat.
|
||||
- Goals: predictable behaviour, clear expectations, minimal permissions, and portability across repositories.
|
||||
- Primary references: VS Code documentation on prompt files and organization-specific conventions.
|
||||
|
||||
## Frontmatter Requirements
|
||||
|
||||
Every prompt file should include YAML frontmatter with the following fields:
|
||||
|
||||
### Required/Recommended Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `description` | Recommended | A short description of the prompt (single sentence, actionable outcome) |
|
||||
| `name` | Optional | The name shown after typing `/` in chat. Defaults to filename if not specified |
|
||||
| `agent` | Recommended | The agent to use: `ask`, `edit`, `agent`, or a custom agent name. Defaults to current agent |
|
||||
| `model` | Optional | The language model to use. Defaults to currently selected model |
|
||||
| `tools` | Optional | List of tool/tool set names available for this prompt |
|
||||
| `argument-hint` | Optional | Hint text shown in chat input to guide user interaction |
|
||||
|
||||
### Guidelines
|
||||
|
||||
- Use consistent quoting (single quotes recommended) and keep one field per line for readability and version control clarity
|
||||
- If `tools` are specified and current agent is `ask` or `edit`, the default agent becomes `agent`
|
||||
- Preserve any additional metadata (`language`, `tags`, `visibility`, etc.) required by your organization
|
||||
|
||||
## File Naming and Placement
|
||||
- Use kebab-case filenames ending with `.prompt.md` and store them under `.github/prompts/` unless your workspace standard specifies another directory.
|
||||
- Provide a short filename that communicates the action (for example, `generate-readme.prompt.md` rather than `prompt1.prompt.md`).
|
||||
|
||||
## Body Structure
|
||||
- Start with an `#` level heading that matches the prompt intent so it surfaces well in Quick Pick search.
|
||||
- Organize content with predictable sections. Recommended baseline: `Mission` or `Primary Directive`, `Scope & Preconditions`, `Inputs`, `Workflow` (step-by-step), `Output Expectations`, and `Quality Assurance`.
|
||||
- Adjust section names to fit the domain, but retain the logical flow: why → context → inputs → actions → outputs → validation.
|
||||
- Reference related prompts or instruction files using relative links to aid discoverability.
|
||||
|
||||
## Input and Context Handling
|
||||
- Use `${input:variableName[:placeholder]}` for required values and explain when the user must supply them. Provide defaults or alternatives where possible.
|
||||
- Call out contextual variables such as `${selection}`, `${file}`, `${workspaceFolder}` only when they are essential, and describe how Copilot should interpret them.
|
||||
- Document how to proceed when mandatory context is missing (for example, “Request the file path and stop if it remains undefined”).
|
||||
|
||||
## Tool and Permission Guidance
|
||||
- Limit `tools` to the smallest set that enables the task. List them in the preferred execution order when the sequence matters.
|
||||
- If the prompt inherits tools from a chat mode, mention that relationship and state any critical tool behaviours or side effects.
|
||||
- Warn about destructive operations (file creation, edits, terminal commands) and include guard rails or confirmation steps in the workflow.
|
||||
|
||||
## Instruction Tone and Style
|
||||
- Write in direct, imperative sentences targeted at Copilot (for example, “Analyze”, “Generate”, “Summarize”).
|
||||
- Keep sentences short and unambiguous, following Google Developer Documentation translation best practices to support localization.
|
||||
- Avoid idioms, humor, or culturally specific references; favor neutral, inclusive language.
|
||||
|
||||
## Output Definition
|
||||
- Specify the format, structure, and location of expected results (for example, “Create an architecture decision record file using the template below, such as `docs/architecture-decisions/record-XXXX.md`).
|
||||
- Include success criteria and failure triggers so Copilot knows when to halt or retry.
|
||||
- Provide validation steps—manual checks, automated commands, or acceptance criteria lists—that reviewers can execute after running the prompt.
|
||||
|
||||
## Examples and Reusable Assets
|
||||
- Embed Good/Bad examples or scaffolds (Markdown templates, JSON stubs) that the prompt should produce or follow.
|
||||
- Maintain reference tables (capabilities, status codes, role descriptions) inline to keep the prompt self-contained. Update these tables when upstream resources change.
|
||||
- Link to authoritative documentation instead of duplicating lengthy guidance.
|
||||
|
||||
## Quality Assurance Checklist
|
||||
- [ ] Frontmatter fields are complete, accurate, and least-privilege.
|
||||
- [ ] Inputs include placeholders, default behaviours, and fallbacks.
|
||||
- [ ] Workflow covers preparation, execution, and post-processing without gaps.
|
||||
- [ ] Output expectations include formatting and storage details.
|
||||
- [ ] Validation steps are actionable (commands, diff checks, review prompts).
|
||||
- [ ] Security, compliance, and privacy policies referenced by the prompt are current.
|
||||
- [ ] Prompt executes successfully in VS Code (`Chat: Run Prompt`) using representative scenarios.
|
||||
|
||||
## Maintenance Guidance
|
||||
- Version-control prompts alongside the code they affect; update them when dependencies, tooling, or review processes change.
|
||||
- Review prompts periodically to ensure tool lists, model requirements, and linked documents remain valid.
|
||||
- Coordinate with other repositories: when a prompt proves broadly useful, extract common guidance into instruction files or shared prompt packs.
|
||||
|
||||
## Additional Resources
|
||||
- [Prompt Files Documentation](https://code.visualstudio.com/docs/copilot/customization/prompt-files#_prompt-file-format)
|
||||
- [Awesome Copilot Prompt Files](https://github.com/github/awesome-copilot/tree/main/prompts)
|
||||
- [Tool Configuration](https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode#_agent-mode-tools)
|
||||
68
.github/instructions/runner-settings-ui.instructions.md
vendored
Normal file
68
.github/instructions/runner-settings-ui.instructions.md
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
description: 'Guidelines for Runner and Settings UI components that communicate via named pipes and manage module lifecycle'
|
||||
applyTo: 'src/runner/**,src/settings-ui/**'
|
||||
---
|
||||
|
||||
# Runner & Settings UI – Core Components Guidance
|
||||
|
||||
Guidelines for modifying the Runner (tray/module loader) and Settings UI (configuration app). These components communicate via Windows Named Pipes using JSON messages.
|
||||
|
||||
## Runner (`src/runner/`)
|
||||
|
||||
### Scope
|
||||
|
||||
- Module bootstrap, hotkey management, settings bridge, update/elevation handling
|
||||
|
||||
### Guidelines
|
||||
|
||||
- If IPC/JSON contracts change, mirror updates in `src/settings-ui/**`
|
||||
- Keep module discovery in `src/runner/main.cpp` in sync when adding/removing modules
|
||||
- Keep startup lean: avoid blocking/network calls in early init path
|
||||
- Preserve GPO & elevation behaviors; confirm no regression in policy handling
|
||||
- Ask before modifying update workflow or elevation logic
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- Stable startup, consistent contracts, no unnecessary logging noise
|
||||
|
||||
## Settings UI (`src/settings-ui/`)
|
||||
|
||||
### Scope
|
||||
|
||||
- WinUI/WPF UI, communicates with Runner over named pipes; manages persisted settings schema
|
||||
|
||||
### Guidelines
|
||||
|
||||
- Don't break settings schema silently; add migration when shape changes
|
||||
- If IPC/JSON contracts change, align with `src/runner/**` implementation
|
||||
- Keep UI responsive: marshal to UI thread for UI-bound operations
|
||||
- Reuse existing styles/resources; avoid duplicate theme keys
|
||||
- Add/adjust migration or serialization tests when changing persisted settings
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- Schema integrity preserved, responsive UI, consistent contracts, no style duplication
|
||||
|
||||
## Shared Concerns
|
||||
|
||||
### IPC Contract Changes
|
||||
|
||||
When modifying the JSON message format between Runner and Settings UI:
|
||||
|
||||
1. Update both `src/runner/` and `src/settings-ui/` in the same PR
|
||||
2. Preserve backward compatibility where possible
|
||||
3. Add migration logic for settings schema changes
|
||||
4. Test both directions of communication
|
||||
|
||||
### Code Style
|
||||
|
||||
- **C++ (Runner)**: Follow `.clang-format` in `src/`
|
||||
- **C# (Settings UI)**: Follow `src/.editorconfig`, use StyleCop.Analyzers
|
||||
- **XAML**: Use XamlStyler or run `.\.pipelines\applyXamlStyling.ps1 -Main`
|
||||
|
||||
## Validation
|
||||
|
||||
- Build Runner: `tools\build\build.cmd` from `src/runner/`
|
||||
- Build Settings UI: `tools\build\build.cmd` from `src/settings-ui/`
|
||||
- Test IPC: Launch both Runner and Settings UI, verify communication works
|
||||
- Schema changes: Run serialization tests if settings shape changed
|
||||
@@ -1,9 +1,11 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: GPT-5-Codex (Preview)
|
||||
description: 'Generate an 80-character git commit title for the local diff.'
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Generate an 80-character git commit title for the local diff'
|
||||
---
|
||||
|
||||
# Generate Commit Title
|
||||
|
||||
**Goal:** Provide a ready-to-paste git commit title (<= 80 characters) that captures the most important local changes since `HEAD`.
|
||||
|
||||
**Workflow:**
|
||||
|
||||
8
.github/prompts/create-pr-summary.prompt.md
vendored
8
.github/prompts/create-pr-summary.prompt.md
vendored
@@ -1,9 +1,11 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: GPT-5-Codex (Preview)
|
||||
description: 'Generate a PowerToys-ready pull request description from the local diff.'
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Generate a PowerToys-ready pull request description from the local diff'
|
||||
---
|
||||
|
||||
# Generate PR Summary
|
||||
|
||||
**Goal:** Produce a ready-to-paste PR title and description that follows PowerToys conventions by comparing the current branch against a user-selected target branch.
|
||||
|
||||
**Repo guardrails:**
|
||||
|
||||
73
.github/prompts/fix-issue.prompt.md
vendored
Normal file
73
.github/prompts/fix-issue.prompt.md
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Execute the fix for a GitHub issue using the previously generated implementation plan'
|
||||
---
|
||||
|
||||
# Fix GitHub Issue
|
||||
|
||||
## Dependencies
|
||||
Source review prompt (for generating the implementation plan if missing):
|
||||
- .github/prompts/review-issue.prompt.md
|
||||
|
||||
Required plan file (single source of truth):
|
||||
- Generated Files/issueReview/{{issue_number}}/implementation-plan.md
|
||||
|
||||
## Dependency Handling
|
||||
1) If `implementation-plan.md` exists → proceed.
|
||||
2) If missing → run the review prompt:
|
||||
- Invoke: `.github/prompts/review-issue.prompt.md`
|
||||
- Pass: `issue_number={{issue_number}}`
|
||||
- Then re-check for `implementation-plan.md`.
|
||||
3) If still missing → stop and generate:
|
||||
- `Generated Files/issueFix/{{issue_number}}/manual-steps.md` containing:
|
||||
“implementation-plan.md not found; please run .github/prompts/review-issue.prompt.md for #{{issue_number}}.”
|
||||
|
||||
# GOAL
|
||||
For **#{{issue_number}}**:
|
||||
- Use implementation-plan.md as the single authority.
|
||||
- Apply code and test changes directly in the repository.
|
||||
- Produce a PR-ready description.
|
||||
|
||||
# OUTPUT FILES
|
||||
1) Generated Files/issueFix/{{issue_number}}/pr-description.md
|
||||
2) Generated Files/issueFix/{{issue_number}}/manual-steps.md # only if human interaction or external setup is required
|
||||
|
||||
# EXECUTION RULES
|
||||
1) Read implementation-plan.md and execute:
|
||||
- Layers & Files → edit/create as listed
|
||||
- Pattern Choices → follow repository conventions
|
||||
- Fundamentals (perf, security, compatibility, accessibility)
|
||||
- Logging & Exceptions
|
||||
- Telemetry (only if explicitly included in the plan)
|
||||
- Risks & Mitigations
|
||||
- Tests to Add
|
||||
2) Locate affected files via `rg` or `git grep`.
|
||||
3) Add/update tests to enforce the fixed behavior.
|
||||
4) If any ambiguity exists, add:
|
||||
// TODO(Human input needed): <clarification needed>
|
||||
5) Verify locally: build & tests run successfully.
|
||||
|
||||
# pr-description.md should include:
|
||||
- Title: `Fix: <short summary> (#{{issue_number}})`
|
||||
- What changed and why the fix works
|
||||
- Files or modules touched
|
||||
- Risks & mitigations (implemented)
|
||||
- Tests added/updated and how to run them
|
||||
- Telemetry behavior (if applicable)
|
||||
- Validation / reproduction steps
|
||||
- `Closes #{{issue_number}}`
|
||||
|
||||
# manual-steps.md (only if needed)
|
||||
- List required human actions: secrets, config, approvals, missing info, or code comments requiring human decisions.
|
||||
|
||||
# IMPORTANT
|
||||
- Apply code and tests directly; do not produce patch files.
|
||||
- Follow implementation-plan.md as the source of truth.
|
||||
- Insert comments for human review where a decision or input is required.
|
||||
- Use repository conventions and deterministic, minimal changes.
|
||||
|
||||
# FINALIZE
|
||||
- Write pr-description.md
|
||||
- Write manual-steps.md only if needed
|
||||
- Print concise success message or note items requiring human interaction
|
||||
8
.github/prompts/fix-spelling.prompt.md
vendored
8
.github/prompts/fix-spelling.prompt.md
vendored
@@ -1,9 +1,11 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: GPT-5-Codex (Preview)
|
||||
description: 'Resolve Code scanning / check-spelling comments on the active PR.'
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Resolve Code scanning / check-spelling comments on the active PR'
|
||||
---
|
||||
|
||||
# Fix Spelling Comments
|
||||
|
||||
**Goal:** Clear every outstanding GitHub pull request comment created by the `Code scanning / check-spelling` workflow by explicitly allowing intentional terms.
|
||||
|
||||
**Guardrails:**
|
||||
|
||||
160
.github/prompts/review-issue.prompt.md
vendored
Normal file
160
.github/prompts/review-issue.prompt.md
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Review a GitHub issue, score it (0-100), and generate an implementation plan'
|
||||
---
|
||||
|
||||
# Review GitHub Issue
|
||||
|
||||
## Goal
|
||||
For **#{{issue_number}}** produce:
|
||||
1) `Generated Files/issueReview/{{issue_number}}/overview.md`
|
||||
2) `Generated Files/issueReview/{{issue_number}}/implementation-plan.md`
|
||||
|
||||
## Inputs
|
||||
Figure out required inputs {{issue_number}} from the invocation context; if anything is missing, ask for the value or note it as a gap.
|
||||
|
||||
# CONTEXT (brief)
|
||||
Ground evidence using `gh issue view {{issue_number}} --json number,title,body,author,createdAt,updatedAt,state,labels,milestone,reactions,comments,linkedPullRequests`, and download images to better understand the issue context.
|
||||
Locate source code in the current workspace; feel free to use `rg`/`git grep`. Link related issues and PRs.
|
||||
|
||||
# OVERVIEW.MD
|
||||
## Summary
|
||||
Issue, state, milestone, labels. **Signals**: 👍/❤️/👎, comment count, last activity, linked PRs.
|
||||
|
||||
## At-a-Glance Score Table
|
||||
Present all ratings in a compact table for quick scanning:
|
||||
|
||||
| Dimension | Score | Assessment | Key Drivers |
|
||||
|-----------|-------|------------|-------------|
|
||||
| **A) Business Importance** | X/100 | Low/Medium/High | Top 2 factors with scores |
|
||||
| **B) Community Excitement** | X/100 | Low/Medium/High | Top 2 factors with scores |
|
||||
| **C) Technical Feasibility** | X/100 | Low/Medium/High | Top 2 factors with scores |
|
||||
| **D) Requirement Clarity** | X/100 | Low/Medium/High | Top 2 factors with scores |
|
||||
| **Overall Priority** | X/100 | Low/Medium/High/Critical | Average or weighted summary |
|
||||
| **Effort Estimate** | X days (T-shirt) | XS/S/M/L/XL/XXL/Epic | Type: bug/feature/chore |
|
||||
| **Similar Issues Found** | X open, Y closed | — | Quick reference to related work |
|
||||
| **Potential Assignees** | @username, @username | — | Top contributors to module |
|
||||
|
||||
**Assessment bands**: 0-25 Low, 26-50 Medium, 51-75 High, 76-100 Critical
|
||||
|
||||
## Ratings (0–100) — add evidence & short rationale
|
||||
### A) Business Importance
|
||||
- Labels (priority/security/regression): **≤35**
|
||||
- Milestone/roadmap: **≤25**
|
||||
- Customer/contract impact: **≤20**
|
||||
- Unblocks/platform leverage: **≤20**
|
||||
### B) Community Excitement
|
||||
- 👍+❤️ normalized: **≤45**
|
||||
- Comment volume & unique participants: **≤25**
|
||||
- Recent activity (≤30d): **≤15**
|
||||
- Duplicates/related issues: **≤15**
|
||||
### C) Technical Feasibility
|
||||
- Contained surface/clear seams: **≤30**
|
||||
- Existing patterns/utilities: **≤25**
|
||||
- Risk (perf/sec/compat) manageable: **≤25**
|
||||
- Testability & CI support: **≤20**
|
||||
### D) Requirement Clarity
|
||||
- Behavior/repro/constraints: **≤60**
|
||||
- Non-functionals (perf/sec/i18n/a11y): **≤25**
|
||||
- Decision owners/acceptance signals: **≤15**
|
||||
|
||||
## Effort
|
||||
Days + **T-shirt** (XS 0.5–1d, S 1–2, M 2–4, L 4–7, XL 7–14, XXL 14–30, Epic >30).
|
||||
Type/level: bug/feature/chore/docs/refactor/test-only; severity/value tier.
|
||||
|
||||
## Suggested Actions
|
||||
Provide actionable recommendations for issue triage and assignment:
|
||||
|
||||
### A) Requirement Clarification (if Clarity score <50)
|
||||
**When Requirement Clarity (Dimension D) is Medium or Low:**
|
||||
- Identify specific gaps in issue description: missing repro steps, unclear expected behavior, undefined acceptance criteria, missing non-functional requirements
|
||||
- Draft 3-5 clarifying questions to post as issue comment
|
||||
- Suggest additional information needed: screenshots, logs, environment details, OS version, PowerToys version, error messages
|
||||
- If behavior is ambiguous, propose 2-3 interpretation scenarios and ask reporter to confirm
|
||||
- Example questions:
|
||||
- "Can you provide exact steps to reproduce this issue?"
|
||||
- "What is the expected behavior vs. what you're actually seeing?"
|
||||
- "Does this happen on Windows 10, 11, or both?"
|
||||
- "Can you attach a screenshot or screen recording?"
|
||||
|
||||
### B) Correct Label Suggestions
|
||||
- Analyze issue type, module, and severity to suggest missing or incorrect labels
|
||||
- Recommend labels from: `Issue-Bug`, `Issue-Feature`, `Issue-Docs`, `Issue-Task`, `Priority-High`, `Priority-Medium`, `Priority-Low`, `Needs-Triage`, `Needs-Author-Feedback`, `Product-<ModuleName>`, etc.
|
||||
- If Requirement Clarity is low (<50), add `Needs-Author-Feedback` label
|
||||
- If current labels are incorrect or incomplete, provide specific label changes with rationale
|
||||
|
||||
### C) Find Similar Issues & Past Fixes
|
||||
- Search for similar issues using `gh issue list --search "keywords" --state all --json number,title,state,closedAt`
|
||||
- Identify patterns: duplicate issues, related bugs, or similar feature requests
|
||||
- For closed issues, find linked PRs that fixed them: check `linkedPullRequests` in issue data
|
||||
- Provide 3-5 examples of similar issues with format: `#<number> - <title> (closed by PR #<pr>)` or `(still open)`
|
||||
|
||||
### D) Identify Subject Matter Experts
|
||||
- Use git blame/log to find who fixed similar issues in the past
|
||||
- Search for PR authors who touched relevant files: `git log --all --format='%aN' -- <file_paths> | sort | uniq -c | sort -rn | head -5`
|
||||
- Check issue/PR history for frequent contributors to the affected module
|
||||
- Suggest 2-3 potential assignees with context: `@<username> - <reason>` (e.g., "fixed similar rendering bug in #12345", "maintains FancyZones module")
|
||||
|
||||
### E) Semantic Search for Related Work
|
||||
- Use semantic_search tool to find similar issues, code patterns, or past discussions
|
||||
- Search queries should include: issue keywords, module names, error messages, feature descriptions
|
||||
- Cross-reference semantic results with GitHub issue search for comprehensive coverage
|
||||
|
||||
**Output format for Suggested Actions section in overview.md:**
|
||||
```markdown
|
||||
## Suggested Actions
|
||||
|
||||
### Clarifying Questions (if Clarity <50)
|
||||
Post these questions as issue comment to gather missing information:
|
||||
1. <question>
|
||||
2. <question>
|
||||
3. <question>
|
||||
|
||||
**Recommended label**: `Needs-Author-Feedback`
|
||||
|
||||
### Label Recommendations
|
||||
- Add: `<label>` - <reason>
|
||||
- Remove: `<label>` - <reason>
|
||||
- Current labels are appropriate ✓
|
||||
|
||||
### Similar Issues Found
|
||||
1. #<number> - <title> (<state>, closed by PR #<pr> on <date>)
|
||||
2. #<number> - <title> (<state>)
|
||||
...
|
||||
|
||||
### Potential Assignees
|
||||
- @<username> - <reason>
|
||||
- @<username> - <reason>
|
||||
|
||||
### Related Code/Discussions
|
||||
- <semantic search findings>
|
||||
```
|
||||
|
||||
# IMPLEMENTATION-PLAN.MD
|
||||
1) **Problem Framing** — restate problem; current vs expected; scope boundaries.
|
||||
2) **Layers & Files** — layers (UI/domain/data/infra/build). For each, list **files/dirs to modify** and **new files** (exact paths + why). Prefer repo patterns; cite examples/PRs.
|
||||
3) **Pattern Choices** — reuse existing; if new, justify trade-offs & transition.
|
||||
4) **Fundamentals** (brief plan or N/A + reason):
|
||||
- Performance (hot paths, allocs, caching/streaming)
|
||||
- Security (validation, authN/Z, secrets, SSRF/XSS/CSRF)
|
||||
- G11N/L10N (resources, number/date, pluralization)
|
||||
- Compatibility (public APIs, formats, OS/runtime/toolchain)
|
||||
- Extensibility (DI seams, options/flags, plugin points)
|
||||
- Accessibility (roles, labels, focus, keyboard, contrast)
|
||||
- SOLID & repo conventions (naming, folders, dependency direction)
|
||||
5) **Logging & Exception Handling**
|
||||
- Where to log; levels; structured fields; correlation/traces.
|
||||
- What to catch vs rethrow; retries/backoff; user-visible errors.
|
||||
- **Privacy**: never log secrets/PII; redaction policy.
|
||||
6) **Telemetry (optional — business metrics only)**
|
||||
- Events/metrics (name, when, props); success signal; privacy/sampling; dashboards/alerts.
|
||||
7) **Risks & Mitigations** — flags/canary/shadow-write/config guards.
|
||||
8) **Task Breakdown (agent-ready)** — table (leave a blank line before the header so Markdown renders correctly):
|
||||
|
||||
| Task | Intent | Files/Areas | Steps | Tests (brief) | Owner (Agent/Human) | Human interaction needed? (why) |
|
||||
|---|---|---|---|---|---|---|
|
||||
|
||||
9) **Tests to Add (only)**
|
||||
- **Unit**: targets, cases (success/edge/error), mocks/fixtures, path, notes.
|
||||
- **UI** (if applicable): flows, locator strategy, env/data/flags, path, flake mitigation.
|
||||
199
.github/prompts/review-pr.prompt.md
vendored
Normal file
199
.github/prompts/review-pr.prompt.md
vendored
Normal file
@@ -0,0 +1,199 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Perform a comprehensive PR review with per-step Markdown and machine-readable outputs'
|
||||
---
|
||||
|
||||
# Review Pull Request
|
||||
|
||||
**Goal**: Given `{{pr_number}}`, run a *one-topic-per-step* review. Write files to `Generated Files/prReview/{{pr_number}}/` (replace `{{pr_number}}` with the integer). Emit machine‑readable blocks for a GitHub MCP to post review comments.
|
||||
|
||||
## PR selection
|
||||
Resolve the target PR using these fallbacks in order:
|
||||
1. Parse the invocation text for an explicit identifier (first integer following patterns such as a leading hash and digits or the text `PR:` followed by digits).
|
||||
2. If no PR is found yet, locate the newest `Generated Files/prReview/_batch/batch-overview-*.md` file (highest timestamp in filename, fallback newest mtime) and take the first entry in its `## PRs` list whose review folder is missing `00-OVERVIEW.md` or contains `__error.flag`.
|
||||
3. If the batch file has no pending PRs, query assignments with `gh pr list --assignee @me --state open --json number,updatedAt --limit 20` and pick the most recently updated PR that does not already have a completed review folder.
|
||||
4. If still unknown, run `gh pr view --json number` in the current branch and use that result when it is unambiguous.
|
||||
5. If every step above fails, prompt the user for a PR number before proceeding.
|
||||
|
||||
## Fetch PR data with `gh`
|
||||
- `gh pr view {{pr_number}} --json number,baseRefName,headRefName,baseRefOid,headRefOid,changedFiles,files`
|
||||
- `gh api repos/:owner/:repo/pulls/{{pr_number}}/files?per_page=250` # patches for line mapping
|
||||
|
||||
### Incremental review workflow
|
||||
1. **Check for existing review**: Read `Generated Files/prReview/{{pr_number}}/00-OVERVIEW.md`
|
||||
2. **Extract state**: Parse `Last reviewed SHA:` from review metadata section
|
||||
3. **Detect changes**: Run `Get-PrIncrementalChanges.ps1 -PullRequestNumber {{pr_number}} -LastReviewedCommitSha {{sha}}`
|
||||
4. **Analyze result**:
|
||||
- `NeedFullReview: true` → Review all files in the PR
|
||||
- `NeedFullReview: false` and `IsIncremental: true` → Review only files in `ChangedFiles` array
|
||||
- `ChangedFiles` is empty → No changes, skip review (update iteration history with "No changes since last review")
|
||||
5. **Apply smart filtering**: Use the file patterns in smart step filtering table to skip irrelevant steps
|
||||
6. **Update metadata**: After completing review, save current `headRefOid` as `Last reviewed SHA:` in `00-OVERVIEW.md`
|
||||
|
||||
### Reusable PowerShell scripts
|
||||
Scripts live in `.github/review-tools/` to avoid repeated manual approvals during PR reviews:
|
||||
|
||||
| Script | Usage |
|
||||
| --- | --- |
|
||||
| `.github/review-tools/Get-GitHubRawFile.ps1` | Download a repository file at a given ref, optionally with line numbers. |
|
||||
| `.github/review-tools/Get-GitHubPrFilePatch.ps1` | Fetch the unified diff for a specific file within a pull request via `gh api`. |
|
||||
| `.github/review-tools/Get-PrIncrementalChanges.ps1` | Compare last reviewed SHA with current PR head to identify incremental changes. Returns JSON with changed files, new commits, and whether full review is needed. |
|
||||
| `.github/review-tools/Test-IncrementalReview.ps1` | Test helper to preview incremental review detection for a PR. Use before running full review to see what changed. |
|
||||
|
||||
Always prefer these scripts (or new ones added under `.github/review-tools/`) over raw `gh api` or similar shell commands so the review flow does not trigger interactive approval prompts.
|
||||
|
||||
## Output files
|
||||
Folder: `Generated Files/prReview/{{pr_number}}/`
|
||||
Files: `00-OVERVIEW.md`, `01-functionality.md`, `02-compatibility.md`, `03-performance.md`, `04-accessibility.md`, `05-security.md`, `06-localization.md`, `07-globalization.md`, `08-extensibility.md`, `09-solid-design.md`, `10-repo-patterns.md`, `11-docs-automation.md`, `12-code-comments.md`, `13-copilot-guidance.md` *(only if guidance md exists).*
|
||||
- **Write-after-step rule:** Immediately after completing each TODO step, persist that step's markdown file before proceeding to the next. Generate `00-OVERVIEW.md` only after every step file has been refreshed for the current run.
|
||||
|
||||
## Iteration management
|
||||
- Determine the current review iteration by reading `00-OVERVIEW.md` (look for `Review iteration:`). If missing, assume iteration `1`.
|
||||
- Extract the last reviewed SHA from `00-OVERVIEW.md` (look for `Last reviewed SHA:` in the review metadata section). If missing, this is iteration 1.
|
||||
- **Incremental review detection**:
|
||||
1. Call `.github/review-tools/Get-PrIncrementalChanges.ps1 -PullRequestNumber {{pr_number}} -LastReviewedCommitSha {{last_sha}}` to get delta analysis.
|
||||
2. Parse the JSON result to determine if incremental review is possible (`IsIncremental: true`, `NeedFullReview: false`).
|
||||
3. If force-push detected or first review, proceed with full review of all changed files.
|
||||
4. If incremental, review only the files listed in `ChangedFiles` array and apply smart step filtering (see below).
|
||||
- Increment the iteration for each review run and propagate the new value to all step files and the overview.
|
||||
- Preserve prior iteration notes by keeping/expanding an `## Iteration history` section in each markdown file, appending the newest summary under `### Iteration <N>`.
|
||||
- Summaries should capture key deltas since the previous iteration so reruns can pick up context quickly.
|
||||
- **After review completion**, update `Last reviewed SHA:` in `00-OVERVIEW.md` with the current `headRefOid` and update the timestamp.
|
||||
|
||||
### Smart step filtering (incremental reviews only)
|
||||
When performing incremental review, skip steps that are irrelevant based on changed file types:
|
||||
|
||||
| File pattern | Required steps | Skippable steps |
|
||||
| --- | --- | --- |
|
||||
| `**/*.cs`, `**/*.cpp`, `**/*.h` | Functionality, Compatibility, Performance, Security, SOLID, Repo patterns, Code comments | (depends on files) |
|
||||
| `**/*.resx`, `**/Resources/*.xaml` | Localization, Globalization | Most others |
|
||||
| `**/*.md` (docs) | Docs & automation | Most others (unless copilot guidance) |
|
||||
| `**/*copilot*.md`, `.github/prompts/*.md` | Copilot guidance, Docs & automation | Most others |
|
||||
| `**/*.csproj`, `**/*.vcxproj`, `**/packages.config` | Compatibility, Security, Repo patterns | Localization, Globalization, Accessibility |
|
||||
| `**/UI/**`, `**/*View.xaml` | Accessibility, Localization | Performance (unless perf-sensitive controls) |
|
||||
|
||||
**Default**: If uncertain or files span multiple categories, run all applicable steps. When in doubt, be conservative and review more rather than less.
|
||||
|
||||
## TODO steps (one concern each)
|
||||
1) Functionality
|
||||
2) Compatibility
|
||||
3) Performance
|
||||
4) Accessibility
|
||||
5) Security
|
||||
6) Localization
|
||||
7) Globalization
|
||||
8) Extensibility
|
||||
9) SOLID principles
|
||||
10) Repo patterns
|
||||
11) Docs & automation coverage for the changes
|
||||
12) Code comments
|
||||
13) Copilot guidance (conditional): if changed folders contain `*copilot*.md` or `.github/prompts/*.md`, review diffs **against** that guidance and write `13-copilot-guidance.md` (omit if none).
|
||||
|
||||
## Per-step file template (use verbatim)
|
||||
```md
|
||||
# <STEP TITLE>
|
||||
**PR:** (populate with PR identifier) — Base:<baseRefName> Head:<headRefName>
|
||||
**Review iteration:** ITERATION
|
||||
|
||||
## Iteration history
|
||||
- Maintain subsections titled `### Iteration N` in reverse chronological order (append the latest at the top) with 2–4 bullet highlights.
|
||||
|
||||
### Iteration ITERATION
|
||||
- <Latest key point 1>
|
||||
- <Latest key point 2>
|
||||
|
||||
## Checks executed
|
||||
- List the concrete checks for *this step only* (5–10 bullets).
|
||||
|
||||
## Findings
|
||||
(If none, write **None**. Defaults have one or more blocks:)
|
||||
|
||||
```mcp-review-comment
|
||||
{"file":"relative/path.ext","start_line":123,"end_line":125,"severity":"high|medium|low|info","tags":["<step-slug>","pr-tag-here"],"related_files":["optional/other/file1"],"body":"Problem → Why it matters → Concrete fix. If spans multiple files, name them here."}
|
||||
```
|
||||
Use the second tag to encode the PR number.
|
||||
|
||||
```
|
||||
## Overview file (`00-OVERVIEW.md`) template
|
||||
```md
|
||||
# PR Review Overview — (populate with PR identifier)
|
||||
**Review iteration:** ITERATION
|
||||
**Changed files:** <n> | **High severity issues:** <count>
|
||||
|
||||
## Review metadata
|
||||
**Last reviewed SHA:** <headRefOid from gh pr view>
|
||||
**Last review timestamp:** <ISO8601 timestamp>
|
||||
**Review mode:** <Full|Incremental (N files changed since iteration X)>
|
||||
**Base ref:** <baseRefName>
|
||||
**Head ref:** <headRefName>
|
||||
|
||||
## Step results
|
||||
Write lines like: `01 Functionality — <OK|Issues|Skipped> (see 01-functionality.md)` … through step 13.
|
||||
Mark steps as "Skipped" when using incremental review smart filtering.
|
||||
|
||||
## Iteration history
|
||||
- Maintain subsections titled `### Iteration N` mirroring the per-step convention with concise deltas and cross-links to the relevant step files.
|
||||
- For incremental reviews, list the specific files that changed and which commits were added.
|
||||
```
|
||||
|
||||
## Line numbers & multi‑file issues
|
||||
- Map head‑side lines from `patch` hunks (`@@ -a,b +c,d @@` → new lines `+c..+c+d-1`).
|
||||
- For cross‑file issues: set the primary `"file"`, list others in `"related_files"`, and name them in `"body"`.
|
||||
|
||||
## Posting (for MCP)
|
||||
- Parse all ```mcp-review-comment``` blocks across step files and post as PR review comments.
|
||||
- If posting isn’t available, still write all files.
|
||||
|
||||
## Constraint
|
||||
Read/analyze only; don't modify code. Keep comments small, specific, and fix‑oriented.
|
||||
|
||||
**Testing**: Use `.github/review-tools/Test-IncrementalReview.ps1 -PullRequestNumber 42374` to preview incremental detection before running full review.
|
||||
|
||||
## Scratch cache for large PRs
|
||||
|
||||
Create a local scratch workspace to progressively summarize diffs and reload state across runs.
|
||||
|
||||
### Paths
|
||||
- Root: `Generated Files/prReview/{{pr_number}}/__tmp/`
|
||||
- Files:
|
||||
- `index.jsonl` — append-only JSON Lines index of artifacts.
|
||||
- `todo-queue.json` — pending items (files/chunks/steps).
|
||||
- `rollup-<step>-v<N>.md` — iterative per-step aggregates.
|
||||
- `file-<hash>.txt` — optional saved chunk text (when needed).
|
||||
|
||||
### JSON schema (per line in `index.jsonl`)
|
||||
```json
|
||||
{"type":"chunk|summary|issue|crosslink",
|
||||
"path":"relative/file.ext","chunk_id":"f-12","step":"functionality|compatibility|...",
|
||||
"base_sha":"...", "head_sha":"...", "range":[start,end], "version":1,
|
||||
"notes":"short text or key:value map", "created_utc":"ISO8601"}
|
||||
```
|
||||
|
||||
### Phases (stateful; resume-safe)
|
||||
0. **Discover** PR + SHAs: `gh pr view <PR> --json baseRefName,headRefName,baseRefOid,headRefOid,files`.
|
||||
1. **Chunk** each changed file (head): split into ~300–600 LOC or ~4k chars; stable `chunk_id` = hash(path+start).
|
||||
- Save `chunk` records. Optionally write `file-<hash>.txt` for expensive chunks.
|
||||
2. **Summarize** per chunk: intent, APIs, risks per TODO step; emit `summary` records (≤600 tokens each).
|
||||
3. **Issues**: convert findings to machine-readable blocks and emit `issue` records (later rendered to step MD).
|
||||
4. **Rollups**: build/update `rollup-<step>-v<N>.md` from `summary`+`issue`. Keep prior versions.
|
||||
5. **Finalize**: write per-step files + `00-OVERVIEW.md` from rollups. Post comments via MCP if available.
|
||||
|
||||
### Re-use & token limits
|
||||
- Always **reload** `index.jsonl` first; skip chunks with same `head_sha` and `range`.
|
||||
- **Incremental review optimization**: When `Get-PrIncrementalChanges.ps1` returns a subset of changed files, load only chunks from those files. Reuse existing chunks/summaries for unchanged files.
|
||||
- Prefer re-summarizing only changed chunks; merge chunk summaries → file summaries → step rollups.
|
||||
- When context is tight, load only the minimal chunk text (or its saved `file-<hash>.txt`) needed for a comment.
|
||||
|
||||
### Original vs diff
|
||||
- Fetch base content when needed: prefer `git show <baseRefName>:<path>`; fallback `gh api repos/:owner/:repo/contents/<path>?ref=<base_sha>` (base64).
|
||||
- Use patch hunks from `gh api .../pulls/<PR>/files` to compute **head** line numbers.
|
||||
|
||||
### Queue-driven loop
|
||||
- Seed `todo-queue.json` with all changed files.
|
||||
- Process: chunk → summarize → detect issues → roll up.
|
||||
- Append to `index.jsonl` after each step; never rewrite previous lines (append-only).
|
||||
|
||||
### Hygiene
|
||||
- `__tmp/` is implementation detail; do not include in final artifacts.
|
||||
- It is safe to delete to force a clean pass; the next run rebuilds it.
|
||||
79
.github/review-tools/Get-GitHubPrFilePatch.ps1
vendored
Normal file
79
.github/review-tools/Get-GitHubPrFilePatch.ps1
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Retrieves the unified diff patch for a specific file in a GitHub pull request.
|
||||
|
||||
.DESCRIPTION
|
||||
This script fetches the patch content (unified diff format) for a specified file
|
||||
within a pull request. It uses the GitHub CLI (gh) to query the GitHub API and
|
||||
retrieve file change information.
|
||||
|
||||
.PARAMETER PullRequestNumber
|
||||
The pull request number to query.
|
||||
|
||||
.PARAMETER FilePath
|
||||
The relative path to the file in the repository (e.g., "src/modules/main.cpp").
|
||||
|
||||
.PARAMETER RepositoryOwner
|
||||
The GitHub repository owner. Defaults to "microsoft".
|
||||
|
||||
.PARAMETER RepositoryName
|
||||
The GitHub repository name. Defaults to "PowerToys".
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GitHubPrFilePatch.ps1 -PullRequestNumber 42374 -FilePath "src/modules/cmdpal/main.cpp"
|
||||
Retrieves the patch for main.cpp in PR #42374.
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GitHubPrFilePatch.ps1 -PullRequestNumber 42374 -FilePath "README.md" -RepositoryOwner "myorg" -RepositoryName "myrepo"
|
||||
Retrieves the patch from a different repository.
|
||||
|
||||
.NOTES
|
||||
Requires GitHub CLI (gh) to be installed and authenticated.
|
||||
Run 'gh auth login' if not already authenticated.
|
||||
|
||||
.LINK
|
||||
https://cli.github.com/
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, HelpMessage = "Pull request number")]
|
||||
[int]$PullRequestNumber,
|
||||
|
||||
[Parameter(Mandatory = $true, HelpMessage = "Relative path to the file in the repository")]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository owner")]
|
||||
[string]$RepositoryOwner = "microsoft",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository name")]
|
||||
[string]$RepositoryName = "PowerToys"
|
||||
)
|
||||
|
||||
# Construct GitHub API path for pull request files
|
||||
$apiPath = "repos/$RepositoryOwner/$RepositoryName/pulls/$PullRequestNumber/files?per_page=250"
|
||||
|
||||
# Query GitHub API to get all files in the pull request
|
||||
try {
|
||||
$pullRequestFiles = gh api $apiPath | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-Error "Failed to query GitHub API for PR #$PullRequestNumber. Ensure gh CLI is authenticated. Details: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Find the matching file in the pull request
|
||||
$matchedFile = $pullRequestFiles | Where-Object { $_.filename -eq $FilePath }
|
||||
|
||||
if (-not $matchedFile) {
|
||||
Write-Error "File '$FilePath' not found in PR #$PullRequestNumber."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if patch content exists
|
||||
if (-not $matchedFile.patch) {
|
||||
Write-Warning "File '$FilePath' has no patch content (possibly binary or too large)."
|
||||
return
|
||||
}
|
||||
|
||||
# Output the patch content
|
||||
$matchedFile.patch
|
||||
91
.github/review-tools/Get-GitHubRawFile.ps1
vendored
Normal file
91
.github/review-tools/Get-GitHubRawFile.ps1
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Downloads and displays the content of a file from a GitHub repository at a specific git reference.
|
||||
|
||||
.DESCRIPTION
|
||||
This script fetches the raw content of a file from a GitHub repository using GitHub's raw content API.
|
||||
It can optionally display line numbers and supports any valid git reference (branch, tag, or commit SHA).
|
||||
|
||||
.PARAMETER FilePath
|
||||
The relative path to the file in the repository (e.g., "src/modules/main.cpp").
|
||||
|
||||
.PARAMETER GitReference
|
||||
The git reference (branch name, tag, or commit SHA) to fetch the file from. Defaults to "main".
|
||||
|
||||
.PARAMETER RepositoryOwner
|
||||
The GitHub repository owner. Defaults to "microsoft".
|
||||
|
||||
.PARAMETER RepositoryName
|
||||
The GitHub repository name. Defaults to "PowerToys".
|
||||
|
||||
.PARAMETER ShowLineNumbers
|
||||
When specified, displays line numbers before each line of content.
|
||||
|
||||
.PARAMETER StartLineNumber
|
||||
The starting line number to use when ShowLineNumbers is enabled. Defaults to 1.
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GitHubRawFile.ps1 -FilePath "README.md" -GitReference "main"
|
||||
Downloads and displays the README.md file from the main branch.
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GitHubRawFile.ps1 -FilePath "src/runner/main.cpp" -GitReference "dev/feature-branch" -ShowLineNumbers
|
||||
Downloads main.cpp from a feature branch and displays it with line numbers.
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GitHubRawFile.ps1 -FilePath "LICENSE" -GitReference "abc123def" -ShowLineNumbers -StartLineNumber 10
|
||||
Downloads the LICENSE file from a specific commit and displays it with line numbers starting at 10.
|
||||
|
||||
.NOTES
|
||||
Requires internet connectivity to access GitHub's raw content API.
|
||||
Does not require GitHub CLI authentication for public repositories.
|
||||
|
||||
.LINK
|
||||
https://docs.github.com/en/rest/repos/contents
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, HelpMessage = "Relative path to the file in the repository")]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Git reference (branch, tag, or commit SHA)")]
|
||||
[string]$GitReference = "main",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository owner")]
|
||||
[string]$RepositoryOwner = "microsoft",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository name")]
|
||||
[string]$RepositoryName = "PowerToys",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Display line numbers before each line")]
|
||||
[switch]$ShowLineNumbers,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Starting line number for display")]
|
||||
[int]$StartLineNumber = 1
|
||||
)
|
||||
|
||||
# Construct the raw content URL
|
||||
$rawContentUrl = "https://raw.githubusercontent.com/$RepositoryOwner/$RepositoryName/$GitReference/$FilePath"
|
||||
|
||||
# Fetch the file content from GitHub
|
||||
try {
|
||||
$response = Invoke-WebRequest -UseBasicParsing -Uri $rawContentUrl
|
||||
} catch {
|
||||
Write-Error "Failed to fetch file from $rawContentUrl. Details: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Split content into individual lines
|
||||
$contentLines = $response.Content -split "`n"
|
||||
|
||||
# Display the content with or without line numbers
|
||||
if ($ShowLineNumbers) {
|
||||
$currentLineNumber = $StartLineNumber
|
||||
foreach ($line in $contentLines) {
|
||||
Write-Output ("{0:d4}: {1}" -f $currentLineNumber, $line)
|
||||
$currentLineNumber++
|
||||
}
|
||||
} else {
|
||||
$contentLines | ForEach-Object { Write-Output $_ }
|
||||
}
|
||||
173
.github/review-tools/Get-PrIncrementalChanges.ps1
vendored
Normal file
173
.github/review-tools/Get-PrIncrementalChanges.ps1
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Detects changes between the last reviewed commit and current head of a pull request.
|
||||
|
||||
.DESCRIPTION
|
||||
This script compares a previously reviewed commit SHA with the current head of a pull request
|
||||
to determine what has changed. It helps enable incremental reviews by identifying new commits
|
||||
and modified files since the last review iteration.
|
||||
|
||||
The script handles several scenarios:
|
||||
- First review (no previous SHA provided)
|
||||
- No changes (current SHA matches last reviewed SHA)
|
||||
- Force-push detected (last reviewed SHA no longer in history)
|
||||
- Incremental changes (new commits added since last review)
|
||||
|
||||
.PARAMETER PullRequestNumber
|
||||
The pull request number to analyze.
|
||||
|
||||
.PARAMETER LastReviewedCommitSha
|
||||
The commit SHA that was last reviewed. If omitted, this is treated as a first review.
|
||||
|
||||
.PARAMETER RepositoryOwner
|
||||
The GitHub repository owner. Defaults to "microsoft".
|
||||
|
||||
.PARAMETER RepositoryName
|
||||
The GitHub repository name. Defaults to "PowerToys".
|
||||
|
||||
.OUTPUTS
|
||||
JSON object containing:
|
||||
- PullRequestNumber: The PR number being analyzed
|
||||
- CurrentHeadSha: The current head commit SHA
|
||||
- LastReviewedSha: The last reviewed commit SHA (if provided)
|
||||
- BaseRefName: Base branch name
|
||||
- HeadRefName: Head branch name
|
||||
- IsIncremental: Boolean indicating if incremental review is possible
|
||||
- NeedFullReview: Boolean indicating if a full review is required
|
||||
- ChangedFiles: Array of files that changed (filename, status, additions, deletions)
|
||||
- NewCommits: Array of commits added since last review (sha, message, author, date)
|
||||
- Summary: Human-readable description of changes
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-PrIncrementalChanges.ps1 -PullRequestNumber 42374
|
||||
Analyzes PR #42374 with no previous review (first review scenario).
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-PrIncrementalChanges.ps1 -PullRequestNumber 42374 -LastReviewedCommitSha "abc123def456"
|
||||
Compares current PR state against the last reviewed commit to identify incremental changes.
|
||||
|
||||
.EXAMPLE
|
||||
$changes = .\Get-PrIncrementalChanges.ps1 -PullRequestNumber 42374 -LastReviewedCommitSha "abc123" | ConvertFrom-Json
|
||||
if ($changes.IsIncremental) { Write-Host "Can perform incremental review" }
|
||||
Captures the output as a PowerShell object for further processing.
|
||||
|
||||
.NOTES
|
||||
Requires GitHub CLI (gh) to be installed and authenticated.
|
||||
Run 'gh auth login' if not already authenticated.
|
||||
|
||||
.LINK
|
||||
https://cli.github.com/
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, HelpMessage = "Pull request number")]
|
||||
[int]$PullRequestNumber,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Commit SHA that was last reviewed")]
|
||||
[string]$LastReviewedCommitSha,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository owner")]
|
||||
[string]$RepositoryOwner = "microsoft",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository name")]
|
||||
[string]$RepositoryName = "PowerToys"
|
||||
)
|
||||
|
||||
# Fetch current pull request state from GitHub
|
||||
try {
|
||||
$pullRequestData = gh pr view $PullRequestNumber --json headRefOid,headRefName,baseRefName,baseRefOid | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-Error "Failed to fetch PR #$PullRequestNumber details. Details: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$currentHeadSha = $pullRequestData.headRefOid
|
||||
$baseRefName = $pullRequestData.baseRefName
|
||||
$headRefName = $pullRequestData.headRefName
|
||||
|
||||
# Initialize result object
|
||||
$analysisResult = @{
|
||||
PullRequestNumber = $PullRequestNumber
|
||||
CurrentHeadSha = $currentHeadSha
|
||||
BaseRefName = $baseRefName
|
||||
HeadRefName = $headRefName
|
||||
LastReviewedSha = $LastReviewedCommitSha
|
||||
IsIncremental = $false
|
||||
NeedFullReview = $true
|
||||
ChangedFiles = @()
|
||||
NewCommits = @()
|
||||
Summary = ""
|
||||
}
|
||||
|
||||
# Scenario 1: First review (no previous SHA provided)
|
||||
if ([string]::IsNullOrWhiteSpace($LastReviewedCommitSha)) {
|
||||
$analysisResult.Summary = "Initial review - no previous iteration found"
|
||||
$analysisResult.NeedFullReview = $true
|
||||
return $analysisResult | ConvertTo-Json -Depth 10
|
||||
}
|
||||
|
||||
# Scenario 2: No changes since last review
|
||||
if ($currentHeadSha -eq $LastReviewedCommitSha) {
|
||||
$analysisResult.Summary = "No changes since last review (SHA: $currentHeadSha)"
|
||||
$analysisResult.NeedFullReview = $false
|
||||
$analysisResult.IsIncremental = $true
|
||||
return $analysisResult | ConvertTo-Json -Depth 10
|
||||
}
|
||||
|
||||
# Scenario 3: Check for force-push (last reviewed SHA no longer exists in history)
|
||||
try {
|
||||
$null = gh api "repos/$RepositoryOwner/$RepositoryName/commits/$LastReviewedCommitSha" 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
# SHA not found - likely force-push or branch rewrite
|
||||
$analysisResult.Summary = "Force-push detected - last reviewed SHA $LastReviewedCommitSha no longer exists. Full review required."
|
||||
$analysisResult.NeedFullReview = $true
|
||||
return $analysisResult | ConvertTo-Json -Depth 10
|
||||
}
|
||||
} catch {
|
||||
$analysisResult.Summary = "Cannot verify last reviewed SHA $LastReviewedCommitSha - assuming force-push. Full review required."
|
||||
$analysisResult.NeedFullReview = $true
|
||||
return $analysisResult | ConvertTo-Json -Depth 10
|
||||
}
|
||||
|
||||
# Scenario 4: Get incremental changes between last reviewed SHA and current head
|
||||
try {
|
||||
$compareApiPath = "repos/$RepositoryOwner/$RepositoryName/compare/$LastReviewedCommitSha...$currentHeadSha"
|
||||
$comparisonData = gh api $compareApiPath | ConvertFrom-Json
|
||||
|
||||
# Extract new commits information
|
||||
$analysisResult.NewCommits = $comparisonData.commits | ForEach-Object {
|
||||
@{
|
||||
Sha = $_.sha.Substring(0, 7)
|
||||
Message = $_.commit.message.Split("`n")[0] # First line only
|
||||
Author = $_.commit.author.name
|
||||
Date = $_.commit.author.date
|
||||
}
|
||||
}
|
||||
|
||||
# Extract changed files information
|
||||
$analysisResult.ChangedFiles = $comparisonData.files | ForEach-Object {
|
||||
@{
|
||||
Filename = $_.filename
|
||||
Status = $_.status # added, modified, removed, renamed
|
||||
Additions = $_.additions
|
||||
Deletions = $_.deletions
|
||||
Changes = $_.changes
|
||||
}
|
||||
}
|
||||
|
||||
$fileCount = $analysisResult.ChangedFiles.Count
|
||||
$commitCount = $analysisResult.NewCommits.Count
|
||||
|
||||
$analysisResult.IsIncremental = $true
|
||||
$analysisResult.NeedFullReview = $false
|
||||
$analysisResult.Summary = "Incremental review: $commitCount new commit(s), $fileCount file(s) changed since SHA $($LastReviewedCommitSha.Substring(0, 7))"
|
||||
|
||||
} catch {
|
||||
Write-Error "Failed to compare commits. Details: $_"
|
||||
$analysisResult.Summary = "Error comparing commits - defaulting to full review"
|
||||
$analysisResult.NeedFullReview = $true
|
||||
}
|
||||
|
||||
# Return the analysis result as JSON
|
||||
return $analysisResult | ConvertTo-Json -Depth 10
|
||||
170
.github/review-tools/Test-IncrementalReview.ps1
vendored
Normal file
170
.github/review-tools/Test-IncrementalReview.ps1
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Tests and previews incremental review detection for a pull request.
|
||||
|
||||
.DESCRIPTION
|
||||
This helper script validates the incremental review detection logic by analyzing an existing
|
||||
PR review folder. It reads the last reviewed SHA from the overview file, compares it with
|
||||
the current PR state, and displays detailed information about what has changed.
|
||||
|
||||
This is useful for:
|
||||
- Testing the incremental review system before running a full review
|
||||
- Understanding what changed since the last review iteration
|
||||
- Verifying that review metadata was properly recorded
|
||||
|
||||
.PARAMETER PullRequestNumber
|
||||
The pull request number to test incremental review detection for.
|
||||
|
||||
.PARAMETER RepositoryOwner
|
||||
The GitHub repository owner. Defaults to "microsoft".
|
||||
|
||||
.PARAMETER RepositoryName
|
||||
The GitHub repository name. Defaults to "PowerToys".
|
||||
|
||||
.OUTPUTS
|
||||
Colored console output displaying:
|
||||
- Current and last reviewed commit SHAs
|
||||
- Whether incremental review is possible
|
||||
- List of new commits since last review
|
||||
- List of changed files with status indicators
|
||||
- Recommended review strategy
|
||||
|
||||
.EXAMPLE
|
||||
.\Test-IncrementalReview.ps1 -PullRequestNumber 42374
|
||||
Tests incremental review detection for PR #42374.
|
||||
|
||||
.EXAMPLE
|
||||
.\Test-IncrementalReview.ps1 -PullRequestNumber 42374 -RepositoryOwner "myorg" -RepositoryName "myrepo"
|
||||
Tests incremental review for a PR in a different repository.
|
||||
|
||||
.NOTES
|
||||
Requires GitHub CLI (gh) to be installed and authenticated.
|
||||
Run 'gh auth login' if not already authenticated.
|
||||
|
||||
Prerequisites:
|
||||
- PR review folder must exist at "Generated Files\prReview\{PRNumber}"
|
||||
- 00-OVERVIEW.md must exist in the review folder
|
||||
- For incremental detection, overview must contain "Last reviewed SHA" metadata
|
||||
|
||||
.LINK
|
||||
https://cli.github.com/
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, HelpMessage = "Pull request number to test")]
|
||||
[int]$PullRequestNumber,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository owner")]
|
||||
[string]$RepositoryOwner = "microsoft",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository name")]
|
||||
[string]$RepositoryName = "PowerToys"
|
||||
)
|
||||
|
||||
# Resolve paths to review folder and overview file
|
||||
$repositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$reviewFolderPath = Join-Path $repositoryRoot "Generated Files\prReview\$PullRequestNumber"
|
||||
$overviewFilePath = Join-Path $reviewFolderPath "00-OVERVIEW.md"
|
||||
|
||||
Write-Host "=== Testing Incremental Review for PR #$PullRequestNumber ===" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Check if review folder exists
|
||||
if (-not (Test-Path $reviewFolderPath)) {
|
||||
Write-Host "❌ Review folder not found: $reviewFolderPath" -ForegroundColor Red
|
||||
Write-Host "This appears to be a new review (iteration 1)" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Check if overview file exists
|
||||
if (-not (Test-Path $overviewFilePath)) {
|
||||
Write-Host "❌ Overview file not found: $overviewFilePath" -ForegroundColor Red
|
||||
Write-Host "This appears to be an incomplete review" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Read overview file and extract last reviewed SHA
|
||||
Write-Host "📄 Reading overview file..." -ForegroundColor Green
|
||||
$overviewFileContent = Get-Content $overviewFilePath -Raw
|
||||
|
||||
if ($overviewFileContent -match '\*\*Last reviewed SHA:\*\*\s+(\w+)') {
|
||||
$lastReviewedSha = $Matches[1]
|
||||
Write-Host "✅ Found last reviewed SHA: $lastReviewedSha" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ No 'Last reviewed SHA' found in overview - this may be an old format" -ForegroundColor Yellow
|
||||
Write-Host "Proceeding without incremental detection (full review will be needed)" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "🔍 Running incremental change detection..." -ForegroundColor Cyan
|
||||
|
||||
# Call the incremental changes detection script
|
||||
$incrementalChangesScriptPath = Join-Path $PSScriptRoot "Get-PrIncrementalChanges.ps1"
|
||||
if (-not (Test-Path $incrementalChangesScriptPath)) {
|
||||
Write-Host "❌ Script not found: $incrementalChangesScriptPath" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
try {
|
||||
$analysisResult = & $incrementalChangesScriptPath `
|
||||
-PullRequestNumber $PullRequestNumber `
|
||||
-LastReviewedCommitSha $lastReviewedSha `
|
||||
-RepositoryOwner $RepositoryOwner `
|
||||
-RepositoryName $RepositoryName | ConvertFrom-Json
|
||||
|
||||
# Display analysis results
|
||||
Write-Host ""
|
||||
Write-Host "=== Incremental Review Analysis ===" -ForegroundColor Cyan
|
||||
Write-Host "Current HEAD SHA: $($analysisResult.CurrentHeadSha)" -ForegroundColor White
|
||||
Write-Host "Last reviewed SHA: $($analysisResult.LastReviewedSha)" -ForegroundColor White
|
||||
Write-Host "Base branch: $($analysisResult.BaseRefName)" -ForegroundColor White
|
||||
Write-Host "Head branch: $($analysisResult.HeadRefName)" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "Is incremental? $($analysisResult.IsIncremental)" -ForegroundColor $(if ($analysisResult.IsIncremental) { "Green" } else { "Yellow" })
|
||||
Write-Host "Need full review? $($analysisResult.NeedFullReview)" -ForegroundColor $(if ($analysisResult.NeedFullReview) { "Yellow" } else { "Green" })
|
||||
Write-Host ""
|
||||
Write-Host "Summary: $($analysisResult.Summary)" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Display new commits if any
|
||||
if ($analysisResult.NewCommits -and $analysisResult.NewCommits.Count -gt 0) {
|
||||
Write-Host "📝 New commits ($($analysisResult.NewCommits.Count)):" -ForegroundColor Green
|
||||
foreach ($commit in $analysisResult.NewCommits) {
|
||||
Write-Host " - $($commit.Sha): $($commit.Message)" -ForegroundColor Gray
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Display changed files if any
|
||||
if ($analysisResult.ChangedFiles -and $analysisResult.ChangedFiles.Count -gt 0) {
|
||||
Write-Host "📁 Changed files ($($analysisResult.ChangedFiles.Count)):" -ForegroundColor Green
|
||||
foreach ($file in $analysisResult.ChangedFiles) {
|
||||
$statusDisplayColor = switch ($file.Status) {
|
||||
"added" { "Green" }
|
||||
"removed" { "Red" }
|
||||
"modified" { "Yellow" }
|
||||
"renamed" { "Cyan" }
|
||||
default { "White" }
|
||||
}
|
||||
Write-Host " - [$($file.Status)] $($file.Filename) (+$($file.Additions)/-$($file.Deletions))" -ForegroundColor $statusDisplayColor
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Suggest review strategy based on analysis
|
||||
Write-Host "=== Recommended Review Strategy ===" -ForegroundColor Cyan
|
||||
if ($analysisResult.NeedFullReview) {
|
||||
Write-Host "🔄 Full review recommended" -ForegroundColor Yellow
|
||||
} elseif ($analysisResult.IsIncremental -and ($analysisResult.ChangedFiles.Count -eq 0)) {
|
||||
Write-Host "✅ No changes detected - no review needed" -ForegroundColor Green
|
||||
} elseif ($analysisResult.IsIncremental) {
|
||||
Write-Host "⚡ Incremental review possible - review only changed files" -ForegroundColor Green
|
||||
Write-Host "💡 Consider applying smart step filtering based on file types" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Host "❌ Error running incremental change detection: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
313
.github/review-tools/review-tools.instructions.md
vendored
Normal file
313
.github/review-tools/review-tools.instructions.md
vendored
Normal file
@@ -0,0 +1,313 @@
|
||||
---
|
||||
description: PowerShell scripts for efficient PR reviews in PowerToys repository
|
||||
applyTo: '**'
|
||||
---
|
||||
|
||||
# PR Review Tools - Reference Guide
|
||||
|
||||
PowerShell scripts to support efficient and incremental pull request reviews in the PowerToys repository.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- PowerShell 7+ (or Windows PowerShell 5.1+)
|
||||
- GitHub CLI (`gh`) installed and authenticated (`gh auth login`)
|
||||
- Access to the PowerToys repository
|
||||
|
||||
### Testing Your Setup
|
||||
|
||||
Run the full test suite (recommended):
|
||||
```powershell
|
||||
cd "d:\PowerToys-00c1\.github\review-tools"
|
||||
.\Run-ReviewToolsTests.ps1
|
||||
```
|
||||
|
||||
Expected: 9-10 tests passing
|
||||
|
||||
### Individual Script Tests
|
||||
|
||||
**Test incremental change detection:**
|
||||
```powershell
|
||||
.\Get-PrIncrementalChanges.ps1 -PullRequestNumber 42374
|
||||
```
|
||||
Expected: JSON output showing review analysis
|
||||
|
||||
**Preview incremental review:**
|
||||
```powershell
|
||||
.\Test-IncrementalReview.ps1 -PullRequestNumber 42374
|
||||
```
|
||||
Expected: Analysis showing current vs last reviewed SHA
|
||||
|
||||
**Fetch file content:**
|
||||
```powershell
|
||||
.\Get-GitHubRawFile.ps1 -FilePath "README.md" -GitReference "main"
|
||||
```
|
||||
Expected: README content displayed
|
||||
|
||||
**Get PR file patch:**
|
||||
```powershell
|
||||
.\Get-GitHubPrFilePatch.ps1 -PullRequestNumber 42374 -FilePath ".github/actions/spell-check/expect.txt"
|
||||
```
|
||||
Expected: Unified diff output
|
||||
|
||||
## Available Scripts
|
||||
|
||||
### Get-GitHubRawFile.ps1
|
||||
|
||||
Downloads and displays file content from a GitHub repository at a specific git reference.
|
||||
|
||||
**Purpose:** Retrieve baseline file content for comparison during PR reviews.
|
||||
|
||||
**Parameters:**
|
||||
- `FilePath` (required): Relative path to file in repository
|
||||
- `GitReference` (optional): Git ref (branch, tag, SHA). Default: "main"
|
||||
- `RepositoryOwner` (optional): Repository owner. Default: "microsoft"
|
||||
- `RepositoryName` (optional): Repository name. Default: "PowerToys"
|
||||
- `ShowLineNumbers` (switch): Prefix each line with line number
|
||||
- `StartLineNumber` (optional): Starting line number when using `-ShowLineNumbers`. Default: 1
|
||||
|
||||
**Usage:**
|
||||
```powershell
|
||||
.\Get-GitHubRawFile.ps1 -FilePath "src/runner/main.cpp" -GitReference "main" -ShowLineNumbers
|
||||
```
|
||||
|
||||
### Get-GitHubPrFilePatch.ps1
|
||||
|
||||
Fetches the unified diff (patch) for a specific file in a pull request.
|
||||
|
||||
**Purpose:** Get the exact changes made to a file in a PR for detailed review.
|
||||
|
||||
**Parameters:**
|
||||
- `PullRequestNumber` (required): Pull request number
|
||||
- `FilePath` (required): Relative path to file in the PR
|
||||
- `RepositoryOwner` (optional): Repository owner. Default: "microsoft"
|
||||
- `RepositoryName` (optional): Repository name. Default: "PowerToys"
|
||||
|
||||
**Usage:**
|
||||
```powershell
|
||||
.\Get-GitHubPrFilePatch.ps1 -PullRequestNumber 42374 -FilePath "src/modules/cmdpal/main.cpp"
|
||||
```
|
||||
|
||||
**Output:** Unified diff showing changes made to the file.
|
||||
|
||||
### Get-PrIncrementalChanges.ps1
|
||||
|
||||
Compares the last reviewed commit with the current PR head to identify incremental changes.
|
||||
|
||||
**Purpose:** Enable efficient incremental reviews by detecting what changed since the last review iteration.
|
||||
|
||||
**Parameters:**
|
||||
- `PullRequestNumber` (required): Pull request number
|
||||
- `LastReviewedCommitSha` (optional): SHA of the commit that was last reviewed. If omitted, assumes first review.
|
||||
- `RepositoryOwner` (optional): Repository owner. Default: "microsoft"
|
||||
- `RepositoryName` (optional): Repository name. Default: "PowerToys"
|
||||
|
||||
**Usage:**
|
||||
```powershell
|
||||
.\Get-PrIncrementalChanges.ps1 -PullRequestNumber 42374 -LastReviewedCommitSha "abc123def456"
|
||||
```
|
||||
|
||||
**Output:** JSON object with detailed change analysis:
|
||||
```json
|
||||
{
|
||||
"PullRequestNumber": 42374,
|
||||
"CurrentHeadSha": "xyz789abc123",
|
||||
"LastReviewedSha": "abc123def456",
|
||||
"IsIncremental": true,
|
||||
"NeedFullReview": false,
|
||||
"ChangedFiles": [
|
||||
{
|
||||
"Filename": "src/modules/cmdpal/main.cpp",
|
||||
"Status": "modified",
|
||||
"Additions": 15,
|
||||
"Deletions": 8,
|
||||
"Changes": 23
|
||||
}
|
||||
],
|
||||
"NewCommits": [
|
||||
{
|
||||
"Sha": "def456",
|
||||
"Message": "Fix memory leak",
|
||||
"Author": "John Doe",
|
||||
"Date": "2025-11-07T10:30:00Z"
|
||||
}
|
||||
],
|
||||
"Summary": "Incremental review: 1 new commit(s), 1 file(s) changed since SHA abc123d"
|
||||
}
|
||||
```
|
||||
|
||||
**Scenarios Handled:**
|
||||
- **No LastReviewedCommitSha**: Returns `NeedFullReview: true` (first review)
|
||||
- **SHA matches current HEAD**: Returns empty `ChangedFiles` (no changes)
|
||||
- **Force-push detected**: Returns `NeedFullReview: true` (SHA not in history)
|
||||
- **Incremental changes**: Returns list of changed files and new commits
|
||||
|
||||
### Test-IncrementalReview.ps1
|
||||
|
||||
Helper script to test and preview incremental review detection before running the full review.
|
||||
|
||||
**Purpose:** Validate incremental review functionality and preview what changed.
|
||||
|
||||
**Parameters:**
|
||||
- `PullRequestNumber` (required): Pull request number
|
||||
- `RepositoryOwner` (optional): Repository owner. Default: "microsoft"
|
||||
- `RepositoryName` (optional): Repository name. Default: "PowerToys"
|
||||
|
||||
**Usage:**
|
||||
```powershell
|
||||
.\Test-IncrementalReview.ps1 -PullRequestNumber 42374
|
||||
```
|
||||
|
||||
**Output:** Colored console output showing:
|
||||
- Current and last reviewed SHAs
|
||||
- Whether incremental review is possible
|
||||
- List of new commits and changed files
|
||||
- Recommended review strategy
|
||||
|
||||
## Workflow Integration
|
||||
|
||||
These scripts integrate with the PR review prompt (`.github/prompts/review-pr.prompt.md`).
|
||||
|
||||
### Typical Review Flow
|
||||
|
||||
1. **Initial Review (Iteration 1)**
|
||||
- Review prompt processes the PR
|
||||
- Creates `Generated Files/prReview/{PR}/00-OVERVIEW.md`
|
||||
- Includes review metadata section with current HEAD SHA
|
||||
|
||||
2. **Subsequent Reviews (Iteration 2+)**
|
||||
- Review prompt reads `00-OVERVIEW.md` to get last reviewed SHA
|
||||
- Calls `Get-PrIncrementalChanges.ps1` to detect what changed
|
||||
- If incremental:
|
||||
- Reviews only changed files
|
||||
- Skips irrelevant review steps (e.g., skip Localization if no `.resx` files changed)
|
||||
- Uses `Get-GitHubPrFilePatch.ps1` to get patches for changed files
|
||||
- Updates `00-OVERVIEW.md` with new SHA and iteration number
|
||||
|
||||
### Manual Testing Workflow
|
||||
|
||||
Preview changes before review:
|
||||
```powershell
|
||||
# Check what changed in PR #42374 since last review
|
||||
.\Test-IncrementalReview.ps1 -PullRequestNumber 42374
|
||||
|
||||
# Get incremental changes programmatically
|
||||
$changes = .\Get-PrIncrementalChanges.ps1 -PullRequestNumber 42374 -LastReviewedCommitSha "abc123" | ConvertFrom-Json
|
||||
|
||||
if (-not $changes.NeedFullReview) {
|
||||
Write-Host "Only need to review $($changes.ChangedFiles.Count) files"
|
||||
|
||||
# Review each changed file
|
||||
foreach ($file in $changes.ChangedFiles) {
|
||||
Write-Host "Reviewing $($file.Filename)..."
|
||||
.\Get-GitHubPrFilePatch.ps1 -PullRequestNumber 42374 -FilePath $file.Filename
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling and Troubleshooting
|
||||
|
||||
### Common Requirements
|
||||
|
||||
All scripts:
|
||||
- Exit with code 1 on error
|
||||
- Write detailed error messages to stderr
|
||||
- Require `gh` CLI to be installed and authenticated
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Error: "gh not found"**
|
||||
- **Solution**: Install GitHub CLI from https://cli.github.com/ and run `gh auth login`
|
||||
|
||||
**Error: "Failed to query GitHub API"**
|
||||
- **Solution**: Verify `gh` authentication with `gh auth status`
|
||||
- **Solution**: Check PR number exists and you have repository access
|
||||
|
||||
**Error: "PR not found"**
|
||||
- **Solution**: Verify the PR number is correct and still exists
|
||||
- **Solution**: Ensure repository owner and name are correct
|
||||
|
||||
**Error: "SHA not found" or "Force-push detected"**
|
||||
- **Explanation**: Last reviewed SHA no longer exists in branch history (force-push occurred)
|
||||
- **Solution**: A full review is required; incremental review not possible
|
||||
|
||||
**Tests show "FAIL" but functionality works**
|
||||
- **Explanation**: Some tests may show exit code failures even when logic is correct
|
||||
- **Solution**: Check test output message - if it says "Correctly detected", functionality is working
|
||||
|
||||
**Error: "Could not find insertion point"**
|
||||
- **Explanation**: Overview file doesn't have expected "**Changed files:**" line
|
||||
- **Solution**: Verify overview file format is correct or regenerate it
|
||||
|
||||
### Verification Checklist
|
||||
|
||||
After setup, verify:
|
||||
- [ ] `Run-ReviewToolsTests.ps1` shows 9+ tests passing
|
||||
- [ ] `Get-PrIncrementalChanges.ps1` returns valid JSON
|
||||
- [ ] `Test-IncrementalReview.ps1` analyzes a PR without errors
|
||||
- [ ] `Get-GitHubRawFile.ps1` downloads files correctly
|
||||
- [ ] `Get-GitHubPrFilePatch.ps1` retrieves patches correctly
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Review Authors
|
||||
|
||||
1. **Test before full review**: Use `Test-IncrementalReview.ps1` to preview changes
|
||||
2. **Check for force-push**: Review the analysis output - force-pushes require full reviews
|
||||
3. **Smart step filtering**: Skip review steps for file types that didn't change
|
||||
|
||||
### For Script Users
|
||||
|
||||
1. **Use absolute paths**: When specifying folders, use absolute paths to avoid ambiguity
|
||||
2. **Check exit codes**: Scripts exit with code 1 on error - check `$LASTEXITCODE` in automation
|
||||
3. **Parse JSON output**: Use `ConvertFrom-Json` to work with structured output from `Get-PrIncrementalChanges.ps1`
|
||||
4. **Handle empty results**: Check `ChangedFiles.Count` before iterating
|
||||
|
||||
### Performance Tips
|
||||
|
||||
1. **Batch operations**: When reviewing multiple PRs, collect all PR numbers and process in batch
|
||||
2. **Cache raw files**: Download baseline files once and reuse for multiple comparisons
|
||||
3. **Filter early**: Use incremental detection to skip unnecessary file reviews
|
||||
4. **Parallel processing**: Consider processing independent PRs in parallel
|
||||
|
||||
## Integration with AI Review Systems
|
||||
|
||||
These tools are designed to work with AI-powered review systems:
|
||||
|
||||
1. **Copilot Instructions**: This file serves as reference documentation for GitHub Copilot
|
||||
2. **Structured Output**: JSON output from scripts is easily parsed by AI systems
|
||||
3. **Incremental Intelligence**: AI can focus on changed files for more efficient reviews
|
||||
4. **Metadata Tracking**: Review iterations are tracked for context-aware suggestions
|
||||
|
||||
### Example AI Integration
|
||||
|
||||
```powershell
|
||||
# Get incremental changes
|
||||
$analysis = .\Get-PrIncrementalChanges.ps1 -PullRequestNumber $PR | ConvertFrom-Json
|
||||
|
||||
# Feed to AI review system
|
||||
$reviewPrompt = @"
|
||||
Review the following changed files in PR #$PR:
|
||||
$($analysis.ChangedFiles | ForEach-Object { "- $($_.Filename) ($($_.Status))" } | Out-String)
|
||||
|
||||
Focus on incremental changes only. Previous review was at SHA $($analysis.LastReviewedSha).
|
||||
"@
|
||||
|
||||
# Execute AI review with context
|
||||
Invoke-AIReview -Prompt $reviewPrompt -Files $analysis.ChangedFiles
|
||||
```
|
||||
|
||||
## Support and Further Information
|
||||
|
||||
For detailed script documentation, use PowerShell's help system:
|
||||
```powershell
|
||||
Get-Help .\Get-PrIncrementalChanges.ps1 -Full
|
||||
Get-Help .\Test-IncrementalReview.ps1 -Detailed
|
||||
```
|
||||
|
||||
Related documentation:
|
||||
- `.github/prompts/review-pr.prompt.md` - Complete review workflow guide
|
||||
- `doc/devdocs/` - PowerToys development documentation
|
||||
- GitHub CLI documentation: https://cli.github.com/manual/
|
||||
|
||||
For issues or questions, refer to the PowerToys contribution guidelines.
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -358,4 +358,4 @@ src/common/Telemetry/*.etl
|
||||
/src/settings-ui/Settings.UI/Assets/Settings/search.index.json
|
||||
|
||||
# PowerToysInstaller Build Temp Files
|
||||
installer/*/*.wxs.bk
|
||||
installer/*/*.wxs.bk
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"WinUI3Apps\\PowerToys.FileLocksmithUI.dll",
|
||||
"WinUI3Apps\\PowerToys.FileLocksmithContextMenu.dll",
|
||||
"FileLocksmithContextMenuPackage.msix",
|
||||
"FileLocksmithCLI.exe",
|
||||
|
||||
"WinUI3Apps\\Peek.Common.dll",
|
||||
"WinUI3Apps\\Peek.FilePreviewer.dll",
|
||||
@@ -124,6 +125,10 @@
|
||||
"WinUI3Apps\\Powertoys.Peek.UI.exe",
|
||||
"WinUI3Apps\\Powertoys.Peek.dll",
|
||||
|
||||
"WinUI3Apps\\PowerToys.QuickAccess.dll",
|
||||
"WinUI3Apps\\PowerToys.QuickAccess.exe",
|
||||
"WinUI3Apps\\PowerToys.Settings.UI.Controls.dll",
|
||||
|
||||
"WinUI3Apps\\PowerToys.EnvironmentVariablesModuleInterface.dll",
|
||||
"WinUI3Apps\\PowerToys.EnvironmentVariablesUILib.dll",
|
||||
"WinUI3Apps\\PowerToys.EnvironmentVariables.dll",
|
||||
@@ -131,6 +136,8 @@
|
||||
|
||||
"PowerToys.ImageResizer.exe",
|
||||
"PowerToys.ImageResizer.dll",
|
||||
"WinUI3Apps\\PowerToys.ImageResizerCLI.exe",
|
||||
"WinUI3Apps\\PowerToys.ImageResizerCLI.dll",
|
||||
"PowerToys.ImageResizerExt.dll",
|
||||
"PowerToys.ImageResizerContextMenu.dll",
|
||||
"ImageResizerContextMenuPackage.msix",
|
||||
@@ -235,6 +242,14 @@
|
||||
|
||||
"PowerToys.CmdPalModuleInterface.dll",
|
||||
"CmdPalKeyboardService.dll",
|
||||
"PowerToys.ModuleContracts.dll",
|
||||
"Awake.ModuleServices.dll",
|
||||
"ColorPicker.ModuleServices.dll",
|
||||
"Workspaces.ModuleServices.dll",
|
||||
"Microsoft.CommandPalette.Extensions.dll",
|
||||
"Microsoft.CommandPalette.Extensions.Toolkit.dll",
|
||||
"Microsoft.CmdPal.Ext.PowerToys.dll",
|
||||
"Microsoft.CmdPal.Ext.PowerToys.exe",
|
||||
"*Microsoft.CmdPal.UI_*.msix",
|
||||
|
||||
"PowerToys.DSC.dll",
|
||||
@@ -358,9 +373,13 @@
|
||||
"boost_regex-vc143-mt-x32-1_87.dll",
|
||||
"boost_regex-vc143-mt-x64-1_87.dll",
|
||||
|
||||
"Microsoft.ML.OnnxRuntime.dll",
|
||||
|
||||
"UnitsNet.dll",
|
||||
"UtfUnknown.dll",
|
||||
"Wpf.Ui.dll"
|
||||
"Wpf.Ui.dll",
|
||||
"Shmuelie.WinRTServer.dll",
|
||||
"ToolGood.Words.Pinyin.dll"
|
||||
],
|
||||
"SigningInfo": {
|
||||
"Operations": [
|
||||
|
||||
@@ -624,4 +624,4 @@ jobs:
|
||||
- publish: $(JobOutputDirectory)
|
||||
artifact: $(JobOutputArtifactName)-failure-$(System.JobAttempt)
|
||||
displayName: Publish failure logs
|
||||
condition: or(failed(), canceled())
|
||||
condition: or(failed(), canceled())
|
||||
@@ -68,14 +68,13 @@ jobs:
|
||||
|
||||
- template: .\steps-restore-nuget.yml
|
||||
|
||||
- task: NuGetCommand@2
|
||||
- task: MSBuild@1
|
||||
displayName: Restore solution-level NuGet packages
|
||||
inputs:
|
||||
command: restore
|
||||
feedsToUse: config
|
||||
configPath: nuget.config
|
||||
restoreSolution: PowerToys.slnx
|
||||
restoreDirectory: '$(Build.SourcesDirectory)\packages'
|
||||
solution: PowerToys.slnx
|
||||
msbuildArguments: '/t:restore /p:RestorePackagesConfig=true'
|
||||
platform: $(BuildPlatform)
|
||||
configuration: $(BuildConfiguration)
|
||||
|
||||
# Build all UI test projects if no specific modules are specified
|
||||
- ${{ if eq(length(parameters.uiTestModules), 0) }}:
|
||||
|
||||
@@ -63,3 +63,20 @@ stages:
|
||||
winAppSDKVersionNumber: ${{ parameters.winAppSDKVersionNumber }}
|
||||
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
|
||||
timeoutInMinutes: 90
|
||||
|
||||
- stage: Build_SDK
|
||||
displayName: Build Command Palette Toolkit SDK
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: job-build-sdk.yml
|
||||
parameters:
|
||||
pool:
|
||||
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
|
||||
name: SHINE-INT-L
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
buildConfigurations: [Release]
|
||||
official: false
|
||||
codeSign: false
|
||||
|
||||
@@ -104,4 +104,4 @@ if ($totalFailure -gt 0) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
exit 0
|
||||
exit 0
|
||||
106
.vscode/tasks.json
vendored
Normal file
106
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"windows": {
|
||||
"options": {
|
||||
"shell": {
|
||||
"executable": "cmd.exe",
|
||||
"args": ["/d", "/c"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"inputs": [
|
||||
{
|
||||
"id": "config",
|
||||
"type": "pickString",
|
||||
"description": "Configuration",
|
||||
"options": ["Debug", "Release"],
|
||||
"default": "Debug"
|
||||
},
|
||||
{
|
||||
"id": "platform",
|
||||
"type": "pickString",
|
||||
"description": "Platform (leave empty to auto-detect host platform)",
|
||||
"options": ["", "X64", "ARM64"],
|
||||
"default": "X64"
|
||||
},
|
||||
{
|
||||
"id": "msbuildExtra",
|
||||
"type": "promptString",
|
||||
"description": "Extra MSBuild args (optional). Example: /p:CIBuild=true /m",
|
||||
"default": ""
|
||||
}
|
||||
],
|
||||
|
||||
"tasks": [
|
||||
{
|
||||
"label": "PT: Build (quick)",
|
||||
"type": "shell",
|
||||
"command": "\"${workspaceFolder}\\tools\\build\\build.cmd\"",
|
||||
"args": [
|
||||
"-Path",
|
||||
"${fileDirname}"
|
||||
],
|
||||
"group": { "kind": "build", "isDefault": true },
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated",
|
||||
"clear": true
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
|
||||
{
|
||||
"label": "PT: Build (with options)",
|
||||
"type": "shell",
|
||||
"command": "\"${workspaceFolder}\\tools\\build\\build.cmd\"",
|
||||
"args": [
|
||||
"-Path",
|
||||
"${fileDirname}",
|
||||
"-Platform",
|
||||
"${input:platform}",
|
||||
"-Configuration",
|
||||
"${input:config}",
|
||||
"${input:msbuildExtra}"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated",
|
||||
"clear": true
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
|
||||
{
|
||||
"label": "PT: Build Essentials (quick)",
|
||||
"type": "shell",
|
||||
"command": "\"${workspaceFolder}\\tools\\build\\build-essentials.cmd\"",
|
||||
"args": [],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated",
|
||||
"clear": true
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
|
||||
{
|
||||
"label": "PT: Build Essentials (with options)",
|
||||
"type": "shell",
|
||||
"command": "\"${workspaceFolder}\\tools\\build\\build-essentials.cmd\"",
|
||||
"args": [
|
||||
"-Platform",
|
||||
"${input:platform}",
|
||||
"-Configuration",
|
||||
"${input:config}",
|
||||
"${input:msbuildExtra}"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated",
|
||||
"clear": true
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
165
AGENTS.md
Normal file
165
AGENTS.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
description: 'Top-level AI contributor guidance for developing PowerToys - a collection of Windows productivity utilities'
|
||||
applyTo: '**'
|
||||
---
|
||||
|
||||
# PowerToys – AI Contributor Guide
|
||||
|
||||
This is the top-level guidance for AI contributions to PowerToys. Keep changes atomic, follow existing patterns, and cite exact paths in PRs.
|
||||
|
||||
## Overview
|
||||
|
||||
PowerToys is a set of utilities for power users to tune and streamline their Windows experience.
|
||||
|
||||
| Area | Location | Description |
|
||||
|------|----------|-------------|
|
||||
| Runner | `src/runner/` | Main executable, tray icon, module loader, hotkey management |
|
||||
| Settings UI | `src/settings-ui/` | WinUI/WPF configuration app communicating via named pipes |
|
||||
| Modules | `src/modules/` | Individual PowerToys utilities (each in its own subfolder) |
|
||||
| Common Libraries | `src/common/` | Shared code: logging, IPC, settings, DPI, telemetry, utilities |
|
||||
| Build Tools | `tools/build/` | Build scripts and automation |
|
||||
| Documentation | `doc/devdocs/` | Developer documentation |
|
||||
| Installer | `installer/` | WiX-based installer projects |
|
||||
|
||||
For architecture details and module types, see [Architecture Overview](doc/devdocs/core/architecture.md).
|
||||
|
||||
## Conventions
|
||||
|
||||
For detailed coding conventions, see:
|
||||
- [Coding Guidelines](doc/devdocs/development/guidelines.md) – Dependencies, testing, PR management
|
||||
- [Coding Style](doc/devdocs/development/style.md) – Formatting, C++/C#/XAML style rules
|
||||
- [Logging](doc/devdocs/development/logging.md) – C++ spdlog and C# Logger usage
|
||||
|
||||
### Component-Specific Instructions
|
||||
|
||||
These instruction files are automatically applied when working in their respective areas:
|
||||
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md) – IPC contracts, schema migrations
|
||||
- [Common Libraries](.github/instructions/common-libraries.instructions.md) – ABI stability, shared code guidelines
|
||||
|
||||
## Build
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Visual Studio 2022 17.4+
|
||||
- Windows 10 1803+ (April 2018 Update or newer)
|
||||
- Initialize submodules once: `git submodule update --init --recursive`
|
||||
|
||||
### Build Commands
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| First build / NuGet restore | `tools\build\build-essentials.cmd` |
|
||||
| Build current folder | `tools\build\build.cmd` |
|
||||
| Build with options | `build.ps1 -Platform x64 -Configuration Release` |
|
||||
|
||||
### Build Discipline
|
||||
|
||||
1. One terminal per operation (build → test). Do not switch or open new ones mid-flow
|
||||
2. After making changes, `cd` to the project folder that changed (`.csproj`/`.vcxproj`)
|
||||
3. Use scripts to build: `tools/build/build.ps1` or `tools/build/build.cmd`
|
||||
4. For first build or missing NuGet packages, run `build-essentials.cmd` first
|
||||
5. **Exit code 0 = success; non-zero = failure** – treat this as absolute
|
||||
6. On failure, read the errors log: `build.<config>.<platform>.errors.log`
|
||||
7. Do not start tests or launch Runner until the build succeeds
|
||||
|
||||
### Build Logs
|
||||
|
||||
Located next to the solution/project being built:
|
||||
- `build.<configuration>.<platform>.errors.log` – errors only (check this first)
|
||||
- `build.<configuration>.<platform>.all.log` – full log
|
||||
- `build.<configuration>.<platform>.trace.binlog` – for MSBuild Structured Log Viewer
|
||||
|
||||
For complete details, see [Build Guidelines](tools/build/BUILD-GUIDELINES.md).
|
||||
|
||||
## Tests
|
||||
|
||||
### Test Discovery
|
||||
|
||||
- Find test projects by product code prefix (e.g., `FancyZones`, `AdvancedPaste`)
|
||||
- Look for sibling folders or 1-2 levels up named `<Product>*UnitTests` or `<Product>*UITests`
|
||||
|
||||
### Running Tests
|
||||
|
||||
1. **Build the test project first**, wait for exit code 0
|
||||
2. Run via VS Test Explorer (`Ctrl+E, T`) or `vstest.console.exe` with filters
|
||||
3. **Avoid `dotnet test`** in this repo – use VS Test Explorer or vstest.console.exe
|
||||
|
||||
### Test Types
|
||||
|
||||
| Type | Requirements | Setup |
|
||||
|------|--------------|-------|
|
||||
| Unit Tests | Standard dev environment | None |
|
||||
| UI Tests | WinAppDriver v1.2.1, Developer Mode | Install from [WinAppDriver releases](https://github.com/microsoft/WinAppDriver/releases/tag/v1.2.1) |
|
||||
| Fuzz Tests | OneFuzz, .NET 8 | See [Fuzzing Tests](doc/devdocs/tools/fuzzingtesting.md) |
|
||||
|
||||
### Test Discipline
|
||||
|
||||
1. Add or adjust tests when changing behavior
|
||||
2. If tests skipped, state why (e.g., comment-only change, string rename)
|
||||
3. New modules handling file I/O or user input **must** implement fuzzing tests
|
||||
|
||||
### Special Requirements
|
||||
|
||||
- **Mouse Without Borders**: Requires 2+ physical computers (not VMs)
|
||||
- **Multi-monitor utilities**: Test with 2+ monitors, different DPI settings
|
||||
|
||||
For UI test setup details, see [UI Tests](doc/devdocs/development/ui-tests.md).
|
||||
|
||||
## Boundaries
|
||||
|
||||
### Ask for Clarification When
|
||||
|
||||
- Ambiguous spec after scanning relevant docs
|
||||
- Cross-module impact (shared enum/struct) is unclear
|
||||
- Security, elevation, or installer changes involved
|
||||
- GPO or policy handling modifications needed
|
||||
|
||||
### Areas Requiring Extra Care
|
||||
|
||||
| Area | Concern | Reference |
|
||||
|------|---------|-----------|
|
||||
| `src/common/` | ABI breaks | [Common Libraries Instructions](.github/instructions/common-libraries.instructions.md) |
|
||||
| `src/runner/`, `src/settings-ui/` | IPC contracts, schema | [Runner & Settings UI Instructions](.github/instructions/runner-settings-ui.instructions.md) |
|
||||
| Installer files | Release impact | Careful review required |
|
||||
| Elevation/GPO logic | Security | Confirm no regression in policy handling |
|
||||
|
||||
### What NOT to Do
|
||||
|
||||
- Don't merge incomplete features into main (use feature branches)
|
||||
- Don't break IPC/JSON contracts without updating both runner and settings-ui
|
||||
- Don't add noisy logs in hot paths
|
||||
- Don't introduce third-party deps without PM approval and `NOTICE.md` update
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before finishing, verify:
|
||||
|
||||
- [ ] Build clean with exit code 0
|
||||
- [ ] Tests updated and passing locally
|
||||
- [ ] No unintended ABI breaks or schema changes
|
||||
- [ ] IPC contracts consistent between runner and settings-ui
|
||||
- [ ] New dependencies added to `NOTICE.md`
|
||||
- [ ] PR is atomic (one logical change), with issue linked
|
||||
|
||||
## Documentation Index
|
||||
|
||||
### Core Architecture
|
||||
- [Architecture Overview](doc/devdocs/core/architecture.md)
|
||||
- [Runner](doc/devdocs/core/runner.md)
|
||||
- [Settings System](doc/devdocs/core/settings/readme.md)
|
||||
- [Module Interface](doc/devdocs/modules/interface.md)
|
||||
|
||||
### Development
|
||||
- [Coding Guidelines](doc/devdocs/development/guidelines.md)
|
||||
- [Coding Style](doc/devdocs/development/style.md)
|
||||
- [Logging](doc/devdocs/development/logging.md)
|
||||
- [UI Tests](doc/devdocs/development/ui-tests.md)
|
||||
- [Fuzzing Tests](doc/devdocs/tools/fuzzingtesting.md)
|
||||
|
||||
### Build & Tools
|
||||
- [Build Guidelines](tools/build/BUILD-GUIDELINES.md)
|
||||
- [Tools Overview](doc/devdocs/tools/readme.md)
|
||||
|
||||
### Instructions (Auto-Applied)
|
||||
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md)
|
||||
- [Common Libraries](.github/instructions/common-libraries.instructions.md)
|
||||
36
COMMUNITY.md
36
COMMUNITY.md
@@ -6,9 +6,6 @@ Names are in alphabetical order based on first name.
|
||||
|
||||
## High impact community members
|
||||
|
||||
### [@Noraa-Junker](https://github.com/Noraa-Junker) - [Noraa Junker](https://noraajunker.ch)
|
||||
Noraa has helped triaging, discussing, and creating a substantial number of issues and contributed features/fixes. Noraa was the primary person for helping build the File Explorer preview pane handler for developer files.
|
||||
|
||||
### [@cgaarden](https://github.com/cgaarden) - [Christian Gaarden Gaardmark](https://www.onegreatworld.com)
|
||||
Christian contributed New+ utility
|
||||
|
||||
@@ -42,6 +39,12 @@ Jay has helped triaging, discussing, creating a substantial number of issues and
|
||||
### [@jefflord](https://github.com/Jjefflord) - Jeff Lord
|
||||
Jeff added in multiple new features into Keyboard manager, such as key chord support and launching apps. He also contributed multiple features/fixes to PowerToys.
|
||||
|
||||
### [@snickler](https://github.com/snickler) - [Jeremy Sinclair](http://sinclairinat0r.com)
|
||||
Jeremy has helped drive large sums of the ARM64 support inside PowerToys
|
||||
|
||||
### [@jiripolasek](https://github.com/jiripolasek) - [Jiří Polášek](https://github.com/jiripolasek)
|
||||
Jiří has contributed a massive number of features and improvements to Command Palette, including drag & drop support, custom themes, Web Search enhancements, Remote Desktop extension fixes, and many UX improvements.
|
||||
|
||||
### [@TheJoeFin](https://github.com/TheJoeFin) - [Joe Finney](https://joefinapps.com)
|
||||
Joe has helped triaging, discussing, issues as well as fixing bugs and building features for Text Extractor.
|
||||
|
||||
@@ -57,6 +60,9 @@ Color Picker is from Martin.
|
||||
### [@mikeclayton](https://github.com/mikeclayton) - [Michael Clayton](https://michael-clayton.com)
|
||||
Michael contributed the [initial version](https://github.com/microsoft/PowerToys/issues/23216) of the Mouse Jump tool and [a number of updates](https://github.com/microsoft/PowerToys/pulls?q=is%3Apr+author%3Amikeclayton) based on his FancyMouse utility.
|
||||
|
||||
### [@Noraa-Junker](https://github.com/Noraa-Junker) - [Noraa Junker](https://noraajunker.ch)
|
||||
Noraa has helped triaging, discussing, and creating a substantial number of issues and contributed features/fixes. Noraa was the primary person for helping build the File Explorer preview pane handler for developer files.
|
||||
|
||||
### [@pedrolamas](https://github.com/pedrolamas/) - Pedro Lamas
|
||||
Pedro helped create the thumbnail and File Explorer previewers for 3D files like STL and GCode. If you like 3D printing, these are very helpful.
|
||||
|
||||
@@ -69,15 +75,12 @@ Rafael has helped do the [upgrade from CppWinRT 1.x to 2.0](https://github.com/m
|
||||
### [@royvou](https://github.com/royvou)
|
||||
Roy has helped out contributing multiple features to PowerToys Run
|
||||
|
||||
### [@snickler](https://github.com/snickler) - [Jeremy Sinclair](http://sinclairinat0r.com)
|
||||
Jeremy has helped drive large sums of the ARM64 support inside PowerToys
|
||||
### [@ThiefZero](https://github.com/ThiefZero)
|
||||
ThiefZero has helped out contributing a features to PowerToys Run such as the unit converter plugin
|
||||
|
||||
### [@TobiasSekan](https://github.com/TobiasSekan) - Tobias Sekan
|
||||
Tobias Sekan has helped out contributing features to PowerToys Run such as Settings plugin, Registry plugin
|
||||
|
||||
### [@ThiefZero](https://github.com/ThiefZero)
|
||||
ThiefZero has helped out contributing a features to PowerToys Run such as the unit converter plugin
|
||||
|
||||
## Open source projects
|
||||
|
||||
As PowerToys creates new utilities, some will be based off existing technology. We'll continue to do our best to contribute back to these projects but their efforts were the base of some of our projects. We want to be sure their work is directly recognized.
|
||||
@@ -187,18 +190,10 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter
|
||||
- [@niels9001](https://github.com/niels9001/) - Niels Laute - Product Manager
|
||||
- [@dhowett](https://github.com/dhowett) - Dustin Howett - Dev Lead
|
||||
- [@yeelam-gordon](https://github.com/yeelam-gordon) - Gordon Lam - Dev Lead
|
||||
- [@jamrobot](https://github.com/jamrobot) - Jerry Xu - Dev Lead
|
||||
- [@lei9444](https://github.com/lei9444) - Leilei Zhang - Dev
|
||||
- [@shuaiyuanxx](https://github.com/shuaiyuanxx) - Shawn Yuan - Dev
|
||||
- [@moooyo](https://github.com/moooyo) - Yu Leng - Dev
|
||||
- [@haoliuu](https://github.com/haoliuu) - Hao Liu - Dev
|
||||
- [@chenmy77](https://github.com/chenmy77) - Mengyuan Chen - Dev
|
||||
- [@chemwolf6922](https://github.com/chemwolf6922) - Feng Wang - Dev
|
||||
- [@yaqingmi](https://github.com/yaqingmi) - Yaqing Mi - Dev
|
||||
- [@zhaoqpcn](https://github.com/zhaoqpcn) - Qingpeng Zhao - Dev
|
||||
- [@urnotdfs](https://github.com/urnotdfs) - Xiaofeng Wang - Dev
|
||||
- [@zhaopy536](https://github.com/zhaopy536) - Peiyao Zhao - Dev
|
||||
- [@wang563681252](https://github.com/wang563681252) - Zhaopeng Wang - Dev
|
||||
- [@vanzue](https://github.com/vanzue) - Kai Tao - Dev
|
||||
- [@zadjii-msft](https://github.com/zadjii-msft) - Mike Griese - Dev
|
||||
- [@khmyznikov](https://github.com/khmyznikov) - Gleb Khmyznikov - Dev
|
||||
@@ -229,3 +224,12 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter
|
||||
- [@SeraphimaZykova](https://github.com/SeraphimaZykova) - Seraphima Zykova - Dev
|
||||
- [@stefansjfw](https://github.com/stefansjfw) - Stefan Markovic - Dev
|
||||
- [@jaimecbernardo](https://github.com/jaimecbernardo) - Jaime Bernardo - Dev Lead
|
||||
- [@haoliuu](https://github.com/haoliuu) - Hao Liu - Dev
|
||||
- [@chenmy77](https://github.com/chenmy77) - Mengyuan Chen - Dev
|
||||
- [@chemwolf6922](https://github.com/chemwolf6922) - Feng Wang - Dev
|
||||
- [@yaqingmi](https://github.com/yaqingmi) - Yaqing Mi - Dev
|
||||
- [@zhaoqpcn](https://github.com/zhaoqpcn) - Qingpeng Zhao - Dev
|
||||
- [@urnotdfs](https://github.com/urnotdfs) - Xiaofeng Wang - Dev
|
||||
- [@zhaopy536](https://github.com/zhaopy536) - Peiyao Zhao - Dev
|
||||
- [@wang563681252](https://github.com/wang563681252) - Zhaopeng Wang - Dev
|
||||
- [@jamrobot](https://github.com/jamrobot) - Jerry Xu - Dev Lead
|
||||
|
||||
@@ -444,6 +444,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.FancyZones_ZoneWindowKeyUp</td>
|
||||
<td>Occurs when a key is released while interacting with zones.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FancyZones_CLICommand</td>
|
||||
<td>Triggered when a FancyZones CLI command is executed, logging the command name and success status.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### FileExplorerAddOns
|
||||
@@ -690,6 +694,30 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Light Switch
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.LightSwitch_EnableLightSwitch</td>
|
||||
<td>Triggered when Light Switch is enabled or disabled.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.LightSwitch_ShortcutInvoked</td>
|
||||
<td>Occurs when the shortcut for Light Switch is invoked.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.LightSwitch_ScheduleModeToggled</td>
|
||||
<td>Occurs when a new schedule mode is selected for Light Switch.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.LightSwitch_ThemeTargetChanged</td>
|
||||
<td>Occurs when the options for targeting the system or apps is updated.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Mouse Highlighter
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
<MSBuildCacheIgnoredInputPatterns>$(MSBuildCacheIgnoredInputPatterns);$(PackagesConfigFile)</MSBuildCacheIgnoredInputPatterns>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(MSBuildCacheEnabled)' == 'true' AND '$(MSBuildCachePackageRoot)' == ''">
|
||||
<PropertyGroup Condition="'$(MSBuildCacheEnabled)' == 'true' and '$(MSBuildCachePackageRoot)' == ''">
|
||||
<PackagesConfigContents>$([System.IO.File]::ReadAllText("$(PackagesConfigFile)"))</PackagesConfigContents>
|
||||
<MSBuildCachePackageVersion>$([System.Text.RegularExpressions.Regex]::Match($(PackagesConfigContents), 'Microsoft.MSBuildCache.*?version="(.*?)"').Groups[1].Value)</MSBuildCachePackageVersion>
|
||||
<MSBuildCachePackageRoot>$(MSBuildThisFileDirectory)packages\$(MSBuildCachePackageName).$(MSBuildCachePackageVersion)</MSBuildCachePackageRoot>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
|
||||
<PackageVersion Include="MessagePack" Version="3.1.3" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.5.250829002" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
|
||||
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
|
||||
@@ -94,6 +95,7 @@
|
||||
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
||||
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
||||
<PackageVersion Include="SharpCompress" Version="0.37.2" />
|
||||
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.1.1" />
|
||||
<!-- Don't update SkiaSharp.Views.WinUI to version 3.* branch as this brakes the HexBox control in Registry Preview. -->
|
||||
<PackageVersion Include="SkiaSharp.Views.WinUI" Version="2.88.9" />
|
||||
<PackageVersion Include="StreamJsonRpc" Version="2.21.69" />
|
||||
@@ -145,4 +147,4 @@
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -1560,6 +1560,7 @@ SOFTWARE.
|
||||
- ReverseMarkdown
|
||||
- ScipBe.Common.Office.OneNote
|
||||
- SharpCompress
|
||||
- Shmuelie.WinRTServer
|
||||
- SkiaSharp.Views.WinUI
|
||||
- StreamJsonRpc
|
||||
- StyleCop.Analyzers
|
||||
|
||||
@@ -44,6 +44,10 @@
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/common/PowerToys.ModuleContracts/PowerToys.ModuleContracts.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/common/SettingsAPI/SettingsAPI.vcxproj" Id="6955446d-23f7-4023-9bb3-8657f904af99" />
|
||||
<Project Path="src/common/Themes/Themes.vcxproj" Id="98537082-0fdb-40de-abd8-0dc5a4269bab" />
|
||||
<Project Path="src/common/UITestAutomation/UITestAutomation.csproj">
|
||||
@@ -82,7 +86,6 @@
|
||||
<Project Path="src/common/Telemetry/EtwTrace/EtwTrace.vcxproj" Id="8f021b46-362b-485c-bfba-ccf83e820cbd" />
|
||||
</Folder>
|
||||
<Folder Name="/common/utils/">
|
||||
<Project Path="src/common/utils/utils.vcxproj" Id="e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e" />
|
||||
<File Path="src/common/utils/appMutex.h" />
|
||||
<File Path="src/common/utils/color.h" />
|
||||
<File Path="src/common/utils/com_object_factory.h" />
|
||||
@@ -157,6 +160,10 @@
|
||||
<Project Path="src/modules/alwaysontop/AlwaysOnTopModuleInterface/AlwaysOnTopModuleInterface.vcxproj" Id="48a0a19e-a0be-4256-acf8-cc3b80291af9" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/awake/">
|
||||
<Project Path="src/modules/awake/Awake.ModuleServices/Awake.ModuleServices.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/awake/Awake/Awake.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
@@ -167,6 +174,10 @@
|
||||
<Project Path="src/modules/cmdNotFound/CmdNotFoundModuleInterface/CmdNotFoundModuleInterface.vcxproj" Id="0014d652-901f-4456-8d65-06fc5f997fb0" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/colorpicker/">
|
||||
<Project Path="src/modules/colorPicker/ColorPicker.ModuleServices/ColorPicker.ModuleServices.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/colorPicker/ColorPicker/ColorPicker.vcxproj" Id="655c9af2-18d3-4da6-80e4-85504a7722ba">
|
||||
<BuildDependency Project="src/common/logger/logger.vcxproj" />
|
||||
</Project>
|
||||
@@ -207,6 +218,11 @@
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
<Deploy />
|
||||
</Project>
|
||||
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
<Deploy />
|
||||
</Project>
|
||||
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Microsoft.CmdPal.Ext.Registry.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
@@ -404,6 +420,7 @@
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/FileLocksmith/">
|
||||
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj" Id="49D456D3-F485-45AF-8875-45B44F193DDC" />
|
||||
<Project Path="src/modules/FileLocksmith/FileLocksmithContextMenu/FileLocksmithContextMenu.vcxproj" Id="799a50d8-de89-4ed1-8ff8-ad5a9ed8c0ca" />
|
||||
<Project Path="src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj" Id="57175ec7-92a5-4c1e-8244-e3fbca2a81de" />
|
||||
<Project Path="src/modules/FileLocksmith/FileLocksmithLib/FileLocksmithLib.vcxproj" Id="9d52fd25-ef90-4f9a-a015-91efc5daf54f" />
|
||||
@@ -413,6 +430,9 @@
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/FileLocksmith/Tests/">
|
||||
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLIUnitTests.vcxproj" Id="A1B2C3D4-E5F6-7890-1234-567890ABCDEF" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/Hosts/">
|
||||
<Project Path="src/modules/Hosts/Hosts/Hosts.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
@@ -443,6 +463,10 @@
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/imageresizer/ImageResizerCLI/ImageResizerCLI.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/imageresizer/Tests/">
|
||||
<Project Path="src/modules/imageresizer/tests/ImageResizer.UnitTests.csproj">
|
||||
@@ -933,6 +957,10 @@
|
||||
<Project Path="src/modules/ShortcutGuide/ShortcutGuideModuleInterface/ShortcutGuideModuleInterface.vcxproj" Id="2d604c07-51fc-46bb-9eb7-75aecc7f5e81" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/Workspaces/">
|
||||
<Project Path="src/modules/Workspaces/Workspaces.ModuleServices/Workspaces.ModuleServices.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/Workspaces/WorkspacesCsharpLibrary/WorkspacesCsharpLibrary.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
@@ -977,6 +1005,14 @@
|
||||
<Project Path="src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettingsInterop.vcxproj" Id="ca7d8106-30b9-4aec-9d05-b69b31b8c461" />
|
||||
</Folder>
|
||||
<Folder Name="/settings-ui/">
|
||||
<Project Path="src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/settings-ui/Settings.UI.Controls/Settings.UI.Controls.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
|
||||
93
doc/devdocs/cli-conventions.md
Normal file
93
doc/devdocs/cli-conventions.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# CLI Conventions
|
||||
|
||||
This document describes the conventions for implementing command-line interfaces (CLI) in PowerToys modules.
|
||||
|
||||
## Library
|
||||
|
||||
Use the **System.CommandLine** library for CLI argument parsing. This is already defined in `Directory.Packages.props`:
|
||||
|
||||
```xml
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
```
|
||||
|
||||
Add the reference to your project:
|
||||
|
||||
```xml
|
||||
<PackageReference Include="System.CommandLine" />
|
||||
```
|
||||
|
||||
## Option Naming and Definition
|
||||
|
||||
- Use `--kebab-case` for long form (e.g., `--shrink-only`).
|
||||
- Use single `-x` for short form (e.g., `-s`, `-w`).
|
||||
- Define aliases as static readonly arrays: `["--silent", "-s"]`.
|
||||
- Create options using `Option<T>` with descriptive help text.
|
||||
- Add validators for options that require range or format checking.
|
||||
|
||||
## RootCommand Setup
|
||||
|
||||
- Create a `RootCommand` with a brief description.
|
||||
- Add all options and arguments to the command.
|
||||
|
||||
## Parsing
|
||||
|
||||
- Use `Parser(rootCommand).Parse(args)` to parse CLI arguments.
|
||||
- Extract option values using `parseResult.GetValueForOption()`.
|
||||
- Note: Use `Parser` directly; `RootCommand.Parse()` may not be available with the pinned System.CommandLine version.
|
||||
|
||||
### Parse/Validation Errors
|
||||
|
||||
- On parse/validation errors, print error messages and usage, then exit with non-zero code.
|
||||
|
||||
## Examples
|
||||
|
||||
Reference implementations:
|
||||
- Awake: `src/modules/Awake/Awake/Program.cs`
|
||||
- ImageResizer: `src/modules/imageresizer/ui/Cli/`
|
||||
|
||||
## Help Output
|
||||
|
||||
- Provide a `PrintUsage()` method for custom help formatting if needed.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Consistency**: Follow existing module patterns.
|
||||
2. **Documentation**: Always provide help text for each option.
|
||||
3. **Validation**: Validate input and provide clear error messages.
|
||||
4. **Atomicity**: Make one logical change per PR; avoid drive-by refactors.
|
||||
5. **Build/Test Discipline**: Build and test synchronously, one terminal per operation.
|
||||
6. **Style**: Follow repo analyzers (`.editorconfig`, StyleCop) and formatting rules.
|
||||
|
||||
## Logging Requirements
|
||||
|
||||
- Use `ManagedCommon.Logger` for consistent logging.
|
||||
- Initialize logging early in `Main()`.
|
||||
- Use dual output (console + log file) for errors and warnings to ensure visibility.
|
||||
- Reference: `src/modules/imageresizer/ui/Cli/CliLogger.cs`
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Exit Codes
|
||||
|
||||
- `0`: Success
|
||||
- `1`: General error (parsing, validation, runtime)
|
||||
- `2`: Invalid arguments (optional)
|
||||
|
||||
### Exception Handling
|
||||
|
||||
- Always wrap `Main()` in try-catch for unhandled exceptions.
|
||||
- Log exceptions before exiting with non-zero code.
|
||||
- Display user-friendly error messages to stderr.
|
||||
- Preserve detailed stack traces in log files only.
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
- Include tests for argument parsing, validation, and edge cases.
|
||||
- Place CLI tests in module-specific test projects (e.g., `src/modules/[module]/tests/*CliTests.cs`).
|
||||
|
||||
## Signing and Deployment
|
||||
|
||||
- CLI executables are signed automatically in CI/CD.
|
||||
- **New CLI tools**: Add your executable and dll to `.pipelines/ESRPSigning_core.json` in the signing list.
|
||||
- CLI executables are deployed alongside their parent module (e.g., `C:\Program Files\PowerToys\modules\[ModuleName]\`).
|
||||
- Use self-contained deployment (import `Common.SelfContained.props`).
|
||||
@@ -58,8 +58,8 @@ string validUIDisplayString = Resources.ValidUIDisplayString;
|
||||
## More On Coding Guidance
|
||||
Please review these brief docs below relating to our coding standards, etc.
|
||||
|
||||
* [Coding Style](./style.md)
|
||||
* [Code Organization](./readme.md)
|
||||
* [Coding Style](development/style.md)
|
||||
* [Code Organization](readme.md)
|
||||
|
||||
|
||||
[VS Resource Editor]: https://learn.microsoft.com/cpp/windows/resource-editors?view=vs-2019
|
||||
|
||||
@@ -20,6 +20,9 @@ Creates a window showing the selected area of the original window. Changes in th
|
||||
### Reparent Mode
|
||||
Creates a window that replaces the original window, showing only the selected area. The application is controlled through the cropped window.
|
||||
|
||||
### Screenshot Mode
|
||||
Creates a window showing a freezed snapshot of the original window.
|
||||
|
||||
## Code Structure
|
||||
|
||||
### Project Layout
|
||||
@@ -30,6 +33,7 @@ The Crop and Lock module is part of the PowerToys solution. All the logic-relate
|
||||
- **OverlayWindow.cpp**: Thumbnail module type's window concrete implementation.
|
||||
- **ReparentCropAndLockWindow.cpp**: Defines the UI for the reparent mode.
|
||||
- **ChildWindow.cpp**: Reparent module type's window concrete implementation.
|
||||
- **ScreenshotCropAndLockWindow.cpp**: Defines the UI for the screenshot mode.
|
||||
|
||||
## Known Issues
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
|
||||
## Rules
|
||||
|
||||
- **Follow the pattern of what you already see in the code.**
|
||||
- [Coding style](style.md).
|
||||
- [Coding style](development/style.md).
|
||||
- Try to package new functionality/components into libraries that have nicely defined interfaces.
|
||||
- Package new functionality into classes or refactor existing functionality into a class as you extend the code.
|
||||
- When adding new classes/methods/changing existing code, add new unit tests or update the existing tests.
|
||||
|
||||
@@ -50,6 +50,8 @@ Contact the developers of a plugin directly for assistance with a specific plugi
|
||||
| [Hotkeys](https://github.com/ruslanlap/PowerToysRun-Hotkeys) | [ruslanlap](https://github.com/ruslanlap) | Create, manage, and trigger custom keyboard shortcuts directly from PowerToys Run. |
|
||||
| [RandomGen](https://github.com/ruslanlap/PowerToysRun-RandomGen) | [ruslanlap](https://github.com/ruslanlap) | 🎲 Generate random data instantly with a single keystroke. Perfect for developers, testers, designers, and anyone who needs quick access to random data. Features include secure passwords, PINs, names, business data, dates, numbers, GUIDs, color codes, and more. Especially useful for designers who need random color codes and placeholder content. |
|
||||
| [Open With Cursor](https://github.com/VictorNoxx/PowerToys-Run-Cursor/) | [VictorNoxx](https://github.com/VictorNoxx) | Open Visual Studio, VS Code recents with Cursor AI |
|
||||
| [Open With Antigravity](https://github.com/artickc/PowerToys-Run-Antygravity) | [artickc](https://github.com/artickc) | Open Visual Studio, VS Code recents with Antigravity AI |
|
||||
| [Project Launcher Plugin](https://github.com/artickc/ProjectLauncherPowerToysPlugin) | [artickc](https://github.com/artickc) | Access your projects using Project Launcher and PowerToys Run |
|
||||
| [CheatSheets](https://github.com/ruslanlap/PowerToysRun-CheatSheets) | [ruslanlap](https://github.com/ruslanlap) | 📚 Find cheat sheets and command examples instantly from tldr pages, cheat.sh, and devhints.io. Features include favorites system, categories, offline mode, and smart caching. |
|
||||
| [QuickAI](https://github.com/ruslanlap/PowerToysRun-QuickAi) | [ruslanlap](https://github.com/ruslanlap) | AI-powered assistance with instant, smart responses from multiple providers (Groq, Together, Fireworks, OpenRouter, Cohere) |
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
</Project>
|
||||
<Project Path="../src/common/Telemetry/EtwTrace/EtwTrace.vcxproj" Id="8f021b46-362b-485c-bfba-ccf83e820cbd" />
|
||||
<Project Path="../src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
|
||||
<Project Path="../src/common/utils/utils.vcxproj" Id="e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e" />
|
||||
<Project Path="../src/logging/logging.vcxproj" Id="7e1e3f13-2bd6-3f75-a6a7-873a2b55c60f">
|
||||
<Build Solution="Debug|ARM64" Project="false" />
|
||||
</Project>
|
||||
|
||||
@@ -162,9 +162,6 @@
|
||||
<ProjectReference Include="..\..\src\common\Telemetry\EtwTrace\EtwTrace.vcxproj">
|
||||
<Project>{8f021b46-362b-485c-bfba-ccf83e820cbd}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\src\common\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
|
||||
@@ -7,11 +7,18 @@
|
||||
|
||||
<Fragment>
|
||||
<DirectoryRef Id="INSTALLFOLDER">
|
||||
<Component Id="Microsoft_CommandPalette_Extensions_winmd" Guid="304AD25A-A986-4058-940E-61DB79EBD78C" Bitness="always64">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="Microsoft_CommandPalette_Extensions_winmd" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<File Id="Microsoft.CommandPalette.Extensions.winmd" Source="$(var.BinDir)Microsoft.CommandPalette.Extensions.winmd" />
|
||||
</Component>
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--BaseApplicationsFiles_Component_Def-->
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="BaseApplicationsComponentGroup">
|
||||
<ComponentRef Id="Microsoft_CommandPalette_Extensions_winmd" />
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
|
||||
@@ -173,4 +173,4 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
<Target Name="Restore" />
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -47,9 +47,6 @@
|
||||
<ProjectReference Include="..\common\SettingsAPI\SettingsAPI.vcxproj">
|
||||
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\common\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="resource.h" />
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
|
||||
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
|
||||
xmlns:systemai="http://schemas.microsoft.com/appx/manifest/systemai/windows10"
|
||||
IgnorableNamespaces="uap uap2 uap3 rescap desktop uap10 systemai">
|
||||
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
|
||||
IgnorableNamespaces="uap uap2 uap3 rescap desktop uap10 systemai com">
|
||||
<Identity
|
||||
Name="Microsoft.PowerToys.SparseApp"
|
||||
Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
||||
@@ -30,6 +31,7 @@
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19000.0" MaxVersionTested="10.0.26226.0" />
|
||||
</Dependencies>
|
||||
<Capabilities>
|
||||
<Capability Name="internetClient" />
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
<systemai:Capability Name="systemAIModels"/>
|
||||
<rescap:Capability Name="unvirtualizedResources"/>
|
||||
@@ -66,5 +68,42 @@
|
||||
AppListEntry="none">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
<Application Id="PowerToys.CmdPalExtension" Executable="Microsoft.CmdPal.Ext.PowerToys.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
<uap:VisualElements
|
||||
DisplayName="PowerToys.CommandPaletteExtension"
|
||||
Description="PowerToys Command Palette Extension"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png"
|
||||
AppListEntry="none">
|
||||
</uap:VisualElements>
|
||||
<Extensions>
|
||||
<com:Extension Category="windows.comServer">
|
||||
<com:ComServer>
|
||||
<com:ExeServer Executable="Microsoft.CmdPal.Ext.PowerToys.exe" Arguments="-RegisterProcessAsComServer" DisplayName="PowerToys Command Palette Extension">
|
||||
<com:Class Id="7EC02C7D-8F98-4A2E-9F23-B58C2C2F2B17" DisplayName="PowerToys Command Palette Extension" />
|
||||
</com:ExeServer>
|
||||
</com:ComServer>
|
||||
</com:Extension>
|
||||
<uap3:Extension Category="windows.appExtension">
|
||||
<uap3:AppExtension Name="com.microsoft.commandpalette"
|
||||
Id="PowerToys"
|
||||
PublicFolder="Public"
|
||||
DisplayName="PowerToys"
|
||||
Description="Surface PowerToys commands inside Command Palette">
|
||||
<uap3:Properties>
|
||||
<CmdPalProvider>
|
||||
<Activation>
|
||||
<CreateInstance ClassId="7EC02C7D-8F98-4A2E-9F23-B58C2C2F2B17" />
|
||||
</Activation>
|
||||
<SupportedInterfaces>
|
||||
<Commands/>
|
||||
</SupportedInterfaces>
|
||||
</CmdPalProvider>
|
||||
</uap3:Properties>
|
||||
</uap3:AppExtension>
|
||||
</uap3:Extension>
|
||||
</Extensions>
|
||||
</Application>
|
||||
</Applications>
|
||||
</Package>
|
||||
</Package>
|
||||
|
||||
@@ -53,9 +53,6 @@
|
||||
<ProjectReference Include="..\common\updating\updating.vcxproj">
|
||||
<Project>{17da04df-e393-4397-9cf0-84dabe11032e}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\common\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="resource.h" />
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace Common.UI
|
||||
{
|
||||
@@ -120,28 +122,33 @@ namespace Common.UI
|
||||
}
|
||||
}
|
||||
|
||||
public static void OpenSettings(SettingsWindow window, bool mainExecutableIsOnTheParentFolder)
|
||||
// What about debug build? Should also consider debug build, maybe tray window message?
|
||||
public static void OpenSettings(SettingsWindow window)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directoryPath = System.AppContext.BaseDirectory;
|
||||
if (mainExecutableIsOnTheParentFolder)
|
||||
var exePath = Path.Combine(
|
||||
PowerToysPathResolver.GetPowerToysInstallPath(),
|
||||
"PowerToys.exe");
|
||||
|
||||
if (exePath == null || !File.Exists(exePath))
|
||||
{
|
||||
// Need to go into parent folder for PowerToys.exe. Likely a WinUI3 App SDK application.
|
||||
directoryPath = Path.Combine(directoryPath, "..");
|
||||
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
|
||||
}
|
||||
else
|
||||
{
|
||||
// PowerToys.exe is in the same path as the application.
|
||||
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
|
||||
Logger.LogError($"Failed to find powertoys exe path, {exePath}");
|
||||
return;
|
||||
}
|
||||
|
||||
Process.Start(new ProcessStartInfo(directoryPath) { Arguments = "--open-settings=" + SettingsWindowNameToString(window) });
|
||||
var args = "--open-settings=" + SettingsWindowNameToString(window);
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = exePath,
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
});
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO(stefan): Log exception once unified logging is implemented
|
||||
Logger.LogError(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,9 +113,6 @@
|
||||
<ProjectReference Include="..\..\common\version\version.vcxproj">
|
||||
<Project>{cc6e41ac-8174-4e8a-8d22-85dd7f4851df}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\common\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
@@ -128,4 +125,4 @@
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -36,5 +36,6 @@ namespace ManagedCommon
|
||||
PowerOCR,
|
||||
Workspaces,
|
||||
ZoomIt,
|
||||
GeneralSettings,
|
||||
}
|
||||
}
|
||||
|
||||
168
src/common/ManagedCommon/PowerToysPathResolver.cs
Normal file
168
src/common/ManagedCommon/PowerToysPathResolver.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace ManagedCommon
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
public class PowerToysPathResolver
|
||||
{
|
||||
private const string PowerToysRegistryKey = @"Software\Classes\powertoys";
|
||||
private const string PowerToysExe = "PowerToys.exe";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the PowerToys installation path by checking registry entries
|
||||
/// </summary>
|
||||
/// <returns>The path to PowerToys installation directory, or null if not found</returns>
|
||||
public static string GetPowerToysInstallPath()
|
||||
{
|
||||
#if DEBUG
|
||||
// In debug builds, resolve directly from the running process (no installer/registry involved).
|
||||
return GetPathFromCurrentProcess();
|
||||
#else
|
||||
// Try to get path from Per-User installation first
|
||||
string path = GetPathFromRegistry(RegistryHive.CurrentUser);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
// Fall back to Per-Machine installation
|
||||
path = GetPathFromRegistry(RegistryHive.LocalMachine);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
|
||||
private static string GetPathFromRegistry(RegistryHive hive)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var baseKey = RegistryKey.OpenBaseKey(hive, RegistryView.Registry64);
|
||||
|
||||
// First try to get path from the powertoys protocol registration
|
||||
string path = GetPathFromProtocolRegistration(baseKey);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore registry access errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string GetPathFromProtocolRegistration(RegistryKey baseKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = baseKey.OpenSubKey($@"{PowerToysRegistryKey}\shell\open\command");
|
||||
|
||||
if (key != null)
|
||||
{
|
||||
string command = key.GetValue(string.Empty)?.ToString();
|
||||
if (!string.IsNullOrEmpty(command))
|
||||
{
|
||||
// Parse command like: "C:\Program Files\PowerToys\PowerToys.exe" "%1"
|
||||
return ExtractPathFromCommand(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore registry access errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string GetPathFromCurrentProcess()
|
||||
{
|
||||
try
|
||||
{
|
||||
// If we're running inside PowerToys.exe (dev/debug builds), use the executable location.
|
||||
var processPath = Process.GetCurrentProcess().MainModule?.FileName;
|
||||
if (!string.IsNullOrEmpty(processPath))
|
||||
{
|
||||
var processDir = Path.GetDirectoryName(processPath);
|
||||
if (!string.IsNullOrEmpty(processDir) && File.Exists(Path.Combine(processDir, PowerToysExe)))
|
||||
{
|
||||
return processDir;
|
||||
}
|
||||
}
|
||||
|
||||
// As a fallback, walk up from AppContext.BaseDirectory to find PowerToys.exe.
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (directory != null)
|
||||
{
|
||||
var candidate = Path.Combine(directory.FullName, PowerToysExe);
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return directory.FullName;
|
||||
}
|
||||
|
||||
directory = directory.Parent;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore reflection/process permission errors; caller will see null and handle accordingly.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string ExtractPathFromCommand(string command)
|
||||
{
|
||||
if (string.IsNullOrEmpty(command))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Handle quoted paths: "C:\Program Files\PowerToys\PowerToys.exe" "%1"
|
||||
if (command.StartsWith('\"'))
|
||||
{
|
||||
int endQuote = command.IndexOf('\"', 1);
|
||||
if (endQuote > 1)
|
||||
{
|
||||
string exePath = command.Substring(1, endQuote - 1);
|
||||
if (File.Exists(exePath))
|
||||
{
|
||||
return Path.GetDirectoryName(exePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle unquoted paths (less common)
|
||||
string[] parts = command.Split(' ');
|
||||
if (parts.Length > 0 && File.Exists(parts[0]))
|
||||
{
|
||||
return Path.GetDirectoryName(parts[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore path parsing errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/common/PowerToys.ModuleContracts/IModuleService.cs
Normal file
47
src/common/PowerToys.ModuleContracts/IModuleService.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Common.UI;
|
||||
|
||||
namespace PowerToys.ModuleContracts;
|
||||
|
||||
/// <summary>
|
||||
/// Base contract for PowerToys modules exposed to the Command Palette.
|
||||
/// </summary>
|
||||
public interface IModuleService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets module identifier (e.g., Workspaces, Awake).
|
||||
/// </summary>
|
||||
string Key { get; }
|
||||
|
||||
Task<OperationResult> LaunchAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<OperationResult> OpenSettingsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper base to reduce duplication for simple modules.
|
||||
/// </summary>
|
||||
public abstract class ModuleServiceBase : IModuleService
|
||||
{
|
||||
public abstract string Key { get; }
|
||||
|
||||
protected abstract SettingsDeepLink.SettingsWindow SettingsWindow { get; }
|
||||
|
||||
public abstract Task<OperationResult> LaunchAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
public virtual Task<OperationResult> OpenSettingsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
SettingsDeepLink.OpenSettings(SettingsWindow);
|
||||
return Task.FromResult(OperationResult.Ok());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Task.FromResult(OperationResult.Fail($"Failed to open settings for {Key}: {ex.Message}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/common/PowerToys.ModuleContracts/OperationResult.cs
Normal file
30
src/common/PowerToys.ModuleContracts/OperationResult.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace PowerToys.ModuleContracts;
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight result type for module operations.
|
||||
/// </summary>
|
||||
public readonly record struct OperationResult(bool Success, string? Error = null)
|
||||
{
|
||||
public static OperationResult Ok() => new(true, null);
|
||||
|
||||
public static OperationResult Fail(string error) => new(false, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result type with a payload.
|
||||
/// </summary>
|
||||
public readonly record struct OperationResult<T>(bool Success, T? Value, string? Error = null);
|
||||
|
||||
/// <summary>
|
||||
/// Factory helpers for creating operation results.
|
||||
/// </summary>
|
||||
public static class OperationResults
|
||||
{
|
||||
public static OperationResult<T> Ok<T>(T value) => new(true, value, null);
|
||||
|
||||
public static OperationResult<T> Fail<T>(string error) => new(false, default, error);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Common.UI\Common.UI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,24 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<ProjectGuid>{6955446D-23F7-4023-9BB3-8657F904AF99}</ProjectGuid>
|
||||
@@ -67,9 +50,6 @@
|
||||
<ProjectReference Include="..\logger\logger.vcxproj">
|
||||
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Import Project="..\..\..\deps\spdlog.props" />
|
||||
@@ -85,4 +65,4 @@
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -119,6 +119,16 @@ namespace PowerToysSettings
|
||||
class HotkeyObject
|
||||
{
|
||||
public:
|
||||
HotkeyObject() :
|
||||
m_json(json::JsonObject())
|
||||
{
|
||||
m_json.SetNamedValue(L"win", json::value(false));
|
||||
m_json.SetNamedValue(L"ctrl", json::value(false));
|
||||
m_json.SetNamedValue(L"alt", json::value(false));
|
||||
m_json.SetNamedValue(L"shift", json::value(false));
|
||||
m_json.SetNamedValue(L"code", json::value(0));
|
||||
m_json.SetNamedValue(L"key", json::value(L""));
|
||||
}
|
||||
static HotkeyObject from_json(json::JsonObject json)
|
||||
{
|
||||
return HotkeyObject(std::move(json));
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
applyTo: "**/*.cs,**/*.cpp,**/*.c,**/*.h,**/*.hpp"
|
||||
---
|
||||
# Common – shared libraries guidance (concise)
|
||||
|
||||
Scope
|
||||
- Logging, IPC, settings, DPI, telemetry, utilities consumed by multiple modules.
|
||||
|
||||
Guidelines
|
||||
- Avoid breaking public headers/APIs; if changed, search & update all callers.
|
||||
- Coordinate ABI-impacting struct/class layout changes; keep binary compatibility.
|
||||
- Watch perf in hot paths (hooks, timers, serialization); avoid avoidable allocations.
|
||||
- Ask before adding third‑party deps or changing serialization formats.
|
||||
|
||||
Acceptance
|
||||
- No unintended ABI breaks, no noisy logs, new non-obvious symbols briefly commented.
|
||||
@@ -75,10 +75,62 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
{
|
||||
return CommonSharedConstants::ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE;
|
||||
}
|
||||
hstring Constants::AdvancedPasteShowUIEvent()
|
||||
{
|
||||
return CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_EVENT;
|
||||
}
|
||||
hstring Constants::AdvancedPasteTerminateAppMessage()
|
||||
{
|
||||
return CommonSharedConstants::ADVANCED_PASTE_TERMINATE_APP_MESSAGE;
|
||||
}
|
||||
hstring Constants::AlwaysOnTopPinEvent()
|
||||
{
|
||||
return CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT;
|
||||
}
|
||||
hstring Constants::FindMyMouseTriggerEvent()
|
||||
{
|
||||
return CommonSharedConstants::FIND_MY_MOUSE_TRIGGER_EVENT;
|
||||
}
|
||||
hstring Constants::MouseHighlighterTriggerEvent()
|
||||
{
|
||||
return CommonSharedConstants::MOUSE_HIGHLIGHTER_TRIGGER_EVENT;
|
||||
}
|
||||
hstring Constants::MouseCrosshairsTriggerEvent()
|
||||
{
|
||||
return CommonSharedConstants::MOUSE_CROSSHAIRS_TRIGGER_EVENT;
|
||||
}
|
||||
hstring Constants::CursorWrapTriggerEvent()
|
||||
{
|
||||
return CommonSharedConstants::CURSOR_WRAP_TRIGGER_EVENT;
|
||||
}
|
||||
hstring Constants::LightSwitchToggleEvent()
|
||||
{
|
||||
return CommonSharedConstants::LIGHTSWITCH_TOGGLE_EVENT;
|
||||
}
|
||||
hstring Constants::ZoomItZoomEvent()
|
||||
{
|
||||
return CommonSharedConstants::ZOOMIT_ZOOM_EVENT;
|
||||
}
|
||||
hstring Constants::ZoomItDrawEvent()
|
||||
{
|
||||
return CommonSharedConstants::ZOOMIT_DRAW_EVENT;
|
||||
}
|
||||
hstring Constants::ZoomItBreakEvent()
|
||||
{
|
||||
return CommonSharedConstants::ZOOMIT_BREAK_EVENT;
|
||||
}
|
||||
hstring Constants::ZoomItLiveZoomEvent()
|
||||
{
|
||||
return CommonSharedConstants::ZOOMIT_LIVEZOOM_EVENT;
|
||||
}
|
||||
hstring Constants::ZoomItSnipEvent()
|
||||
{
|
||||
return CommonSharedConstants::ZOOMIT_SNIP_EVENT;
|
||||
}
|
||||
hstring Constants::ZoomItRecordEvent()
|
||||
{
|
||||
return CommonSharedConstants::ZOOMIT_RECORD_EVENT;
|
||||
}
|
||||
hstring Constants::ShowPowerOCRSharedEvent()
|
||||
{
|
||||
return CommonSharedConstants::SHOW_POWEROCR_SHARED_EVENT;
|
||||
@@ -171,6 +223,10 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
{
|
||||
return CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT;
|
||||
}
|
||||
hstring Constants::CropAndLockScreenshotEvent()
|
||||
{
|
||||
return CommonSharedConstants::CROP_AND_LOCK_SCREENSHOT_EVENT;
|
||||
}
|
||||
hstring Constants::ShowEnvironmentVariablesSharedEvent()
|
||||
{
|
||||
return CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT;
|
||||
|
||||
@@ -23,6 +23,20 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring AdvancedPasteAdditionalActionMessage();
|
||||
static hstring AdvancedPasteCustomActionMessage();
|
||||
static hstring AdvancedPasteTerminateAppMessage();
|
||||
static hstring AdvancedPasteShowUIEvent();
|
||||
static hstring AlwaysOnTopPinEvent();
|
||||
static hstring MeasureToolTriggerEvent();
|
||||
static hstring FindMyMouseTriggerEvent();
|
||||
static hstring MouseHighlighterTriggerEvent();
|
||||
static hstring MouseCrosshairsTriggerEvent();
|
||||
static hstring CursorWrapTriggerEvent();
|
||||
static hstring LightSwitchToggleEvent();
|
||||
static hstring ZoomItZoomEvent();
|
||||
static hstring ZoomItDrawEvent();
|
||||
static hstring ZoomItBreakEvent();
|
||||
static hstring ZoomItLiveZoomEvent();
|
||||
static hstring ZoomItSnipEvent();
|
||||
static hstring ZoomItRecordEvent();
|
||||
static hstring ShowPowerOCRSharedEvent();
|
||||
static hstring TerminatePowerOCRSharedEvent();
|
||||
static hstring MouseJumpShowPreviewEvent();
|
||||
@@ -33,7 +47,6 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring PowerAccentExitEvent();
|
||||
static hstring ShortcutGuideTriggerEvent();
|
||||
static hstring RegistryPreviewTriggerEvent();
|
||||
static hstring MeasureToolTriggerEvent();
|
||||
static hstring GcodePreviewResizeEvent();
|
||||
static hstring BgcodePreviewResizeEvent();
|
||||
static hstring QoiPreviewResizeEvent();
|
||||
@@ -46,6 +59,7 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring TerminateHostsSharedEvent();
|
||||
static hstring CropAndLockThumbnailEvent();
|
||||
static hstring CropAndLockReparentEvent();
|
||||
static hstring CropAndLockScreenshotEvent();
|
||||
static hstring ShowEnvironmentVariablesSharedEvent();
|
||||
static hstring ShowEnvironmentVariablesAdminSharedEvent();
|
||||
static hstring WorkspacesLaunchEditorEvent();
|
||||
|
||||
@@ -20,6 +20,19 @@ namespace PowerToys
|
||||
static String AdvancedPasteAdditionalActionMessage();
|
||||
static String AdvancedPasteCustomActionMessage();
|
||||
static String AdvancedPasteTerminateAppMessage();
|
||||
static String AdvancedPasteShowUIEvent();
|
||||
static String AlwaysOnTopPinEvent();
|
||||
static String FindMyMouseTriggerEvent();
|
||||
static String MouseHighlighterTriggerEvent();
|
||||
static String MouseCrosshairsTriggerEvent();
|
||||
static String CursorWrapTriggerEvent();
|
||||
static String LightSwitchToggleEvent();
|
||||
static String ZoomItZoomEvent();
|
||||
static String ZoomItDrawEvent();
|
||||
static String ZoomItBreakEvent();
|
||||
static String ZoomItLiveZoomEvent();
|
||||
static String ZoomItSnipEvent();
|
||||
static String ZoomItRecordEvent();
|
||||
static String ShowPowerOCRSharedEvent();
|
||||
static String TerminatePowerOCRSharedEvent();
|
||||
static String MouseJumpShowPreviewEvent();
|
||||
@@ -43,6 +56,7 @@ namespace PowerToys
|
||||
static String TerminateHostsSharedEvent();
|
||||
static String CropAndLockThumbnailEvent();
|
||||
static String CropAndLockReparentEvent();
|
||||
static String CropAndLockScreenshotEvent();
|
||||
static String ShowEnvironmentVariablesSharedEvent();
|
||||
static String ShowEnvironmentVariablesAdminSharedEvent();
|
||||
static String WorkspacesLaunchEditorEvent();
|
||||
@@ -51,4 +65,4 @@ namespace PowerToys
|
||||
static String ShowCmdPalEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,9 +168,6 @@
|
||||
<ProjectReference Include="..\version\version.vcxproj">
|
||||
<Project>{cc6e41ac-8174-4e8a-8d22-85dd7f4851df}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
|
||||
@@ -40,6 +40,8 @@ namespace CommonSharedConstants
|
||||
const wchar_t ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE[] = L"CustomAction";
|
||||
|
||||
const wchar_t ADVANCED_PASTE_TERMINATE_APP_MESSAGE[] = L"TerminateApp";
|
||||
|
||||
const wchar_t ADVANCED_PASTE_SHOW_UI_EVENT[] = L"Local\\PowerToys_AdvancedPaste_ShowUI";
|
||||
|
||||
// Path to the event used to show Color Picker
|
||||
const wchar_t SHOW_COLOR_PICKER_SHARED_EVENT[] = L"Local\\ShowColorPickerEvent-8c46be2a-3e05-4186-b56b-4ae986ef2525";
|
||||
@@ -83,12 +85,21 @@ namespace CommonSharedConstants
|
||||
|
||||
const wchar_t TERMINATE_MOUSE_JUMP_SHARED_EVENT[] = L"Local\\TerminateMouseJumpEvent-252fa337-317f-4c37-a61f-99464c3f9728";
|
||||
|
||||
// Paths to the events used by other Mouse Utilities
|
||||
const wchar_t FIND_MY_MOUSE_TRIGGER_EVENT[] = L"Local\\FindMyMouseTriggerEvent-5a9dc5f4-1c74-4f2f-a66f-1b9b6a2f9b23";
|
||||
const wchar_t MOUSE_HIGHLIGHTER_TRIGGER_EVENT[] = L"Local\\MouseHighlighterTriggerEvent-1e3c9c3d-3fdf-4f9a-9a52-31c9b3c3a8f4";
|
||||
const wchar_t MOUSE_CROSSHAIRS_TRIGGER_EVENT[] = L"Local\\MouseCrosshairsTriggerEvent-0d4c7f92-0a5c-4f5c-b64b-8a2a2f7e0b21";
|
||||
const wchar_t CURSOR_WRAP_TRIGGER_EVENT[] = L"Local\\CursorWrapTriggerEvent-1f8452b5-4e6e-45b3-8b09-13f14a5900c9";
|
||||
|
||||
// Path to the event used by RegistryPreview
|
||||
const wchar_t REGISTRY_PREVIEW_TRIGGER_EVENT[] = L"Local\\RegistryPreviewEvent-4C559468-F75A-4E7F-BC4F-9C9688316687";
|
||||
|
||||
// Path to the event used by MeasureTool
|
||||
const wchar_t MEASURE_TOOL_TRIGGER_EVENT[] = L"Local\\MeasureToolEvent-3d46745f-09b3-4671-a577-236be7abd199";
|
||||
|
||||
// Path to the event used by LightSwitch
|
||||
const wchar_t LIGHTSWITCH_TOGGLE_EVENT[] = L"Local\\PowerToys-LightSwitch-ToggleEvent-d8dc2f29-8c94-4ca1-8c5f-3e2b1e3c4f5a";
|
||||
|
||||
// Path to the event used by GcodePreviewHandler
|
||||
const wchar_t GCODE_PREVIEW_RESIZE_EVENT[] = L"Local\\PowerToysGcodePreviewResizeEvent-6ff1f9bd-ccbd-4b24-a79f-40a34fb0317d";
|
||||
|
||||
@@ -121,6 +132,7 @@ namespace CommonSharedConstants
|
||||
// Path to the events used by CropAndLock
|
||||
const wchar_t CROP_AND_LOCK_REPARENT_EVENT[] = L"Local\\PowerToysCropAndLockReparentEvent-6060860a-76a1-44e8-8d0e-6355785e9c36";
|
||||
const wchar_t CROP_AND_LOCK_THUMBNAIL_EVENT[] = L"Local\\PowerToysCropAndLockThumbnailEvent-1637be50-da72-46b2-9220-b32b206b2434";
|
||||
const wchar_t CROP_AND_LOCK_SCREENSHOT_EVENT[] = L"Local\\PowerToysCropAndLockScreenshotEvent-ff077ab2-8360-4bd1-864a-637389d35593";
|
||||
const wchar_t CROP_AND_LOCK_EXIT_EVENT[] = L"Local\\PowerToysCropAndLockExitEvent-d995d409-7b70-482b-bad6-e7c8666f375a";
|
||||
|
||||
// Path to the events used by EnvironmentVariables
|
||||
@@ -130,6 +142,12 @@ namespace CommonSharedConstants
|
||||
// Path to the events used by ZoomIt
|
||||
const wchar_t ZOOMIT_REFRESH_SETTINGS_EVENT[] = L"Local\\PowerToysZoomIt-RefreshSettingsEvent-f053a563-d519-4b0d-8152-a54489c13324";
|
||||
const wchar_t ZOOMIT_EXIT_EVENT[] = L"Local\\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220";
|
||||
const wchar_t ZOOMIT_ZOOM_EVENT[] = L"Local\\PowerToysZoomIt-ZoomEvent-1e4190d7-94bc-4ad5-adc0-9a8fd07cb393";
|
||||
const wchar_t ZOOMIT_DRAW_EVENT[] = L"Local\\PowerToysZoomIt-DrawEvent-56338997-404d-4549-bd9a-d132b6766975";
|
||||
const wchar_t ZOOMIT_BREAK_EVENT[] = L"Local\\PowerToysZoomIt-BreakEvent-17f2e63c-4c56-41dd-90a0-2d12f9f50c6b";
|
||||
const wchar_t ZOOMIT_LIVEZOOM_EVENT[] = L"Local\\PowerToysZoomIt-LiveZoomEvent-390bf0c7-616f-47dc-bafe-a2d228add20d";
|
||||
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";
|
||||
|
||||
// used from quick access window
|
||||
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "EventLocker.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
EventLocker::EventLocker(HANDLE handle) :
|
||||
eventHandle(handle)
|
||||
{
|
||||
if (eventHandle)
|
||||
{
|
||||
SetEvent(eventHandle);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<EventLocker> EventLocker::Get(std::wstring eventName)
|
||||
{
|
||||
EventLocker locker(std::move(eventName));
|
||||
if (!locker.eventHandle)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return std::optional<EventLocker>(std::move(locker));
|
||||
}
|
||||
|
||||
EventLocker::EventLocker(EventLocker&& other) noexcept :
|
||||
eventHandle(other.eventHandle)
|
||||
{
|
||||
other.eventHandle = nullptr;
|
||||
}
|
||||
|
||||
EventLocker& EventLocker::operator=(EventLocker&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
eventHandle = other.eventHandle;
|
||||
other.eventHandle = nullptr;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
EventLocker::~EventLocker()
|
||||
{
|
||||
if (eventHandle)
|
||||
{
|
||||
ResetEvent(eventHandle);
|
||||
CloseHandle(eventHandle);
|
||||
eventHandle = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
EventLocker::EventLocker(std::wstring eventName)
|
||||
{
|
||||
eventHandle = CreateEvent(nullptr, true, false, eventName.c_str());
|
||||
if (!eventHandle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetEvent(eventHandle);
|
||||
}
|
||||
@@ -1,26 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
|
||||
class EventLocker
|
||||
{
|
||||
public:
|
||||
explicit EventLocker(HANDLE handle);
|
||||
EventLocker(HANDLE h)
|
||||
{
|
||||
eventHandle = h;
|
||||
SetEvent(eventHandle);
|
||||
}
|
||||
|
||||
static std::optional<EventLocker> Get(std::wstring eventName);
|
||||
static std::optional<EventLocker> Get(std::wstring eventName)
|
||||
{
|
||||
EventLocker locker(eventName);
|
||||
if (!locker.eventHandle)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
EventLocker(EventLocker&) = delete;
|
||||
EventLocker& operator=(EventLocker&) = delete;
|
||||
return locker;
|
||||
}
|
||||
|
||||
EventLocker(EventLocker&& other) noexcept;
|
||||
EventLocker& operator=(EventLocker&& other) noexcept;
|
||||
EventLocker(EventLocker& e) = delete;
|
||||
EventLocker& operator=(EventLocker& e) = delete;
|
||||
|
||||
~EventLocker();
|
||||
EventLocker(EventLocker&& e) noexcept
|
||||
{
|
||||
this->eventHandle = e.eventHandle;
|
||||
e.eventHandle = nullptr;
|
||||
}
|
||||
|
||||
EventLocker& operator=(EventLocker&& e) noexcept
|
||||
{
|
||||
this->eventHandle = e.eventHandle;
|
||||
e.eventHandle = nullptr;
|
||||
}
|
||||
|
||||
~EventLocker()
|
||||
{
|
||||
if (eventHandle)
|
||||
{
|
||||
ResetEvent(eventHandle);
|
||||
CloseHandle(eventHandle);
|
||||
eventHandle = nullptr;
|
||||
}
|
||||
}
|
||||
private:
|
||||
explicit EventLocker(std::wstring eventName);
|
||||
EventLocker(std::wstring eventName)
|
||||
{
|
||||
eventHandle = CreateEvent(nullptr, true, false, eventName.c_str());
|
||||
if (!eventHandle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HANDLE eventHandle = nullptr;
|
||||
SetEvent(eventHandle);
|
||||
}
|
||||
|
||||
HANDLE eventHandle;
|
||||
};
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "EventWaiter.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
EventWaiter::EventWaiter(const std::wstring& name, std::function<void(DWORD)> callback)
|
||||
{
|
||||
// Create localExitThreadEvent and localWaitingEvent for capturing. We cannot capture 'this' as we implement move constructor.
|
||||
const auto localExitThreadEvent = exitThreadEvent = CreateEvent(nullptr, false, false, nullptr);
|
||||
const HANDLE localWaitingEvent = waitingEvent = CreateEvent(nullptr, false, false, name.c_str());
|
||||
|
||||
std::thread([=]() {
|
||||
HANDLE events[2] = { localWaitingEvent, localExitThreadEvent };
|
||||
while (true)
|
||||
{
|
||||
const auto waitResult = WaitForMultipleObjects(2, events, false, INFINITE);
|
||||
if (waitResult == WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (waitResult == WAIT_FAILED)
|
||||
{
|
||||
callback(GetLastError());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (waitResult == WAIT_OBJECT_0)
|
||||
{
|
||||
callback(ERROR_SUCCESS);
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
EventWaiter::EventWaiter(EventWaiter&& other) noexcept :
|
||||
exitThreadEvent(other.exitThreadEvent),
|
||||
waitingEvent(other.waitingEvent)
|
||||
{
|
||||
other.exitThreadEvent = nullptr;
|
||||
other.waitingEvent = nullptr;
|
||||
}
|
||||
|
||||
EventWaiter& EventWaiter::operator=(EventWaiter&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
exitThreadEvent = other.exitThreadEvent;
|
||||
waitingEvent = other.waitingEvent;
|
||||
other.exitThreadEvent = nullptr;
|
||||
other.waitingEvent = nullptr;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
EventWaiter::~EventWaiter()
|
||||
{
|
||||
if (exitThreadEvent)
|
||||
{
|
||||
SetEvent(exitThreadEvent);
|
||||
CloseHandle(exitThreadEvent);
|
||||
exitThreadEvent = nullptr;
|
||||
}
|
||||
|
||||
if (waitingEvent)
|
||||
{
|
||||
CloseHandle(waitingEvent);
|
||||
waitingEvent = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,128 @@
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <string>
|
||||
#include <atomic>
|
||||
#include <windows.h>
|
||||
|
||||
/// <summary>
|
||||
/// A reusable utility class that listens for a named Windows event and invokes a callback when triggered.
|
||||
/// Provides RAII-based resource management for event handles and the listener thread.
|
||||
/// The thread is properly joined on destruction to ensure clean shutdown.
|
||||
/// </summary>
|
||||
class EventWaiter
|
||||
{
|
||||
public:
|
||||
EventWaiter() = default;
|
||||
EventWaiter(const std::wstring& name, std::function<void(DWORD)> callback);
|
||||
EventWaiter(EventWaiter&) = delete;
|
||||
EventWaiter& operator=(EventWaiter&) = delete;
|
||||
EventWaiter(EventWaiter&& other) noexcept;
|
||||
EventWaiter& operator=(EventWaiter&& other) noexcept;
|
||||
~EventWaiter();
|
||||
|
||||
EventWaiter(const EventWaiter&) = delete;
|
||||
EventWaiter& operator=(const EventWaiter&) = delete;
|
||||
EventWaiter(EventWaiter&&) = delete;
|
||||
EventWaiter& operator=(EventWaiter&&) = delete;
|
||||
|
||||
~EventWaiter()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts listening for the specified named event. When the event is signaled, the callback is invoked.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the Windows event to listen for.</param>
|
||||
/// <param name="callback">The callback function to invoke when the event is triggered. Receives ERROR_SUCCESS on success.</param>
|
||||
/// <returns>true if listening started successfully, false otherwise.</returns>
|
||||
bool start(const std::wstring& name, std::function<void(DWORD)> callback)
|
||||
{
|
||||
if (m_listening)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_exitThreadEvent = CreateEventW(nullptr, false, false, nullptr);
|
||||
m_waitingEvent = CreateEventW(nullptr, false, false, name.c_str());
|
||||
|
||||
if (!m_exitThreadEvent || !m_waitingEvent)
|
||||
{
|
||||
cleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_listening = true;
|
||||
m_eventThread = std::thread([this, cb = std::move(callback)]() {
|
||||
HANDLE events[2] = { m_waitingEvent, m_exitThreadEvent };
|
||||
while (m_listening)
|
||||
{
|
||||
auto waitResult = WaitForMultipleObjects(2, events, false, INFINITE);
|
||||
if (!m_listening)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (waitResult == WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
// Exit event signaled
|
||||
break;
|
||||
}
|
||||
|
||||
if (waitResult == WAIT_FAILED)
|
||||
{
|
||||
cb(GetLastError());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (waitResult == WAIT_OBJECT_0)
|
||||
{
|
||||
cb(ERROR_SUCCESS);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops listening for the event and cleans up resources.
|
||||
/// Waits for the listener thread to finish before returning.
|
||||
/// Safe to call multiple times.
|
||||
/// </summary>
|
||||
void stop()
|
||||
{
|
||||
m_listening = false;
|
||||
if (m_exitThreadEvent)
|
||||
{
|
||||
SetEvent(m_exitThreadEvent);
|
||||
}
|
||||
if (m_eventThread.joinable())
|
||||
{
|
||||
m_eventThread.join();
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the listener is currently active.
|
||||
/// </summary>
|
||||
bool is_listening() const
|
||||
{
|
||||
return m_listening;
|
||||
}
|
||||
|
||||
private:
|
||||
HANDLE exitThreadEvent = nullptr;
|
||||
HANDLE waitingEvent = nullptr;
|
||||
};
|
||||
void cleanup()
|
||||
{
|
||||
if (m_exitThreadEvent)
|
||||
{
|
||||
CloseHandle(m_exitThreadEvent);
|
||||
m_exitThreadEvent = nullptr;
|
||||
}
|
||||
if (m_waitingEvent)
|
||||
{
|
||||
CloseHandle(m_waitingEvent);
|
||||
m_waitingEvent = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
HANDLE m_exitThreadEvent = nullptr;
|
||||
HANDLE m_waitingEvent = nullptr;
|
||||
std::thread m_eventThread;
|
||||
std::atomic_bool m_listening{ false };
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "HDropIterator.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
HDropIterator::HDropIterator(IDataObject* dataObject)
|
||||
{
|
||||
FORMATETC formatetc{
|
||||
CF_HDROP,
|
||||
nullptr,
|
||||
DVASPECT_CONTENT,
|
||||
-1,
|
||||
TYMED_HGLOBAL
|
||||
};
|
||||
|
||||
if (dataObject && SUCCEEDED(dataObject->GetData(&formatetc, &m_medium)))
|
||||
{
|
||||
_listCount = DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), 0xFFFFFFFF, nullptr, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_medium = {};
|
||||
}
|
||||
}
|
||||
|
||||
HDropIterator::~HDropIterator()
|
||||
{
|
||||
if (m_medium.tymed)
|
||||
{
|
||||
ReleaseStgMedium(&m_medium);
|
||||
}
|
||||
}
|
||||
|
||||
void HDropIterator::First()
|
||||
{
|
||||
_current = 0;
|
||||
}
|
||||
|
||||
void HDropIterator::Next()
|
||||
{
|
||||
++_current;
|
||||
}
|
||||
|
||||
bool HDropIterator::IsDone() const
|
||||
{
|
||||
return _current >= _listCount;
|
||||
}
|
||||
|
||||
LPTSTR HDropIterator::CurrentItem() const
|
||||
{
|
||||
const UINT cch = DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), _current, nullptr, 0) + 1;
|
||||
LPTSTR path = static_cast<LPTSTR>(malloc(sizeof(TCHAR) * cch));
|
||||
if (!path)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), _current, path, cch);
|
||||
return path;
|
||||
}
|
||||
@@ -1,21 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <objidl.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
class HDropIterator
|
||||
{
|
||||
public:
|
||||
explicit HDropIterator(IDataObject* dataObject);
|
||||
~HDropIterator();
|
||||
HDropIterator(IDataObject* pDataObject)
|
||||
{
|
||||
_current = 0;
|
||||
_listCount = 0;
|
||||
|
||||
void First();
|
||||
void Next();
|
||||
[[nodiscard]] bool IsDone() const;
|
||||
[[nodiscard]] LPTSTR CurrentItem() const;
|
||||
FORMATETC formatetc = {
|
||||
CF_HDROP,
|
||||
NULL,
|
||||
DVASPECT_CONTENT,
|
||||
-1,
|
||||
TYMED_HGLOBAL
|
||||
};
|
||||
|
||||
if (SUCCEEDED(pDataObject->GetData(&formatetc, &m_medium)))
|
||||
{
|
||||
_listCount = DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), 0xFFFFFFFF, NULL, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_medium = {};
|
||||
}
|
||||
}
|
||||
|
||||
~HDropIterator()
|
||||
{
|
||||
if (m_medium.tymed)
|
||||
{
|
||||
ReleaseStgMedium(&m_medium);
|
||||
}
|
||||
}
|
||||
|
||||
void First()
|
||||
{
|
||||
_current = 0;
|
||||
}
|
||||
|
||||
void Next()
|
||||
{
|
||||
_current++;
|
||||
}
|
||||
|
||||
bool IsDone() const
|
||||
{
|
||||
return _current >= _listCount;
|
||||
}
|
||||
|
||||
LPTSTR CurrentItem() const
|
||||
{
|
||||
UINT cch = DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), _current, NULL, 0) + 1;
|
||||
LPTSTR pszPath = static_cast<LPTSTR>(malloc(sizeof(TCHAR) * cch));
|
||||
|
||||
DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), _current, pszPath, cch);
|
||||
|
||||
return pszPath;
|
||||
}
|
||||
|
||||
private:
|
||||
UINT _listCount = 0;
|
||||
STGMEDIUM m_medium{};
|
||||
UINT _current = 0;
|
||||
UINT _listCount;
|
||||
STGMEDIUM m_medium;
|
||||
UINT _current;
|
||||
};
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "HttpClient.h"
|
||||
|
||||
namespace http
|
||||
{
|
||||
HttpClient::HttpClient()
|
||||
{
|
||||
auto headers = m_client.DefaultRequestHeaders();
|
||||
headers.UserAgent().TryParseAdd(USER_AGENT);
|
||||
}
|
||||
|
||||
std::future<std::wstring> HttpClient::request(const winrt::Windows::Foundation::Uri& url)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url);
|
||||
(void)response.EnsureSuccessStatusCode();
|
||||
auto body = co_await response.Content().ReadAsStringAsync();
|
||||
co_return std::wstring(body);
|
||||
}
|
||||
|
||||
std::future<void> HttpClient::download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url);
|
||||
(void)response.EnsureSuccessStatusCode();
|
||||
auto fileStream = co_await storage::Streams::FileRandomAccessStream::OpenAsync(
|
||||
dstFilePath.c_str(),
|
||||
storage::FileAccessMode::ReadWrite,
|
||||
storage::StorageOpenOptions::AllowReadersAndWriters,
|
||||
storage::Streams::FileOpenDisposition::CreateAlways);
|
||||
co_await response.Content().WriteToStreamAsync(fileStream);
|
||||
fileStream.Close();
|
||||
}
|
||||
|
||||
std::future<void> HttpClient::download(const winrt::Windows::Foundation::Uri& url,
|
||||
const std::wstring& dstFilePath,
|
||||
const std::function<void(float)>& progressUpdateCallback)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url, winrt::Windows::Web::Http::HttpCompletionOption::ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
const uint64_t totalBytes = response.Content().Headers().ContentLength().GetUInt64();
|
||||
auto contentStream = co_await response.Content().ReadAsInputStreamAsync();
|
||||
|
||||
uint64_t totalBytesRead = 0;
|
||||
storage::Streams::Buffer buffer(8192);
|
||||
auto fileStream = co_await storage::Streams::FileRandomAccessStream::OpenAsync(
|
||||
dstFilePath.c_str(),
|
||||
storage::FileAccessMode::ReadWrite,
|
||||
storage::StorageOpenOptions::AllowReadersAndWriters,
|
||||
storage::Streams::FileOpenDisposition::CreateAlways);
|
||||
|
||||
co_await contentStream.ReadAsync(buffer, buffer.Capacity(), storage::Streams::InputStreamOptions::None);
|
||||
while (buffer.Length() > 0)
|
||||
{
|
||||
co_await fileStream.WriteAsync(buffer);
|
||||
totalBytesRead += buffer.Length();
|
||||
if (progressUpdateCallback)
|
||||
{
|
||||
const float percentage = static_cast<float>(totalBytesRead) / totalBytes;
|
||||
progressUpdateCallback(percentage);
|
||||
}
|
||||
|
||||
co_await contentStream.ReadAsync(buffer, buffer.Capacity(), storage::Streams::InputStreamOptions::None);
|
||||
}
|
||||
|
||||
if (progressUpdateCallback)
|
||||
{
|
||||
progressUpdateCallback(1);
|
||||
}
|
||||
|
||||
fileStream.Close();
|
||||
contentStream.Close();
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,63 @@ namespace http
|
||||
class HttpClient
|
||||
{
|
||||
public:
|
||||
HttpClient();
|
||||
HttpClient()
|
||||
{
|
||||
auto headers = m_client.DefaultRequestHeaders();
|
||||
headers.UserAgent().TryParseAdd(USER_AGENT);
|
||||
}
|
||||
|
||||
std::future<std::wstring> request(const winrt::Windows::Foundation::Uri& url);
|
||||
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath);
|
||||
std::future<void> download(const winrt::Windows::Foundation::Uri& url,
|
||||
const std::wstring& dstFilePath,
|
||||
const std::function<void(float)>& progressUpdateCallback);
|
||||
std::future<std::wstring> request(const winrt::Windows::Foundation::Uri& url)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url);
|
||||
(void)response.EnsureSuccessStatusCode();
|
||||
auto body = co_await response.Content().ReadAsStringAsync();
|
||||
co_return std::wstring(body);
|
||||
}
|
||||
|
||||
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url);
|
||||
(void)response.EnsureSuccessStatusCode();
|
||||
auto file_stream = co_await storage::Streams::FileRandomAccessStream::OpenAsync(dstFilePath.c_str(), storage::FileAccessMode::ReadWrite, storage::StorageOpenOptions::AllowReadersAndWriters, storage::Streams::FileOpenDisposition::CreateAlways);
|
||||
co_await response.Content().WriteToStreamAsync(file_stream);
|
||||
file_stream.Close();
|
||||
}
|
||||
|
||||
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath, const std::function<void(float)>& progressUpdateCallback)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url, HttpCompletionOption::ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
uint64_t totalBytes = response.Content().Headers().ContentLength().GetUInt64();
|
||||
auto contentStream = co_await response.Content().ReadAsInputStreamAsync();
|
||||
|
||||
uint64_t totalBytesRead = 0;
|
||||
storage::Streams::Buffer buffer(8192);
|
||||
auto fileStream = co_await storage::Streams::FileRandomAccessStream::OpenAsync(dstFilePath.c_str(), storage::FileAccessMode::ReadWrite, storage::StorageOpenOptions::AllowReadersAndWriters, storage::Streams::FileOpenDisposition::CreateAlways);
|
||||
|
||||
co_await contentStream.ReadAsync(buffer, buffer.Capacity(), storage::Streams::InputStreamOptions::None);
|
||||
while (buffer.Length() > 0)
|
||||
{
|
||||
co_await fileStream.WriteAsync(buffer);
|
||||
totalBytesRead += buffer.Length();
|
||||
if (progressUpdateCallback)
|
||||
{
|
||||
float percentage = static_cast<float>(totalBytesRead) / totalBytes;
|
||||
progressUpdateCallback(percentage);
|
||||
}
|
||||
|
||||
co_await contentStream.ReadAsync(buffer, buffer.Capacity(), storage::Streams::InputStreamOptions::None);
|
||||
}
|
||||
|
||||
if (progressUpdateCallback)
|
||||
{
|
||||
progressUpdateCallback(1);
|
||||
}
|
||||
|
||||
fileStream.Close();
|
||||
contentStream.Close();
|
||||
}
|
||||
|
||||
private:
|
||||
winrt::Windows::Web::Http::HttpClient m_client;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "MsWindowsSettings.h"
|
||||
|
||||
bool GetAnimationsEnabled()
|
||||
{
|
||||
BOOL enabled = 0;
|
||||
const auto result = SystemParametersInfo(SPI_GETCLIENTAREAANIMATION, 0, &enabled, 0);
|
||||
if (!result)
|
||||
{
|
||||
Logger::error("SystemParametersInfo SPI_GETCLIENTAREAANIMATION failed.");
|
||||
}
|
||||
return enabled != 0;
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <common/logger/logger.h>
|
||||
#include <windows.h>
|
||||
|
||||
bool GetAnimationsEnabled();
|
||||
inline bool GetAnimationsEnabled()
|
||||
{
|
||||
BOOL enabled = 0;
|
||||
BOOL fResult;
|
||||
fResult = SystemParametersInfo(SPI_GETCLIENTAREAANIMATION, 0, &enabled, 0);
|
||||
if (!fResult)
|
||||
{
|
||||
Logger::error("SystemParametersInfo SPI_GETCLIENTAREAANIMATION failed.");
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "MsiUtils.h"
|
||||
|
||||
std::optional<std::wstring> GetMsiPackageInstalledPath(bool perUser)
|
||||
{
|
||||
constexpr size_t guid_length = 39;
|
||||
wchar_t productId[guid_length];
|
||||
const std::wstring upgradeCode = perUser ? POWER_TOYS_UPGRADE_CODE_USER : POWER_TOYS_UPGRADE_CODE;
|
||||
if (const bool found = ERROR_SUCCESS == MsiEnumRelatedProductsW(upgradeCode.c_str(), 0, 0, productId); !found)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (const bool installed = INSTALLSTATE_DEFAULT == MsiQueryProductStateW(productId); !installed)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
DWORD bufferSize = MAX_PATH;
|
||||
wchar_t buffer[MAX_PATH];
|
||||
if (ERROR_SUCCESS == MsiGetProductInfoW(productId, INSTALLPROPERTY_INSTALLLOCATION, buffer, &bufferSize) && bufferSize)
|
||||
{
|
||||
return buffer;
|
||||
}
|
||||
|
||||
DWORD packagePathSize = 0;
|
||||
|
||||
if (ERROR_SUCCESS != MsiGetProductInfoW(productId, INSTALLPROPERTY_LOCALPACKAGE, nullptr, &packagePathSize))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::wstring packagePath(++packagePathSize, L'\0');
|
||||
|
||||
if (ERROR_SUCCESS != MsiGetProductInfoW(productId, INSTALLPROPERTY_LOCALPACKAGE, packagePath.data(), &packagePathSize))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
packagePath.resize(size(packagePath) - 1); // trim additional \0 which we got from MsiGetProductInfoW
|
||||
|
||||
wchar_t path[MAX_PATH];
|
||||
DWORD pathSize = MAX_PATH;
|
||||
MsiGetComponentPathW(productId, POWERTOYS_EXE_COMPONENT, path, &pathSize);
|
||||
if (!pathSize)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
PathCchRemoveFileSpec(path, pathSize);
|
||||
return path;
|
||||
}
|
||||
|
||||
std::wstring GetMsiPackagePath()
|
||||
{
|
||||
std::wstring packagePath;
|
||||
wchar_t productString[39];
|
||||
if (const bool found = ERROR_SUCCESS == MsiEnumRelatedProductsW(POWER_TOYS_UPGRADE_CODE, 0, 0, productString); !found)
|
||||
{
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
if (const bool installed = INSTALLSTATE_DEFAULT == MsiQueryProductStateW(productString); !installed)
|
||||
{
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
DWORD packagePathSize = 0;
|
||||
|
||||
if (const bool hasPackagePath = ERROR_SUCCESS == MsiGetProductInfoW(productString, INSTALLPROPERTY_LOCALPACKAGE, nullptr, &packagePathSize); !hasPackagePath)
|
||||
{
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
packagePath = std::wstring(++packagePathSize, L'\0');
|
||||
if (const bool gotPackagePath = ERROR_SUCCESS == MsiGetProductInfoW(productString, INSTALLPROPERTY_LOCALPACKAGE, packagePath.data(), &packagePathSize); !gotPackagePath)
|
||||
{
|
||||
packagePath.clear();
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
packagePath.resize(size(packagePath) - 1); // trim additional \0 which we got from MsiGetProductInfoW
|
||||
|
||||
return packagePath;
|
||||
}
|
||||
@@ -16,5 +16,82 @@ namespace // Strings in this namespace should not be localized
|
||||
const inline wchar_t POWERTOYS_EXE_COMPONENT[] = L"{A2C66D91-3485-4D00-B04D-91844E6B345B}";
|
||||
}
|
||||
|
||||
std::optional<std::wstring> GetMsiPackageInstalledPath(bool perUser);
|
||||
std::wstring GetMsiPackagePath();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "OnThreadExecutor.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
OnThreadExecutor::OnThreadExecutor() :
|
||||
_worker_thread([this] { worker_thread(); })
|
||||
{
|
||||
}
|
||||
|
||||
OnThreadExecutor::~OnThreadExecutor()
|
||||
{
|
||||
_shutdown_request = true;
|
||||
_task_cv.notify_one();
|
||||
if (_worker_thread.joinable())
|
||||
{
|
||||
_worker_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
std::future<void> OnThreadExecutor::submit(task_t task)
|
||||
{
|
||||
auto future = task.get_future();
|
||||
{
|
||||
std::lock_guard lock{ _task_mutex };
|
||||
_task_queue.emplace(std::move(task));
|
||||
}
|
||||
_task_cv.notify_one();
|
||||
return future;
|
||||
}
|
||||
|
||||
void OnThreadExecutor::cancel()
|
||||
{
|
||||
std::lock_guard lock{ _task_mutex };
|
||||
std::queue<task_t> emptyQueue;
|
||||
std::swap(_task_queue, emptyQueue);
|
||||
_task_cv.notify_one();
|
||||
}
|
||||
|
||||
void OnThreadExecutor::worker_thread()
|
||||
{
|
||||
while (!_shutdown_request)
|
||||
{
|
||||
task_t task;
|
||||
{
|
||||
std::unique_lock task_lock{ _task_mutex };
|
||||
_task_cv.wait(task_lock, [this] { return !_task_queue.empty() || _shutdown_request; });
|
||||
if (_shutdown_request)
|
||||
{
|
||||
return;
|
||||
}
|
||||
task = std::move(_task_queue.front());
|
||||
_task_queue.pop();
|
||||
}
|
||||
task();
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
#include <functional>
|
||||
#include <queue>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
|
||||
// OnThreadExecutor allows its caller to off-load some work to a persistently running background thread.
|
||||
// This might come in handy if you use the API which sets thread-wide global state and the state needs
|
||||
@@ -17,18 +15,58 @@ class OnThreadExecutor final
|
||||
public:
|
||||
using task_t = std::packaged_task<void()>;
|
||||
|
||||
OnThreadExecutor();
|
||||
~OnThreadExecutor();
|
||||
OnThreadExecutor() :
|
||||
_shutdown_request{ false },
|
||||
_worker_thread{ [this] { worker_thread(); } }
|
||||
{
|
||||
}
|
||||
|
||||
std::future<void> submit(task_t task);
|
||||
void cancel();
|
||||
~OnThreadExecutor()
|
||||
{
|
||||
_shutdown_request = true;
|
||||
_task_cv.notify_one();
|
||||
_worker_thread.join();
|
||||
}
|
||||
|
||||
std::future<void> submit(task_t task)
|
||||
{
|
||||
auto future = task.get_future();
|
||||
std::lock_guard lock{ _task_mutex };
|
||||
_task_queue.emplace(std::move(task));
|
||||
_task_cv.notify_one();
|
||||
return future;
|
||||
}
|
||||
|
||||
void cancel()
|
||||
{
|
||||
std::lock_guard lock{ _task_mutex };
|
||||
_task_queue = {};
|
||||
_task_cv.notify_one();
|
||||
}
|
||||
|
||||
private:
|
||||
void worker_thread();
|
||||
void worker_thread()
|
||||
{
|
||||
while (!_shutdown_request)
|
||||
{
|
||||
task_t task;
|
||||
{
|
||||
std::unique_lock task_lock{ _task_mutex };
|
||||
_task_cv.wait(task_lock, [this] { return !_task_queue.empty() || _shutdown_request; });
|
||||
if (_shutdown_request)
|
||||
{
|
||||
return;
|
||||
}
|
||||
task = std::move(_task_queue.front());
|
||||
_task_queue.pop();
|
||||
}
|
||||
task();
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex _task_mutex;
|
||||
std::condition_variable _task_cv;
|
||||
std::atomic_bool _shutdown_request{ false };
|
||||
std::atomic_bool _shutdown_request;
|
||||
std::queue<std::packaged_task<void()>> _task_queue;
|
||||
std::thread _worker_thread;
|
||||
};
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "ProcessWaiter.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace ProcessWaiter
|
||||
{
|
||||
void OnProcessTerminate(std::wstring parent_pid, std::function<void(DWORD)> callback)
|
||||
{
|
||||
DWORD pid = 0;
|
||||
try
|
||||
{
|
||||
pid = std::stoul(parent_pid);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
callback(ERROR_INVALID_PARAMETER);
|
||||
return;
|
||||
}
|
||||
|
||||
std::thread([pid, callback = std::move(callback)]() mutable {
|
||||
wil::unique_handle process{ OpenProcess(SYNCHRONIZE, FALSE, pid) };
|
||||
if (process)
|
||||
{
|
||||
if (WaitForSingleObject(process.get(), INFINITE) == WAIT_OBJECT_0)
|
||||
{
|
||||
callback(ERROR_SUCCESS);
|
||||
}
|
||||
else
|
||||
{
|
||||
callback(GetLastError());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
callback(GetLastError());
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <Windows.h>
|
||||
#include <thread>
|
||||
|
||||
namespace ProcessWaiter
|
||||
{
|
||||
void OnProcessTerminate(std::wstring parent_pid, std::function<void(DWORD)> callback);
|
||||
void OnProcessTerminate(std::wstring parent_pid, std::function<void(DWORD)> callback)
|
||||
{
|
||||
DWORD pid = std::stol(parent_pid);
|
||||
std::thread([=]() {
|
||||
HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, pid);
|
||||
if (process != nullptr)
|
||||
{
|
||||
if (WaitForSingleObject(process, INFINITE) == WAIT_OBJECT_0)
|
||||
{
|
||||
CloseHandle(process);
|
||||
callback(ERROR_SUCCESS);
|
||||
}
|
||||
else
|
||||
{
|
||||
CloseHandle(process);
|
||||
callback(GetLastError());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
callback(GetLastError());
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "UnhandledExceptionHandler.h"
|
||||
|
||||
#include <DbgHelp.h>
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
#include <sstream>
|
||||
|
||||
#include "winapi_error.h"
|
||||
#include "../logger/logger.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::atomic_bool processingException{ false };
|
||||
|
||||
const char* exceptionDescription(const DWORD code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case EXCEPTION_ACCESS_VIOLATION:
|
||||
return "EXCEPTION_ACCESS_VIOLATION";
|
||||
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
|
||||
return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED";
|
||||
case EXCEPTION_BREAKPOINT:
|
||||
return "EXCEPTION_BREAKPOINT";
|
||||
case EXCEPTION_DATATYPE_MISALIGNMENT:
|
||||
return "EXCEPTION_DATATYPE_MISALIGNMENT";
|
||||
case EXCEPTION_FLT_DENORMAL_OPERAND:
|
||||
return "EXCEPTION_FLT_DENORMAL_OPERAND";
|
||||
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_FLT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_FLT_INEXACT_RESULT:
|
||||
return "EXCEPTION_FLT_INEXACT_RESULT";
|
||||
case EXCEPTION_FLT_INVALID_OPERATION:
|
||||
return "EXCEPTION_FLT_INVALID_OPERATION";
|
||||
case EXCEPTION_FLT_OVERFLOW:
|
||||
return "EXCEPTION_FLT_OVERFLOW";
|
||||
case EXCEPTION_FLT_STACK_CHECK:
|
||||
return "EXCEPTION_FLT_STACK_CHECK";
|
||||
case EXCEPTION_FLT_UNDERFLOW:
|
||||
return "EXCEPTION_FLT_UNDERFLOW";
|
||||
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
||||
return "EXCEPTION_ILLEGAL_INSTRUCTION";
|
||||
case EXCEPTION_IN_PAGE_ERROR:
|
||||
return "EXCEPTION_IN_PAGE_ERROR";
|
||||
case EXCEPTION_INT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_INT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_INT_OVERFLOW:
|
||||
return "EXCEPTION_INT_OVERFLOW";
|
||||
case EXCEPTION_INVALID_DISPOSITION:
|
||||
return "EXCEPTION_INVALID_DISPOSITION";
|
||||
case EXCEPTION_NONCONTINUABLE_EXCEPTION:
|
||||
return "EXCEPTION_NONCONTINUABLE_EXCEPTION";
|
||||
case EXCEPTION_PRIV_INSTRUCTION:
|
||||
return "EXCEPTION_PRIV_INSTRUCTION";
|
||||
case EXCEPTION_SINGLE_STEP:
|
||||
return "EXCEPTION_SINGLE_STEP";
|
||||
case EXCEPTION_STACK_OVERFLOW:
|
||||
return "EXCEPTION_STACK_OVERFLOW";
|
||||
default:
|
||||
return "UNKNOWN EXCEPTION";
|
||||
}
|
||||
}
|
||||
|
||||
int GetFilenameStart(wchar_t* path)
|
||||
{
|
||||
int pos = 0;
|
||||
int found = 0;
|
||||
if (path != nullptr)
|
||||
{
|
||||
while (path[pos] != L'\0' && pos < MAX_PATH)
|
||||
{
|
||||
if (path[pos] == L'\\')
|
||||
{
|
||||
found = pos + 1;
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
std::wstring GetModuleName(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static wchar_t modulePath[MAX_PATH]{};
|
||||
memset(&modulePath[0], '\0', sizeof(modulePath));
|
||||
|
||||
const DWORD64 moduleBase = SymGetModuleBase64(process, stack.AddrPC.Offset);
|
||||
if (!moduleBase)
|
||||
{
|
||||
Logger::error(L"Failed to get a module. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
if (!GetModuleFileNameW(reinterpret_cast<HINSTANCE>(moduleBase), modulePath, MAX_PATH))
|
||||
{
|
||||
Logger::error(L"Failed to get a module path. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
const int start = GetFilenameStart(modulePath);
|
||||
return std::wstring(modulePath, start);
|
||||
}
|
||||
|
||||
std::wstring GetName(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static IMAGEHLP_SYMBOL64* pSymbol = static_cast<IMAGEHLP_SYMBOL64*>(malloc(sizeof(IMAGEHLP_SYMBOL64) + MAX_PATH * sizeof(TCHAR)));
|
||||
if (!pSymbol)
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
memset(pSymbol, '\0', sizeof(*pSymbol) + MAX_PATH);
|
||||
pSymbol->MaxNameLength = MAX_PATH;
|
||||
pSymbol->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL64);
|
||||
|
||||
DWORD64 displacement = 0;
|
||||
if (!SymGetSymFromAddr64(process, stack.AddrPC.Offset, &displacement, pSymbol))
|
||||
{
|
||||
Logger::error(L"Failed to get a symbol. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
std::string str = pSymbol->Name;
|
||||
return std::wstring(str.begin(), str.end());
|
||||
}
|
||||
|
||||
std::wstring GetLine(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static IMAGEHLP_LINE64 line{};
|
||||
|
||||
memset(&line, '\0', sizeof(IMAGEHLP_LINE64));
|
||||
line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
|
||||
line.LineNumber = 0;
|
||||
|
||||
DWORD displacement = 0;
|
||||
if (!SymGetLineFromAddr64(process, stack.AddrPC.Offset, &displacement, &line))
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
std::string fileName(line.FileName);
|
||||
return L"(" + std::wstring(fileName.begin(), fileName.end()) + L":" + std::to_wstring(line.LineNumber) + L")";
|
||||
}
|
||||
|
||||
void LogStackTrace()
|
||||
{
|
||||
CONTEXT context;
|
||||
try
|
||||
{
|
||||
RtlCaptureContext(&context);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Failed to capture context. {}", get_last_error_or_default(GetLastError()));
|
||||
return;
|
||||
}
|
||||
|
||||
STACKFRAME64 stack;
|
||||
memset(&stack, 0, sizeof(STACKFRAME64));
|
||||
|
||||
HANDLE process = GetCurrentProcess();
|
||||
HANDLE thread = GetCurrentThread();
|
||||
|
||||
#ifdef _M_ARM64
|
||||
stack.AddrPC.Offset = context.Pc;
|
||||
stack.AddrStack.Offset = context.Sp;
|
||||
stack.AddrFrame.Offset = context.Fp;
|
||||
#else
|
||||
stack.AddrPC.Offset = context.Rip;
|
||||
stack.AddrStack.Offset = context.Rsp;
|
||||
stack.AddrFrame.Offset = context.Rbp;
|
||||
#endif
|
||||
stack.AddrPC.Mode = AddrModeFlat;
|
||||
stack.AddrStack.Mode = AddrModeFlat;
|
||||
stack.AddrFrame.Mode = AddrModeFlat;
|
||||
|
||||
std::wstringstream ss;
|
||||
for (;;)
|
||||
{
|
||||
const BOOL result = StackWalk64(
|
||||
#ifdef _M_ARM64
|
||||
IMAGE_FILE_MACHINE_ARM64,
|
||||
#else
|
||||
IMAGE_FILE_MACHINE_AMD64,
|
||||
#endif
|
||||
process,
|
||||
thread,
|
||||
&stack,
|
||||
&context,
|
||||
NULL,
|
||||
SymFunctionTableAccess64,
|
||||
SymGetModuleBase64,
|
||||
NULL);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ss << GetModuleName(process, stack) << "!" << GetName(process, stack) << GetLine(process, stack) << std::endl;
|
||||
}
|
||||
|
||||
Logger::error(L"STACK TRACE\r\n{}", ss.str());
|
||||
Logger::flush();
|
||||
}
|
||||
}
|
||||
|
||||
LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info)
|
||||
{
|
||||
bool expected = false;
|
||||
if (!processingException.compare_exchange_strong(expected, true))
|
||||
{
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
auto guard = wil::scope_exit([]() noexcept { processingException = false; });
|
||||
|
||||
try
|
||||
{
|
||||
const char* description = "Exception code not available";
|
||||
if (info != nullptr && info->ExceptionRecord != nullptr && info->ExceptionRecord->ExceptionCode != 0)
|
||||
{
|
||||
description = exceptionDescription(info->ExceptionRecord->ExceptionCode);
|
||||
}
|
||||
|
||||
Logger::error(description);
|
||||
LogStackTrace();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to log stack trace");
|
||||
Logger::flush();
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
void AbortHandler(int /*signal_number*/)
|
||||
{
|
||||
Logger::error("--- ABORT");
|
||||
try
|
||||
{
|
||||
LogStackTrace();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to log stack trace on abort");
|
||||
Logger::flush();
|
||||
}
|
||||
}
|
||||
|
||||
void InitSymbols()
|
||||
{
|
||||
// Preload symbols so they will be available in case of out-of-memory exception
|
||||
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME);
|
||||
HANDLE process = GetCurrentProcess();
|
||||
if (!SymInitialize(process, NULL, TRUE))
|
||||
{
|
||||
Logger::error(L"Failed to initialize symbol handler. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
}
|
||||
|
||||
void InitUnhandledExceptionHandler()
|
||||
{
|
||||
try
|
||||
{
|
||||
InitSymbols();
|
||||
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||
signal(SIGABRT, &AbortHandler);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to init global unhandled exception handler");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,280 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include <DbgHelp.h>
|
||||
#include <signal.h>
|
||||
#include <sstream>
|
||||
#include <stdio.h>
|
||||
|
||||
LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info);
|
||||
void AbortHandler(int signal_number);
|
||||
void InitSymbols();
|
||||
void InitUnhandledExceptionHandler();
|
||||
#include "winapi_error.h"
|
||||
#include "../logger/logger.h"
|
||||
|
||||
static BOOLEAN processingException = FALSE;
|
||||
|
||||
static inline const char* exceptionDescription(const DWORD& code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case EXCEPTION_ACCESS_VIOLATION:
|
||||
return "EXCEPTION_ACCESS_VIOLATION";
|
||||
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
|
||||
return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED";
|
||||
case EXCEPTION_BREAKPOINT:
|
||||
return "EXCEPTION_BREAKPOINT";
|
||||
case EXCEPTION_DATATYPE_MISALIGNMENT:
|
||||
return "EXCEPTION_DATATYPE_MISALIGNMENT";
|
||||
case EXCEPTION_FLT_DENORMAL_OPERAND:
|
||||
return "EXCEPTION_FLT_DENORMAL_OPERAND";
|
||||
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_FLT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_FLT_INEXACT_RESULT:
|
||||
return "EXCEPTION_FLT_INEXACT_RESULT";
|
||||
case EXCEPTION_FLT_INVALID_OPERATION:
|
||||
return "EXCEPTION_FLT_INVALID_OPERATION";
|
||||
case EXCEPTION_FLT_OVERFLOW:
|
||||
return "EXCEPTION_FLT_OVERFLOW";
|
||||
case EXCEPTION_FLT_STACK_CHECK:
|
||||
return "EXCEPTION_FLT_STACK_CHECK";
|
||||
case EXCEPTION_FLT_UNDERFLOW:
|
||||
return "EXCEPTION_FLT_UNDERFLOW";
|
||||
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
||||
return "EXCEPTION_ILLEGAL_INSTRUCTION";
|
||||
case EXCEPTION_IN_PAGE_ERROR:
|
||||
return "EXCEPTION_IN_PAGE_ERROR";
|
||||
case EXCEPTION_INT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_INT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_INT_OVERFLOW:
|
||||
return "EXCEPTION_INT_OVERFLOW";
|
||||
case EXCEPTION_INVALID_DISPOSITION:
|
||||
return "EXCEPTION_INVALID_DISPOSITION";
|
||||
case EXCEPTION_NONCONTINUABLE_EXCEPTION:
|
||||
return "EXCEPTION_NONCONTINUABLE_EXCEPTION";
|
||||
case EXCEPTION_PRIV_INSTRUCTION:
|
||||
return "EXCEPTION_PRIV_INSTRUCTION";
|
||||
case EXCEPTION_SINGLE_STEP:
|
||||
return "EXCEPTION_SINGLE_STEP";
|
||||
case EXCEPTION_STACK_OVERFLOW:
|
||||
return "EXCEPTION_STACK_OVERFLOW";
|
||||
default:
|
||||
return "UNKNOWN EXCEPTION";
|
||||
}
|
||||
}
|
||||
|
||||
/* Returns the index of the last backslash in the file path */
|
||||
inline int GetFilenameStart(wchar_t* path)
|
||||
{
|
||||
int pos = 0;
|
||||
int found = 0;
|
||||
if (path != NULL)
|
||||
{
|
||||
while (path[pos] != L'\0' && pos < MAX_PATH)
|
||||
{
|
||||
if (path[pos] == L'\\')
|
||||
{
|
||||
found = pos + 1;
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
inline std::wstring GetModuleName(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static wchar_t modulePath[MAX_PATH]{};
|
||||
const size_t size = sizeof(modulePath);
|
||||
memset(&modulePath[0], '\0', size);
|
||||
|
||||
DWORD64 moduleBase = SymGetModuleBase64(process, stack.AddrPC.Offset);
|
||||
if (!moduleBase)
|
||||
{
|
||||
Logger::error(L"Failed to get a module. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
if (!GetModuleFileNameW(reinterpret_cast<HINSTANCE>(moduleBase), modulePath, MAX_PATH))
|
||||
{
|
||||
Logger::error(L"Failed to get a module path. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
const int start = GetFilenameStart(modulePath);
|
||||
return std::wstring(modulePath, start);
|
||||
}
|
||||
|
||||
inline std::wstring GetName(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static IMAGEHLP_SYMBOL64* pSymbol = static_cast<IMAGEHLP_SYMBOL64*>(malloc(sizeof(IMAGEHLP_SYMBOL64) + MAX_PATH * sizeof(TCHAR)));
|
||||
if (!pSymbol)
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
memset(pSymbol, '\0', sizeof(*pSymbol) + MAX_PATH);
|
||||
pSymbol->MaxNameLength = MAX_PATH;
|
||||
pSymbol->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL64);
|
||||
|
||||
DWORD64 dw64Displacement = 0;
|
||||
if (!SymGetSymFromAddr64(process, stack.AddrPC.Offset, &dw64Displacement, pSymbol))
|
||||
{
|
||||
Logger::error(L"Failed to get a symbol. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
std::string str = pSymbol->Name;
|
||||
return std::wstring(str.begin(), str.end());
|
||||
}
|
||||
|
||||
inline std::wstring GetLine(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static IMAGEHLP_LINE64 line{};
|
||||
|
||||
memset(&line, '\0', sizeof(IMAGEHLP_LINE64));
|
||||
line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
|
||||
line.LineNumber = 0;
|
||||
|
||||
DWORD dwDisplacement = 0;
|
||||
if (!SymGetLineFromAddr64(process, stack.AddrPC.Offset, &dwDisplacement, &line))
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
std::string fileName(line.FileName);
|
||||
return L"(" + std::wstring(fileName.begin(), fileName.end()) + L":" + std::to_wstring(line.LineNumber) + L")";
|
||||
}
|
||||
|
||||
inline void LogStackTrace()
|
||||
{
|
||||
CONTEXT context;
|
||||
try
|
||||
{
|
||||
RtlCaptureContext(&context);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Failed to capture context. {}", get_last_error_or_default(GetLastError()));
|
||||
return;
|
||||
}
|
||||
|
||||
STACKFRAME64 stack;
|
||||
memset(&stack, 0, sizeof(STACKFRAME64));
|
||||
|
||||
HANDLE process = GetCurrentProcess();
|
||||
HANDLE thread = GetCurrentThread();
|
||||
|
||||
#ifdef _M_ARM64
|
||||
stack.AddrPC.Offset = context.Pc;
|
||||
stack.AddrStack.Offset = context.Sp;
|
||||
stack.AddrFrame.Offset = context.Fp;
|
||||
#else
|
||||
stack.AddrPC.Offset = context.Rip;
|
||||
stack.AddrStack.Offset = context.Rsp;
|
||||
stack.AddrFrame.Offset = context.Rbp;
|
||||
#endif
|
||||
stack.AddrPC.Mode = AddrModeFlat;
|
||||
stack.AddrStack.Mode = AddrModeFlat;
|
||||
stack.AddrFrame.Mode = AddrModeFlat;
|
||||
|
||||
BOOL result = false;
|
||||
std::wstringstream ss;
|
||||
for (;;)
|
||||
{
|
||||
result = StackWalk64(
|
||||
#ifdef _M_ARM64
|
||||
IMAGE_FILE_MACHINE_ARM64,
|
||||
#else
|
||||
IMAGE_FILE_MACHINE_AMD64,
|
||||
#endif
|
||||
process,
|
||||
thread,
|
||||
&stack,
|
||||
&context,
|
||||
NULL,
|
||||
SymFunctionTableAccess64,
|
||||
SymGetModuleBase64,
|
||||
NULL);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ss << GetModuleName(process, stack) << "!" << GetName(process, stack) << GetLine(process, stack) << std::endl;
|
||||
}
|
||||
|
||||
Logger::error(L"STACK TRACE\r\n{}", ss.str());
|
||||
Logger::flush();
|
||||
}
|
||||
|
||||
inline LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info)
|
||||
{
|
||||
if (!processingException)
|
||||
{
|
||||
bool headerLogged = false;
|
||||
try
|
||||
{
|
||||
const char* exDescription = "Exception code not available";
|
||||
processingException = true;
|
||||
if (info != NULL && info->ExceptionRecord != NULL && info->ExceptionRecord->ExceptionCode != NULL)
|
||||
{
|
||||
exDescription = exceptionDescription(info->ExceptionRecord->ExceptionCode);
|
||||
}
|
||||
|
||||
headerLogged = true;
|
||||
Logger::error(exDescription);
|
||||
LogStackTrace();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to log stack trace");
|
||||
Logger::flush();
|
||||
}
|
||||
|
||||
processingException = false;
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
/* Handler to trap abort() calls */
|
||||
inline void AbortHandler(int /*signal_number*/)
|
||||
{
|
||||
Logger::error("--- ABORT");
|
||||
try
|
||||
{
|
||||
LogStackTrace();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to log stack trace on abort");
|
||||
Logger::flush();
|
||||
}
|
||||
}
|
||||
|
||||
inline void InitSymbols()
|
||||
{
|
||||
// Preload symbols so they will be available in case of out-of-memory exception
|
||||
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME);
|
||||
HANDLE process = GetCurrentProcess();
|
||||
if (!SymInitialize(process, NULL, TRUE))
|
||||
{
|
||||
Logger::error(L"Failed to initialize symbol handler. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
}
|
||||
|
||||
inline void InitUnhandledExceptionHandler(void)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitSymbols();
|
||||
// Global handler for unhandled exceptions
|
||||
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||
// Handler for abort()
|
||||
signal(SIGABRT, &AbortHandler);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to init global unhandled exception handler");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "appMutex.h"
|
||||
|
||||
wil::unique_mutex_nothrow createAppMutex(const std::wstring& mutexName)
|
||||
{
|
||||
wil::unique_mutex_nothrow result{ CreateMutexW(nullptr, TRUE, mutexName.c_str()) };
|
||||
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -14,4 +14,9 @@ namespace
|
||||
constexpr inline wchar_t POWERTOYS_BOOTSTRAPPER_MUTEX_NAME[] = L"Local\\PowerToys_Bootstrapper_InstanceMutex";
|
||||
}
|
||||
|
||||
wil::unique_mutex_nothrow createAppMutex(const std::wstring& mutexName);
|
||||
inline wil::unique_mutex_nothrow createAppMutex(const std::wstring& mutexName)
|
||||
{
|
||||
wil::unique_mutex_nothrow result{ CreateMutexW(nullptr, TRUE, mutexName.c_str()) };
|
||||
|
||||
return GetLastError() == ERROR_ALREADY_EXISTS ? wil::unique_mutex_nothrow{} : std::move(result);
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "clean_video_conference.h"
|
||||
|
||||
void clean_video_conference()
|
||||
{
|
||||
// 31AD75E9-8C3A-49C8-B9ED-5880D6B4A764 is the CLSID GUID for the 64 video conference mute driver.
|
||||
// 31AD75E9-8C3A-49C8-B9ED-5880D6B4A732 is the CLSID GUID for the 32 video conference mute driver.
|
||||
// 860BB310-5D01-11D0-BD3B-00A0C911CE86 is the CLSID GUID for CLSID_VideoInputDeviceCategory.
|
||||
|
||||
// Unregister the 64 bit driver CLSID:
|
||||
RegDeleteTreeW(HKEY_CLASSES_ROOT, L"CLSID\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A764}");
|
||||
// Unregister the 64 bit driver CLSID from Video Input Devices:
|
||||
RegDeleteTreeW(HKEY_CLASSES_ROOT, L"CLSID\\{860BB310-5D01-11D0-BD3B-00A0C911CE86}\\Instance\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A764}");
|
||||
// Unregister the 32 bit driver CLSID:
|
||||
RegDeleteTreeW(HKEY_LOCAL_MACHINE, L"Software\\WOW6432Node\\Classes\\CLSID\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A732}");
|
||||
// Unregister the 32 bit driver CLSID from Video Input Devices:
|
||||
RegDeleteTreeW(HKEY_LOCAL_MACHINE, L"Software\\WOW6432Node\\Classes\\CLSID\\{860BB310-5D01-11D0-BD3B-00A0C911CE86}\\Instance\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A732}");
|
||||
}
|
||||
@@ -1,6 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
// Video Conference Mute was a utility we deprecated. However, this required a manual user disable of the module to remove the camera registration, so we include the disable code here to be able to clean up.
|
||||
void clean_video_conference();
|
||||
void clean_video_conference()
|
||||
{
|
||||
// 31AD75E9-8C3A-49C8-B9ED-5880D6B4A764 is the CLSID GUID for the 64 video conference mute driver.
|
||||
// 31AD75E9-8C3A-49C8-B9ED-5880D6B4A732 is the CLSID GUID for the 32 video conference mute driver.
|
||||
// 860BB310-5D01-11D0-BD3B-00A0C911CE86 is the CLSID GUID for CLSID_VideoInputDeviceCategory.
|
||||
|
||||
// Unregister the 64 bit driver CLSID:
|
||||
RegDeleteTreeW(HKEY_CLASSES_ROOT, L"CLSID\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A764}");
|
||||
// Unregister the 64 bit driver CLSID from Video Input Devices:
|
||||
RegDeleteTreeW(HKEY_CLASSES_ROOT, L"CLSID\\{860BB310-5D01-11D0-BD3B-00A0C911CE86}\\Instance\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A764}");
|
||||
// Unregister the 32 bit driver CLSID:
|
||||
RegDeleteTreeW(HKEY_LOCAL_MACHINE, L"Software\\WOW6432Node\\Classes\\CLSID\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A732}");
|
||||
// Unregister the 32 bit driver CLSID from Video Input Devices:
|
||||
RegDeleteTreeW(HKEY_LOCAL_MACHINE, L"Software\\WOW6432Node\\Classes\\CLSID\\{860BB310-5D01-11D0-BD3B-00A0C911CE86}\\Instance\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A732}");
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "color.h"
|
||||
|
||||
bool checkValidRGB(std::wstring_view hex, uint8_t* R, uint8_t* G, uint8_t* B)
|
||||
{
|
||||
if (hex.length() != 7)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
hex = hex.substr(1, 6); // remove #
|
||||
for (const auto& c : hex)
|
||||
{
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (swscanf_s(hex.data(), L"%2hhx%2hhx%2hhx", R, G, B) != 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool checkValidARGB(std::wstring_view hex, uint8_t* A, uint8_t* R, uint8_t* G, uint8_t* B)
|
||||
{
|
||||
if (hex.length() != 9)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
hex = hex.substr(1, 8); // remove #
|
||||
for (const auto& c : hex)
|
||||
{
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (swscanf_s(hex.data(), L"%2hhx%2hhx%2hhx%2hhx", A, R, G, B) != 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,10 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
// helper function to get the RGB from a #FFFFFF string.
|
||||
bool checkValidRGB(std::wstring_view hex, uint8_t* R, uint8_t* G, uint8_t* B);
|
||||
inline bool checkValidRGB(std::wstring_view hex, uint8_t* R, uint8_t* G, uint8_t* B)
|
||||
{
|
||||
if (hex.length() != 7)
|
||||
return false;
|
||||
hex = hex.substr(1, 6); // remove #
|
||||
for (auto& c : hex)
|
||||
{
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (swscanf_s(hex.data(), L"%2hhx%2hhx%2hhx", R, G, B) != 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// helper function to get the ARGB from a #FFFFFFFF string.
|
||||
bool checkValidARGB(std::wstring_view hex, uint8_t* A, uint8_t* R, uint8_t* G, uint8_t* B);
|
||||
inline bool checkValidARGB(std::wstring_view hex, uint8_t* A, uint8_t* R, uint8_t* G, uint8_t* B)
|
||||
{
|
||||
if (hex.length() != 9)
|
||||
return false;
|
||||
hex = hex.substr(1, 8); // remove #
|
||||
for (auto& c : hex)
|
||||
{
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (swscanf_s(hex.data(), L"%2hhx%2hhx%2hhx%2hhx", A, R, G, B) != 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,506 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "elevation.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::wstring GetErrorString(HRESULT handle)
|
||||
{
|
||||
_com_error err(handle);
|
||||
return err.ErrorMessage();
|
||||
}
|
||||
|
||||
bool FindDesktopFolderView(REFIID riid, void** ppv)
|
||||
{
|
||||
CComPtr<IShellWindows> spShellWindows;
|
||||
auto result = spShellWindows.CoCreateInstance(CLSID_ShellWindows);
|
||||
if (result != S_OK || spShellWindows == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to create instance. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComVariant vtLoc(CSIDL_DESKTOP);
|
||||
CComVariant vtEmpty;
|
||||
long lhwnd;
|
||||
CComPtr<IDispatch> spdisp;
|
||||
result = spShellWindows->FindWindowSW(&vtLoc, &vtEmpty, SWC_DESKTOP, &lhwnd, SWFO_NEEDDISPATCH, &spdisp);
|
||||
if (result != S_OK || spdisp == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to find the window. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IShellBrowser> spBrowser;
|
||||
result = CComQIPtr<IServiceProvider>(spdisp)->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&spBrowser));
|
||||
if (result != S_OK || spBrowser == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query service. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IShellView> spView;
|
||||
result = spBrowser->QueryActiveShellView(&spView);
|
||||
if (result != S_OK || spView == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query active shell window. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
result = spView->QueryInterface(riid, ppv);
|
||||
if (result != S_OK || ppv == nullptr || *ppv == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query interface. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GetDesktopAutomationObject(REFIID riid, void** ppv)
|
||||
{
|
||||
CComPtr<IShellView> spsv;
|
||||
|
||||
// Desktop may not be available on startup
|
||||
auto attempts = 5;
|
||||
for (auto i = 1; i <= attempts; i++)
|
||||
{
|
||||
if (FindDesktopFolderView(IID_PPV_ARGS(&spsv)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Logger::warn(L"FindDesktopFolderView() failed attempt {}", i);
|
||||
|
||||
if (i == attempts)
|
||||
{
|
||||
Logger::warn(L"FindDesktopFolderView() max attempts reached");
|
||||
return false;
|
||||
}
|
||||
Sleep(3000);
|
||||
}
|
||||
|
||||
CComPtr<IDispatch> spdispView;
|
||||
auto result = spsv->GetItemObject(SVGIO_BACKGROUND, IID_PPV_ARGS(&spdispView));
|
||||
if (result != S_OK || spdispView == nullptr)
|
||||
{
|
||||
Logger::warn(L"spsv->GetItemObject() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
return SUCCEEDED(spdispView->QueryInterface(riid, ppv));
|
||||
}
|
||||
|
||||
bool ShellExecuteFromExplorer(LPCWSTR file, LPCWSTR parameters, LPCWSTR directory)
|
||||
{
|
||||
CComPtr<IShellFolderViewDual> spFolderView;
|
||||
if (!GetDesktopAutomationObject(IID_PPV_ARGS(&spFolderView)))
|
||||
{
|
||||
Logger::warn(L"GetDesktopAutomationObject() failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IDispatch> spdispShell;
|
||||
auto result = spFolderView->get_Application(&spdispShell);
|
||||
if (result != S_OK || spdispShell == nullptr)
|
||||
{
|
||||
Logger::warn(L"spFolderView->get_Application() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComQIPtr<IShellDispatch2> shell(spdispShell);
|
||||
if (shell == nullptr)
|
||||
{
|
||||
Logger::warn(L"IShellDispatch2 is nullptr");
|
||||
return false;
|
||||
}
|
||||
|
||||
CComVariant args(parameters ? parameters : L"");
|
||||
CComVariant dir(directory ? directory : L"");
|
||||
CComVariant operation(L"open");
|
||||
CComVariant show(SW_SHOWNORMAL);
|
||||
result = shell->ShellExecute(CComBSTR(file), args, dir, operation, show);
|
||||
if (result != S_OK)
|
||||
{
|
||||
Logger::warn(L"IShellDispatch2::ShellExecute() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_process_elevated(const bool use_cached_value)
|
||||
{
|
||||
auto detection_func = []() {
|
||||
HANDLE token = nullptr;
|
||||
bool elevated = false;
|
||||
|
||||
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token))
|
||||
{
|
||||
TOKEN_ELEVATION elevation;
|
||||
DWORD size;
|
||||
if (GetTokenInformation(token, TokenElevation, &elevation, sizeof(elevation), &size))
|
||||
{
|
||||
elevated = (elevation.TokenIsElevated != 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (token)
|
||||
{
|
||||
CloseHandle(token);
|
||||
}
|
||||
|
||||
return elevated;
|
||||
};
|
||||
static const bool cached_value = detection_func();
|
||||
return use_cached_value ? cached_value : detection_func();
|
||||
}
|
||||
|
||||
bool drop_elevated_privileges()
|
||||
{
|
||||
HANDLE token = nullptr;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_DEFAULT | WRITE_OWNER, &token))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PSID medium_sid = NULL;
|
||||
if (!::ConvertStringSidToSid(SDDL_ML_MEDIUM, &medium_sid))
|
||||
{
|
||||
CloseHandle(token);
|
||||
return false;
|
||||
}
|
||||
|
||||
TOKEN_MANDATORY_LABEL tml = {};
|
||||
tml.Label.Attributes = SE_GROUP_INTEGRITY;
|
||||
tml.Label.Sid = medium_sid;
|
||||
DWORD dwLen = sizeof(TOKEN_MANDATORY_LABEL) + ::GetLengthSid(medium_sid);
|
||||
BOOL res = ::SetTokenInformation(token, TokenIntegrityLevel, &tml, dwLen);
|
||||
LocalFree(medium_sid);
|
||||
CloseHandle(token);
|
||||
return res == TRUE;
|
||||
}
|
||||
|
||||
HANDLE run_as_different_user(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir, const bool showWindow)
|
||||
{
|
||||
Logger::info(L"run_as_different_user with params={}", params);
|
||||
SHELLEXECUTEINFOW exec_info = { 0 };
|
||||
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
|
||||
exec_info.lpVerb = L"runAsUser";
|
||||
exec_info.lpFile = file.c_str();
|
||||
exec_info.lpParameters = params.c_str();
|
||||
exec_info.hwnd = 0;
|
||||
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
exec_info.lpDirectory = workingDir;
|
||||
exec_info.hInstApp = 0;
|
||||
exec_info.nShow = showWindow ? SW_SHOWDEFAULT : SW_HIDE; // may have limited effect
|
||||
return ShellExecuteExW(&exec_info) ? exec_info.hProcess : nullptr;
|
||||
}
|
||||
|
||||
HANDLE run_elevated(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir, const bool showWindow)
|
||||
{
|
||||
Logger::info(L"run_elevated with params={}", params);
|
||||
SHELLEXECUTEINFOW exec_info = { 0 };
|
||||
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
|
||||
exec_info.lpVerb = L"runas";
|
||||
exec_info.lpFile = file.c_str();
|
||||
exec_info.lpParameters = params.c_str();
|
||||
exec_info.hwnd = 0;
|
||||
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
exec_info.lpDirectory = workingDir;
|
||||
exec_info.hInstApp = 0;
|
||||
exec_info.nShow = showWindow ? SW_SHOWDEFAULT : SW_HIDE; // may have limited effect
|
||||
BOOL result = ShellExecuteExW(&exec_info);
|
||||
return result ? exec_info.hProcess : nullptr;
|
||||
}
|
||||
|
||||
bool run_non_elevated(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir, const bool showWindow)
|
||||
{
|
||||
Logger::info(L"run_non_elevated with params={}", params);
|
||||
auto executable_args = L"\"" + file + L"\"";
|
||||
if (!params.empty())
|
||||
{
|
||||
executable_args += L" " + params;
|
||||
}
|
||||
|
||||
HWND hwnd = GetShellWindow();
|
||||
if (!hwnd)
|
||||
{
|
||||
if (GetLastError() == ERROR_SUCCESS)
|
||||
{
|
||||
Logger::warn(L"GetShellWindow() returned null. Shell window is not available");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"GetShellWindow() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
DWORD pid;
|
||||
GetWindowThreadProcessId(hwnd, &pid);
|
||||
|
||||
winrt::handle process{ OpenProcess(PROCESS_CREATE_PROCESS, FALSE, pid) };
|
||||
if (!process)
|
||||
{
|
||||
Logger::error(L"OpenProcess() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
SIZE_T size = 0;
|
||||
InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
|
||||
auto pproc_buffer = std::make_unique<char[]>(size);
|
||||
auto pptal = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(pproc_buffer.get());
|
||||
if (!pptal)
|
||||
{
|
||||
Logger::error(L"pptal failed to initialize. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!InitializeProcThreadAttributeList(pptal, 1, 0, &size))
|
||||
{
|
||||
Logger::error(L"InitializeProcThreadAttributeList() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
HANDLE process_handle = process.get();
|
||||
if (!UpdateProcThreadAttribute(pptal,
|
||||
0,
|
||||
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
|
||||
&process_handle,
|
||||
sizeof(process_handle),
|
||||
nullptr,
|
||||
nullptr))
|
||||
{
|
||||
Logger::error(L"UpdateProcThreadAttribute() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
STARTUPINFOEX siex = { 0 };
|
||||
siex.lpAttributeList = pptal;
|
||||
siex.StartupInfo.cb = sizeof(siex);
|
||||
PROCESS_INFORMATION pi = { 0 };
|
||||
auto dwCreationFlags = EXTENDED_STARTUPINFO_PRESENT;
|
||||
|
||||
if (!showWindow)
|
||||
{
|
||||
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
|
||||
siex.StartupInfo.wShowWindow = SW_HIDE;
|
||||
dwCreationFlags = CREATE_NO_WINDOW;
|
||||
}
|
||||
|
||||
std::wstring cmdLine = executable_args;
|
||||
auto succeeded = CreateProcessW(file.c_str(),
|
||||
&cmdLine[0],
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
dwCreationFlags,
|
||||
nullptr,
|
||||
workingDir,
|
||||
&siex.StartupInfo,
|
||||
&pi);
|
||||
|
||||
if (succeeded)
|
||||
{
|
||||
if (returnPid)
|
||||
{
|
||||
*returnPid = pi.dwProcessId;
|
||||
}
|
||||
if (pi.hProcess)
|
||||
{
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
if (pi.hThread)
|
||||
{
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
}
|
||||
|
||||
DeleteProcThreadAttributeList(pptal);
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
bool RunNonElevatedEx(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir)
|
||||
{
|
||||
bool success = false;
|
||||
HRESULT co_init = E_FAIL;
|
||||
try
|
||||
{
|
||||
co_init = CoInitialize(nullptr);
|
||||
success = ShellExecuteFromExplorer(file.c_str(), params.c_str(), working_dir.c_str());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
if (SUCCEEDED(co_init))
|
||||
{
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
std::optional<ProcessInfo> RunNonElevatedFailsafe(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir, DWORD handleAccess)
|
||||
{
|
||||
bool launched = RunNonElevatedEx(file, params, working_dir);
|
||||
if (!launched)
|
||||
{
|
||||
Logger::warn(L"RunNonElevatedEx() failed. Trying fallback");
|
||||
std::wstring action_runner_path = get_module_folderpath() + L"\\PowerToys.ActionRunner.exe";
|
||||
std::wstring newParams = L"-run-non-elevated -target \"" + file + L"\" " + params;
|
||||
launched = run_non_elevated(action_runner_path, newParams, nullptr, working_dir.c_str());
|
||||
if (launched)
|
||||
{
|
||||
Logger::trace(L"Started {}", file);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Failed to start {}", file);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
auto handles = getProcessHandlesByName(std::filesystem::path{ file }.filename().wstring(), PROCESS_QUERY_INFORMATION | SYNCHRONIZE | handleAccess);
|
||||
if (handles.empty())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ProcessInfo result;
|
||||
result.processID = GetProcessId(handles[0].get());
|
||||
result.processHandle = std::move(handles[0]);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool run_same_elevation(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir)
|
||||
{
|
||||
auto executable_args = L"\"" + file + L"\"";
|
||||
if (!params.empty())
|
||||
{
|
||||
executable_args += L" " + params;
|
||||
}
|
||||
|
||||
STARTUPINFO si = { sizeof(STARTUPINFO) };
|
||||
PROCESS_INFORMATION pi = { 0 };
|
||||
|
||||
auto succeeded = CreateProcessW(file.c_str(),
|
||||
&executable_args[0],
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
0,
|
||||
nullptr,
|
||||
workingDir,
|
||||
&si,
|
||||
&pi);
|
||||
|
||||
if (succeeded)
|
||||
{
|
||||
if (pi.hProcess)
|
||||
{
|
||||
if (returnPid)
|
||||
{
|
||||
*returnPid = GetProcessId(pi.hProcess);
|
||||
}
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
if (pi.hThread)
|
||||
{
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
bool check_user_is_admin()
|
||||
{
|
||||
auto freeMemory = [](PSID pSID, PTOKEN_GROUPS pGroupInfo) {
|
||||
if (pSID)
|
||||
{
|
||||
FreeSid(pSID);
|
||||
}
|
||||
if (pGroupInfo)
|
||||
{
|
||||
GlobalFree(pGroupInfo);
|
||||
}
|
||||
};
|
||||
|
||||
HANDLE hToken;
|
||||
DWORD dwSize = 0;
|
||||
PTOKEN_GROUPS pGroupInfo;
|
||||
SID_IDENTIFIER_AUTHORITY SIDAuth = SECURITY_NT_AUTHORITY;
|
||||
PSID pSID = NULL;
|
||||
|
||||
// Open a handle to the access token for the calling process.
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Call GetTokenInformation to get the buffer size.
|
||||
if (!GetTokenInformation(hToken, TokenGroups, NULL, dwSize, &dwSize))
|
||||
{
|
||||
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate the buffer.
|
||||
pGroupInfo = static_cast<PTOKEN_GROUPS>(GlobalAlloc(GPTR, dwSize));
|
||||
|
||||
// Call GetTokenInformation again to get the group information.
|
||||
if (!GetTokenInformation(hToken, TokenGroups, pGroupInfo, dwSize, &dwSize))
|
||||
{
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create a SID for the BUILTIN\\Administrators group.
|
||||
if (!AllocateAndInitializeSid(&SIDAuth, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &pSID))
|
||||
{
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Loop through the group SIDs looking for the administrator SID.
|
||||
for (DWORD i = 0; i < pGroupInfo->GroupCount; ++i)
|
||||
{
|
||||
if (EqualSid(pSID, pGroupInfo->Groups[i].Sid))
|
||||
{
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsProcessOfWindowElevated(HWND window)
|
||||
{
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
if (!pid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
wil::unique_handle hProcess{ OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
FALSE,
|
||||
pid) };
|
||||
|
||||
wil::unique_handle token;
|
||||
|
||||
if (OpenProcessToken(hProcess.get(), TOKEN_QUERY, &token))
|
||||
{
|
||||
TOKEN_ELEVATION elevation;
|
||||
DWORD size;
|
||||
if (GetTokenInformation(token.get(), TokenElevation, &elevation, sizeof(elevation), &size))
|
||||
{
|
||||
return elevation.TokenIsElevated != 0;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
@@ -14,8 +14,6 @@
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
|
||||
@@ -24,23 +22,376 @@
|
||||
#include <common/utils/process_path.h>
|
||||
#include <common/utils/processApi.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
inline std::wstring GetErrorString(HRESULT handle)
|
||||
{
|
||||
_com_error err(handle);
|
||||
return err.ErrorMessage();
|
||||
}
|
||||
|
||||
inline bool FindDesktopFolderView(REFIID riid, void** ppv)
|
||||
{
|
||||
CComPtr<IShellWindows> spShellWindows;
|
||||
auto result = spShellWindows.CoCreateInstance(CLSID_ShellWindows);
|
||||
if (result != S_OK || spShellWindows == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to create instance. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComVariant vtLoc(CSIDL_DESKTOP);
|
||||
CComVariant vtEmpty;
|
||||
long lhwnd;
|
||||
CComPtr<IDispatch> spdisp;
|
||||
result = spShellWindows->FindWindowSW(
|
||||
&vtLoc, &vtEmpty, SWC_DESKTOP, &lhwnd, SWFO_NEEDDISPATCH, &spdisp);
|
||||
|
||||
if (result != S_OK || spdisp == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to find the window. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IShellBrowser> spBrowser;
|
||||
result = CComQIPtr<IServiceProvider>(spdisp)->QueryService(SID_STopLevelBrowser,
|
||||
IID_PPV_ARGS(&spBrowser));
|
||||
if (result != S_OK || spBrowser == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query service. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IShellView> spView;
|
||||
result = spBrowser->QueryActiveShellView(&spView);
|
||||
if (result != S_OK || spView == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query active shell window. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
result = spView->QueryInterface(riid, ppv);
|
||||
if (result != S_OK || ppv == nullptr || *ppv == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query interface. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool GetDesktopAutomationObject(REFIID riid, void** ppv)
|
||||
{
|
||||
CComPtr<IShellView> spsv;
|
||||
|
||||
// Desktop may not be available on startup
|
||||
auto attempts = 5;
|
||||
for (auto i = 1; i <= attempts; i++)
|
||||
{
|
||||
if (FindDesktopFolderView(IID_PPV_ARGS(&spsv)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Logger::warn(L"FindDesktopFolderView() failed attempt {}", i);
|
||||
|
||||
if (i == attempts)
|
||||
{
|
||||
Logger::warn(L"FindDesktopFolderView() max attempts reached");
|
||||
return false;
|
||||
}
|
||||
|
||||
Sleep(3000);
|
||||
}
|
||||
|
||||
CComPtr<IDispatch> spdispView;
|
||||
auto result = spsv->GetItemObject(SVGIO_BACKGROUND, IID_PPV_ARGS(&spdispView));
|
||||
if (result != S_OK)
|
||||
{
|
||||
Logger::warn(L"GetItemObject() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
result = spdispView->QueryInterface(riid, ppv);
|
||||
if (result != S_OK)
|
||||
{
|
||||
Logger::warn(L"QueryInterface() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ShellExecuteFromExplorer(
|
||||
PCWSTR pszFile,
|
||||
PCWSTR pszParameters = nullptr,
|
||||
PCWSTR workingDir = L"")
|
||||
{
|
||||
CComPtr<IShellFolderViewDual> spFolderView;
|
||||
if (!GetDesktopAutomationObject(IID_PPV_ARGS(&spFolderView)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IDispatch> spdispShell;
|
||||
auto result = spFolderView->get_Application(&spdispShell);
|
||||
if (result != S_OK)
|
||||
{
|
||||
Logger::warn(L"get_Application() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComQIPtr<IShellDispatch2>(spdispShell)
|
||||
->ShellExecuteW(CComBSTR(pszFile),
|
||||
CComVariant(pszParameters ? pszParameters : L""),
|
||||
CComVariant(workingDir),
|
||||
CComVariant(L""),
|
||||
CComVariant(SW_SHOWNORMAL));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if the current process is running with elevated privileges
|
||||
bool is_process_elevated(const bool use_cached_value = true);
|
||||
inline bool is_process_elevated(const bool use_cached_value = true)
|
||||
{
|
||||
auto detection_func = []() {
|
||||
HANDLE token = nullptr;
|
||||
bool elevated = false;
|
||||
|
||||
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token))
|
||||
{
|
||||
TOKEN_ELEVATION elevation;
|
||||
DWORD size;
|
||||
if (GetTokenInformation(token, TokenElevation, &elevation, sizeof(elevation), &size))
|
||||
{
|
||||
elevated = (elevation.TokenIsElevated != 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (token)
|
||||
{
|
||||
CloseHandle(token);
|
||||
}
|
||||
|
||||
return elevated;
|
||||
};
|
||||
static const bool cached_value = detection_func();
|
||||
return use_cached_value ? cached_value : detection_func();
|
||||
}
|
||||
|
||||
// Drops the elevated privileges if present
|
||||
bool drop_elevated_privileges();
|
||||
inline bool drop_elevated_privileges()
|
||||
{
|
||||
HANDLE token = nullptr;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_DEFAULT | WRITE_OWNER, &token))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Run command as different user, returns process handle or null on failure
|
||||
HANDLE run_as_different_user(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true);
|
||||
PSID medium_sid = NULL;
|
||||
if (!::ConvertStringSidToSid(SDDL_ML_MEDIUM, &medium_sid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Run command as elevated user, returns process handle or null on failure
|
||||
HANDLE run_elevated(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true);
|
||||
TOKEN_MANDATORY_LABEL label = { 0 };
|
||||
label.Label.Attributes = SE_GROUP_INTEGRITY;
|
||||
label.Label.Sid = medium_sid;
|
||||
DWORD size = static_cast<DWORD>(sizeof(TOKEN_MANDATORY_LABEL) + ::GetLengthSid(medium_sid));
|
||||
|
||||
BOOL result = SetTokenInformation(token, TokenIntegrityLevel, &label, size);
|
||||
LocalFree(medium_sid);
|
||||
CloseHandle(token);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Run command as different user, returns true if succeeded
|
||||
inline HANDLE run_as_different_user(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true)
|
||||
{
|
||||
Logger::info(L"run_elevated with params={}", params);
|
||||
SHELLEXECUTEINFOW exec_info = { 0 };
|
||||
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
|
||||
exec_info.lpVerb = L"runAsUser";
|
||||
exec_info.lpFile = file.c_str();
|
||||
exec_info.lpParameters = params.c_str();
|
||||
exec_info.hwnd = 0;
|
||||
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
exec_info.lpDirectory = workingDir;
|
||||
exec_info.hInstApp = 0;
|
||||
if (showWindow)
|
||||
{
|
||||
exec_info.nShow = SW_SHOWDEFAULT;
|
||||
}
|
||||
else
|
||||
{
|
||||
// might have limited success, but only option with ShellExecuteExW
|
||||
exec_info.nShow = SW_HIDE;
|
||||
}
|
||||
|
||||
return ShellExecuteExW(&exec_info) ? exec_info.hProcess : nullptr;
|
||||
}
|
||||
|
||||
// Run command as elevated user, returns true if succeeded
|
||||
inline HANDLE run_elevated(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true)
|
||||
{
|
||||
Logger::info(L"run_elevated with params={}", params);
|
||||
SHELLEXECUTEINFOW exec_info = { 0 };
|
||||
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
|
||||
exec_info.lpVerb = L"runas";
|
||||
exec_info.lpFile = file.c_str();
|
||||
exec_info.lpParameters = params.c_str();
|
||||
exec_info.hwnd = 0;
|
||||
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
exec_info.lpDirectory = workingDir;
|
||||
exec_info.hInstApp = 0;
|
||||
|
||||
if (showWindow)
|
||||
{
|
||||
exec_info.nShow = SW_SHOWDEFAULT;
|
||||
}
|
||||
else
|
||||
{
|
||||
// might have limited success, but only option with ShellExecuteExW
|
||||
exec_info.nShow = SW_HIDE;
|
||||
}
|
||||
|
||||
BOOL result = ShellExecuteExW(&exec_info);
|
||||
|
||||
return result ? exec_info.hProcess : nullptr;
|
||||
}
|
||||
|
||||
// Run command as non-elevated user, returns true if succeeded, puts the process id into returnPid if returnPid != NULL
|
||||
bool run_non_elevated(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr, const bool showWindow = true);
|
||||
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;
|
||||
}
|
||||
|
||||
// Try running through the shell's automation object
|
||||
bool RunNonElevatedEx(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir);
|
||||
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;
|
||||
}
|
||||
|
||||
struct ProcessInfo
|
||||
{
|
||||
@@ -48,16 +399,172 @@ struct ProcessInfo
|
||||
DWORD processID = {};
|
||||
};
|
||||
|
||||
// Fallback to ActionRunner when shell route fails and return process info if available
|
||||
std::optional<ProcessInfo> RunNonElevatedFailsafe(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir, DWORD handleAccess = 0);
|
||||
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;
|
||||
}
|
||||
|
||||
// Run command with the same elevation, returns true if succeeded
|
||||
bool run_same_elevation(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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.
|
||||
bool check_user_is_admin();
|
||||
inline bool check_user_is_admin()
|
||||
{
|
||||
auto freeMemory = [](PSID pSID, PTOKEN_GROUPS pGroupInfo) {
|
||||
if (pSID)
|
||||
{
|
||||
FreeSid(pSID);
|
||||
}
|
||||
if (pGroupInfo)
|
||||
{
|
||||
GlobalFree(pGroupInfo);
|
||||
}
|
||||
};
|
||||
|
||||
bool IsProcessOfWindowElevated(HWND window);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "excluded_apps.h"
|
||||
|
||||
bool find_app_name_in_path(const std::wstring& where, const std::vector<std::wstring>& what)
|
||||
{
|
||||
for (const auto& row : what)
|
||||
{
|
||||
const auto pos = where.rfind(row);
|
||||
const auto last_slash = where.rfind('\\');
|
||||
// Check that row occurs in where, and its last occurrence contains the first character after the last backslash.
|
||||
if (pos != std::wstring::npos && pos <= last_slash + 1 && pos + row.length() > last_slash)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool find_folder_in_path(const std::wstring& where, const std::vector<std::wstring>& what)
|
||||
{
|
||||
for (const auto& row : what)
|
||||
{
|
||||
if (where.rfind(row) != std::wstring::npos)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool check_excluded_app_with_title(const HWND& hwnd, const std::vector<std::wstring>& excludedApps)
|
||||
{
|
||||
WCHAR title[MAX_TITLE_LENGTH];
|
||||
const int len = GetWindowTextW(hwnd, title, MAX_TITLE_LENGTH);
|
||||
if (len <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring titleStr(title);
|
||||
CharUpperBuffW(titleStr.data(), static_cast<DWORD>(titleStr.length()));
|
||||
|
||||
for (const auto& app : excludedApps)
|
||||
{
|
||||
if (titleStr.contains(app))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool check_excluded_app(const HWND& hwnd, const std::wstring& processPath, const std::vector<std::wstring>& excludedApps)
|
||||
{
|
||||
bool res = find_app_name_in_path(processPath, excludedApps);
|
||||
|
||||
if (!res)
|
||||
{
|
||||
res = check_excluded_app_with_title(hwnd, excludedApps);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -1,15 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <windows.h>
|
||||
|
||||
// Checks if a process path is included in a list of strings.
|
||||
bool find_app_name_in_path(const std::wstring& where, const std::vector<std::wstring>& what);
|
||||
inline bool find_app_name_in_path(const std::wstring& where, const std::vector<std::wstring>& what)
|
||||
{
|
||||
for (const auto& row : what)
|
||||
{
|
||||
const auto pos = where.rfind(row);
|
||||
const auto last_slash = where.rfind('\\');
|
||||
//Check that row occurs in where, and its last occurrence contains in itself the first character after the last backslash.
|
||||
if (pos != std::wstring::npos && pos <= last_slash + 1 && pos + row.length() > last_slash)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool find_folder_in_path(const std::wstring& where, const std::vector<std::wstring>& what);
|
||||
inline bool find_folder_in_path(const std::wstring& where, const std::vector<std::wstring>& what)
|
||||
{
|
||||
for (const auto& row : what)
|
||||
{
|
||||
const auto pos = where.rfind(row);
|
||||
if (pos != std::wstring::npos)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#define MAX_TITLE_LENGTH 255
|
||||
bool check_excluded_app_with_title(const HWND& hwnd, const std::vector<std::wstring>& excludedApps);
|
||||
inline bool check_excluded_app_with_title(const HWND& hwnd, const std::vector<std::wstring>& excludedApps)
|
||||
{
|
||||
WCHAR title[MAX_TITLE_LENGTH];
|
||||
int len = GetWindowTextW(hwnd, title, MAX_TITLE_LENGTH);
|
||||
if (len <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool check_excluded_app(const HWND& hwnd, const std::wstring& processPath, const std::vector<std::wstring>& excludedApps);
|
||||
std::wstring titleStr(title);
|
||||
CharUpperBuffW(titleStr.data(), static_cast<DWORD>(titleStr.length()));
|
||||
|
||||
for (const auto& app : excludedApps)
|
||||
{
|
||||
if (titleStr.contains(app))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool check_excluded_app(const HWND& hwnd, const std::wstring& processPath, const std::vector<std::wstring>& excludedApps)
|
||||
{
|
||||
bool res = find_app_name_in_path(processPath, excludedApps);
|
||||
|
||||
if (!res)
|
||||
{
|
||||
res = check_excluded_app_with_title(hwnd, excludedApps);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "exec.h"
|
||||
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
|
||||
std::optional<std::string> exec_and_read_output(const std::wstring_view command, DWORD timeout_ms)
|
||||
{
|
||||
SECURITY_ATTRIBUTES saAttr{ sizeof(saAttr) };
|
||||
saAttr.bInheritHandle = false;
|
||||
|
||||
constexpr size_t bufferSize = 4096;
|
||||
// We must use a named pipe for async I/O
|
||||
char pipename[MAX_PATH + 1];
|
||||
if (!GetTempFileNameA(R"(\\.\pipe\)", "tmp", 1, pipename))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
wil::unique_handle readPipe{
|
||||
CreateNamedPipeA(pipename, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, PIPE_UNLIMITED_INSTANCES, bufferSize, bufferSize, 0, &saAttr)
|
||||
};
|
||||
|
||||
saAttr.bInheritHandle = true;
|
||||
wil::unique_handle writePipe{
|
||||
CreateFileA(pipename, GENERIC_WRITE, 0, &saAttr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)
|
||||
};
|
||||
|
||||
if (!readPipe || !writePipe)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
PROCESS_INFORMATION piProcInfo{};
|
||||
STARTUPINFOW siStartInfo{ sizeof(siStartInfo) };
|
||||
|
||||
siStartInfo.hStdError = writePipe.get();
|
||||
siStartInfo.hStdOutput = writePipe.get();
|
||||
siStartInfo.dwFlags |= STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
|
||||
siStartInfo.wShowWindow = SW_HIDE;
|
||||
|
||||
std::wstring cmdLine{ command };
|
||||
if (!CreateProcessW(nullptr,
|
||||
cmdLine.data(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
true,
|
||||
NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE,
|
||||
nullptr,
|
||||
nullptr,
|
||||
&siStartInfo,
|
||||
&piProcInfo))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
// Child process inherited the write end of the pipe, we can close it now
|
||||
writePipe.reset();
|
||||
|
||||
auto closeProcessHandles = wil::scope_exit([&] {
|
||||
CloseHandle(piProcInfo.hThread);
|
||||
CloseHandle(piProcInfo.hProcess);
|
||||
});
|
||||
|
||||
std::string childOutput;
|
||||
bool processExited = false;
|
||||
for (;;)
|
||||
{
|
||||
char buffer[bufferSize];
|
||||
DWORD gotBytes = 0;
|
||||
wil::unique_handle ioEvent{ CreateEventW(nullptr, true, false, nullptr) };
|
||||
OVERLAPPED overlapped{ .hEvent = ioEvent.get() };
|
||||
ReadFile(readPipe.get(), buffer, sizeof(buffer), nullptr, &overlapped);
|
||||
|
||||
const std::array<HANDLE, 2> handlesToWait = { overlapped.hEvent, piProcInfo.hProcess };
|
||||
switch (WaitForMultipleObjects(1 + !processExited, handlesToWait.data(), false, timeout_ms))
|
||||
{
|
||||
case WAIT_OBJECT_0 + 1:
|
||||
if (!processExited)
|
||||
{
|
||||
// When the process exits, we can reduce timeout and read the rest of the output w/o possibly big timeout
|
||||
timeout_ms = 1000;
|
||||
processExited = true;
|
||||
closeProcessHandles.reset();
|
||||
}
|
||||
[[fallthrough]];
|
||||
case WAIT_OBJECT_0:
|
||||
if (GetOverlappedResultEx(readPipe.get(), &overlapped, &gotBytes, timeout_ms, true))
|
||||
{
|
||||
childOutput += std::string_view{ buffer, gotBytes };
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
default:
|
||||
goto exit;
|
||||
}
|
||||
}
|
||||
exit:
|
||||
CancelIo(readPipe.get());
|
||||
return childOutput;
|
||||
}
|
||||
@@ -15,4 +15,94 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
std::optional<std::string> exec_and_read_output(const std::wstring_view command, DWORD timeout_ms = 30000);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "game_mode.h"
|
||||
|
||||
bool detect_game_mode()
|
||||
{
|
||||
QUERY_USER_NOTIFICATION_STATE notification_state;
|
||||
if (SHQueryUserNotificationState(¬ification_state) != S_OK)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return notification_state == QUNS_RUNNING_D3D_FULL_SCREEN;
|
||||
}
|
||||
@@ -1,4 +1,12 @@
|
||||
#pragma once
|
||||
#include <shellapi.h>
|
||||
|
||||
bool detect_game_mode();
|
||||
inline bool detect_game_mode()
|
||||
{
|
||||
QUERY_USER_NOTIFICATION_STATE notification_state;
|
||||
if (SHQueryUserNotificationState(¬ification_state) != S_OK)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return (notification_state == QUNS_RUNNING_D3D_FULL_SCREEN);
|
||||
}
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "gpo.h"
|
||||
|
||||
namespace powertoys_gpo
|
||||
{
|
||||
std::optional<std::wstring> readRegistryStringValue(HKEY hRootKey, const std::wstring& subKey, const std::wstring& value_name, const bool is_multi_line_text)
|
||||
{
|
||||
// Set value type
|
||||
DWORD reg_value_type = REG_SZ;
|
||||
DWORD reg_flags = RRF_RT_REG_SZ;
|
||||
if (is_multi_line_text)
|
||||
{
|
||||
reg_value_type = REG_MULTI_SZ;
|
||||
reg_flags = RRF_RT_REG_MULTI_SZ;
|
||||
}
|
||||
|
||||
DWORD string_buffer_capacity;
|
||||
// Request required buffer capacity / string length
|
||||
if (RegGetValueW(hRootKey, subKey.c_str(), value_name.c_str(), reg_flags, ®_value_type, NULL, &string_buffer_capacity) != ERROR_SUCCESS)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
else if (string_buffer_capacity == 0)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// RegGetValueW overshoots sometimes. Use a buffer first to not have characters past the string end.
|
||||
wchar_t* temp_buffer = new wchar_t[string_buffer_capacity / sizeof(wchar_t) + 1];
|
||||
// Read string
|
||||
if (RegGetValueW(hRootKey, subKey.c_str(), value_name.c_str(), reg_flags, ®_value_type, temp_buffer, &string_buffer_capacity) != ERROR_SUCCESS)
|
||||
{
|
||||
delete[] temp_buffer;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Convert buffer to std::wstring
|
||||
std::wstring string_value = L"";
|
||||
if (reg_value_type == REG_MULTI_SZ)
|
||||
{
|
||||
// If it is REG_MULTI_SZ handle this way
|
||||
wchar_t* currentString = temp_buffer;
|
||||
while (*currentString != L'\0')
|
||||
{
|
||||
// If first entry then assign the string, else add to the string
|
||||
string_value = (string_value == L"") ? currentString : (string_value + L"\r\n" + currentString);
|
||||
currentString += wcslen(currentString) + 1; // Move to the next string
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If it is REG_SZ handle this way
|
||||
string_value = temp_buffer;
|
||||
}
|
||||
|
||||
// delete buffer, return string value
|
||||
delete[] temp_buffer;
|
||||
return string_value;
|
||||
}
|
||||
|
||||
gpo_rule_configured_t getConfiguredValue(const std::wstring& registry_value_name)
|
||||
{
|
||||
HKEY key{};
|
||||
DWORD value = 0xFFFFFFFE;
|
||||
DWORD valueSize = sizeof(value);
|
||||
|
||||
bool machine_key_found = true;
|
||||
if (auto res = RegOpenKeyExW(POLICIES_SCOPE_MACHINE, POLICIES_PATH.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
machine_key_found = false;
|
||||
}
|
||||
|
||||
if (machine_key_found)
|
||||
{
|
||||
// If the path was found in the machine, we need to check if the value for the policy exists.
|
||||
auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &valueSize);
|
||||
|
||||
RegCloseKey(key);
|
||||
|
||||
if (res != ERROR_SUCCESS)
|
||||
{
|
||||
// Value not found on the path.
|
||||
machine_key_found = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!machine_key_found)
|
||||
{
|
||||
// If there's no value found on the machine scope, try to get it from the user scope.
|
||||
if (auto res = RegOpenKeyExW(POLICIES_SCOPE_USER, POLICIES_PATH.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
if (res == ERROR_FILE_NOT_FOUND)
|
||||
{
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
return gpo_rule_configured_unavailable;
|
||||
}
|
||||
auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &valueSize);
|
||||
RegCloseKey(key);
|
||||
|
||||
if (res != ERROR_SUCCESS)
|
||||
{
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case 0:
|
||||
return gpo_rule_configured_disabled;
|
||||
case 1:
|
||||
return gpo_rule_configured_enabled;
|
||||
default:
|
||||
return gpo_rule_configured_wrong_value;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::wstring> getPolicyListValue(const std::wstring& registry_list_path, const std::wstring& registry_list_value_name)
|
||||
{
|
||||
// This function returns the value of an entry of a policy list. The user scope is only checked, if the list is not enabled for the machine to not mix the lists.
|
||||
|
||||
HKEY key{};
|
||||
|
||||
// Try to read from the machine list.
|
||||
bool machine_list_found = false;
|
||||
if (RegOpenKeyExW(POLICIES_SCOPE_MACHINE, registry_list_path.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
machine_list_found = true;
|
||||
RegCloseKey(key);
|
||||
|
||||
// If the path exists in the machine registry, we try to read the value.
|
||||
auto regValueData = readRegistryStringValue(POLICIES_SCOPE_MACHINE, registry_list_path, registry_list_value_name);
|
||||
|
||||
if (regValueData.has_value())
|
||||
{
|
||||
// Return the value from the machine list.
|
||||
return *regValueData;
|
||||
}
|
||||
}
|
||||
|
||||
// If no list exists for machine, we try to read from the user list.
|
||||
if (!machine_list_found)
|
||||
{
|
||||
if (RegOpenKeyExW(POLICIES_SCOPE_USER, registry_list_path.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
RegCloseKey(key);
|
||||
|
||||
// If the path exists in the user registry, we try to read the value.
|
||||
auto regValueData = readRegistryStringValue(POLICIES_SCOPE_USER, registry_list_path, registry_list_value_name);
|
||||
|
||||
if (regValueData.has_value())
|
||||
{
|
||||
// Return the value from the user list.
|
||||
return *regValueData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No list exists for machine and user, or no value was found in the list, or an error ocurred while reading the value.
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
gpo_rule_configured_t getUtilityEnabledValue(const std::wstring& utility_name)
|
||||
{
|
||||
auto individual_value = getConfiguredValue(utility_name);
|
||||
|
||||
if (individual_value == gpo_rule_configured_disabled || individual_value == gpo_rule_configured_enabled)
|
||||
{
|
||||
return individual_value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return getConfiguredValue(POLICY_CONFIGURE_ENABLED_GLOBAL_ALL_UTILITIES);
|
||||
}
|
||||
}
|
||||
|
||||
gpo_rule_configured_t getRunPluginEnabledValue(std::string pluginID)
|
||||
{
|
||||
if (pluginID == "" || pluginID == " ")
|
||||
{
|
||||
// this plugin id can't exist in the registry
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
|
||||
std::wstring plugin_id(pluginID.begin(), pluginID.end());
|
||||
auto individual_plugin_setting = getPolicyListValue(POWER_LAUNCHER_INDIVIDUAL_PLUGIN_ENABLED_LIST_PATH, plugin_id);
|
||||
|
||||
if (individual_plugin_setting.has_value())
|
||||
{
|
||||
if (*individual_plugin_setting == L"0")
|
||||
{
|
||||
// force disabled
|
||||
return gpo_rule_configured_disabled;
|
||||
}
|
||||
else if (*individual_plugin_setting == L"1")
|
||||
{
|
||||
// force enabled
|
||||
return gpo_rule_configured_enabled;
|
||||
}
|
||||
else if (*individual_plugin_setting == L"2")
|
||||
{
|
||||
// user takes control
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
else
|
||||
{
|
||||
return gpo_rule_configured_wrong_value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no individual plugin policy exists, we check the policy with the setting for all plugins.
|
||||
return getConfiguredValue(POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER_ALL_PLUGINS);
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring getConfiguredMwbPolicyDefinedIpMappingRules()
|
||||
{
|
||||
// Important: HKLM has priority over HKCU
|
||||
auto mapping_rules = readRegistryStringValue(HKEY_LOCAL_MACHINE, POLICIES_PATH, POLICY_MWB_POLICY_DEFINED_IP_MAPPING_RULES, true);
|
||||
if (!mapping_rules.has_value())
|
||||
{
|
||||
mapping_rules = readRegistryStringValue(HKEY_CURRENT_USER, POLICIES_PATH, POLICY_MWB_POLICY_DEFINED_IP_MAPPING_RULES, true);
|
||||
}
|
||||
|
||||
// return value
|
||||
if (mapping_rules.has_value())
|
||||
{
|
||||
return mapping_rules.value();
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,10 +105,176 @@ namespace powertoys_gpo
|
||||
|
||||
// Methods used for reading the registry
|
||||
#pragma region ReadRegistryMethods
|
||||
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);
|
||||
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
|
||||
|
||||
// Utility enabled state policies
|
||||
@@ -378,7 +544,45 @@ namespace powertoys_gpo
|
||||
return getConfiguredValue(POLICY_CONFIGURE_RUN_AT_STARTUP);
|
||||
}
|
||||
|
||||
gpo_rule_configured_t getRunPluginEnabledValue(std::string pluginID);
|
||||
inline gpo_rule_configured_t getRunPluginEnabledValue(std::string pluginID)
|
||||
{
|
||||
if (pluginID == "" || pluginID == " ")
|
||||
{
|
||||
// this plugin id can't exist in the registry
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
|
||||
std::wstring plugin_id(pluginID.begin(), pluginID.end());
|
||||
auto individual_plugin_setting = getPolicyListValue(POWER_LAUNCHER_INDIVIDUAL_PLUGIN_ENABLED_LIST_PATH, plugin_id);
|
||||
|
||||
if (individual_plugin_setting.has_value())
|
||||
{
|
||||
if (*individual_plugin_setting == L"0")
|
||||
{
|
||||
// force disabled
|
||||
return gpo_rule_configured_disabled;
|
||||
}
|
||||
else if (*individual_plugin_setting == L"1")
|
||||
{
|
||||
// force enabled
|
||||
return gpo_rule_configured_enabled;
|
||||
}
|
||||
else if (*individual_plugin_setting == L"2")
|
||||
{
|
||||
// user takes control
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
else
|
||||
{
|
||||
return gpo_rule_configured_wrong_value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no individual plugin policy exists, we check the policy with the setting for all plugins.
|
||||
return getConfiguredValue(POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER_ALL_PLUGINS);
|
||||
}
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getAllowedAdvancedPasteOnlineAIModelsValue()
|
||||
{
|
||||
@@ -460,7 +664,25 @@ namespace powertoys_gpo
|
||||
return getConfiguredValue(POLICY_MWB_DISABLE_USER_DEFINED_IP_MAPPING_RULES);
|
||||
}
|
||||
|
||||
std::wstring getConfiguredMwbPolicyDefinedIpMappingRules();
|
||||
inline std::wstring getConfiguredMwbPolicyDefinedIpMappingRules()
|
||||
{
|
||||
// Important: HKLM has priority over HKCU
|
||||
auto mapping_rules = readRegistryStringValue(HKEY_LOCAL_MACHINE, POLICIES_PATH, POLICY_MWB_POLICY_DEFINED_IP_MAPPING_RULES, true);
|
||||
if (!mapping_rules.has_value())
|
||||
{
|
||||
mapping_rules = readRegistryStringValue(HKEY_CURRENT_USER, POLICIES_PATH, POLICY_MWB_POLICY_DEFINED_IP_MAPPING_RULES, true);
|
||||
}
|
||||
|
||||
// return value
|
||||
if (mapping_rules.has_value())
|
||||
{
|
||||
return mapping_rules.value();
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getConfiguredNewPlusHideTemplateFilenameExtensionValue()
|
||||
{
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "json.h"
|
||||
|
||||
namespace json
|
||||
{
|
||||
std::optional<JsonObject> from_file(std::wstring_view file_name)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::ifstream file(file_name.data(), std::ios::binary);
|
||||
if (file.is_open())
|
||||
{
|
||||
using iterator = std::istreambuf_iterator<char>;
|
||||
std::string objStr{ iterator{ file }, iterator{} };
|
||||
return JsonValue::Parse(winrt::to_hstring(objStr)).GetObjectW();
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
void to_file(std::wstring_view file_name, const JsonObject& obj)
|
||||
{
|
||||
std::wstring objStr{ obj.Stringify().c_str() };
|
||||
std::ofstream{ file_name.data(), std::ios::binary } << winrt::to_string(objStr);
|
||||
}
|
||||
|
||||
bool has(const json::JsonObject& o, std::wstring_view name, const json::JsonValueType type)
|
||||
{
|
||||
return o.HasKey(name) && o.GetNamedValue(name).ValueType() == type;
|
||||
}
|
||||
|
||||
JsonValue value(const bool boolean)
|
||||
{
|
||||
return json::JsonValue::CreateBooleanValue(boolean);
|
||||
}
|
||||
|
||||
JsonValue value(JsonObject valueObject)
|
||||
{
|
||||
return valueObject.as<JsonValue>();
|
||||
}
|
||||
|
||||
JsonValue value(JsonValue valueObject)
|
||||
{
|
||||
return valueObject; // identity function overload for convenience
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,38 @@ namespace json
|
||||
{
|
||||
using namespace winrt::Windows::Data::Json;
|
||||
|
||||
std::optional<JsonObject> from_file(std::wstring_view file_name);
|
||||
inline std::optional<JsonObject> from_file(std::wstring_view file_name)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::ifstream file(file_name.data(), std::ios::binary);
|
||||
if (file.is_open())
|
||||
{
|
||||
using isbi = std::istreambuf_iterator<char>;
|
||||
std::string obj_str{ isbi{ file }, isbi{} };
|
||||
return JsonValue::Parse(winrt::to_hstring(obj_str)).GetObjectW();
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
void to_file(std::wstring_view file_name, const JsonObject& obj);
|
||||
inline void to_file(std::wstring_view file_name, const JsonObject& obj)
|
||||
{
|
||||
std::wstring obj_str{ obj.Stringify().c_str() };
|
||||
std::ofstream{ file_name.data(), std::ios::binary } << winrt::to_string(obj_str);
|
||||
}
|
||||
|
||||
bool has(const json::JsonObject& o, std::wstring_view name, const json::JsonValueType type = JsonValueType::Object);
|
||||
inline bool has(
|
||||
const json::JsonObject& o,
|
||||
std::wstring_view name,
|
||||
const json::JsonValueType type = JsonValueType::Object)
|
||||
{
|
||||
return o.HasKey(name) && o.GetNamedValue(name).ValueType() == type;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline std::enable_if_t<std::is_arithmetic_v<T>, JsonValue> value(const T arithmetic)
|
||||
@@ -29,11 +56,20 @@ namespace json
|
||||
return json::JsonValue::CreateStringValue(s);
|
||||
}
|
||||
|
||||
JsonValue value(const bool boolean);
|
||||
inline JsonValue value(const bool boolean)
|
||||
{
|
||||
return json::JsonValue::CreateBooleanValue(boolean);
|
||||
}
|
||||
|
||||
JsonValue value(JsonObject value);
|
||||
inline JsonValue value(JsonObject value)
|
||||
{
|
||||
return value.as<JsonValue>();
|
||||
}
|
||||
|
||||
JsonValue value(JsonValue value);
|
||||
inline JsonValue value(JsonValue value)
|
||||
{
|
||||
return value; // identity function overload for convenience
|
||||
}
|
||||
|
||||
template<typename T, typename D = std::optional<T>>
|
||||
requires std::constructible_from<std::optional<T>, D>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "language_helper.h"
|
||||
|
||||
namespace LanguageHelpers
|
||||
{
|
||||
std::wstring load_language()
|
||||
{
|
||||
std::filesystem::path languageJsonFilePath(PTSettingsHelper::get_root_save_folder_location() + L"\\language.json");
|
||||
|
||||
auto langJson = json::from_file(languageJsonFilePath.c_str());
|
||||
if (!langJson.has_value())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring language = langJson->GetNamedString(L"language", L"").c_str();
|
||||
return language;
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,17 @@
|
||||
|
||||
namespace LanguageHelpers
|
||||
{
|
||||
std::wstring load_language();
|
||||
inline std::wstring load_language()
|
||||
{
|
||||
std::filesystem::path languageJsonFilePath(PTSettingsHelper::get_root_save_folder_location() + L"\\language.json");
|
||||
|
||||
auto langJson = json::from_file(languageJsonFilePath.c_str());
|
||||
if (!langJson.has_value())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring language = langJson->GetNamedString(L"language", L"").c_str();
|
||||
return language;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user