/// <summary>
 /// Gets called when the external process writes to the error pipe.
 /// </summary>
 /// <param name="result"></param>
 /// <param name="data"></param>
 protected virtual void OnErrorReceived(ProcessResult result, string data)
 {
     result.Errors.Add(data);
 }
        /// <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>
 /// Gets called when the external process writes to the output pipe.
 /// </summary>
 /// <param name="result"></param>
 /// <param name="data"></param>
 protected virtual void OnOutputReceived(ProcessResult result, string data)
 {
     //TODO: this function might need more context parameters than result and string data...
     result.Output.Add(data);
 }
        /// <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);
            }
        }