using System.Diagnostics; using System.Text.RegularExpressions; using InnovEnergy.SysTools.Utils; using static System.ConsoleColor; namespace InnovEnergy.SysTools.Process; using Env = Dictionary; 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 predicate) { foreach (var l in ReadStdOut()) { if (predicate(l)) return l; } throw new Exception("no matching output"); } public IEnumerable ReadStdOut(TimeSpan timeOut = default) => ReadMany(timeOut).ThrownOnStdErr(); public IEnumerable 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 ThrownOnStdErr(this IEnumerable 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 } } }