mirror of
https://github.com/ElectronNET/Electron.NET.git
synced 2026-07-09 10:17:49 +00:00
598 lines
18 KiB
C#
598 lines
18 KiB
C#
namespace ElectronNET.Common
|
|
{
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
/// <summary>
|
|
/// Class encapsulating out-of-process execution of console applications.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
/// <seealso cref="IDisposable" />
|
|
[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<string> LineReceived;
|
|
|
|
private volatile ManualResetEvent stdOutEvent;
|
|
private volatile ManualResetEvent stdErrEvent;
|
|
private volatile Stopwatch stopwatch;
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="ProcessRunner" /> class.</summary>
|
|
/// <param name="name">A name identifying the process to execute.</param>
|
|
public ProcessRunner(string name)
|
|
{
|
|
this.Name = name;
|
|
}
|
|
|
|
public event EventHandler<EventArgs> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets the name identifying the process.</summary>
|
|
/// <value>The name.</value>
|
|
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<bool> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Sychronously waits for the specified amount and ends the process afterwards.</summary>
|
|
/// <param name="timeoutMs">The timeout ms.</param>
|
|
/// <remarks>This method allows for a clean exit, since it also waits until the StandardOutput and
|
|
/// StandardError pipes are processed to the end.</remarks>
|
|
/// <returns>true, if the process has exited gracefully; false otherwise.</returns>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>Asynchronously waits for the specified amount and ends the process afterwards.</summary>
|
|
/// <param name="timeoutMs">The timeout ms.</param>
|
|
/// <remarks>Tjhis method performs the wait operation on a threadpool thread.
|
|
/// Only recommended for short timeouts and situations where a synchronous call is undesired.</remarks>
|
|
/// <returns>true, if the process has exited gracefully; false otherwise.</returns>
|
|
public Task<bool> WaitAndKillAsync(int timeoutMs)
|
|
{
|
|
var task = Task.Run(() => this.WaitAndKill(timeoutMs));
|
|
return task;
|
|
}
|
|
|
|
/// <summary>Waits asynchronously for the process to exit.</summary>
|
|
/// <param name="timeoutMs">The timeout ms.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>true, if the process has exited, false if the process is still running.</returns>
|
|
/// <remarks>
|
|
/// This methods waits until the process has existed or the
|
|
/// <paramref name="timeoutMs" /> has elapsed.
|
|
/// This method does not end the process itself.
|
|
/// </remarks>
|
|
public Task<bool> 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);
|
|
}
|
|
|
|
/// <summary>Waits asynchronously for the process to exit.</summary>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <remarks>This methods waits until the process has existed or the
|
|
/// <paramref name="cancellationToken"/> has been triggered.
|
|
/// This method does not end the process itself.</remarks>
|
|
/// <returns>true, if the process has exited, false if the process is still running.</returns>
|
|
public async Task<bool> WaitForExitAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var proc = this.process;
|
|
|
|
if (proc == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var tcs = new TaskCompletionSource<bool>();
|
|
|
|
// 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.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |