284 lines
7.1 KiB
C#
284 lines
7.1 KiB
C#
using System.Diagnostics;
|
|
using System.Text.RegularExpressions;
|
|
using InnovEnergy.SysTools.Utils;
|
|
using static System.ConsoleColor;
|
|
|
|
namespace InnovEnergy.SysTools.Process;
|
|
|
|
using Env = Dictionary<String, String>;
|
|
|
|
|
|
public class SyncProcess
|
|
{
|
|
public SysCommand Command { get; }
|
|
public Env Environment { get; }
|
|
public Boolean LogToConsole { get; }
|
|
|
|
public Boolean HasFinished => Process.HasExited;
|
|
|
|
private Task<(DateTime timeStamp, StdOutput output)> _ReadStdOut;
|
|
private Task<(DateTime timeStamp, StdError error )> _ReadStdErr;
|
|
|
|
private System.Diagnostics.Process Process { get; }
|
|
|
|
public SyncProcess(SysCommand command, Env environment = null, Boolean logToConsole = false)
|
|
{
|
|
Command = command;
|
|
Environment = environment;
|
|
LogToConsole = logToConsole;
|
|
Process = InitProcess();
|
|
|
|
_ReadStdOut = ReadNextStdOut();
|
|
_ReadStdErr = ReadNextStdErr();
|
|
}
|
|
|
|
private Task<(DateTime timeStamp, StdOutput output)> ReadNextStdOut()
|
|
{
|
|
Debug.Assert(_ReadStdOut == null || _ReadStdOut.IsCompleted);
|
|
|
|
return Process
|
|
.StandardOutput
|
|
.ReadLineAsync()
|
|
.ContinueWith(t => (DateTime.Now, SyncProcessOutput.StdOutput(t.Result)));
|
|
|
|
}
|
|
|
|
private Task<(DateTime timeStamp, StdError error)> ReadNextStdErr()
|
|
{
|
|
Debug.Assert(_ReadStdErr == null || _ReadStdErr.IsCompleted);
|
|
|
|
return Process
|
|
.StandardError
|
|
.ReadLineAsync()
|
|
.ContinueWith(t => (DateTime.Now, SyncProcessOutput.StdError(t.Result)));
|
|
}
|
|
|
|
|
|
private System.Diagnostics.Process InitProcess()
|
|
{
|
|
var startInfo = new ProcessStartInfo
|
|
{
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
RedirectStandardInput = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
Arguments = Command.Args.Trim(),
|
|
FileName = Command.Path,
|
|
};
|
|
|
|
foreach (var ev in Environment.EmptyIfNull())
|
|
startInfo.EnvironmentVariables[ev.Key] = ev.Value;
|
|
|
|
var proc = new System.Diagnostics.Process
|
|
{
|
|
StartInfo = startInfo,
|
|
EnableRaisingEvents = true
|
|
};
|
|
|
|
proc.Start(); // must start BEFORE we access Streams
|
|
|
|
return proc;
|
|
}
|
|
|
|
public Match ReadStdOutUntil(Regex rx)
|
|
{
|
|
foreach (var l in ReadStdOut())
|
|
{
|
|
|
|
var match = rx.Match(l);
|
|
if (match.Success)
|
|
return match;
|
|
}
|
|
|
|
throw new Exception("no matching output");
|
|
}
|
|
|
|
public String ReadStdOutUntil(String line)
|
|
{
|
|
return ReadStdOutUntil(l => l == line);
|
|
}
|
|
|
|
|
|
public String ReadStdOutUntil(Func<String, Boolean> predicate)
|
|
{
|
|
foreach (var l in ReadStdOut())
|
|
{
|
|
if (predicate(l))
|
|
return l;
|
|
}
|
|
|
|
throw new Exception("no matching output");
|
|
}
|
|
|
|
public IEnumerable<String> ReadStdOut(TimeSpan timeOut = default) => ReadMany(timeOut).ThrownOnStdErr();
|
|
|
|
public IEnumerable<SyncProcessOutput> ReadMany(TimeSpan timeOut = default)
|
|
{
|
|
SyncProcessOutput output;
|
|
|
|
do
|
|
{
|
|
output = ReadNext(timeOut);
|
|
yield return output;
|
|
}
|
|
while (!(output is ExitCode) && !(output is TimeoutError));
|
|
}
|
|
|
|
public SyncProcessOutput ReadNext(TimeSpan timeOut = default)
|
|
{
|
|
var wait = timeOut == default
|
|
? Task.WaitAny(_ReadStdOut, _ReadStdErr)
|
|
: Task.WaitAny(_ReadStdOut, _ReadStdErr, Task.Delay(timeOut));
|
|
|
|
if (wait == 2)
|
|
{
|
|
if (LogToConsole)
|
|
"Timeout".WriteLine(Red);
|
|
|
|
return SyncProcessOutput.TimeoutError(timeOut);
|
|
}
|
|
|
|
if (_ReadStdOut.IsCompleted && _ReadStdErr.IsCompleted)
|
|
{
|
|
var (outTimeStamp, output) = _ReadStdOut.Result;
|
|
var (errTimeStamp, error) = _ReadStdErr.Result;
|
|
|
|
if (outTimeStamp < errTimeStamp && output != null)
|
|
{
|
|
_ReadStdOut = ReadNextStdOut();
|
|
|
|
if (LogToConsole)
|
|
output.Data.WriteLine();
|
|
|
|
return output;
|
|
}
|
|
|
|
if (error != null)
|
|
{
|
|
_ReadStdErr = ReadNextStdErr();
|
|
|
|
if (LogToConsole)
|
|
error.Data.WriteLine(Red);
|
|
|
|
return error;
|
|
}
|
|
|
|
Process.WaitForExit();
|
|
|
|
var exitCode = Process.ExitCode;
|
|
|
|
if (LogToConsole)
|
|
{
|
|
"ExitCode: ".Write(Cyan);
|
|
exitCode.WriteLine(exitCode == 0 ? Green : Red);
|
|
}
|
|
|
|
return SyncProcessOutput.ExitCode(exitCode);
|
|
}
|
|
|
|
if (_ReadStdOut.IsCompleted)
|
|
{
|
|
var (_, output) = _ReadStdOut.Result;
|
|
|
|
if (output != null)
|
|
{
|
|
_ReadStdOut = ReadNextStdOut();
|
|
return output;
|
|
}
|
|
|
|
}
|
|
|
|
if (_ReadStdErr.IsCompleted)
|
|
{
|
|
var (_, error) = _ReadStdErr.Result;
|
|
|
|
if (error != null)
|
|
{
|
|
_ReadStdErr = ReadNextStdErr();
|
|
return error;
|
|
}
|
|
|
|
}
|
|
|
|
throw new Exception("This should not happen");
|
|
}
|
|
|
|
public SyncProcess Terminate()
|
|
{
|
|
Process.Kill();
|
|
return this;
|
|
}
|
|
|
|
public SyncProcess Kill()
|
|
{
|
|
Process.Kill();
|
|
return this;
|
|
}
|
|
|
|
public void Write(String line)
|
|
{
|
|
Process.StandardInput.WriteLine(line);
|
|
}
|
|
|
|
public override String ToString() => Command.ToString(); // TODO: env
|
|
}
|
|
|
|
|
|
public abstract class SyncProcessOutput
|
|
{
|
|
public static StdOutput StdOutput (String data ) => new StdOutput(data);
|
|
public static StdError StdError (String data ) => new StdError(data);
|
|
public static ExitCode ExitCode (Int32 data ) => new ExitCode(data);
|
|
public static TimeoutError TimeoutError (TimeSpan timeout) => new TimeoutError(timeout);
|
|
}
|
|
|
|
public class ExitCode : SyncProcessOutput
|
|
{
|
|
public ExitCode(Int32 data) { Data = data;}
|
|
public Int32 Data { get; }
|
|
|
|
public override String ToString() => Data.ToString();
|
|
}
|
|
|
|
public class StdError : SyncProcessOutput
|
|
{
|
|
public StdError(String data) { Data = data;}
|
|
public String Data { get; }
|
|
|
|
public override String ToString() => Data;
|
|
}
|
|
|
|
public class StdOutput : SyncProcessOutput
|
|
{
|
|
public StdOutput(String data) { Data = data; }
|
|
public String Data { get; }
|
|
|
|
public override String ToString() => Data;
|
|
}
|
|
|
|
public class TimeoutError : SyncProcessOutput
|
|
{
|
|
public TimeoutError(TimeSpan timeout) { Timeout = timeout; }
|
|
public TimeSpan Timeout { get; }
|
|
|
|
public override String ToString() => Timeout.ToString();
|
|
}
|
|
|
|
|
|
public static class ProcessOutputExtensions
|
|
{
|
|
public static IEnumerable<String> ThrownOnStdErr(this IEnumerable<SyncProcessOutput> po)
|
|
{
|
|
foreach (var p in po)
|
|
{
|
|
if (p is StdOutput o)
|
|
yield return o.Data;
|
|
|
|
if (p is StdError e)
|
|
throw new Exception(e.Data);
|
|
|
|
// Ignore exit code
|
|
}
|
|
}
|
|
} |