SlunkCrypt/gui/Utilities/ProcessRunner.cs

354 lines
12 KiB
C#

/******************************************************************************/
/* SlunkCrypt, by LoRd_MuldeR <MuldeR2@GMX.de> */
/* This work has been released under the CC0 1.0 Universal license! */
/******************************************************************************/
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace com.muldersoft.slunkcrypt.gui.utils
{
class ProcessRunner : IDisposable
{
public delegate void OutputLineHandler(string line, bool stream);
public delegate void ProgressChangedHandler(double progress);
public delegate bool ProgressStringHandler(string line, out double progress);
public event OutputLineHandler OutputAvailable;
public event ProgressChangedHandler ProgressChanged;
public const bool STDOUT = false, STDERR = true;
private readonly Process m_process = new Process();
private readonly TaskCompletionSource<int> m_hasExited = new TaskCompletionSource<int>();
private readonly Dispatcher m_dispatcher;
private readonly ProcessPriorityClass? m_priorityClass;
private volatile bool m_running = false, m_aborted = false, m_disposed = false;
// =============================================================================
// Exception classes
// =============================================================================
public class ProcessStartException : IOException
{
public ProcessStartException(string message, Exception innerException) : base(message, innerException)
{
}
}
public class ProcessInterruptedException : IOException
{
public ProcessInterruptedException(string message) : base(message)
{
}
}
// =============================================================================
// Constructor
// =============================================================================
public ProcessRunner(Dispatcher dispatcher, ProcessPriorityClass? priorityClass = null)
{
if (ReferenceEquals(m_dispatcher = dispatcher, null))
{
throw new ArgumentException("Dispatcher must not be null!");
}
if ((m_priorityClass = priorityClass).HasValue && (!Enum.IsDefined(typeof(ProcessPriorityClass), priorityClass.Value)))
{
throw new ArgumentException("The given ProcessPriorityClass is undefined!");
}
m_process.StartInfo.UseShellExecute = false;
m_process.StartInfo.CreateNoWindow = true;
m_process.StartInfo.RedirectStandardOutput = true;
m_process.StartInfo.RedirectStandardError = true;
m_process.StartInfo.StandardOutputEncoding = Encoding.UTF8;
m_process.StartInfo.StandardErrorEncoding = Encoding.UTF8;
m_process.EnableRaisingEvents = true;
m_process.Exited += ProcessExitedEventHandler;
}
~ProcessRunner()
{
Dispose(); /*just to be sure*/
}
// =============================================================================
// Public methods
// =============================================================================
public async Task<int> ExecAsnyc(string executablePath, string[] arguments = null, IReadOnlyDictionary<string, string> environmentVariables = null)
{
m_dispatcher.VerifyAccess();
if (m_disposed)
{
throw new ObjectDisposedException("ProcessRunner");
}
if (m_running)
{
throw new InvalidOperationException("Process is already running!");
}
m_running = true;
try
{
return await DoExecAsnyc(executablePath, arguments, environmentVariables);
}
finally
{
m_running = false;
}
}
public void AbortProcess()
{
if ((!m_disposed) && m_running)
{
m_aborted = true;
KillProcess();
}
}
public virtual void Dispose()
{
if (!m_disposed)
{
m_disposed = true;
GC.SuppressFinalize(this);
KillProcess();
try
{
m_process.Dispose();
}
catch { }
}
}
// =============================================================================
// Protected methods
// =============================================================================
protected virtual double ParseProgressString(StringBuilder currentLine)
{
return double.NaN;
}
// =============================================================================
// Event handlers
// =============================================================================
private void ProcessExitedEventHandler(object sender, EventArgs e)
{
if (m_process.HasExited)
{
m_hasExited.TrySetResult(m_process.ExitCode);
}
}
// =============================================================================
// Internal methods
// =============================================================================
private async Task<int> DoExecAsnyc(string executablePath, string[] arguments, IReadOnlyDictionary<string, string> environmentVariables)
{
try
{
StartProcess(executablePath, arguments, environmentVariables);
}
catch (Exception err)
{
throw new ProcessStartException("Failed to create the process!", err);
}
return await WaitForExit();
}
private void StartProcess(string executablePath, string[] arguments, IReadOnlyDictionary<string, string> environmentVariables)
{
m_process.StartInfo.FileName = executablePath;
m_process.StartInfo.Arguments = CreateArgumentList(arguments);
SetupEnvironment(m_process.StartInfo.EnvironmentVariables, environmentVariables);
m_process.StartInfo.WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory;
m_process.Start();
SetProcessPriority(m_priorityClass);
}
private async Task<int> WaitForExit()
{
Task readStdOutTask = Task.Run(() => ReadProcessOutput(m_process.StandardOutput, STDOUT));
Task readStdErrTask = Task.Run(() => ReadProcessOutput(m_process.StandardError, STDERR));
Task<int> hasExited = m_hasExited.Task;
await Task.WhenAll(readStdOutTask, readStdErrTask, hasExited);
if (m_aborted || m_disposed)
{
NotifyOutputAvailable("\u2192 Process has been aborted !!!", true);
throw new ProcessInterruptedException("Process has been aborted!");
}
NotifyOutputAvailable(string.Format("\u2192 Process terminated normally [Exit status: {0:D}]", hasExited.Result), false);
return hasExited.Result;
}
private void ReadProcessOutput(StreamReader reader, bool stream)
{
using(reader)
{
char[] buffer = new char[1024];
StringBuilder currentLine = new StringBuilder();
while (!reader.EndOfStream)
{
ReadNextChunk(reader, buffer, currentLine, stream);
}
NotifyOutputAvailable(currentLine, stream);
}
}
private void ReadNextChunk(StreamReader reader, char[] buffer, StringBuilder currentLine, bool stderr)
{
int count = reader.Read(buffer, 0, buffer.Length);
if (count > 0)
{
for (int i = 0; i < count; ++i)
{
char c = buffer[i];
if ((c != '\0') && (c != '\n') && (c != '\r') && (c != '\b'))
{
currentLine.Append(c);
}
else
{
CheckForProgressUpdate(currentLine);
NotifyOutputAvailable(currentLine, stderr);
currentLine.Clear();
}
}
CheckForProgressUpdate(currentLine);
}
}
private void CheckForProgressUpdate(StringBuilder currentLine)
{
if (currentLine.Length > 0)
{
double progress = ParseProgressString(currentLine);
if (!(double.IsNaN(progress) || double.IsInfinity(progress)))
{
NotifyProgressChanged(progress);
}
}
}
private void NotifyOutputAvailable(StringBuilder currentLine, bool stream)
{
if (currentLine.Length > 0)
{
String line = currentLine.ToString().Trim();
if (!string.IsNullOrEmpty(line))
{
NotifyOutputAvailable(line, stream);
}
}
}
private void NotifyOutputAvailable(string line, bool stream)
{
if (!ReferenceEquals(OutputAvailable, null))
{
m_dispatcher.InvokeAsync(() => OutputAvailable(line, stream));
}
}
private void NotifyProgressChanged(double progress)
{
if (!ReferenceEquals(ProgressChanged, null))
{
m_dispatcher.InvokeAsync(() => ProgressChanged(Math.Max(0.0, Math.Min(1.0, progress))));
}
}
private static string CreateArgumentList(string[] arguments)
{
StringBuilder argumentList = new StringBuilder();
if (!ReferenceEquals(arguments, null))
{
for (int i = 0; i < arguments.Length; ++i)
{
if (i > 0)
{
argumentList.Append("\x20");
}
argumentList.Append(EscapeArgument(arguments[i]));
}
}
return argumentList.ToString();
}
private static string EscapeArgument(string str)
{
if (RequiresQuotation(str))
{
StringBuilder sb = new StringBuilder().Append('"');
foreach (char c in str)
{
if (c == '"')
{
sb.Append('\\').Append('"');
}
else
{
sb.Append(c);
}
}
return sb.Append('"').ToString();
}
return str;
}
private static bool RequiresQuotation(string str)
{
foreach (char c in str)
{
if (char.IsWhiteSpace(c) || (c == '"'))
{
return true;
}
}
return false;
}
private static void SetupEnvironment(StringDictionary dictionary, IReadOnlyDictionary<string, string> environmentVariables)
{
if (!ReferenceEquals(environmentVariables, null))
{
foreach (KeyValuePair<string, string> entry in environmentVariables)
{
dictionary.Add(entry.Key, entry.Value);
}
}
}
private void KillProcess()
{
try
{
m_process.Kill();
}
catch { }
}
private void SetProcessPriority(ProcessPriorityClass? priorityClass)
{
try
{
if (priorityClass.HasValue)
{
m_process.PriorityClass = priorityClass.Value;
}
}
catch { }
}
}
}