Innovenergy_trunk/csharp/Lib/SysTools/Process/AsyncProcess.cs

185 lines
4.7 KiB
C#

using System.Diagnostics;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using InnovEnergy.Lib.SysTools.Utils;
using static System.ConsoleColor;
namespace InnovEnergy.Lib.SysTools.Process;
using Env = Dictionary<String, String>;
public class AsyncProcess
{
private readonly Subject<String> _StandardIn;
private readonly Subject<String> _StandardOut;
private readonly Subject<String> _StandardErr;
private readonly ReplaySubject<Int32> _ExitCode;
public SysCommand Command { get; }
public Env Environment { get; }
public IObserver<String> StandardIn => _StandardIn;
public IObservable<String> StandardOut => _StandardOut;
public IObservable<String> StandardErr => _StandardErr;
public IObservable<Int32> ExitCode => _ExitCode;
public Boolean HasStarted => Process != null;
public Boolean HasFinished => HasStarted && Process.HasExited;
private System.Diagnostics.Process Process { get; set; }
public AsyncProcess(SysCommand command, Env environment = null, Boolean logToConsole = false)
{
Command = command;
Environment = environment;
_StandardIn = new Subject<String>();
_StandardOut = new Subject<String>();
_StandardErr = new Subject<String>();
_ExitCode = new ReplaySubject<Int32>();
if (logToConsole)
LogToConsole();
}
private void LogToConsole()
{
_StandardOut.Subscribe(d => d.WriteLine());
_StandardErr.Subscribe(d => d.WriteLine(Red));
_ExitCode.Subscribe(ec =>
{
"ExitCode: ".Write(Cyan);
ec.WriteLine(ec == 0 ? Green : Red);
});
}
public Int32 WaitForExit()
{
EnsureStarted();
return ExitCode.Wait();
//Process.WaitForExit(); // TODO
//return Process.ExitCode;
}
private void EnsureNotStarted()
{
if (HasStarted)
throw new Exception("Process has already started");
}
private void EnsureStarted()
{
if (!HasStarted)
throw new Exception("Process has not yet started");
}
public AsyncProcess Run()
{
EnsureNotStarted();
Process = InitProcess();
return this;
}
private System.Diagnostics.Process InitProcess()
{
var startInfo = new ProcessStartInfo
{
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
UseShellExecute = false,
CreateNoWindow = true,
Arguments = Command.Args,
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
Observable.Repeat(proc.StandardOutput, TaskPoolScheduler.Default)
.Select(s => s.ReadLine())
.TakeWhile(l => l != null)
.Subscribe(_StandardOut);
Observable.Repeat(proc.StandardError, TaskPoolScheduler.Default)
.Select(s => s.ReadLine())
.TakeWhile(l => l != null)
.Subscribe(_StandardErr);
_StandardIn.Subscribe(onNext: proc.StandardInput.WriteLine,
onError: _ => proc.StandardInput.Close(),
onCompleted: proc.StandardInput.Close);
Observable.FromEventPattern(e => proc.Exited += e, e => proc.Exited -= e)
.Take(1)
.Do(_=>Console.WriteLine("Exited"))
.OfType<Object>()
.Concat(StandardOut.IgnoreElements()) // make sure std streams finish first
.Concat(StandardErr.IgnoreElements())
.Select(_ => Process.ExitCode)
.Subscribe(_ExitCode);
_ExitCode.Subscribe(_ =>
{
proc.StandardInput.Close();
proc.StandardOutput.Close();
proc.StandardError.Close();
});
return proc;
}
public AsyncProcess Terminate()
{
try
{
Process.Kill();
}
catch
{
// ignored
}
return this;
}
public AsyncProcess Kill()
{
EnsureStarted();
try
{
Process.Kill();
}
catch
{
// ignored
}
return this;
}
public IDisposable ThrowOnStdErr()
{
return StandardErr.Subscribe(e => throw new Exception(e));
}
public override String ToString() => Command.ToString(); // TODO: env
}