Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions RunCommand/Elevation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace ktsu.RunCommand;

/// <summary>
/// Specifies the privilege level under which a command should be executed.
/// </summary>
public enum Elevation
{
/// <summary>
/// Run the command with the current process's privileges. Standard output and standard error are captured.
/// </summary>
Default,

/// <summary>
/// Run the command with elevated privileges. On Windows, launches the process with the
/// <c>runas</c> verb, prompting the user for UAC consent; output cannot be captured in this mode
/// because elevation requires <c>UseShellExecute</c>. On non-Windows platforms this value has no
/// effect; prefix the command with <c>sudo</c> there instead.
/// </summary>
Elevated,
}
101 changes: 81 additions & 20 deletions RunCommand/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,29 @@ public static int Execute(string command) =>
public static int Execute(string command, OutputHandler outputHandler) =>
ExecuteAsync(command, outputHandler).Result;

/// <summary>
/// Executes a shell command synchronously at the specified elevation level.
/// </summary>
/// <param name="command">The command to execute.</param>
/// <param name="elevation">The privilege level under which to run the command.</param>
/// <returns>The exit code of the executed process.</returns>
public static int Execute(string command, Elevation elevation) =>
ExecuteAsync(command, elevation).Result;

/// <summary>
/// Executes a shell command synchronously with an output handler at the specified elevation level.
/// </summary>
/// <param name="command">The command to execute.</param>
/// <param name="outputHandler">
/// The handler for processing command output. Not invoked when <paramref name="elevation"/> is
/// <see cref="Elevation.Elevated"/> on Windows because elevation requires <c>UseShellExecute</c>,
/// which is incompatible with output redirection.
/// </param>
/// <param name="elevation">The privilege level under which to run the command.</param>
/// <returns>The exit code of the executed process.</returns>
public static int Execute(string command, OutputHandler outputHandler, Elevation elevation) =>
ExecuteAsync(command, outputHandler, elevation).Result;

/// <summary>
/// Executes a shell command asynchronously
/// </summary>
Expand All @@ -44,6 +67,29 @@ public static async Task<int> ExecuteAsync(string command)
/// <param name="outputHandler">The handler for processing command output.</param>
/// <returns>A task representing the asynchronous operation with the process exit code.</returns>
public static async Task<int> ExecuteAsync(string command, OutputHandler outputHandler)
=> await ExecuteAsync(command, outputHandler, Elevation.Default).ConfigureAwait(false);

/// <summary>
/// Executes a shell command asynchronously at the specified elevation level.
/// </summary>
/// <param name="command">The command to execute.</param>
/// <param name="elevation">The privilege level under which to run the command.</param>
/// <returns>A task representing the asynchronous operation with the process exit code.</returns>
public static async Task<int> ExecuteAsync(string command, Elevation elevation)
=> await ExecuteAsync(command, new(), elevation).ConfigureAwait(false);

/// <summary>
/// Executes a shell command asynchronously with an output handler at the specified elevation level.
/// </summary>
/// <param name="command">The command to execute.</param>
/// <param name="outputHandler">
/// The handler for processing command output. Not invoked when <paramref name="elevation"/> is
/// <see cref="Elevation.Elevated"/> on Windows because elevation requires <c>UseShellExecute</c>,
/// which is incompatible with output redirection.
/// </param>
/// <param name="elevation">The privilege level under which to run the command.</param>
/// <returns>A task representing the asynchronous operation with the process exit code.</returns>
public static async Task<int> ExecuteAsync(string command, OutputHandler outputHandler, Elevation elevation)
{
Ensure.NotNull(command);
Ensure.NotNull(outputHandler);
Expand All @@ -53,35 +99,50 @@ public static async Task<int> 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;

process.Start();
if (isWindows)
{
startInfo.LoadUserProfile = true;
}
}

AsyncProcessStreamReader outputReader = new(process, outputHandler);
using Process process = new() { StartInfo = startInfo };

Task outputTask = outputReader.Start();
Task processTask = process.WaitForExitAsync();
process.Start();

await Task.WhenAll(outputTask, processTask).ConfigureAwait(false);
if (useElevation)
{
await process.WaitForExitAsync().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;
}
Expand Down