From e104c0f41c2c248dbde83a0e78c2ac5f54dba074 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Thu, 27 Jun 2019 09:55:18 +0200 Subject: [PATCH] Add ExecFunctionOptions --- README.md | 52 +++++- src/Tmds.ExecFunction/ExecFunction.cs | 157 ++++++++++++++----- src/Tmds.ExecFunction/ExecFunctionOptions.cs | 17 ++ src/Tmds.ExecFunction/FunctionExecutor.cs | 136 ++++++++-------- test/Tmds.ExecFunction.Tests/UnitTest1.cs | 33 ++-- 5 files changed, 270 insertions(+), 125 deletions(-) create mode 100644 src/Tmds.ExecFunction/ExecFunctionOptions.cs diff --git a/README.md b/README.md index ddf06b5..62b0f11 100644 --- a/README.md +++ b/README.md @@ -13,21 +13,57 @@ This library supports .NET Core 2.0+ on Windows and Linux. # Usage -The main method of the library is `ExecFunction.Start`. It accepts a delegate that is the function to execute in the remote process. The function can have the same signature of a .NET `Main`: a `void`/`string[]` argument, and a `void`/`int`/`Task`/`Task` return type. - -The method returns the started process as a `System.Diagnostics.Process`. +The main method of the library is `ExecFunction.Run`. It accepts a delegate that is the function to execute in the remote process. The function can have the same signature of a .NET `Main`: a `void`/`string[]` argument, and a `void`/`int`/`Task`/`Task` return type. For example: ```cs -using (Process p = ExecFunction.Start(() => Console.WriteLine("Hello from child process!"))) -{ - p.WaitForExit(); -} +ExecFunction.Run(() => Console.WriteLine("Hello from child process!")); ``` The `ProcessStartInfo` that is used to start the process can be configured, by adding a configuration delegate: ```cs -ExecFunction.Start(..., psi => psi.RedirectStandardOutput = true); +ExecFunction.Run(..., o => o.StartInfo.RedirectStandardOutput = true); +``` + +If you want to re-use the same configuration for multiple invocations you can use the `FunctionExecutor` class. + +```cs +private FunctionExecutor FunctionExecutor = new FunctionExecutor(o.StartInfo.RedirectStandardOutput = true); + +// Now call FunctionExecutor.Run(...). +``` + +The configuration allows you to add an `OnExit` action. For example, you can use this FunctionExecutor in an xunit project and `Assert` in the child process: + +```cs +private FunctionExecutor FunctionExecutor = new FunctionExecutor( + o => + { + o.StartInfo.RedirectStandardError = true; + o.OnExit = p => + { + if (p.ExitCode != 0) + { + string message = $"Function exit code failed with exit code: {p.ExitCode}" + Environment.NewLine + + p.StandardError.ReadToEnd(); + throw new Xunit.Sdk.XunitException(message); + } + }; + } +); + +[Fact] +public void TestArgStringArrayReturnVoid() +{ + FunctionExecutor.Run( + (string[] args) => + { + Assert.Equal("arg1", args[0]); + Assert.Equal("arg2", args[1]); + }, + new string[] { "arg1", "arg2" } + ); +} ``` When `ExecFunction` is used from the `dotnet` host, it will work out-of-the box. diff --git a/src/Tmds.ExecFunction/ExecFunction.cs b/src/Tmds.ExecFunction/ExecFunction.cs index e0d0431..6b5b0b7 100644 --- a/src/Tmds.ExecFunction/ExecFunction.cs +++ b/src/Tmds.ExecFunction/ExecFunction.cs @@ -14,49 +14,137 @@ namespace Tmds.Utils { - public static class ProcessStartInfoExtensions - { - public static ProcessStartInfo WithRedirectedStdio(this ProcessStartInfo psi) - { - psi.RedirectStandardError = true; - psi.RedirectStandardInput = true; - psi.RedirectStandardOutput = true; - return psi; - } - } - public static partial class ExecFunction { - public static readonly Action RedirectStdio = psi => psi.WithRedirectedStdio(); + public static Process Start(Action action, Action configure = null) + => Start(GetMethodInfo(action), Array.Empty(), configure).process; + + public static Process Start(Action action, string[] args, Action configure = null) + => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure).process; + + public static Process Start(Func action, Action configure = null) + => Start(GetMethodInfo(action), Array.Empty(), configure).process; + + public static Process Start(Func action, string[] args, Action configure = null) + => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure).process; + + public static Process Start(Func action, Action configure = null) + => Start(GetMethodInfo(action), Array.Empty(), configure).process; + + public static Process Start(Func action, string[] args, Action configure = null) + => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure).process; + + public static Process Start(Func> action, Action configure = null) + => Start(GetMethodInfo(action), Array.Empty(), configure).process; + + public static Process Start(Func> action, string[] args, Action configure = null) + => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure).process; - public static Process Start(Action action, Action configure = null) - => Start(GetMethodInfo(action), Array.Empty(), configure); + public static void Run(Action action, Action configure = null) + => Start(GetMethodInfo(action), Array.Empty(), configure, waitForExit: true); - public static Process Start(Action action, string[] args, Action configure = null) - => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure); + public static void Run(Action action, string[] args, Action configure = null) + => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, waitForExit: true); - public static Process Start(Func action, Action configure = null) - => Start(GetMethodInfo(action), Array.Empty(), configure); + public static void Run(Func action, Action configure = null) + => Start(GetMethodInfo(action), Array.Empty(), configure, waitForExit: true); - public static Process Start(Func action, string[] args, Action configure = null) - => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure); + public static void Run(Func action, string[] args, Action configure = null) + => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, waitForExit: true); - public static Process Start(Func action, Action configure = null) - => Start(GetMethodInfo(action), Array.Empty(), configure); + public static void Run(Func action, Action configure = null) + => Start(GetMethodInfo(action), Array.Empty(), configure, waitForExit: true); - public static Process Start(Func action, string[] args, Action configure = null) - => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure); + public static void Run(Func action, string[] args, Action configure = null) + => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, waitForExit: true); - public static Process Start(Func> action, Action configure = null) - => Start(GetMethodInfo(action), Array.Empty(), configure); + public static void Run(Func> action, Action configure = null) + => Start(GetMethodInfo(action), Array.Empty(), configure, waitForExit: true); - public static Process Start(Func> action, string[] args, Action configure = null) - => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure); + public static void Run(Func> action, string[] args, Action configure = null) + => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, waitForExit: true); - private static Process Start(MethodInfo method, string[] args, Action configure) - => Process.Start(CreateProcessStartInfo(method, args, configure)); + public static Task RunAsync(Action action, Action configure = null) + => Start(GetMethodInfo(action), Array.Empty(), configure, returnTask: true).exitedTask; - private static ProcessStartInfo CreateProcessStartInfo(MethodInfo method, string[] args, Action configure) + public static Task RunAsync(Action action, string[] args, Action configure = null) + => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, returnTask: true).exitedTask; + + public static Task RunAsync(Func action, Action configure = null) + => Start(GetMethodInfo(action), Array.Empty(), configure, returnTask: true).exitedTask; + + public static Task RunAsync(Func action, string[] args, Action configure = null) + => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, returnTask: true).exitedTask; + + public static Task RunAsync(Func action, Action configure = null) + => Start(GetMethodInfo(action), Array.Empty(), configure, returnTask: true).exitedTask; + + public static Task RunAsync(Func action, string[] args, Action configure = null) + => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, returnTask: true).exitedTask; + + public static Task RunAsync(Func> action, Action configure = null) + => Start(GetMethodInfo(action), Array.Empty(), configure, returnTask: true).exitedTask; + + public static Task RunAsync(Func> action, string[] args, Action configure = null) + => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, returnTask: true).exitedTask; + + private static (Process process, Task exitedTask) Start(MethodInfo method, string[] args, Action configure, bool waitForExit = false, bool returnTask = false) + { + Process process = null; + Task exitedTask = null; + try + { + process = new Process(); + + ExecFunctionOptions options = new ExecFunctionOptions(process.StartInfo); + ConfigureProcessStartInfoForMethodInvocation(method, args, options.StartInfo); + configure?.Invoke(options); + + TaskCompletionSource tcs = null; + if (returnTask == true) + { + tcs = new TaskCompletionSource(); + } + + if (options.OnExit != null || tcs != null) + { + process.EnableRaisingEvents = true; + process.Exited += (_1, _2) => + { + options.OnExit(process); + + if (tcs != null) + { + tcs?.SetResult(true); + process.Dispose(); + } + }; + } + + process.Start(); + + if (waitForExit) + { + process.WaitForExit(); + } + + return (process, exitedTask); + } + catch + { + process?.Dispose(); + throw; + } + finally + { + if (waitForExit) + { + process?.Dispose(); + } + } + } + + private static void ConfigureProcessStartInfoForMethodInvocation(MethodInfo method, string[] args, ProcessStartInfo psi) { if (method.ReturnType != typeof(void) && method.ReturnType != typeof(int) && @@ -73,11 +161,6 @@ private static ProcessStartInfo CreateProcessStartInfo(MethodInfo method, string throw new ArgumentException("method has non string[] argument", nameof(method)); } - // Start the other process and return a wrapper for it to handle its lifetime and exit checking. - ProcessStartInfo psi = new ProcessStartInfo(); - psi.UseShellExecute = false; - configure?.Invoke(psi); - // If we need the host (if it exists), use it, otherwise target the console app directly. Type t = method.DeclaringType; Assembly a = t.GetTypeInfo().Assembly; @@ -87,8 +170,6 @@ private static ProcessStartInfo CreateProcessStartInfo(MethodInfo method, string psi.FileName = HostFilename; psi.Arguments = fullArgs; - - return psi; } private static MethodInfo GetMethodInfo(Delegate d) diff --git a/src/Tmds.ExecFunction/ExecFunctionOptions.cs b/src/Tmds.ExecFunction/ExecFunctionOptions.cs new file mode 100644 index 0000000..3042e54 --- /dev/null +++ b/src/Tmds.ExecFunction/ExecFunctionOptions.cs @@ -0,0 +1,17 @@ +using System; +using System.Diagnostics; + +namespace Tmds.Utils +{ + public class ExecFunctionOptions + { + internal ExecFunctionOptions(ProcessStartInfo psi) + { + StartInfo = psi; + } + + public ProcessStartInfo StartInfo { get; } + + public Action OnExit { get; set; } + } +} \ No newline at end of file diff --git a/src/Tmds.ExecFunction/FunctionExecutor.cs b/src/Tmds.ExecFunction/FunctionExecutor.cs index 6a8996b..2142684 100644 --- a/src/Tmds.ExecFunction/FunctionExecutor.cs +++ b/src/Tmds.ExecFunction/FunctionExecutor.cs @@ -10,88 +10,86 @@ namespace Tmds.Utils { public class FunctionExecutor { - private readonly Action _configure; - private readonly Action _onExit; + private readonly Action _configure; - public FunctionExecutor(Action configure, Action onExit = null) + public FunctionExecutor(Action configure) { _configure = configure; - _onExit = onExit; } - public void Run(Action action, Action configure = null) - { - using (Process process = ExecFunction.Start(action, CombineConfigures(_configure, configure))) - { - process.WaitForExit(); - _onExit?.Invoke(process); - } - } + public Process Start(Action action, Action configure = null) + => ExecFunction.Start(action, CombineConfigures(_configure, configure)); - public void Run(Action action, string[] args, Action configure = null) - { - using (Process process = ExecFunction.Start(action, args, CombineConfigures(_configure, configure))) - { - process.WaitForExit(); - _onExit?.Invoke(process); - } - } + public Process Start(Action action, string[] args, Action configure = null) + => ExecFunction.Start(action, args, CombineConfigures(_configure, configure)); - public void Run(Func action, Action configure = null) - { - using (Process process = ExecFunction.Start(action, CombineConfigures(_configure, configure))) - { - process.WaitForExit(); - _onExit?.Invoke(process); - } - } + public Process Start(Func action, Action configure = null) + => ExecFunction.Start(action, CombineConfigures(_configure, configure)); - public void Run(Func action, string[] args, Action configure = null) - { - using (Process process = ExecFunction.Start(action, args, CombineConfigures(_configure, configure))) - { - process.WaitForExit(); - _onExit?.Invoke(process); - } - } + public Process Start(Func action, string[] args, Action configure = null) + => ExecFunction.Start(action, args, CombineConfigures(_configure, configure)); - public void Run(Func action, Action configure = null) - { - using (Process process = ExecFunction.Start(action, CombineConfigures(_configure, configure))) - { - process.WaitForExit(); - _onExit?.Invoke(process); - } - } + public Process Start(Func action, Action configure = null) + => ExecFunction.Start(action, CombineConfigures(_configure, configure)); - public void Run(Func action, string[] args, Action configure = null) - { - using (Process process = ExecFunction.Start(action, args, CombineConfigures(_configure, configure))) - { - process.WaitForExit(); - _onExit?.Invoke(process); - } - } + public Process Start(Func action, string[] args, Action configure = null) + => ExecFunction.Start(action, args, CombineConfigures(_configure, configure)); - public void Run(Func> action, Action configure = null) - { - using (Process process = ExecFunction.Start(action, CombineConfigures(_configure, configure))) - { - process.WaitForExit(); - _onExit?.Invoke(process); - } - } + public Process Start(Func> action, Action configure = null) + => ExecFunction.Start(action, CombineConfigures(_configure, configure)); - public void Run(Func> action, string[] args, Action configure = null) - { - using (Process process = ExecFunction.Start(action, args, CombineConfigures(_configure, configure))) - { - process.WaitForExit(); - _onExit?.Invoke(process); - } - } + public Process Start(Func> action, string[] args, Action configure = null) + => ExecFunction.Start(action, args, CombineConfigures(_configure, configure)); + + public void Run(Action action, Action configure = null) + => ExecFunction.Run(action, CombineConfigures(_configure, configure)); + + public void Run(Action action, string[] args, Action configure = null) + => ExecFunction.Run(action, args, CombineConfigures(_configure, configure)); + + public void Run(Func action, Action configure = null) + => ExecFunction.Run(action, CombineConfigures(_configure, configure)); + + public void Run(Func action, string[] args, Action configure = null) + => ExecFunction.Run(action, args, CombineConfigures(_configure, configure)); + + public void Run(Func action, Action configure = null) + => ExecFunction.Run(action, CombineConfigures(_configure, configure)); + + public void Run(Func action, string[] args, Action configure = null) + => ExecFunction.Run(action, args, CombineConfigures(_configure, configure)); + + public void Run(Func> action, Action configure = null) + => ExecFunction.Run(action, CombineConfigures(_configure, configure)); + + public void Run(Func> action, string[] args, Action configure = null) + => ExecFunction.Run(action, args, CombineConfigures(_configure, configure)); + + public Task RunAsync(Action action, Action configure = null) + => ExecFunction.RunAsync(action, CombineConfigures(_configure, configure)); + + public Task RunAsync(Action action, string[] args, Action configure = null) + => ExecFunction.RunAsync(action, args, CombineConfigures(_configure, configure)); + + public Task RunAsync(Func action, Action configure = null) + => ExecFunction.RunAsync(action, CombineConfigures(_configure, configure)); + + public Task RunAsync(Func action, string[] args, Action configure = null) + => ExecFunction.RunAsync(action, args, CombineConfigures(_configure, configure)); + + public Task RunAsync(Func action, Action configure = null) + => ExecFunction.RunAsync(action, CombineConfigures(_configure, configure)); + + public Task RunAsync(Func action, string[] args, Action configure = null) + => ExecFunction.RunAsync(action, args, CombineConfigures(_configure, configure)); + + public Task RunAsync(Func> action, Action configure = null) + => ExecFunction.RunAsync(action, CombineConfigures(_configure, configure)); + + public Task RunAsync(Func> action, string[] args, Action configure = null) + => ExecFunction.RunAsync(action, args, CombineConfigures(_configure, configure)); - private static Action CombineConfigures(Action first, Action second) + private static Action CombineConfigures(Action first, Action second) { if (first == null) { diff --git a/test/Tmds.ExecFunction.Tests/UnitTest1.cs b/test/Tmds.ExecFunction.Tests/UnitTest1.cs index 796ce4f..8f005aa 100644 --- a/test/Tmds.ExecFunction.Tests/UnitTest1.cs +++ b/test/Tmds.ExecFunction.Tests/UnitTest1.cs @@ -20,17 +20,30 @@ public void TestArgVoidReturnInt() [Fact] public void TestArgStringArrayReturnVoid() { - using (Process p = ExecFunction.Start((string[] args) => - { - Assert.Equal("arg1", args[0]); - Assert.Equal("arg2", args[1]); - }, - new string[] { "arg1", "arg2" }, - ExecFunction.RedirectStdio)) + FunctionExecutor.Run( + (string[] args) => + { + Assert.Equal("arg1", args[0]); + Assert.Equal("arg2", args[1]); + }, + new string[] { "arg1", "arg2" } + ); + } + + private FunctionExecutor FunctionExecutor = new FunctionExecutor( + o => { - p.WaitForExit(); - Assert.True(p.ExitCode == 0, p.StandardError.ReadToEnd()); + o.StartInfo.RedirectStandardError = true; + o.OnExit = p => + { + if (p.ExitCode != 0) + { + string message = $"Function exit code failed with exit code: {p.ExitCode}" + Environment.NewLine + + p.StandardError.ReadToEnd(); + throw new Xunit.Sdk.XunitException(message); + } + }; } - } + ); } }