public static ProcessResult Execute(this IProcessHandler handler, string executable, string arguments, int timeout = 30000)
        {
            var setup = new ProcessSetup()
            {
                Executable = executable, Arguments = arguments
            };

            return(handler.Execute(setup, timeout));
        }
        public static async Task <ProcessResult> ExecuteAsync(this IProcessHandler handler, string executable, string arguments, CancellationToken cancel = default)
        {
            var setup = new ProcessSetup()
            {
                Executable = executable,
                Arguments  = arguments
            };

            return(await handler.ExecuteAsync(setup, cancel));
        }
        /// <summary>
        /// Set process parameters based on setup object.
        /// </summary>
        /// <param name="process"></param>
        /// <param name="setup"></param>
        private static void SetupProcess(Process process, ProcessSetup setup)
        {
            process.StartInfo.FileName = setup.Executable;
            if (!string.IsNullOrWhiteSpace(setup.WorkingDir))
            {
                process.StartInfo.WorkingDirectory = setup.WorkingDir;
            }
            process.StartInfo.Arguments = setup.Arguments;

            process.StartInfo.UseShellExecute = false;
            process.StartInfo.CreateNoWindow  = true;

            if (setup.EnvironmentVariables != null)
            {
                foreach (var kv in setup.EnvironmentVariables)
                {
                    process.StartInfo.Environment.Add(kv.key, kv.value);
                }
            }
        }
        /// <summary>
        /// Handles setting the process startup info from setup.
        /// We dont use a processtartinfo object since that would limit what a user could do.
        /// Unfortunately, that also means the user could easily break/expand its usage.
        /// </summary>
        /// <param name="process"></param>
        /// <param name="setup"></param>
        /// <returns></returns>
        protected virtual bool OnSetupProcess(Process process, ProcessSetup setup)
        {
            process.StartInfo.FileName = setup.Executable;
            if (!string.IsNullOrWhiteSpace(setup.WorkingDir))
            {
                process.StartInfo.WorkingDirectory = setup.WorkingDir;
            }
            process.StartInfo.Arguments = setup.Arguments;

            process.StartInfo.UseShellExecute = false;
            process.StartInfo.CreateNoWindow  = true;

            if (setup.EnvironmentVariables != null)
            {
                //TODO: Should we check existing environmentvariables before just writing ours?
                foreach (var kv in setup.EnvironmentVariables)
                {
                    process.StartInfo.Environment.Add(kv.key, kv.value);
                }
            }

            return(true);
        }
        /*
         * Also, add static helper functions for running external processes without needing to create a ProcessHandler.
         *
         */
        #region Static Helper functions for running external processes.

        /// <summary>
        /// Static function for executing external processes without going thru an instance process.
        /// Note that this code path is not exactly the same as the instance ones.
        /// </summary>
        /// <param name="setup"></param>
        /// <param name="timeout">time to wait before killing process in ms.</param>
        /// <returns></returns>
        public static ProcessResult RunProcess(ProcessSetup setup, int timeout = 30000)
        {
            var result = new ProcessResult();

            using (var process = new Process())
            {
                SetupProcess(process, setup);

                // setup output handling.
                result.Output = new List <string>();
                process.OutputDataReceived += (sender, args) =>
                {
                    if (args.Data != null)
                    {
                        result.Output.Add(args.Data);
                    }
                };
                process.StartInfo.RedirectStandardOutput = true;

                // setup error handling
                result.Errors              = new List <string>();
                process.ErrorDataReceived += (sender, args) =>
                {
                    if (args.Data != null)
                    {
                        result.Errors.Add(args.Data);
                    }
                };
                process.StartInfo.RedirectStandardError = true;

                // try to start process.
                try
                {
                    result.WasStarted = process.Start();
                }
                catch (Exception e)
                {
                    // Usually it occurs when an executable file is not found or is not executable
                    // or requires admin privileges.
                    result.Errors.Add(e.Message);
                    return(result);
                }

                // start listening for readlines.
                process.BeginOutputReadLine();
                process.BeginErrorReadLine();

                var flagTimedOut = process.WaitForExit(timeout);

                if (!flagTimedOut)
                {
                    process.WaitForExit(); // wait more for redirect handlers to cmplete.
                }
                else
                {
                    try
                    {
                        // kill hung process.
                        process.Kill();
                    }
                    catch
                    { } // ignore error.
                }

                result.HasCompleted = true;
                result.ExitCode     = process.ExitCode;
                return(result);
            }
        }
        /// <summary>
        /// Run a process async with a timeout.
        /// </summary>
        /// <param name="setup"></param>
        /// <param name="timeout"></param>
        /// <returns></returns>
        public static async Task <ProcessResult> RunProcessAsync(ProcessSetup setup, int timeout)
        {
            var result = new ProcessResult();

            using (var process = new Process())
            {
                SetupProcess(process, setup);


                // setup output handling.
                result.Output = new List <string>();
                var outputCloseEvent = new TaskCompletionSource <bool>();
                process.OutputDataReceived += (sender, args) =>
                {
                    var d = args.Data;
                    //if (string.IsNullOrWhiteSpace(d))
                    if (d == null)
                    {
                        outputCloseEvent.SetResult(true);
                    }
                    else
                    {
                        result.Output.Add(d);
                    }
                };
                process.StartInfo.RedirectStandardOutput = true;

                // setup error handling
                result.Errors = new List <string>();
                var errorCloseEvent = new TaskCompletionSource <bool>();
                process.ErrorDataReceived += (sender, args) =>
                {
                    var e = args.Data;
                    if (e == null)
                    {
                        errorCloseEvent.SetResult(true);
                    }
                    else
                    {
                        result.Errors.Add(args.Data);
                    }
                };
                process.StartInfo.RedirectStandardError = true;

                // try to start process.
                try
                {
                    result.WasStarted = process.Start();
                }
                catch (Exception e)
                {
                    // Usually it occurs when an executable file is not found or is not executable
                    // or requires admin privileges.
                    result.Errors.Add(e.Message);
                    return(result);
                }

                if (result.WasStarted)
                {
                    process.BeginOutputReadLine();
                    process.BeginErrorReadLine();

                    var taskWaitForExit = WaitForExitAsync(process, timeout);

                    var taskProcess = Task.WhenAll(taskWaitForExit, outputCloseEvent.Task, errorCloseEvent.Task);

                    if (await Task.WhenAny(Task.Delay(timeout), taskProcess) == taskProcess && taskWaitForExit.Result)
                    {
                        result.HasCompleted = true;
                        result.ExitCode     = process.ExitCode;
                    }
                    else
                    {
                        try
                        {
                            process.Kill();
                        }
                        catch { } // ignore error.
                    }
                }

                return(result);
            }
        }
        /// <summary>
        /// Executes an external process async using provided processsetup and optinally a cancellation token.
        /// Note that this is not entirely the same code path as the instance version.
        /// </summary>
        /// <param name="setup"></param>
        /// <param name="cancel"></param>
        /// <returns></returns>
        public static async Task <ProcessResult> RunProcessAsync(ProcessSetup setup, CancellationToken cancel = default)
        {
            var result = new ProcessResult();

            using (var process = new Process())
            {
                SetupProcess(process, setup);

                // setup output handling.
                result.Output = new List <string>();
                var outputCloseEvent = new TaskCompletionSource <bool>();
                process.OutputDataReceived += (sender, args) =>
                {
                    var d = args.Data;
                    //if (string.IsNullOrWhiteSpace(d))
                    if (d == null)
                    {
                        outputCloseEvent.SetResult(true);
                    }
                    else
                    {
                        result.Output.Add(d);
                    }
                };
                process.StartInfo.RedirectStandardOutput = true;

                // setup error handling
                result.Errors = new List <string>();
                var errorCloseEvent = new TaskCompletionSource <bool>();
                process.ErrorDataReceived += (sender, args) =>
                {
                    var e = args.Data;
                    if (e == null)
                    {
                        errorCloseEvent.SetResult(true);
                    }
                    else
                    {
                        result.Errors.Add(args.Data);
                    }
                };
                process.StartInfo.RedirectStandardError = true;

                // setup exit handling
                var exitCloseEvent = new TaskCompletionSource <bool>();
                process.Exited += (sender, args) =>
                {
                    exitCloseEvent.SetResult(true);
                };
                process.EnableRaisingEvents = true;



                // try to start process.
                try
                {
                    result.WasStarted = process.Start();
                }
                catch (Exception e)
                {
                    // Usually it occurs when an executable file is not found or is not executable
                    // or requires admin privileges.
                    result.Errors.Add(e.Message);
                    return(result);
                }

                // start listening for readlines.
                process.BeginOutputReadLine();
                process.BeginErrorReadLine();

                // see instance method for explaining this code.

                // wrap all the event task into one task.
                var taskProcess = Task.WhenAll(exitCloseEvent.Task, outputCloseEvent.Task, errorCloseEvent.Task);

                // create a task for handling waiting for exit or cancellation whichever happens first.
                var taskWaitForExit = WaitForExitAsync(process, cancel);

                // this awaits any of event tasks and exit task.
                // when any is finished it waits for result from exit task anyway.
                if (await Task.WhenAny(taskProcess, taskWaitForExit) == taskProcess && taskWaitForExit.Result)
                {
                    result.HasCompleted = true;
                    result.ExitCode     = process.ExitCode;
                }
                else
                {
                    try
                    {
                        process.Kill();
                    }
                    catch { } // ignore error.
                }

                return(result);
            }
        }
        /// <summary>
        /// Implements executing an external process with overridable functions.
        /// </summary>
        /// <param name="setup"></param>
        /// <param name="timeout"></param>
        /// <returns></returns>
        public ProcessResult Execute(ProcessSetup setup, int timeout = 30000)
        {
            var result = new ProcessResult()
            {
                Output = new List <string>(),
                Errors = new List <string>()
            };

            using (var process = new Process())
            {
                if (!this.OnSetupProcess(process, setup))
                {
                    // handle erros.
                    result.Errors.Add("OnSetupProcess returned false");
                    return(result);
                }
                // we assume user has handled setup correctly.

                process.OutputDataReceived += (sender, args) =>
                {
                    //TODO: should we just give user all output or filter?
                    if (args.Data != null)
                    {
                        OnOutputReceived(result, args.Data);
                    }
                };
                process.StartInfo.RedirectStandardOutput = true;

                process.ErrorDataReceived += (sender, args) =>
                {
                    if (args.Data != null)
                    {
                        OnErrorReceived(result, args.Data);
                    }
                };
                process.StartInfo.RedirectStandardError = true;

                // try to start process.
                try
                {
                    result.WasStarted = process.Start();
                }
                catch (Exception e)
                {
                    // Usually it occurs when an executable file is not found or is not executable
                    // or requires admin privileges.
                    result.Errors.Add(e.Message);
                    return(result);
                }

                // start listening on readlines.
                process.BeginOutputReadLine();
                process.BeginErrorReadLine();

                var flagExitedBeforeTimeout = process.WaitForExit(timeout);

                if (flagExitedBeforeTimeout)
                {
                    process.WaitForExit(); // wait more to ensure events are processed.

                    result.HasCompleted = true;
                    result.ExitCode     = process.ExitCode;
                }
                else
                {
                    // kill process.
                    try
                    {
                        process.Kill();
                    }
                    catch { } // ignore error.
                }
                //TODO: should we indicate to user that process was killed?

                return(result);
            }
        }
        /// <summary>
        /// Implements executing an external process async with overridable functions.
        /// </summary>
        /// <param name="setup"></param>
        /// <param name="cancel"></param>
        /// <returns></returns>
        public async Task <ProcessResult> ExecuteAsync(ProcessSetup setup, CancellationToken cancel = default)
        {
            var result = new ProcessResult()
            {
                Output = new List <string>(),
                Errors = new List <string>()
            };

            using (var process = new Process())
            {
                if (!OnSetupProcess(process, setup))
                {
                    // handle erros.
                    result.Errors.Add("OnSetupProcess returned false");
                    return(result);
                }
                // we assume user has handled setup correctly.

                // setup output event handling.
                var outputCloseEvent = new TaskCompletionSource <bool>();
                process.OutputDataReceived += (sender, args) =>
                {
                    if (args.Data == null)
                    {
                        outputCloseEvent.SetResult(true);
                    }
                    else
                    {
                        OnOutputReceived(result, args.Data);
                    }
                };
                process.StartInfo.RedirectStandardOutput = true;

                // setup error event listening.
                var errorCloseEvent = new TaskCompletionSource <bool>();
                process.ErrorDataReceived += (sender, args) =>
                {
                    if (args.Data == null)
                    {
                        errorCloseEvent.SetResult(true);
                    }
                    else
                    {
                        OnErrorReceived(result, args.Data);
                    }
                };
                process.StartInfo.RedirectStandardError = true;

                // setup exit event listening.
                var exitCloseEvent = new TaskCompletionSource <bool>();
                process.Exited += (sender, args) =>
                {
                    exitCloseEvent.SetResult(true);
                };
                process.EnableRaisingEvents = true;

                // try to start process.
                try
                {
                    result.WasStarted = process.Start();
                }
                catch (Exception e)
                {
                    // Usually it occurs when an executable file is not found or is not executable
                    // or requires admin privileges.
                    result.Errors.Add(e.Message);
                    return(result);
                }

                // start listening on readlines.
                process.BeginOutputReadLine();
                process.BeginErrorReadLine();

                //TODO: find out how to await a cancellation token.

                /*
                 * There are now 2 types of tasks.
                 * * Those that should all be finished.
                 * * Those that can be either.
                 *
                 * So wrap the event tasks into one taskProcess which awaits all.
                 * Then create a task which handles waiting and cancellation.
                 *
                 * At last, wait for which finished first.
                 */

                // wrap all the event task into one task.
                var taskProcess = Task.WhenAll(exitCloseEvent.Task, outputCloseEvent.Task, errorCloseEvent.Task);

                // create a task for handling waiting for exit or cancellation whichever happens first.
                var taskWaitForExit = OnWaitForExitAsync(process, cancel);

                // this awaits any of event tasks and exit task.
                // when any is finished it waits for result from exit task anyway.
                if (await Task.WhenAny(taskProcess, taskWaitForExit) == taskProcess && taskWaitForExit.Result)
                {
                    result.ExitCode     = process.ExitCode;
                    result.HasCompleted = true;
                }
                else
                {
                    // process canceled. kill it...
                    try
                    {
                        process.Kill();
                    }
                    catch { } // ignore error.
                }

                return(result);
            }
        }