public void ExecuteTest(SimpleCommandWithIntegerResult command, Exception thrownException) { "Given a command whose handler throws an exception" .x(() => command = new SimpleCommandWithIntegerResult(new Exception("something went wrong"))); "When I dispatch the command" .x(async() => { try { await Dispatcher.DispatchAsync(command); } catch (Exception ex) { thrownException = ex; } }); "Then the exception is propagated back to the caller" .x(() => { Assert.NotNull(thrownException); Assert.IsType <CommandExecutionException>(thrownException); CommandExecutionException commandExecutionException = (CommandExecutionException)thrownException; Assert.NotNull(commandExecutionException.InnerException); Assert.Equal("something went wrong", commandExecutionException.InnerException.Message); }); }
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)); }
public async Task ExecuteCommandWithAnUnhandledException_ThrowsWrappedException() { ExceptionCommand command = new(); CommandExecutionException exception = await Assert.ThrowsAsync <CommandExecutionException>(() => CommandExecutor.InvokeAsync(command, CancellationToken.None)); Assert.IsType <CommandException>(exception.InnerException); }
/// <summary> /// Executes the command asynchronously. /// The result of this execution contains the standard output and standard error streams buffered in-memory as strings. /// This method can be awaited. /// </summary> public static CommandTask <BufferedCommandResult> ExecuteBufferedAsync( this Command command, Encoding standardOutputEncoding, Encoding standardErrorEncoding, CancellationToken cancellationToken = default) { var stdOutBuffer = new StringBuilder(); var stdErrBuffer = new StringBuilder(); var stdOutPipe = PipeTarget.Merge( command.StandardOutputPipe, PipeTarget.ToStringBuilder(stdOutBuffer, standardOutputEncoding) ); var stdErrPipe = PipeTarget.Merge( command.StandardErrorPipe, PipeTarget.ToStringBuilder(stdErrBuffer, standardErrorEncoding) ); var commandPiped = command .WithStandardOutputPipe(stdOutPipe) .WithStandardErrorPipe(stdErrPipe) .WithValidation(CommandResultValidation.None); // disable validation because we have our own return(commandPiped .ExecuteAsync(cancellationToken) .Select(r => { // Transform the result var result = new BufferedCommandResult( r.ExitCode, r.StartTime, r.ExitTime, stdOutBuffer.ToString(), stdErrBuffer.ToString() ); // We perform validation separately here because we want to include stderr in the exception as well if (result.ExitCode != 0 && command.Validation.IsZeroExitCodeValidationEnabled()) { throw CommandExecutionException.ExitCodeValidation( command.TargetFilePath, command.Arguments, result.ExitCode, result.StandardError.Trim() ); } return result; })); }
private async Task <CommandResult> ExecuteAsync(ProcessEx process, CancellationToken cancellationToken = default) { // Additional cancellation for stdin in case the process terminates early and doesn't fully exhaust it using var stdInCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); // Setup and start process using var _1 = process; process.Start(); using var _2 = cancellationToken.Register(process.Kill); // Start piping in parallel var pipingTasks = new[] { PipeStandardInputAsync(process, stdInCts.Token), PipeStandardOutputAsync(process, cancellationToken), PipeStandardErrorAsync(process, cancellationToken) }; // Wait until the process terminates or gets killed await process.WaitUntilExitAsync(); // Cancel stdin in case the process terminated early and doesn't need it anymore stdInCts.Cancel(); try { // Wait until piping is done and propagate exceptions await Task.WhenAll(pipingTasks); } catch (OperationCanceledException) when(!cancellationToken.IsCancellationRequested) { // Don't throw if cancellation happened internally and not by user request } // Validate exit code if required 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)); }
public CommandExecutedMessage(CommandExecutor executor, CommandExecutionException exception = null) { Executor = executor; Exception = exception; }