Skip to content

Fix winapp run/init bugs and WinAppSDK 2.0.1 forward compat#512

Merged
nmetulev merged 10 commits intomainfrom
nm/fix-winapp-run-init-bugs
May 1, 2026
Merged

Fix winapp run/init bugs and WinAppSDK 2.0.1 forward compat#512
nmetulev merged 10 commits intomainfrom
nm/fix-winapp-run-init-bugs

Conversation

@nmetulev
Copy link
Copy Markdown
Member

Fixes the winapp run Developer Mode required misreport, the winapp init (null) error after git clean, and the WinAppSDK 2.0.1 runtime-dependency test breakage. Splits cleanly so each fix is independently reviewable and bisectable.

Commits

  1. fix(register): improve registration failure error reporting — HRESULT �x80073CFB (ERROR_INSTALL_PACKAGE_ALREADY_EXISTS) was being misreported as a Developer Mode error. Now scopes the dev-mode-error mapping to �x800704EC only, and emits an actionable hint with the real package identity (parsed from the manifest) for �x80073CFB conflicts.
  2. fix(unregister): safe per-package unregistration to prevent data loss — Adds IPackageRegistrationService.UnregisterByFullNameAsync. Rewrites MsixService.UnregisterExistingPackageAsync as a two-pass loop (classify all → then remove individually by FullName). Replaces the prefix StartsWith(cwd) containment check with a segment-aware Path.GetRelativePath-based helper. Fixes two data-loss bugs: per-package safety check bypassed (H1), and sibling-directory misclassification (H2).
  3. fix(status): surface real exception messages instead of (null)GroupableTask<T>.ExecuteAsync now populates the error tuple's Item2 with ex.Message (stack trace appended only at Debug level). StatusService falls back to "Operation failed without an error message." when CompletedMessage is null/whitespace.
  4. fix(nuget): handle pre-stripped brackets in version range parserParseMinimumVersion no longer early-returns when brackets are absent. Fixes a silent breakage that only fires on cache-warm runs (e.g. after git clean of .winapp while the global NuGet cache still has the package), where FetchDirectDependenciesAsync strips brackets before handing a comma-separated range to the parser.
  5. fix(runtime): support WinAppSDK 2.x major-only framework naming — WinAppSDK 2.0.1 dropped the minor version from the framework package name (Microsoft.WindowsAppRuntime.2 instead of Microsoft.WindowsAppRuntime.2.0). Relaxes the runtime MSIX regex, switches the E2E test to invariant assertions (name prefix + MinVersion derived from SDK version), and refreshes the stale electron sample manifest.
  6. chore(docs): regenerate auto-generated docs and command wrappersscripts/build-cli.ps1 output for upstream description changes.

Tests added

  • PackageRegistrationServiceTests — 8 tests covering BuildRegistrationException hint variants and IsSideloadPolicyError.
  • MsixServiceTests — 9 tests for UnregisterExistingPackageAsync (no installs, dev-mode, non-dev in-tree, non-dev out-of-tree throws, sibling-prefix regression, mixed dev+non-dev refusing all to validate H1 fix) plus IsPathInsideDirectory.
  • GroupableTaskAndStatusErrorTests — 9 tests for tuple-error population, Debug-only stack inclusion, and the StatusService null-message fallback.
  • NugetServiceTests — 12 DataRow cases for ParseMinimumVersion.

Verification

  • Solution builds clean at every commit (verified via per-commit checkout + dotnet build).
  • 63/63 related tests pass.
  • E2E_DotNetApp_PackageShouldIncludeRuntimeDependency (both stable + experimental data rows) now passes — was 1 failed / 1 passed before.
  • Full suite baseline: 5 pre-existing failures remain, all in node-wrapper E2E tests requiring cd src\winapp-npm; npm run build to produce dist/cli.js (environment-only; not regressions).

🤖 Generated with GitHub Copilot CLI

Copilot AI review requested due to automatic review settings April 30, 2026 06:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses several CLI reliability and compatibility issues: improved MSIX registration/unregistration behavior and error reporting, fixes NuGet version-range parsing, and updates WinAppSDK runtime dependency detection (including test/doc regeneration) to be forward-compatible with WinAppSDK 2.0.1 naming changes.

Changes:

  • Refines MSIX registration error mapping/hints and strengthens unregistration safety checks to prevent unintended data loss.
  • Fixes NuGet minimum-version parsing when version ranges have had brackets/parens pre-stripped.
  • Relaxes Windows App Runtime MSIX naming matching for WinAppSDK 2.x and updates E2E/sample/docs accordingly.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/winapp-npm/src/winapp-commands.ts Regenerated command wrappers / updated init description text from newer schema.
src/winapp-CLI/WinApp.Cli/Services/StatusService.cs Adds fallback error text when CompletedMessage is null/whitespace.
src/winapp-CLI/WinApp.Cli/Services/PackageRegistrationService.cs Improves registration failure reporting; adds per-fullname uninstall API; adjusts policy error detection.
src/winapp-CLI/WinApp.Cli/Services/NugetService.cs Fixes ParseMinimumVersion to handle comma-separated ranges even without brackets.
src/winapp-CLI/WinApp.Cli/Services/MsixService.Runtime.cs Updates runtime MSIX filename regex to match WinAppSDK 2.x naming.
src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs Makes unregistration safer via two-pass classification + per-package removal; adds segment-aware path containment helper.
src/winapp-CLI/WinApp.Cli/Services/IPackageRegistrationService.cs Adds UnregisterByFullNameAsync contract and documentation.
src/winapp-CLI/WinApp.Cli/ConsoleTasks/GroupableTask.cs Improves tuple-task error propagation by surfacing exception messages (and stack at Debug).
src/winapp-CLI/WinApp.Cli.Tests/PackageRegistrationServiceTests.cs Adds unit tests for registration exception building and sideload policy error detection.
src/winapp-CLI/WinApp.Cli.Tests/NugetServiceTests.cs Adds data-driven tests for ParseMinimumVersion.
src/winapp-CLI/WinApp.Cli.Tests/MsixServiceTests.cs Adds tests covering unregister safety rules and IsPathInsideDirectory behavior.
src/winapp-CLI/WinApp.Cli.Tests/GroupableTaskAndStatusErrorTests.cs Adds tests ensuring error messages aren’t logged as “(null)” and stack traces are Debug-only.
src/winapp-CLI/WinApp.Cli.Tests/FakePackageRegistrationService.cs Extends fake to support UnregisterByFullNameAsync call tracking.
src/winapp-CLI/WinApp.Cli.Tests/EndToEndTests.cs Updates runtime dependency assertions to be invariant across SDK naming changes.
samples/electron/appxmanifest.xml Refreshes sample dependency to WinAppRuntime 2.x major-only naming.
docs/npm-usage.md Regenerates npm usage docs and updates init / utility function descriptions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/winapp-CLI/WinApp.Cli/Services/PackageRegistrationService.cs Outdated
Comment thread src/winapp-CLI/WinApp.Cli/Services/PackageRegistrationService.cs Outdated
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs
Comment thread docs/npm-usage.md
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 30, 2026

Build Metrics Report

Binary Sizes

Artifact Baseline Current Delta
CLI (ARM64) 30.66 MB 30.67 MB 📈 +10.5 KB (+0.03%)
CLI (x64) 31.03 MB 31.04 MB 📈 +10.0 KB (+0.03%)
MSIX (ARM64) N/A 12.94 MB N/A
MSIX (x64) N/A 13.75 MB N/A
NPM Package N/A 26.91 MB N/A
NuGet Package N/A 27.00 MB N/A
VS Code Extension N/A 19.73 MB N/A

Test Results

825 passed, 1 skipped out of 826 tests in 848.1s (+39 tests, +423.7s vs. baseline)

Test Coverage

21.5% line coverage, 36.7% branch coverage · ✅ no change vs. baseline

CLI Startup Time

34ms median (x64, winapp --version) · ✅ -9ms vs. baseline


Updated 2026-05-01 01:42:55 UTC · commit 2c411c2 · workflow run

nmetulev pushed a commit that referenced this pull request May 1, 2026
…ter/unregister paths

Address Copilot reviewer findings on PR #512:

- PackageRegistrationService.RegisterLooseLayoutAsync / RegisterSparseAsync:
  exception filters now also exclude OperationCanceledException so user
  cancellations aren't wrapped into InvalidOperationException via
  BuildRegistrationException.
- MsixService.UnregisterExistingPackageAsync: explicit catch+rethrow for
  OperationCanceledException so cancellation isn't silently reported as a
  normal "no package removed" outcome. Updated the catch-all log text to
  reflect that failures can occur during removal as well as inspection.
- Added clarifying comment at the FindDevPackages call site noting it
  returns *all* same-name packages (not just dev-mode), which is what the
  IsDevelopmentMode classification below relies on.
- Added regression test ensuring OperationCanceledException from
  UnregisterByFullNameAsync propagates through UnregisterExistingPackageAsync.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
nmetulev pushed a commit that referenced this pull request May 1, 2026
…ter/unregister paths

Address Copilot reviewer findings on PR #512:

- PackageRegistrationService.RegisterLooseLayoutAsync / RegisterSparseAsync:
  exception filters now also exclude OperationCanceledException so user
  cancellations aren't wrapped into InvalidOperationException via
  BuildRegistrationException.
- MsixService.UnregisterExistingPackageAsync: explicit catch+rethrow for
  OperationCanceledException so cancellation isn't silently reported as a
  normal "no package removed" outcome. Updated the catch-all log text to
  reflect that failures can occur during removal as well as inspection.
- Added clarifying comment at the FindDevPackages call site noting it
  returns *all* same-name packages (not just dev-mode), which is what the
  IsDevelopmentMode classification below relies on.
- Added regression test ensuring OperationCanceledException from
  UnregisterByFullNameAsync propagates through UnregisterExistingPackageAsync.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nmetulev nmetulev force-pushed the nm/fix-winapp-run-init-bugs branch from a11c2f9 to 51c0d54 Compare May 1, 2026 01:11
nmetulev and others added 9 commits April 30, 2026 18:11
HRESULT 0x80073CFB (ERROR_INSTALL_PACKAGE_ALREADY_EXISTS) was being misreported as a Developer Mode error, hiding the real cause: a same-identity non-dev-mode package is already installed and blocking re-registration as loose files.

Changes:

- Rename IsDeveloperModeError -> IsSideloadPolicyError, scoped to 0x800704EC (Group Policy block) only.

- Add BuildRegistrationException helper that surfaces the real ErrorText + HRESULT and, for 0x80073CFB conflicts, includes an actionable 'Get-AppxPackage <identity> | Remove-AppxPackage' hint.

- Read the package identity name from the manifest in RegisterLooseLayoutAsync / RegisterSparseAsync so the hint embeds the real identity instead of a literal '<PackageName>' placeholder.

- Make IsSideloadPolicyError + BuildRegistrationException internal static for direct unit testing.

Adds PackageRegistrationServiceTests covering all hint branches.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
MsixService.UnregisterExistingPackageAsync had two data-loss bugs surfaced by the recent auto-recovery code in winapp run:

H1 (correctness): the per-package safety loop iterated FindDevPackages but every removal still called the name-based UnregisterAsync(packageName), which removes ALL same-name packages. The first iteration could wipe sibling packages before their safety check ran.

H2 (security/correctness): the in-tree containment check used StartsWith(cwd, OrdinalIgnoreCase), so a sibling directory whose path shared a string prefix (e.g. C:\\proj vs C:\\project2) was misclassified as 'inside the project' and silently removed with preserveAppData: false, deleting end-user app data.

Changes:

- Add IPackageRegistrationService.UnregisterByFullNameAsync(fullName, preserveAppData, ct) and FakePackageRegistrationService impl that records UnregisterByFullNameCalls.

- Rewrite UnregisterExistingPackageAsync as a two-pass loop: first classify all installed packages and refuse the entire operation if any non-dev install lives outside the project tree; then remove each by FullName so one iteration cannot wipe packages a later check would reject.

- Replace prefix StartsWith with new IsPathInsideDirectory helper that uses Path.GetRelativePath plus rooted/'..' guards. Sibling dirs with shared prefixes are now correctly treated as outside.

Adds 9 MsixServiceTests covering: no installs, dev-mode, non-dev in-tree, non-dev out-of-tree throws, sibling-prefix regression, mixed dev+non-dev refusing all (validates H1 fix), and direct IsPathInsideDirectory checks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a GroupableTask<T>-wrapped operation threw, two bugs combined to render
the user-visible error as '[ERROR] - (null)':

1. GroupableTask<T>.ExecuteAsync only Debug.WriteLine'd the exception. For
   T = (int, string) it set Item1 = 1 but left Item2 = default (null).
2. StatusService then logged that null via 'logger.LogError(\"{CompletedMessage}\", null)',
   which FormattedLogValues renders as the literal string '(null)'.

Changes:
- GroupableTask<T> now populates Item2 with ex.Message (or the exception type
  name when blank), and appends the stack trace only when the logger has
  Debug enabled.
- StatusService null-guards CompletedMessage and falls back to
  'Operation failed without an error message.' when Item2 is null/whitespace.

Adds GroupableTaskAndStatusErrorTests with 9 cases covering message surfacing,
type-name fallback, Debug-only stack inclusion, success-path passthrough,
null/whitespace fallback, and verbose-hint suppression at Debug level.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ParseMinimumVersion had an early-return for 'no brackets present' that
returned the input verbatim. This broke when FetchDirectDependenciesAsync
fed it a version range from the HTTP NuGet feed: that code path strips
brackets up-front via BracketsAndParenthesesRegex but leaves commas
intact, so a multi-version range like '2.0.300, 3.0.0' was returned
unchanged and later failed NuGet version parsing as the literal string
'2.0.300, 3.0.0'.

Two install paths exist with different bracket handling:
- InstallPackageRecursiveAsync reads ranges from local nuspecs (brackets
  intact) -> ParseMinimumVersion's bracket branch handled it correctly.
- GetPackageDependenciesAsync uses the HTTP fetcher (brackets pre-stripped)
  -> hit the buggy no-bracket early return.

This is why the bug only fired on cache-validate runs (e.g. after
git clean of .winapp but with the global NuGet cache still warm) and
not on a from-scratch first install.

Fix: split on ',' unconditionally and take the lowest minimum, regardless
of whether brackets were present.

Adds 12 NugetServiceTests DataRow cases covering single versions, exclusive
and inclusive ranges, comma-separated lists with and without brackets,
prerelease versions, and whitespace handling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
WinAppSDK 2.0.1 stable changed the framework package naming convention:
the minor version was dropped from the runtime identity. The package
name in MSIX.inventory is now 'Microsoft.WindowsAppRuntime.2' instead
of 'Microsoft.WindowsAppRuntime.2.0'.

Naming convention table:
- 1.x stable / per-minor SxS:        Microsoft.WindowsAppRuntime.1.8
- 2.x stable / major-only (2.0.1+):  Microsoft.WindowsAppRuntime.2
- 2.x experimental:                  Microsoft.WindowsAppRuntime.2-experimentalN

Two things broke:

1. WindowsAppRuntimeMsixRegex required two dotted numeric segments
   (\d+\.\d+) so 'Microsoft.WindowsAppRuntime.2.msix' did not match.
   This is only a latent bug because it sits in the inventory-parse
   fallback path (the fallback ?? msixFiles[0] currently happens to
   pick the right file by alphabetical order), but it could silently
   pick a DDLM/Singleton/Main variant if file ordering ever shifted.

2. EndToEndTests.E2E_DotNetApp_PackageShouldIncludeRuntimeDependency
   reconstructed the full expected name as
   'Microsoft.WindowsAppRuntime.{major}.{minor}' for stable releases,
   so it asserted 'Microsoft.WindowsAppRuntime.2.0' against an actual
   manifest containing 'Microsoft.WindowsAppRuntime.2'.

Changes:
- Relax the regex to '\d+(\.\d+)?' so single-segment 2.x and any
  hypothetical future shape (2.0.2, 2.1.0 either-way, 3.0) all match.
- Drop the test's full-name reconstruction in favour of two invariants:
  PackageDependency name starts with Microsoft.WindowsAppRuntime.{major},
  and MinVersion starts with the SDK version's leading three components.
  This survives any future SDK naming changes since the CLI itself
  faithfully propagates whatever name MSIX.inventory ships with.
- Refresh samples/electron/appxmanifest.xml from the stale pre-2.0
  experimental placeholder to a current 2.0.1 reference.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Run via scripts/build-cli.ps1. Picks up unrelated upstream description
changes for 'init' and 'addMsixIdentityToExe' that hadn't been
regenerated yet (init mentions --setup-sdks none; addMsixIdentityToExe
references appxmanifest.xml instead of Package.appxmanifest), and
bumps the source schema version stamp from 0.2.2 to 0.3.1.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…p package (#496)

When multiple `.exe` files exist in the build output (e.g.
`createdump.exe` alongside the actual app), the identity registration
path (`winapp run`) picked the first one alphabetically — often
`createdump.exe` — causing the app to crash on launch.

**Root cause:** `MsixService.Identity.cs` had its own exe detection
logic using `EnumerateFiles` with `AllDirectories`, bypassing the
filtering used by `winapp package`.

**Changes:**
- Filter known .NET runtime tools (`createdump.exe`, `apphost.exe`) via
a shared `IsRuntimeToolExecutable()` helper.
- Reuse `ResolveManifestPlaceholders` in the identity path so `winapp
run` and `winapp package` share the same exe detection, filtering, and
error handling.
- Add `--executable` / `--exe` option to `winapp run` (mirrors `winapp
package`) so users can disambiguate when the build output legitimately
contains multiple exes.
- Restore an explicit "executable not found in output directory" error
in the identity path.
- Add regression tests for the createdump / apphost filtering cases.

---------

Co-authored-by: Nikola Metulev <711864+nmetulev@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Zach Teutsch <88554871+zateutsch@users.noreply.github.com>
…ter/unregister paths

Address Copilot reviewer findings on PR #512:

- PackageRegistrationService.RegisterLooseLayoutAsync / RegisterSparseAsync:
  exception filters now also exclude OperationCanceledException so user
  cancellations aren't wrapped into InvalidOperationException via
  BuildRegistrationException.
- MsixService.UnregisterExistingPackageAsync: explicit catch+rethrow for
  OperationCanceledException so cancellation isn't silently reported as a
  normal "no package removed" outcome. Updated the catch-all log text to
  reflect that failures can occur during removal as well as inspection.
- Added clarifying comment at the FindDevPackages call site noting it
  returns *all* same-name packages (not just dev-mode), which is what the
  IsDevelopmentMode classification below relies on.
- Added regression test ensuring OperationCanceledException from
  UnregisterByFullNameAsync propagates through UnregisterExistingPackageAsync.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nmetulev nmetulev force-pushed the nm/fix-winapp-run-init-bugs branch from 51c0d54 to 936cedd Compare May 1, 2026 01:11
@nmetulev nmetulev enabled auto-merge (squash) May 1, 2026 01:16
@nmetulev nmetulev disabled auto-merge May 1, 2026 04:18
@nmetulev nmetulev merged commit 3593c25 into main May 1, 2026
21 checks passed
@nmetulev nmetulev deleted the nm/fix-winapp-run-init-bugs branch May 1, 2026 16:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants