From d99658d6e19b931609a9f81a09f41db212985ef1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 03:29:12 +0000 Subject: [PATCH 1/2] Add elevation support to RunCommand Adds an Elevation enum and overloads to Execute/ExecuteAsync that let callers request elevated privileges. On Windows, Elevation.Elevated launches the process with the runas verb via UseShellExecute, which triggers a UAC prompt; output redirection is disabled in that mode since it is incompatible with UseShellExecute. On non-Windows the value is a no-op, leaving sudo prefixing to the caller. --- RunCommand/Elevation.cs | 24 +++++++++++ RunCommand/RunCommand.cs | 92 +++++++++++++++++++++++++++++++++------- 2 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 RunCommand/Elevation.cs diff --git a/RunCommand/Elevation.cs b/RunCommand/Elevation.cs new file mode 100644 index 0000000..4af2f8b --- /dev/null +++ b/RunCommand/Elevation.cs @@ -0,0 +1,24 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.RunCommand; + +/// +/// Specifies the privilege level under which a command should be executed. +/// +public enum Elevation +{ + /// + /// Run the command with the current process's privileges. Standard output and standard error are captured. + /// + Default, + + /// + /// Run the command with elevated privileges. On Windows, launches the process with the + /// runas verb, prompting the user for UAC consent; output cannot be captured in this mode + /// because elevation requires UseShellExecute. On non-Windows platforms this value has no + /// effect; prefix the command with sudo there instead. + /// + Elevated, +} diff --git a/RunCommand/RunCommand.cs b/RunCommand/RunCommand.cs index f2ba1f1..5c0ee02 100644 --- a/RunCommand/RunCommand.cs +++ b/RunCommand/RunCommand.cs @@ -29,6 +29,29 @@ public static int Execute(string command) => public static int Execute(string command, OutputHandler outputHandler) => ExecuteAsync(command, outputHandler).Result; + /// + /// Executes a shell command synchronously at the specified elevation level. + /// + /// The command to execute. + /// The privilege level under which to run the command. + /// The exit code of the executed process. + public static int Execute(string command, Elevation elevation) => + ExecuteAsync(command, elevation).Result; + + /// + /// Executes a shell command synchronously with an output handler at the specified elevation level. + /// + /// The command to execute. + /// + /// The handler for processing command output. Not invoked when is + /// on Windows because elevation requires UseShellExecute, + /// which is incompatible with output redirection. + /// + /// The privilege level under which to run the command. + /// The exit code of the executed process. + public static int Execute(string command, OutputHandler outputHandler, Elevation elevation) => + ExecuteAsync(command, outputHandler, elevation).Result; + /// /// Executes a shell command asynchronously /// @@ -44,6 +67,29 @@ public static async Task ExecuteAsync(string command) /// The handler for processing command output. /// A task representing the asynchronous operation with the process exit code. public static async Task ExecuteAsync(string command, OutputHandler outputHandler) + => await ExecuteAsync(command, outputHandler, Elevation.Default).ConfigureAwait(false); + + /// + /// Executes a shell command asynchronously at the specified elevation level. + /// + /// The command to execute. + /// The privilege level under which to run the command. + /// A task representing the asynchronous operation with the process exit code. + public static async Task ExecuteAsync(string command, Elevation elevation) + => await ExecuteAsync(command, new(), elevation).ConfigureAwait(false); + + /// + /// Executes a shell command asynchronously with an output handler at the specified elevation level. + /// + /// The command to execute. + /// + /// The handler for processing command output. Not invoked when is + /// on Windows because elevation requires UseShellExecute, + /// which is incompatible with output redirection. + /// + /// The privilege level under which to run the command. + /// A task representing the asynchronous operation with the process exit code. + public static async Task ExecuteAsync(string command, OutputHandler outputHandler, Elevation elevation) { Ensure.NotNull(command); Ensure.NotNull(outputHandler); @@ -53,29 +99,45 @@ public static async Task ExecuteAsync(string command, OutputHandler outputH string filename = commandParts[0]; string arguments = commandParts.Length > 1 ? commandParts[1] : string.Empty; - using Process process = new() + bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + bool useElevation = elevation == Elevation.Elevated && isWindows; + + ProcessStartInfo startInfo = new() { - StartInfo = new ProcessStartInfo - { - FileName = filename, - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - StandardOutputEncoding = outputHandler.Encoding, - StandardErrorEncoding = outputHandler.Encoding, - UseShellExecute = false, - CreateNoWindow = true, - } + FileName = filename, + Arguments = arguments, + CreateNoWindow = true, }; - bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - if (isWindows) + if (useElevation) { - process.StartInfo.LoadUserProfile = true; + startInfo.UseShellExecute = true; + startInfo.Verb = "runas"; } + else + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.StandardOutputEncoding = outputHandler.Encoding; + startInfo.StandardErrorEncoding = outputHandler.Encoding; + startInfo.UseShellExecute = false; + + if (isWindows) + { + startInfo.LoadUserProfile = true; + } + } + + using Process process = new() { StartInfo = startInfo }; process.Start(); + if (useElevation) + { + await process.WaitForExitAsync().ConfigureAwait(false); + return process.ExitCode; + } + AsyncProcessStreamReader outputReader = new(process, outputHandler); Task outputTask = outputReader.Start(); From 1bb08421868a7adafaf5e8585a1623c0418c8434 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 03:40:03 +0000 Subject: [PATCH 2/2] Use single return path in ExecuteAsync Replaces the early return in the elevated branch with an if/else so the method has one exit point, making the control flow easier to follow. --- RunCommand/RunCommand.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/RunCommand/RunCommand.cs b/RunCommand/RunCommand.cs index 5c0ee02..b7bc41f 100644 --- a/RunCommand/RunCommand.cs +++ b/RunCommand/RunCommand.cs @@ -135,15 +135,14 @@ public static async Task ExecuteAsync(string command, OutputHandler outputH if (useElevation) { await process.WaitForExitAsync().ConfigureAwait(false); - return process.ExitCode; } - - AsyncProcessStreamReader outputReader = new(process, outputHandler); - - Task outputTask = outputReader.Start(); - Task processTask = process.WaitForExitAsync(); - - await Task.WhenAll(outputTask, processTask).ConfigureAwait(false); + else + { + AsyncProcessStreamReader outputReader = new(process, outputHandler); + Task outputTask = outputReader.Start(); + Task processTask = process.WaitForExitAsync(); + await Task.WhenAll(outputTask, processTask).ConfigureAwait(false); + } return process.ExitCode; }