Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions CliWrap.Tests/PipingSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,55 @@ public async Task I_can_execute_a_command_and_pipe_the_stdout_into_multiple_targ
.Contain("Expected exception.");
}

[Fact(Timeout = 15000)]
public async Task I_can_execute_a_command_and_not_hang_on_large_output_if_the_stdout_pipe_throws_an_exception()
{
// Arrange
var stdOutReadCount = 0;
var stdErrPipeCancellationTcs = new TaskCompletionSource();

var cmd = Cli.Wrap(Dummy.Program.FilePath)
.WithArguments(["generate binary", "--target", "all", "--length", "1000000"])
.WithStandardOutputPipe(
PipeTarget.Create(
async (origin, cancellationToken) =>
{
using var buffer = MemoryPool<byte>.Shared.Rent(1);

while (
await origin
.ReadAsync(buffer.Memory[..1], cancellationToken)
.ConfigureAwait(false) > 0
)
{
if (++stdOutReadCount == 3)
throw new Exception("Expected exception.");
}
}
)
)
.WithStandardErrorPipe(
PipeTarget.Create(
(_, cancellationToken) =>
{
cancellationToken.Register(() => stdErrPipeCancellationTcs.SetResult());
return stdErrPipeCancellationTcs.Task;
}
)
);

// Act
var task = cmd.ExecuteAsync();
var act = async () => await task;

// Assert
(await act.Should().ThrowAsync<Exception>())
.Which.Message.Should()
.Contain("Expected exception.");
await stdErrPipeCancellationTcs.Task;
Process.IsRunning(task.ProcessId).Should().BeFalse();
}

[Fact(Timeout = 15000)]
public async Task I_can_execute_a_command_and_pipe_the_stdout_into_multiple_hierarchical_targets()
{
Expand Down
62 changes: 23 additions & 39 deletions CliWrap/Buffered/BufferedCommandExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,26 @@ CancellationToken gracefulCancellationToken
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)
);

// Execute the command with the pipes extended to capture the output and error streams into buffers
return command
.WithStandardOutputPipe(stdOutPipe)
.WithStandardErrorPipe(stdErrPipe)
// Extend the existing standard output pipe to also write data to a buffer
.WithStandardOutputPipe(
PipeTarget.Merge(
command.StandardOutputPipe,
PipeTarget.ToStringBuilder(stdOutBuffer, standardOutputEncoding)
)
)
// Extend the existing standard error pipe to also write data to a buffer
.WithStandardErrorPipe(
PipeTarget.Merge(
command.StandardErrorPipe,
PipeTarget.ToStringBuilder(stdErrBuffer, standardErrorEncoding)
)
)
.ExecuteAsync(forcefulCancellationToken, gracefulCancellationToken)
.Bind(async task =>
// CommandTask<> doesn't have a method builder, so we wrap it manually to
// transform the result into an object that also includes the contents of
// the standard output and standard error buffers.
.Wrap(async task =>
{
try
{
Expand All @@ -62,6 +66,8 @@ CancellationToken gracefulCancellationToken
}
catch (CommandExecutionException ex)
{
// In case of a command exception (i.e., non-zero exit code), we can also include the
// standard error output in the exception for better diagnostics.
throw new CommandExecutionException(
ex.Command,
ex.ExitCode,
Expand All @@ -77,14 +83,7 @@ CancellationToken gracefulCancellationToken
});
}

/// <summary>
/// Executes the command asynchronously with buffering.
/// Data written to the standard output and standard error streams is decoded as text
/// and returned as part of the result object.
/// </summary>
/// <remarks>
/// This method can be awaited.
/// </remarks>
/// <inheritdoc cref="ExecuteBufferedAsync(Command, Encoding, Encoding, CancellationToken, CancellationToken)" />
public CommandTask<BufferedCommandResult> ExecuteBufferedAsync(
Encoding standardOutputEncoding,
Encoding standardErrorEncoding,
Expand All @@ -97,28 +96,13 @@ public CommandTask<BufferedCommandResult> ExecuteBufferedAsync(
CancellationToken.None
);

/// <summary>
/// Executes the command asynchronously with buffering.
/// Data written to the standard output and standard error streams is decoded as text
/// and returned as part of the result object.
/// </summary>
/// <remarks>
/// This method can be awaited.
/// </remarks>
/// <inheritdoc cref="ExecuteBufferedAsync(Command, Encoding, Encoding, CancellationToken, CancellationToken)" />
public CommandTask<BufferedCommandResult> ExecuteBufferedAsync(
Encoding encoding,
CancellationToken cancellationToken = default
) => command.ExecuteBufferedAsync(encoding, encoding, cancellationToken);

/// <summary>
/// Executes the command asynchronously with buffering.
/// Data written to the standard output and standard error streams is decoded as text
/// and returned as part of the result object.
/// Uses <see cref="Encoding.Default" /> for decoding.
/// </summary>
/// <remarks>
/// This method can be awaited.
/// </remarks>
/// <inheritdoc cref="ExecuteBufferedAsync(Command, Encoding, Encoding, CancellationToken, CancellationToken)" />
public CommandTask<BufferedCommandResult> ExecuteBufferedAsync(
CancellationToken cancellationToken = default
) => command.ExecuteBufferedAsync(Encoding.Default, cancellationToken);
Expand Down
2 changes: 1 addition & 1 deletion CliWrap/Buffered/BufferedCommandResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public void Deconstruct(out int exitCode, out string standardOutput, out string
public partial class BufferedCommandResult
{
/// <summary>
/// Converts the result to a string value that corresponds to the <see cref="BufferedCommandResult.StandardOutput" /> property.
/// Converts the result to a string value that corresponds to the <see cref="StandardOutput" /> property.
/// </summary>
public static implicit operator string(BufferedCommandResult result) => result.StandardOutput;
}
36 changes: 9 additions & 27 deletions CliWrap/Builders/ArgumentsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ public ArgumentsBuilder Add(string value, bool escape)
return this;
}

/// <summary>
/// Adds the specified value to the list of arguments.
/// </summary>
/// <inheritdoc cref="Add(string, bool)" />
// TODO: (breaking change) remove in favor of optional parameter
public ArgumentsBuilder Add(string value) => Add(value, true);

Expand All @@ -44,9 +42,7 @@ public ArgumentsBuilder Add(IEnumerable<string> values, bool escape)
return this;
}

/// <summary>
/// Adds the specified values to the list of arguments.
/// </summary>
/// <inheritdoc cref="Add(IEnumerable{string}, bool)" />
// TODO: (breaking change) remove in favor of optional parameter
public ArgumentsBuilder Add(IEnumerable<string> values) => Add(values, true);

Expand All @@ -59,16 +55,12 @@ public ArgumentsBuilder Add(
bool escape = true
) => Add(value.ToString(null, formatProvider), escape);

/// <summary>
/// Adds the specified value to the list of arguments.
/// </summary>
/// <inheritdoc cref="Add(IFormattable, IFormatProvider, bool)" />
// TODO: (breaking change) remove in favor of the other overloads
public ArgumentsBuilder Add(IFormattable value, CultureInfo cultureInfo, bool escape) =>
Add(value, (IFormatProvider)cultureInfo, escape);

/// <summary>
/// Adds the specified value to the list of arguments.
/// </summary>
/// <inheritdoc cref="Add(IFormattable, CultureInfo, bool)" />
// TODO: (breaking change) remove in favor of the other overloads
public ArgumentsBuilder Add(IFormattable value, CultureInfo cultureInfo) =>
Add(value, cultureInfo, true);
Expand All @@ -80,10 +72,7 @@ public ArgumentsBuilder Add(IFormattable value, CultureInfo cultureInfo) =>
public ArgumentsBuilder Add(IFormattable value, bool escape) =>
Add(value, DefaultFormatProvider, escape);

/// <summary>
/// Adds the specified value to the list of arguments.
/// The value is converted to string using invariant culture.
/// </summary>
/// <inheritdoc cref="Add(IFormattable, bool)" />
// TODO: (breaking change) remove in favor of optional parameter
public ArgumentsBuilder Add(IFormattable value) => Add(value, true);

Expand All @@ -102,34 +91,27 @@ public ArgumentsBuilder Add(
return this;
}

/// <summary>
/// Adds the specified values to the list of arguments.
/// </summary>
/// <inheritdoc cref="Add(IEnumerable{IFormattable}, IFormatProvider, bool)" />
// TODO: (breaking change) remove in favor of the other overloads
public ArgumentsBuilder Add(
IEnumerable<IFormattable> values,
CultureInfo cultureInfo,
bool escape
) => Add(values, (IFormatProvider)cultureInfo, escape);

/// <summary>
/// Adds the specified values to the list of arguments.
/// </summary>
/// <inheritdoc cref="Add(IEnumerable{IFormattable}, CultureInfo, bool)" />
// TODO: (breaking change) remove in favor of the other overloads
public ArgumentsBuilder Add(IEnumerable<IFormattable> values, CultureInfo cultureInfo) =>
Add(values, cultureInfo, true);

/// <summary>
/// Adds the specified values to the list of arguments.
/// The values are converted to string using invariant culture.
/// The values are converted to strings using the invariant culture.
/// </summary>
public ArgumentsBuilder Add(IEnumerable<IFormattable> values, bool escape) =>
Add(values, DefaultFormatProvider, escape);

/// <summary>
/// Adds the specified values to the list of arguments.
/// The values are converted to string using invariant culture.
/// </summary>
/// <inheritdoc cref="Add(IEnumerable{IFormattable}, bool)" />
// TODO: (breaking change) remove in favor of optional parameter
public ArgumentsBuilder Add(IEnumerable<IFormattable> values) => Add(values, true);

Expand Down
8 changes: 4 additions & 4 deletions CliWrap/Builders/CredentialsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class CredentialsBuilder
/// Sets the Active Directory domain used when starting the process.
/// </summary>
/// <remarks>
/// For information on platform support, see attributes on <see cref="ProcessStartInfo.Domain" />.
/// For information on platform support, see the attributes on <see cref="ProcessStartInfo.Domain" />.
/// </remarks>
public CredentialsBuilder SetDomain(string? domain)
{
Expand All @@ -28,7 +28,7 @@ public CredentialsBuilder SetDomain(string? domain)
/// Sets the username used when starting the process.
/// </summary>
/// <remarks>
/// For information on platform support, see attributes on <see cref="ProcessStartInfo.UserName" />.
/// For information on platform support, see the attributes on <see cref="ProcessStartInfo.UserName" />.
/// </remarks>
public CredentialsBuilder SetUserName(string? userName)
{
Expand All @@ -40,7 +40,7 @@ public CredentialsBuilder SetUserName(string? userName)
/// Sets the password used when starting the process.
/// </summary>
/// <remarks>
/// For information on platform support, see attributes on <see cref="ProcessStartInfo.Password" />.
/// For information on platform support, see the attributes on <see cref="ProcessStartInfo.Password" />.
/// </remarks>
public CredentialsBuilder SetPassword(string? password)
{
Expand All @@ -52,7 +52,7 @@ public CredentialsBuilder SetPassword(string? password)
/// Instructs whether to load the user profile when starting the process.
/// </summary>
/// <remarks>
/// For information on platform support, see attributes on <see cref="ProcessStartInfo.LoadUserProfile" />.
/// For information on platform support, see the attributes on <see cref="ProcessStartInfo.LoadUserProfile" />.
/// </remarks>
public CredentialsBuilder LoadUserProfile(bool loadUserProfile = true)
{
Expand Down
4 changes: 1 addition & 3 deletions CliWrap/Builders/EnvironmentVariablesBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ public EnvironmentVariablesBuilder Set(IEnumerable<KeyValuePair<string, string?>
return this;
}

/// <summary>
/// Sets multiple environment variables from the specified dictionary.
/// </summary>
/// <inheritdoc cref="Set(IEnumerable{KeyValuePair{string, string}})" />
public EnvironmentVariablesBuilder Set(IReadOnlyDictionary<string, string?> variables) =>
Set((IEnumerable<KeyValuePair<string, string?>>)variables);

Expand Down
8 changes: 4 additions & 4 deletions CliWrap/Builders/ResourcePolicyBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class ResourcePolicyBuilder
/// Sets the priority class of the process.
/// </summary>
/// <remarks>
/// For information on platform support, see attributes on <see cref="Process.PriorityClass" />.
/// For information on platform support, see the attributes on <see cref="Process.PriorityClass" />.
/// </remarks>
public ResourcePolicyBuilder SetPriority(ProcessPriorityClass? priority)
{
Expand All @@ -29,7 +29,7 @@ public ResourcePolicyBuilder SetPriority(ProcessPriorityClass? priority)
/// For example, to set the affinity to cores 1 and 3 out of 4, pass 0b1010.
/// </summary>
/// <remarks>
/// For information on platform support, see attributes on <see cref="Process.ProcessorAffinity" />.
/// For information on platform support, see the attributes on <see cref="Process.ProcessorAffinity" />.
/// </remarks>
public ResourcePolicyBuilder SetAffinity(nint? affinity)
{
Expand All @@ -41,7 +41,7 @@ public ResourcePolicyBuilder SetAffinity(nint? affinity)
/// Sets the minimum working set size of the process.
/// </summary>
/// <remarks>
/// For information on platform support, see attributes on <see cref="Process.MinWorkingSet" />.
/// For information on platform support, see the attributes on <see cref="Process.MinWorkingSet" />.
/// </remarks>
public ResourcePolicyBuilder SetMinWorkingSet(nint? minWorkingSet)
{
Expand All @@ -53,7 +53,7 @@ public ResourcePolicyBuilder SetMinWorkingSet(nint? minWorkingSet)
/// Sets the maximum working set size of the process.
/// </summary>
/// <remarks>
/// For information on platform support, see attributes on <see cref="Process.MaxWorkingSet" />.
/// For information on platform support, see the attributes on <see cref="Process.MaxWorkingSet" />.
/// </remarks>
public ResourcePolicyBuilder SetMaxWorkingSet(nint? maxWorkingSet)
{
Expand Down
Loading