/// <summary> /// Runs the child process task. This method reads and validates the command line /// arguments, starts listening to the parent process (for cancellation/termination), /// runs the specified function, and returns the result to the parent process. /// Should be called by the child process when it starts. /// </summary> /// <typeparam name="TInput">The child process input type</typeparam> /// <typeparam name="TOutput">The child process output type</typeparam> /// <param name="args">The command line arguments</param> /// <param name="functionToRun">The function to run</param> /// <param name="exceptionToExitCodeConverter">A function used to convert an exception thrown by the process to the exit code to return to the parent.</param> /// <param name="waitAfterFlush">Whether to wait after flushing the telemetry, to allow all traces to be sent.</param> /// <exception cref="ArgumentException">The wrong number of arguments was provided</exception> /// <returns>A <see cref="Task"/>, running the specified function and listening to the parent, returning the exit code to be returned from the process</returns> public async Task <int> RunAndListenToParentAsync <TInput, TOutput>( string[] args, Func <TInput, CancellationToken, Task <TOutput> > functionToRun, Func <Exception, int> exceptionToExitCodeConverter, bool waitAfterFlush = true) where TOutput : class { ChildProcessArguments arguments = ChildProcessArguments.FromCommandLineArguments(args); string pipeParentToChildHandle = arguments.PipeParentToChildHandle; string pipeChildToParentHandle = arguments.PipeChildToParentHandle; try { using (PipeStream pipe = new AnonymousPipeClientStream(PipeDirection.In, pipeParentToChildHandle)) { CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); try { // Read the input var input = await this.ReadFromStream <TInput>(pipe, cancellationTokenSource.Token); // Start listening to parent process - run the listeners in separate tasks // We should not wait on these tasks, since: // * If any of these tasks fail, it will requests cancellation, and it is enough to wait on the main method and // let it handle cancellation gracefully // * The cancellation listener is blocking, and cannot be canceled (anonymous pipes do not support cancellation). // Waiting on it will block the current thread. #pragma warning disable 4014 this.ParentLiveListenerAsync(pipe, cancellationTokenSource); this.ParentCancellationListenerAsync(pipe, cancellationTokenSource); #pragma warning restore 4014 // Run the main function TOutput output = await functionToRun(input, cancellationTokenSource.Token); // Write the output back to the parent await this.WriteChildProcessResult(pipeChildToParentHandle, output); // Success - return zero for exit code return(0); } catch (Exception e) { // If the exception is due to cancellation, than return dedicated error codes if (cancellationTokenSource.IsCancellationRequested) { await this.WriteChildProcessResult(pipeChildToParentHandle, "Child process was canceled by the parent"); return((int)HttpStatusCode.InternalServerError); } return(await this.HandleChildProcessException(e, pipeChildToParentHandle, exceptionToExitCodeConverter)); } finally { // Cancel the token to stop the listener tasks cancellationTokenSource.Cancel(); } } } catch (Exception e) { return(await this.HandleChildProcessException(e, pipeChildToParentHandle, exceptionToExitCodeConverter)); } finally { this.tracer.Flush(); if (waitAfterFlush) { await Task.Delay(1000 * 5); } } }
/// <summary> /// Converts the specified instance of <see cref="ChildProcessArguments"/> to command line arguments /// </summary> /// <param name="arguments">The <see cref="ChildProcessArguments"/> instance</param> /// <returns>The command line arguments (as string)</returns> public static string ToCommandLineArguments(ChildProcessArguments arguments) { return(Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(arguments, Formatting.None)))); }
/// <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, IExtendedTracer 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) /// { /// IExtendedTracer 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}"); // 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); } }