Files
mixtape/zero.Web/DevServer/ProcessProxy.cs
T

253 lines
6.9 KiB
C#
Raw Normal View History

2020-11-01 23:23:36 +01:00
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Zero.Web.DevServer
{
2020-11-03 00:49:41 +01:00
public class ProcessProxy
2020-11-01 23:23:36 +01:00
{
string workingDirectory = null;
string script = null;
Action<ProcessStartInfo> onProcessConfigure = null;
Action<string, bool> captureLog = null;
bool isWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
Dictionary<string, string> envVars = new();
HashSet<string> arguments = new();
bool forwardLog = false;
Process process = null;
EventedStreamReader stdOut = null;
EventedStreamReader stdErr = null;
2020-11-03 00:49:41 +01:00
public ProcessProxy(string workingDirectory, string script, bool forwardLog = false)
2020-11-01 23:23:36 +01:00
{
this.workingDirectory = workingDirectory;
this.script = script;
this.forwardLog = forwardLog;
}
/// <summary>
/// Adds an environment variable
/// </summary>
2020-11-03 00:49:41 +01:00
public ProcessProxy EnvVar(string key, string value)
2020-11-01 23:23:36 +01:00
{
envVars.Add(key, value);
return this;
}
/// <summary>
/// Adds an argument
/// </summary>
2020-11-03 00:49:41 +01:00
public ProcessProxy Argument(string argument)
2020-11-01 23:23:36 +01:00
{
arguments.Add(argument);
return this;
}
/// <summary>
/// Configures the process start info
/// </summary>
2020-11-03 00:49:41 +01:00
public ProcessProxy Configure(Action<ProcessStartInfo> onProcessConfigure)
2020-11-01 23:23:36 +01:00
{
this.onProcessConfigure = onProcessConfigure;
return this;
}
/// <summary>
/// Capture the log instead of outputting it to the console
/// </summary>
2020-11-03 00:49:41 +01:00
public ProcessProxy Capture(Action<string, bool> action)
2020-11-01 23:23:36 +01:00
{
this.captureLog = action;
return this;
}
/// <summary>
/// Run the script and wait for completion.
/// This is only recommended for scripts which finish automatically and have no user interaction.
/// </summary>
public async Task ExecuteAsync(TimeSpan timeout = default)
{
StartProcess();
using var stdErrReader = new EventedStreamStringReader(stdErr);
try
{
// TODO implement timeout
// https://stackoverflow.com/questions/18760252/timeout-an-async-method-implemented-with-taskcompletionsource
await stdOut.WaitForFinish();
}
catch (EndOfStreamException ex)
{
throw new InvalidOperationException($"The script '{script}' exited without indicating that the server was listening for requests.\nThe error output was: " + $"{stdErrReader.ReadAsString()}", ex);
}
}
/// <summary>
/// Run the script and return as soon as a condition is met.
/// The script will not cancel and will continue to run as a sub-process as long as it does not auto-close.
/// </summary>
public async Task RunAsync(string startupCondition, TimeSpan startupTimeout = default)
{
StartProcess();
using var stdErrReader = new EventedStreamStringReader(stdErr);
try
{
await stdOut.WaitForMatch(new Regex(startupCondition, RegexOptions.IgnoreCase, startupTimeout == default ? TimeSpan.FromMinutes(5) : startupTimeout));
}
catch (EndOfStreamException ex)
{
throw new InvalidOperationException($"The script '{script}' exited without indicating that the server was listening for requests.\nThe error output was: " + $"{stdErrReader.ReadAsString()}", ex);
}
}
public void Exit()
{
try { process?.Kill(); } catch { }
try { process?.WaitForExit(); } catch { }
AppDomain.CurrentDomain.DomainUnload -= UnloadHandler;
AppDomain.CurrentDomain.ProcessExit -= UnloadHandler;
AppDomain.CurrentDomain.UnhandledException -= UnloadHandler;
}
/// <summary>
/// Run
/// </summary>
Process StartProcess()
{
string executable = script;
StringBuilder command = new();
command.Append(script);
command.Append(' ');
foreach (string arg in arguments)
{
command.Append(arg);
}
string argumentList = command.ToString();
if (isWindows)
{
argumentList = $"/c {argumentList}";
executable = "cmd";
}
ProcessStartInfo startInfo = new(executable)
{
Arguments = argumentList,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDirectory = workingDirectory
};
foreach (var envVar in envVars)
{
startInfo.Environment[envVar.Key] = envVar.Value;
}
onProcessConfigure?.Invoke(startInfo);
try
{
process = Process.Start(startInfo);
process.EnableRaisingEvents = true;
}
catch (Exception ex)
{
var message = $"Failed to start '{startInfo.FileName}'. To resolve this:.\n\n"
+ $"[1] Ensure that '{startInfo.FileName}' is installed and can be found in one of the PATH directories.\n"
+ $" Current PATH enviroment variable is: { Environment.GetEnvironmentVariable("PATH") }\n"
+ " Make sure the executable is in one of those directories, or update your PATH.\n\n"
+ "[2] See the InnerException for further details of the cause.";
throw new InvalidOperationException(message, ex);
}
2020-11-03 00:49:41 +01:00
AttachLogger();
2020-11-01 23:23:36 +01:00
AppDomain.CurrentDomain.DomainUnload += UnloadHandler;
AppDomain.CurrentDomain.ProcessExit += UnloadHandler;
AppDomain.CurrentDomain.UnhandledException += UnloadHandler;
return process;
}
void UnloadHandler(object sender, EventArgs e)
{
Exit();
}
void AttachLogger(bool isStream = false)
{
stdOut = new EventedStreamReader(process.StandardOutput);
stdErr = new EventedStreamReader(process.StandardError);
stdOut.OnReceivedLine += line => WriteToLog(line);
stdOut.OnReceivedChunk += chunk => WriteToLog(chunk);
stdErr.OnReceivedLine += line => WriteToLog(line, true);
stdErr.OnReceivedChunk += chunk => WriteToLog(chunk, true);
}
void WriteToLog(string line, bool isError = false)
{
if (String.IsNullOrWhiteSpace(line))
{
return;
}
line = line.StartsWith("<s>") ? line.Substring(3) : line;
if (captureLog != null)
{
captureLog(line, isError);
}
2020-11-03 00:49:41 +01:00
if (forwardLog)
2020-11-01 23:23:36 +01:00
{
(isError ? Console.Error : Console.Out).WriteLine(line);
}
}
void WriteToLog(ArraySegment<char> chunk, bool isError = false)
{
bool containsNewline = Array.IndexOf(chunk.Array, '\n', chunk.Offset, chunk.Count) >= 0;
if (containsNewline)
{
return;
}
if (captureLog != null)
{
captureLog(new String(chunk.Array, chunk.Offset, chunk.Count), isError);
}
2020-11-03 00:49:41 +01:00
if (forwardLog)
2020-11-01 23:23:36 +01:00
{
(isError ? Console.Error : Console.Out).Write(chunk.Array, chunk.Offset, chunk.Count);
}
}
}
}