private async Task PipeStandardInputAsync(ProcessEx process, CancellationToken cancellationToken = default) { try { // Some streams do not support cancellation, so we add a fallback that // drops the task and returns early. // Doing so does leave the original piping task still alive, which is // unfortunate, but still better than having everything freeze up. // This is important with stdin because the process might finish before // the pipe completes, and in case with infinite input stream it would // normally result in a deadlock. await StandardInputPipe.CopyToAsync(process.StdIn, cancellationToken) .WithDangerousCancellation(cancellationToken); } catch (IOException) { // IOException: The pipe has been ended. // This may happen if the process terminated before the pipe could complete. // It's not an exceptional situation because the process may not need // the entire stdin to complete successfully. } finally { await process.StdIn.DisposeAsync(); } }
private async Task <CommandResult> ExecuteAsync(ProcessEx process, CancellationToken cancellationToken = default) { using var _ = process; process.Start(); // Register cancellation using var cancellation = cancellationToken.Register(() => process.TryKill()); // Handle stdin pipe using (process.StdIn) await StandardInputPipe.CopyToAsync(process.StdIn, cancellationToken); // Handle stdout/stderr pipes and wait for exit await Task.WhenAll( StandardOutputPipe.CopyFromAsync(process.StdOut, cancellationToken), StandardErrorPipe.CopyFromAsync(process.StdErr, cancellationToken), process.WaitUntilExitAsync()); if (process.ExitCode != 0 && Validation.IsZeroExitCodeValidationEnabled()) { throw CommandExecutionException.ExitCodeValidation(TargetFilePath, Arguments, process.ExitCode); } return(new CommandResult(process.ExitCode, process.StartTime, process.ExitTime)); }
private async Task <CommandResult> ExecuteAsync(ProcessEx process, CancellationToken cancellationToken = default) { using var _ = process; process.Start(); // Register cancellation using var cancellation = cancellationToken.Register(() => process.TryKill()); // Stdin must be closed after it finished to avoid deadlock if the process reads the stream to end async Task HandleStdInAsync() { using (process.StdIn) await StandardInputPipe.CopyToAsync(process.StdIn, cancellationToken); } // Stdout doesn't need to be closed but we do it for good measure async Task HandleStdOutAsync() { using (process.StdOut) await StandardOutputPipe.CopyFromAsync(process.StdOut, cancellationToken); } // Stderr doesn't need to be closed but we do it for good measure async Task HandleStdErrAsync() { using (process.StdErr) await StandardErrorPipe.CopyFromAsync(process.StdErr, cancellationToken); } // Handle pipes in parallel to avoid deadlocks await Task.WhenAll( HandleStdInAsync(), HandleStdOutAsync(), HandleStdErrAsync(), process.WaitUntilExitAsync() ); if (process.ExitCode != 0 && Validation.IsZeroExitCodeValidationEnabled()) { throw CommandExecutionException.ExitCodeValidation(TargetFilePath, Arguments, process.ExitCode); } return(new CommandResult(process.ExitCode, process.StartTime, process.ExitTime)); }
private async Task <CommandResult> ExecuteAsync(ProcessEx process, CancellationToken cancellationToken = default) { using var _ = process; process.Start(); // Stdin pipe may need to be canceled early if the process terminates before it finishes using var stdInCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); // Register early process termination using var cancellation = cancellationToken.Register(() => process.TryKill()); // Stdin must be closed after it finished to avoid deadlock if the process reads the stream to end async Task HandleStdInAsync() { try { // Some streams don't support cancellation, in which case we need a fallback mechanism to avoid deadlocks. // For example, WindowsConsoleStream (from Console.OpenStandardInput()) in particular doesn't support cancellation. // In the following case the operation will terminate but a rogue Task will leak and might cause problems. // This is a non-issue, however, if the user closes the stream at the earliest opportunity. // Otherwise we enter an indeterminate state and tell ourselves we did everything we could to avoid it. await Task.WhenAny( StandardInputPipe.CopyToAsync(process.StdIn, stdInCts.Token), Task.Delay(-1, stdInCts.Token) ); } // Ignore cancellation here, will propagate later catch (OperationCanceledException) { } // We want to ignore I/O exceptions that happen when the output stream has already closed. // This can happen when the process reads only a portion of stdin and then exits. // Unfortunately we can't catch a specific exception for this exact event so we have no choice but to catch all of them. catch (IOException) { } finally { await process.StdIn.DisposeAsync(); } } // Stdout doesn't need to be closed but we do it for good measure async Task HandleStdOutAsync() { try { await StandardOutputPipe.CopyFromAsync(process.StdOut, cancellationToken); } // Ignore cancellation here, will propagate later catch (OperationCanceledException) { } finally { await process.StdOut.DisposeAsync(); } } // Stderr doesn't need to be closed but we do it for good measure async Task HandleStdErrAsync() { try { await StandardErrorPipe.CopyFromAsync(process.StdErr, cancellationToken); } // Ignore cancellation here, will propagate later catch (OperationCanceledException) { } finally { await process.StdErr.DisposeAsync(); } } // Handle pipes in background and in parallel to avoid deadlocks var pipingTasks = new[] { HandleStdInAsync(), HandleStdOutAsync(), HandleStdErrAsync() }; // Wait until the process terminates or gets killed await process.WaitUntilExitAsync(); // Stop piping stdin if the process has already exited (can happen if not all of stdin is read) stdInCts.Cancel(); // Ensure all pipes are finished await Task.WhenAll(pipingTasks); // Propagate cancellation to the user cancellationToken.ThrowIfCancellationRequested(); // Validate exit code if (process.ExitCode != 0 && Validation.IsZeroExitCodeValidationEnabled()) { throw CommandExecutionException.ExitCodeValidation(TargetFilePath, Arguments, process.ExitCode); } return(new CommandResult(process.ExitCode, process.StartTime, process.ExitTime)); }