namespace ElectronNET.Common { using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text; using System.Threading; using System.Threading.Tasks; /// /// Class encapsulating out-of-process execution of console applications. /// /// /// Why this class? /// Probably everybody who has tried to use System.Diagnotics.Process cross-platform and with reading /// stderr and stdout will know that it is a pretty quirky API. /// The code below may look weird, even non-sensical, but it works 100% reliable with all .net frameworks /// and .net versions and on every platform where .net runs. This is just the innermost core, that's why /// there are many dead ends, but it has all the crucial parts. /// /// [SuppressMessage("ReSharper", "SuspiciousLockOverSynchronizationPrimitive")] public class ProcessRunner : IDisposable { private volatile Process process; private readonly StringBuilder stdOut = new StringBuilder(4 * 1024); private readonly StringBuilder stdErr = new StringBuilder(4 * 1024); public event EventHandler LineReceived; private volatile ManualResetEvent stdOutEvent; private volatile ManualResetEvent stdErrEvent; private volatile Stopwatch stopwatch; /// Initializes a new instance of the class. /// A name identifying the process to execute. public ProcessRunner(string name) { this.Name = name; } public event EventHandler ProcessExited; public bool IsDisposed { get; private set; } private Process Process { get { return this.process; } } public bool IsRunning { get { var proc = this.process; if (proc != null) { try { return !proc.HasExited; } catch { return false; } } return false; } } /// Gets the name identifying the process. /// The name. public string Name { get; } public string CommandLine { get; private set; } public string ExecutableFileName { get; private set; } public string WorkingFolder { get; private set; } public bool RecordStandardOutput { get; set; } public bool RecordStandardError { get; set; } public string StandardOutput { get { lock (this.stdOut) { return this.stdOut.ToString(); } } } public string StandardError { get { lock (this.stdErr) { return this.stdErr.ToString(); } } } public int? LastExitCode { get; private set; } public bool Run(string exeFileName, string commandLineArgs, string workingDirectory) { this.CommandLine = commandLineArgs; this.WorkingFolder = workingDirectory; this.ExecutableFileName = exeFileName; var startInfo = new RunnerParams(exeFileName) { Arguments = commandLineArgs, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true, ErrorDialog = false, CreateNoWindow = true, WorkingDirectory = workingDirectory }; return this.Run(startInfo); } protected bool Run(RunnerParams runnerParams) { if (this.IsDisposed) { throw new ObjectDisposedException(this.GetType().ToString()); } this.Close(); this.LastExitCode = null; lock (this.stdOut) { this.stdOut.Clear(); } lock (this.stdErr) { this.stdErr.Clear(); } this.stdOutEvent = new ManualResetEvent(false); this.stdErrEvent = new ManualResetEvent(false); if (!this.OnBeforeStartProcessCore(runnerParams)) { return false; } var startInfo = new ProcessStartInfo(runnerParams.FileName) { Arguments = runnerParams.Arguments, UseShellExecute = runnerParams.UseShellExecute, RedirectStandardOutput = runnerParams.RedirectStandardOutput, RedirectStandardError = runnerParams.RedirectStandardError, RedirectStandardInput = runnerParams.RedirectStandardInput, ErrorDialog = runnerParams.ErrorDialog, CreateNoWindow = runnerParams.CreateNoWindow, WorkingDirectory = runnerParams.WorkingDirectory }; foreach (var variableSetting in runnerParams.EnvironmentVariables) { startInfo.EnvironmentVariables[variableSetting.Key] = variableSetting.Value; } var proc = new Process { StartInfo = startInfo }; proc.EnableRaisingEvents = true; this.RegisterProcessEvents(proc); this.process = proc; try { this.process.Start(); this.stopwatch = Stopwatch.StartNew(); this.process.BeginOutputReadLine(); this.process.BeginErrorReadLine(); this.process.Refresh(); this.OnProcessStartedCore(); } catch (Exception ex) { this.OnProcessErrorCore(ex); this.Close(); throw; } return true; } public async Task WriteAsync(string data) { var proc = this.Process; if (proc != null && !proc.HasExited) { try { await proc.StandardInput.WriteAsync(data).ConfigureAwait(false); return true; } catch (Exception ex) { Console.WriteLine("{0}.{1}: {2}", ex, nameof(ProcessRunner), nameof(this.WriteAsync)); } } return false; } public bool WaitForExit() { var proc = this.process; if (proc == null) { return true; } try { // Wait for process and all I/O to finish. proc.WaitForExit(); return true; } catch (Exception ex) { this.OnProcessErrorCore(ex); return false; } } /// Sychronously waits for the specified amount and ends the process afterwards. /// The timeout ms. /// This method allows for a clean exit, since it also waits until the StandardOutput and /// StandardError pipes are processed to the end. /// true, if the process has exited gracefully; false otherwise. public bool WaitAndKill(int timeoutMs) { var proc = this.process; if (proc == null) { return true; } try { if (timeoutMs <= 0) { throw new ArgumentException("Argument must be greater then 0", nameof(timeoutMs)); } // Timed waiting. We need to wait for I/O ourselves. if (!proc.WaitForExit(timeoutMs)) { this.Cancel(); } // Wait for the I/O to finish. var waitMs = (int)(timeoutMs - this.stopwatch.ElapsedMilliseconds); waitMs = Math.Max(waitMs, 10); this.stdOutEvent?.WaitOne(waitMs); waitMs = (int)(timeoutMs - this.stopwatch.ElapsedMilliseconds); waitMs = Math.Max(waitMs, 10); return this.stdErrEvent?.WaitOne(waitMs) ?? false; } finally { // Cleanup. this.Cancel(); } } /// Asynchronously waits for the specified amount and ends the process afterwards. /// The timeout ms. /// Tjhis method performs the wait operation on a threadpool thread. /// Only recommended for short timeouts and situations where a synchronous call is undesired. /// true, if the process has exited gracefully; false otherwise. public Task WaitAndKillAsync(int timeoutMs) { var task = Task.Run(() => this.WaitAndKill(timeoutMs)); return task; } /// Waits asynchronously for the process to exit. /// The timeout ms. /// The cancellation token. /// true, if the process has exited, false if the process is still running. /// /// This methods waits until the process has existed or the /// has elapsed. /// This method does not end the process itself. /// public Task WaitForExitAsync(int timeoutMs, CancellationToken cancellationToken = default) { timeoutMs = Math.Max(0, timeoutMs); var timeoutSource = new CancellationTokenSource(timeoutMs); var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, cancellationToken); return this.WaitForExitAsync(linkedSource.Token); } /// Waits asynchronously for the process to exit. /// The cancellation token. /// This methods waits until the process has existed or the /// has been triggered. /// This method does not end the process itself. /// true, if the process has exited, false if the process is still running. public async Task WaitForExitAsync(CancellationToken cancellationToken = default) { var proc = this.process; if (proc == null) { return true; } var tcs = new TaskCompletionSource(); // Use local function instead of a lambda to allow proper deregistration of the event void ProcessExited(object sender, EventArgs e) { tcs.TrySetResult(true); } try { proc.EnableRaisingEvents = true; proc.Exited += ProcessExited; if (proc.HasExited) { return true; } using (cancellationToken.Register(() => tcs.TrySetResult(false))) { return await tcs.Task.ConfigureAwait(false); } } finally { proc.Exited -= ProcessExited; } } public void Cancel() { var proc = this.process; if (proc != null) { try { // Invalidate cached data to requery. proc.Refresh(); // We need to do this in case of a non-UI proc // or one to be forced to cancel. if (!proc.HasExited) { // Cancel all pending IO ops. proc.CancelErrorRead(); proc.CancelOutputRead(); } if (!proc.HasExited) { proc.Kill(); } } catch { // Kill will throw when/if the process has already exited. } } var outEvent = this.stdOutEvent; this.stdOutEvent = null; if (outEvent != null) { lock (outEvent) { outEvent.Close(); outEvent.Dispose(); } } var errEvent = this.stdErrEvent; this.stdErrEvent = null; if (errEvent != null) { lock (errEvent) { errEvent.Close(); errEvent.Dispose(); } } } private void Close() { this.Cancel(); var proc = this.process; this.process = null; if (proc != null) { try { this.UnRegisterProcessEvents(proc); // Dispose in all cases. proc.Close(); proc.Dispose(); } catch (Exception ex) { this.OnProcessErrorCore(ex); } } } protected virtual void OnDispose() { } void IDisposable.Dispose() { this.IsDisposed = true; this.Close(); this.OnDispose(); } public override string ToString() { return string.Format("{0}: {1} {2}", this.GetType().Name, this.Name, this.process); } protected virtual bool OnBeforeStartProcessCore(RunnerParams processRunnerInfo) { return true; } protected virtual void OnProcessStartedCore() { } protected virtual void OnProcessErrorCore(Exception processException) { } protected virtual void OnProcessExitCore(int exitCode) { } private void RegisterProcessEvents(Process proc) { proc.ErrorDataReceived += this.Process_ErrorDataReceived; proc.OutputDataReceived += this.Process_OutputDataReceived; proc.Exited += this.Process_Exited; } private void UnRegisterProcessEvents(Process proc) { proc.ErrorDataReceived -= this.Process_ErrorDataReceived; proc.OutputDataReceived -= this.Process_OutputDataReceived; proc.Exited -= this.Process_Exited; } private void Process_Exited(object sender, EventArgs e) { this.WaitForExitAfterExited(); this.SetExitCode(); this.OnProcessExitCore(this.LastExitCode ?? -9998); this.ProcessExited?.Invoke(this, new EventArgs()); } private void WaitForExitAfterExited() { try { // This shouldn't throw here, but the mono process implementation doesn't always behave as it should. this.process.WaitForExit(); } catch (Exception ex) { Console.WriteLine("Error when calling WaitForExit after exited event has fired: {0}.{1}: {2}", ex, nameof(ProcessRunner), nameof(this.WaitForExitAfterExited)); } } private void SetExitCode() { int exitCode = -9999; try { if (this.Process != null) { exitCode = this.Process.ExitCode; } } catch { // Ignore error. } this.LastExitCode = exitCode; } private void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e) { if (this.RecordStandardError) { lock (this.stdErr) { this.stdErr.AppendLine(e.Data); } } if (e.Data != null) { Console.WriteLine("|| " + e.Data); } else { var evt = this.stdErrEvent; if (evt != null) { lock (evt) { try { evt.Set(); } catch { // Ignore error. } } } } } private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e) { if (this.RecordStandardOutput) { lock (this.stdOut) { this.stdOut.AppendLine(e.Data); } } if (e.Data != null) { Console.WriteLine("|| " + e.Data); LineReceived?.Invoke(this, e.Data); } else { var evt = this.stdOutEvent; if (evt != null) { lock (evt) { try { evt.Set(); } catch { // Ignore error. } } } } } } }