/// <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<OutputData> RunInChildProcess(string childProcessName, InputData input, ITracer tracer, CancellationToken cancellationToken) /// { /// IChildProcessManager childProcessManager = new ChildProcessManager(); /// OutputData output = await childProcessManager.RunChildProcessAsync<OutputData>(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<InputData, OutputData>(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<OutputData> RunInChildProcess(string childProcessName, InputData input, ITracer tracer, CancellationToken cancellationToken) /// { /// IChildProcessManager childProcessManager = new ChildProcessManager(); /// OutputData output = await childProcessManager.RunChildProcessAsync<OutputData>(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<InputData, OutputData>(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); } }