mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-01 09:56:32 +01:00
Compare commits
40 Commits
shawn/fixI
...
dev/vanzue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c897105b42 | ||
|
|
29858c6782 | ||
|
|
aa0dd19b64 | ||
|
|
39afe4f196 | ||
|
|
5b6645ac27 | ||
|
|
c67f5d52f1 | ||
|
|
ce7cfee3f3 | ||
|
|
b354f56a72 | ||
|
|
737b092022 | ||
|
|
8c5fb85abb | ||
|
|
098244d7f2 | ||
|
|
8b94d8c233 | ||
|
|
9f1caa5ff2 | ||
|
|
fdc61e5dff | ||
|
|
dbc30045b5 | ||
|
|
a7587a9af1 | ||
|
|
fe236fb20c | ||
|
|
78df0c6b9c | ||
|
|
5fb5c91703 | ||
|
|
e120eca6c9 | ||
|
|
591371bbba | ||
|
|
0bce23fff0 | ||
|
|
4bdb9de7d6 | ||
|
|
75354e676e | ||
|
|
95294419ba | ||
|
|
a3a07e3a0e | ||
|
|
ac38d9abaf | ||
|
|
210124510a | ||
|
|
7f055d11b5 | ||
|
|
f891ca2f27 | ||
|
|
f6b97cc16d | ||
|
|
56611d07ff | ||
|
|
278667834f | ||
|
|
bdf3bff18d | ||
|
|
a100d9b352 | ||
|
|
7f5b5d57ad | ||
|
|
63b6ccc2c5 | ||
|
|
c005996b0c | ||
|
|
0303d59d86 | ||
|
|
dee6a62a68 |
3
.github/actions/spell-check/allow/code.txt
vendored
3
.github/actions/spell-check/allow/code.txt
vendored
@@ -330,9 +330,6 @@ HHH
|
||||
riday
|
||||
YYY
|
||||
|
||||
# Unicode
|
||||
precomposed
|
||||
|
||||
# GitHub issue/PR commands
|
||||
azp
|
||||
feedbackhub
|
||||
|
||||
29
.github/actions/spell-check/expect.txt
vendored
29
.github/actions/spell-check/expect.txt
vendored
@@ -216,7 +216,6 @@ CImage
|
||||
cla
|
||||
CLASSDC
|
||||
CLASSNOTAVAILABLE
|
||||
CLEARTYPE
|
||||
clickable
|
||||
clickonce
|
||||
CLIENTEDGE
|
||||
@@ -254,7 +253,6 @@ colorhistory
|
||||
colorhistorylimit
|
||||
COLORKEY
|
||||
colorref
|
||||
Convs
|
||||
comctl
|
||||
comdlg
|
||||
comexp
|
||||
@@ -393,7 +391,6 @@ devpal
|
||||
dfx
|
||||
DIALOGEX
|
||||
digicert
|
||||
diffs
|
||||
DINORMAL
|
||||
DISABLEASACTIONKEY
|
||||
DISABLENOSCROLL
|
||||
@@ -531,12 +528,9 @@ eyetracker
|
||||
FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
FARPROC
|
||||
fdw
|
||||
fdx
|
||||
FErase
|
||||
fesf
|
||||
FFFF
|
||||
FInc
|
||||
Figma
|
||||
FILEEXPLORER
|
||||
fileexploreraddons
|
||||
@@ -578,7 +572,6 @@ formatetc
|
||||
FORPARSING
|
||||
foundrylocal
|
||||
FRAMECHANGED
|
||||
FRestore
|
||||
frm
|
||||
FROMTOUCH
|
||||
fsanitize
|
||||
@@ -613,7 +606,6 @@ GETSCREENSAVERRUNNING
|
||||
GETSECKEY
|
||||
GETSTICKYKEYS
|
||||
GETTEXTLENGTH
|
||||
gfx
|
||||
GHND
|
||||
gitmodules
|
||||
GMEM
|
||||
@@ -664,7 +656,6 @@ hdwwiz
|
||||
Helpline
|
||||
helptext
|
||||
HGFE
|
||||
hgdiobj
|
||||
hglobal
|
||||
hhk
|
||||
HHmmssfff
|
||||
@@ -712,7 +703,7 @@ hotlight
|
||||
hotspot
|
||||
HPAINTBUFFER
|
||||
HRAWINPUT
|
||||
hredraw
|
||||
HREDRAW
|
||||
hres
|
||||
hresult
|
||||
hrgn
|
||||
@@ -890,7 +881,7 @@ LINKOVERLAY
|
||||
LINQTo
|
||||
listview
|
||||
LIVEDRAW
|
||||
livezoom
|
||||
LIVEZOOM
|
||||
LLKH
|
||||
llkhf
|
||||
LMEM
|
||||
@@ -919,7 +910,6 @@ LPBITMAPINFOHEADER
|
||||
LPCFHOOKPROC
|
||||
LPCITEMIDLIST
|
||||
LPCLSID
|
||||
lpch
|
||||
lpcmi
|
||||
LPCMINVOKECOMMANDINFO
|
||||
LPCREATESTRUCT
|
||||
@@ -944,7 +934,6 @@ lptpm
|
||||
LPTR
|
||||
LPTSTR
|
||||
lpv
|
||||
LPrivate
|
||||
LPW
|
||||
lpwcx
|
||||
lpwndpl
|
||||
@@ -993,7 +982,6 @@ mdtext
|
||||
mdtxt
|
||||
mdwn
|
||||
meme
|
||||
mcp
|
||||
memicmp
|
||||
MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
@@ -1084,8 +1072,6 @@ muxxc
|
||||
muxxh
|
||||
MVPs
|
||||
mvvm
|
||||
myorg
|
||||
myrepo
|
||||
MVVMTK
|
||||
MWBEx
|
||||
MYICON
|
||||
@@ -1232,7 +1218,6 @@ opensource
|
||||
openxmlformats
|
||||
ollama
|
||||
onnx
|
||||
openurl
|
||||
OPTIMIZEFORINVOKE
|
||||
ORPHANEDDIALOGTITLE
|
||||
ORSCANS
|
||||
@@ -1310,7 +1295,6 @@ phwnd
|
||||
pici
|
||||
pidl
|
||||
PIDLIST
|
||||
PII
|
||||
pinfo
|
||||
pinvoke
|
||||
pipename
|
||||
@@ -1359,7 +1343,6 @@ ppv
|
||||
ppwsz
|
||||
prc
|
||||
Prefixer
|
||||
Premul
|
||||
prependpath
|
||||
prepopulate
|
||||
prevhost
|
||||
@@ -1509,7 +1492,6 @@ riid
|
||||
RKey
|
||||
RNumber
|
||||
rop
|
||||
rollups
|
||||
ROUNDSMALL
|
||||
ROWSETEXT
|
||||
rpcrt
|
||||
@@ -1654,7 +1636,6 @@ SKIPOWNPROCESS
|
||||
sku
|
||||
SLGP
|
||||
sln
|
||||
slnf
|
||||
slnx
|
||||
SMALLICON
|
||||
smartphone
|
||||
@@ -1820,6 +1801,7 @@ tlb
|
||||
tlbimp
|
||||
tlc
|
||||
tmain
|
||||
tml
|
||||
TNP
|
||||
Toolhelp
|
||||
toolwindow
|
||||
@@ -1915,7 +1897,7 @@ valuegenerator
|
||||
variantassignment
|
||||
VARTYPE
|
||||
vcamp
|
||||
vcenter
|
||||
VCENTER
|
||||
vcgtq
|
||||
VCINSTALLDIR
|
||||
Vcpkg
|
||||
@@ -1947,7 +1929,7 @@ vorrq
|
||||
VOS
|
||||
vpaddlq
|
||||
vqsubq
|
||||
vredraw
|
||||
VREDRAW
|
||||
vreinterpretq
|
||||
VSC
|
||||
VSCBD
|
||||
@@ -2115,7 +2097,6 @@ xstyler
|
||||
XTimer
|
||||
XUP
|
||||
XVIRTUALSCREEN
|
||||
XXL
|
||||
xxxxxx
|
||||
YAxis
|
||||
ycombinator
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: Claude Sonnet 4.5
|
||||
model: GPT-5-Codex (Preview)
|
||||
description: 'Generate an 80-character git commit title for the local diff.'
|
||||
---
|
||||
|
||||
|
||||
2
.github/prompts/create-pr-summary.prompt.md
vendored
2
.github/prompts/create-pr-summary.prompt.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: Claude Sonnet 4.5
|
||||
model: GPT-5-Codex (Preview)
|
||||
description: 'Generate a PowerToys-ready pull request description from the local diff.'
|
||||
---
|
||||
|
||||
|
||||
71
.github/prompts/fix-issue.prompt.md
vendored
71
.github/prompts/fix-issue.prompt.md
vendored
@@ -1,71 +0,0 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: GPT-5-Codex (Preview)
|
||||
description: " Execute the fix for a GitHub issue using the previously generated implementation plan. Apply code & tests directly in the repo. Output only a PR description (and optional manual steps)."
|
||||
---
|
||||
|
||||
# DEPENDENCY
|
||||
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
|
||||
158
.github/prompts/review-issue.prompt.md
vendored
158
.github/prompts/review-issue.prompt.md
vendored
@@ -1,158 +0,0 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: Claude Sonnet 4.5
|
||||
description: "You are github issue review and planning expertise, Score (0–100) and write one Implementation Plan. Outputs: overview.md, implementation-plan.md."
|
||||
---
|
||||
|
||||
# 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 from the prompt on the
|
||||
|
||||
# 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 the image for understand the context of the issue more.
|
||||
Locate source code in current workspace, but also free feel to use via `rg`/`git grep`. Link related issues/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
199
.github/prompts/review-pr.prompt.md
vendored
@@ -1,199 +0,0 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: Claude Sonnet 4.5
|
||||
description: "gh-driven PR review; per-step Markdown + machine-readable outputs"
|
||||
---
|
||||
|
||||
# PR Review — gh + stepwise
|
||||
|
||||
**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
79
.github/review-tools/Get-GitHubPrFilePatch.ps1
vendored
@@ -1,79 +0,0 @@
|
||||
<#
|
||||
.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
91
.github/review-tools/Get-GitHubRawFile.ps1
vendored
@@ -1,91 +0,0 @@
|
||||
<#
|
||||
.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
173
.github/review-tools/Get-PrIncrementalChanges.ps1
vendored
@@ -1,173 +0,0 @@
|
||||
<#
|
||||
.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
170
.github/review-tools/Test-IncrementalReview.ps1
vendored
@@ -1,170 +0,0 @@
|
||||
<#
|
||||
.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
313
.github/review-tools/review-tools.instructions.md
vendored
@@ -1,313 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -131,8 +131,6 @@
|
||||
|
||||
"PowerToys.ImageResizer.exe",
|
||||
"PowerToys.ImageResizer.dll",
|
||||
"WinUI3Apps\\PowerToys.ImageResizerCLI.exe",
|
||||
"WinUI3Apps\\PowerToys.ImageResizerCLI.dll",
|
||||
"PowerToys.ImageResizerExt.dll",
|
||||
"PowerToys.ImageResizerContextMenu.dll",
|
||||
"ImageResizerContextMenuPackage.msix",
|
||||
@@ -237,14 +235,6 @@
|
||||
|
||||
"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",
|
||||
@@ -368,13 +358,9 @@
|
||||
"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",
|
||||
"Shmuelie.WinRTServer.dll",
|
||||
"ToolGood.Words.Pinyin.dll"
|
||||
"Wpf.Ui.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())
|
||||
|
||||
@@ -63,20 +63,3 @@ 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
|
||||
|
||||
@@ -444,10 +444,6 @@ _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
|
||||
|
||||
@@ -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,7 +37,6 @@
|
||||
<!-- 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" />
|
||||
@@ -95,7 +94,6 @@
|
||||
<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" />
|
||||
@@ -147,4 +145,4 @@
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1560,7 +1560,6 @@ SOFTWARE.
|
||||
- ReverseMarkdown
|
||||
- ScipBe.Common.Office.OneNote
|
||||
- SharpCompress
|
||||
- Shmuelie.WinRTServer
|
||||
- SkiaSharp.Views.WinUI
|
||||
- StreamJsonRpc
|
||||
- StyleCop.Analyzers
|
||||
|
||||
@@ -44,10 +44,6 @@
|
||||
<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">
|
||||
@@ -86,6 +82,7 @@
|
||||
<Project Path="src/common/Telemetry/EtwTrace/EtwTrace.vcxproj" Id="8f021b46-362b-485c-bfba-ccf83e820cbd" />
|
||||
</Folder>
|
||||
<Folder Name="/common/utils/">
|
||||
<Project Path="src/common/utils/utils.vcxproj" Id="e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e" />
|
||||
<File Path="src/common/utils/appMutex.h" />
|
||||
<File Path="src/common/utils/color.h" />
|
||||
<File Path="src/common/utils/com_object_factory.h" />
|
||||
@@ -160,10 +157,6 @@
|
||||
<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" />
|
||||
@@ -174,10 +167,6 @@
|
||||
<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>
|
||||
@@ -218,11 +207,6 @@
|
||||
<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" />
|
||||
@@ -459,10 +443,6 @@
|
||||
<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">
|
||||
@@ -953,10 +933,6 @@
|
||||
<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" />
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
# 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`).
|
||||
@@ -8,6 +8,7 @@
|
||||
</Project>
|
||||
<Project Path="../src/common/Telemetry/EtwTrace/EtwTrace.vcxproj" Id="8f021b46-362b-485c-bfba-ccf83e820cbd" />
|
||||
<Project Path="../src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
|
||||
<Project Path="../src/common/utils/utils.vcxproj" Id="e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e" />
|
||||
<Project Path="../src/logging/logging.vcxproj" Id="7e1e3f13-2bd6-3f75-a6a7-873a2b55c60f">
|
||||
<Build Solution="Debug|ARM64" Project="false" />
|
||||
</Project>
|
||||
|
||||
@@ -162,6 +162,9 @@
|
||||
<ProjectReference Include="..\..\src\common\Telemetry\EtwTrace\EtwTrace.vcxproj">
|
||||
<Project>{8f021b46-362b-485c-bfba-ccf83e820cbd}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\src\common\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
|
||||
@@ -7,18 +7,11 @@
|
||||
|
||||
<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,6 +47,9 @@
|
||||
<ProjectReference Include="..\common\SettingsAPI\SettingsAPI.vcxproj">
|
||||
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\common\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="resource.h" />
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
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"
|
||||
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
|
||||
IgnorableNamespaces="uap uap2 uap3 rescap desktop uap10 systemai com">
|
||||
IgnorableNamespaces="uap uap2 uap3 rescap desktop uap10 systemai">
|
||||
<Identity
|
||||
Name="Microsoft.PowerToys.SparseApp"
|
||||
Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
||||
@@ -31,7 +30,6 @@
|
||||
<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"/>
|
||||
@@ -68,42 +66,5 @@
|
||||
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,6 +53,9 @@
|
||||
<ProjectReference Include="..\common\updating\updating.vcxproj">
|
||||
<Project>{17da04df-e393-4397-9cf0-84dabe11032e}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\common\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="resource.h" />
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
// 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
|
||||
{
|
||||
@@ -122,33 +120,28 @@ namespace Common.UI
|
||||
}
|
||||
}
|
||||
|
||||
// What about debug build? Should also consider debug build, maybe tray window message?
|
||||
public static void OpenSettings(SettingsWindow window)
|
||||
public static void OpenSettings(SettingsWindow window, bool mainExecutableIsOnTheParentFolder)
|
||||
{
|
||||
try
|
||||
{
|
||||
var exePath = Path.Combine(
|
||||
PowerToysPathResolver.GetPowerToysInstallPath(),
|
||||
"PowerToys.exe");
|
||||
|
||||
if (exePath == null || !File.Exists(exePath))
|
||||
var directoryPath = System.AppContext.BaseDirectory;
|
||||
if (mainExecutableIsOnTheParentFolder)
|
||||
{
|
||||
Logger.LogError($"Failed to find powertoys exe path, {exePath}");
|
||||
return;
|
||||
// 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");
|
||||
}
|
||||
|
||||
var args = "--open-settings=" + SettingsWindowNameToString(window);
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = exePath,
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
});
|
||||
Process.Start(new ProcessStartInfo(directoryPath) { Arguments = "--open-settings=" + SettingsWindowNameToString(window) });
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
Logger.LogError(ex.Message);
|
||||
// TODO(stefan): Log exception once unified logging is implemented
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Microsoft.PowerToys.FilePreviewCommon
|
||||
var softlineBreak = new Markdig.Extensions.Hardlines.SoftlineBreakAsHardlineExtension();
|
||||
|
||||
MarkdownPipelineBuilder pipelineBuilder;
|
||||
pipelineBuilder = new MarkdownPipelineBuilder().UseAdvancedExtensions().UseEmojiAndSmiley().UseYamlFrontMatter().UseMathematics().DisableHtml();
|
||||
pipelineBuilder = new MarkdownPipelineBuilder().UseAdvancedExtensions().UseEmojiAndSmiley().UseYamlFrontMatter().UseMathematics();
|
||||
pipelineBuilder.Extensions.Add(extension);
|
||||
pipelineBuilder.Extensions.Add(softlineBreak);
|
||||
|
||||
|
||||
@@ -113,6 +113,9 @@
|
||||
<ProjectReference Include="..\..\common\version\version.vcxproj">
|
||||
<Project>{cc6e41ac-8174-4e8a-8d22-85dd7f4851df}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\common\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
@@ -125,4 +128,4 @@
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using 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}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\Common.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,7 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<ProjectGuid>{6955446D-23F7-4023-9BB3-8657F904AF99}</ProjectGuid>
|
||||
@@ -50,6 +67,9 @@
|
||||
<ProjectReference Include="..\logger\logger.vcxproj">
|
||||
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Import Project="..\..\..\deps\spdlog.props" />
|
||||
@@ -65,4 +85,4 @@
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -75,62 +75,10 @@ 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;
|
||||
|
||||
@@ -23,20 +23,6 @@ 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();
|
||||
@@ -47,6 +33,7 @@ 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();
|
||||
|
||||
@@ -20,19 +20,6 @@ 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();
|
||||
@@ -64,4 +51,4 @@ namespace PowerToys
|
||||
static String ShowCmdPalEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,6 +168,9 @@
|
||||
<ProjectReference Include="..\version\version.vcxproj">
|
||||
<Project>{cc6e41ac-8174-4e8a-8d22-85dd7f4851df}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\utils\utils.vcxproj">
|
||||
<Project>{e8470e15-88c4-4c4b-b872-7f1a8f8e7d7e}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
|
||||
@@ -40,8 +40,6 @@ 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";
|
||||
@@ -85,21 +83,12 @@ 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";
|
||||
|
||||
@@ -141,12 +130,6 @@ 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";
|
||||
|
||||
62
src/common/utils/EventLocker.cpp
Normal file
62
src/common/utils/EventLocker.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#include "pch.h"
|
||||
#include "EventLocker.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
EventLocker::EventLocker(HANDLE handle) :
|
||||
eventHandle(handle)
|
||||
{
|
||||
if (eventHandle)
|
||||
{
|
||||
SetEvent(eventHandle);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<EventLocker> EventLocker::Get(std::wstring eventName)
|
||||
{
|
||||
EventLocker locker(std::move(eventName));
|
||||
if (!locker.eventHandle)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return std::optional<EventLocker>(std::move(locker));
|
||||
}
|
||||
|
||||
EventLocker::EventLocker(EventLocker&& other) noexcept :
|
||||
eventHandle(other.eventHandle)
|
||||
{
|
||||
other.eventHandle = nullptr;
|
||||
}
|
||||
|
||||
EventLocker& EventLocker::operator=(EventLocker&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
eventHandle = other.eventHandle;
|
||||
other.eventHandle = nullptr;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
EventLocker::~EventLocker()
|
||||
{
|
||||
if (eventHandle)
|
||||
{
|
||||
ResetEvent(eventHandle);
|
||||
CloseHandle(eventHandle);
|
||||
eventHandle = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
EventLocker::EventLocker(std::wstring eventName)
|
||||
{
|
||||
eventHandle = CreateEvent(nullptr, true, false, eventName.c_str());
|
||||
if (!eventHandle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetEvent(eventHandle);
|
||||
}
|
||||
@@ -1,61 +1,26 @@
|
||||
#include <windows.h>
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <windows.h>
|
||||
|
||||
class EventLocker
|
||||
{
|
||||
public:
|
||||
EventLocker(HANDLE h)
|
||||
{
|
||||
eventHandle = h;
|
||||
SetEvent(eventHandle);
|
||||
}
|
||||
explicit EventLocker(HANDLE handle);
|
||||
|
||||
static std::optional<EventLocker> Get(std::wstring eventName)
|
||||
{
|
||||
EventLocker locker(eventName);
|
||||
if (!locker.eventHandle)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
static std::optional<EventLocker> Get(std::wstring eventName);
|
||||
|
||||
return locker;
|
||||
}
|
||||
EventLocker(EventLocker&) = delete;
|
||||
EventLocker& operator=(EventLocker&) = delete;
|
||||
|
||||
EventLocker(EventLocker& e) = delete;
|
||||
EventLocker& operator=(EventLocker& e) = delete;
|
||||
EventLocker(EventLocker&& other) noexcept;
|
||||
EventLocker& operator=(EventLocker&& other) noexcept;
|
||||
|
||||
EventLocker(EventLocker&& e) noexcept
|
||||
{
|
||||
this->eventHandle = e.eventHandle;
|
||||
e.eventHandle = nullptr;
|
||||
}
|
||||
|
||||
EventLocker& operator=(EventLocker&& e) noexcept
|
||||
{
|
||||
this->eventHandle = e.eventHandle;
|
||||
e.eventHandle = nullptr;
|
||||
}
|
||||
~EventLocker();
|
||||
|
||||
~EventLocker()
|
||||
{
|
||||
if (eventHandle)
|
||||
{
|
||||
ResetEvent(eventHandle);
|
||||
CloseHandle(eventHandle);
|
||||
eventHandle = nullptr;
|
||||
}
|
||||
}
|
||||
private:
|
||||
EventLocker(std::wstring eventName)
|
||||
{
|
||||
eventHandle = CreateEvent(nullptr, true, false, eventName.c_str());
|
||||
if (!eventHandle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
explicit EventLocker(std::wstring eventName);
|
||||
|
||||
SetEvent(eventHandle);
|
||||
}
|
||||
|
||||
HANDLE eventHandle;
|
||||
HANDLE eventHandle = nullptr;
|
||||
};
|
||||
|
||||
71
src/common/utils/EventWaiter.cpp
Normal file
71
src/common/utils/EventWaiter.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
#include "pch.h"
|
||||
#include "EventWaiter.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
EventWaiter::EventWaiter(const std::wstring& name, std::function<void(DWORD)> callback)
|
||||
{
|
||||
// Create localExitThreadEvent and localWaitingEvent for capturing. We cannot capture 'this' as we implement move constructor.
|
||||
const auto localExitThreadEvent = exitThreadEvent = CreateEvent(nullptr, false, false, nullptr);
|
||||
const HANDLE localWaitingEvent = waitingEvent = CreateEvent(nullptr, false, false, name.c_str());
|
||||
|
||||
std::thread([=]() {
|
||||
HANDLE events[2] = { localWaitingEvent, localExitThreadEvent };
|
||||
while (true)
|
||||
{
|
||||
const auto waitResult = WaitForMultipleObjects(2, events, false, INFINITE);
|
||||
if (waitResult == WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (waitResult == WAIT_FAILED)
|
||||
{
|
||||
callback(GetLastError());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (waitResult == WAIT_OBJECT_0)
|
||||
{
|
||||
callback(ERROR_SUCCESS);
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
EventWaiter::EventWaiter(EventWaiter&& other) noexcept :
|
||||
exitThreadEvent(other.exitThreadEvent),
|
||||
waitingEvent(other.waitingEvent)
|
||||
{
|
||||
other.exitThreadEvent = nullptr;
|
||||
other.waitingEvent = nullptr;
|
||||
}
|
||||
|
||||
EventWaiter& EventWaiter::operator=(EventWaiter&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
exitThreadEvent = other.exitThreadEvent;
|
||||
waitingEvent = other.waitingEvent;
|
||||
other.exitThreadEvent = nullptr;
|
||||
other.waitingEvent = nullptr;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
EventWaiter::~EventWaiter()
|
||||
{
|
||||
if (exitThreadEvent)
|
||||
{
|
||||
SetEvent(exitThreadEvent);
|
||||
CloseHandle(exitThreadEvent);
|
||||
exitThreadEvent = nullptr;
|
||||
}
|
||||
|
||||
if (waitingEvent)
|
||||
{
|
||||
CloseHandle(waitingEvent);
|
||||
waitingEvent = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -3,128 +3,20 @@
|
||||
#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 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;
|
||||
}
|
||||
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();
|
||||
|
||||
private:
|
||||
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 };
|
||||
};
|
||||
HANDLE exitThreadEvent = nullptr;
|
||||
HANDLE waitingEvent = nullptr;
|
||||
};
|
||||
|
||||
60
src/common/utils/HDropIterator.cpp
Normal file
60
src/common/utils/HDropIterator.cpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#include "pch.h"
|
||||
#include "HDropIterator.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
HDropIterator::HDropIterator(IDataObject* dataObject)
|
||||
{
|
||||
FORMATETC formatetc{
|
||||
CF_HDROP,
|
||||
nullptr,
|
||||
DVASPECT_CONTENT,
|
||||
-1,
|
||||
TYMED_HGLOBAL
|
||||
};
|
||||
|
||||
if (dataObject && SUCCEEDED(dataObject->GetData(&formatetc, &m_medium)))
|
||||
{
|
||||
_listCount = DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), 0xFFFFFFFF, nullptr, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_medium = {};
|
||||
}
|
||||
}
|
||||
|
||||
HDropIterator::~HDropIterator()
|
||||
{
|
||||
if (m_medium.tymed)
|
||||
{
|
||||
ReleaseStgMedium(&m_medium);
|
||||
}
|
||||
}
|
||||
|
||||
void HDropIterator::First()
|
||||
{
|
||||
_current = 0;
|
||||
}
|
||||
|
||||
void HDropIterator::Next()
|
||||
{
|
||||
++_current;
|
||||
}
|
||||
|
||||
bool HDropIterator::IsDone() const
|
||||
{
|
||||
return _current >= _listCount;
|
||||
}
|
||||
|
||||
LPTSTR HDropIterator::CurrentItem() const
|
||||
{
|
||||
const UINT cch = DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), _current, nullptr, 0) + 1;
|
||||
LPTSTR path = static_cast<LPTSTR>(malloc(sizeof(TCHAR) * cch));
|
||||
if (!path)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), _current, path, cch);
|
||||
return path;
|
||||
}
|
||||
@@ -1,67 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <objidl.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
class HDropIterator
|
||||
{
|
||||
public:
|
||||
HDropIterator(IDataObject* pDataObject)
|
||||
{
|
||||
_current = 0;
|
||||
_listCount = 0;
|
||||
explicit HDropIterator(IDataObject* dataObject);
|
||||
~HDropIterator();
|
||||
|
||||
FORMATETC formatetc = {
|
||||
CF_HDROP,
|
||||
NULL,
|
||||
DVASPECT_CONTENT,
|
||||
-1,
|
||||
TYMED_HGLOBAL
|
||||
};
|
||||
|
||||
if (SUCCEEDED(pDataObject->GetData(&formatetc, &m_medium)))
|
||||
{
|
||||
_listCount = DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), 0xFFFFFFFF, NULL, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_medium = {};
|
||||
}
|
||||
}
|
||||
|
||||
~HDropIterator()
|
||||
{
|
||||
if (m_medium.tymed)
|
||||
{
|
||||
ReleaseStgMedium(&m_medium);
|
||||
}
|
||||
}
|
||||
|
||||
void First()
|
||||
{
|
||||
_current = 0;
|
||||
}
|
||||
|
||||
void Next()
|
||||
{
|
||||
_current++;
|
||||
}
|
||||
|
||||
bool IsDone() const
|
||||
{
|
||||
return _current >= _listCount;
|
||||
}
|
||||
|
||||
LPTSTR CurrentItem() const
|
||||
{
|
||||
UINT cch = DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), _current, NULL, 0) + 1;
|
||||
LPTSTR pszPath = static_cast<LPTSTR>(malloc(sizeof(TCHAR) * cch));
|
||||
|
||||
DragQueryFile(static_cast<HDROP>(m_medium.hGlobal), _current, pszPath, cch);
|
||||
|
||||
return pszPath;
|
||||
}
|
||||
void First();
|
||||
void Next();
|
||||
[[nodiscard]] bool IsDone() const;
|
||||
[[nodiscard]] LPTSTR CurrentItem() const;
|
||||
|
||||
private:
|
||||
UINT _listCount;
|
||||
STGMEDIUM m_medium;
|
||||
UINT _current;
|
||||
UINT _listCount = 0;
|
||||
STGMEDIUM m_medium{};
|
||||
UINT _current = 0;
|
||||
};
|
||||
|
||||
73
src/common/utils/HttpClient.cpp
Normal file
73
src/common/utils/HttpClient.cpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#include "pch.h"
|
||||
#include "HttpClient.h"
|
||||
|
||||
namespace http
|
||||
{
|
||||
HttpClient::HttpClient()
|
||||
{
|
||||
auto headers = m_client.DefaultRequestHeaders();
|
||||
headers.UserAgent().TryParseAdd(USER_AGENT);
|
||||
}
|
||||
|
||||
std::future<std::wstring> HttpClient::request(const winrt::Windows::Foundation::Uri& url)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url);
|
||||
(void)response.EnsureSuccessStatusCode();
|
||||
auto body = co_await response.Content().ReadAsStringAsync();
|
||||
co_return std::wstring(body);
|
||||
}
|
||||
|
||||
std::future<void> HttpClient::download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url);
|
||||
(void)response.EnsureSuccessStatusCode();
|
||||
auto fileStream = co_await storage::Streams::FileRandomAccessStream::OpenAsync(
|
||||
dstFilePath.c_str(),
|
||||
storage::FileAccessMode::ReadWrite,
|
||||
storage::StorageOpenOptions::AllowReadersAndWriters,
|
||||
storage::Streams::FileOpenDisposition::CreateAlways);
|
||||
co_await response.Content().WriteToStreamAsync(fileStream);
|
||||
fileStream.Close();
|
||||
}
|
||||
|
||||
std::future<void> HttpClient::download(const winrt::Windows::Foundation::Uri& url,
|
||||
const std::wstring& dstFilePath,
|
||||
const std::function<void(float)>& progressUpdateCallback)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url, winrt::Windows::Web::Http::HttpCompletionOption::ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
const uint64_t totalBytes = response.Content().Headers().ContentLength().GetUInt64();
|
||||
auto contentStream = co_await response.Content().ReadAsInputStreamAsync();
|
||||
|
||||
uint64_t totalBytesRead = 0;
|
||||
storage::Streams::Buffer buffer(8192);
|
||||
auto fileStream = co_await storage::Streams::FileRandomAccessStream::OpenAsync(
|
||||
dstFilePath.c_str(),
|
||||
storage::FileAccessMode::ReadWrite,
|
||||
storage::StorageOpenOptions::AllowReadersAndWriters,
|
||||
storage::Streams::FileOpenDisposition::CreateAlways);
|
||||
|
||||
co_await contentStream.ReadAsync(buffer, buffer.Capacity(), storage::Streams::InputStreamOptions::None);
|
||||
while (buffer.Length() > 0)
|
||||
{
|
||||
co_await fileStream.WriteAsync(buffer);
|
||||
totalBytesRead += buffer.Length();
|
||||
if (progressUpdateCallback)
|
||||
{
|
||||
const float percentage = static_cast<float>(totalBytesRead) / totalBytes;
|
||||
progressUpdateCallback(percentage);
|
||||
}
|
||||
|
||||
co_await contentStream.ReadAsync(buffer, buffer.Capacity(), storage::Streams::InputStreamOptions::None);
|
||||
}
|
||||
|
||||
if (progressUpdateCallback)
|
||||
{
|
||||
progressUpdateCallback(1);
|
||||
}
|
||||
|
||||
fileStream.Close();
|
||||
contentStream.Close();
|
||||
}
|
||||
}
|
||||
@@ -15,63 +15,13 @@ namespace http
|
||||
class HttpClient
|
||||
{
|
||||
public:
|
||||
HttpClient()
|
||||
{
|
||||
auto headers = m_client.DefaultRequestHeaders();
|
||||
headers.UserAgent().TryParseAdd(USER_AGENT);
|
||||
}
|
||||
HttpClient();
|
||||
|
||||
std::future<std::wstring> request(const winrt::Windows::Foundation::Uri& url)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url);
|
||||
(void)response.EnsureSuccessStatusCode();
|
||||
auto body = co_await response.Content().ReadAsStringAsync();
|
||||
co_return std::wstring(body);
|
||||
}
|
||||
|
||||
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url);
|
||||
(void)response.EnsureSuccessStatusCode();
|
||||
auto file_stream = co_await storage::Streams::FileRandomAccessStream::OpenAsync(dstFilePath.c_str(), storage::FileAccessMode::ReadWrite, storage::StorageOpenOptions::AllowReadersAndWriters, storage::Streams::FileOpenDisposition::CreateAlways);
|
||||
co_await response.Content().WriteToStreamAsync(file_stream);
|
||||
file_stream.Close();
|
||||
}
|
||||
|
||||
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath, const std::function<void(float)>& progressUpdateCallback)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url, HttpCompletionOption::ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
uint64_t totalBytes = response.Content().Headers().ContentLength().GetUInt64();
|
||||
auto contentStream = co_await response.Content().ReadAsInputStreamAsync();
|
||||
|
||||
uint64_t totalBytesRead = 0;
|
||||
storage::Streams::Buffer buffer(8192);
|
||||
auto fileStream = co_await storage::Streams::FileRandomAccessStream::OpenAsync(dstFilePath.c_str(), storage::FileAccessMode::ReadWrite, storage::StorageOpenOptions::AllowReadersAndWriters, storage::Streams::FileOpenDisposition::CreateAlways);
|
||||
|
||||
co_await contentStream.ReadAsync(buffer, buffer.Capacity(), storage::Streams::InputStreamOptions::None);
|
||||
while (buffer.Length() > 0)
|
||||
{
|
||||
co_await fileStream.WriteAsync(buffer);
|
||||
totalBytesRead += buffer.Length();
|
||||
if (progressUpdateCallback)
|
||||
{
|
||||
float percentage = static_cast<float>(totalBytesRead) / totalBytes;
|
||||
progressUpdateCallback(percentage);
|
||||
}
|
||||
|
||||
co_await contentStream.ReadAsync(buffer, buffer.Capacity(), storage::Streams::InputStreamOptions::None);
|
||||
}
|
||||
|
||||
if (progressUpdateCallback)
|
||||
{
|
||||
progressUpdateCallback(1);
|
||||
}
|
||||
|
||||
fileStream.Close();
|
||||
contentStream.Close();
|
||||
}
|
||||
std::future<std::wstring> request(const winrt::Windows::Foundation::Uri& url);
|
||||
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath);
|
||||
std::future<void> download(const winrt::Windows::Foundation::Uri& url,
|
||||
const std::wstring& dstFilePath,
|
||||
const std::function<void(float)>& progressUpdateCallback);
|
||||
|
||||
private:
|
||||
winrt::Windows::Web::Http::HttpClient m_client;
|
||||
|
||||
13
src/common/utils/MsWindowsSettings.cpp
Normal file
13
src/common/utils/MsWindowsSettings.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#include "pch.h"
|
||||
#include "MsWindowsSettings.h"
|
||||
|
||||
bool GetAnimationsEnabled()
|
||||
{
|
||||
BOOL enabled = 0;
|
||||
const auto result = SystemParametersInfo(SPI_GETCLIENTAREAANIMATION, 0, &enabled, 0);
|
||||
if (!result)
|
||||
{
|
||||
Logger::error("SystemParametersInfo SPI_GETCLIENTAREAANIMATION failed.");
|
||||
}
|
||||
return enabled != 0;
|
||||
}
|
||||
@@ -1,13 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
inline bool GetAnimationsEnabled()
|
||||
{
|
||||
BOOL enabled = 0;
|
||||
BOOL fResult;
|
||||
fResult = SystemParametersInfo(SPI_GETCLIENTAREAANIMATION, 0, &enabled, 0);
|
||||
if (!fResult)
|
||||
{
|
||||
Logger::error("SystemParametersInfo SPI_GETCLIENTAREAANIMATION failed.");
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
#include <common/logger/logger.h>
|
||||
#include <windows.h>
|
||||
|
||||
bool GetAnimationsEnabled();
|
||||
|
||||
83
src/common/utils/MsiUtils.cpp
Normal file
83
src/common/utils/MsiUtils.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "pch.h"
|
||||
#include "MsiUtils.h"
|
||||
|
||||
std::optional<std::wstring> GetMsiPackageInstalledPath(bool perUser)
|
||||
{
|
||||
constexpr size_t guid_length = 39;
|
||||
wchar_t productId[guid_length];
|
||||
const std::wstring upgradeCode = perUser ? POWER_TOYS_UPGRADE_CODE_USER : POWER_TOYS_UPGRADE_CODE;
|
||||
if (const bool found = ERROR_SUCCESS == MsiEnumRelatedProductsW(upgradeCode.c_str(), 0, 0, productId); !found)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (const bool installed = INSTALLSTATE_DEFAULT == MsiQueryProductStateW(productId); !installed)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
DWORD bufferSize = MAX_PATH;
|
||||
wchar_t buffer[MAX_PATH];
|
||||
if (ERROR_SUCCESS == MsiGetProductInfoW(productId, INSTALLPROPERTY_INSTALLLOCATION, buffer, &bufferSize) && bufferSize)
|
||||
{
|
||||
return buffer;
|
||||
}
|
||||
|
||||
DWORD packagePathSize = 0;
|
||||
|
||||
if (ERROR_SUCCESS != MsiGetProductInfoW(productId, INSTALLPROPERTY_LOCALPACKAGE, nullptr, &packagePathSize))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::wstring packagePath(++packagePathSize, L'\0');
|
||||
|
||||
if (ERROR_SUCCESS != MsiGetProductInfoW(productId, INSTALLPROPERTY_LOCALPACKAGE, packagePath.data(), &packagePathSize))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
packagePath.resize(size(packagePath) - 1); // trim additional \0 which we got from MsiGetProductInfoW
|
||||
|
||||
wchar_t path[MAX_PATH];
|
||||
DWORD pathSize = MAX_PATH;
|
||||
MsiGetComponentPathW(productId, POWERTOYS_EXE_COMPONENT, path, &pathSize);
|
||||
if (!pathSize)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
PathCchRemoveFileSpec(path, pathSize);
|
||||
return path;
|
||||
}
|
||||
|
||||
std::wstring GetMsiPackagePath()
|
||||
{
|
||||
std::wstring packagePath;
|
||||
wchar_t productString[39];
|
||||
if (const bool found = ERROR_SUCCESS == MsiEnumRelatedProductsW(POWER_TOYS_UPGRADE_CODE, 0, 0, productString); !found)
|
||||
{
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
if (const bool installed = INSTALLSTATE_DEFAULT == MsiQueryProductStateW(productString); !installed)
|
||||
{
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
DWORD packagePathSize = 0;
|
||||
|
||||
if (const bool hasPackagePath = ERROR_SUCCESS == MsiGetProductInfoW(productString, INSTALLPROPERTY_LOCALPACKAGE, nullptr, &packagePathSize); !hasPackagePath)
|
||||
{
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
packagePath = std::wstring(++packagePathSize, L'\0');
|
||||
if (const bool gotPackagePath = ERROR_SUCCESS == MsiGetProductInfoW(productString, INSTALLPROPERTY_LOCALPACKAGE, packagePath.data(), &packagePathSize); !gotPackagePath)
|
||||
{
|
||||
packagePath.clear();
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
packagePath.resize(size(packagePath) - 1); // trim additional \0 which we got from MsiGetProductInfoW
|
||||
|
||||
return packagePath;
|
||||
}
|
||||
@@ -16,82 +16,5 @@ namespace // Strings in this namespace should not be localized
|
||||
const inline wchar_t POWERTOYS_EXE_COMPONENT[] = L"{A2C66D91-3485-4D00-B04D-91844E6B345B}";
|
||||
}
|
||||
|
||||
std::optional<std::wstring> GetMsiPackageInstalledPath(bool perUser)
|
||||
{
|
||||
constexpr size_t guid_length = 39;
|
||||
wchar_t product_ID[guid_length];
|
||||
std::wstring upgradeCode = (perUser ? POWER_TOYS_UPGRADE_CODE_USER : POWER_TOYS_UPGRADE_CODE);
|
||||
if (const bool found = ERROR_SUCCESS == MsiEnumRelatedProductsW(upgradeCode.c_str(), 0, 0, product_ID); !found)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (const bool installed = INSTALLSTATE_DEFAULT == MsiQueryProductStateW(product_ID); !installed)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
DWORD buf_size = MAX_PATH;
|
||||
wchar_t buf[MAX_PATH];
|
||||
if (ERROR_SUCCESS == MsiGetProductInfoW(product_ID, INSTALLPROPERTY_INSTALLLOCATION, buf, &buf_size) && buf_size)
|
||||
{
|
||||
return buf;
|
||||
}
|
||||
|
||||
DWORD package_path_size = 0;
|
||||
|
||||
if (ERROR_SUCCESS != MsiGetProductInfoW(product_ID, INSTALLPROPERTY_LOCALPACKAGE, nullptr, &package_path_size))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
std::wstring package_path(++package_path_size, L'\0');
|
||||
|
||||
if (ERROR_SUCCESS != MsiGetProductInfoW(product_ID, INSTALLPROPERTY_LOCALPACKAGE, package_path.data(), &package_path_size))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
package_path.resize(size(package_path) - 1); // trim additional \0 which we got from MsiGetProductInfoW
|
||||
|
||||
wchar_t path[MAX_PATH];
|
||||
DWORD path_size = MAX_PATH;
|
||||
MsiGetComponentPathW(product_ID, POWERTOYS_EXE_COMPONENT, path, &path_size);
|
||||
if (!path_size)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
PathCchRemoveFileSpec(path, path_size);
|
||||
return path;
|
||||
}
|
||||
|
||||
std::wstring GetMsiPackagePath()
|
||||
{
|
||||
std::wstring package_path;
|
||||
wchar_t GUID_product_string[39];
|
||||
if (const bool found = ERROR_SUCCESS == MsiEnumRelatedProductsW(POWER_TOYS_UPGRADE_CODE, 0, 0, GUID_product_string); !found)
|
||||
{
|
||||
return package_path;
|
||||
}
|
||||
|
||||
if (const bool installed = INSTALLSTATE_DEFAULT == MsiQueryProductStateW(GUID_product_string); !installed)
|
||||
{
|
||||
return package_path;
|
||||
}
|
||||
|
||||
DWORD package_path_size = 0;
|
||||
|
||||
if (const bool has_package_path = ERROR_SUCCESS == MsiGetProductInfoW(GUID_product_string, INSTALLPROPERTY_LOCALPACKAGE, nullptr, &package_path_size); !has_package_path)
|
||||
{
|
||||
return package_path;
|
||||
}
|
||||
|
||||
package_path = std::wstring(++package_path_size, L'\0');
|
||||
if (const bool got_package_path = ERROR_SUCCESS == MsiGetProductInfoW(GUID_product_string, INSTALLPROPERTY_LOCALPACKAGE, package_path.data(), &package_path_size); !got_package_path)
|
||||
{
|
||||
package_path = {};
|
||||
return package_path;
|
||||
}
|
||||
|
||||
package_path.resize(size(package_path) - 1); // trim additional \0 which we got from MsiGetProductInfoW
|
||||
|
||||
return package_path;
|
||||
}
|
||||
std::optional<std::wstring> GetMsiPackageInstalledPath(bool perUser);
|
||||
std::wstring GetMsiPackagePath();
|
||||
|
||||
57
src/common/utils/OnThreadExecutor.cpp
Normal file
57
src/common/utils/OnThreadExecutor.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "pch.h"
|
||||
#include "OnThreadExecutor.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
OnThreadExecutor::OnThreadExecutor() :
|
||||
_worker_thread([this] { worker_thread(); })
|
||||
{
|
||||
}
|
||||
|
||||
OnThreadExecutor::~OnThreadExecutor()
|
||||
{
|
||||
_shutdown_request = true;
|
||||
_task_cv.notify_one();
|
||||
if (_worker_thread.joinable())
|
||||
{
|
||||
_worker_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
std::future<void> OnThreadExecutor::submit(task_t task)
|
||||
{
|
||||
auto future = task.get_future();
|
||||
{
|
||||
std::lock_guard lock{ _task_mutex };
|
||||
_task_queue.emplace(std::move(task));
|
||||
}
|
||||
_task_cv.notify_one();
|
||||
return future;
|
||||
}
|
||||
|
||||
void OnThreadExecutor::cancel()
|
||||
{
|
||||
std::lock_guard lock{ _task_mutex };
|
||||
std::queue<task_t> emptyQueue;
|
||||
std::swap(_task_queue, emptyQueue);
|
||||
_task_cv.notify_one();
|
||||
}
|
||||
|
||||
void OnThreadExecutor::worker_thread()
|
||||
{
|
||||
while (!_shutdown_request)
|
||||
{
|
||||
task_t task;
|
||||
{
|
||||
std::unique_lock task_lock{ _task_mutex };
|
||||
_task_cv.wait(task_lock, [this] { return !_task_queue.empty() || _shutdown_request; });
|
||||
if (_shutdown_request)
|
||||
{
|
||||
return;
|
||||
}
|
||||
task = std::move(_task_queue.front());
|
||||
_task_queue.pop();
|
||||
}
|
||||
task();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <functional>
|
||||
#include <queue>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
|
||||
// OnThreadExecutor allows its caller to off-load some work to a persistently running background thread.
|
||||
// This might come in handy if you use the API which sets thread-wide global state and the state needs
|
||||
@@ -15,58 +17,18 @@ class OnThreadExecutor final
|
||||
public:
|
||||
using task_t = std::packaged_task<void()>;
|
||||
|
||||
OnThreadExecutor() :
|
||||
_shutdown_request{ false },
|
||||
_worker_thread{ [this] { worker_thread(); } }
|
||||
{
|
||||
}
|
||||
OnThreadExecutor();
|
||||
~OnThreadExecutor();
|
||||
|
||||
~OnThreadExecutor()
|
||||
{
|
||||
_shutdown_request = true;
|
||||
_task_cv.notify_one();
|
||||
_worker_thread.join();
|
||||
}
|
||||
|
||||
std::future<void> submit(task_t task)
|
||||
{
|
||||
auto future = task.get_future();
|
||||
std::lock_guard lock{ _task_mutex };
|
||||
_task_queue.emplace(std::move(task));
|
||||
_task_cv.notify_one();
|
||||
return future;
|
||||
}
|
||||
|
||||
void cancel()
|
||||
{
|
||||
std::lock_guard lock{ _task_mutex };
|
||||
_task_queue = {};
|
||||
_task_cv.notify_one();
|
||||
}
|
||||
std::future<void> submit(task_t task);
|
||||
void cancel();
|
||||
|
||||
private:
|
||||
void worker_thread()
|
||||
{
|
||||
while (!_shutdown_request)
|
||||
{
|
||||
task_t task;
|
||||
{
|
||||
std::unique_lock task_lock{ _task_mutex };
|
||||
_task_cv.wait(task_lock, [this] { return !_task_queue.empty() || _shutdown_request; });
|
||||
if (_shutdown_request)
|
||||
{
|
||||
return;
|
||||
}
|
||||
task = std::move(_task_queue.front());
|
||||
_task_queue.pop();
|
||||
}
|
||||
task();
|
||||
}
|
||||
}
|
||||
void worker_thread();
|
||||
|
||||
std::mutex _task_mutex;
|
||||
std::condition_variable _task_cv;
|
||||
std::atomic_bool _shutdown_request;
|
||||
std::atomic_bool _shutdown_request{ false };
|
||||
std::queue<std::packaged_task<void()>> _task_queue;
|
||||
std::thread _worker_thread;
|
||||
};
|
||||
|
||||
40
src/common/utils/ProcessWaiter.cpp
Normal file
40
src/common/utils/ProcessWaiter.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include "pch.h"
|
||||
#include "ProcessWaiter.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace ProcessWaiter
|
||||
{
|
||||
void OnProcessTerminate(std::wstring parent_pid, std::function<void(DWORD)> callback)
|
||||
{
|
||||
DWORD pid = 0;
|
||||
try
|
||||
{
|
||||
pid = std::stoul(parent_pid);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
callback(ERROR_INVALID_PARAMETER);
|
||||
return;
|
||||
}
|
||||
|
||||
std::thread([pid, callback = std::move(callback)]() mutable {
|
||||
wil::unique_handle process{ OpenProcess(SYNCHRONIZE, FALSE, pid) };
|
||||
if (process)
|
||||
{
|
||||
if (WaitForSingleObject(process.get(), INFINITE) == WAIT_OBJECT_0)
|
||||
{
|
||||
callback(ERROR_SUCCESS);
|
||||
}
|
||||
else
|
||||
{
|
||||
callback(GetLastError());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
callback(GetLastError());
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <Windows.h>
|
||||
#include <thread>
|
||||
#include <Windows.h>
|
||||
|
||||
namespace ProcessWaiter
|
||||
{
|
||||
void OnProcessTerminate(std::wstring parent_pid, std::function<void(DWORD)> callback)
|
||||
{
|
||||
DWORD pid = std::stol(parent_pid);
|
||||
std::thread([=]() {
|
||||
HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, pid);
|
||||
if (process != nullptr)
|
||||
{
|
||||
if (WaitForSingleObject(process, INFINITE) == WAIT_OBJECT_0)
|
||||
{
|
||||
CloseHandle(process);
|
||||
callback(ERROR_SUCCESS);
|
||||
}
|
||||
else
|
||||
{
|
||||
CloseHandle(process);
|
||||
callback(GetLastError());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
callback(GetLastError());
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
void OnProcessTerminate(std::wstring parent_pid, std::function<void(DWORD)> callback);
|
||||
}
|
||||
|
||||
277
src/common/utils/UnhandledExceptionHandler.cpp
Normal file
277
src/common/utils/UnhandledExceptionHandler.cpp
Normal file
@@ -0,0 +1,277 @@
|
||||
#include "pch.h"
|
||||
#include "UnhandledExceptionHandler.h"
|
||||
|
||||
#include <DbgHelp.h>
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
#include <sstream>
|
||||
|
||||
#include "winapi_error.h"
|
||||
#include "../logger/logger.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::atomic_bool processingException{ false };
|
||||
|
||||
const char* exceptionDescription(const DWORD code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case EXCEPTION_ACCESS_VIOLATION:
|
||||
return "EXCEPTION_ACCESS_VIOLATION";
|
||||
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
|
||||
return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED";
|
||||
case EXCEPTION_BREAKPOINT:
|
||||
return "EXCEPTION_BREAKPOINT";
|
||||
case EXCEPTION_DATATYPE_MISALIGNMENT:
|
||||
return "EXCEPTION_DATATYPE_MISALIGNMENT";
|
||||
case EXCEPTION_FLT_DENORMAL_OPERAND:
|
||||
return "EXCEPTION_FLT_DENORMAL_OPERAND";
|
||||
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_FLT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_FLT_INEXACT_RESULT:
|
||||
return "EXCEPTION_FLT_INEXACT_RESULT";
|
||||
case EXCEPTION_FLT_INVALID_OPERATION:
|
||||
return "EXCEPTION_FLT_INVALID_OPERATION";
|
||||
case EXCEPTION_FLT_OVERFLOW:
|
||||
return "EXCEPTION_FLT_OVERFLOW";
|
||||
case EXCEPTION_FLT_STACK_CHECK:
|
||||
return "EXCEPTION_FLT_STACK_CHECK";
|
||||
case EXCEPTION_FLT_UNDERFLOW:
|
||||
return "EXCEPTION_FLT_UNDERFLOW";
|
||||
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
||||
return "EXCEPTION_ILLEGAL_INSTRUCTION";
|
||||
case EXCEPTION_IN_PAGE_ERROR:
|
||||
return "EXCEPTION_IN_PAGE_ERROR";
|
||||
case EXCEPTION_INT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_INT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_INT_OVERFLOW:
|
||||
return "EXCEPTION_INT_OVERFLOW";
|
||||
case EXCEPTION_INVALID_DISPOSITION:
|
||||
return "EXCEPTION_INVALID_DISPOSITION";
|
||||
case EXCEPTION_NONCONTINUABLE_EXCEPTION:
|
||||
return "EXCEPTION_NONCONTINUABLE_EXCEPTION";
|
||||
case EXCEPTION_PRIV_INSTRUCTION:
|
||||
return "EXCEPTION_PRIV_INSTRUCTION";
|
||||
case EXCEPTION_SINGLE_STEP:
|
||||
return "EXCEPTION_SINGLE_STEP";
|
||||
case EXCEPTION_STACK_OVERFLOW:
|
||||
return "EXCEPTION_STACK_OVERFLOW";
|
||||
default:
|
||||
return "UNKNOWN EXCEPTION";
|
||||
}
|
||||
}
|
||||
|
||||
int GetFilenameStart(wchar_t* path)
|
||||
{
|
||||
int pos = 0;
|
||||
int found = 0;
|
||||
if (path != nullptr)
|
||||
{
|
||||
while (path[pos] != L'\0' && pos < MAX_PATH)
|
||||
{
|
||||
if (path[pos] == L'\\')
|
||||
{
|
||||
found = pos + 1;
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
std::wstring GetModuleName(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static wchar_t modulePath[MAX_PATH]{};
|
||||
memset(&modulePath[0], '\0', sizeof(modulePath));
|
||||
|
||||
const DWORD64 moduleBase = SymGetModuleBase64(process, stack.AddrPC.Offset);
|
||||
if (!moduleBase)
|
||||
{
|
||||
Logger::error(L"Failed to get a module. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
if (!GetModuleFileNameW(reinterpret_cast<HINSTANCE>(moduleBase), modulePath, MAX_PATH))
|
||||
{
|
||||
Logger::error(L"Failed to get a module path. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
const int start = GetFilenameStart(modulePath);
|
||||
return std::wstring(modulePath, start);
|
||||
}
|
||||
|
||||
std::wstring GetName(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static IMAGEHLP_SYMBOL64* pSymbol = static_cast<IMAGEHLP_SYMBOL64*>(malloc(sizeof(IMAGEHLP_SYMBOL64) + MAX_PATH * sizeof(TCHAR)));
|
||||
if (!pSymbol)
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
memset(pSymbol, '\0', sizeof(*pSymbol) + MAX_PATH);
|
||||
pSymbol->MaxNameLength = MAX_PATH;
|
||||
pSymbol->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL64);
|
||||
|
||||
DWORD64 displacement = 0;
|
||||
if (!SymGetSymFromAddr64(process, stack.AddrPC.Offset, &displacement, pSymbol))
|
||||
{
|
||||
Logger::error(L"Failed to get a symbol. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
std::string str = pSymbol->Name;
|
||||
return std::wstring(str.begin(), str.end());
|
||||
}
|
||||
|
||||
std::wstring GetLine(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static IMAGEHLP_LINE64 line{};
|
||||
|
||||
memset(&line, '\0', sizeof(IMAGEHLP_LINE64));
|
||||
line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
|
||||
line.LineNumber = 0;
|
||||
|
||||
DWORD displacement = 0;
|
||||
if (!SymGetLineFromAddr64(process, stack.AddrPC.Offset, &displacement, &line))
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
std::string fileName(line.FileName);
|
||||
return L"(" + std::wstring(fileName.begin(), fileName.end()) + L":" + std::to_wstring(line.LineNumber) + L")";
|
||||
}
|
||||
|
||||
void LogStackTrace()
|
||||
{
|
||||
CONTEXT context;
|
||||
try
|
||||
{
|
||||
RtlCaptureContext(&context);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Failed to capture context. {}", get_last_error_or_default(GetLastError()));
|
||||
return;
|
||||
}
|
||||
|
||||
STACKFRAME64 stack;
|
||||
memset(&stack, 0, sizeof(STACKFRAME64));
|
||||
|
||||
HANDLE process = GetCurrentProcess();
|
||||
HANDLE thread = GetCurrentThread();
|
||||
|
||||
#ifdef _M_ARM64
|
||||
stack.AddrPC.Offset = context.Pc;
|
||||
stack.AddrStack.Offset = context.Sp;
|
||||
stack.AddrFrame.Offset = context.Fp;
|
||||
#else
|
||||
stack.AddrPC.Offset = context.Rip;
|
||||
stack.AddrStack.Offset = context.Rsp;
|
||||
stack.AddrFrame.Offset = context.Rbp;
|
||||
#endif
|
||||
stack.AddrPC.Mode = AddrModeFlat;
|
||||
stack.AddrStack.Mode = AddrModeFlat;
|
||||
stack.AddrFrame.Mode = AddrModeFlat;
|
||||
|
||||
std::wstringstream ss;
|
||||
for (;;)
|
||||
{
|
||||
const BOOL result = StackWalk64(
|
||||
#ifdef _M_ARM64
|
||||
IMAGE_FILE_MACHINE_ARM64,
|
||||
#else
|
||||
IMAGE_FILE_MACHINE_AMD64,
|
||||
#endif
|
||||
process,
|
||||
thread,
|
||||
&stack,
|
||||
&context,
|
||||
NULL,
|
||||
SymFunctionTableAccess64,
|
||||
SymGetModuleBase64,
|
||||
NULL);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ss << GetModuleName(process, stack) << "!" << GetName(process, stack) << GetLine(process, stack) << std::endl;
|
||||
}
|
||||
|
||||
Logger::error(L"STACK TRACE\r\n{}", ss.str());
|
||||
Logger::flush();
|
||||
}
|
||||
}
|
||||
|
||||
LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info)
|
||||
{
|
||||
bool expected = false;
|
||||
if (!processingException.compare_exchange_strong(expected, true))
|
||||
{
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
auto guard = wil::scope_exit([]() noexcept { processingException = false; });
|
||||
|
||||
try
|
||||
{
|
||||
const char* description = "Exception code not available";
|
||||
if (info != nullptr && info->ExceptionRecord != nullptr && info->ExceptionRecord->ExceptionCode != 0)
|
||||
{
|
||||
description = exceptionDescription(info->ExceptionRecord->ExceptionCode);
|
||||
}
|
||||
|
||||
Logger::error(description);
|
||||
LogStackTrace();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to log stack trace");
|
||||
Logger::flush();
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
void AbortHandler(int /*signal_number*/)
|
||||
{
|
||||
Logger::error("--- ABORT");
|
||||
try
|
||||
{
|
||||
LogStackTrace();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to log stack trace on abort");
|
||||
Logger::flush();
|
||||
}
|
||||
}
|
||||
|
||||
void InitSymbols()
|
||||
{
|
||||
// Preload symbols so they will be available in case of out-of-memory exception
|
||||
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME);
|
||||
HANDLE process = GetCurrentProcess();
|
||||
if (!SymInitialize(process, NULL, TRUE))
|
||||
{
|
||||
Logger::error(L"Failed to initialize symbol handler. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
}
|
||||
|
||||
void InitUnhandledExceptionHandler()
|
||||
{
|
||||
try
|
||||
{
|
||||
InitSymbols();
|
||||
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||
signal(SIGABRT, &AbortHandler);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to init global unhandled exception handler");
|
||||
}
|
||||
}
|
||||
@@ -1,280 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include <DbgHelp.h>
|
||||
#include <signal.h>
|
||||
#include <sstream>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "winapi_error.h"
|
||||
#include "../logger/logger.h"
|
||||
|
||||
static BOOLEAN processingException = FALSE;
|
||||
|
||||
static inline const char* exceptionDescription(const DWORD& code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case EXCEPTION_ACCESS_VIOLATION:
|
||||
return "EXCEPTION_ACCESS_VIOLATION";
|
||||
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
|
||||
return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED";
|
||||
case EXCEPTION_BREAKPOINT:
|
||||
return "EXCEPTION_BREAKPOINT";
|
||||
case EXCEPTION_DATATYPE_MISALIGNMENT:
|
||||
return "EXCEPTION_DATATYPE_MISALIGNMENT";
|
||||
case EXCEPTION_FLT_DENORMAL_OPERAND:
|
||||
return "EXCEPTION_FLT_DENORMAL_OPERAND";
|
||||
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_FLT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_FLT_INEXACT_RESULT:
|
||||
return "EXCEPTION_FLT_INEXACT_RESULT";
|
||||
case EXCEPTION_FLT_INVALID_OPERATION:
|
||||
return "EXCEPTION_FLT_INVALID_OPERATION";
|
||||
case EXCEPTION_FLT_OVERFLOW:
|
||||
return "EXCEPTION_FLT_OVERFLOW";
|
||||
case EXCEPTION_FLT_STACK_CHECK:
|
||||
return "EXCEPTION_FLT_STACK_CHECK";
|
||||
case EXCEPTION_FLT_UNDERFLOW:
|
||||
return "EXCEPTION_FLT_UNDERFLOW";
|
||||
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
||||
return "EXCEPTION_ILLEGAL_INSTRUCTION";
|
||||
case EXCEPTION_IN_PAGE_ERROR:
|
||||
return "EXCEPTION_IN_PAGE_ERROR";
|
||||
case EXCEPTION_INT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_INT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_INT_OVERFLOW:
|
||||
return "EXCEPTION_INT_OVERFLOW";
|
||||
case EXCEPTION_INVALID_DISPOSITION:
|
||||
return "EXCEPTION_INVALID_DISPOSITION";
|
||||
case EXCEPTION_NONCONTINUABLE_EXCEPTION:
|
||||
return "EXCEPTION_NONCONTINUABLE_EXCEPTION";
|
||||
case EXCEPTION_PRIV_INSTRUCTION:
|
||||
return "EXCEPTION_PRIV_INSTRUCTION";
|
||||
case EXCEPTION_SINGLE_STEP:
|
||||
return "EXCEPTION_SINGLE_STEP";
|
||||
case EXCEPTION_STACK_OVERFLOW:
|
||||
return "EXCEPTION_STACK_OVERFLOW";
|
||||
default:
|
||||
return "UNKNOWN EXCEPTION";
|
||||
}
|
||||
}
|
||||
|
||||
/* Returns the index of the last backslash in the file path */
|
||||
inline int GetFilenameStart(wchar_t* path)
|
||||
{
|
||||
int pos = 0;
|
||||
int found = 0;
|
||||
if (path != NULL)
|
||||
{
|
||||
while (path[pos] != L'\0' && pos < MAX_PATH)
|
||||
{
|
||||
if (path[pos] == L'\\')
|
||||
{
|
||||
found = pos + 1;
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
inline std::wstring GetModuleName(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static wchar_t modulePath[MAX_PATH]{};
|
||||
const size_t size = sizeof(modulePath);
|
||||
memset(&modulePath[0], '\0', size);
|
||||
|
||||
DWORD64 moduleBase = SymGetModuleBase64(process, stack.AddrPC.Offset);
|
||||
if (!moduleBase)
|
||||
{
|
||||
Logger::error(L"Failed to get a module. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
if (!GetModuleFileNameW(reinterpret_cast<HINSTANCE>(moduleBase), modulePath, MAX_PATH))
|
||||
{
|
||||
Logger::error(L"Failed to get a module path. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
const int start = GetFilenameStart(modulePath);
|
||||
return std::wstring(modulePath, start);
|
||||
}
|
||||
|
||||
inline std::wstring GetName(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static IMAGEHLP_SYMBOL64* pSymbol = static_cast<IMAGEHLP_SYMBOL64*>(malloc(sizeof(IMAGEHLP_SYMBOL64) + MAX_PATH * sizeof(TCHAR)));
|
||||
if (!pSymbol)
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
memset(pSymbol, '\0', sizeof(*pSymbol) + MAX_PATH);
|
||||
pSymbol->MaxNameLength = MAX_PATH;
|
||||
pSymbol->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL64);
|
||||
|
||||
DWORD64 dw64Displacement = 0;
|
||||
if (!SymGetSymFromAddr64(process, stack.AddrPC.Offset, &dw64Displacement, pSymbol))
|
||||
{
|
||||
Logger::error(L"Failed to get a symbol. {}", get_last_error_or_default(GetLastError()));
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
std::string str = pSymbol->Name;
|
||||
return std::wstring(str.begin(), str.end());
|
||||
}
|
||||
|
||||
inline std::wstring GetLine(HANDLE process, const STACKFRAME64& stack)
|
||||
{
|
||||
static IMAGEHLP_LINE64 line{};
|
||||
|
||||
memset(&line, '\0', sizeof(IMAGEHLP_LINE64));
|
||||
line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
|
||||
line.LineNumber = 0;
|
||||
|
||||
DWORD dwDisplacement = 0;
|
||||
if (!SymGetLineFromAddr64(process, stack.AddrPC.Offset, &dwDisplacement, &line))
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
std::string fileName(line.FileName);
|
||||
return L"(" + std::wstring(fileName.begin(), fileName.end()) + L":" + std::to_wstring(line.LineNumber) + L")";
|
||||
}
|
||||
|
||||
inline void LogStackTrace()
|
||||
{
|
||||
CONTEXT context;
|
||||
try
|
||||
{
|
||||
RtlCaptureContext(&context);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Failed to capture context. {}", get_last_error_or_default(GetLastError()));
|
||||
return;
|
||||
}
|
||||
|
||||
STACKFRAME64 stack;
|
||||
memset(&stack, 0, sizeof(STACKFRAME64));
|
||||
|
||||
HANDLE process = GetCurrentProcess();
|
||||
HANDLE thread = GetCurrentThread();
|
||||
|
||||
#ifdef _M_ARM64
|
||||
stack.AddrPC.Offset = context.Pc;
|
||||
stack.AddrStack.Offset = context.Sp;
|
||||
stack.AddrFrame.Offset = context.Fp;
|
||||
#else
|
||||
stack.AddrPC.Offset = context.Rip;
|
||||
stack.AddrStack.Offset = context.Rsp;
|
||||
stack.AddrFrame.Offset = context.Rbp;
|
||||
#endif
|
||||
stack.AddrPC.Mode = AddrModeFlat;
|
||||
stack.AddrStack.Mode = AddrModeFlat;
|
||||
stack.AddrFrame.Mode = AddrModeFlat;
|
||||
|
||||
BOOL result = false;
|
||||
std::wstringstream ss;
|
||||
for (;;)
|
||||
{
|
||||
result = StackWalk64(
|
||||
#ifdef _M_ARM64
|
||||
IMAGE_FILE_MACHINE_ARM64,
|
||||
#else
|
||||
IMAGE_FILE_MACHINE_AMD64,
|
||||
#endif
|
||||
process,
|
||||
thread,
|
||||
&stack,
|
||||
&context,
|
||||
NULL,
|
||||
SymFunctionTableAccess64,
|
||||
SymGetModuleBase64,
|
||||
NULL);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ss << GetModuleName(process, stack) << "!" << GetName(process, stack) << GetLine(process, stack) << std::endl;
|
||||
}
|
||||
|
||||
Logger::error(L"STACK TRACE\r\n{}", ss.str());
|
||||
Logger::flush();
|
||||
}
|
||||
|
||||
inline LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info)
|
||||
{
|
||||
if (!processingException)
|
||||
{
|
||||
bool headerLogged = false;
|
||||
try
|
||||
{
|
||||
const char* exDescription = "Exception code not available";
|
||||
processingException = true;
|
||||
if (info != NULL && info->ExceptionRecord != NULL && info->ExceptionRecord->ExceptionCode != NULL)
|
||||
{
|
||||
exDescription = exceptionDescription(info->ExceptionRecord->ExceptionCode);
|
||||
}
|
||||
|
||||
headerLogged = true;
|
||||
Logger::error(exDescription);
|
||||
LogStackTrace();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to log stack trace");
|
||||
Logger::flush();
|
||||
}
|
||||
|
||||
processingException = false;
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
/* Handler to trap abort() calls */
|
||||
inline void AbortHandler(int /*signal_number*/)
|
||||
{
|
||||
Logger::error("--- ABORT");
|
||||
try
|
||||
{
|
||||
LogStackTrace();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to log stack trace on abort");
|
||||
Logger::flush();
|
||||
}
|
||||
}
|
||||
|
||||
inline void InitSymbols()
|
||||
{
|
||||
// Preload symbols so they will be available in case of out-of-memory exception
|
||||
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME);
|
||||
HANDLE process = GetCurrentProcess();
|
||||
if (!SymInitialize(process, NULL, TRUE))
|
||||
{
|
||||
Logger::error(L"Failed to initialize symbol handler. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
}
|
||||
|
||||
inline void InitUnhandledExceptionHandler(void)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitSymbols();
|
||||
// Global handler for unhandled exceptions
|
||||
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||
// Handler for abort()
|
||||
signal(SIGABRT, &AbortHandler);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to init global unhandled exception handler");
|
||||
}
|
||||
}
|
||||
LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info);
|
||||
void AbortHandler(int signal_number);
|
||||
void InitSymbols();
|
||||
void InitUnhandledExceptionHandler();
|
||||
|
||||
14
src/common/utils/appMutex.cpp
Normal file
14
src/common/utils/appMutex.cpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#include "pch.h"
|
||||
#include "appMutex.h"
|
||||
|
||||
wil::unique_mutex_nothrow createAppMutex(const std::wstring& mutexName)
|
||||
{
|
||||
wil::unique_mutex_nothrow result{ CreateMutexW(nullptr, TRUE, mutexName.c_str()) };
|
||||
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -14,9 +14,4 @@ namespace
|
||||
constexpr inline wchar_t POWERTOYS_BOOTSTRAPPER_MUTEX_NAME[] = L"Local\\PowerToys_Bootstrapper_InstanceMutex";
|
||||
}
|
||||
|
||||
inline wil::unique_mutex_nothrow createAppMutex(const std::wstring& mutexName)
|
||||
{
|
||||
wil::unique_mutex_nothrow result{ CreateMutexW(nullptr, TRUE, mutexName.c_str()) };
|
||||
|
||||
return GetLastError() == ERROR_ALREADY_EXISTS ? wil::unique_mutex_nothrow{} : std::move(result);
|
||||
}
|
||||
wil::unique_mutex_nothrow createAppMutex(const std::wstring& mutexName);
|
||||
|
||||
18
src/common/utils/clean_video_conference.cpp
Normal file
18
src/common/utils/clean_video_conference.cpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#include "pch.h"
|
||||
#include "clean_video_conference.h"
|
||||
|
||||
void clean_video_conference()
|
||||
{
|
||||
// 31AD75E9-8C3A-49C8-B9ED-5880D6B4A764 is the CLSID GUID for the 64 video conference mute driver.
|
||||
// 31AD75E9-8C3A-49C8-B9ED-5880D6B4A732 is the CLSID GUID for the 32 video conference mute driver.
|
||||
// 860BB310-5D01-11D0-BD3B-00A0C911CE86 is the CLSID GUID for CLSID_VideoInputDeviceCategory.
|
||||
|
||||
// Unregister the 64 bit driver CLSID:
|
||||
RegDeleteTreeW(HKEY_CLASSES_ROOT, L"CLSID\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A764}");
|
||||
// Unregister the 64 bit driver CLSID from Video Input Devices:
|
||||
RegDeleteTreeW(HKEY_CLASSES_ROOT, L"CLSID\\{860BB310-5D01-11D0-BD3B-00A0C911CE86}\\Instance\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A764}");
|
||||
// Unregister the 32 bit driver CLSID:
|
||||
RegDeleteTreeW(HKEY_LOCAL_MACHINE, L"Software\\WOW6432Node\\Classes\\CLSID\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A732}");
|
||||
// Unregister the 32 bit driver CLSID from Video Input Devices:
|
||||
RegDeleteTreeW(HKEY_LOCAL_MACHINE, L"Software\\WOW6432Node\\Classes\\CLSID\\{860BB310-5D01-11D0-BD3B-00A0C911CE86}\\Instance\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A732}");
|
||||
}
|
||||
@@ -1,18 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
// Video Conference Mute was a utility we deprecated. However, this required a manual user disable of the module to remove the camera registration, so we include the disable code here to be able to clean up.
|
||||
void clean_video_conference()
|
||||
{
|
||||
// 31AD75E9-8C3A-49C8-B9ED-5880D6B4A764 is the CLSID GUID for the 64 video conference mute driver.
|
||||
// 31AD75E9-8C3A-49C8-B9ED-5880D6B4A732 is the CLSID GUID for the 32 video conference mute driver.
|
||||
// 860BB310-5D01-11D0-BD3B-00A0C911CE86 is the CLSID GUID for CLSID_VideoInputDeviceCategory.
|
||||
#include <windows.h>
|
||||
|
||||
// Unregister the 64 bit driver CLSID:
|
||||
RegDeleteTreeW(HKEY_CLASSES_ROOT, L"CLSID\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A764}");
|
||||
// Unregister the 64 bit driver CLSID from Video Input Devices:
|
||||
RegDeleteTreeW(HKEY_CLASSES_ROOT, L"CLSID\\{860BB310-5D01-11D0-BD3B-00A0C911CE86}\\Instance\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A764}");
|
||||
// Unregister the 32 bit driver CLSID:
|
||||
RegDeleteTreeW(HKEY_LOCAL_MACHINE, L"Software\\WOW6432Node\\Classes\\CLSID\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A732}");
|
||||
// Unregister the 32 bit driver CLSID from Video Input Devices:
|
||||
RegDeleteTreeW(HKEY_LOCAL_MACHINE, L"Software\\WOW6432Node\\Classes\\CLSID\\{860BB310-5D01-11D0-BD3B-00A0C911CE86}\\Instance\\{31AD75E9-8C3A-49C8-B9ED-5880D6B4A732}");
|
||||
}
|
||||
// Video Conference Mute was a utility we deprecated. However, this required a manual user disable of the module to remove the camera registration, so we include the disable code here to be able to clean up.
|
||||
void clean_video_conference();
|
||||
|
||||
50
src/common/utils/color.cpp
Normal file
50
src/common/utils/color.cpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#include "pch.h"
|
||||
#include "color.h"
|
||||
|
||||
bool checkValidRGB(std::wstring_view hex, uint8_t* R, uint8_t* G, uint8_t* B)
|
||||
{
|
||||
if (hex.length() != 7)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
hex = hex.substr(1, 6); // remove #
|
||||
for (const auto& c : hex)
|
||||
{
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (swscanf_s(hex.data(), L"%2hhx%2hhx%2hhx", R, G, B) != 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool checkValidARGB(std::wstring_view hex, uint8_t* A, uint8_t* R, uint8_t* G, uint8_t* B)
|
||||
{
|
||||
if (hex.length() != 9)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
hex = hex.substr(1, 8); // remove #
|
||||
for (const auto& c : hex)
|
||||
{
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (swscanf_s(hex.data(), L"%2hhx%2hhx%2hhx%2hhx", A, R, G, B) != 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,41 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
// helper function to get the RGB from a #FFFFFF string.
|
||||
inline bool checkValidRGB(std::wstring_view hex, uint8_t* R, uint8_t* G, uint8_t* B)
|
||||
{
|
||||
if (hex.length() != 7)
|
||||
return false;
|
||||
hex = hex.substr(1, 6); // remove #
|
||||
for (auto& c : hex)
|
||||
{
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (swscanf_s(hex.data(), L"%2hhx%2hhx%2hhx", R, G, B) != 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool checkValidRGB(std::wstring_view hex, uint8_t* R, uint8_t* G, uint8_t* B);
|
||||
|
||||
// helper function to get the ARGB from a #FFFFFFFF string.
|
||||
inline bool checkValidARGB(std::wstring_view hex, uint8_t* A, uint8_t* R, uint8_t* G, uint8_t* B)
|
||||
{
|
||||
if (hex.length() != 9)
|
||||
return false;
|
||||
hex = hex.substr(1, 8); // remove #
|
||||
for (auto& c : hex)
|
||||
{
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (swscanf_s(hex.data(), L"%2hhx%2hhx%2hhx%2hhx", A, R, G, B) != 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool checkValidARGB(std::wstring_view hex, uint8_t* A, uint8_t* R, uint8_t* G, uint8_t* B);
|
||||
|
||||
506
src/common/utils/elevation.cpp
Normal file
506
src/common/utils/elevation.cpp
Normal file
@@ -0,0 +1,506 @@
|
||||
#include "pch.h"
|
||||
#include "elevation.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::wstring GetErrorString(HRESULT handle)
|
||||
{
|
||||
_com_error err(handle);
|
||||
return err.ErrorMessage();
|
||||
}
|
||||
|
||||
bool FindDesktopFolderView(REFIID riid, void** ppv)
|
||||
{
|
||||
CComPtr<IShellWindows> spShellWindows;
|
||||
auto result = spShellWindows.CoCreateInstance(CLSID_ShellWindows);
|
||||
if (result != S_OK || spShellWindows == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to create instance. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComVariant vtLoc(CSIDL_DESKTOP);
|
||||
CComVariant vtEmpty;
|
||||
long lhwnd;
|
||||
CComPtr<IDispatch> spdisp;
|
||||
result = spShellWindows->FindWindowSW(&vtLoc, &vtEmpty, SWC_DESKTOP, &lhwnd, SWFO_NEEDDISPATCH, &spdisp);
|
||||
if (result != S_OK || spdisp == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to find the window. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IShellBrowser> spBrowser;
|
||||
result = CComQIPtr<IServiceProvider>(spdisp)->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&spBrowser));
|
||||
if (result != S_OK || spBrowser == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query service. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IShellView> spView;
|
||||
result = spBrowser->QueryActiveShellView(&spView);
|
||||
if (result != S_OK || spView == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query active shell window. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
result = spView->QueryInterface(riid, ppv);
|
||||
if (result != S_OK || ppv == nullptr || *ppv == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query interface. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GetDesktopAutomationObject(REFIID riid, void** ppv)
|
||||
{
|
||||
CComPtr<IShellView> spsv;
|
||||
|
||||
// Desktop may not be available on startup
|
||||
auto attempts = 5;
|
||||
for (auto i = 1; i <= attempts; i++)
|
||||
{
|
||||
if (FindDesktopFolderView(IID_PPV_ARGS(&spsv)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Logger::warn(L"FindDesktopFolderView() failed attempt {}", i);
|
||||
|
||||
if (i == attempts)
|
||||
{
|
||||
Logger::warn(L"FindDesktopFolderView() max attempts reached");
|
||||
return false;
|
||||
}
|
||||
Sleep(3000);
|
||||
}
|
||||
|
||||
CComPtr<IDispatch> spdispView;
|
||||
auto result = spsv->GetItemObject(SVGIO_BACKGROUND, IID_PPV_ARGS(&spdispView));
|
||||
if (result != S_OK || spdispView == nullptr)
|
||||
{
|
||||
Logger::warn(L"spsv->GetItemObject() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
return SUCCEEDED(spdispView->QueryInterface(riid, ppv));
|
||||
}
|
||||
|
||||
bool ShellExecuteFromExplorer(LPCWSTR file, LPCWSTR parameters, LPCWSTR directory)
|
||||
{
|
||||
CComPtr<IShellFolderViewDual> spFolderView;
|
||||
if (!GetDesktopAutomationObject(IID_PPV_ARGS(&spFolderView)))
|
||||
{
|
||||
Logger::warn(L"GetDesktopAutomationObject() failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IDispatch> spdispShell;
|
||||
auto result = spFolderView->get_Application(&spdispShell);
|
||||
if (result != S_OK || spdispShell == nullptr)
|
||||
{
|
||||
Logger::warn(L"spFolderView->get_Application() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComQIPtr<IShellDispatch2> shell(spdispShell);
|
||||
if (shell == nullptr)
|
||||
{
|
||||
Logger::warn(L"IShellDispatch2 is nullptr");
|
||||
return false;
|
||||
}
|
||||
|
||||
CComVariant args(parameters ? parameters : L"");
|
||||
CComVariant dir(directory ? directory : L"");
|
||||
CComVariant operation(L"open");
|
||||
CComVariant show(SW_SHOWNORMAL);
|
||||
result = shell->ShellExecute(CComBSTR(file), args, dir, operation, show);
|
||||
if (result != S_OK)
|
||||
{
|
||||
Logger::warn(L"IShellDispatch2::ShellExecute() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_process_elevated(const bool use_cached_value)
|
||||
{
|
||||
auto detection_func = []() {
|
||||
HANDLE token = nullptr;
|
||||
bool elevated = false;
|
||||
|
||||
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token))
|
||||
{
|
||||
TOKEN_ELEVATION elevation;
|
||||
DWORD size;
|
||||
if (GetTokenInformation(token, TokenElevation, &elevation, sizeof(elevation), &size))
|
||||
{
|
||||
elevated = (elevation.TokenIsElevated != 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (token)
|
||||
{
|
||||
CloseHandle(token);
|
||||
}
|
||||
|
||||
return elevated;
|
||||
};
|
||||
static const bool cached_value = detection_func();
|
||||
return use_cached_value ? cached_value : detection_func();
|
||||
}
|
||||
|
||||
bool drop_elevated_privileges()
|
||||
{
|
||||
HANDLE token = nullptr;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_DEFAULT | WRITE_OWNER, &token))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PSID medium_sid = NULL;
|
||||
if (!::ConvertStringSidToSid(SDDL_ML_MEDIUM, &medium_sid))
|
||||
{
|
||||
CloseHandle(token);
|
||||
return false;
|
||||
}
|
||||
|
||||
TOKEN_MANDATORY_LABEL tml = {};
|
||||
tml.Label.Attributes = SE_GROUP_INTEGRITY;
|
||||
tml.Label.Sid = medium_sid;
|
||||
DWORD dwLen = sizeof(TOKEN_MANDATORY_LABEL) + ::GetLengthSid(medium_sid);
|
||||
BOOL res = ::SetTokenInformation(token, TokenIntegrityLevel, &tml, dwLen);
|
||||
LocalFree(medium_sid);
|
||||
CloseHandle(token);
|
||||
return res == TRUE;
|
||||
}
|
||||
|
||||
HANDLE run_as_different_user(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir, const bool showWindow)
|
||||
{
|
||||
Logger::info(L"run_as_different_user with params={}", params);
|
||||
SHELLEXECUTEINFOW exec_info = { 0 };
|
||||
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
|
||||
exec_info.lpVerb = L"runAsUser";
|
||||
exec_info.lpFile = file.c_str();
|
||||
exec_info.lpParameters = params.c_str();
|
||||
exec_info.hwnd = 0;
|
||||
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
exec_info.lpDirectory = workingDir;
|
||||
exec_info.hInstApp = 0;
|
||||
exec_info.nShow = showWindow ? SW_SHOWDEFAULT : SW_HIDE; // may have limited effect
|
||||
return ShellExecuteExW(&exec_info) ? exec_info.hProcess : nullptr;
|
||||
}
|
||||
|
||||
HANDLE run_elevated(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir, const bool showWindow)
|
||||
{
|
||||
Logger::info(L"run_elevated with params={}", params);
|
||||
SHELLEXECUTEINFOW exec_info = { 0 };
|
||||
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
|
||||
exec_info.lpVerb = L"runas";
|
||||
exec_info.lpFile = file.c_str();
|
||||
exec_info.lpParameters = params.c_str();
|
||||
exec_info.hwnd = 0;
|
||||
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
exec_info.lpDirectory = workingDir;
|
||||
exec_info.hInstApp = 0;
|
||||
exec_info.nShow = showWindow ? SW_SHOWDEFAULT : SW_HIDE; // may have limited effect
|
||||
BOOL result = ShellExecuteExW(&exec_info);
|
||||
return result ? exec_info.hProcess : nullptr;
|
||||
}
|
||||
|
||||
bool run_non_elevated(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir, const bool showWindow)
|
||||
{
|
||||
Logger::info(L"run_non_elevated with params={}", params);
|
||||
auto executable_args = L"\"" + file + L"\"";
|
||||
if (!params.empty())
|
||||
{
|
||||
executable_args += L" " + params;
|
||||
}
|
||||
|
||||
HWND hwnd = GetShellWindow();
|
||||
if (!hwnd)
|
||||
{
|
||||
if (GetLastError() == ERROR_SUCCESS)
|
||||
{
|
||||
Logger::warn(L"GetShellWindow() returned null. Shell window is not available");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"GetShellWindow() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
DWORD pid;
|
||||
GetWindowThreadProcessId(hwnd, &pid);
|
||||
|
||||
winrt::handle process{ OpenProcess(PROCESS_CREATE_PROCESS, FALSE, pid) };
|
||||
if (!process)
|
||||
{
|
||||
Logger::error(L"OpenProcess() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
SIZE_T size = 0;
|
||||
InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
|
||||
auto pproc_buffer = std::make_unique<char[]>(size);
|
||||
auto pptal = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(pproc_buffer.get());
|
||||
if (!pptal)
|
||||
{
|
||||
Logger::error(L"pptal failed to initialize. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!InitializeProcThreadAttributeList(pptal, 1, 0, &size))
|
||||
{
|
||||
Logger::error(L"InitializeProcThreadAttributeList() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
HANDLE process_handle = process.get();
|
||||
if (!UpdateProcThreadAttribute(pptal,
|
||||
0,
|
||||
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
|
||||
&process_handle,
|
||||
sizeof(process_handle),
|
||||
nullptr,
|
||||
nullptr))
|
||||
{
|
||||
Logger::error(L"UpdateProcThreadAttribute() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
STARTUPINFOEX siex = { 0 };
|
||||
siex.lpAttributeList = pptal;
|
||||
siex.StartupInfo.cb = sizeof(siex);
|
||||
PROCESS_INFORMATION pi = { 0 };
|
||||
auto dwCreationFlags = EXTENDED_STARTUPINFO_PRESENT;
|
||||
|
||||
if (!showWindow)
|
||||
{
|
||||
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
|
||||
siex.StartupInfo.wShowWindow = SW_HIDE;
|
||||
dwCreationFlags = CREATE_NO_WINDOW;
|
||||
}
|
||||
|
||||
std::wstring cmdLine = executable_args;
|
||||
auto succeeded = CreateProcessW(file.c_str(),
|
||||
&cmdLine[0],
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
dwCreationFlags,
|
||||
nullptr,
|
||||
workingDir,
|
||||
&siex.StartupInfo,
|
||||
&pi);
|
||||
|
||||
if (succeeded)
|
||||
{
|
||||
if (returnPid)
|
||||
{
|
||||
*returnPid = pi.dwProcessId;
|
||||
}
|
||||
if (pi.hProcess)
|
||||
{
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
if (pi.hThread)
|
||||
{
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
}
|
||||
|
||||
DeleteProcThreadAttributeList(pptal);
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
bool RunNonElevatedEx(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir)
|
||||
{
|
||||
bool success = false;
|
||||
HRESULT co_init = E_FAIL;
|
||||
try
|
||||
{
|
||||
co_init = CoInitialize(nullptr);
|
||||
success = ShellExecuteFromExplorer(file.c_str(), params.c_str(), working_dir.c_str());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
if (SUCCEEDED(co_init))
|
||||
{
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
std::optional<ProcessInfo> RunNonElevatedFailsafe(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir, DWORD handleAccess)
|
||||
{
|
||||
bool launched = RunNonElevatedEx(file, params, working_dir);
|
||||
if (!launched)
|
||||
{
|
||||
Logger::warn(L"RunNonElevatedEx() failed. Trying fallback");
|
||||
std::wstring action_runner_path = get_module_folderpath() + L"\\PowerToys.ActionRunner.exe";
|
||||
std::wstring newParams = L"-run-non-elevated -target \"" + file + L"\" " + params;
|
||||
launched = run_non_elevated(action_runner_path, newParams, nullptr, working_dir.c_str());
|
||||
if (launched)
|
||||
{
|
||||
Logger::trace(L"Started {}", file);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Failed to start {}", file);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
auto handles = getProcessHandlesByName(std::filesystem::path{ file }.filename().wstring(), PROCESS_QUERY_INFORMATION | SYNCHRONIZE | handleAccess);
|
||||
if (handles.empty())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ProcessInfo result;
|
||||
result.processID = GetProcessId(handles[0].get());
|
||||
result.processHandle = std::move(handles[0]);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool run_same_elevation(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir)
|
||||
{
|
||||
auto executable_args = L"\"" + file + L"\"";
|
||||
if (!params.empty())
|
||||
{
|
||||
executable_args += L" " + params;
|
||||
}
|
||||
|
||||
STARTUPINFO si = { sizeof(STARTUPINFO) };
|
||||
PROCESS_INFORMATION pi = { 0 };
|
||||
|
||||
auto succeeded = CreateProcessW(file.c_str(),
|
||||
&executable_args[0],
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
0,
|
||||
nullptr,
|
||||
workingDir,
|
||||
&si,
|
||||
&pi);
|
||||
|
||||
if (succeeded)
|
||||
{
|
||||
if (pi.hProcess)
|
||||
{
|
||||
if (returnPid)
|
||||
{
|
||||
*returnPid = GetProcessId(pi.hProcess);
|
||||
}
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
if (pi.hThread)
|
||||
{
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
bool check_user_is_admin()
|
||||
{
|
||||
auto freeMemory = [](PSID pSID, PTOKEN_GROUPS pGroupInfo) {
|
||||
if (pSID)
|
||||
{
|
||||
FreeSid(pSID);
|
||||
}
|
||||
if (pGroupInfo)
|
||||
{
|
||||
GlobalFree(pGroupInfo);
|
||||
}
|
||||
};
|
||||
|
||||
HANDLE hToken;
|
||||
DWORD dwSize = 0;
|
||||
PTOKEN_GROUPS pGroupInfo;
|
||||
SID_IDENTIFIER_AUTHORITY SIDAuth = SECURITY_NT_AUTHORITY;
|
||||
PSID pSID = NULL;
|
||||
|
||||
// Open a handle to the access token for the calling process.
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Call GetTokenInformation to get the buffer size.
|
||||
if (!GetTokenInformation(hToken, TokenGroups, NULL, dwSize, &dwSize))
|
||||
{
|
||||
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate the buffer.
|
||||
pGroupInfo = static_cast<PTOKEN_GROUPS>(GlobalAlloc(GPTR, dwSize));
|
||||
|
||||
// Call GetTokenInformation again to get the group information.
|
||||
if (!GetTokenInformation(hToken, TokenGroups, pGroupInfo, dwSize, &dwSize))
|
||||
{
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create a SID for the BUILTIN\\Administrators group.
|
||||
if (!AllocateAndInitializeSid(&SIDAuth, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &pSID))
|
||||
{
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Loop through the group SIDs looking for the administrator SID.
|
||||
for (DWORD i = 0; i < pGroupInfo->GroupCount; ++i)
|
||||
{
|
||||
if (EqualSid(pSID, pGroupInfo->Groups[i].Sid))
|
||||
{
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsProcessOfWindowElevated(HWND window)
|
||||
{
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
if (!pid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
wil::unique_handle hProcess{ OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
FALSE,
|
||||
pid) };
|
||||
|
||||
wil::unique_handle token;
|
||||
|
||||
if (OpenProcessToken(hProcess.get(), TOKEN_QUERY, &token))
|
||||
{
|
||||
TOKEN_ELEVATION elevation;
|
||||
DWORD size;
|
||||
if (GetTokenInformation(token.get(), TokenElevation, &elevation, sizeof(elevation), &size))
|
||||
{
|
||||
return elevation.TokenIsElevated != 0;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
@@ -14,6 +14,8 @@
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
|
||||
@@ -22,376 +24,23 @@
|
||||
#include <common/utils/process_path.h>
|
||||
#include <common/utils/processApi.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
inline std::wstring GetErrorString(HRESULT handle)
|
||||
{
|
||||
_com_error err(handle);
|
||||
return err.ErrorMessage();
|
||||
}
|
||||
|
||||
inline bool FindDesktopFolderView(REFIID riid, void** ppv)
|
||||
{
|
||||
CComPtr<IShellWindows> spShellWindows;
|
||||
auto result = spShellWindows.CoCreateInstance(CLSID_ShellWindows);
|
||||
if (result != S_OK || spShellWindows == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to create instance. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComVariant vtLoc(CSIDL_DESKTOP);
|
||||
CComVariant vtEmpty;
|
||||
long lhwnd;
|
||||
CComPtr<IDispatch> spdisp;
|
||||
result = spShellWindows->FindWindowSW(
|
||||
&vtLoc, &vtEmpty, SWC_DESKTOP, &lhwnd, SWFO_NEEDDISPATCH, &spdisp);
|
||||
|
||||
if (result != S_OK || spdisp == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to find the window. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IShellBrowser> spBrowser;
|
||||
result = CComQIPtr<IServiceProvider>(spdisp)->QueryService(SID_STopLevelBrowser,
|
||||
IID_PPV_ARGS(&spBrowser));
|
||||
if (result != S_OK || spBrowser == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query service. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IShellView> spView;
|
||||
result = spBrowser->QueryActiveShellView(&spView);
|
||||
if (result != S_OK || spView == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query active shell window. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
result = spView->QueryInterface(riid, ppv);
|
||||
if (result != S_OK || ppv == nullptr || *ppv == nullptr)
|
||||
{
|
||||
Logger::warn(L"Failed to query interface. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool GetDesktopAutomationObject(REFIID riid, void** ppv)
|
||||
{
|
||||
CComPtr<IShellView> spsv;
|
||||
|
||||
// Desktop may not be available on startup
|
||||
auto attempts = 5;
|
||||
for (auto i = 1; i <= attempts; i++)
|
||||
{
|
||||
if (FindDesktopFolderView(IID_PPV_ARGS(&spsv)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Logger::warn(L"FindDesktopFolderView() failed attempt {}", i);
|
||||
|
||||
if (i == attempts)
|
||||
{
|
||||
Logger::warn(L"FindDesktopFolderView() max attempts reached");
|
||||
return false;
|
||||
}
|
||||
|
||||
Sleep(3000);
|
||||
}
|
||||
|
||||
CComPtr<IDispatch> spdispView;
|
||||
auto result = spsv->GetItemObject(SVGIO_BACKGROUND, IID_PPV_ARGS(&spdispView));
|
||||
if (result != S_OK)
|
||||
{
|
||||
Logger::warn(L"GetItemObject() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
result = spdispView->QueryInterface(riid, ppv);
|
||||
if (result != S_OK)
|
||||
{
|
||||
Logger::warn(L"QueryInterface() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ShellExecuteFromExplorer(
|
||||
PCWSTR pszFile,
|
||||
PCWSTR pszParameters = nullptr,
|
||||
PCWSTR workingDir = L"")
|
||||
{
|
||||
CComPtr<IShellFolderViewDual> spFolderView;
|
||||
if (!GetDesktopAutomationObject(IID_PPV_ARGS(&spFolderView)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IDispatch> spdispShell;
|
||||
auto result = spFolderView->get_Application(&spdispShell);
|
||||
if (result != S_OK)
|
||||
{
|
||||
Logger::warn(L"get_Application() failed. {}", GetErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
CComQIPtr<IShellDispatch2>(spdispShell)
|
||||
->ShellExecuteW(CComBSTR(pszFile),
|
||||
CComVariant(pszParameters ? pszParameters : L""),
|
||||
CComVariant(workingDir),
|
||||
CComVariant(L""),
|
||||
CComVariant(SW_SHOWNORMAL));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if the current process is running with elevated privileges
|
||||
inline bool is_process_elevated(const bool use_cached_value = true)
|
||||
{
|
||||
auto detection_func = []() {
|
||||
HANDLE token = nullptr;
|
||||
bool elevated = false;
|
||||
|
||||
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token))
|
||||
{
|
||||
TOKEN_ELEVATION elevation;
|
||||
DWORD size;
|
||||
if (GetTokenInformation(token, TokenElevation, &elevation, sizeof(elevation), &size))
|
||||
{
|
||||
elevated = (elevation.TokenIsElevated != 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (token)
|
||||
{
|
||||
CloseHandle(token);
|
||||
}
|
||||
|
||||
return elevated;
|
||||
};
|
||||
static const bool cached_value = detection_func();
|
||||
return use_cached_value ? cached_value : detection_func();
|
||||
}
|
||||
bool is_process_elevated(const bool use_cached_value = true);
|
||||
|
||||
// Drops the elevated privileges if present
|
||||
inline bool drop_elevated_privileges()
|
||||
{
|
||||
HANDLE token = nullptr;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_DEFAULT | WRITE_OWNER, &token))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool drop_elevated_privileges();
|
||||
|
||||
PSID medium_sid = NULL;
|
||||
if (!::ConvertStringSidToSid(SDDL_ML_MEDIUM, &medium_sid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Run command as different user, returns process handle or null on failure
|
||||
HANDLE run_as_different_user(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true);
|
||||
|
||||
TOKEN_MANDATORY_LABEL label = { 0 };
|
||||
label.Label.Attributes = SE_GROUP_INTEGRITY;
|
||||
label.Label.Sid = medium_sid;
|
||||
DWORD size = static_cast<DWORD>(sizeof(TOKEN_MANDATORY_LABEL) + ::GetLengthSid(medium_sid));
|
||||
|
||||
BOOL result = SetTokenInformation(token, TokenIntegrityLevel, &label, size);
|
||||
LocalFree(medium_sid);
|
||||
CloseHandle(token);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Run command as different user, returns true if succeeded
|
||||
inline HANDLE run_as_different_user(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true)
|
||||
{
|
||||
Logger::info(L"run_elevated with params={}", params);
|
||||
SHELLEXECUTEINFOW exec_info = { 0 };
|
||||
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
|
||||
exec_info.lpVerb = L"runAsUser";
|
||||
exec_info.lpFile = file.c_str();
|
||||
exec_info.lpParameters = params.c_str();
|
||||
exec_info.hwnd = 0;
|
||||
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
exec_info.lpDirectory = workingDir;
|
||||
exec_info.hInstApp = 0;
|
||||
if (showWindow)
|
||||
{
|
||||
exec_info.nShow = SW_SHOWDEFAULT;
|
||||
}
|
||||
else
|
||||
{
|
||||
// might have limited success, but only option with ShellExecuteExW
|
||||
exec_info.nShow = SW_HIDE;
|
||||
}
|
||||
|
||||
return ShellExecuteExW(&exec_info) ? exec_info.hProcess : nullptr;
|
||||
}
|
||||
|
||||
// Run command as elevated user, returns true if succeeded
|
||||
inline HANDLE run_elevated(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true)
|
||||
{
|
||||
Logger::info(L"run_elevated with params={}", params);
|
||||
SHELLEXECUTEINFOW exec_info = { 0 };
|
||||
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
|
||||
exec_info.lpVerb = L"runas";
|
||||
exec_info.lpFile = file.c_str();
|
||||
exec_info.lpParameters = params.c_str();
|
||||
exec_info.hwnd = 0;
|
||||
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
exec_info.lpDirectory = workingDir;
|
||||
exec_info.hInstApp = 0;
|
||||
|
||||
if (showWindow)
|
||||
{
|
||||
exec_info.nShow = SW_SHOWDEFAULT;
|
||||
}
|
||||
else
|
||||
{
|
||||
// might have limited success, but only option with ShellExecuteExW
|
||||
exec_info.nShow = SW_HIDE;
|
||||
}
|
||||
|
||||
BOOL result = ShellExecuteExW(&exec_info);
|
||||
|
||||
return result ? exec_info.hProcess : nullptr;
|
||||
}
|
||||
// Run command as elevated user, returns process handle or null on failure
|
||||
HANDLE run_elevated(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true);
|
||||
|
||||
// Run command as non-elevated user, returns true if succeeded, puts the process id into returnPid if returnPid != NULL
|
||||
inline bool run_non_elevated(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr, const bool showWindow = true)
|
||||
{
|
||||
Logger::info(L"run_non_elevated with params={}", params);
|
||||
auto executable_args = L"\"" + file + L"\"";
|
||||
if (!params.empty())
|
||||
{
|
||||
executable_args += L" " + params;
|
||||
}
|
||||
bool run_non_elevated(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr, const bool showWindow = true);
|
||||
|
||||
HWND hwnd = GetShellWindow();
|
||||
if (!hwnd)
|
||||
{
|
||||
if (GetLastError() == ERROR_SUCCESS)
|
||||
{
|
||||
Logger::warn(L"GetShellWindow() returned null. Shell window is not available");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"GetShellWindow() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
DWORD pid;
|
||||
GetWindowThreadProcessId(hwnd, &pid);
|
||||
|
||||
winrt::handle process{ OpenProcess(PROCESS_CREATE_PROCESS, FALSE, pid) };
|
||||
if (!process)
|
||||
{
|
||||
Logger::error(L"OpenProcess() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
SIZE_T size = 0;
|
||||
|
||||
InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
|
||||
auto pproc_buffer = std::make_unique<char[]>(size);
|
||||
auto pptal = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(pproc_buffer.get());
|
||||
if (!pptal)
|
||||
{
|
||||
Logger::error(L"pptal failed to initialize. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!InitializeProcThreadAttributeList(pptal, 1, 0, &size))
|
||||
{
|
||||
Logger::error(L"InitializeProcThreadAttributeList() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
HANDLE process_handle = process.get();
|
||||
if (!UpdateProcThreadAttribute(pptal,
|
||||
0,
|
||||
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
|
||||
&process_handle,
|
||||
sizeof(process_handle),
|
||||
nullptr,
|
||||
nullptr))
|
||||
{
|
||||
Logger::error(L"UpdateProcThreadAttribute() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
STARTUPINFOEX siex = { 0 };
|
||||
siex.lpAttributeList = pptal;
|
||||
siex.StartupInfo.cb = sizeof(siex);
|
||||
PROCESS_INFORMATION pi = { 0 };
|
||||
auto dwCreationFlags = EXTENDED_STARTUPINFO_PRESENT;
|
||||
|
||||
if (!showWindow)
|
||||
{
|
||||
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
|
||||
siex.StartupInfo.wShowWindow = SW_HIDE;
|
||||
dwCreationFlags = CREATE_NO_WINDOW;
|
||||
}
|
||||
|
||||
auto succeeded = CreateProcessW(file.c_str(),
|
||||
&executable_args[0],
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
dwCreationFlags,
|
||||
nullptr,
|
||||
workingDir,
|
||||
&siex.StartupInfo,
|
||||
&pi);
|
||||
if (succeeded)
|
||||
{
|
||||
if (pi.hProcess)
|
||||
{
|
||||
if (returnPid)
|
||||
{
|
||||
*returnPid = GetProcessId(pi.hProcess);
|
||||
}
|
||||
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
if (pi.hThread)
|
||||
{
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"CreateProcessW() failed. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
inline bool RunNonElevatedEx(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir)
|
||||
{
|
||||
bool success = false;
|
||||
HRESULT co_init = E_FAIL;
|
||||
try
|
||||
{
|
||||
co_init = CoInitialize(nullptr);
|
||||
success = ShellExecuteFromExplorer(file.c_str(), params.c_str(), working_dir.c_str());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
if (SUCCEEDED(co_init))
|
||||
{
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
// Try running through the shell's automation object
|
||||
bool RunNonElevatedEx(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir);
|
||||
|
||||
struct ProcessInfo
|
||||
{
|
||||
@@ -399,172 +48,16 @@ struct ProcessInfo
|
||||
DWORD processID = {};
|
||||
};
|
||||
|
||||
inline std::optional<ProcessInfo> RunNonElevatedFailsafe(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir, DWORD handleAccess = 0)
|
||||
{
|
||||
bool launched = RunNonElevatedEx(file, params, working_dir);
|
||||
if (!launched)
|
||||
{
|
||||
Logger::warn(L"RunNonElevatedEx() failed. Trying fallback");
|
||||
std::wstring action_runner_path = get_module_folderpath() + L"\\PowerToys.ActionRunner.exe";
|
||||
std::wstring newParams = fmt::format(L"-run-non-elevated -target \"{}\" {}", file, params);
|
||||
launched = run_non_elevated(action_runner_path, newParams, nullptr, working_dir.c_str());
|
||||
if (launched)
|
||||
{
|
||||
Logger::trace(L"Started {}", file);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Failed to start {}", file);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
auto handles = getProcessHandlesByName(std::filesystem::path{ file }.filename().wstring(), PROCESS_QUERY_INFORMATION | SYNCHRONIZE | handleAccess);
|
||||
|
||||
if (handles.empty())
|
||||
return std::nullopt;
|
||||
|
||||
ProcessInfo result;
|
||||
result.processID = GetProcessId(handles[0].get());
|
||||
result.processHandle = std::move(handles[0]);
|
||||
|
||||
return result;
|
||||
}
|
||||
// Fallback to ActionRunner when shell route fails and return process info if available
|
||||
std::optional<ProcessInfo> RunNonElevatedFailsafe(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir, DWORD handleAccess = 0);
|
||||
|
||||
// Run command with the same elevation, returns true if succeeded
|
||||
inline bool run_same_elevation(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr)
|
||||
{
|
||||
auto executable_args = L"\"" + file + L"\"";
|
||||
if (!params.empty())
|
||||
{
|
||||
executable_args += L" " + params;
|
||||
}
|
||||
|
||||
STARTUPINFO si = { sizeof(STARTUPINFO) };
|
||||
PROCESS_INFORMATION pi = { 0 };
|
||||
|
||||
auto succeeded = CreateProcessW(file.c_str(),
|
||||
&executable_args[0],
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
0,
|
||||
nullptr,
|
||||
workingDir,
|
||||
&si,
|
||||
&pi);
|
||||
|
||||
if (succeeded)
|
||||
{
|
||||
if (pi.hProcess)
|
||||
{
|
||||
if (returnPid)
|
||||
{
|
||||
*returnPid = GetProcessId(pi.hProcess);
|
||||
}
|
||||
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
|
||||
if (pi.hThread)
|
||||
{
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
bool run_same_elevation(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr);
|
||||
|
||||
// Returns true if the current process is running from administrator account
|
||||
// The function returns true in case of error since we want to return false
|
||||
// only in case of a positive verification that the user is not an admin.
|
||||
inline bool check_user_is_admin()
|
||||
{
|
||||
auto freeMemory = [](PSID pSID, PTOKEN_GROUPS pGroupInfo) {
|
||||
if (pSID)
|
||||
{
|
||||
FreeSid(pSID);
|
||||
}
|
||||
if (pGroupInfo)
|
||||
{
|
||||
GlobalFree(pGroupInfo);
|
||||
}
|
||||
};
|
||||
bool check_user_is_admin();
|
||||
|
||||
HANDLE hToken;
|
||||
DWORD dwSize = 0;
|
||||
PTOKEN_GROUPS pGroupInfo;
|
||||
SID_IDENTIFIER_AUTHORITY SIDAuth = SECURITY_NT_AUTHORITY;
|
||||
PSID pSID = NULL;
|
||||
bool IsProcessOfWindowElevated(HWND window);
|
||||
|
||||
// Open a handle to the access token for the calling process.
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Call GetTokenInformation to get the buffer size.
|
||||
if (!GetTokenInformation(hToken, TokenGroups, NULL, dwSize, &dwSize))
|
||||
{
|
||||
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate the buffer.
|
||||
pGroupInfo = static_cast<PTOKEN_GROUPS>(GlobalAlloc(GPTR, dwSize));
|
||||
|
||||
// Call GetTokenInformation again to get the group information.
|
||||
if (!GetTokenInformation(hToken, TokenGroups, pGroupInfo, dwSize, &dwSize))
|
||||
{
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create a SID for the BUILTIN\Administrators group.
|
||||
if (!AllocateAndInitializeSid(&SIDAuth, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &pSID))
|
||||
{
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Loop through the group SIDs looking for the administrator SID.
|
||||
for (DWORD i = 0; i < pGroupInfo->GroupCount; ++i)
|
||||
{
|
||||
if (EqualSid(pSID, pGroupInfo->Groups[i].Sid))
|
||||
{
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
freeMemory(pSID, pGroupInfo);
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool IsProcessOfWindowElevated(HWND window)
|
||||
{
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
if (!pid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
wil::unique_handle hProcess{ OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
FALSE,
|
||||
pid) };
|
||||
|
||||
wil::unique_handle token;
|
||||
|
||||
if (OpenProcessToken(hProcess.get(), TOKEN_QUERY, &token))
|
||||
{
|
||||
TOKEN_ELEVATION elevation;
|
||||
DWORD size;
|
||||
if (GetTokenInformation(token.get(), TokenElevation, &elevation, sizeof(elevation), &size))
|
||||
{
|
||||
return elevation.TokenIsElevated != 0;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
63
src/common/utils/excluded_apps.cpp
Normal file
63
src/common/utils/excluded_apps.cpp
Normal file
@@ -0,0 +1,63 @@
|
||||
#include "pch.h"
|
||||
#include "excluded_apps.h"
|
||||
|
||||
bool find_app_name_in_path(const std::wstring& where, const std::vector<std::wstring>& what)
|
||||
{
|
||||
for (const auto& row : what)
|
||||
{
|
||||
const auto pos = where.rfind(row);
|
||||
const auto last_slash = where.rfind('\\');
|
||||
// Check that row occurs in where, and its last occurrence contains the first character after the last backslash.
|
||||
if (pos != std::wstring::npos && pos <= last_slash + 1 && pos + row.length() > last_slash)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool find_folder_in_path(const std::wstring& where, const std::vector<std::wstring>& what)
|
||||
{
|
||||
for (const auto& row : what)
|
||||
{
|
||||
if (where.rfind(row) != std::wstring::npos)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool check_excluded_app_with_title(const HWND& hwnd, const std::vector<std::wstring>& excludedApps)
|
||||
{
|
||||
WCHAR title[MAX_TITLE_LENGTH];
|
||||
const int len = GetWindowTextW(hwnd, title, MAX_TITLE_LENGTH);
|
||||
if (len <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring titleStr(title);
|
||||
CharUpperBuffW(titleStr.data(), static_cast<DWORD>(titleStr.length()));
|
||||
|
||||
for (const auto& app : excludedApps)
|
||||
{
|
||||
if (titleStr.contains(app))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool check_excluded_app(const HWND& hwnd, const std::wstring& processPath, const std::vector<std::wstring>& excludedApps)
|
||||
{
|
||||
bool res = find_app_name_in_path(processPath, excludedApps);
|
||||
|
||||
if (!res)
|
||||
{
|
||||
res = check_excluded_app_with_title(hwnd, excludedApps);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -1,67 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <windows.h>
|
||||
|
||||
// Checks if a process path is included in a list of strings.
|
||||
inline bool find_app_name_in_path(const std::wstring& where, const std::vector<std::wstring>& what)
|
||||
{
|
||||
for (const auto& row : what)
|
||||
{
|
||||
const auto pos = where.rfind(row);
|
||||
const auto last_slash = where.rfind('\\');
|
||||
//Check that row occurs in where, and its last occurrence contains in itself the first character after the last backslash.
|
||||
if (pos != std::wstring::npos && pos <= last_slash + 1 && pos + row.length() > last_slash)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool find_app_name_in_path(const std::wstring& where, const std::vector<std::wstring>& what);
|
||||
|
||||
inline bool find_folder_in_path(const std::wstring& where, const std::vector<std::wstring>& what)
|
||||
{
|
||||
for (const auto& row : what)
|
||||
{
|
||||
const auto pos = where.rfind(row);
|
||||
if (pos != std::wstring::npos)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool find_folder_in_path(const std::wstring& where, const std::vector<std::wstring>& what);
|
||||
|
||||
#define MAX_TITLE_LENGTH 255
|
||||
inline bool check_excluded_app_with_title(const HWND& hwnd, const std::vector<std::wstring>& excludedApps)
|
||||
{
|
||||
WCHAR title[MAX_TITLE_LENGTH];
|
||||
int len = GetWindowTextW(hwnd, title, MAX_TITLE_LENGTH);
|
||||
if (len <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool check_excluded_app_with_title(const HWND& hwnd, const std::vector<std::wstring>& excludedApps);
|
||||
|
||||
std::wstring titleStr(title);
|
||||
CharUpperBuffW(titleStr.data(), static_cast<DWORD>(titleStr.length()));
|
||||
|
||||
for (const auto& app : excludedApps)
|
||||
{
|
||||
if (titleStr.contains(app))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool check_excluded_app(const HWND& hwnd, const std::wstring& processPath, const std::vector<std::wstring>& excludedApps)
|
||||
{
|
||||
bool res = find_app_name_in_path(processPath, excludedApps);
|
||||
|
||||
if (!res)
|
||||
{
|
||||
res = check_excluded_app_with_title(hwnd, excludedApps);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
bool check_excluded_app(const HWND& hwnd, const std::wstring& processPath, const std::vector<std::wstring>& excludedApps);
|
||||
|
||||
100
src/common/utils/exec.cpp
Normal file
100
src/common/utils/exec.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
#include "pch.h"
|
||||
#include "exec.h"
|
||||
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
|
||||
std::optional<std::string> exec_and_read_output(const std::wstring_view command, DWORD timeout_ms)
|
||||
{
|
||||
SECURITY_ATTRIBUTES saAttr{ sizeof(saAttr) };
|
||||
saAttr.bInheritHandle = false;
|
||||
|
||||
constexpr size_t bufferSize = 4096;
|
||||
// We must use a named pipe for async I/O
|
||||
char pipename[MAX_PATH + 1];
|
||||
if (!GetTempFileNameA(R"(\\.\pipe\)", "tmp", 1, pipename))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
wil::unique_handle readPipe{
|
||||
CreateNamedPipeA(pipename, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, PIPE_UNLIMITED_INSTANCES, bufferSize, bufferSize, 0, &saAttr)
|
||||
};
|
||||
|
||||
saAttr.bInheritHandle = true;
|
||||
wil::unique_handle writePipe{
|
||||
CreateFileA(pipename, GENERIC_WRITE, 0, &saAttr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)
|
||||
};
|
||||
|
||||
if (!readPipe || !writePipe)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
PROCESS_INFORMATION piProcInfo{};
|
||||
STARTUPINFOW siStartInfo{ sizeof(siStartInfo) };
|
||||
|
||||
siStartInfo.hStdError = writePipe.get();
|
||||
siStartInfo.hStdOutput = writePipe.get();
|
||||
siStartInfo.dwFlags |= STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
|
||||
siStartInfo.wShowWindow = SW_HIDE;
|
||||
|
||||
std::wstring cmdLine{ command };
|
||||
if (!CreateProcessW(nullptr,
|
||||
cmdLine.data(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
true,
|
||||
NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE,
|
||||
nullptr,
|
||||
nullptr,
|
||||
&siStartInfo,
|
||||
&piProcInfo))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
// Child process inherited the write end of the pipe, we can close it now
|
||||
writePipe.reset();
|
||||
|
||||
auto closeProcessHandles = wil::scope_exit([&] {
|
||||
CloseHandle(piProcInfo.hThread);
|
||||
CloseHandle(piProcInfo.hProcess);
|
||||
});
|
||||
|
||||
std::string childOutput;
|
||||
bool processExited = false;
|
||||
for (;;)
|
||||
{
|
||||
char buffer[bufferSize];
|
||||
DWORD gotBytes = 0;
|
||||
wil::unique_handle ioEvent{ CreateEventW(nullptr, true, false, nullptr) };
|
||||
OVERLAPPED overlapped{ .hEvent = ioEvent.get() };
|
||||
ReadFile(readPipe.get(), buffer, sizeof(buffer), nullptr, &overlapped);
|
||||
|
||||
const std::array<HANDLE, 2> handlesToWait = { overlapped.hEvent, piProcInfo.hProcess };
|
||||
switch (WaitForMultipleObjects(1 + !processExited, handlesToWait.data(), false, timeout_ms))
|
||||
{
|
||||
case WAIT_OBJECT_0 + 1:
|
||||
if (!processExited)
|
||||
{
|
||||
// When the process exits, we can reduce timeout and read the rest of the output w/o possibly big timeout
|
||||
timeout_ms = 1000;
|
||||
processExited = true;
|
||||
closeProcessHandles.reset();
|
||||
}
|
||||
[[fallthrough]];
|
||||
case WAIT_OBJECT_0:
|
||||
if (GetOverlappedResultEx(readPipe.get(), &overlapped, &gotBytes, timeout_ms, true))
|
||||
{
|
||||
childOutput += std::string_view{ buffer, gotBytes };
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
default:
|
||||
goto exit;
|
||||
}
|
||||
}
|
||||
exit:
|
||||
CancelIo(readPipe.get());
|
||||
return childOutput;
|
||||
}
|
||||
@@ -15,94 +15,4 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
inline std::optional<std::string> exec_and_read_output(const std::wstring_view command, DWORD timeout_ms = 30000)
|
||||
{
|
||||
SECURITY_ATTRIBUTES saAttr{ sizeof(saAttr) };
|
||||
saAttr.bInheritHandle = false;
|
||||
|
||||
constexpr size_t bufferSize = 4096;
|
||||
// We must use a named pipe for async I/O
|
||||
char pipename[MAX_PATH + 1];
|
||||
if (!GetTempFileNameA(R"(\\.\pipe\)", "tmp", 1, pipename))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
wil::unique_handle readPipe{ CreateNamedPipeA(pipename, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, PIPE_UNLIMITED_INSTANCES, bufferSize, bufferSize, 0, &saAttr) };
|
||||
|
||||
saAttr.bInheritHandle = true;
|
||||
wil::unique_handle writePipe{ CreateFileA(pipename, GENERIC_WRITE, 0, &saAttr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) };
|
||||
|
||||
if (!readPipe || !writePipe)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
PROCESS_INFORMATION piProcInfo{};
|
||||
STARTUPINFOW siStartInfo{ sizeof(siStartInfo) };
|
||||
|
||||
siStartInfo.hStdError = writePipe.get();
|
||||
siStartInfo.hStdOutput = writePipe.get();
|
||||
siStartInfo.dwFlags |= STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
|
||||
siStartInfo.wShowWindow = SW_HIDE;
|
||||
|
||||
std::wstring cmdLine{ command };
|
||||
if (!CreateProcessW(nullptr,
|
||||
cmdLine.data(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
true,
|
||||
NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE,
|
||||
nullptr,
|
||||
nullptr,
|
||||
&siStartInfo,
|
||||
&piProcInfo))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
// Child process inherited the write end of the pipe, we can close it now
|
||||
writePipe.reset();
|
||||
|
||||
auto closeProcessHandles = wil::scope_exit([&] {
|
||||
CloseHandle(piProcInfo.hThread);
|
||||
CloseHandle(piProcInfo.hProcess);
|
||||
});
|
||||
|
||||
std::string childOutput;
|
||||
bool processExited = false;
|
||||
for (;;)
|
||||
{
|
||||
char buffer[bufferSize];
|
||||
DWORD gotBytes = 0;
|
||||
wil::unique_handle IOEvent{ CreateEventW(nullptr, true, false, nullptr) };
|
||||
OVERLAPPED overlapped{ .hEvent = IOEvent.get() };
|
||||
ReadFile(readPipe.get(), buffer, sizeof(buffer), nullptr, &overlapped);
|
||||
|
||||
const std::array<HANDLE, 2> handlesToWait = { overlapped.hEvent, piProcInfo.hProcess };
|
||||
switch (WaitForMultipleObjects(1 + !processExited, handlesToWait.data(), false, timeout_ms))
|
||||
{
|
||||
case WAIT_OBJECT_0 + 1:
|
||||
if (!processExited)
|
||||
{
|
||||
// When the process exits, we can reduce timeout and read the rest of the output w/o possibly big timeout
|
||||
timeout_ms = 1000;
|
||||
processExited = true;
|
||||
closeProcessHandles.reset();
|
||||
}
|
||||
[[fallthrough]];
|
||||
case WAIT_OBJECT_0:
|
||||
if (GetOverlappedResultEx(readPipe.get(), &overlapped, &gotBytes, timeout_ms, true))
|
||||
{
|
||||
childOutput += std::string_view{ buffer, gotBytes };
|
||||
break;
|
||||
}
|
||||
// Timeout
|
||||
[[fallthrough]];
|
||||
default:
|
||||
goto exit;
|
||||
}
|
||||
}
|
||||
exit:
|
||||
CancelIo(readPipe.get());
|
||||
return childOutput;
|
||||
}
|
||||
std::optional<std::string> exec_and_read_output(const std::wstring_view command, DWORD timeout_ms = 30000);
|
||||
|
||||
12
src/common/utils/game_mode.cpp
Normal file
12
src/common/utils/game_mode.cpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#include "pch.h"
|
||||
#include "game_mode.h"
|
||||
|
||||
bool detect_game_mode()
|
||||
{
|
||||
QUERY_USER_NOTIFICATION_STATE notification_state;
|
||||
if (SHQueryUserNotificationState(¬ification_state) != S_OK)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return notification_state == QUNS_RUNNING_D3D_FULL_SCREEN;
|
||||
}
|
||||
@@ -1,12 +1,4 @@
|
||||
#pragma once
|
||||
#include <shellapi.h>
|
||||
|
||||
inline bool detect_game_mode()
|
||||
{
|
||||
QUERY_USER_NOTIFICATION_STATE notification_state;
|
||||
if (SHQueryUserNotificationState(¬ification_state) != S_OK)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return (notification_state == QUNS_RUNNING_D3D_FULL_SCREEN);
|
||||
}
|
||||
bool detect_game_mode();
|
||||
|
||||
236
src/common/utils/gpo.cpp
Normal file
236
src/common/utils/gpo.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
#include "pch.h"
|
||||
#include "gpo.h"
|
||||
|
||||
namespace powertoys_gpo
|
||||
{
|
||||
std::optional<std::wstring> readRegistryStringValue(HKEY hRootKey, const std::wstring& subKey, const std::wstring& value_name, const bool is_multi_line_text)
|
||||
{
|
||||
// Set value type
|
||||
DWORD reg_value_type = REG_SZ;
|
||||
DWORD reg_flags = RRF_RT_REG_SZ;
|
||||
if (is_multi_line_text)
|
||||
{
|
||||
reg_value_type = REG_MULTI_SZ;
|
||||
reg_flags = RRF_RT_REG_MULTI_SZ;
|
||||
}
|
||||
|
||||
DWORD string_buffer_capacity;
|
||||
// Request required buffer capacity / string length
|
||||
if (RegGetValueW(hRootKey, subKey.c_str(), value_name.c_str(), reg_flags, ®_value_type, NULL, &string_buffer_capacity) != ERROR_SUCCESS)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
else if (string_buffer_capacity == 0)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// RegGetValueW overshoots sometimes. Use a buffer first to not have characters past the string end.
|
||||
wchar_t* temp_buffer = new wchar_t[string_buffer_capacity / sizeof(wchar_t) + 1];
|
||||
// Read string
|
||||
if (RegGetValueW(hRootKey, subKey.c_str(), value_name.c_str(), reg_flags, ®_value_type, temp_buffer, &string_buffer_capacity) != ERROR_SUCCESS)
|
||||
{
|
||||
delete[] temp_buffer;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Convert buffer to std::wstring
|
||||
std::wstring string_value = L"";
|
||||
if (reg_value_type == REG_MULTI_SZ)
|
||||
{
|
||||
// If it is REG_MULTI_SZ handle this way
|
||||
wchar_t* currentString = temp_buffer;
|
||||
while (*currentString != L'\0')
|
||||
{
|
||||
// If first entry then assign the string, else add to the string
|
||||
string_value = (string_value == L"") ? currentString : (string_value + L"\r\n" + currentString);
|
||||
currentString += wcslen(currentString) + 1; // Move to the next string
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If it is REG_SZ handle this way
|
||||
string_value = temp_buffer;
|
||||
}
|
||||
|
||||
// delete buffer, return string value
|
||||
delete[] temp_buffer;
|
||||
return string_value;
|
||||
}
|
||||
|
||||
gpo_rule_configured_t getConfiguredValue(const std::wstring& registry_value_name)
|
||||
{
|
||||
HKEY key{};
|
||||
DWORD value = 0xFFFFFFFE;
|
||||
DWORD valueSize = sizeof(value);
|
||||
|
||||
bool machine_key_found = true;
|
||||
if (auto res = RegOpenKeyExW(POLICIES_SCOPE_MACHINE, POLICIES_PATH.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
machine_key_found = false;
|
||||
}
|
||||
|
||||
if (machine_key_found)
|
||||
{
|
||||
// If the path was found in the machine, we need to check if the value for the policy exists.
|
||||
auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &valueSize);
|
||||
|
||||
RegCloseKey(key);
|
||||
|
||||
if (res != ERROR_SUCCESS)
|
||||
{
|
||||
// Value not found on the path.
|
||||
machine_key_found = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!machine_key_found)
|
||||
{
|
||||
// If there's no value found on the machine scope, try to get it from the user scope.
|
||||
if (auto res = RegOpenKeyExW(POLICIES_SCOPE_USER, POLICIES_PATH.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
if (res == ERROR_FILE_NOT_FOUND)
|
||||
{
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
return gpo_rule_configured_unavailable;
|
||||
}
|
||||
auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &valueSize);
|
||||
RegCloseKey(key);
|
||||
|
||||
if (res != ERROR_SUCCESS)
|
||||
{
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case 0:
|
||||
return gpo_rule_configured_disabled;
|
||||
case 1:
|
||||
return gpo_rule_configured_enabled;
|
||||
default:
|
||||
return gpo_rule_configured_wrong_value;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::wstring> getPolicyListValue(const std::wstring& registry_list_path, const std::wstring& registry_list_value_name)
|
||||
{
|
||||
// This function returns the value of an entry of a policy list. The user scope is only checked, if the list is not enabled for the machine to not mix the lists.
|
||||
|
||||
HKEY key{};
|
||||
|
||||
// Try to read from the machine list.
|
||||
bool machine_list_found = false;
|
||||
if (RegOpenKeyExW(POLICIES_SCOPE_MACHINE, registry_list_path.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
machine_list_found = true;
|
||||
RegCloseKey(key);
|
||||
|
||||
// If the path exists in the machine registry, we try to read the value.
|
||||
auto regValueData = readRegistryStringValue(POLICIES_SCOPE_MACHINE, registry_list_path, registry_list_value_name);
|
||||
|
||||
if (regValueData.has_value())
|
||||
{
|
||||
// Return the value from the machine list.
|
||||
return *regValueData;
|
||||
}
|
||||
}
|
||||
|
||||
// If no list exists for machine, we try to read from the user list.
|
||||
if (!machine_list_found)
|
||||
{
|
||||
if (RegOpenKeyExW(POLICIES_SCOPE_USER, registry_list_path.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
RegCloseKey(key);
|
||||
|
||||
// If the path exists in the user registry, we try to read the value.
|
||||
auto regValueData = readRegistryStringValue(POLICIES_SCOPE_USER, registry_list_path, registry_list_value_name);
|
||||
|
||||
if (regValueData.has_value())
|
||||
{
|
||||
// Return the value from the user list.
|
||||
return *regValueData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No list exists for machine and user, or no value was found in the list, or an error ocurred while reading the value.
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
gpo_rule_configured_t getUtilityEnabledValue(const std::wstring& utility_name)
|
||||
{
|
||||
auto individual_value = getConfiguredValue(utility_name);
|
||||
|
||||
if (individual_value == gpo_rule_configured_disabled || individual_value == gpo_rule_configured_enabled)
|
||||
{
|
||||
return individual_value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return getConfiguredValue(POLICY_CONFIGURE_ENABLED_GLOBAL_ALL_UTILITIES);
|
||||
}
|
||||
}
|
||||
|
||||
gpo_rule_configured_t getRunPluginEnabledValue(std::string pluginID)
|
||||
{
|
||||
if (pluginID == "" || pluginID == " ")
|
||||
{
|
||||
// this plugin id can't exist in the registry
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
|
||||
std::wstring plugin_id(pluginID.begin(), pluginID.end());
|
||||
auto individual_plugin_setting = getPolicyListValue(POWER_LAUNCHER_INDIVIDUAL_PLUGIN_ENABLED_LIST_PATH, plugin_id);
|
||||
|
||||
if (individual_plugin_setting.has_value())
|
||||
{
|
||||
if (*individual_plugin_setting == L"0")
|
||||
{
|
||||
// force disabled
|
||||
return gpo_rule_configured_disabled;
|
||||
}
|
||||
else if (*individual_plugin_setting == L"1")
|
||||
{
|
||||
// force enabled
|
||||
return gpo_rule_configured_enabled;
|
||||
}
|
||||
else if (*individual_plugin_setting == L"2")
|
||||
{
|
||||
// user takes control
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
else
|
||||
{
|
||||
return gpo_rule_configured_wrong_value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no individual plugin policy exists, we check the policy with the setting for all plugins.
|
||||
return getConfiguredValue(POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER_ALL_PLUGINS);
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring getConfiguredMwbPolicyDefinedIpMappingRules()
|
||||
{
|
||||
// Important: HKLM has priority over HKCU
|
||||
auto mapping_rules = readRegistryStringValue(HKEY_LOCAL_MACHINE, POLICIES_PATH, POLICY_MWB_POLICY_DEFINED_IP_MAPPING_RULES, true);
|
||||
if (!mapping_rules.has_value())
|
||||
{
|
||||
mapping_rules = readRegistryStringValue(HKEY_CURRENT_USER, POLICIES_PATH, POLICY_MWB_POLICY_DEFINED_IP_MAPPING_RULES, true);
|
||||
}
|
||||
|
||||
// return value
|
||||
if (mapping_rules.has_value())
|
||||
{
|
||||
return mapping_rules.value();
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,176 +105,10 @@ namespace powertoys_gpo
|
||||
|
||||
// Methods used for reading the registry
|
||||
#pragma region ReadRegistryMethods
|
||||
inline std::optional<std::wstring> readRegistryStringValue(HKEY hRootKey, const std::wstring& subKey, const std::wstring& value_name, const bool is_multi_line_text = false)
|
||||
{
|
||||
// Set value type
|
||||
DWORD reg_value_type = REG_SZ;
|
||||
DWORD reg_flags = RRF_RT_REG_SZ;
|
||||
if (is_multi_line_text)
|
||||
{
|
||||
reg_value_type = REG_MULTI_SZ;
|
||||
reg_flags = RRF_RT_REG_MULTI_SZ;
|
||||
}
|
||||
|
||||
DWORD string_buffer_capacity;
|
||||
// Request required buffer capacity / string length
|
||||
if (RegGetValueW(hRootKey, subKey.c_str(), value_name.c_str(), reg_flags, ®_value_type, NULL, &string_buffer_capacity) != ERROR_SUCCESS)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
else if (string_buffer_capacity == 0)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// RegGetValueW overshoots sometimes. Use a buffer first to not have characters past the string end.
|
||||
wchar_t* temp_buffer = new wchar_t[string_buffer_capacity / sizeof(wchar_t) + 1];
|
||||
// Read string
|
||||
if (RegGetValueW(hRootKey, subKey.c_str(), value_name.c_str(), reg_flags, ®_value_type, temp_buffer, &string_buffer_capacity) != ERROR_SUCCESS)
|
||||
{
|
||||
delete temp_buffer;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Convert buffer to std::wstring
|
||||
std::wstring string_value = L"";
|
||||
if (reg_value_type == REG_MULTI_SZ)
|
||||
{
|
||||
// If it is REG_MULTI_SZ handle this way
|
||||
wchar_t* currentString = temp_buffer;
|
||||
while (*currentString != L'\0')
|
||||
{
|
||||
// If first entry then assign the string, else add to the string
|
||||
string_value = (string_value == L"") ? currentString : (string_value + L"\r\n" + currentString);
|
||||
currentString += wcslen(currentString) + 1; // Move to the next string
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If it is REG_SZ handle this way
|
||||
string_value = temp_buffer;
|
||||
}
|
||||
|
||||
// delete buffer, return string value
|
||||
delete temp_buffer;
|
||||
return string_value;
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getConfiguredValue(const std::wstring& registry_value_name)
|
||||
{
|
||||
HKEY key{};
|
||||
DWORD value = 0xFFFFFFFE;
|
||||
DWORD valueSize = sizeof(value);
|
||||
|
||||
bool machine_key_found = true;
|
||||
if (auto res = RegOpenKeyExW(POLICIES_SCOPE_MACHINE, POLICIES_PATH.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
machine_key_found = false;
|
||||
}
|
||||
|
||||
if (machine_key_found)
|
||||
{
|
||||
// If the path was found in the machine, we need to check if the value for the policy exists.
|
||||
auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &valueSize);
|
||||
|
||||
RegCloseKey(key);
|
||||
|
||||
if (res != ERROR_SUCCESS)
|
||||
{
|
||||
// Value not found on the path.
|
||||
machine_key_found = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!machine_key_found)
|
||||
{
|
||||
// If there's no value found on the machine scope, try to get it from the user scope.
|
||||
if (auto res = RegOpenKeyExW(POLICIES_SCOPE_USER, POLICIES_PATH.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
if (res == ERROR_FILE_NOT_FOUND)
|
||||
{
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
return gpo_rule_configured_unavailable;
|
||||
}
|
||||
auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &valueSize);
|
||||
RegCloseKey(key);
|
||||
|
||||
if (res != ERROR_SUCCESS)
|
||||
{
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case 0:
|
||||
return gpo_rule_configured_disabled;
|
||||
case 1:
|
||||
return gpo_rule_configured_enabled;
|
||||
default:
|
||||
return gpo_rule_configured_wrong_value;
|
||||
}
|
||||
}
|
||||
|
||||
inline std::optional<std::wstring> getPolicyListValue(const std::wstring& registry_list_path, const std::wstring& registry_list_value_name)
|
||||
{
|
||||
// This function returns the value of an entry of a policy list. The user scope is only checked, if the list is not enabled for the machine to not mix the lists.
|
||||
|
||||
HKEY key{};
|
||||
|
||||
// Try to read from the machine list.
|
||||
bool machine_list_found = false;
|
||||
if (RegOpenKeyExW(POLICIES_SCOPE_MACHINE, registry_list_path.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
machine_list_found = true;
|
||||
RegCloseKey(key);
|
||||
|
||||
// If the path exists in the machine registry, we try to read the value.
|
||||
auto regValueData = readRegistryStringValue(POLICIES_SCOPE_MACHINE, registry_list_path, registry_list_value_name);
|
||||
|
||||
if (regValueData.has_value())
|
||||
{
|
||||
// Return the value from the machine list.
|
||||
return *regValueData;
|
||||
}
|
||||
}
|
||||
|
||||
// If no list exists for machine, we try to read from the user list.
|
||||
if (!machine_list_found)
|
||||
{
|
||||
if (RegOpenKeyExW(POLICIES_SCOPE_USER, registry_list_path.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
RegCloseKey(key);
|
||||
|
||||
// If the path exists in the user registry, we try to read the value.
|
||||
auto regValueData = readRegistryStringValue(POLICIES_SCOPE_USER, registry_list_path, registry_list_value_name);
|
||||
|
||||
if (regValueData.has_value())
|
||||
{
|
||||
// Return the value from the user list.
|
||||
return *regValueData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No list exists for machine and user, or no value was found in the list, or an error ocurred while reading the value.
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getUtilityEnabledValue(const std::wstring& utility_name)
|
||||
{
|
||||
auto individual_value = getConfiguredValue(utility_name);
|
||||
|
||||
if (individual_value == gpo_rule_configured_disabled || individual_value == gpo_rule_configured_enabled)
|
||||
{
|
||||
return individual_value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return getConfiguredValue(POLICY_CONFIGURE_ENABLED_GLOBAL_ALL_UTILITIES);
|
||||
}
|
||||
}
|
||||
std::optional<std::wstring> readRegistryStringValue(HKEY hRootKey, const std::wstring& subKey, const std::wstring& value_name, const bool is_multi_line_text = false);
|
||||
gpo_rule_configured_t getConfiguredValue(const std::wstring& registry_value_name);
|
||||
std::optional<std::wstring> getPolicyListValue(const std::wstring& registry_list_path, const std::wstring& registry_list_value_name);
|
||||
gpo_rule_configured_t getUtilityEnabledValue(const std::wstring& utility_name);
|
||||
#pragma endregion ReadRegistryMethods
|
||||
|
||||
// Utility enabled state policies
|
||||
@@ -544,45 +378,7 @@ namespace powertoys_gpo
|
||||
return getConfiguredValue(POLICY_CONFIGURE_RUN_AT_STARTUP);
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getRunPluginEnabledValue(std::string pluginID)
|
||||
{
|
||||
if (pluginID == "" || pluginID == " ")
|
||||
{
|
||||
// this plugin id can't exist in the registry
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
|
||||
std::wstring plugin_id(pluginID.begin(), pluginID.end());
|
||||
auto individual_plugin_setting = getPolicyListValue(POWER_LAUNCHER_INDIVIDUAL_PLUGIN_ENABLED_LIST_PATH, plugin_id);
|
||||
|
||||
if (individual_plugin_setting.has_value())
|
||||
{
|
||||
if (*individual_plugin_setting == L"0")
|
||||
{
|
||||
// force disabled
|
||||
return gpo_rule_configured_disabled;
|
||||
}
|
||||
else if (*individual_plugin_setting == L"1")
|
||||
{
|
||||
// force enabled
|
||||
return gpo_rule_configured_enabled;
|
||||
}
|
||||
else if (*individual_plugin_setting == L"2")
|
||||
{
|
||||
// user takes control
|
||||
return gpo_rule_configured_not_configured;
|
||||
}
|
||||
else
|
||||
{
|
||||
return gpo_rule_configured_wrong_value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no individual plugin policy exists, we check the policy with the setting for all plugins.
|
||||
return getConfiguredValue(POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER_ALL_PLUGINS);
|
||||
}
|
||||
}
|
||||
gpo_rule_configured_t getRunPluginEnabledValue(std::string pluginID);
|
||||
|
||||
inline gpo_rule_configured_t getAllowedAdvancedPasteOnlineAIModelsValue()
|
||||
{
|
||||
@@ -664,25 +460,7 @@ namespace powertoys_gpo
|
||||
return getConfiguredValue(POLICY_MWB_DISABLE_USER_DEFINED_IP_MAPPING_RULES);
|
||||
}
|
||||
|
||||
inline std::wstring getConfiguredMwbPolicyDefinedIpMappingRules()
|
||||
{
|
||||
// Important: HKLM has priority over HKCU
|
||||
auto mapping_rules = readRegistryStringValue(HKEY_LOCAL_MACHINE, POLICIES_PATH, POLICY_MWB_POLICY_DEFINED_IP_MAPPING_RULES, true);
|
||||
if (!mapping_rules.has_value())
|
||||
{
|
||||
mapping_rules = readRegistryStringValue(HKEY_CURRENT_USER, POLICIES_PATH, POLICY_MWB_POLICY_DEFINED_IP_MAPPING_RULES, true);
|
||||
}
|
||||
|
||||
// return value
|
||||
if (mapping_rules.has_value())
|
||||
{
|
||||
return mapping_rules.value();
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
}
|
||||
std::wstring getConfiguredMwbPolicyDefinedIpMappingRules();
|
||||
|
||||
inline gpo_rule_configured_t getConfiguredNewPlusHideTemplateFilenameExtensionValue()
|
||||
{
|
||||
|
||||
50
src/common/utils/json.cpp
Normal file
50
src/common/utils/json.cpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#include "pch.h"
|
||||
#include "json.h"
|
||||
|
||||
namespace json
|
||||
{
|
||||
std::optional<JsonObject> from_file(std::wstring_view file_name)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::ifstream file(file_name.data(), std::ios::binary);
|
||||
if (file.is_open())
|
||||
{
|
||||
using iterator = std::istreambuf_iterator<char>;
|
||||
std::string objStr{ iterator{ file }, iterator{} };
|
||||
return JsonValue::Parse(winrt::to_hstring(objStr)).GetObjectW();
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
void to_file(std::wstring_view file_name, const JsonObject& obj)
|
||||
{
|
||||
std::wstring objStr{ obj.Stringify().c_str() };
|
||||
std::ofstream{ file_name.data(), std::ios::binary } << winrt::to_string(objStr);
|
||||
}
|
||||
|
||||
bool has(const json::JsonObject& o, std::wstring_view name, const json::JsonValueType type)
|
||||
{
|
||||
return o.HasKey(name) && o.GetNamedValue(name).ValueType() == type;
|
||||
}
|
||||
|
||||
JsonValue value(const bool boolean)
|
||||
{
|
||||
return json::JsonValue::CreateBooleanValue(boolean);
|
||||
}
|
||||
|
||||
JsonValue value(JsonObject valueObject)
|
||||
{
|
||||
return valueObject.as<JsonValue>();
|
||||
}
|
||||
|
||||
JsonValue value(JsonValue valueObject)
|
||||
{
|
||||
return valueObject; // identity function overload for convenience
|
||||
}
|
||||
}
|
||||
@@ -11,38 +11,11 @@ namespace json
|
||||
{
|
||||
using namespace winrt::Windows::Data::Json;
|
||||
|
||||
inline std::optional<JsonObject> from_file(std::wstring_view file_name)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::ifstream file(file_name.data(), std::ios::binary);
|
||||
if (file.is_open())
|
||||
{
|
||||
using isbi = std::istreambuf_iterator<char>;
|
||||
std::string obj_str{ isbi{ file }, isbi{} };
|
||||
return JsonValue::Parse(winrt::to_hstring(obj_str)).GetObjectW();
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
std::optional<JsonObject> from_file(std::wstring_view file_name);
|
||||
|
||||
inline void to_file(std::wstring_view file_name, const JsonObject& obj)
|
||||
{
|
||||
std::wstring obj_str{ obj.Stringify().c_str() };
|
||||
std::ofstream{ file_name.data(), std::ios::binary } << winrt::to_string(obj_str);
|
||||
}
|
||||
void to_file(std::wstring_view file_name, const JsonObject& obj);
|
||||
|
||||
inline bool has(
|
||||
const json::JsonObject& o,
|
||||
std::wstring_view name,
|
||||
const json::JsonValueType type = JsonValueType::Object)
|
||||
{
|
||||
return o.HasKey(name) && o.GetNamedValue(name).ValueType() == type;
|
||||
}
|
||||
bool has(const json::JsonObject& o, std::wstring_view name, const json::JsonValueType type = JsonValueType::Object);
|
||||
|
||||
template<typename T>
|
||||
inline std::enable_if_t<std::is_arithmetic_v<T>, JsonValue> value(const T arithmetic)
|
||||
@@ -56,20 +29,11 @@ namespace json
|
||||
return json::JsonValue::CreateStringValue(s);
|
||||
}
|
||||
|
||||
inline JsonValue value(const bool boolean)
|
||||
{
|
||||
return json::JsonValue::CreateBooleanValue(boolean);
|
||||
}
|
||||
JsonValue value(const bool boolean);
|
||||
|
||||
inline JsonValue value(JsonObject value)
|
||||
{
|
||||
return value.as<JsonValue>();
|
||||
}
|
||||
JsonValue value(JsonObject value);
|
||||
|
||||
inline JsonValue value(JsonValue value)
|
||||
{
|
||||
return value; // identity function overload for convenience
|
||||
}
|
||||
JsonValue value(JsonValue value);
|
||||
|
||||
template<typename T, typename D = std::optional<T>>
|
||||
requires std::constructible_from<std::optional<T>, D>
|
||||
|
||||
19
src/common/utils/language_helper.cpp
Normal file
19
src/common/utils/language_helper.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "pch.h"
|
||||
#include "language_helper.h"
|
||||
|
||||
namespace LanguageHelpers
|
||||
{
|
||||
std::wstring load_language()
|
||||
{
|
||||
std::filesystem::path languageJsonFilePath(PTSettingsHelper::get_root_save_folder_location() + L"\\language.json");
|
||||
|
||||
auto langJson = json::from_file(languageJsonFilePath.c_str());
|
||||
if (!langJson.has_value())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring language = langJson->GetNamedString(L"language", L"").c_str();
|
||||
return language;
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,5 @@
|
||||
|
||||
namespace LanguageHelpers
|
||||
{
|
||||
inline std::wstring load_language()
|
||||
{
|
||||
std::filesystem::path languageJsonFilePath(PTSettingsHelper::get_root_save_folder_location() + L"\\language.json");
|
||||
|
||||
auto langJson = json::from_file(languageJsonFilePath.c_str());
|
||||
if (!langJson.has_value())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring language = langJson->GetNamedString(L"language", L"").c_str();
|
||||
return language;
|
||||
}
|
||||
std::wstring load_language();
|
||||
}
|
||||
|
||||
96
src/common/utils/logger_helper.cpp
Normal file
96
src/common/utils/logger_helper.cpp
Normal file
@@ -0,0 +1,96 @@
|
||||
#include "pch.h"
|
||||
#include "logger_helper.h"
|
||||
|
||||
namespace LoggerHelpers
|
||||
{
|
||||
std::filesystem::path get_log_folder_path(std::wstring_view appPath)
|
||||
{
|
||||
std::filesystem::path logFolderPath(appPath);
|
||||
logFolderPath.append(LogSettings::logPath);
|
||||
logFolderPath.append(get_product_version());
|
||||
return logFolderPath;
|
||||
}
|
||||
|
||||
bool delete_old_log_folder(const std::filesystem::path& logFolderPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::filesystem::remove_all(logFolderPath);
|
||||
return true;
|
||||
}
|
||||
catch (std::filesystem::filesystem_error& e)
|
||||
{
|
||||
Logger::error("Failed to delete old log folder: {}", e.what());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool dir_exists(std::filesystem::path dir)
|
||||
{
|
||||
std::error_code err;
|
||||
auto entry = std::filesystem::directory_entry(dir, err);
|
||||
if (err.value())
|
||||
{
|
||||
Logger::error("Failed to create directory entry. {}", err.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
return entry.exists();
|
||||
}
|
||||
|
||||
bool delete_other_versions_log_folders(std::wstring_view appPath, const std::filesystem::path& currentVersionLogFolder)
|
||||
{
|
||||
bool result = true;
|
||||
std::filesystem::path logFolderPath(appPath);
|
||||
logFolderPath.append(LogSettings::logPath);
|
||||
|
||||
if (!dir_exists(logFolderPath))
|
||||
{
|
||||
Logger::trace("Directory {} does not exist", logFolderPath.string());
|
||||
return true;
|
||||
}
|
||||
|
||||
std::error_code err;
|
||||
auto folders = std::filesystem::directory_iterator(logFolderPath, err);
|
||||
if (err.value())
|
||||
{
|
||||
Logger::error("Failed to create directory iterator for {}. {}", logFolderPath.string(), err.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& dir : folders)
|
||||
{
|
||||
if (dir != currentVersionLogFolder)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::filesystem::remove_all(dir);
|
||||
}
|
||||
catch (std::filesystem::filesystem_error& e)
|
||||
{
|
||||
Logger::error("Failed to delete previous version log folder: {}", e.what());
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void init_logger(std::wstring moduleName, std::wstring internalPath, std::string loggerName)
|
||||
{
|
||||
std::filesystem::path rootFolder(PTSettingsHelper::get_module_save_folder_location(moduleName));
|
||||
rootFolder.append(internalPath);
|
||||
|
||||
auto currentFolder = rootFolder;
|
||||
currentFolder.append(LogSettings::logPath);
|
||||
currentFolder.append(get_product_version());
|
||||
|
||||
auto logsPath = currentFolder;
|
||||
logsPath.append(L"log.log");
|
||||
Logger::init(loggerName, logsPath.wstring(), PTSettingsHelper::get_log_settings_file_location());
|
||||
|
||||
delete_other_versions_log_folders(rootFolder.wstring(), currentFolder);
|
||||
}
|
||||
}
|
||||
@@ -1,99 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <common/version/version.h>
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
|
||||
namespace LoggerHelpers
|
||||
{
|
||||
inline std::filesystem::path get_log_folder_path(std::wstring_view appPath)
|
||||
{
|
||||
std::filesystem::path logFolderPath(appPath);
|
||||
logFolderPath.append(LogSettings::logPath);
|
||||
logFolderPath.append(get_product_version());
|
||||
return logFolderPath;
|
||||
}
|
||||
std::filesystem::path get_log_folder_path(std::wstring_view appPath);
|
||||
|
||||
inline bool delete_old_log_folder(const std::filesystem::path& logFolderPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::filesystem::remove_all(logFolderPath);
|
||||
return true;
|
||||
}
|
||||
catch (std::filesystem::filesystem_error& e)
|
||||
{
|
||||
Logger::error("Failed to delete old log folder: {}", e.what());
|
||||
}
|
||||
bool delete_old_log_folder(const std::filesystem::path& logFolderPath);
|
||||
|
||||
return false;
|
||||
}
|
||||
bool dir_exists(std::filesystem::path dir);
|
||||
|
||||
inline bool dir_exists(std::filesystem::path dir)
|
||||
{
|
||||
std::error_code err;
|
||||
auto entry = std::filesystem::directory_entry(dir, err);
|
||||
if (err.value())
|
||||
{
|
||||
Logger::error("Failed to create directory entry. {}", err.message());
|
||||
return false;
|
||||
}
|
||||
bool delete_other_versions_log_folders(std::wstring_view appPath, const std::filesystem::path& currentVersionLogFolder);
|
||||
|
||||
return entry.exists();
|
||||
}
|
||||
|
||||
inline bool delete_other_versions_log_folders(std::wstring_view appPath, const std::filesystem::path& currentVersionLogFolder)
|
||||
{
|
||||
bool result = true;
|
||||
std::filesystem::path logFolderPath(appPath);
|
||||
logFolderPath.append(LogSettings::logPath);
|
||||
|
||||
if (!dir_exists(logFolderPath))
|
||||
{
|
||||
Logger::trace("Directory {} does not exist", logFolderPath.string());
|
||||
return true;
|
||||
}
|
||||
|
||||
std::error_code err;
|
||||
auto folders = std::filesystem::directory_iterator(logFolderPath, err);
|
||||
if (err.value())
|
||||
{
|
||||
Logger::error("Failed to create directory iterator for {}. {}", logFolderPath.string(), err.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& dir : folders)
|
||||
{
|
||||
if (dir != currentVersionLogFolder)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::filesystem::remove_all(dir);
|
||||
}
|
||||
catch (std::filesystem::filesystem_error& e)
|
||||
{
|
||||
Logger::error("Failed to delete previous version log folder: {}", e.what());
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
inline void init_logger(std::wstring moduleName, std::wstring internalPath, std::string loggerName)
|
||||
{
|
||||
std::filesystem::path rootFolder(PTSettingsHelper::get_module_save_folder_location(moduleName));
|
||||
rootFolder.append(internalPath);
|
||||
|
||||
auto currentFolder = rootFolder;
|
||||
currentFolder.append(LogSettings::logPath);
|
||||
currentFolder.append(get_product_version());
|
||||
|
||||
auto logsPath = currentFolder;
|
||||
logsPath.append(L"log.log");
|
||||
Logger::init(loggerName, logsPath.wstring(), PTSettingsHelper::get_log_settings_file_location());
|
||||
|
||||
delete_other_versions_log_folders(rootFolder.wstring(), currentFolder);
|
||||
}
|
||||
void init_logger(std::wstring moduleName, std::wstring internalPath, std::string loggerName);
|
||||
}
|
||||
|
||||
318
src/common/utils/modulesRegistry.cpp
Normal file
318
src/common/utils/modulesRegistry.cpp
Normal file
@@ -0,0 +1,318 @@
|
||||
#include "pch.h"
|
||||
#include "modulesRegistry.h"
|
||||
|
||||
#include <utility>
|
||||
namespace
|
||||
{
|
||||
registry::ChangeSet createPreviewChangeSet(registry::shellex::PreviewHandlerType type,
|
||||
bool perUser,
|
||||
const wchar_t* clsid,
|
||||
std::wstring dllName,
|
||||
std::wstring handlerClsid,
|
||||
std::wstring handlerDisplayName,
|
||||
std::vector<std::wstring> extensions,
|
||||
std::wstring perceivedType = L"",
|
||||
std::wstring fileKindType = L"")
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
|
||||
return generatePreviewHandler(type,
|
||||
perUser,
|
||||
clsid,
|
||||
get_std_product_version(),
|
||||
std::move(dllName),
|
||||
std::move(handlerClsid),
|
||||
std::move(handlerDisplayName),
|
||||
std::move(extensions),
|
||||
std::move(perceivedType),
|
||||
std::move(fileKindType));
|
||||
}
|
||||
}
|
||||
|
||||
registry::ChangeSet getSvgPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{FCDD4EED-41AA-492F-8A84-31A1546226E0}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.SvgPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"SvgPreviewHandler",
|
||||
L"Svg Preview Handler",
|
||||
NonLocalizable::ExtSVG);
|
||||
}
|
||||
|
||||
registry::ChangeSet getMdPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{60789D87-9C3C-44AF-B18C-3DE2C2820ED3}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.MarkdownPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"MarkdownPreviewHandler",
|
||||
L"Markdown Preview Handler",
|
||||
NonLocalizable::ExtMarkdown);
|
||||
}
|
||||
|
||||
registry::ChangeSet getMonacoPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
std::vector<std::wstring> extensions;
|
||||
|
||||
std::vector<std::wstring> exclusions;
|
||||
exclusions.insert(exclusions.end(), NonLocalizable::ExtMarkdown.begin(), NonLocalizable::ExtMarkdown.end());
|
||||
exclusions.insert(exclusions.end(), NonLocalizable::ExtSVG.begin(), NonLocalizable::ExtSVG.end());
|
||||
exclusions.insert(exclusions.end(), NonLocalizable::ExtNoNoNo.begin(), NonLocalizable::ExtNoNoNo.end());
|
||||
|
||||
std::wstring languagesFilePath = fs::path{ installationDir } / NonLocalizable::MONACO_LANGUAGES_FILE_NAME;
|
||||
auto jsonValue = json::from_file(languagesFilePath);
|
||||
|
||||
if (jsonValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto list = jsonValue->GetNamedArray(NonLocalizable::ListID);
|
||||
for (uint32_t i = 0; i < list.Size(); ++i)
|
||||
{
|
||||
auto entry = list.GetObjectAt(i);
|
||||
if (entry.HasKey(NonLocalizable::ExtensionsID))
|
||||
{
|
||||
auto extensionsList = entry.GetNamedArray(NonLocalizable::ExtensionsID);
|
||||
|
||||
for (uint32_t j = 0; j < extensionsList.Size(); ++j)
|
||||
{
|
||||
auto extension = extensionsList.GetStringAt(j);
|
||||
|
||||
bool isExcluded = false;
|
||||
for (const auto& excluded : exclusions)
|
||||
{
|
||||
if (std::wstring{ extension } == excluded)
|
||||
{
|
||||
isExcluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isExcluded)
|
||||
{
|
||||
extensions.emplace_back(extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{D8034CFA-F34B-41FE-AD45-62FCBB52A6DA}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.MonacoPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"MonacoPreviewHandler",
|
||||
L"Monaco Preview Handler",
|
||||
std::move(extensions));
|
||||
}
|
||||
|
||||
registry::ChangeSet getPdfPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{A5A41CC7-02CB-41D4-8C9B-9087040D6098}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.PdfPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"PdfPreviewHandler",
|
||||
L"Pdf Preview Handler",
|
||||
NonLocalizable::ExtPDF);
|
||||
}
|
||||
|
||||
registry::ChangeSet getGcodePreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{A0257634-8812-4CE8-AF11-FA69ACAEAFAE}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.GcodePreviewHandlerCpp.dll)d").wstring(),
|
||||
L"GcodePreviewHandler",
|
||||
L"G-code Preview Handler",
|
||||
NonLocalizable::ExtGCode);
|
||||
}
|
||||
|
||||
registry::ChangeSet getBgcodePreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{0e6d5bdd-d5f8-4692-a089-8bb88cdd37f4}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.BgcodePreviewHandlerCpp.dll)d").wstring(),
|
||||
L"BgcodePreviewHandler",
|
||||
L"Binary G-code Preview Handler",
|
||||
NonLocalizable::ExtBGCode);
|
||||
}
|
||||
|
||||
registry::ChangeSet getQoiPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{729B72CD-B72E-4FE9-BCBF-E954B33FE699}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.QoiPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"QoiPreviewHandler",
|
||||
L"Qoi Preview Handler",
|
||||
NonLocalizable::ExtQOI);
|
||||
}
|
||||
|
||||
registry::ChangeSet getSvgThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{10144713-1526-46C9-88DA-1FB52807A9FF}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.SvgThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"SvgThumbnailProvider",
|
||||
L"Svg Thumbnail Provider",
|
||||
NonLocalizable::ExtSVG,
|
||||
L"image",
|
||||
L"Picture");
|
||||
}
|
||||
|
||||
registry::ChangeSet getPdfThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{D8BB9942-93BD-412D-87E4-33FAB214DC1A}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.PdfThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"PdfThumbnailProvider",
|
||||
L"Pdf Thumbnail Provider",
|
||||
NonLocalizable::ExtPDF);
|
||||
}
|
||||
|
||||
registry::ChangeSet getGcodeThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{F2847CBE-CD03-4C83-A359-1A8052C1B9D5}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.GcodeThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"GcodeThumbnailProvider",
|
||||
L"G-code Thumbnail Provider",
|
||||
NonLocalizable::ExtGCode);
|
||||
}
|
||||
|
||||
registry::ChangeSet getBgcodeThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{5c93a1e4-99d0-4fb3-991c-6c296a27be21}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.BgcodeThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"BgcodeThumbnailProvider",
|
||||
L"Binary G-code Thumbnail Provider",
|
||||
NonLocalizable::ExtBGCode);
|
||||
}
|
||||
|
||||
registry::ChangeSet getStlThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{77257004-6F25-4521-B602-50ECC6EC62A6}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.StlThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"StlThumbnailProvider",
|
||||
L"Stl Thumbnail Provider",
|
||||
NonLocalizable::ExtSTL);
|
||||
}
|
||||
|
||||
registry::ChangeSet getQoiThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
return createPreviewChangeSet(
|
||||
registry::shellex::PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{AD856B15-D25E-4008-AFB7-AFAA55586188}",
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.QoiThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"QoiThumbnailProvider",
|
||||
L"Qoi Thumbnail Provider",
|
||||
NonLocalizable::ExtQOI,
|
||||
L"image",
|
||||
L"Picture");
|
||||
}
|
||||
|
||||
registry::ChangeSet getRegistryPreviewSetDefaultAppChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
|
||||
|
||||
std::vector<registry::ValueChange> changes;
|
||||
|
||||
std::wstring appName = L"Registry Preview";
|
||||
std::wstring fullAppName = L"PowerToys.RegistryPreview";
|
||||
std::wstring registryKeyPrefix = L"Software\\Classes\\";
|
||||
|
||||
std::wstring appPath = installationDir + L"\\WinUI3Apps\\PowerToys.RegistryPreview.exe";
|
||||
std::wstring command = appPath + L" \"----ms-protocol:ms-encodedlaunch:App?ContractId=Windows.File&Verb=open&File=%1\"";
|
||||
|
||||
changes.push_back({ scope, registryKeyPrefix + fullAppName + L"\\Application", L"ApplicationName", appName });
|
||||
changes.push_back({ scope, registryKeyPrefix + fullAppName + L"\\DefaultIcon", std::nullopt, appPath });
|
||||
changes.push_back({ scope, registryKeyPrefix + fullAppName + L"\\shell\\open\\command", std::nullopt, command });
|
||||
changes.push_back({ scope, registryKeyPrefix + L".reg\\OpenWithProgIDs", fullAppName, L"" });
|
||||
|
||||
return { changes };
|
||||
}
|
||||
|
||||
registry::ChangeSet getRegistryPreviewChangeSet(const std::wstring& installationDir, bool perUser)
|
||||
{
|
||||
const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
|
||||
|
||||
std::vector<registry::ValueChange> changes;
|
||||
|
||||
std::wstring command = installationDir;
|
||||
command.append(L"\\WinUI3Apps\\PowerToys.RegistryPreview.exe \"%1\"");
|
||||
changes.push_back({ scope, L"Software\\Classes\\regfile\\shell\\preview\\command", std::nullopt, command });
|
||||
|
||||
std::wstring iconPath = installationDir;
|
||||
iconPath.append(L"\\WinUI3Apps\\Assets\\RegistryPreview\\RegistryPreview.ico");
|
||||
changes.push_back({ scope, L"Software\\Classes\\regfile\\shell\\preview", L"icon", iconPath });
|
||||
|
||||
return { changes };
|
||||
}
|
||||
|
||||
std::vector<registry::ChangeSet> getAllOnByDefaultModulesChangeSets(const std::wstring& installationDir)
|
||||
{
|
||||
constexpr bool perUser = true;
|
||||
return {
|
||||
getSvgPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getMdPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getMonacoPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getGcodePreviewHandlerChangeSet(installationDir, perUser),
|
||||
getBgcodePreviewHandlerChangeSet(installationDir, perUser),
|
||||
getQoiPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getSvgThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getGcodeThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getBgcodeThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getStlThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getQoiThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getRegistryPreviewChangeSet(installationDir, perUser)
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<registry::ChangeSet> getAllModulesChangeSets(const std::wstring& installationDir)
|
||||
{
|
||||
constexpr bool perUser = true;
|
||||
return {
|
||||
getSvgPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getMdPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getMonacoPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getPdfPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getGcodePreviewHandlerChangeSet(installationDir, perUser),
|
||||
getBgcodePreviewHandlerChangeSet(installationDir, perUser),
|
||||
getQoiPreviewHandlerChangeSet(installationDir, perUser),
|
||||
getSvgThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getPdfThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getGcodeThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getBgcodeThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getStlThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getQoiThumbnailHandlerChangeSet(installationDir, perUser),
|
||||
getRegistryPreviewChangeSet(installationDir, perUser),
|
||||
getRegistryPreviewSetDefaultAppChangeSet(installationDir, perUser)
|
||||
};
|
||||
}
|
||||
@@ -25,309 +25,36 @@ namespace NonLocalizable
|
||||
};
|
||||
}
|
||||
|
||||
inline registry::ChangeSet getSvgPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{FCDD4EED-41AA-492F-8A84-31A1546226E0}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } /
|
||||
LR"d(PowerToys.SvgPreviewHandlerCpp.dll)d")
|
||||
.wstring(),
|
||||
L"SvgPreviewHandler",
|
||||
L"Svg Preview Handler",
|
||||
NonLocalizable::ExtSVG);
|
||||
}
|
||||
registry::ChangeSet getSvgPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
inline registry::ChangeSet getMdPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{60789D87-9C3C-44AF-B18C-3DE2C2820ED3}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.MarkdownPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"MarkdownPreviewHandler",
|
||||
L"Markdown Preview Handler",
|
||||
NonLocalizable::ExtMarkdown);
|
||||
}
|
||||
registry::ChangeSet getMdPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
inline registry::ChangeSet getMonacoPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
registry::ChangeSet getMonacoPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
// Set up a list of extensions for the preview handler to take over
|
||||
std::vector<std::wstring> extensions;
|
||||
registry::ChangeSet getPdfPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
// Set up a list of extensions that Monaco support but the preview handler shouldn't take over
|
||||
std::vector<std::wstring> ExtExclusions;
|
||||
ExtExclusions.insert(ExtExclusions.end(), NonLocalizable::ExtMarkdown.begin(), NonLocalizable::ExtMarkdown.end());
|
||||
ExtExclusions.insert(ExtExclusions.end(), NonLocalizable::ExtSVG.begin(), NonLocalizable::ExtSVG.end());
|
||||
ExtExclusions.insert(ExtExclusions.end(), NonLocalizable::ExtNoNoNo.begin(), NonLocalizable::ExtNoNoNo.end());
|
||||
bool IsExcluded = false;
|
||||
registry::ChangeSet getGcodePreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
std::wstring languagesFilePath = fs::path{ installationDir } / NonLocalizable::MONACO_LANGUAGES_FILE_NAME;
|
||||
auto json = json::from_file(languagesFilePath);
|
||||
registry::ChangeSet getBgcodePreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
if (json)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto list = json->GetNamedArray(NonLocalizable::ListID);
|
||||
for (uint32_t i = 0; i < list.Size(); ++i)
|
||||
{
|
||||
auto entry = list.GetObjectAt(i);
|
||||
if (entry.HasKey(NonLocalizable::ExtensionsID))
|
||||
{
|
||||
auto extensionsList = entry.GetNamedArray(NonLocalizable::ExtensionsID);
|
||||
registry::ChangeSet getQoiPreviewHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
for (uint32_t j = 0; j < extensionsList.Size(); ++j)
|
||||
{
|
||||
auto extension = extensionsList.GetStringAt(j);
|
||||
registry::ChangeSet getSvgThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
// Ignore extensions in the exclusion list
|
||||
IsExcluded = false;
|
||||
registry::ChangeSet getPdfThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
for (std::wstring k : ExtExclusions)
|
||||
{
|
||||
if (std::wstring{ extension } == k)
|
||||
{
|
||||
IsExcluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (IsExcluded)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
extensions.push_back(std::wstring{ extension });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
registry::ChangeSet getGcodeThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
return generatePreviewHandler(PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{D8034CFA-F34B-41FE-AD45-62FCBB52A6DA}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.MonacoPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"MonacoPreviewHandler",
|
||||
L"Monaco Preview Handler",
|
||||
extensions);
|
||||
}
|
||||
registry::ChangeSet getBgcodeThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
inline registry::ChangeSet getPdfPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{A5A41CC7-02CB-41D4-8C9B-9087040D6098}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.PdfPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"PdfPreviewHandler",
|
||||
L"Pdf Preview Handler",
|
||||
NonLocalizable::ExtPDF);
|
||||
}
|
||||
registry::ChangeSet getStlThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
inline registry::ChangeSet getGcodePreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{A0257634-8812-4CE8-AF11-FA69ACAEAFAE}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.GcodePreviewHandlerCpp.dll)d").wstring(),
|
||||
L"GcodePreviewHandler",
|
||||
L"G-code Preview Handler",
|
||||
NonLocalizable::ExtGCode);
|
||||
}
|
||||
registry::ChangeSet getQoiThumbnailHandlerChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
inline registry::ChangeSet getBgcodePreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{0e6d5bdd-d5f8-4692-a089-8bb88cdd37f4}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.BgcodePreviewHandlerCpp.dll)d").wstring(),
|
||||
L"BgcodePreviewHandler",
|
||||
L"Binary G-code Preview Handler",
|
||||
NonLocalizable::ExtBGCode);
|
||||
}
|
||||
registry::ChangeSet getRegistryPreviewSetDefaultAppChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
inline registry::ChangeSet getQoiPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::preview,
|
||||
perUser,
|
||||
L"{729B72CD-B72E-4FE9-BCBF-E954B33FE699}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.QoiPreviewHandlerCpp.dll)d").wstring(),
|
||||
L"QoiPreviewHandler",
|
||||
L"Qoi Preview Handler",
|
||||
NonLocalizable::ExtQOI);
|
||||
}
|
||||
registry::ChangeSet getRegistryPreviewChangeSet(const std::wstring& installationDir, bool perUser);
|
||||
|
||||
inline registry::ChangeSet getSvgThumbnailHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{10144713-1526-46C9-88DA-1FB52807A9FF}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.SvgThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"SvgThumbnailProvider",
|
||||
L"Svg Thumbnail Provider",
|
||||
NonLocalizable::ExtSVG,
|
||||
L"image",
|
||||
L"Picture");
|
||||
}
|
||||
std::vector<registry::ChangeSet> getAllOnByDefaultModulesChangeSets(const std::wstring& installationDir);
|
||||
|
||||
inline registry::ChangeSet getPdfThumbnailHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{D8BB9942-93BD-412D-87E4-33FAB214DC1A}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.PdfThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"PdfThumbnailProvider",
|
||||
L"Pdf Thumbnail Provider",
|
||||
NonLocalizable::ExtPDF);
|
||||
}
|
||||
|
||||
inline registry::ChangeSet getGcodeThumbnailHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{F2847CBE-CD03-4C83-A359-1A8052C1B9D5}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.GcodeThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"GcodeThumbnailProvider",
|
||||
L"G-code Thumbnail Provider",
|
||||
NonLocalizable::ExtGCode);
|
||||
}
|
||||
|
||||
inline registry::ChangeSet getBgcodeThumbnailHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{5c93a1e4-99d0-4fb3-991c-6c296a27be21}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.BgcodeThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"BgcodeThumbnailProvider",
|
||||
L"Binary G-code Thumbnail Provider",
|
||||
NonLocalizable::ExtBGCode);
|
||||
}
|
||||
|
||||
inline registry::ChangeSet getStlThumbnailHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{77257004-6F25-4521-B602-50ECC6EC62A6}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.StlThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"StlThumbnailProvider",
|
||||
L"Stl Thumbnail Provider",
|
||||
NonLocalizable::ExtSTL);
|
||||
}
|
||||
|
||||
inline registry::ChangeSet getQoiThumbnailHandlerChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
using namespace registry::shellex;
|
||||
return generatePreviewHandler(PreviewHandlerType::thumbnail,
|
||||
perUser,
|
||||
L"{AD856B15-D25E-4008-AFB7-AFAA55586188}",
|
||||
get_std_product_version(),
|
||||
(fs::path{ installationDir } / LR"d(PowerToys.QoiThumbnailProviderCpp.dll)d").wstring(),
|
||||
L"QoiThumbnailProvider",
|
||||
L"Qoi Thumbnail Provider",
|
||||
NonLocalizable::ExtQOI,
|
||||
L"image",
|
||||
L"Picture");
|
||||
}
|
||||
|
||||
inline registry::ChangeSet getRegistryPreviewSetDefaultAppChangeSet(const std::wstring installationDir, const bool perUser)
|
||||
{
|
||||
const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
|
||||
|
||||
using vec_t = std::vector<registry::ValueChange>;
|
||||
vec_t changes;
|
||||
|
||||
std::wstring appName = L"Registry Preview";
|
||||
std::wstring fullAppName = L"PowerToys.RegistryPreview";
|
||||
std::wstring registryKeyPrefix = L"Software\\Classes\\";
|
||||
|
||||
std::wstring appPath = installationDir + L"\\WinUI3Apps\\PowerToys.RegistryPreview.exe";
|
||||
std::wstring command = appPath + L" \"----ms-protocol:ms-encodedlaunch:App?ContractId=Windows.File&Verb=open&File=%1\"";
|
||||
|
||||
changes.push_back({ scope, registryKeyPrefix + fullAppName + L"\\" + L"Application", L"ApplicationName", appName });
|
||||
changes.push_back({ scope, registryKeyPrefix + fullAppName + L"\\" + L"DefaultIcon", std::nullopt, appPath });
|
||||
changes.push_back({ scope, registryKeyPrefix + fullAppName + L"\\" + L"shell\\open\\command", std::nullopt, command });
|
||||
changes.push_back({ scope, registryKeyPrefix + L".reg\\OpenWithProgIDs", fullAppName, L"" });
|
||||
|
||||
return { changes };
|
||||
}
|
||||
|
||||
inline registry::ChangeSet getRegistryPreviewChangeSet(const std::wstring installationDir,const bool perUser)
|
||||
{
|
||||
const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
|
||||
|
||||
using vec_t = std::vector<registry::ValueChange>;
|
||||
vec_t changes;
|
||||
|
||||
std::wstring command = installationDir;
|
||||
command.append(L"\\WinUI3Apps\\PowerToys.RegistryPreview.exe \"%1\"");
|
||||
changes.push_back({ scope, L"Software\\Classes\\regfile\\shell\\preview\\command", std::nullopt, command });
|
||||
|
||||
std::wstring icon_path = installationDir;
|
||||
icon_path.append(L"\\WinUI3Apps\\Assets\\RegistryPreview\\RegistryPreview.ico");
|
||||
changes.push_back({ scope, L"Software\\Classes\\regfile\\shell\\preview", L"icon", icon_path });
|
||||
|
||||
return { changes };
|
||||
}
|
||||
|
||||
inline std::vector<registry::ChangeSet> getAllOnByDefaultModulesChangeSets(const std::wstring installationDir)
|
||||
{
|
||||
constexpr bool PER_USER = true;
|
||||
return { getSvgPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getMdPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getMonacoPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getGcodePreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getBgcodePreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getQoiPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getSvgThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getGcodeThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getBgcodeThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getStlThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getQoiThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getRegistryPreviewChangeSet(installationDir, PER_USER) };
|
||||
}
|
||||
|
||||
inline std::vector<registry::ChangeSet> getAllModulesChangeSets(const std::wstring installationDir)
|
||||
{
|
||||
constexpr bool PER_USER = true;
|
||||
return { getSvgPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getMdPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getMonacoPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getPdfPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getGcodePreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getBgcodePreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getQoiPreviewHandlerChangeSet(installationDir, PER_USER),
|
||||
getSvgThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getPdfThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getGcodeThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getBgcodeThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getStlThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getQoiThumbnailHandlerChangeSet(installationDir, PER_USER),
|
||||
getRegistryPreviewChangeSet(installationDir, PER_USER),
|
||||
getRegistryPreviewSetDefaultAppChangeSet(installationDir, PER_USER) };
|
||||
}
|
||||
std::vector<registry::ChangeSet> getAllModulesChangeSets(const std::wstring& installationDir);
|
||||
|
||||
417
src/common/utils/package.cpp
Normal file
417
src/common/utils/package.cpp
Normal file
@@ -0,0 +1,417 @@
|
||||
#include "pch.h"
|
||||
#include "package.h"
|
||||
#include <common/utils/winapi_error.h>
|
||||
|
||||
namespace package
|
||||
{
|
||||
using namespace winrt::Windows::Foundation;
|
||||
using namespace winrt::Windows::ApplicationModel;
|
||||
using namespace winrt::Windows::Management::Deployment;
|
||||
|
||||
BOOL IsWin11OrGreater()
|
||||
{
|
||||
OSVERSIONINFOEX osvi{};
|
||||
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
|
||||
osvi.dwMajorVersion = HIBYTE(_WIN32_WINNT_WINTHRESHOLD); // 10
|
||||
osvi.dwMinorVersion = LOBYTE(_WIN32_WINNT_WINTHRESHOLD); // 0
|
||||
osvi.dwBuildNumber = 22000; // Windows 11 RTM build
|
||||
|
||||
DWORDLONG mask = 0;
|
||||
BYTE op = VER_GREATER_EQUAL;
|
||||
VER_SET_CONDITION(mask, VER_MAJORVERSION, op);
|
||||
VER_SET_CONDITION(mask, VER_MINORVERSION, op);
|
||||
VER_SET_CONDITION(mask, VER_BUILDNUMBER, op);
|
||||
|
||||
return VerifyVersionInfo(&osvi,
|
||||
VER_MAJORVERSION | VER_MINORVERSION | VER_BUILDNUMBER,
|
||||
mask);
|
||||
}
|
||||
|
||||
ComInitializer::ComInitializer(DWORD coInitFlags) : _initialized(false)
|
||||
{
|
||||
const HRESULT hr = CoInitializeEx(nullptr, coInitFlags);
|
||||
_initialized = SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
ComInitializer::~ComInitializer()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
CoUninitialize();
|
||||
}
|
||||
}
|
||||
|
||||
bool ComInitializer::Succeeded() const
|
||||
{
|
||||
return _initialized;
|
||||
}
|
||||
|
||||
static inline int compare_versions(const PackageVersion& a, const PACKAGE_VERSION& b)
|
||||
{
|
||||
if (a.Major != b.Major) return (a.Major < b.Major) ? -1 : 1;
|
||||
if (a.Minor != b.Minor) return (a.Minor < b.Minor) ? -1 : 1;
|
||||
if (a.Build != b.Build) return (a.Build < b.Build) ? -1 : 1;
|
||||
if (a.Revision != b.Revision) return (a.Revision < b.Revision) ? -1 : 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool GetPackageNameAndVersionFromAppx(const std::wstring& appxPath,
|
||||
std::wstring& outName,
|
||||
PACKAGE_VERSION& outVersion)
|
||||
{
|
||||
try
|
||||
{
|
||||
ComInitializer comInit;
|
||||
if (!comInit.Succeeded())
|
||||
{
|
||||
Logger::error(L"COM initialization failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IAppxFactory> factory;
|
||||
Microsoft::WRL::ComPtr<IStream> stream;
|
||||
Microsoft::WRL::ComPtr<IAppxPackageReader> reader;
|
||||
Microsoft::WRL::ComPtr<IAppxManifestReader> manifest;
|
||||
Microsoft::WRL::ComPtr<IAppxManifestPackageId> packageId;
|
||||
|
||||
HRESULT hr = CoCreateInstance(__uuidof(AppxFactory), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&factory));
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Logger::error(L"CoCreateInstance(AppxFactory) failed. {}", get_last_error_or_default(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = SHCreateStreamOnFileEx(appxPath.c_str(), STGM_READ | STGM_SHARE_DENY_WRITE, FILE_ATTRIBUTE_NORMAL, FALSE, nullptr, &stream);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Logger::error(L"SHCreateStreamOnFileEx failed. {}", get_last_error_or_default(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = factory->CreatePackageReader(stream.Get(), &reader);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Logger::error(L"CreatePackageReader failed. {}", get_last_error_or_default(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = reader->GetManifest(&manifest);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Logger::error(L"GetManifest failed. {}", get_last_error_or_default(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = manifest->GetPackageId(&packageId);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Logger::error(L"GetPackageId failed. {}", get_last_error_or_default(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
LPWSTR name = nullptr;
|
||||
hr = packageId->GetName(&name);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
Logger::error(L"GetName failed. {}", get_last_error_or_default(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
UINT64 ver64 = 0;
|
||||
hr = packageId->GetVersion(&ver64);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
CoTaskMemFree(name);
|
||||
Logger::error(L"GetVersion failed. {}", get_last_error_or_default(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
outName = std::wstring(name);
|
||||
CoTaskMemFree(name);
|
||||
|
||||
outVersion.Major = static_cast<UINT16>((ver64 >> 48) & 0xFFFF);
|
||||
outVersion.Minor = static_cast<UINT16>((ver64 >> 32) & 0xFFFF);
|
||||
outVersion.Build = static_cast<UINT16>((ver64 >> 16) & 0xFFFF);
|
||||
outVersion.Revision = static_cast<UINT16>(ver64 & 0xFFFF);
|
||||
|
||||
Logger::info(L"Package name: {}, version: {}.{}.{}.{}, appxPath: {}",
|
||||
outName,
|
||||
outVersion.Major,
|
||||
outVersion.Minor,
|
||||
outVersion.Build,
|
||||
outVersion.Revision,
|
||||
appxPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
Logger::error(L"Standard exception: {}", winrt::to_hstring(ex.what()));
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Unknown or non-standard exception occurred.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Package> GetRegisteredPackage(std::wstring packageDisplayName, bool checkVersion)
|
||||
{
|
||||
PackageManager packageManager;
|
||||
for (const auto& pkg : packageManager.FindPackagesForUser({}))
|
||||
{
|
||||
const auto& fullName = std::wstring{ pkg.Id().FullName() };
|
||||
if (fullName.find(packageDisplayName) != std::wstring::npos)
|
||||
{
|
||||
if (!checkVersion)
|
||||
{
|
||||
return pkg;
|
||||
}
|
||||
const auto& ver = pkg.Id().Version();
|
||||
if (ver.Major == VERSION_MAJOR && ver.Minor == VERSION_MINOR && ver.Revision == VERSION_REVISION)
|
||||
{
|
||||
return pkg;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool IsPackageRegisteredWithPowerToysVersion(std::wstring packageDisplayName)
|
||||
{
|
||||
return GetRegisteredPackage(packageDisplayName, true).has_value();
|
||||
}
|
||||
|
||||
bool RegisterSparsePackage(const std::wstring& externalLocation, const std::wstring& sparsePkgPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
Uri externalUri{ externalLocation };
|
||||
Uri packageUri{ sparsePkgPath };
|
||||
|
||||
PackageManager packageManager;
|
||||
AddPackageOptions options;
|
||||
options.ExternalLocationUri(externalUri);
|
||||
options.ForceUpdateFromAnyVersion(true);
|
||||
|
||||
auto deploymentOperation = packageManager.AddPackageByUriAsync(packageUri, options);
|
||||
deploymentOperation.get();
|
||||
|
||||
if (deploymentOperation.Status() == AsyncStatus::Error)
|
||||
{
|
||||
auto deploymentResult{ deploymentOperation.GetResults() };
|
||||
auto errorCode = deploymentOperation.ErrorCode();
|
||||
auto errorText = deploymentResult.ErrorText();
|
||||
Logger::error(L"Register {} package failed. ErrorCode: {}, ErrorText: {}", sparsePkgPath, std::to_wstring(errorCode), errorText);
|
||||
return false;
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Canceled)
|
||||
{
|
||||
Logger::error(L"Register {} package canceled.", sparsePkgPath);
|
||||
return false;
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Completed)
|
||||
{
|
||||
Logger::info(L"Register {} package completed.", sparsePkgPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"Register {} package started.", sparsePkgPath);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Logger::error("Exception thrown while trying to register package: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool UnRegisterPackage(const std::wstring& pkgDisplayName)
|
||||
{
|
||||
try
|
||||
{
|
||||
PackageManager packageManager;
|
||||
const auto packages = packageManager.FindPackagesForUser({});
|
||||
|
||||
bool any = false;
|
||||
for (auto const& pkg : packages)
|
||||
{
|
||||
const auto& fullName = std::wstring{ pkg.Id().FullName() };
|
||||
if (fullName.find(pkgDisplayName) != std::wstring::npos)
|
||||
{
|
||||
any = true;
|
||||
auto op = packageManager.RemovePackageAsync(fullName);
|
||||
op.get();
|
||||
|
||||
if (op.Status() == AsyncStatus::Error)
|
||||
{
|
||||
auto res = op.GetResults();
|
||||
auto code = op.ErrorCode();
|
||||
auto text = res.ErrorText();
|
||||
Logger::error(L"Unregister {} package failed. ErrorCode: {}, ErrorText: {}", fullName, std::to_wstring(code), text);
|
||||
return false;
|
||||
}
|
||||
else if (op.Status() == AsyncStatus::Canceled)
|
||||
{
|
||||
Logger::error(L"Unregister {} package canceled.", fullName);
|
||||
return false;
|
||||
}
|
||||
else if (op.Status() == AsyncStatus::Completed)
|
||||
{
|
||||
Logger::info(L"Unregister {} package completed.", fullName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"Unregister {} package started.", fullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If nothing matched, treat as success (nothing to remove)
|
||||
return true;
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Logger::error("Exception thrown while trying to unregister package: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::wstring> FindMsixFile(const std::wstring& directoryPath, bool recursive)
|
||||
{
|
||||
std::vector<std::wstring> results;
|
||||
try
|
||||
{
|
||||
if (recursive)
|
||||
{
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(directoryPath))
|
||||
{
|
||||
if (!entry.is_regular_file()) continue;
|
||||
auto ext = entry.path().extension().wstring();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), ::towlower);
|
||||
if (ext == L".msix" || ext == L".msixbundle")
|
||||
{
|
||||
results.push_back(entry.path().wstring());
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const auto& entry : std::filesystem::directory_iterator(directoryPath))
|
||||
{
|
||||
if (!entry.is_regular_file()) continue;
|
||||
auto ext = entry.path().extension().wstring();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), ::towlower);
|
||||
if (ext == L".msix" || ext == L".msixbundle")
|
||||
{
|
||||
results.push_back(entry.path().wstring());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
Logger::error(L"FindMsixFile error: {}", winrt::to_hstring(e.what()));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
bool IsPackageSatisfied(const std::wstring& appxPath)
|
||||
{
|
||||
std::wstring targetName;
|
||||
PACKAGE_VERSION targetVersion{};
|
||||
if (!GetPackageNameAndVersionFromAppx(appxPath, targetName, targetVersion))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PackageManager pm;
|
||||
for (const auto& pkg : pm.FindPackagesForUser({}))
|
||||
{
|
||||
if (std::wstring{ pkg.Id().Name() } == targetName)
|
||||
{
|
||||
auto v = pkg.Id().Version();
|
||||
if (compare_versions(v, targetVersion) >= 0)
|
||||
{
|
||||
Logger::info(L"Package {} is satisfied. Installed version {}.{}.{}.{} >= target {}.{}.{}.{}, appxPath: {}",
|
||||
targetName,
|
||||
v.Major, v.Minor, v.Build, v.Revision,
|
||||
targetVersion.Major, targetVersion.Minor, targetVersion.Build, targetVersion.Revision,
|
||||
appxPath);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Logger::info(L"Package {} is not satisfied. Target version: {}.{}.{}.{}; appxPath: {}",
|
||||
targetName,
|
||||
targetVersion.Major, targetVersion.Minor, targetVersion.Build, targetVersion.Revision,
|
||||
appxPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RegisterPackage(std::wstring pkgPath, std::vector<std::wstring> dependencies)
|
||||
{
|
||||
try
|
||||
{
|
||||
Uri packageUri{ pkgPath };
|
||||
PackageManager packageManager;
|
||||
|
||||
DeploymentOptions options = DeploymentOptions::ForceTargetApplicationShutdown;
|
||||
Collections::IVector<Uri> uris = winrt::single_threaded_vector<Uri>();
|
||||
|
||||
for (const auto& dep : dependencies)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsPackageSatisfied(dep))
|
||||
{
|
||||
Logger::info(L"Dependency already satisfied: {}", dep);
|
||||
}
|
||||
else
|
||||
{
|
||||
uris.Append(Uri(dep));
|
||||
}
|
||||
}
|
||||
catch (const winrt::hresult_error& ex)
|
||||
{
|
||||
Logger::error(L"Error creating Uri for dependency: %s", ex.message().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
auto op = packageManager.AddPackageAsync(packageUri, uris, options);
|
||||
op.get();
|
||||
|
||||
if (op.Status() == AsyncStatus::Error)
|
||||
{
|
||||
auto res = op.GetResults();
|
||||
auto code = op.ErrorCode();
|
||||
auto text = res.ErrorText();
|
||||
Logger::error(L"Register {} package failed. ErrorCode: {}, ErrorText: {}", pkgPath, std::to_wstring(code), text);
|
||||
return false;
|
||||
}
|
||||
else if (op.Status() == AsyncStatus::Canceled)
|
||||
{
|
||||
Logger::error(L"Register {} package canceled.", pkgPath);
|
||||
return false;
|
||||
}
|
||||
else if (op.Status() == AsyncStatus::Completed)
|
||||
{
|
||||
Logger::info(L"Register {} package completed.", pkgPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"Register {} package started.", pkgPath);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Logger::error("Exception thrown while trying to register package: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Management.Deployment.h>
|
||||
|
||||
#include "../logger/logger.h"
|
||||
#include "../version/version.h"
|
||||
|
||||
namespace package
|
||||
{
|
||||
using namespace winrt::Windows::Foundation;
|
||||
@@ -25,30 +22,7 @@ namespace package
|
||||
using namespace winrt::Windows::Management::Deployment;
|
||||
using Microsoft::WRL::ComPtr;
|
||||
|
||||
inline BOOL IsWin11OrGreater()
|
||||
{
|
||||
OSVERSIONINFOEX osvi{};
|
||||
DWORDLONG dwlConditionMask = 0;
|
||||
byte op = VER_GREATER_EQUAL;
|
||||
|
||||
// Initialize the OSVERSIONINFOEX structure.
|
||||
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
|
||||
osvi.dwMajorVersion = HIBYTE(_WIN32_WINNT_WINTHRESHOLD);
|
||||
osvi.dwMinorVersion = LOBYTE(_WIN32_WINNT_WINTHRESHOLD);
|
||||
// Windows 11 build number
|
||||
osvi.dwBuildNumber = 22000;
|
||||
|
||||
// Initialize the condition mask.
|
||||
VER_SET_CONDITION(dwlConditionMask, VER_MAJORVERSION, op);
|
||||
VER_SET_CONDITION(dwlConditionMask, VER_MINORVERSION, op);
|
||||
VER_SET_CONDITION(dwlConditionMask, VER_BUILDNUMBER, op);
|
||||
|
||||
// Perform the test.
|
||||
return VerifyVersionInfo(
|
||||
&osvi,
|
||||
VER_MAJORVERSION | VER_MINORVERSION | VER_BUILDNUMBER,
|
||||
dwlConditionMask);
|
||||
}
|
||||
BOOL IsWin11OrGreater();
|
||||
|
||||
struct PACKAGE_VERSION
|
||||
{
|
||||
@@ -61,412 +35,30 @@ namespace package
|
||||
class ComInitializer
|
||||
{
|
||||
public:
|
||||
explicit ComInitializer(DWORD coInitFlags = COINIT_MULTITHREADED) :
|
||||
_initialized(false)
|
||||
{
|
||||
const HRESULT hr = CoInitializeEx(nullptr, coInitFlags);
|
||||
_initialized = SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
~ComInitializer()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
CoUninitialize();
|
||||
}
|
||||
}
|
||||
|
||||
bool Succeeded() const { return _initialized; }
|
||||
explicit ComInitializer(DWORD coInitFlags = COINIT_MULTITHREADED);
|
||||
~ComInitializer();
|
||||
bool Succeeded() const;
|
||||
|
||||
private:
|
||||
bool _initialized;
|
||||
};
|
||||
|
||||
inline bool GetPackageNameAndVersionFromAppx(
|
||||
bool GetPackageNameAndVersionFromAppx(
|
||||
const std::wstring& appxPath,
|
||||
std::wstring& outName,
|
||||
PACKAGE_VERSION& outVersion)
|
||||
{
|
||||
try
|
||||
{
|
||||
ComInitializer comInit;
|
||||
if (!comInit.Succeeded())
|
||||
{
|
||||
Logger::error(L"COM initialization failed.");
|
||||
return false;
|
||||
}
|
||||
PACKAGE_VERSION& outVersion);
|
||||
|
||||
ComPtr<IAppxFactory> factory;
|
||||
ComPtr<IStream> stream;
|
||||
ComPtr<IAppxPackageReader> reader;
|
||||
ComPtr<IAppxManifestReader> manifest;
|
||||
ComPtr<IAppxManifestPackageId> packageId;
|
||||
std::optional<Package> GetRegisteredPackage(std::wstring packageDisplayName, bool checkVersion);
|
||||
|
||||
HRESULT hr = CoCreateInstance(__uuidof(AppxFactory), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&factory));
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
bool IsPackageRegisteredWithPowerToysVersion(std::wstring packageDisplayName);
|
||||
|
||||
hr = SHCreateStreamOnFileEx(appxPath.c_str(), STGM_READ | STGM_SHARE_DENY_WRITE, FILE_ATTRIBUTE_NORMAL, FALSE, nullptr, &stream);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
bool RegisterSparsePackage(const std::wstring& externalLocation, const std::wstring& sparsePkgPath);
|
||||
|
||||
hr = factory->CreatePackageReader(stream.Get(), &reader);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
bool UnRegisterPackage(const std::wstring& pkgDisplayName);
|
||||
|
||||
hr = reader->GetManifest(&manifest);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
std::vector<std::wstring> FindMsixFile(const std::wstring& directoryPath, bool recursive);
|
||||
|
||||
hr = manifest->GetPackageId(&packageId);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
bool IsPackageSatisfied(const std::wstring& appxPath);
|
||||
|
||||
LPWSTR name = nullptr;
|
||||
hr = packageId->GetName(&name);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
UINT64 version = 0;
|
||||
hr = packageId->GetVersion(&version);
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
outName = std::wstring(name);
|
||||
CoTaskMemFree(name);
|
||||
|
||||
outVersion.Major = static_cast<UINT16>((version >> 48) & 0xFFFF);
|
||||
outVersion.Minor = static_cast<UINT16>((version >> 32) & 0xFFFF);
|
||||
outVersion.Build = static_cast<UINT16>((version >> 16) & 0xFFFF);
|
||||
outVersion.Revision = static_cast<UINT16>(version & 0xFFFF);
|
||||
|
||||
Logger::info(L"Package name: {}, version: {}.{}.{}.{}, appxPath: {}",
|
||||
outName,
|
||||
outVersion.Major,
|
||||
outVersion.Minor,
|
||||
outVersion.Build,
|
||||
outVersion.Revision,
|
||||
appxPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
Logger::error(L"Standard exception: {}", winrt::to_hstring(ex.what()));
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Unknown or non-standard exception occurred.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
inline std::optional<Package> GetRegisteredPackage(std::wstring packageDisplayName, bool checkVersion)
|
||||
{
|
||||
PackageManager packageManager;
|
||||
|
||||
for (const auto& package : packageManager.FindPackagesForUser({}))
|
||||
{
|
||||
const auto& packageFullName = std::wstring{ package.Id().FullName() };
|
||||
const auto& packageVersion = package.Id().Version();
|
||||
|
||||
if (packageFullName.contains(packageDisplayName))
|
||||
{
|
||||
// If checkVersion is true, verify if the package has the same version as PowerToys.
|
||||
if ((!checkVersion) || (packageVersion.Major == VERSION_MAJOR && packageVersion.Minor == VERSION_MINOR && packageVersion.Revision == VERSION_REVISION))
|
||||
{
|
||||
return { package };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
inline bool IsPackageRegisteredWithPowerToysVersion(std::wstring packageDisplayName)
|
||||
{
|
||||
return GetRegisteredPackage(packageDisplayName, true).has_value();
|
||||
}
|
||||
|
||||
inline bool RegisterSparsePackage(const std::wstring& externalLocation, const std::wstring& sparsePkgPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
Uri externalUri{ externalLocation };
|
||||
Uri packageUri{ sparsePkgPath };
|
||||
|
||||
PackageManager packageManager;
|
||||
|
||||
// Declare use of an external location
|
||||
AddPackageOptions options;
|
||||
options.ExternalLocationUri(externalUri);
|
||||
options.ForceUpdateFromAnyVersion(true);
|
||||
|
||||
IAsyncOperationWithProgress<DeploymentResult, DeploymentProgress> deploymentOperation = packageManager.AddPackageByUriAsync(packageUri, options);
|
||||
deploymentOperation.get();
|
||||
|
||||
// Check the status of the operation
|
||||
if (deploymentOperation.Status() == AsyncStatus::Error)
|
||||
{
|
||||
auto deploymentResult{ deploymentOperation.GetResults() };
|
||||
auto errorCode = deploymentOperation.ErrorCode();
|
||||
auto errorText = deploymentResult.ErrorText();
|
||||
|
||||
Logger::error(L"Register {} package failed. ErrorCode: {}, ErrorText: {}", sparsePkgPath, std::to_wstring(errorCode), errorText);
|
||||
return false;
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Canceled)
|
||||
{
|
||||
Logger::error(L"Register {} package canceled.", sparsePkgPath);
|
||||
return false;
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Completed)
|
||||
{
|
||||
Logger::info(L"Register {} package completed.", sparsePkgPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"Register {} package started.", sparsePkgPath);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Logger::error("Exception thrown while trying to register package: {}", e.what());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool UnRegisterPackage(const std::wstring& pkgDisplayName)
|
||||
{
|
||||
try
|
||||
{
|
||||
PackageManager packageManager;
|
||||
const static auto packages = packageManager.FindPackagesForUser({});
|
||||
|
||||
for (auto const& package : packages)
|
||||
{
|
||||
const auto& packageFullName = std::wstring{ package.Id().FullName() };
|
||||
|
||||
if (packageFullName.contains(pkgDisplayName))
|
||||
{
|
||||
auto deploymentOperation{ packageManager.RemovePackageAsync(packageFullName) };
|
||||
deploymentOperation.get();
|
||||
|
||||
// Check the status of the operation
|
||||
if (deploymentOperation.Status() == AsyncStatus::Error)
|
||||
{
|
||||
auto deploymentResult{ deploymentOperation.GetResults() };
|
||||
auto errorCode = deploymentOperation.ErrorCode();
|
||||
auto errorText = deploymentResult.ErrorText();
|
||||
|
||||
Logger::error(L"Unregister {} package failed. ErrorCode: {}, ErrorText: {}", packageFullName, std::to_wstring(errorCode), errorText);
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Canceled)
|
||||
{
|
||||
Logger::error(L"Unregister {} package canceled.", packageFullName);
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Completed)
|
||||
{
|
||||
Logger::info(L"Unregister {} package completed.", packageFullName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"Unregister {} package started.", packageFullName);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Logger::error("Exception thrown while trying to unregister package: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::vector<std::wstring> FindMsixFile(const std::wstring& directoryPath, bool recursive)
|
||||
{
|
||||
if (directoryPath.empty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!std::filesystem::exists(directoryPath))
|
||||
{
|
||||
Logger::error(L"The directory '" + directoryPath + L"' does not exist.");
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::regex pattern(R"(^.+\.(appx|msix|msixbundle)$)", std::regex_constants::icase);
|
||||
std::vector<std::wstring> matchedFiles;
|
||||
|
||||
try
|
||||
{
|
||||
if (recursive)
|
||||
{
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(directoryPath))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
const auto& fileName = entry.path().filename().string();
|
||||
if (std::regex_match(fileName, pattern))
|
||||
{
|
||||
matchedFiles.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const auto& entry : std::filesystem::directory_iterator(directoryPath))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
const auto& fileName = entry.path().filename().string();
|
||||
if (std::regex_match(fileName, pattern))
|
||||
{
|
||||
matchedFiles.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
Logger::error("An error occurred while searching for MSIX files: " + std::string(ex.what()));
|
||||
}
|
||||
|
||||
return matchedFiles;
|
||||
}
|
||||
|
||||
inline bool IsPackageSatisfied(const std::wstring& appxPath)
|
||||
{
|
||||
std::wstring targetName;
|
||||
PACKAGE_VERSION targetVersion{};
|
||||
|
||||
if (!GetPackageNameAndVersionFromAppx(appxPath, targetName, targetVersion))
|
||||
{
|
||||
Logger::error(L"Failed to get package name and version from appx: " + appxPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
PackageManager pm;
|
||||
|
||||
for (const auto& package : pm.FindPackagesForUser({}))
|
||||
{
|
||||
const auto& id = package.Id();
|
||||
if (std::wstring(id.Name()) == targetName)
|
||||
{
|
||||
const auto& version = id.Version();
|
||||
|
||||
if (version.Major > targetVersion.Major ||
|
||||
(version.Major == targetVersion.Major && version.Minor > targetVersion.Minor) ||
|
||||
(version.Major == targetVersion.Major && version.Minor == targetVersion.Minor && version.Build > targetVersion.Build) ||
|
||||
(version.Major == targetVersion.Major && version.Minor == targetVersion.Minor && version.Build == targetVersion.Build && version.Revision >= targetVersion.Revision))
|
||||
{
|
||||
Logger::info(
|
||||
L"Package {} is already satisfied with version {}.{}.{}.{}; target version {}.{}.{}.{}; appxPath: {}",
|
||||
id.Name(),
|
||||
version.Major,
|
||||
version.Minor,
|
||||
version.Build,
|
||||
version.Revision,
|
||||
targetVersion.Major,
|
||||
targetVersion.Minor,
|
||||
targetVersion.Build,
|
||||
targetVersion.Revision,
|
||||
appxPath);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger::info(
|
||||
L"Package {} is not satisfied. Target version: {}.{}.{}.{}; appxPath: {}",
|
||||
targetName,
|
||||
targetVersion.Major,
|
||||
targetVersion.Minor,
|
||||
targetVersion.Build,
|
||||
targetVersion.Revision,
|
||||
appxPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool RegisterPackage(std::wstring pkgPath, std::vector<std::wstring> dependencies)
|
||||
{
|
||||
try
|
||||
{
|
||||
Uri packageUri{ pkgPath };
|
||||
|
||||
PackageManager packageManager;
|
||||
|
||||
// Declare use of an external location
|
||||
DeploymentOptions options = DeploymentOptions::ForceTargetApplicationShutdown;
|
||||
|
||||
Collections::IVector<Uri> uris = winrt::single_threaded_vector<Uri>();
|
||||
if (!dependencies.empty())
|
||||
{
|
||||
for (const auto& dependency : dependencies)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsPackageSatisfied(dependency))
|
||||
{
|
||||
Logger::info(L"Dependency already satisfied: {}", dependency);
|
||||
}
|
||||
else
|
||||
{
|
||||
uris.Append(Uri(dependency));
|
||||
}
|
||||
}
|
||||
catch (const winrt::hresult_error& ex)
|
||||
{
|
||||
Logger::error(L"Error creating Uri for dependency: %s", ex.message().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IAsyncOperationWithProgress<DeploymentResult, DeploymentProgress> deploymentOperation = packageManager.AddPackageAsync(packageUri, uris, options);
|
||||
deploymentOperation.get();
|
||||
|
||||
// Check the status of the operation
|
||||
if (deploymentOperation.Status() == AsyncStatus::Error)
|
||||
{
|
||||
auto deploymentResult{ deploymentOperation.GetResults() };
|
||||
auto errorCode = deploymentOperation.ErrorCode();
|
||||
auto errorText = deploymentResult.ErrorText();
|
||||
|
||||
Logger::error(L"Register {} package failed. ErrorCode: {}, ErrorText: {}", pkgPath, std::to_wstring(errorCode), errorText);
|
||||
return false;
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Canceled)
|
||||
{
|
||||
Logger::error(L"Register {} package canceled.", pkgPath);
|
||||
return false;
|
||||
}
|
||||
else if (deploymentOperation.Status() == AsyncStatus::Completed)
|
||||
{
|
||||
Logger::info(L"Register {} package completed.", pkgPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"Register {} package started.", pkgPath);
|
||||
}
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Logger::error("Exception thrown while trying to register package: {}", e.what());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
bool RegisterPackage(std::wstring pkgPath, std::vector<std::wstring> dependencies);
|
||||
}
|
||||
|
||||
5
src/common/utils/packages.config
Normal file
5
src/common/utils/packages.config
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
|
||||
</packages>
|
||||
1
src/common/utils/pch.cpp
Normal file
1
src/common/utils/pch.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "pch.h"
|
||||
38
src/common/utils/pch.h
Normal file
38
src/common/utils/pch.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
#include <sddl.h>
|
||||
#include <shldisp.h>
|
||||
#include <shlobj.h>
|
||||
#include <Shlwapi.h>
|
||||
#include <exdisp.h>
|
||||
#include <atlbase.h>
|
||||
#include <comdef.h>
|
||||
#include <appxpackaging.h>
|
||||
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
#include <winrt/Windows.ApplicationModel.h>
|
||||
#include <winrt/Windows.Management.Deployment.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
#include <regex>
|
||||
#include <fstream>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
|
||||
#include <wil/result.h>
|
||||
#include <wil/com.h>
|
||||
#include <wil/resource.h>
|
||||
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/version/version.h>
|
||||
44
src/common/utils/processApi.cpp
Normal file
44
src/common/utils/processApi.cpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#include "pch.h"
|
||||
#include "processApi.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
std::vector<wil::unique_process_handle> getProcessHandlesByName(std::wstring_view processName, DWORD handleAccess)
|
||||
{
|
||||
std::vector<wil::unique_process_handle> result;
|
||||
DWORD bytesRequired = 0;
|
||||
std::vector<DWORD> processIds;
|
||||
processIds.resize(4096 / sizeof(processIds[0]));
|
||||
auto processIdSize = static_cast<DWORD>(processIds.size() * sizeof(processIds[0]));
|
||||
EnumProcesses(processIds.data(), processIdSize, &bytesRequired);
|
||||
while (bytesRequired == processIdSize)
|
||||
{
|
||||
processIdSize *= 2;
|
||||
processIds.resize(processIdSize / sizeof(processIds[0]));
|
||||
EnumProcesses(processIds.data(), processIdSize, &bytesRequired);
|
||||
}
|
||||
processIds.resize(bytesRequired / sizeof(processIds[0]));
|
||||
|
||||
handleAccess |= PROCESS_QUERY_LIMITED_INFORMATION;
|
||||
for (const DWORD processId : processIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
wil::unique_process_handle hProcess{ OpenProcess(handleAccess, FALSE, processId) };
|
||||
wchar_t name[MAX_PATH + 1];
|
||||
DWORD length = MAX_PATH;
|
||||
if (!hProcess || !QueryFullProcessImageNameW(hProcess.get(), 0, name, &length))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (processName == PathFindFileNameW(name))
|
||||
{
|
||||
result.push_back(std::move(hProcess));
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -6,42 +6,4 @@
|
||||
#include <Psapi.h>
|
||||
#include <string_view>
|
||||
|
||||
inline std::vector<wil::unique_process_handle> getProcessHandlesByName(const std::wstring_view processName, DWORD handleAccess)
|
||||
{
|
||||
std::vector<wil::unique_process_handle> result;
|
||||
DWORD bytesRequired;
|
||||
std::vector<DWORD> processIds;
|
||||
processIds.resize(4096 / sizeof(processIds[0]));
|
||||
auto processIdSize = static_cast<DWORD>(size(processIds) * sizeof(processIds[0]));
|
||||
EnumProcesses(processIds.data(), processIdSize, &bytesRequired);
|
||||
while (bytesRequired == processIdSize)
|
||||
{
|
||||
processIdSize *= 2;
|
||||
processIds.resize(processIdSize / sizeof(processIds[0]));
|
||||
EnumProcesses(processIds.data(), processIdSize, &bytesRequired);
|
||||
}
|
||||
processIds.resize(bytesRequired / sizeof(processIds[0]));
|
||||
|
||||
handleAccess |= PROCESS_QUERY_LIMITED_INFORMATION;
|
||||
for (const DWORD processId : processIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
wil::unique_process_handle hProcess{ OpenProcess(handleAccess, FALSE, processId) };
|
||||
wchar_t name[MAX_PATH + 1];
|
||||
DWORD length = MAX_PATH;
|
||||
if (!hProcess || !QueryFullProcessImageNameW(hProcess.get(), 0, name, &length))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (processName == PathFindFileNameW(name))
|
||||
{
|
||||
result.push_back(std::move(hProcess));
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
std::vector<wil::unique_process_handle> getProcessHandlesByName(std::wstring_view processName, DWORD handleAccess);
|
||||
|
||||
111
src/common/utils/process_path.cpp
Normal file
111
src/common/utils/process_path.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#include "pch.h"
|
||||
#include "process_path.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
std::wstring get_process_path(DWORD pid) noexcept
|
||||
{
|
||||
wil::unique_handle process{ OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, TRUE, pid) };
|
||||
std::wstring name;
|
||||
if (process)
|
||||
{
|
||||
name.resize(MAX_PATH);
|
||||
DWORD nameLength = static_cast<DWORD>(name.length());
|
||||
if (QueryFullProcessImageNameW(process.get(), 0, name.data(), &nameLength) == 0)
|
||||
{
|
||||
nameLength = 0;
|
||||
}
|
||||
name.resize(nameLength);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
std::wstring get_process_path(HWND window) noexcept
|
||||
{
|
||||
const static std::wstring appFrameHost = L"ApplicationFrameHost.exe";
|
||||
|
||||
DWORD pid{};
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
auto name = get_process_path(pid);
|
||||
|
||||
if (name.length() >= appFrameHost.length() &&
|
||||
name.compare(name.length() - appFrameHost.length(), appFrameHost.length(), appFrameHost) == 0)
|
||||
{
|
||||
DWORD newPid = pid;
|
||||
|
||||
EnumChildWindows(
|
||||
window,
|
||||
[](HWND hwnd, LPARAM param) -> BOOL {
|
||||
auto newPidPtr = reinterpret_cast<DWORD*>(param);
|
||||
DWORD childPid;
|
||||
GetWindowThreadProcessId(hwnd, &childPid);
|
||||
if (childPid != *newPidPtr)
|
||||
{
|
||||
*newPidPtr = childPid;
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
},
|
||||
reinterpret_cast<LPARAM>(&newPid));
|
||||
|
||||
if (newPid != pid)
|
||||
{
|
||||
return get_process_path(newPid);
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
std::wstring get_process_path_waiting_uwp(HWND window)
|
||||
{
|
||||
const static std::wstring appFrameHost = L"ApplicationFrameHost.exe";
|
||||
|
||||
int attempt = 0;
|
||||
auto processPath = get_process_path(window);
|
||||
|
||||
while (++attempt < 30 && processPath.length() >= appFrameHost.length() &&
|
||||
processPath.compare(processPath.length() - appFrameHost.length(), appFrameHost.length(), appFrameHost) == 0)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
processPath = get_process_path(window);
|
||||
}
|
||||
|
||||
return processPath;
|
||||
}
|
||||
|
||||
std::wstring get_module_filename(HMODULE mod)
|
||||
{
|
||||
wchar_t buffer[MAX_PATH + 1];
|
||||
DWORD actualLength = GetModuleFileNameW(mod, buffer, MAX_PATH);
|
||||
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
const DWORD longPathLength = 0xFFFF;
|
||||
std::wstring longFilename(longPathLength, L'\0');
|
||||
actualLength = GetModuleFileNameW(mod, longFilename.data(), longPathLength);
|
||||
return longFilename.substr(0, actualLength);
|
||||
}
|
||||
return { buffer, actualLength };
|
||||
}
|
||||
|
||||
std::wstring get_module_folderpath(HMODULE mod, bool removeFilename)
|
||||
{
|
||||
wchar_t buffer[MAX_PATH + 1];
|
||||
DWORD actualLength = GetModuleFileNameW(mod, buffer, MAX_PATH);
|
||||
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
const DWORD longPathLength = 0xFFFF;
|
||||
std::wstring longFilename(longPathLength, L'\0');
|
||||
actualLength = GetModuleFileNameW(mod, longFilename.data(), longPathLength);
|
||||
PathRemoveFileSpecW(longFilename.data());
|
||||
longFilename.resize(std::wcslen(longFilename.data()));
|
||||
longFilename.shrink_to_fit();
|
||||
return longFilename;
|
||||
}
|
||||
|
||||
if (removeFilename)
|
||||
{
|
||||
PathRemoveFileSpecW(buffer);
|
||||
}
|
||||
return { buffer, static_cast<uint64_t>(lstrlenW(buffer)) };
|
||||
}
|
||||
@@ -7,116 +7,13 @@
|
||||
#include <thread>
|
||||
|
||||
// Get the executable path or module name for modern apps
|
||||
inline std::wstring get_process_path(DWORD pid) noexcept
|
||||
{
|
||||
auto process = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, TRUE, pid);
|
||||
std::wstring name;
|
||||
if (process != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
name.resize(MAX_PATH);
|
||||
DWORD name_length = static_cast<DWORD>(name.length());
|
||||
if (QueryFullProcessImageNameW(process, 0, name.data(), &name_length) == 0)
|
||||
{
|
||||
name_length = 0;
|
||||
}
|
||||
name.resize(name_length);
|
||||
CloseHandle(process);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
std::wstring get_process_path(DWORD pid) noexcept;
|
||||
|
||||
// Get the executable path or module name for modern apps
|
||||
inline std::wstring get_process_path(HWND window) noexcept
|
||||
{
|
||||
const static std::wstring app_frame_host = L"ApplicationFrameHost.exe";
|
||||
std::wstring get_process_path(HWND window) noexcept;
|
||||
|
||||
DWORD pid{};
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
auto name = get_process_path(pid);
|
||||
std::wstring get_process_path_waiting_uwp(HWND window);
|
||||
|
||||
if (name.length() >= app_frame_host.length() &&
|
||||
name.compare(name.length() - app_frame_host.length(), app_frame_host.length(), app_frame_host) == 0)
|
||||
{
|
||||
// It is a UWP app. We will enumerate the windows and look for one created
|
||||
// by something with a different PID
|
||||
DWORD new_pid = pid;
|
||||
std::wstring get_module_filename(HMODULE mod = nullptr);
|
||||
|
||||
EnumChildWindows(
|
||||
window, [](HWND hwnd, LPARAM param) -> BOOL {
|
||||
auto new_pid_ptr = reinterpret_cast<DWORD*>(param);
|
||||
DWORD pid;
|
||||
GetWindowThreadProcessId(hwnd, &pid);
|
||||
if (pid != *new_pid_ptr)
|
||||
{
|
||||
*new_pid_ptr = pid;
|
||||
return FALSE;
|
||||
}
|
||||
else
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
},
|
||||
reinterpret_cast<LPARAM>(&new_pid));
|
||||
|
||||
// If we have a new pid, get the new name.
|
||||
if (new_pid != pid)
|
||||
{
|
||||
return get_process_path(new_pid);
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
inline std::wstring get_process_path_waiting_uwp(HWND window)
|
||||
{
|
||||
const static std::wstring appFrameHost = L"ApplicationFrameHost.exe";
|
||||
|
||||
int attempt = 0;
|
||||
auto processPath = get_process_path(window);
|
||||
|
||||
while (++attempt < 30 && processPath.length() >= appFrameHost.length() &&
|
||||
processPath.compare(processPath.length() - appFrameHost.length(), appFrameHost.length(), appFrameHost) == 0)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
processPath = get_process_path(window);
|
||||
}
|
||||
|
||||
return processPath;
|
||||
}
|
||||
|
||||
inline std::wstring get_module_filename(HMODULE mod = nullptr)
|
||||
{
|
||||
wchar_t buffer[MAX_PATH + 1];
|
||||
DWORD actual_length = GetModuleFileNameW(mod, buffer, MAX_PATH);
|
||||
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
const DWORD long_path_length = 0xFFFF; // should be always enough
|
||||
std::wstring long_filename(long_path_length, L'\0');
|
||||
actual_length = GetModuleFileNameW(mod, long_filename.data(), long_path_length);
|
||||
return long_filename.substr(0, actual_length);
|
||||
}
|
||||
return { buffer, actual_length };
|
||||
}
|
||||
|
||||
inline std::wstring get_module_folderpath(HMODULE mod = nullptr, const bool removeFilename = true)
|
||||
{
|
||||
wchar_t buffer[MAX_PATH + 1];
|
||||
DWORD actual_length = GetModuleFileNameW(mod, buffer, MAX_PATH);
|
||||
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
const DWORD long_path_length = 0xFFFF; // should be always enough
|
||||
std::wstring long_filename(long_path_length, L'\0');
|
||||
actual_length = GetModuleFileNameW(mod, long_filename.data(), long_path_length);
|
||||
PathRemoveFileSpecW(long_filename.data());
|
||||
long_filename.resize(std::wcslen(long_filename.data()));
|
||||
long_filename.shrink_to_fit();
|
||||
return long_filename;
|
||||
}
|
||||
|
||||
if (removeFilename)
|
||||
{
|
||||
PathRemoveFileSpecW(buffer);
|
||||
}
|
||||
return { buffer, static_cast<uint64_t>(lstrlenW(buffer))};
|
||||
}
|
||||
std::wstring get_module_folderpath(HMODULE mod = nullptr, bool removeFilename = true);
|
||||
|
||||
398
src/common/utils/registry.cpp
Normal file
398
src/common/utils/registry.cpp
Normal file
@@ -0,0 +1,398 @@
|
||||
#include "pch.h"
|
||||
#include "registry.h"
|
||||
|
||||
namespace registry
|
||||
{
|
||||
namespace install_scope
|
||||
{
|
||||
const InstallScope get_current_install_scope()
|
||||
{
|
||||
// Open HKLM key
|
||||
HKEY perMachineKey{};
|
||||
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
|
||||
INSTALL_SCOPE_REG_KEY,
|
||||
0,
|
||||
KEY_READ,
|
||||
&perMachineKey) != ERROR_SUCCESS)
|
||||
{
|
||||
// Open HKCU key
|
||||
HKEY perUserKey{};
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER,
|
||||
INSTALL_SCOPE_REG_KEY,
|
||||
0,
|
||||
KEY_READ,
|
||||
&perUserKey) != ERROR_SUCCESS)
|
||||
{
|
||||
// both keys are missing
|
||||
return InstallScope::PerMachine;
|
||||
}
|
||||
else
|
||||
{
|
||||
DWORD dataSize{};
|
||||
if (RegGetValueW(
|
||||
perUserKey,
|
||||
nullptr,
|
||||
L"InstallScope",
|
||||
RRF_RT_REG_SZ,
|
||||
nullptr,
|
||||
nullptr,
|
||||
&dataSize) != ERROR_SUCCESS)
|
||||
{
|
||||
// HKCU key is missing
|
||||
RegCloseKey(perUserKey);
|
||||
return InstallScope::PerMachine;
|
||||
}
|
||||
|
||||
std::wstring data;
|
||||
data.resize(dataSize / sizeof(wchar_t));
|
||||
|
||||
if (RegGetValueW(
|
||||
perUserKey,
|
||||
nullptr,
|
||||
L"InstallScope",
|
||||
RRF_RT_REG_SZ,
|
||||
nullptr,
|
||||
&data[0],
|
||||
&dataSize) != ERROR_SUCCESS)
|
||||
{
|
||||
// HKCU key is missing
|
||||
RegCloseKey(perUserKey);
|
||||
return InstallScope::PerMachine;
|
||||
}
|
||||
RegCloseKey(perUserKey);
|
||||
|
||||
if (data.contains(L"perUser"))
|
||||
{
|
||||
return InstallScope::PerUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return InstallScope::PerMachine;
|
||||
}
|
||||
}
|
||||
|
||||
namespace detail
|
||||
{
|
||||
const wchar_t* getScopeName(HKEY scope)
|
||||
{
|
||||
if (scope == HKEY_LOCAL_MACHINE)
|
||||
{
|
||||
return L"HKLM";
|
||||
}
|
||||
else if (scope == HKEY_CURRENT_USER)
|
||||
{
|
||||
return L"HKCU";
|
||||
}
|
||||
else if (scope == HKEY_CLASSES_ROOT)
|
||||
{
|
||||
return L"HKCR";
|
||||
}
|
||||
else
|
||||
{
|
||||
return L"HK??";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring ValueChange::toString() const
|
||||
{
|
||||
using namespace detail;
|
||||
|
||||
std::wstring value_str;
|
||||
std::visit(overloaded{ [&](DWORD value) {
|
||||
std::wostringstream oss;
|
||||
oss << value;
|
||||
value_str = oss.str();
|
||||
},
|
||||
[&](const std::wstring& value) { value_str = value; } },
|
||||
value);
|
||||
|
||||
return fmt::format(L"{}\\{}\\{}:{}", detail::getScopeName(scope), path, name ? *name : L"Default", value_str);
|
||||
}
|
||||
|
||||
bool ValueChange::isApplied() const
|
||||
{
|
||||
HKEY key{};
|
||||
if (auto res = RegOpenKeyExW(scope, path.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::info(L"isApplied of {}: RegOpenKeyExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
detail::on_exit closeKey{ [key] { RegCloseKey(key); } };
|
||||
|
||||
const DWORD expectedType = valueTypeToWinapiType(value);
|
||||
|
||||
DWORD retrievedType{};
|
||||
wchar_t buffer[VALUE_BUFFER_SIZE];
|
||||
DWORD valueSize = sizeof(buffer);
|
||||
if (auto res = RegQueryValueExW(key,
|
||||
name.has_value() ? name->c_str() : nullptr,
|
||||
0,
|
||||
&retrievedType,
|
||||
reinterpret_cast<LPBYTE>(&buffer),
|
||||
&valueSize);
|
||||
res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::info(L"isApplied of {}: RegQueryValueExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expectedType != retrievedType)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (const auto retrievedValue = bufferToValue(buffer, valueSize, retrievedType))
|
||||
{
|
||||
return value == retrievedValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ValueChange::apply() const
|
||||
{
|
||||
HKEY key{};
|
||||
|
||||
if (auto res = RegCreateKeyExW(scope, path.c_str(), 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &key, nullptr); res !=
|
||||
ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"apply of {}: RegCreateKeyExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
detail::on_exit closeKey{ [key] { RegCloseKey(key); } };
|
||||
|
||||
wchar_t buffer[VALUE_BUFFER_SIZE];
|
||||
DWORD valueSize;
|
||||
DWORD valueType;
|
||||
|
||||
valueToBuffer(value, buffer, valueSize, valueType);
|
||||
if (auto res = RegSetValueExW(key,
|
||||
name.has_value() ? name->c_str() : nullptr,
|
||||
0,
|
||||
valueType,
|
||||
reinterpret_cast<BYTE*>(buffer),
|
||||
valueSize);
|
||||
res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"apply of {}: RegSetValueExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ValueChange::unApply() const
|
||||
{
|
||||
HKEY key{};
|
||||
if (auto res = RegOpenKeyExW(scope, path.c_str(), 0, KEY_ALL_ACCESS, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"unApply of {}: RegOpenKeyExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
detail::on_exit closeKey{ [key] { RegCloseKey(key); } };
|
||||
|
||||
// delete the value itself
|
||||
if (auto res = RegDeleteKeyValueW(scope, path.c_str(), name.has_value() ? name->c_str() : nullptr); res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"unApply of {}: RegDeleteKeyValueW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the path doesn't contain anything and delete it if so
|
||||
DWORD nValues = 0;
|
||||
DWORD maxValueLen = 0;
|
||||
const auto ok =
|
||||
RegQueryInfoKeyW(
|
||||
key, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, &nValues, nullptr, &maxValueLen, nullptr, nullptr) ==
|
||||
ERROR_SUCCESS;
|
||||
|
||||
if (ok && (!nValues || !maxValueLen))
|
||||
{
|
||||
RegDeleteTreeW(scope, path.c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
DWORD ValueChange::valueTypeToWinapiType(const value_t& v)
|
||||
{
|
||||
return std::visit(
|
||||
[](auto&& arg) {
|
||||
using T = std::decay_t<decltype(arg)>;
|
||||
if constexpr (std::is_same_v<T, DWORD>)
|
||||
return REG_DWORD;
|
||||
else if constexpr (std::is_same_v<T, std::wstring>)
|
||||
return REG_SZ;
|
||||
else
|
||||
static_assert(always_false_v<T>, "support for this registry type is not implemented");
|
||||
},
|
||||
v);
|
||||
}
|
||||
|
||||
void ValueChange::valueToBuffer(const value_t& value, wchar_t buffer[VALUE_BUFFER_SIZE], DWORD& valueSize, DWORD& type)
|
||||
{
|
||||
using detail::overloaded;
|
||||
|
||||
std::visit(overloaded{ [&](DWORD value) {
|
||||
*reinterpret_cast<DWORD*>(buffer) = value;
|
||||
type = REG_DWORD;
|
||||
valueSize = sizeof(value);
|
||||
},
|
||||
[&](const std::wstring& value) {
|
||||
assert(value.size() < VALUE_BUFFER_SIZE);
|
||||
value.copy(buffer, value.size());
|
||||
type = REG_SZ;
|
||||
valueSize = static_cast<DWORD>(sizeof(wchar_t) * value.size());
|
||||
} },
|
||||
value);
|
||||
}
|
||||
|
||||
std::optional<ValueChange::value_t> ValueChange::bufferToValue(const wchar_t buffer[VALUE_BUFFER_SIZE],
|
||||
const DWORD valueSize,
|
||||
const DWORD type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case REG_DWORD:
|
||||
return *reinterpret_cast<const DWORD*>(buffer);
|
||||
case REG_SZ:
|
||||
{
|
||||
if (!valueSize)
|
||||
{
|
||||
return std::wstring{};
|
||||
}
|
||||
std::wstring result{ buffer, valueSize / sizeof(wchar_t) };
|
||||
while (result[result.size() - 1] == L'\0')
|
||||
{
|
||||
result.resize(result.size() - 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
bool ChangeSet::isApplied() const
|
||||
{
|
||||
for (const auto& c : changes)
|
||||
{
|
||||
if (c.required && !c.isApplied())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChangeSet::apply() const
|
||||
{
|
||||
bool ok = true;
|
||||
for (const auto& c : changes)
|
||||
{
|
||||
ok = (c.apply()||!c.required) && ok;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool ChangeSet::unApply() const
|
||||
{
|
||||
bool ok = true;
|
||||
for (const auto& c : changes)
|
||||
{
|
||||
ok = (c.unApply()||!c.required) && ok;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
namespace shellex
|
||||
{
|
||||
registry::ChangeSet generatePreviewHandler(const PreviewHandlerType handlerType,
|
||||
const bool perUser,
|
||||
std::wstring handlerClsid,
|
||||
std::wstring powertoysVersion,
|
||||
std::wstring fullPathToHandler,
|
||||
std::wstring className,
|
||||
std::wstring displayName,
|
||||
std::vector<std::wstring> fileTypes,
|
||||
std::wstring perceivedType,
|
||||
std::wstring fileKindType)
|
||||
{
|
||||
const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
|
||||
|
||||
std::wstring clsidPath = L"Software\\Classes\\CLSID";
|
||||
clsidPath += L'\\';
|
||||
clsidPath += handlerClsid;
|
||||
|
||||
std::wstring inprocServerPath = clsidPath;
|
||||
inprocServerPath += L'\\';
|
||||
inprocServerPath += L"InprocServer32";
|
||||
|
||||
std::wstring assemblyKeyValue;
|
||||
if (const auto lastDotPos = className.rfind(L'.'); lastDotPos != std::wstring::npos)
|
||||
{
|
||||
assemblyKeyValue = L"PowerToys." + className.substr(lastDotPos + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
assemblyKeyValue = L"PowerToys." + className;
|
||||
}
|
||||
|
||||
assemblyKeyValue += L", Version=";
|
||||
assemblyKeyValue += powertoysVersion;
|
||||
assemblyKeyValue += L", Culture=neutral";
|
||||
|
||||
std::wstring versionPath = inprocServerPath;
|
||||
versionPath += L'\\';
|
||||
versionPath += powertoysVersion;
|
||||
|
||||
using vec_t = std::vector<registry::ValueChange>;
|
||||
// TODO: verify that we actually need all of those
|
||||
vec_t changes = { { scope, clsidPath, L"DisplayName", displayName },
|
||||
{ scope, clsidPath, std::nullopt, className },
|
||||
{ scope, inprocServerPath, std::nullopt, fullPathToHandler },
|
||||
{ scope, inprocServerPath, L"Assembly", assemblyKeyValue },
|
||||
{ scope, inprocServerPath, L"Class", className },
|
||||
{ scope, inprocServerPath, L"ThreadingModel", L"Apartment" } };
|
||||
|
||||
for (const auto& fileType : fileTypes)
|
||||
{
|
||||
std::wstring fileTypePath = L"Software\\Classes\\" + fileType;
|
||||
std::wstring fileAssociationPath = fileTypePath + L"\\shellex\\";
|
||||
fileAssociationPath += handlerType == PreviewHandlerType::preview ? IPREVIEW_HANDLER_CLSID : ITHUMBNAIL_PROVIDER_CLSID;
|
||||
changes.push_back({ scope, fileAssociationPath, std::nullopt, handlerClsid });
|
||||
if (!fileKindType.empty())
|
||||
{
|
||||
// Registering a file type as a kind needs to be done at the HKEY_LOCAL_MACHINE level.
|
||||
// Make it optional as well so that we don't fail registering the handler if we can't write to HKEY_LOCAL_MACHINE.
|
||||
std::wstring kindMapPath = L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\KindMap";
|
||||
changes.push_back({ HKEY_LOCAL_MACHINE, kindMapPath, fileType, fileKindType, false});
|
||||
}
|
||||
if (!perceivedType.empty())
|
||||
{
|
||||
changes.push_back({ scope, fileTypePath, L"PerceivedType", perceivedType });
|
||||
}
|
||||
if (handlerType == PreviewHandlerType::preview && fileType == L".reg")
|
||||
{
|
||||
// this regfile registry key has precedence over Software\Classes\.reg for .reg files
|
||||
std::wstring regfilePath = L"Software\\Classes\\regfile\\shellex\\" + IPREVIEW_HANDLER_CLSID + L"\\";
|
||||
changes.push_back({ scope, regfilePath, std::nullopt, handlerClsid });
|
||||
}
|
||||
}
|
||||
|
||||
if (handlerType == PreviewHandlerType::preview)
|
||||
{
|
||||
const std::wstring previewHostClsid = L"{6d2b5079-2f0b-48dd-ab7f-97cec514d30b}";
|
||||
const std::wstring previewHandlerListPath = LR"(Software\Microsoft\Windows\CurrentVersion\PreviewHandlers)";
|
||||
|
||||
changes.push_back({ scope, clsidPath, L"AppID", previewHostClsid });
|
||||
changes.push_back({ scope, previewHandlerListPath, handlerClsid, displayName });
|
||||
}
|
||||
|
||||
return registry::ChangeSet{ .changes = std::move(changes) };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,226 +217,28 @@ namespace registry
|
||||
{
|
||||
}
|
||||
|
||||
std::wstring toString() const
|
||||
{
|
||||
using namespace detail;
|
||||
|
||||
std::wstring value_str;
|
||||
std::visit(overloaded{ [&](DWORD value) {
|
||||
std::wostringstream oss;
|
||||
oss << value;
|
||||
value_str = oss.str();
|
||||
},
|
||||
[&](const std::wstring& value) { value_str = value; } },
|
||||
value);
|
||||
|
||||
return fmt::format(L"{}\\{}\\{}:{}", detail::getScopeName(scope), path, name ? *name : L"Default", value_str);
|
||||
}
|
||||
|
||||
bool isApplied() const
|
||||
{
|
||||
HKEY key{};
|
||||
if (auto res = RegOpenKeyExW(scope, path.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::info(L"isApplied of {}: RegOpenKeyExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
detail::on_exit closeKey{ [key] { RegCloseKey(key); } };
|
||||
|
||||
const DWORD expectedType = valueTypeToWinapiType(value);
|
||||
|
||||
DWORD retrievedType{};
|
||||
wchar_t buffer[VALUE_BUFFER_SIZE];
|
||||
DWORD valueSize = sizeof(buffer);
|
||||
if (auto res = RegQueryValueExW(key,
|
||||
name.has_value() ? name->c_str() : nullptr,
|
||||
0,
|
||||
&retrievedType,
|
||||
reinterpret_cast<LPBYTE>(&buffer),
|
||||
&valueSize);
|
||||
res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::info(L"isApplied of {}: RegQueryValueExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expectedType != retrievedType)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (const auto retrievedValue = bufferToValue(buffer, valueSize, retrievedType))
|
||||
{
|
||||
return value == retrievedValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool apply() const
|
||||
{
|
||||
HKEY key{};
|
||||
|
||||
if (auto res = RegCreateKeyExW(scope, path.c_str(), 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &key, nullptr); res !=
|
||||
ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"apply of {}: RegCreateKeyExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
detail::on_exit closeKey{ [key] { RegCloseKey(key); } };
|
||||
|
||||
wchar_t buffer[VALUE_BUFFER_SIZE];
|
||||
DWORD valueSize;
|
||||
DWORD valueType;
|
||||
|
||||
valueToBuffer(value, buffer, valueSize, valueType);
|
||||
if (auto res = RegSetValueExW(key,
|
||||
name.has_value() ? name->c_str() : nullptr,
|
||||
0,
|
||||
valueType,
|
||||
reinterpret_cast<BYTE*>(buffer),
|
||||
valueSize);
|
||||
res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"apply of {}: RegSetValueExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unApply() const
|
||||
{
|
||||
HKEY key{};
|
||||
if (auto res = RegOpenKeyExW(scope, path.c_str(), 0, KEY_ALL_ACCESS, &key); res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"unApply of {}: RegOpenKeyExW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
detail::on_exit closeKey{ [key] { RegCloseKey(key); } };
|
||||
|
||||
// delete the value itself
|
||||
if (auto res = RegDeleteKeyValueW(scope, path.c_str(), name.has_value() ? name->c_str() : nullptr); res != ERROR_SUCCESS)
|
||||
{
|
||||
Logger::error(L"unApply of {}: RegDeleteKeyValueW failed: {}", toString(), get_last_error_or_default(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the path doesn't contain anything and delete it if so
|
||||
DWORD nValues = 0;
|
||||
DWORD maxValueLen = 0;
|
||||
const auto ok =
|
||||
RegQueryInfoKeyW(
|
||||
key, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, &nValues, nullptr, &maxValueLen, nullptr, nullptr) ==
|
||||
ERROR_SUCCESS;
|
||||
|
||||
if (ok && (!nValues || !maxValueLen))
|
||||
{
|
||||
RegDeleteTreeW(scope, path.c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::wstring toString() const;
|
||||
bool isApplied() const;
|
||||
bool apply() const;
|
||||
bool unApply() const;
|
||||
|
||||
bool requiresElevation() const { return scope == HKEY_LOCAL_MACHINE; }
|
||||
|
||||
private:
|
||||
static DWORD valueTypeToWinapiType(const value_t& v)
|
||||
{
|
||||
return std::visit(
|
||||
[](auto&& arg) {
|
||||
using T = std::decay_t<decltype(arg)>;
|
||||
if constexpr (std::is_same_v<T, DWORD>)
|
||||
return REG_DWORD;
|
||||
else if constexpr (std::is_same_v<T, std::wstring>)
|
||||
return REG_SZ;
|
||||
else
|
||||
static_assert(always_false_v<T>, "support for this registry type is not implemented");
|
||||
},
|
||||
v);
|
||||
}
|
||||
|
||||
static void valueToBuffer(const value_t& value, wchar_t buffer[VALUE_BUFFER_SIZE], DWORD& valueSize, DWORD& type)
|
||||
{
|
||||
using detail::overloaded;
|
||||
|
||||
std::visit(overloaded{ [&](DWORD value) {
|
||||
*reinterpret_cast<DWORD*>(buffer) = value;
|
||||
type = REG_DWORD;
|
||||
valueSize = sizeof(value);
|
||||
},
|
||||
[&](const std::wstring& value) {
|
||||
assert(value.size() < VALUE_BUFFER_SIZE);
|
||||
value.copy(buffer, value.size());
|
||||
type = REG_SZ;
|
||||
valueSize = static_cast<DWORD>(sizeof(wchar_t) * value.size());
|
||||
} },
|
||||
value);
|
||||
}
|
||||
|
||||
static DWORD valueTypeToWinapiType(const value_t& v);
|
||||
static void valueToBuffer(const value_t& value, wchar_t buffer[VALUE_BUFFER_SIZE], DWORD& valueSize, DWORD& type);
|
||||
static std::optional<value_t> bufferToValue(const wchar_t buffer[VALUE_BUFFER_SIZE],
|
||||
const DWORD valueSize,
|
||||
const DWORD type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case REG_DWORD:
|
||||
return *reinterpret_cast<const DWORD*>(buffer);
|
||||
case REG_SZ:
|
||||
{
|
||||
if (!valueSize)
|
||||
{
|
||||
return std::wstring{};
|
||||
}
|
||||
std::wstring result{ buffer, valueSize / sizeof(wchar_t) };
|
||||
while (result[result.size() - 1] == L'\0')
|
||||
{
|
||||
result.resize(result.size() - 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
const DWORD type);
|
||||
};
|
||||
|
||||
struct ChangeSet
|
||||
{
|
||||
std::vector<ValueChange> changes;
|
||||
|
||||
bool isApplied() const
|
||||
{
|
||||
for (const auto& c : changes)
|
||||
{
|
||||
if (c.required && !c.isApplied())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool apply() const
|
||||
{
|
||||
bool ok = true;
|
||||
for (const auto& c : changes)
|
||||
{
|
||||
ok = (c.apply()||!c.required) && ok;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool unApply() const
|
||||
{
|
||||
bool ok = true;
|
||||
for (const auto& c : changes)
|
||||
{
|
||||
ok = (c.unApply()||!c.required) && ok;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
bool isApplied() const;
|
||||
bool apply() const;
|
||||
bool unApply() const;
|
||||
};
|
||||
|
||||
const inline std::wstring DOTNET_COMPONENT_CATEGORY_CLSID = L"{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}";
|
||||
@@ -451,7 +253,7 @@ namespace registry
|
||||
thumbnail
|
||||
};
|
||||
|
||||
inline registry::ChangeSet generatePreviewHandler(const PreviewHandlerType handlerType,
|
||||
registry::ChangeSet generatePreviewHandler(const PreviewHandlerType handlerType,
|
||||
const bool perUser,
|
||||
std::wstring handlerClsid,
|
||||
std::wstring powertoysVersion,
|
||||
@@ -460,80 +262,6 @@ namespace registry
|
||||
std::wstring displayName,
|
||||
std::vector<std::wstring> fileTypes,
|
||||
std::wstring perceivedType = L"",
|
||||
std::wstring fileKindType = L"")
|
||||
{
|
||||
const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
|
||||
|
||||
std::wstring clsidPath = L"Software\\Classes\\CLSID";
|
||||
clsidPath += L'\\';
|
||||
clsidPath += handlerClsid;
|
||||
|
||||
std::wstring inprocServerPath = clsidPath;
|
||||
inprocServerPath += L'\\';
|
||||
inprocServerPath += L"InprocServer32";
|
||||
|
||||
std::wstring assemblyKeyValue;
|
||||
if (const auto lastDotPos = className.rfind(L'.'); lastDotPos != std::wstring::npos)
|
||||
{
|
||||
assemblyKeyValue = L"PowerToys." + className.substr(lastDotPos + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
assemblyKeyValue = L"PowerToys." + className;
|
||||
}
|
||||
|
||||
assemblyKeyValue += L", Version=";
|
||||
assemblyKeyValue += powertoysVersion;
|
||||
assemblyKeyValue += L", Culture=neutral";
|
||||
|
||||
std::wstring versionPath = inprocServerPath;
|
||||
versionPath += L'\\';
|
||||
versionPath += powertoysVersion;
|
||||
|
||||
using vec_t = std::vector<registry::ValueChange>;
|
||||
// TODO: verify that we actually need all of those
|
||||
vec_t changes = { { scope, clsidPath, L"DisplayName", displayName },
|
||||
{ scope, clsidPath, std::nullopt, className },
|
||||
{ scope, inprocServerPath, std::nullopt, fullPathToHandler },
|
||||
{ scope, inprocServerPath, L"Assembly", assemblyKeyValue },
|
||||
{ scope, inprocServerPath, L"Class", className },
|
||||
{ scope, inprocServerPath, L"ThreadingModel", L"Apartment" } };
|
||||
|
||||
for (const auto& fileType : fileTypes)
|
||||
{
|
||||
std::wstring fileTypePath = L"Software\\Classes\\" + fileType;
|
||||
std::wstring fileAssociationPath = fileTypePath + L"\\shellex\\";
|
||||
fileAssociationPath += handlerType == PreviewHandlerType::preview ? IPREVIEW_HANDLER_CLSID : ITHUMBNAIL_PROVIDER_CLSID;
|
||||
changes.push_back({ scope, fileAssociationPath, std::nullopt, handlerClsid });
|
||||
if (!fileKindType.empty())
|
||||
{
|
||||
// Registering a file type as a kind needs to be done at the HKEY_LOCAL_MACHINE level.
|
||||
// Make it optional as well so that we don't fail registering the handler if we can't write to HKEY_LOCAL_MACHINE.
|
||||
std::wstring kindMapPath = L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\KindMap";
|
||||
changes.push_back({ HKEY_LOCAL_MACHINE, kindMapPath, fileType, fileKindType, false});
|
||||
}
|
||||
if (!perceivedType.empty())
|
||||
{
|
||||
changes.push_back({ scope, fileTypePath, L"PerceivedType", perceivedType });
|
||||
}
|
||||
if (handlerType == PreviewHandlerType::preview && fileType == L".reg")
|
||||
{
|
||||
// this regfile registry key has precedence over Software\Classes\.reg for .reg files
|
||||
std::wstring regfilePath = L"Software\\Classes\\regfile\\shellex\\" + IPREVIEW_HANDLER_CLSID + L"\\";
|
||||
changes.push_back({ scope, regfilePath, std::nullopt, handlerClsid });
|
||||
}
|
||||
}
|
||||
|
||||
if (handlerType == PreviewHandlerType::preview)
|
||||
{
|
||||
const std::wstring previewHostClsid = L"{6d2b5079-2f0b-48dd-ab7f-97cec514d30b}";
|
||||
const std::wstring previewHandlerListPath = LR"(Software\Microsoft\Windows\CurrentVersion\PreviewHandlers)";
|
||||
|
||||
changes.push_back({ scope, clsidPath, L"AppID", previewHostClsid });
|
||||
changes.push_back({ scope, previewHandlerListPath, handlerClsid, displayName });
|
||||
}
|
||||
|
||||
return registry::ChangeSet{ .changes = std::move(changes) };
|
||||
}
|
||||
std::wstring fileKindType = L"");
|
||||
}
|
||||
}
|
||||
|
||||
201
src/common/utils/resources.cpp
Normal file
201
src/common/utils/resources.cpp
Normal file
@@ -0,0 +1,201 @@
|
||||
#include "pch.h"
|
||||
#include "resources.h"
|
||||
#include <atlstr.h>
|
||||
#include <common/utils/language_helper.h>
|
||||
|
||||
std::wstring get_english_fallback_string(UINT resource_id, HINSTANCE instance)
|
||||
{
|
||||
// Try to load en-us string as the first fallback.
|
||||
WORD english_language = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
|
||||
|
||||
ATL::CStringW english_string;
|
||||
try
|
||||
{
|
||||
if (!english_string.LoadStringW(instance, resource_id, english_language))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::wstring(english_string);
|
||||
}
|
||||
|
||||
std::wstring get_resource_string_language_override(UINT resource_id, HINSTANCE instance)
|
||||
{
|
||||
static std::wstring language = LanguageHelpers::load_language();
|
||||
unsigned lang = LANG_ENGLISH;
|
||||
unsigned sublang = SUBLANG_ENGLISH_US;
|
||||
|
||||
if (!language.empty())
|
||||
{
|
||||
// Language list taken from Resources.wxs
|
||||
if (language == L"ar-SA")
|
||||
{
|
||||
lang = LANG_ARABIC;
|
||||
sublang = SUBLANG_ARABIC_SAUDI_ARABIA;
|
||||
}
|
||||
else if (language == L"cs-CZ")
|
||||
{
|
||||
lang = LANG_CZECH;
|
||||
sublang = SUBLANG_CZECH_CZECH_REPUBLIC;
|
||||
}
|
||||
else if (language == L"de-DE")
|
||||
{
|
||||
lang = LANG_GERMAN;
|
||||
sublang = SUBLANG_GERMAN;
|
||||
}
|
||||
else if (language == L"en-US")
|
||||
{
|
||||
lang = LANG_ENGLISH;
|
||||
sublang = SUBLANG_ENGLISH_US;
|
||||
}
|
||||
else if (language == L"es-ES")
|
||||
{
|
||||
lang = LANG_SPANISH;
|
||||
sublang = SUBLANG_SPANISH;
|
||||
}
|
||||
else if (language == L"fa-IR")
|
||||
{
|
||||
lang = LANG_PERSIAN;
|
||||
sublang = SUBLANG_PERSIAN_IRAN;
|
||||
}
|
||||
else if (language == L"fr-FR")
|
||||
{
|
||||
lang = LANG_FRENCH;
|
||||
sublang = SUBLANG_FRENCH;
|
||||
}
|
||||
else if (language == L"he-IL")
|
||||
{
|
||||
lang = LANG_HEBREW;
|
||||
sublang = SUBLANG_HEBREW_ISRAEL;
|
||||
}
|
||||
else if (language == L"hu-HU")
|
||||
{
|
||||
lang = LANG_HUNGARIAN;
|
||||
sublang = SUBLANG_HUNGARIAN_HUNGARY;
|
||||
}
|
||||
else if (language == L"it-IT")
|
||||
{
|
||||
lang = LANG_ITALIAN;
|
||||
sublang = SUBLANG_ITALIAN;
|
||||
}
|
||||
else if (language == L"ja-JP")
|
||||
{
|
||||
lang = LANG_JAPANESE;
|
||||
sublang = SUBLANG_JAPANESE_JAPAN;
|
||||
}
|
||||
else if (language == L"ko-KR")
|
||||
{
|
||||
lang = LANG_KOREAN;
|
||||
sublang = SUBLANG_KOREAN;
|
||||
}
|
||||
else if (language == L"nl-NL")
|
||||
{
|
||||
lang = LANG_DUTCH;
|
||||
sublang = SUBLANG_DUTCH;
|
||||
}
|
||||
else if (language == L"pl-PL")
|
||||
{
|
||||
lang = LANG_POLISH;
|
||||
sublang = SUBLANG_POLISH_POLAND;
|
||||
}
|
||||
else if (language == L"pt-BR")
|
||||
{
|
||||
lang = LANG_PORTUGUESE;
|
||||
sublang = SUBLANG_PORTUGUESE_BRAZILIAN;
|
||||
}
|
||||
else if (language == L"pt-PT")
|
||||
{
|
||||
lang = LANG_PORTUGUESE;
|
||||
sublang = SUBLANG_PORTUGUESE;
|
||||
}
|
||||
else if (language == L"ru-RU")
|
||||
{
|
||||
lang = LANG_RUSSIAN;
|
||||
sublang = SUBLANG_RUSSIAN_RUSSIA;
|
||||
}
|
||||
else if (language == L"sv-SE")
|
||||
{
|
||||
lang = LANG_SWEDISH;
|
||||
sublang = SUBLANG_SWEDISH;
|
||||
}
|
||||
else if (language == L"tr-TR")
|
||||
{
|
||||
lang = LANG_TURKISH;
|
||||
sublang = SUBLANG_TURKISH_TURKEY;
|
||||
}
|
||||
else if (language == L"uk-UA")
|
||||
{
|
||||
lang = LANG_UKRAINIAN;
|
||||
sublang = SUBLANG_UKRAINIAN_UKRAINE;
|
||||
}
|
||||
else if (language == L"zh-CN")
|
||||
{
|
||||
lang = LANG_CHINESE_SIMPLIFIED;
|
||||
sublang = SUBLANG_CHINESE_SIMPLIFIED;
|
||||
}
|
||||
else if (language == L"zh-TW")
|
||||
{
|
||||
lang = LANG_CHINESE_TRADITIONAL;
|
||||
sublang = SUBLANG_CHINESE_TRADITIONAL;
|
||||
}
|
||||
|
||||
WORD languageID = MAKELANGID(lang, sublang);
|
||||
ATL::CStringW result;
|
||||
try
|
||||
{
|
||||
if (!result.LoadStringW(instance, resource_id, languageID))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!result.IsEmpty())
|
||||
{
|
||||
return std::wstring(result);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring get_resource_string(UINT resource_id, HINSTANCE instance, const wchar_t* fallback)
|
||||
{
|
||||
// Try to load en-us string as the first fallback.
|
||||
std::wstring english_string = get_english_fallback_string(resource_id, instance);
|
||||
|
||||
std::wstring language_override_resource = get_resource_string_language_override(resource_id, instance);
|
||||
|
||||
if (!language_override_resource.empty())
|
||||
{
|
||||
return language_override_resource;
|
||||
}
|
||||
else
|
||||
{
|
||||
wchar_t* text_ptr;
|
||||
auto length = LoadStringW(instance, resource_id, reinterpret_cast<wchar_t*>(&text_ptr), 0);
|
||||
if (length == 0)
|
||||
{
|
||||
if (!english_string.empty())
|
||||
{
|
||||
return std::wstring(english_string);
|
||||
}
|
||||
else
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return { text_ptr, static_cast<std::size_t>(length) };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,210 +2,20 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
#include <atlstr.h>
|
||||
|
||||
#include <common/utils/language_helper.h>
|
||||
|
||||
|
||||
inline std::wstring get_english_fallback_string(UINT resource_id, HINSTANCE instance)
|
||||
{
|
||||
// Try to load en-us string as the first fallback.
|
||||
WORD english_language = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
|
||||
|
||||
ATL::CStringW english_string;
|
||||
try
|
||||
{
|
||||
if (!english_string.LoadStringW(instance, resource_id, english_language))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::wstring(english_string);
|
||||
}
|
||||
|
||||
inline std::wstring get_resource_string_language_override(UINT resource_id, HINSTANCE instance)
|
||||
{
|
||||
static std::wstring language = LanguageHelpers::load_language();
|
||||
unsigned lang = LANG_ENGLISH;
|
||||
unsigned sublang = SUBLANG_ENGLISH_US;
|
||||
|
||||
if (!language.empty())
|
||||
{
|
||||
// Language list taken from Resources.wxs
|
||||
if (language == L"ar-SA")
|
||||
{
|
||||
lang = LANG_ARABIC;
|
||||
sublang = SUBLANG_ARABIC_SAUDI_ARABIA;
|
||||
}
|
||||
else if (language == L"cs-CZ")
|
||||
{
|
||||
lang = LANG_CZECH;
|
||||
sublang = SUBLANG_CZECH_CZECH_REPUBLIC;
|
||||
}
|
||||
else if (language == L"de-DE")
|
||||
{
|
||||
lang = LANG_GERMAN;
|
||||
sublang = SUBLANG_GERMAN;
|
||||
}
|
||||
else if (language == L"en-US")
|
||||
{
|
||||
lang = LANG_ENGLISH;
|
||||
sublang = SUBLANG_ENGLISH_US;
|
||||
}
|
||||
else if (language == L"es-ES")
|
||||
{
|
||||
lang = LANG_SPANISH;
|
||||
sublang = SUBLANG_SPANISH;
|
||||
}
|
||||
else if (language == L"fa-IR")
|
||||
{
|
||||
lang = LANG_PERSIAN;
|
||||
sublang = SUBLANG_PERSIAN_IRAN;
|
||||
}
|
||||
else if (language == L"fr-FR")
|
||||
{
|
||||
lang = LANG_FRENCH;
|
||||
sublang = SUBLANG_FRENCH;
|
||||
}
|
||||
else if (language == L"he-IL")
|
||||
{
|
||||
lang = LANG_HEBREW;
|
||||
sublang = SUBLANG_HEBREW_ISRAEL;
|
||||
}
|
||||
else if (language == L"hu-HU")
|
||||
{
|
||||
lang = LANG_HUNGARIAN;
|
||||
sublang = SUBLANG_HUNGARIAN_HUNGARY;
|
||||
}
|
||||
else if (language == L"it-IT")
|
||||
{
|
||||
lang = LANG_ITALIAN;
|
||||
sublang = SUBLANG_ITALIAN;
|
||||
}
|
||||
else if (language == L"ja-JP")
|
||||
{
|
||||
lang = LANG_JAPANESE;
|
||||
sublang = SUBLANG_JAPANESE_JAPAN;
|
||||
}
|
||||
else if (language == L"ko-KR")
|
||||
{
|
||||
lang = LANG_KOREAN;
|
||||
sublang = SUBLANG_KOREAN;
|
||||
}
|
||||
else if (language == L"nl-NL")
|
||||
{
|
||||
lang = LANG_DUTCH;
|
||||
sublang = SUBLANG_DUTCH;
|
||||
}
|
||||
else if (language == L"pl-PL")
|
||||
{
|
||||
lang = LANG_POLISH;
|
||||
sublang = SUBLANG_POLISH_POLAND;
|
||||
}
|
||||
else if (language == L"pt-BR")
|
||||
{
|
||||
lang = LANG_PORTUGUESE;
|
||||
sublang = SUBLANG_PORTUGUESE_BRAZILIAN;
|
||||
}
|
||||
else if (language == L"pt-PT")
|
||||
{
|
||||
lang = LANG_PORTUGUESE;
|
||||
sublang = SUBLANG_PORTUGUESE;
|
||||
}
|
||||
else if (language == L"ru-RU")
|
||||
{
|
||||
lang = LANG_RUSSIAN;
|
||||
sublang = SUBLANG_RUSSIAN_RUSSIA;
|
||||
}
|
||||
else if (language == L"sv-SE")
|
||||
{
|
||||
lang = LANG_SWEDISH;
|
||||
sublang = SUBLANG_SWEDISH;
|
||||
}
|
||||
else if (language == L"tr-TR")
|
||||
{
|
||||
lang = LANG_TURKISH;
|
||||
sublang = SUBLANG_TURKISH_TURKEY;
|
||||
}
|
||||
else if (language == L"uk-UA")
|
||||
{
|
||||
lang = LANG_UKRAINIAN;
|
||||
sublang = SUBLANG_UKRAINIAN_UKRAINE;
|
||||
}
|
||||
else if (language == L"zh-CN")
|
||||
{
|
||||
lang = LANG_CHINESE_SIMPLIFIED;
|
||||
sublang = SUBLANG_CHINESE_SIMPLIFIED;
|
||||
}
|
||||
else if (language == L"zh-TW")
|
||||
{
|
||||
lang = LANG_CHINESE_TRADITIONAL;
|
||||
sublang = SUBLANG_CHINESE_TRADITIONAL;
|
||||
}
|
||||
|
||||
WORD languageID = MAKELANGID(lang, sublang);
|
||||
ATL::CStringW result;
|
||||
try
|
||||
{
|
||||
if (!result.LoadStringW(instance, resource_id, languageID))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!result.IsEmpty())
|
||||
{
|
||||
return std::wstring(result);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// Get a string from the resource file
|
||||
inline std::wstring get_resource_string(UINT resource_id, HINSTANCE instance, const wchar_t* fallback)
|
||||
{
|
||||
// Try to load en-us string as the first fallback.
|
||||
std::wstring english_string = get_english_fallback_string(resource_id, instance);
|
||||
|
||||
std::wstring language_override_resource = get_resource_string_language_override(resource_id, instance);
|
||||
|
||||
if (!language_override_resource.empty())
|
||||
{
|
||||
return language_override_resource;
|
||||
}
|
||||
else
|
||||
{
|
||||
wchar_t* text_ptr;
|
||||
auto length = LoadStringW(instance, resource_id, reinterpret_cast<wchar_t*>(&text_ptr), 0);
|
||||
if (length == 0)
|
||||
{
|
||||
if (!english_string.empty())
|
||||
{
|
||||
return std::wstring(english_string);
|
||||
}
|
||||
else
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return { text_ptr, static_cast<std::size_t>(length) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" IMAGE_DOS_HEADER __ImageBase;
|
||||
|
||||
// Non-localizable - Load English fallback string
|
||||
std::wstring get_english_fallback_string(UINT resource_id, HINSTANCE instance);
|
||||
|
||||
// Non-localizable - Load string with language override
|
||||
std::wstring get_resource_string_language_override(UINT resource_id, HINSTANCE instance);
|
||||
|
||||
// Localizable - Load resource string with fallback support
|
||||
std::wstring get_resource_string(UINT resource_id, HINSTANCE instance, const wchar_t* fallback);
|
||||
|
||||
// Wrapper for getting a string from the resource file. Returns the resource id text when fails.
|
||||
#define GET_RESOURCE_STRING(resource_id) get_resource_string(resource_id, reinterpret_cast<HINSTANCE>(&__ImageBase), L#resource_id)
|
||||
#define GET_RESOURCE_STRING_FALLBACK(resource_id, fallback) get_resource_string(resource_id, reinterpret_cast<HINSTANCE>(&__ImageBase), fallback)
|
||||
|
||||
|
||||
|
||||
270
src/common/utils/shell_ext_registration.cpp
Normal file
270
src/common/utils/shell_ext_registration.cpp
Normal file
@@ -0,0 +1,270 @@
|
||||
#include "pch.h"
|
||||
#include "shell_ext_registration.h"
|
||||
|
||||
#include "../logger/logger.h"
|
||||
|
||||
namespace runtime_shell_ext
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
struct unique_hkey
|
||||
{
|
||||
HKEY h{ nullptr };
|
||||
unique_hkey() = default;
|
||||
explicit unique_hkey(HKEY handle) : h(handle) {}
|
||||
~unique_hkey() { if (h) RegCloseKey(h); }
|
||||
unique_hkey(const unique_hkey&) = delete;
|
||||
unique_hkey& operator=(const unique_hkey&) = delete;
|
||||
unique_hkey(unique_hkey&& other) noexcept : h(other.h) { other.h = nullptr; }
|
||||
unique_hkey& operator=(unique_hkey&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
if (h)
|
||||
{
|
||||
RegCloseKey(h);
|
||||
}
|
||||
h = other.h;
|
||||
other.h = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
HKEY get() const { return h; }
|
||||
HKEY* put()
|
||||
{
|
||||
if (h)
|
||||
{
|
||||
RegCloseKey(h);
|
||||
h = nullptr;
|
||||
}
|
||||
return &h;
|
||||
}
|
||||
};
|
||||
|
||||
std::wstring base_dir_from_module(HMODULE moduleInstance)
|
||||
{
|
||||
wchar_t buf[MAX_PATH];
|
||||
if (GetModuleFileNameW(moduleInstance, buf, MAX_PATH))
|
||||
{
|
||||
PathRemoveFileSpecW(buf);
|
||||
return buf;
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
std::wstring pick_existing_dll(const std::wstring& base, const std::vector<std::wstring>& candidates)
|
||||
{
|
||||
for (const auto& rel : candidates)
|
||||
{
|
||||
std::wstring full = base + L"\\" + rel;
|
||||
if (GetFileAttributesW(full.c_str()) != INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
return full;
|
||||
}
|
||||
}
|
||||
if (!candidates.empty())
|
||||
{
|
||||
return base + L"\\" + candidates.front();
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
bool sentinel_exists(const Spec& spec)
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, KEY_READ, key.put()) != ERROR_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
DWORD value = 0;
|
||||
DWORD size = sizeof(value);
|
||||
return RegQueryValueExW(key.get(), spec.sentinelValue.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size) == ERROR_SUCCESS && value == 1;
|
||||
}
|
||||
|
||||
void write_sentinel(const Spec& spec)
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD one = 1;
|
||||
RegSetValueExW(key.get(), spec.sentinelValue.c_str(), 0, REG_DWORD, reinterpret_cast<const BYTE*>(&one), sizeof(one));
|
||||
}
|
||||
}
|
||||
|
||||
void write_inproc_server(const Spec& spec, const std::wstring& dllPath)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
std::wstring clsidRoot = L"Software\\Classes\\CLSID\\"s + spec.clsid;
|
||||
std::wstring inprocKey = clsidRoot + L"\\InprocServer32";
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, clsidRoot.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
if (!spec.friendlyName.empty())
|
||||
{
|
||||
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(spec.friendlyName.c_str()), static_cast<DWORD>((spec.friendlyName.size() + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
if (spec.writeOptInEmptyValue)
|
||||
{
|
||||
const wchar_t* optIn = L"ContextMenuOptIn";
|
||||
const wchar_t empty = L'\0';
|
||||
RegSetValueExW(key.get(), optIn, 0, REG_SZ, reinterpret_cast<const BYTE*>(&empty), sizeof(empty));
|
||||
}
|
||||
}
|
||||
}
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, inprocKey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(dllPath.c_str()), static_cast<DWORD>((dllPath.size() + 1) * sizeof(wchar_t)));
|
||||
if (spec.writeThreadingModel)
|
||||
{
|
||||
const wchar_t* tm = L"Apartment";
|
||||
RegSetValueExW(key.get(), L"ThreadingModel", 0, REG_SZ, reinterpret_cast<const BYTE*>(tm), static_cast<DWORD>((wcslen(tm) + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring read_inproc_server(const Spec& spec)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
std::wstring inprocKey = L"Software\\Classes\\CLSID\\"s + spec.clsid + L"\\InprocServer32";
|
||||
unique_hkey key;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, inprocKey.c_str(), 0, KEY_READ, key.put()) != ERROR_SUCCESS)
|
||||
{
|
||||
return L"";
|
||||
}
|
||||
wchar_t buf[MAX_PATH];
|
||||
DWORD size = sizeof(buf);
|
||||
if (RegQueryValueExW(key.get(), nullptr, nullptr, nullptr, reinterpret_cast<LPBYTE>(buf), &size) == ERROR_SUCCESS)
|
||||
{
|
||||
return std::wstring(buf);
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
void write_default_value_key(const std::wstring& keyPath, const std::wstring& value)
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(value.c_str()), static_cast<DWORD>((value.size() + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
}
|
||||
|
||||
bool representative_association_exists(const Spec& spec)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
if (spec.representativeSystemExt.empty() || spec.systemFileAssocHandlerName.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
std::wstring keyPath = L"Software\\Classes\\SystemFileAssociations\\"s + spec.representativeSystemExt + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
|
||||
unique_hkey key;
|
||||
return RegOpenKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, KEY_READ, key.put()) == ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
bool EnsureRegistered(const Spec& spec, HMODULE moduleInstance)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
auto base = detail::base_dir_from_module(moduleInstance);
|
||||
auto dllPath = detail::pick_existing_dll(base, spec.dllFileCandidates);
|
||||
if (dllPath.empty())
|
||||
{
|
||||
Logger::error(L"Runtime registration: cannot locate dll path for CLSID {}", spec.clsid);
|
||||
return false;
|
||||
}
|
||||
bool exists = detail::sentinel_exists(spec);
|
||||
bool repaired = false;
|
||||
if (exists)
|
||||
{
|
||||
auto current = detail::read_inproc_server(spec);
|
||||
if (_wcsicmp(current.c_str(), dllPath.c_str()) != 0)
|
||||
{
|
||||
detail::write_inproc_server(spec, dllPath);
|
||||
repaired = true;
|
||||
}
|
||||
if (!detail::representative_association_exists(spec))
|
||||
{
|
||||
repaired = true;
|
||||
}
|
||||
}
|
||||
if (!exists)
|
||||
{
|
||||
detail::write_inproc_server(spec, dllPath);
|
||||
}
|
||||
if (!exists || repaired)
|
||||
{
|
||||
for (const auto& path : spec.contextMenuHandlerKeyPaths)
|
||||
{
|
||||
detail::write_default_value_key(path, spec.clsid);
|
||||
}
|
||||
for (const auto& path : spec.extraAssociationPaths)
|
||||
{
|
||||
detail::write_default_value_key(path, spec.clsid);
|
||||
}
|
||||
for (const auto& ext : spec.systemFileAssocExtensions)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
auto baseKey = L"Software\\Classes\\SystemFileAssociations\\"s + ext + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
|
||||
detail::write_default_value_key(baseKey, spec.clsid);
|
||||
}
|
||||
if (spec.logRepairs)
|
||||
{
|
||||
Logger::info(L"Runtime shell extension registration repaired {}", spec.clsid);
|
||||
}
|
||||
detail::write_sentinel(spec);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::trace(L"Runtime shell extension registration already up to date for {}", spec.clsid);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Unregister(const Spec& spec)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
|
||||
for (const auto& path : spec.contextMenuHandlerKeyPaths)
|
||||
{
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
|
||||
}
|
||||
|
||||
for (const auto& path : spec.extraAssociationPaths)
|
||||
{
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
|
||||
}
|
||||
|
||||
if (!spec.systemFileAssocExtensions.empty() && !spec.systemFileAssocHandlerName.empty())
|
||||
{
|
||||
for (const auto& ext : spec.systemFileAssocExtensions)
|
||||
{
|
||||
std::wstring keyPath = L"Software\\Classes\\SystemFileAssociations\\";
|
||||
keyPath += ext;
|
||||
keyPath += L"\\ShellEx\\ContextMenuHandlers\\";
|
||||
keyPath += spec.systemFileAssocHandlerName;
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, keyPath.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (!spec.clsid.empty())
|
||||
{
|
||||
std::wstring clsidRoot = L"Software\\Classes\\CLSID\\"s + spec.clsid;
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, clsidRoot.c_str());
|
||||
}
|
||||
|
||||
if (!spec.sentinelKey.empty() && !spec.sentinelValue.empty())
|
||||
{
|
||||
HKEY key{};
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, KEY_SET_VALUE, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
RegDeleteValueW(key, spec.sentinelValue.c_str());
|
||||
RegCloseKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
Logger::info(L"Runtime shell extension unregistered for {}", spec.clsid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
// Shared runtime shell extension registration utility for PowerToys modules.
|
||||
// Provides a generic EnsureRegistered function so individual modules only need
|
||||
// to supply a specification (CLSID, sentinel, handler key paths, etc.).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
@@ -9,258 +5,28 @@
|
||||
#include <windows.h>
|
||||
#include <shlwapi.h>
|
||||
|
||||
#include "../logger/logger.h"
|
||||
|
||||
namespace runtime_shell_ext
|
||||
{
|
||||
struct Spec
|
||||
{
|
||||
// Mandatory
|
||||
std::wstring clsid; // e.g. {GUID}
|
||||
std::wstring sentinelKey; // e.g. Software\\Microsoft\\PowerToys\\ModuleName
|
||||
std::wstring sentinelValue; // e.g. ContextMenuRegistered
|
||||
std::wstring clsid; // e.g. {GUID}
|
||||
std::wstring sentinelKey; // e.g. Software\\Microsoft\\PowerToys\\ModuleName
|
||||
std::wstring sentinelValue; // e.g. ContextMenuRegistered
|
||||
std::vector<std::wstring> dllFileCandidates; // relative filenames (pick first existing)
|
||||
std::vector<std::wstring> contextMenuHandlerKeyPaths; // full HKCU relative paths where default value = CLSID
|
||||
|
||||
// Optional
|
||||
std::wstring friendlyName; // if non-empty written as default under CLSID root
|
||||
bool writeOptInEmptyValue = true; // write ContextMenuOptIn="" under CLSID root (legacy pattern)
|
||||
bool writeThreadingModel = true; // write Apartment threading model
|
||||
std::wstring friendlyName; // if non-empty written as default under CLSID root
|
||||
bool writeOptInEmptyValue = true; // write ContextMenuOptIn="" under CLSID root (legacy pattern)
|
||||
bool writeThreadingModel = true; // write Apartment threading model
|
||||
std::vector<std::wstring> extraAssociationPaths; // additional key paths (DragDropHandlers etc.) default=CLSID
|
||||
std::vector<std::wstring> systemFileAssocExtensions; // e.g. .png -> Software\\Classes\\SystemFileAssociations\\.png\\ShellEx\\ContextMenuHandlers\\<HandlerName>
|
||||
std::wstring systemFileAssocHandlerName; // e.g. ImageResizer
|
||||
std::wstring representativeSystemExt; // used to decide if associations need repair (.png)
|
||||
std::wstring representativeSystemExt; // used to decide if associations need repair (.png)
|
||||
bool logRepairs = true;
|
||||
};
|
||||
|
||||
namespace detail
|
||||
{
|
||||
// Minimal RAII wrapper for HKEY
|
||||
struct unique_hkey
|
||||
{
|
||||
HKEY h{ nullptr };
|
||||
unique_hkey() = default;
|
||||
explicit unique_hkey(HKEY handle) : h(handle) {}
|
||||
~unique_hkey() { if (h) RegCloseKey(h); }
|
||||
unique_hkey(const unique_hkey&) = delete;
|
||||
unique_hkey& operator=(const unique_hkey&) = delete;
|
||||
unique_hkey(unique_hkey&& other) noexcept : h(other.h) { other.h = nullptr; }
|
||||
unique_hkey& operator=(unique_hkey&& other) noexcept { if (this != &other) { if (h) RegCloseKey(h); h = other.h; other.h = nullptr; } return *this; }
|
||||
HKEY get() const { return h; }
|
||||
HKEY* put() { if (h) { RegCloseKey(h); h = nullptr; } return &h; }
|
||||
};
|
||||
inline std::wstring base_dir_from_module(HMODULE h)
|
||||
{
|
||||
wchar_t buf[MAX_PATH];
|
||||
if (GetModuleFileNameW(h, buf, MAX_PATH))
|
||||
{
|
||||
PathRemoveFileSpecW(buf);
|
||||
return buf;
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
inline std::wstring pick_existing_dll(const std::wstring& base, const std::vector<std::wstring>& candidates)
|
||||
{
|
||||
for (const auto& rel : candidates)
|
||||
{
|
||||
std::wstring full = base + L"\\" + rel;
|
||||
if (GetFileAttributesW(full.c_str()) != INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
return full;
|
||||
}
|
||||
}
|
||||
if (!candidates.empty())
|
||||
{
|
||||
return base + L"\\" + candidates.front();
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
inline bool sentinel_exists(const Spec& spec)
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, KEY_READ, key.put()) != ERROR_SUCCESS)
|
||||
return false;
|
||||
DWORD v = 0; DWORD sz = sizeof(v);
|
||||
return RegQueryValueExW(key.get(), spec.sentinelValue.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&v), &sz) == ERROR_SUCCESS && v == 1;
|
||||
}
|
||||
|
||||
inline void write_sentinel(const Spec& spec)
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD one = 1;
|
||||
RegSetValueExW(key.get(), spec.sentinelValue.c_str(), 0, REG_DWORD, reinterpret_cast<const BYTE*>(&one), sizeof(one));
|
||||
}
|
||||
}
|
||||
|
||||
inline void write_inproc_server(const Spec& spec, const std::wstring& dllPath)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
std::wstring clsidRoot = L"Software\\Classes\\CLSID\\"s + spec.clsid;
|
||||
std::wstring inprocKey = clsidRoot + L"\\InprocServer32";
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, clsidRoot.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
if (!spec.friendlyName.empty())
|
||||
{
|
||||
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(spec.friendlyName.c_str()), static_cast<DWORD>((spec.friendlyName.size() + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
if (spec.writeOptInEmptyValue)
|
||||
{
|
||||
const wchar_t* optIn = L"ContextMenuOptIn";
|
||||
const wchar_t empty = L'\0';
|
||||
RegSetValueExW(key.get(), optIn, 0, REG_SZ, reinterpret_cast<const BYTE*>(&empty), sizeof(empty));
|
||||
}
|
||||
}
|
||||
}
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, inprocKey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(dllPath.c_str()), static_cast<DWORD>((dllPath.size() + 1) * sizeof(wchar_t)));
|
||||
if (spec.writeThreadingModel)
|
||||
{
|
||||
const wchar_t* tm = L"Apartment";
|
||||
RegSetValueExW(key.get(), L"ThreadingModel", 0, REG_SZ, reinterpret_cast<const BYTE*>(tm), static_cast<DWORD>((wcslen(tm) + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline std::wstring read_inproc_server(const Spec& spec)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
std::wstring inprocKey = L"Software\\Classes\\CLSID\\"s + spec.clsid + L"\\InprocServer32";
|
||||
unique_hkey key;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, inprocKey.c_str(), 0, KEY_READ, key.put()) != ERROR_SUCCESS)
|
||||
return L"";
|
||||
wchar_t buf[MAX_PATH]; DWORD sz = sizeof(buf);
|
||||
if (RegQueryValueExW(key.get(), nullptr, nullptr, nullptr, reinterpret_cast<LPBYTE>(buf), &sz) == ERROR_SUCCESS)
|
||||
return std::wstring(buf);
|
||||
return L"";
|
||||
}
|
||||
|
||||
inline void write_default_value_key(const std::wstring& keyPath, const std::wstring& value)
|
||||
{
|
||||
unique_hkey key;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
|
||||
{
|
||||
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(value.c_str()), static_cast<DWORD>((value.size() + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
}
|
||||
|
||||
inline bool representative_association_exists(const Spec& spec)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
if (spec.representativeSystemExt.empty() || spec.systemFileAssocHandlerName.empty())
|
||||
return true;
|
||||
std::wstring keyPath = L"Software\\Classes\\SystemFileAssociations\\"s + spec.representativeSystemExt + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
|
||||
unique_hkey key;
|
||||
return RegOpenKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, KEY_READ, key.put()) == ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool EnsureRegistered(const Spec& spec, HMODULE moduleInstance)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
auto base = detail::base_dir_from_module(moduleInstance);
|
||||
auto dllPath = detail::pick_existing_dll(base, spec.dllFileCandidates);
|
||||
if (dllPath.empty())
|
||||
{
|
||||
Logger::error(L"Runtime registration: cannot locate dll path for CLSID {}", spec.clsid);
|
||||
return false;
|
||||
}
|
||||
bool exists = detail::sentinel_exists(spec);
|
||||
bool repaired = false;
|
||||
if (exists)
|
||||
{
|
||||
auto current = detail::read_inproc_server(spec);
|
||||
if (_wcsicmp(current.c_str(), dllPath.c_str()) != 0)
|
||||
{
|
||||
detail::write_inproc_server(spec, dllPath);
|
||||
repaired = true;
|
||||
}
|
||||
if (!detail::representative_association_exists(spec))
|
||||
{
|
||||
repaired = true;
|
||||
}
|
||||
}
|
||||
if (!exists)
|
||||
{
|
||||
detail::write_inproc_server(spec, dllPath);
|
||||
}
|
||||
if (!exists || repaired)
|
||||
{
|
||||
for (const auto& path : spec.contextMenuHandlerKeyPaths)
|
||||
{
|
||||
detail::write_default_value_key(path, spec.clsid);
|
||||
}
|
||||
for (const auto& path : spec.extraAssociationPaths)
|
||||
{
|
||||
detail::write_default_value_key(path, spec.clsid);
|
||||
}
|
||||
if (!spec.systemFileAssocExtensions.empty() && !spec.systemFileAssocHandlerName.empty())
|
||||
{
|
||||
for (const auto& ext : spec.systemFileAssocExtensions)
|
||||
{
|
||||
std::wstring path = L"Software\\Classes\\SystemFileAssociations\\"s + ext + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
|
||||
detail::write_default_value_key(path, spec.clsid);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!exists)
|
||||
{
|
||||
detail::write_sentinel(spec);
|
||||
Logger::info(L"Runtime registration completed for CLSID {}", spec.clsid);
|
||||
}
|
||||
else if (repaired && spec.logRepairs)
|
||||
{
|
||||
Logger::info(L"Runtime registration repaired for CLSID {}", spec.clsid);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool Unregister(const Spec& spec)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
// Remove handler key paths
|
||||
for (const auto& path : spec.contextMenuHandlerKeyPaths)
|
||||
{
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
|
||||
}
|
||||
// Remove extra association paths (e.g., drag & drop handlers)
|
||||
for (const auto& path : spec.extraAssociationPaths)
|
||||
{
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
|
||||
}
|
||||
// Remove per-extension system file association handler keys
|
||||
if (!spec.systemFileAssocExtensions.empty() && !spec.systemFileAssocHandlerName.empty())
|
||||
{
|
||||
for (const auto& ext : spec.systemFileAssocExtensions)
|
||||
{
|
||||
std::wstring keyPath = L"Software\\Classes\\SystemFileAssociations\\"s + ext + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, keyPath.c_str());
|
||||
}
|
||||
}
|
||||
// Remove CLSID branch
|
||||
if (!spec.clsid.empty())
|
||||
{
|
||||
std::wstring clsidRoot = L"Software\\Classes\\CLSID\\"s + spec.clsid;
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, clsidRoot.c_str());
|
||||
}
|
||||
// Remove sentinel value (not deleting entire key to avoid disturbing other values)
|
||||
if (!spec.sentinelKey.empty() && !spec.sentinelValue.empty())
|
||||
{
|
||||
HKEY hKey{};
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
RegDeleteValueW(hKey, spec.sentinelValue.c_str());
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
}
|
||||
Logger::info(L"Successfully unregistered CLSID {}", spec.clsid);
|
||||
return true;
|
||||
}
|
||||
bool EnsureRegistered(const Spec& spec, HMODULE moduleInstance);
|
||||
bool Unregister(const Spec& spec);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user