using System; using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; namespace HeuristicLab.PluginInfrastructure { /// /// Enum for the different RunnerStates. /// public enum RunnerState { Created, Cancelled, Starting, Running, Paused, Stopping, Stopped } /// /// Abstract RunnerBase class, which implements the IRunner interface. /// Every specific runner implementation should inherit this class. /// public abstract class RunnerHost : IRunnerHost { #region Vars protected Process process; #endregion #region Properties /// /// The programm, which should be used. For example 'docker'. /// protected string Program { get; private set; } /// /// The specific start arguments for the programm. /// protected string StartArgument { get; private set; } protected string UserName { get; private set; } protected string Password { get; private set; } protected string Domain { get; private set; } /// /// The runner state. /// public RunnerState State { get; protected set; } = RunnerState.Created; #endregion #region Constructors protected RunnerHost(string program, string startArgument, string userName, string password, string domain) { Program = program; StartArgument = startArgument + " --StartAsRunnerHost"; UserName = userName; Password = password; Domain = domain; } #endregion public virtual void Run(IRunner runner) => RunAsync(runner, null).Wait(); public virtual async Task RunAsync(IRunner runner, CancellationToken? token = null) { if (State != RunnerState.Created) throw new InvalidOperationException("Runner must be in state 'Created'."); State = RunnerState.Starting; process = new Process { StartInfo = new ProcessStartInfo { FileName = Program, Arguments = StartArgument, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardInput = true, RedirectStandardError = true, CreateNoWindow = false, UserName = string.IsNullOrEmpty(UserName) ? null : UserName, PasswordInClearText = string.IsNullOrEmpty(Password) ? null : Password, Domain = string.IsNullOrEmpty(Domain) ? null : Domain, WorkingDirectory = Directory.GetCurrentDirectory() }, EnableRaisingEvents = true }; process.Start(); // registers a task for cancellation, prevents the use of polling (while-loop) var task = RegisterCancellation(token.HasValue ? token.Value : CancellationToken.None); // write config to standardinput, runner listens on this and deserializes the config Runner.Serialize(runner, process.StandardInput.BaseStream); process.BeginOutputReadLine(); process.BeginErrorReadLine(); if (!runner.QuietMode) { process.OutputDataReceived += (s, e) => Console.WriteLine(e.Data); process.ErrorDataReceived += (s, e) => Console.WriteLine(e.Data); } State = RunnerState.Running; if (await task) State = RunnerState.Cancelled; else State = RunnerState.Stopped; } /// /// Pauses the runners. The childprocess gets notified with the help of the standard input and a serialized RunnerJob. /// To pause the runner, the state must be "Running". /// public void Pause() { if (State != RunnerState.Running) throw new InvalidOperationException("Runner must be in state 'Running'!"); Runner.Pause(process.StandardInput.BaseStream); State = RunnerState.Paused; } /// /// Resumes a paused runner. The childprocess gets notified with the help of the standard input and a serialized RunnerJob. /// To resume the runner, the state must be "Paused". /// public void Resume() { if (State != RunnerState.Paused) throw new InvalidOperationException("Runner must be in state 'Paused'!"); Runner.Resume(process.StandardInput.BaseStream); State = RunnerState.Running; } /// /// Creates a new LinkedTokenSource and a TaskCompletionSource. /// When the specified token gets cancelled, a cancel requests gets send to the childprocess. /// Afterwards the main process waits for the exit of the child process and sets a result of the TaskCompletionSource. /// When the child process gets finished without requested cancellation, the linked token gets cancelled and a result set. /// private Task RegisterCancellation(CancellationToken token) { if (process != null && State == RunnerState.Starting) { var cts = CancellationTokenSource.CreateLinkedTokenSource(token); var tcs = new TaskCompletionSource(); process.Exited += (s, e) => cts.Cancel(); cts.Token.Register(() => { if (!process.HasExited) { Runner.Cancel(process.StandardInput.BaseStream); process.WaitForExit(); } tcs.SetResult(token.IsCancellationRequested); }); return tcs.Task; } return null; } } }