Esempio n. 1
0
        /// <summary>
        /// Runs a child process, synchronously, with the specified input.
        /// This method should be called by the parent process. It starts the child process, providing it
        /// with specific command line arguments that will allow the child process to support cancellation
        /// and error handling.
        /// The child process should call <see cref="RunAndListenToParentAsync{TInput,TOutput}"/>, provide
        /// it with the command line arguments and the main method that receives the input object and returns
        /// an object of type <see cref="TOutput"/>.
        /// </summary>
        /// <example>
        /// Parent process:
        /// <code>
        /// private async cTask&lt;OutputData&gt; RunInChildProcess(string childProcessName, InputData input, ITracer tracer, CancellationToken cancellationToken)
        /// {
        ///     IChildProcessManager childProcessManager = new ChildProcessManager();
        ///     OutputData output = await childProcessManager.RunChildProcessAsync&lt;OutputData&gt;(childProcessName, input, tracer, cancellationToken);
        ///     return output;
        /// }
        /// </code>
        /// Child process:
        /// <code>
        /// public static void Main(string[] args)
        /// {
        ///     ITracer tracer;
        ///     // Initialize tracer...
        ///
        ///     IChildProcessManager childProcessManager = new ChildProcessManager();
        ///     childProcessManager.RunAndListenToParentAsync&lt;InputData, OutputData&gt;(args, MainFunction, tracer).Wait();
        /// }
        ///
        /// private static OutputData MainFunction(InputData input, CancellationToken cancellationToken)
        /// {
        ///     // ...
        ///
        ///     return output;
        /// }
        /// </code>
        /// </example>
        /// <typeparam name="TOutput">The child process output type</typeparam>
        /// <param name="exePath">The child process' executable file path</param>
        /// <param name="input">The child process input</param>
        /// <param name="cancellationToken">The cancellation token</param>
        /// <exception cref="InvalidOperationException">The child process could not be started</exception>
        /// <exception cref="ChildProcessException">The child process failed - see InnerException ro details</exception>
        /// <returns>A <see cref="Task{TResult}"/>, returning the child process output</returns>
        public async Task <TOutput> RunChildProcessAsync <TOutput>(string exePath, object input, CancellationToken cancellationToken)
        {
            this.CurrentStatus = RunChildProcessStatus.Initializing;
            this.tracer.TraceInformation($"Starting to run child process {exePath}");

            try
            {
                // The pipe from the parent to the child is used to pass a cancellation instruction
                // The pipe from the child to the parent is used to pass the child process output
                using (AnonymousPipeServerStream pipeParentToChild = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable))
                {
                    using (AnonymousPipeServerStream pipeChildToParent = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable))
                    {
                        using (Process childProcess = new Process())
                        {
                            // Write the output to the pipe
                            await this.WriteToStream(input, pipeParentToChild, cancellationToken);

                            // Get pipe handles
                            string pipeParentToChildHandle = pipeParentToChild.GetClientHandleAsString();
                            string pipeChildToParentHandle = pipeChildToParent.GetClientHandleAsString();

                            // Setup the child process
                            childProcess.StartInfo = new ProcessStartInfo(exePath)
                            {
                                Arguments             = pipeParentToChildHandle + " " + pipeChildToParentHandle + " " + this.tracer.SessionId,
                                CreateNoWindow        = true,
                                UseShellExecute       = false,
                                RedirectStandardError = true
                            };

                            // Start the child process
                            Stopwatch sw = Stopwatch.StartNew();
                            childProcess.Start();
                            this.tracer.TraceInformation($"Started to run child process '{Path.GetFileName(exePath)}', process ID {childProcess.Id}");
                            this.CurrentStatus = RunChildProcessStatus.WaitingForProcessToExit;
                            this.ChildProcessIds.Add(childProcess.Id);

                            // Dispose the local copy of the client handle
                            pipeParentToChild.DisposeLocalCopyOfClientHandle();
                            pipeChildToParent.DisposeLocalCopyOfClientHandle();

                            // Wait for the child process to finish
                            bool         wasChildTerminatedByParent = false;
                            MemoryStream outputStream = new MemoryStream();
                            using (cancellationToken.Register(() => { this.CancelChildProcess(childProcess, pipeParentToChild, ref wasChildTerminatedByParent); }))
                            {
                                // Read the child's output
                                // We do not use the cancellation token here - we want to wait for the child to gracefully cancel
                                await pipeChildToParent.CopyToAsync(outputStream, 2048, default(CancellationToken));

                                // Ensure the child existed
                                childProcess.WaitForExit();
                            }

                            this.CurrentStatus = RunChildProcessStatus.Finalizing;
                            sw.Stop();
                            this.tracer.TraceInformation($"Process {exePath} completed, duration {sw.ElapsedMilliseconds / 1000}s, exit code {childProcess.ExitCode}");

                            // If the child process was terminated by the parent, throw appropriate exception
                            if (wasChildTerminatedByParent)
                            {
                                throw new ChildProcessTerminatedByParentException();
                            }

                            // Read the process result from the stream
                            // This read ignores the cancellation token - if there was a cancellation, the process output will contain the appropriate exception
                            outputStream.Seek(0, SeekOrigin.Begin);
                            ChildProcessResult <TOutput> processResult = await this.ReadFromStream <ChildProcessResult <TOutput> >(outputStream, default(CancellationToken));

                            // Return process result
                            if (processResult == null)
                            {
                                throw new ChildProcessException("The child process returned empty results");
                            }
                            else if (processResult.Exception != null)
                            {
                                throw new ChildProcessException("The child process threw an exception: " + processResult.Exception.Message, processResult.Exception);
                            }

                            this.CurrentStatus = RunChildProcessStatus.Completed;
                            return(processResult.Output);
                        }
                    }
                }
            }
            catch (Exception)
            {
                this.CurrentStatus = cancellationToken.IsCancellationRequested ? RunChildProcessStatus.Canceled : RunChildProcessStatus.Failed;
                throw;
            }
        }
        /// <summary>
        /// Runs a child process, synchronously, with the specified input.
        /// This method should be called by the parent process. It starts the child process, providing it
        /// with specific command line arguments that will allow the child process to support cancellation
        /// and error handling.
        /// The child process should call <see cref="RunAndListenToParentAsync{TInput,TOutput}"/>, provide
        /// it with the command line arguments and the main method that receives the input object and returns
        /// an object of type <typeparamref name="TOutput"/>.
        /// </summary>
        /// <example>
        /// Parent process:
        /// <code>
        /// private async cTask&lt;OutputData&gt; RunInChildProcess(string childProcessName, InputData input, ITracer tracer, CancellationToken cancellationToken)
        /// {
        ///     IChildProcessManager childProcessManager = new ChildProcessManager();
        ///     OutputData output = await childProcessManager.RunChildProcessAsync&lt;OutputData&gt;(childProcessName, input, tracer, cancellationToken);
        ///     return output;
        /// }
        /// </code>
        /// Child process:
        /// <code>
        /// public static void Main(string[] args)
        /// {
        ///     ITracer tracer;
        ///     // Initialize tracer...
        ///
        ///     IChildProcessManager childProcessManager = new ChildProcessManager();
        ///     childProcessManager.RunAndListenToParentAsync&lt;InputData, OutputData&gt;(args, MainFunction, tracer).Wait();
        /// }
        ///
        /// private static OutputData MainFunction(InputData input, CancellationToken cancellationToken)
        /// {
        ///     // ...
        ///
        ///     return output;
        /// }
        /// </code>
        /// </example>
        /// <typeparam name="TOutput">The child process output type</typeparam>
        /// <param name="exePath">The child process' executable file path</param>
        /// <param name="input">The child process input</param>
        /// <param name="cancellationToken">The cancellation token</param>
        /// <exception cref="InvalidOperationException">The child process could not be started</exception>
        /// <exception cref="ChildProcessException">The child process failed - see InnerException for details</exception>
        /// <returns>A <see cref="Task{TResult}"/>, returning the child process output</returns>
        public async Task <TOutput> RunChildProcessAsync <TOutput>(string exePath, object input, CancellationToken cancellationToken)
        {
            this.CurrentStatus = RunChildProcessStatus.Initializing;
            this.tracer.TraceInformation($"Starting to run child process {exePath}");

            // Create a temporary folder for the child process
            string tempFolder = FileSystemExtensions.CreateTempFolder(TempSubFolderName);

            this.tracer.TraceInformation($"Created temporary folder for child process: {tempFolder}");

            try
            {
                // The pipe from the parent to the child is used to pass a cancellation instruction
                // The pipe from the child to the parent is used to pass the child process output
                using (AnonymousPipeServerStream pipeParentToChild = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable))
                {
                    using (AnonymousPipeServerStream pipeChildToParent = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable))
                    {
                        using (Process childProcess = new Process())
                        {
                            // Write the output to the pipe
                            await this.WriteToStream(input, pipeParentToChild, cancellationToken);

                            // Get pipe handles
                            string pipeParentToChildHandle = pipeParentToChild.GetClientHandleAsString();
                            string pipeChildToParentHandle = pipeChildToParent.GetClientHandleAsString();

                            // Prepare command line arguments
                            ChildProcessArguments arguments = new ChildProcessArguments(
                                pipeParentToChildHandle,
                                pipeChildToParentHandle,
                                this.tracer.SessionId,
                                this.tracer.GetCustomProperties(),
                                tempFolder);

                            // Setup the child process
                            childProcess.StartInfo = new ProcessStartInfo(exePath)
                            {
                                Arguments             = ChildProcessArguments.ToCommandLineArguments(arguments),
                                CreateNoWindow        = true,
                                UseShellExecute       = false,
                                RedirectStandardError = true
                            };

                            // Start the child process
                            Stopwatch sw = Stopwatch.StartNew();
                            childProcess.Start();
                            this.tracer.TraceInformation($"Started to run child process '{Path.GetFileName(exePath)}', process ID {childProcess.Id}");
                            this.CurrentStatus = RunChildProcessStatus.WaitingForProcessToExit;
                            this.ChildProcessIds.Add(childProcess.Id);

                            // Dispose the local copy of the client handle
                            pipeParentToChild.DisposeLocalCopyOfClientHandle();
                            pipeChildToParent.DisposeLocalCopyOfClientHandle();

                            // Wait for the child process to finish
                            bool         wasChildTerminatedByParent = false;
                            MemoryStream outputStream = new MemoryStream();
                            using (cancellationToken.Register(() => { this.CancelChildProcess(childProcess, pipeParentToChild, ref wasChildTerminatedByParent); }))
                            {
                                // Read the child's output
                                // We do not use the cancellation token here - we want to wait for the child to gracefully cancel
                                await pipeChildToParent.CopyToAsync(outputStream, 2048, default(CancellationToken));

                                // Ensure the child existed
                                childProcess.WaitForExit();
                            }

                            this.CurrentStatus = RunChildProcessStatus.Finalizing;
                            sw.Stop();
                            this.tracer.TraceInformation($"Process {exePath} completed, duration {sw.ElapsedMilliseconds / 1000}s, exit code {childProcess.ExitCode}");

                            // If the child process was terminated by the parent, throw appropriate exception
                            if (wasChildTerminatedByParent)
                            {
                                throw new ChildProcessTerminatedByParentException();
                            }

                            // If the child process has exited with an error code, throw appropriate exception
                            if (childProcess.ExitCode != 0)
                            {
                                // This read ignores the cancellation token - if there was a cancellation, the process output will contain the appropriate exception
                                outputStream.Seek(0, SeekOrigin.Begin);
                                string processOutput = await this.ReadFromStream <string>(outputStream, default(CancellationToken));

                                throw new ChildProcessFailedException(childProcess.ExitCode, processOutput);
                            }

                            // Read the process result from the stream
                            outputStream.Seek(0, SeekOrigin.Begin);
                            TOutput processResult = await this.ReadFromStream <TOutput>(outputStream, cancellationToken);

                            // Return process result
                            this.CurrentStatus = RunChildProcessStatus.Completed;
                            return(processResult);
                        }
                    }
                }
            }
            catch (Exception)
            {
                this.CurrentStatus = cancellationToken.IsCancellationRequested ? RunChildProcessStatus.Canceled : RunChildProcessStatus.Failed;
                throw;
            }
            finally
            {
                FileSystemExtensions.TryDeleteFolder(tempFolder, this.tracer);
                FileSystemExtensions.CleanupTempFolders(TempSubFolderName, tracer: this.tracer);
            }
        }