Skip to content

Commit 0b4bd31

Browse files
authored
Fix Windows uninstaller crash with launcher strategy (#115)
* Attempt to fix Uninstall process * Implement launcher strategy to fix uninstaller crash on Windows Problem: - Uninstaller crashed with access violation (0xc0000005) when executed from installation directory - .NET Single-File runtime extraction fails in locked/restricted directories - Crash occurred before Program.Main, making debugging impossible Root Cause: - When executed from AppData\Local\Programs, .NET runtime cannot extract bundled files - Desktop execution worked fine, proving issue was location-dependent Solution Implemented: - Created UninstallLauncher.exe (WinExe, ~12MB single-file .NET 8 app) - Launcher copies Uninstall.exe to %TEMP%\DotnetPackaging\Uninstallers\{GUID}\ - Executes uninstaller from %TEMP% and waits for completion (WaitForExit) - Registry UninstallString points to launcher instead of uninstaller directly Key Changes: - New project: src/DotnetPackaging.Exe.UninstallLauncher/ * WinExe (no console window) * Single-file, trimmed .NET 8 * Handles copy, execute, and wait logic - Modified src/DotnetPackaging.Exe.Installer/Core/Installer.cs: * Copies installer as Uninstall.exe to installation directory * Extracts UninstallLauncher.exe from embedded resources * Registry points to launcher - Updated DotnetPackaging.Exe.Installer.csproj: * Embeds UninstallLauncher.exe as resource - Comprehensive documentation in WARP.md and AGENTS.md: * Architecture and execution flow * Implementation details * Why it works * 6 future improvements (Native AOT, GitHub downloads, etc.) * Testing checklist * Lessons learned Benefits: ✅ Uninstaller always runs from %TEMP% (no extraction issues) ✅ No console window (WinExe) ✅ No Windows 'program may not have uninstalled correctly' dialog (WaitForExit) ✅ Robust to %TEMP% cleanup (recreates on each run) ✅ Fallback to direct uninstaller if launcher extraction fails Testing: ✅ Install/uninstall works correctly ✅ No console window appears ✅ No Windows error dialogs ✅ Uninstaller UI shows properly ✅ Works after multiple install/uninstall cycles Future Improvements: - Native AOT or C++ launcher (reduce from 12MB to <1MB) - Download launcher from GitHub releases (smaller installer) - Self-deleting temp uninstaller - Retry logic for antivirus locks See WARP.md lines 383-618 for complete documentation. * Restore installation directory removal during uninstall * Add UninstallLauncher project to solution
1 parent ac4c54f commit 0b4bd31

24 files changed

+1630
-112
lines changed

AGENTS.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,177 @@ Windows EXE (.exe)
243243
- Detección avanzada de ejecutable e icono (paridad con .deb/.appimage).
244244
- Modo silencioso.
245245
- Pruebas E2E en Windows.
246+
247+
---
248+
249+
## CRITICAL UNRESOLVED ISSUE: Uninstaller Crash (2025-11-20)
250+
251+
### Problem Summary
252+
The Windows uninstaller (Uninstall.exe) crashes with access violation **before any managed code executes**.
253+
254+
**Error Signature:**
255+
- Exception: 0xc0000005 (Access Violation)
256+
- Exit Code: 0x80131506 (.NET Runtime internal error)
257+
- Crash Offset: 0x00000000000b24f6 (consistent)
258+
- PE Timestamp: 0x68ffe47c (consistent despite rebuilds)
259+
- Assembly Version: 2.0.0.0
260+
261+
**Critical Fact:** Crash occurs BEFORE Program.Main - no managed code runs at all.
262+
263+
### Attempted Fixes (All Failed)
264+
1. Centralized Serilog logging to %TEMP%\DotnetPackaging.Installer\
265+
2. Removed Win32 P/Invoke (FindResource, LoadResource)
266+
3. Fixed Avalonia Dispatcher (base.OnFrameworkInitializationCompleted, Dispatcher.UIThread.Post)
267+
4. Rename strategy for locked directories
268+
5. Non-deterministic builds (Deterministic=false, AssemblyVersion=2.0.0.0)
269+
6. Multiple complete clean rebuilds (bin/obj/temp cache)
270+
7. Emergency logging at top of Program.Main (does not execute)
271+
272+
### Key Evidence
273+
- Installer works perfectly
274+
- Uninstaller is IDENTICAL binary (created via File.Copy)
275+
- Same crash offset across all builds
276+
- Same PE timestamp despite forced non-deterministic builds
277+
- Emergency log (%TEMP%\dp-emergency.log) is never created = crash before Program.Main
278+
279+
### Hypothesis
280+
Crash during .NET Runtime initialization or native DLL loading, NOT in managed code.
281+
Possibly Single-File bundling corruption when executable is copied.
282+
283+
### Diagnostic Steps for Next Agent
284+
285+
1. **Check if crash is before Main:**
286+
`powershell
287+
Get-Content C:\Users\JMN\AppData\Local\Temp\dp-emergency.log
288+
`
289+
If empty/missing → crash is definitely before managed entry point
290+
291+
2. **Use WinDbg:**
292+
Attach to Uninstall.exe and identify what module owns offset 0x00000000000b24f6
293+
294+
3. **Compare binaries:**
295+
`powershell
296+
dumpbin /headers C:\Users\JMN\Desktop\AngorSetup.exe > installer-headers.txt
297+
dumpbin /headers C:\Users\JMN\AppData\Local\Programs\AngorTest\Uninstall.exe > uninstaller-headers.txt
298+
Compare-Object (Get-Content installer-headers.txt) (Get-Content uninstaller-headers.txt)
299+
`
300+
301+
4. **Try without Single-File:**
302+
Modify DotnetPackaging.Exe.Installer.csproj:
303+
- Remove or set PublishSingleFile=false
304+
- Test if crash persists
305+
306+
5. **Alternative approaches:**
307+
- Don't copy installer - registry points to original with --uninstall flag
308+
- Build uninstaller as separate project (not copy)
309+
- Generate PowerShell uninstaller script instead
310+
311+
### Code Locations
312+
- src/DotnetPackaging.Exe.Installer/Program.cs:15 - Emergency log (never executes)
313+
- src/DotnetPackaging.Exe.Installer/Core/Installer.cs:39 - File.Copy creates uninstaller
314+
- src/DotnetPackaging.Exe.Installer/App.axaml.cs:32 - base.OnFrameworkInitializationCompleted
315+
- src/DotnetPackaging.Exe.Installer/Core/LoggerSetup.cs - Logging config
316+
317+
### Test Command
318+
`powershell
319+
# Generate installer
320+
dotnet run --project src\DotnetPackaging.Tool\DotnetPackaging.Tool.csproj -- exe from-project --project F:\Repos\angor\src\Angor\Avalonia\AngorApp.Desktop\AngorApp.Desktop.csproj --output C:\Users\JMN\Desktop\AngorSetup.exe --rid win-x64
321+
322+
# Install to AngorTest, then run uninstaller
323+
C:\Users\JMN\AppData\Local\Programs\AngorTest\Uninstall.exe --uninstall
324+
`
325+
326+
### Environment
327+
- Windows 11
328+
- .NET 8.0.22
329+
- Avalonia 11.x
330+
- Single-file self-contained deployment
331+
332+
**CONFIRMED (2025-11-20 13:37):** dp-emergency.log created during install, NOT during uninstall.
333+
Crash is definitively BEFORE Program.Main. Problem is in .NET Runtime init or native DLL loading.
334+
335+
### BREAKTHROUGH (2025-11-20 15:08)
336+
337+
**ROOT CAUSE FOUND:** Problem is WHERE the executable runs from, NOT the binary itself.
338+
339+
- Desktop\AngorSetup.exe --uninstall → ✅ WORKS
340+
- Programs\AngorTest\Uninstall.exe --uninstall → ❌ CRASHES
341+
- Both files IDENTICAL (SHA256 verified)
342+
343+
**Conclusion:** .NET Single-File runtime fails to extract from installation directory.
344+
345+
**FIX:** Don't copy uninstaller. Registry should point to Desktop installer with --uninstall flag.
346+
Or: Extract uninstaller to %TEMP% before running.
347+
348+
### UPDATE (2025-11-20 15:27)
349+
350+
**SOLUTION IMPLEMENTED:** Uninstaller now copies to %TEMP%
351+
- Modified Installer.cs:RegisterUninstaller() to copy uninstaller to:
352+
%TEMP%\DotnetPackaging\Uninstallers\{appId}\Uninstall.exe
353+
- Registry UninstallString now points to %TEMP% location
354+
- This avoids .NET Single-File extraction issues in installation directory
355+
356+
**NEW BLOCKER:** Avalonia Dispatcher crash prevents testing
357+
After implementing %TEMP% solution, installer crashes with:
358+
System.InvalidOperationException: Cannot perform requested operation because the Dispatcher shut down
359+
360+
**Root cause:** Avalonia Dispatcher closes before async wizard completes.
361+
362+
**Attempted fixes (ALL FAILED):**
363+
1. Dispatcher.UIThread.Post - async void doesn't wait
364+
2. Dispatcher.UIThread.InvokeAsync - same issue
365+
3. desktopLifetime.Startup event - async void
366+
4. mainWindow.Opened event - async void
367+
5. ShutdownMode.OnMainWindowClose - closes too early
368+
6. MainWindow in OnFrameworkInitializationCompleted - same
369+
370+
**Current state:**
371+
- Installer.cs: ✅ %TEMP% copy implemented
372+
- App.axaml.cs: ❌ Dispatcher shutdown issue
373+
- Cannot test %TEMP% solution until Dispatcher is fixed
374+
375+
**Next steps:**
376+
- Fix Avalonia async initialization pattern
377+
- OR revert to working version and implement %TEMP% solution there
378+
- OR use synchronous UI initialization then async operations
379+
380+
See WARP.md line 356 for full details.
381+
382+
### FINAL SOLUTION (2025-11-20 16:15) ✅
383+
384+
**Problem Solved:** Uninstaller crashes when executed from installation directory.
385+
386+
**Root Cause:** .NET Single-File runtime extraction fails in locked/restricted directories.
387+
388+
**Solution Implemented:** Native Launcher Pattern
389+
- Created UninstallLauncher.exe (WinExe, ~12MB, single-file .NET 8)
390+
- Launcher copies Uninstall.exe to %TEMP% and executes it
391+
- Registry points to launcher (stable location in install dir)
392+
- Launcher waits for uninstaller to complete (WaitForExit)
393+
394+
**Key Files:**
395+
- src/DotnetPackaging.Exe.UninstallLauncher/ - New launcher project
396+
- src/DotnetPackaging.Exe.Installer/Core/Installer.cs - Extracts launcher from resources
397+
- src/DotnetPackaging.Exe.Installer/Resources/UninstallLauncher.exe - Embedded binary
398+
399+
**Why It Works:**
400+
✅ Launcher always available (embedded in installer)
401+
✅ Uninstaller always runs from %TEMP% (no extraction issues)
402+
✅ No console window (WinExe)
403+
✅ No Windows error dialog (WaitForExit)
404+
✅ Robust to %TEMP% cleanup (launcher recreates on each run)
405+
406+
**Future Improvements (see WARP.md lines 481-618):**
407+
1. Native AOT or C++ launcher (reduce from 12MB to <1MB)
408+
2. Download launcher from GitHub releases (smaller installer)
409+
3. Self-deleting temp uninstaller
410+
4. Retry logic for antivirus locks
411+
412+
**Testing:**
413+
✅ Install/uninstall works correctly
414+
✅ No console window appears
415+
✅ No Windows error dialogs
416+
✅ Uninstaller UI shows properly
417+
✅ Works after multiple cycles
418+
419+
See WARP.md lines 383-618 for complete documentation.

Directory.Packages.props

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
1919
<PackageVersion Include="NyaFs" Version="1.0.8" />
2020
<PackageVersion Include="Octokit" Version="14.0.0" />
21+
<PackageVersion Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.6.2" />
2122
<PackageVersion Include="ReactiveProperty" Version="9.8.0" />
2223
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.3.8" />
2324
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
@@ -27,8 +28,9 @@
2728
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.8" />
2829
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.8" />
2930
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.6" />
30-
<PackageVersion Include="Zafiro.Avalonia" Version="40.1.1" />
31-
<PackageVersion Include="Zafiro.Avalonia.Dialogs" Version="40.1.1" />
31+
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
32+
<PackageVersion Include="Zafiro.Avalonia" Version="41.0.2" />
33+
<PackageVersion Include="Zafiro.Avalonia.Dialogs" Version="41.0.2" />
3234
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
3335
<PackageVersion Include="Serilog.Sinks.XUnit" Version="3.0.19" />
3436
<PackageVersion Include="SharpCompress" Version="0.41.0" />
@@ -49,12 +51,12 @@
4951
<PackageVersion Include="System.IO.Hashing" Version="10.0.0" />
5052
<PackageVersion Include="DeflateBlockCompressor" Version="0.0.13" />
5153
<PackageVersion Include="NuGet.Versioning" Version="7.0.0" />
52-
<PackageVersion Include="Zafiro.Avalonia.Generators" Version="40.1.1" />
54+
<PackageVersion Include="Zafiro.Avalonia.Generators" Version="41.0.2" />
5355
<PackageVersion Include="Zafiro.FileSystem" Version="41.2.1" />
54-
<PackageVersion Include="Zafiro" Version="41.2.1" />
56+
<PackageVersion Include="Zafiro" Version="41.2.4" />
5557
<PackageVersion Include="Zafiro.FileSystem.Unix" Version="41.2.1" />
5658
<PackageVersion Include="Zafiro.FileSystem.Local" Version="41.2.1" />
57-
<PackageVersion Include="Zafiro.DivineBytes" Version="41.2.1" />
59+
<PackageVersion Include="Zafiro.DivineBytes" Version="41.2.4" />
5860
<PackageVersion Include="DiscUtils" Version="0.16.13" />
5961
</ItemGroup>
6062
</Project>

DotnetPackaging.sln

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetPackaging.AppImage.Te
4343
EndProject
4444
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetPackaging.Dmg.Tests", "test\DotnetPackaging.Dmg.Tests\DotnetPackaging.Dmg.Tests.csproj", "{D97A5E33-6621-44DD-83D0-65F55F56E487}"
4545
EndProject
46+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
47+
EndProject
48+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetPackaging.Exe.Tests", "src\DotnetPackaging.Exe.Tests\DotnetPackaging.Exe.Tests.csproj", "{4AA3D5C2-1C8D-4E3A-901A-07E2FDAE59BD}"
49+
EndProject
50+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetPackaging.Exe.UninstallLauncher", "src\DotnetPackaging.Exe.UninstallLauncher\DotnetPackaging.Exe.UninstallLauncher.csproj", "{37EC0322-AF12-490D-92B7-1712DFDAB731}"
51+
EndProject
4652
Global
4753
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4854
Debug|Any CPU = Debug|Any CPU
@@ -233,16 +239,41 @@ Global
233239
{D97A5E33-6621-44DD-83D0-65F55F56E487}.Release|x64.Build.0 = Release|Any CPU
234240
{D97A5E33-6621-44DD-83D0-65F55F56E487}.Release|x86.ActiveCfg = Release|Any CPU
235241
{D97A5E33-6621-44DD-83D0-65F55F56E487}.Release|x86.Build.0 = Release|Any CPU
242+
{4AA3D5C2-1C8D-4E3A-901A-07E2FDAE59BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
243+
{4AA3D5C2-1C8D-4E3A-901A-07E2FDAE59BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
244+
{4AA3D5C2-1C8D-4E3A-901A-07E2FDAE59BD}.Debug|x64.ActiveCfg = Debug|Any CPU
245+
{4AA3D5C2-1C8D-4E3A-901A-07E2FDAE59BD}.Debug|x64.Build.0 = Debug|Any CPU
246+
{4AA3D5C2-1C8D-4E3A-901A-07E2FDAE59BD}.Debug|x86.ActiveCfg = Debug|Any CPU
247+
{4AA3D5C2-1C8D-4E3A-901A-07E2FDAE59BD}.Debug|x86.Build.0 = Debug|Any CPU
248+
{4AA3D5C2-1C8D-4E3A-901A-07E2FDAE59BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
249+
{4AA3D5C2-1C8D-4E3A-901A-07E2FDAE59BD}.Release|Any CPU.Build.0 = Release|Any CPU
250+
{4AA3D5C2-1C8D-4E3A-901A-07E2FDAE59BD}.Release|x64.ActiveCfg = Release|Any CPU
251+
{4AA3D5C2-1C8D-4E3A-901A-07E2FDAE59BD}.Release|x64.Build.0 = Release|Any CPU
252+
{4AA3D5C2-1C8D-4E3A-901A-07E2FDAE59BD}.Release|x86.ActiveCfg = Release|Any CPU
253+
{4AA3D5C2-1C8D-4E3A-901A-07E2FDAE59BD}.Release|x86.Build.0 = Release|Any CPU
254+
{37EC0322-AF12-490D-92B7-1712DFDAB731}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
255+
{37EC0322-AF12-490D-92B7-1712DFDAB731}.Debug|Any CPU.Build.0 = Debug|Any CPU
256+
{37EC0322-AF12-490D-92B7-1712DFDAB731}.Debug|x64.ActiveCfg = Debug|Any CPU
257+
{37EC0322-AF12-490D-92B7-1712DFDAB731}.Debug|x64.Build.0 = Debug|Any CPU
258+
{37EC0322-AF12-490D-92B7-1712DFDAB731}.Debug|x86.ActiveCfg = Debug|Any CPU
259+
{37EC0322-AF12-490D-92B7-1712DFDAB731}.Debug|x86.Build.0 = Debug|Any CPU
260+
{37EC0322-AF12-490D-92B7-1712DFDAB731}.Release|Any CPU.ActiveCfg = Release|Any CPU
261+
{37EC0322-AF12-490D-92B7-1712DFDAB731}.Release|Any CPU.Build.0 = Release|Any CPU
262+
{37EC0322-AF12-490D-92B7-1712DFDAB731}.Release|x64.ActiveCfg = Release|Any CPU
263+
{37EC0322-AF12-490D-92B7-1712DFDAB731}.Release|x64.Build.0 = Release|Any CPU
264+
{37EC0322-AF12-490D-92B7-1712DFDAB731}.Release|x86.ActiveCfg = Release|Any CPU
265+
{37EC0322-AF12-490D-92B7-1712DFDAB731}.Release|x86.Build.0 = Release|Any CPU
236266
EndGlobalSection
237267
GlobalSection(SolutionProperties) = preSolution
238268
HideSolutionNode = FALSE
239269
EndGlobalSection
240270
GlobalSection(NestedProjects) = preSolution
271+
{79928283-5F6E-49F4-ACC4-FE5994DA1116} = {7E0C2A39-1C29-4B5D-9D45-1B6B7E7A77B6}
241272
{6F3C5C5A-61E4-4D16-BE0D-6A0E9E9B3F9D} = {7E0C2A39-1C29-4B5D-9D45-1B6B7E7A77B6}
242-
{2BD66C13-3A2A-45B5-9E36-F25F77292B10} = {7E0C2A39-1C29-4B5D-9D45-1B6B7E7A77B6}
243273
{F090C5BD-3B1C-4D32-93AD-5D90E4819A69} = {7E0C2A39-1C29-4B5D-9D45-1B6B7E7A77B6}
244-
{79928283-5F6E-49F4-ACC4-FE5994DA1116} = {7E0C2A39-1C29-4B5D-9D45-1B6B7E7A77B6}
274+
{2BD66C13-3A2A-45B5-9E36-F25F77292B10} = {7E0C2A39-1C29-4B5D-9D45-1B6B7E7A77B6}
245275
{D97A5E33-6621-44DD-83D0-65F55F56E487} = {7E0C2A39-1C29-4B5D-9D45-1B6B7E7A77B6}
276+
{4AA3D5C2-1C8D-4E3A-901A-07E2FDAE59BD} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
246277
EndGlobalSection
247278
GlobalSection(ExtensibilityGlobals) = postSolution
248279
SolutionGuid = {1D869DF1-9147-472D-A3D9-D519518A6951}

0 commit comments

Comments
 (0)