Skip to content

Commit e850004

Browse files
committed
Implement programmatic elevation and improve backup button clarity
Programmatic Elevation (inspired by anthonysimmon.com/programmatically-elevate-dotnet-app-on-any-platform/): - Added Utilities/ElevationHelper.cs for cross-platform elevation detection and relaunching - Modified App.xaml.cs to automatically detect if not elevated and relaunch with UAC - Changed app.manifest from requireAdministrator to asInvoker (elevation now programmatic) - This allows 'dotnet run' to work properly - app will prompt for UAC and relaunch elevated - Graceful degradation: warns user if UAC declined but continues with limited functionality Backup Button Improvements: - Context Menus tab: Button now says "Backup Context Menus" (was "Backup Registry Entries") - Startup Programs tab: Button now says "Backup Startup Items" (was "Backup Registry Entries") - Makes it immediately clear what each backup button will save - Dialog titles were already context-specific, now buttons match Helper Scripts (legacy support): - Added run.ps1, run-elevated.ps1, run.cmd for manual elevation - These are now optional since the app handles elevation automatically Benefits: - 'dotnet run' now works seamlessly with elevation - No need to run VS Code as administrator - Better user experience with automatic UAC prompt - Clearer UI for backup operations on each tab Technical Details: - Uses WindowsIdentity/WindowsPrincipal to detect admin rights - ProcessStartInfo with Verb="runas" for UAC elevation - Passes original command-line args to elevated instance - Handles Win32Exception when user declines UAC
1 parent 2e6dcb8 commit e850004

8 files changed

Lines changed: 152 additions & 13 deletions

File tree

App.xaml.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,26 @@ protected override void OnStartup(StartupEventArgs e)
1313
{
1414
base.OnStartup(e);
1515

16+
// Check if running with administrator privileges, relaunch if needed
17+
if (ElevationHelper.TryRelaunchElevated(e.Args))
18+
{
19+
// Elevated process was launched, shut down this instance
20+
Shutdown();
21+
return;
22+
}
23+
24+
// If we get here, we're either elevated or user declined UAC
25+
if (!ElevationHelper.IsElevated())
26+
{
27+
// User declined elevation - show warning and continue (limited functionality)
28+
MessageBox.Show(
29+
"Context Menu Editor requires administrator privileges to modify registry entries.\n\n" +
30+
"The application will start, but registry operations may fail.",
31+
"Administrator Rights Required",
32+
MessageBoxButton.OK,
33+
MessageBoxImage.Warning);
34+
}
35+
1636
// Initialize theme manager (dark mode by default)
1737
_ = ThemeManager.Instance;
1838

Utilities/ElevationHelper.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Runtime.InteropServices;
4+
using System.Security.Principal;
5+
6+
namespace ContextMenuEditor.Utilities;
7+
8+
/// <summary>
9+
/// Helper class for detecting and requesting elevation (admin rights).
10+
/// Based on: https://anthonysimmon.com/programmatically-elevate-dotnet-app-on-any-platform/
11+
/// </summary>
12+
public static class ElevationHelper
13+
{
14+
/// <summary>
15+
/// Checks if the current process is running with administrator privileges.
16+
/// </summary>
17+
/// <returns>True if elevated, false otherwise.</returns>
18+
public static bool IsElevated()
19+
{
20+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
21+
{
22+
using var identity = WindowsIdentity.GetCurrent();
23+
var principal = new WindowsPrincipal(identity);
24+
return principal.IsInRole(WindowsBuiltInRole.Administrator);
25+
}
26+
27+
// On Linux/macOS, check if running as root (UID 0)
28+
// Not applicable for this Windows-only WPF app, but included for completeness
29+
return geteuid() == 0;
30+
}
31+
32+
/// <summary>
33+
/// Attempts to restart the current application with elevated privileges.
34+
/// Returns true if the restart was initiated, false if already elevated or failed.
35+
/// </summary>
36+
/// <param name="args">Command-line arguments to pass to the elevated instance.</param>
37+
/// <returns>True if relaunch initiated (caller should exit), false if already elevated.</returns>
38+
public static bool TryRelaunchElevated(string[] args)
39+
{
40+
if (IsElevated())
41+
{
42+
return false; // Already elevated, no need to relaunch
43+
}
44+
45+
try
46+
{
47+
var currentProcessPath = Environment.ProcessPath
48+
?? throw new InvalidOperationException("Cannot determine current process path");
49+
50+
var startInfo = new ProcessStartInfo
51+
{
52+
UseShellExecute = true,
53+
FileName = currentProcessPath,
54+
Verb = "runas" // Windows UAC elevation
55+
};
56+
57+
// Pass along original command-line arguments
58+
foreach (var arg in args)
59+
{
60+
startInfo.ArgumentList.Add(arg);
61+
}
62+
63+
Process.Start(startInfo);
64+
return true; // Successfully started elevated process, caller should exit
65+
}
66+
catch (System.ComponentModel.Win32Exception)
67+
{
68+
// User declined UAC prompt
69+
return false;
70+
}
71+
catch (Exception ex)
72+
{
73+
System.Diagnostics.Debug.WriteLine($"Failed to elevate: {ex.Message}");
74+
return false;
75+
}
76+
}
77+
78+
// P/Invoke for Unix systems (not used in this Windows-only app, but included for reference)
79+
[DllImport("libc", SetLastError = true)]
80+
private static extern uint geteuid();
81+
}

Views/MainWindow.xaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
Command="{Binding DeleteCommand}"
9090
IsEnabled="{Binding HasSelection}"/>
9191
<Separator Margin="0,8"/>
92-
<Button Content="Backup Registry Entries"
92+
<Button Content="Backup Context Menus"
9393
Command="{Binding BackupCommand}"/>
9494
</StackPanel>
9595
</Border>

Views/StartupView.xaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
Command="{Binding DeleteCommand}"
6868
IsEnabled="{Binding HasSelection}"/>
6969
<Separator Margin="0,8"/>
70-
<Button Content="Backup Registry Entries"
70+
<Button Content="Backup Startup Items"
7171
Command="{Binding BackupCommand}"/>
7272
</StackPanel>
7373
</Border>

app.manifest

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,11 @@
66
<security>
77
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
88
<!-- UAC Manifest Options
9-
If you want to change the Windows User Account Control level replace the
10-
requestedExecutionLevel node with one of the following.
11-
12-
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
13-
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
14-
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
15-
16-
Specifying requestedExecutionLevel element will disable file and registry virtualization.
17-
Remove this element if your application requires this virtualization for backwards
18-
compatibility.
9+
Changed to asInvoker because elevation is now handled programmatically in App.xaml.cs
10+
The application will detect if it's not elevated and relaunch itself with admin rights.
11+
This allows 'dotnet run' to work properly during development.
1912
-->
20-
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
13+
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
2114
</requestedPrivileges>
2215
</security>
2316
</trustInfo>

run-elevated.ps1

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Run Context Menu Editor with Administrator privileges
2+
# This script builds and runs the application with UAC elevation
3+
4+
Write-Host "Building Context Menu Editor..." -ForegroundColor Cyan
5+
dotnet build
6+
7+
if ($LASTEXITCODE -eq 0) {
8+
Write-Host "Build successful! Launching with administrator privileges..." -ForegroundColor Green
9+
10+
# Get the executable path
11+
$exePath = ".\bin\Debug\net8.0-windows\win-x64\ContextMenuEditor.exe"
12+
13+
if (Test-Path $exePath) {
14+
Start-Process -FilePath $exePath -Verb RunAs
15+
Write-Host "Application launched." -ForegroundColor Green
16+
} else {
17+
Write-Host "Error: Executable not found at $exePath" -ForegroundColor Red
18+
Write-Host "Try running 'dotnet build' first." -ForegroundColor Yellow
19+
}
20+
} else {
21+
Write-Host "Build failed. Please fix errors and try again." -ForegroundColor Red
22+
}

run.cmd

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
@echo off
2+
REM Build and run Context Menu Editor with elevation
3+
echo Building...
4+
dotnet build
5+
if %ERRORLEVEL% EQU 0 (
6+
echo Launching with administrator privileges...
7+
powershell -Command "Start-Process '.\bin\Debug\net8.0-windows\win-x64\ContextMenuEditor.exe' -Verb RunAs"
8+
) else (
9+
echo Build failed.
10+
exit /b 1
11+
)

run.ps1

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Quick run - just launch the already-built executable with elevation
2+
# Use this if you've already built the app and just want to run it
3+
4+
$exePath = ".\bin\Debug\net8.0-windows\win-x64\ContextMenuEditor.exe"
5+
6+
if (Test-Path $exePath) {
7+
Write-Host "Launching Context Menu Editor with administrator privileges..." -ForegroundColor Cyan
8+
Start-Process -FilePath $exePath -Verb RunAs
9+
} else {
10+
Write-Host "Error: Executable not found." -ForegroundColor Red
11+
Write-Host "Run './run-elevated.ps1' to build and run." -ForegroundColor Yellow
12+
}

0 commit comments

Comments
 (0)