Skip to content

Commit

Permalink
feat: Implemented an ExeHelper.Execute overload with ability to redir…
Browse files Browse the repository at this point in the history
…ect standard output and error streams to the specified writers.
  • Loading branch information
hennadiilu authored Apr 10, 2024
1 parent 7274c8f commit 8d33d22
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 17 deletions.
101 changes: 88 additions & 13 deletions src/Heleonix.Execution/ExeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ public static class ExeHelper
/// <summary>
/// Executes an executable by the specified <paramref name="exePath"/>.
/// </summary>
/// <param name="exePath">Defines the path to executable.</param>
/// <param name="arguments">Represents the command line arguments.</param>
/// <param name="exePath">The path to the executable file to run.</param>
/// <param name="arguments">Command line arguments to pass into the executable.</param>
/// <param name="extractOutput">Defines whether to redirect and extract standard output and errors or not.</param>
/// <param name="workingDirectory">The current working directory.
/// Relative paths inside the executable will be relative to this working directory.</param>
/// <param name="waitForExit">A number of millisecoonds to wait for process ending.
/// <param name="workingDirectory">The working directory to launch the executable in.</param>
/// <param name="waitForExit">A number of millisecoonds to wait for the process ending.
/// Use <see cref="int.MaxValue"/> to wait infinitely.
/// </param>
/// <returns>An exit result.</returns>
/// <exception cref="InvalidOperationException">See the inner exception for details.</exception>
/// <example>
/// <code>
/// var result = ExeHelper.Execute(
/// @"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
/// "--app=http://www.google.com --window-size=300,300 --new-window",
Expand All @@ -37,6 +37,7 @@ public static class ExeHelper
/// Console.WriteLine(result.ExitCode); // An exit code: value returned by `Main` or by `Environment.Exit(exitCode)` etc.
/// Console.WriteLine(result.Output); // Output like `Console.WriteLine` is available here
/// Console.WriteLine(result.Error); // Output like `Console.Error.WriteLine` is available here.
/// </code>
/// </example>
public static ExeResult Execute(
string exePath,
Expand All @@ -51,11 +52,11 @@ public static ExeResult Execute(
{
StartInfo = new ProcessStartInfo
{
Arguments = arguments,
FileName = exePath,
Arguments = arguments,
WorkingDirectory = workingDirectory,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
RedirectStandardOutput = extractOutput,
RedirectStandardError = extractOutput,
Expand Down Expand Up @@ -85,13 +86,87 @@ public static ExeResult Execute(
}

/// <summary>
/// Executes an executable by the specified path. Does not extract output and error streams.
/// Executes an executable by the specified <paramref name="exePath"/>. Does not extract output and error streams.
/// </summary>
/// <param name="exePath">The execute path.</param>
/// <param name="arguments">The arguments.</param>
/// <param name="workingDirectory">The working directory.</param>
/// <returns>An executable's exit code.</returns>
/// <exception cref="InvalidOperationException">See the inner exception for details.</exception>
/// <param name="exePath">The path to the executable file to run.</param>
/// <param name="arguments">Command line arguments to pass into the executable.</param>
/// <param name="workingDirectory">The working directory to launch the executable in.</param>
/// <returns>The exit code of the executable.</returns>
/// <exception cref="InvalidOperationException">See the inner exception for more details.</exception>
public static int Execute(string exePath, string arguments, string workingDirectory = "")
=> Execute(exePath, arguments, false, workingDirectory, int.MaxValue).ExitCode;

/// <summary>
/// Executes an executable by the specified <paramref name="exePath"/>.
/// Asynchronously forwards <see cref="Process.StandardOutput"/> and <see cref="Process.StandardError"/>
/// of the executable to the specified <paramref name="outputWriter"/> and <paramref name="errorWriter"/> using
/// the intermediate <c>char</c> buffer with the specified <paramref name="bufferSize"/>.
/// </summary>
/// <param name="exePath">The path to the executable file to run.</param>
/// <param name="arguments">Command line arguments to pass into the executable.</param>
/// <param name="outputWriter">The text writer to forward the standard output stream to.</param>
/// <param name="errorWriter">The text writer to forward the standard error stream to.</param>
/// <param name="workingDirectory">The working directory to launch the executable in.</param>
/// <param name="waitForExit">A number of millisecoonds to wait for the process ending.
/// Use <see cref="int.MaxValue"/> to wait infinitely.
/// </param>
/// <param name="bufferSize">The sizes of the intermediate buffers to use for forwarding in number of <c>char</c>.</param>
/// <returns>The exit code of the executable.</returns>
/// <exception cref="InvalidOperationException">See the inner exception for more details.</exception>
/// <example>
/// Launch an executable and forward its output and error streams while it is running.
/// <code>
/// var output = new StringWriter();
/// var error = new StringWriter();
///
/// var exitCode = ExeHelper.Execute("dotnet.exe", "--UNKNOWN", output, error, string.Empty, 5000, 2048);
///
/// var o = output.ToString();
/// var e = error.ToString();
/// </code>
/// Launch an executable and forward its output and error streams to the <see cref="Console"/> of the main process.
/// <code>
/// var exitCode = ExeHelper.Execute("dotnet.exe", "--UNKNOWN", Console.Out, Console.Error, string.Empty, 5000, 2048);
/// </code>
/// </example>
public static int Execute(
string exePath,
string arguments,
TextWriter outputWriter,
TextWriter errorWriter,
string workingDirectory = "",
int waitForExit = int.MaxValue,
int bufferSize = 4096)
{
try
{
using (var process = new Process())
{
process.StartInfo.FileName = exePath;
process.StartInfo.Arguments = arguments;
process.StartInfo.WorkingDirectory = workingDirectory;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.RedirectStandardOutput = true;

process.Start();

var outputRedirector = new StreamRedirector(process.StandardOutput, outputWriter, bufferSize);
var errorRedirector = new StreamRedirector(process.StandardError, errorWriter, bufferSize);

outputRedirector.Start();
errorRedirector.Start();

process.WaitForExit(waitForExit);

return process.ExitCode;
}
}
catch (Exception e)
{
throw new InvalidOperationException(e.Message, e);
}
}
}
56 changes: 56 additions & 0 deletions src/Heleonix.Execution/StreamRedirector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// <copyright file="StreamRedirector.cs" company="Heleonix - Hennadii Lutsyshyn">
// Copyright (c) Heleonix - Hennadii Lutsyshyn. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the repository root for full license information.
// </copyright>

namespace Heleonix.Execution;

/// <summary>
/// Forwards data from a source stream into a destination stream using text writers.
/// </summary>
internal class StreamRedirector
{
private readonly TextReader source;

private readonly TextWriter destination;

private readonly int bufferSize;

/// <summary>
/// Initializes a new instance of the <see cref="StreamRedirector"/> class.
/// </summary>
/// <param name="source">The stream to forward data from.</param>
/// <param name="destination">The stream to forward data to.</param>
/// <param name="bufferSize">The size of the buffer of <see cref="char"/> data to transfer read from the
/// <paramref name="source"/> and write to the <paramref name="destination"/> at once.</param>
public StreamRedirector(TextReader source, TextWriter destination, int bufferSize)
{
this.source = source;
this.destination = destination;
this.bufferSize = bufferSize;
}

/// <summary>
/// Starts forwarding of the assigned stream on the background asynchronously.
/// </summary>
public void Start()
{
Task.Run(async () =>
{
var buffer = new char[this.bufferSize];

while (true)
{
var count = await this.source.ReadAsync(buffer, 0, this.bufferSize);

if (count <= 0)
{
break;
}

await this.destination.WriteAsync(buffer, 0, count);
await this.destination.FlushAsync();
}
});
}
}
66 changes: 62 additions & 4 deletions test/Heleonix.Execution.Tests/ExeHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ namespace Heleonix.Execution.Tests;
public static class ExeHelperTests
{
/// <summary>
/// Tests the <see cref="ExeHelper.Execute(string,string,bool,string,int)"/>.
/// Tests the <see cref="ExeHelper.Execute(string, string, bool, string, int)"/>.
/// </summary>
[MemberTest(Name = nameof(ExeHelper.Execute) + "(string,string,bool,string,int)")]
[MemberTest(Name = nameof(ExeHelper.Execute) + "(string, string, bool, string, int)")]
public static void Execute1()
{
When("the method is executed", () =>
Expand Down Expand Up @@ -102,9 +102,9 @@ public static void Execute1()
}

/// <summary>
/// Tests the <see cref="ExeHelper.Execute(string,string,string)"/>.
/// Tests the <see cref="ExeHelper.Execute(string, string, string)"/>.
/// </summary>
[MemberTest(Name = nameof(ExeHelper.Execute) + "(string,string,string)")]
[MemberTest(Name = nameof(ExeHelper.Execute) + "(string, string, string)")]
public static void Execute2()
{
When("the method is executed", () =>
Expand All @@ -125,4 +125,62 @@ public static void Execute2()
});
});
}

/// <summary>
/// Tests the <see cref="ExeHelper.Execute(string, string, TextWriter, TextWriter, string, int, int)"/>.
/// </summary>
[MemberTest(Name = nameof(ExeHelper.Execute) + "(string, string, TextWriter, TextWriter, string, int, int)")]
public static void Execute3()
{
When("the method is executed", () =>
{
var result = 0;
string exePath = null;
Exception thrownException = null;
StringWriter output = null;
StringWriter error = null;

Arrange(() =>
{
exePath = "dotnet.exe";
error = new ();
output = new ();
});

Act(() =>
{
try
{
result = ExeHelper.Execute(exePath, "--UNKNOWN", output, error, string.Empty, 5000, 2048);
}
catch (Exception e)
{
thrownException = e;
}
});

And("executable file name is specified", () =>
{
Should("return an exit code with forwarder output and error streams", () =>
{
Assert.That(result, Is.EqualTo(1));
Assert.That(output.ToString(), Contains.Substring("You misspelled a built-in dotnet command"));
Assert.That(error.ToString(), Contains.Substring("the specified command or file was not found"));
});
});

And("executable file name is not specified", () =>
{
Arrange(() =>
{
exePath = null;
});

Should("throw the InvalidOperationException", () =>
{
Assert.That(thrownException, Is.InstanceOf<InvalidOperationException>());
});
});
});
}
}

0 comments on commit 8d33d22

Please sign in to comment.