/******************************************************************************/ /* SlunkCrypt, by LoRd_MuldeR */ /* This work has been released under the CC0 1.0 Universal license! */ /******************************************************************************/ using System; using System.Collections.Concurrent; 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 Dispatcher m_dispatcher; private readonly ProcessPriorityClass? m_priorityClass; private readonly Task m_hasExited; private volatile bool m_running = false, m_finished = 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) { } } // ============================================================================= // Event handler class // ============================================================================= private class ProcessExitHandler { private readonly TaskCompletionSource completionSource = new TaskCompletionSource(); public ProcessExitHandler(Process process) { if (ReferenceEquals(process, null)) { throw new ArgumentNullException("Process must not be null!"); } process.Exited += OnProcessExit; } public Task Task { get { return completionSource.Task; } } protected void OnProcessExit(object sender, EventArgs e) { if (sender is Process) { completionSource.TrySetResult(((Process)sender).ExitCode); } } } // ============================================================================= // 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_hasExited = InitializeProcess(m_process, true); } ~ProcessRunner() { Dispose(); /*just to be sure*/ } // ============================================================================= // Public methods // ============================================================================= public async Task ExecAsnyc(string executablePath, string[] arguments = null, string workingDirectory = null, IReadOnlyDictionary environmentVariables = null) { m_dispatcher.VerifyAccess(); if (m_disposed) { throw new ObjectDisposedException("ProcessRunner"); } if (m_running || m_finished) { throw new InvalidOperationException("Process is still running or has already finished!"); } m_running = true; try { return await DoExecAsnyc(executablePath, arguments, workingDirectory, environmentVariables); } finally { m_finished = true; m_running = false; } } public static async Task> ExecAsnyc(string executableFile, string[] arguments = null, string workingDirectory = null, IReadOnlyDictionary environmentVariables = null, ProcessPriorityClass? priorityClass = null, TimeSpan? timeout = null) { using (Process process = new Process()) { Task hasExitedTask = InitializeProcess(process); return await DoExecAsnyc(process, hasExitedTask, executableFile, arguments, workingDirectory, environmentVariables, priorityClass, timeout); } } 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(ref string currentLine, bool stream) { return double.NaN; } // ============================================================================= // Internal methods // ============================================================================= private static Task InitializeProcess(Process process, bool redirStdErr = false) { process.StartInfo.UseShellExecute = false; process.StartInfo.CreateNoWindow = true; process.StartInfo.RedirectStandardOutput = true; process.StartInfo.StandardOutputEncoding = Encoding.UTF8; if (redirStdErr) { process.StartInfo.RedirectStandardError = true; process.StartInfo.StandardErrorEncoding = Encoding.UTF8; } process.EnableRaisingEvents = true; return new ProcessExitHandler(process).Task; } private async Task DoExecAsnyc(string executablePath, string[] arguments, string workingDirectory, IReadOnlyDictionary environmentVariables) { try { StartProcess(m_process, executablePath, arguments, workingDirectory, environmentVariables, m_priorityClass); } catch (Exception err) { throw new ProcessStartException("Failed to create the process!", err); } return await WaitForExit(); } private static async Task> DoExecAsnyc(Process process, Task hasExited, string executablePath, string[] arguments, string workingDirectory, IReadOnlyDictionary environmentVariables, ProcessPriorityClass? priorityClass, TimeSpan? timeout) { try { StartProcess(process, executablePath, arguments, workingDirectory, environmentVariables, priorityClass); } catch (Exception err) { throw new ProcessStartException("Failed to create the process!", err); } string[] outputLines = await WaitForExit(process, hasExited, timeout.GetValueOrDefault(TimeSpan.MaxValue)); return Tuple.Create(hasExited.Result, outputLines); } private static void StartProcess(Process process, string executablePath, string[] arguments, string workingDirectory, IReadOnlyDictionary environmentVariables, ProcessPriorityClass? priorityClass) { process.StartInfo.FileName = executablePath; process.StartInfo.Arguments = CreateArgumentList(arguments); process.StartInfo.WorkingDirectory = string.IsNullOrEmpty(workingDirectory) ? GetWorkingDirectory(executablePath) : workingDirectory; SetupEnvironment(process.StartInfo.EnvironmentVariables, executablePath, environmentVariables); process.Start(); SetProcessPriority(process, priorityClass); } private async Task WaitForExit() { Task readStdOutTask = Task.Run(() => ReadProcessOutput(m_process.StandardOutput, (line) => HandleOutput(line, STDOUT))); Task readStdErrTask = Task.Run(() => ReadProcessOutput(m_process.StandardError, (line) => HandleOutput(line, STDERR))); await Task.WhenAll(readStdOutTask, readStdErrTask, m_hasExited); if (m_aborted || m_disposed) { NotifyOutputAvailable("\u2192 Process has been aborted !!!", true); throw new ProcessInterruptedException("Process has been aborted!"); } int processExitCode = m_hasExited.Result; NotifyOutputAvailable(string.Format("\u2192 Process terminated normally [Exit status: {0:D}]", processExitCode), false); return processExitCode; } private static async Task WaitForExit(Process process, Task hasExited, TimeSpan timeout) { ConcurrentStack outputLines = new ConcurrentStack(); Task readerTask = Task.Run(() => ReadProcessOutput(process.StandardOutput, (line) => outputLines.Push(line))); if (await Task.WhenAny(hasExited, Task.Delay(timeout)) != hasExited) { KillProcess(process); } await Task.WhenAll(readerTask, hasExited); return outputLines.ToArray(); } private static void ReadProcessOutput(StreamReader reader, Action outputHandler) { using (reader) { char[] buffer = new char[1024]; StringBuilder lineBuffer = new StringBuilder(); while (!reader.EndOfStream) { ReadNextChunk(reader, buffer, lineBuffer, outputHandler); } ProcessLine(lineBuffer, outputHandler); } } private static void ReadNextChunk(StreamReader reader, char[] buffer, StringBuilder lineBuffer, Action outputHandler) { 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')) { lineBuffer.Append(c); } else { ProcessLine(lineBuffer, outputHandler); lineBuffer.Clear(); } } } } private static void ProcessLine(StringBuilder lineBuffer, Action outputHandler) { if (lineBuffer.Length > 0) { String currentLine; if (!string.IsNullOrWhiteSpace(currentLine = lineBuffer.ToString())) { outputHandler.Invoke(currentLine.Trim()); } } } private void HandleOutput(string currentLine, bool stream) { CheckForProgressUpdate(ref currentLine, stream); if (!String.IsNullOrEmpty(currentLine)) { NotifyOutputAvailable(currentLine, stream); } } private void CheckForProgressUpdate(ref string currentLine, bool stream) { double progress = ParseProgressString(ref currentLine, stream); if (!double.IsNaN(progress)) { NotifyProgressChanged(progress); } } 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 string GetWorkingDirectory(string executablePath) { try { string directory = Path.GetDirectoryName(executablePath); if (!string.IsNullOrWhiteSpace(directory)) { return directory; } } catch { } return AppDomain.CurrentDomain.BaseDirectory; } private static void SetupEnvironment(StringDictionary dictionary, string executablePath, IReadOnlyDictionary environmentVariables) { dictionary["PATH"] = PathUtils.CreatePathSpec(Path.GetDirectoryName(executablePath)); if (!ReferenceEquals(environmentVariables, null)) { foreach (KeyValuePair entry in environmentVariables) { dictionary[entry.Key] = entry.Value; } } } private void KillProcess() { try { m_process.Kill(); } catch { } } private static void KillProcess(Process process) { try { process.Kill(); } catch { } } private static void SetProcessPriority(Process process, ProcessPriorityClass? priorityClass) { try { if (priorityClass.HasValue) { process.PriorityClass = priorityClass.Value; } } catch { } } } }