Skip to content

Commit

Permalink
Add ExecFunctionOptions
Browse files Browse the repository at this point in the history
  • Loading branch information
tmds committed Jun 27, 2019
1 parent 8be5dcb commit e104c0f
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 125 deletions.
52 changes: 44 additions & 8 deletions README.md
Expand Up @@ -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<int>` 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<int>` 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.
Expand Down
157 changes: 119 additions & 38 deletions src/Tmds.ExecFunction/ExecFunction.cs
Expand Up @@ -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<ProcessStartInfo> RedirectStdio = psi => psi.WithRedirectedStdio();
public static Process Start(Action action, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure).process;

public static Process Start(Action<string[]> action, string[] args, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure).process;

public static Process Start(Func<int> action, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure).process;

public static Process Start(Func<string[], int> action, string[] args, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure).process;

public static Process Start(Func<Task> action, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure).process;

public static Process Start(Func<string[], Task> action, string[] args, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure).process;

public static Process Start(Func<Task<int>> action, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure).process;

public static Process Start(Func<string[], Task<int>> action, string[] args, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure).process;

public static Process Start(Action action, Action<ProcessStartInfo> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure);
public static void Run(Action action, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure, waitForExit: true);

public static Process Start(Action<string[]> action, string[] args, Action<ProcessStartInfo> configure = null)
=> Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure);
public static void Run(Action<string[]> action, string[] args, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, waitForExit: true);

public static Process Start(Func<int> action, Action<ProcessStartInfo> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure);
public static void Run(Func<int> action, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure, waitForExit: true);

public static Process Start(Func<string[], int> action, string[] args, Action<ProcessStartInfo> configure = null)
=> Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure);
public static void Run(Func<string[], int> action, string[] args, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, waitForExit: true);

public static Process Start(Func<Task> action, Action<ProcessStartInfo> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure);
public static void Run(Func<Task> action, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure, waitForExit: true);

public static Process Start(Func<string[], Task> action, string[] args, Action<ProcessStartInfo> configure = null)
=> Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure);
public static void Run(Func<string[], Task> action, string[] args, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, waitForExit: true);

public static Process Start(Func<Task<int>> action, Action<ProcessStartInfo> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure);
public static void Run(Func<Task<int>> action, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure, waitForExit: true);

public static Process Start(Func<string[], Task<int>> action, string[] args, Action<ProcessStartInfo> configure = null)
=> Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure);
public static void Run(Func<string[], Task<int>> action, string[] args, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, waitForExit: true);

private static Process Start(MethodInfo method, string[] args, Action<ProcessStartInfo> configure)
=> Process.Start(CreateProcessStartInfo(method, args, configure));
public static Task RunAsync(Action action, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure, returnTask: true).exitedTask;

private static ProcessStartInfo CreateProcessStartInfo(MethodInfo method, string[] args, Action<ProcessStartInfo> configure)
public static Task RunAsync(Action<string[]> action, string[] args, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, returnTask: true).exitedTask;

public static Task RunAsync(Func<int> action, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure, returnTask: true).exitedTask;

public static Task RunAsync(Func<string[], int> action, string[] args, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, returnTask: true).exitedTask;

public static Task RunAsync(Func<Task> action, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure, returnTask: true).exitedTask;

public static Task RunAsync(Func<string[], Task> action, string[] args, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, returnTask: true).exitedTask;

public static Task RunAsync(Func<Task<int>> action, Action<ExecFunctionOptions> configure = null)
=> Start(GetMethodInfo(action), Array.Empty<string>(), configure, returnTask: true).exitedTask;

public static Task RunAsync(Func<string[], Task<int>> action, string[] args, Action<ExecFunctionOptions> 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<ExecFunctionOptions> 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<bool> tcs = null;
if (returnTask == true)
{
tcs = new TaskCompletionSource<bool>();
}

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) &&
Expand All @@ -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;
Expand All @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions 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<Process> OnExit { get; set; }
}
}

0 comments on commit e104c0f

Please sign in to comment.