diff --git a/CliWrap.Benchmarks/CliWrap.Benchmarks.csproj b/CliWrap.Benchmarks/CliWrap.Benchmarks.csproj
index 7188739b..ede15a1f 100644
--- a/CliWrap.Benchmarks/CliWrap.Benchmarks.csproj
+++ b/CliWrap.Benchmarks/CliWrap.Benchmarks.csproj
@@ -7,8 +7,8 @@
-
-
+
+
diff --git a/CliWrap.Signaler/CliWrap.Signaler.csproj b/CliWrap.Signaler/CliWrap.Signaler.csproj
index 6a2a5e0d..9410585d 100644
--- a/CliWrap.Signaler/CliWrap.Signaler.csproj
+++ b/CliWrap.Signaler/CliWrap.Signaler.csproj
@@ -6,7 +6,7 @@
-
+
\ No newline at end of file
diff --git a/CliWrap.Tests.Dummy/CliWrap.Tests.Dummy.csproj b/CliWrap.Tests.Dummy/CliWrap.Tests.Dummy.csproj
index f0f641cc..7ad3d443 100644
--- a/CliWrap.Tests.Dummy/CliWrap.Tests.Dummy.csproj
+++ b/CliWrap.Tests.Dummy/CliWrap.Tests.Dummy.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/CliWrap.Tests/CliWrap.Tests.csproj b/CliWrap.Tests/CliWrap.Tests.csproj
index 9ce9607b..c6d4adba 100644
--- a/CliWrap.Tests/CliWrap.Tests.csproj
+++ b/CliWrap.Tests/CliWrap.Tests.csproj
@@ -10,15 +10,15 @@
-
-
-
+
+
+
-
-
+
+
diff --git a/CliWrap.Tests/ConfigurationSpecs.cs b/CliWrap.Tests/ConfigurationSpecs.cs
index a02db08a..33a403d0 100644
--- a/CliWrap.Tests/ConfigurationSpecs.cs
+++ b/CliWrap.Tests/ConfigurationSpecs.cs
@@ -1,5 +1,7 @@
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
+using CliWrap.Builders;
using FluentAssertions;
using Xunit;
@@ -17,6 +19,7 @@ public void I_can_create_a_command_with_the_default_configuration()
cmd.TargetFilePath.Should().Be("foo");
cmd.Arguments.Should().BeEmpty();
cmd.WorkingDirPath.Should().Be(Directory.GetCurrentDirectory());
+ cmd.ResourcePolicy.Should().Be(ResourcePolicy.Default);
cmd.Credentials.Should().BeEquivalentTo(Credentials.Default);
cmd.EnvironmentVariables.Should().BeEmpty();
cmd.Validation.Should().Be(CommandResultValidation.ZeroExitCode);
@@ -109,6 +112,47 @@ public void I_can_configure_the_working_directory()
modified.WorkingDirPath.Should().Be("new");
}
+ [Fact]
+ public void I_can_configure_the_resource_policy()
+ {
+ // Arrange
+ var original = Cli.Wrap("foo").WithResourcePolicy(ResourcePolicy.Default);
+
+ // Act
+ var modified = original.WithResourcePolicy(
+ new ResourcePolicy(ProcessPriorityClass.High, 0x1, 1024, 2048)
+ );
+
+ // Assert
+ original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.ResourcePolicy));
+ original.ResourcePolicy.Should().NotBe(modified.ResourcePolicy);
+ modified
+ .ResourcePolicy.Should()
+ .BeEquivalentTo(new ResourcePolicy(ProcessPriorityClass.High, 0x1, 1024, 2048));
+ }
+
+ [Fact]
+ public void I_can_configure_the_resource_policy_using_a_builder()
+ {
+ // Arrange
+ var original = Cli.Wrap("foo").WithResourcePolicy(ResourcePolicy.Default);
+
+ // Act
+ var modified = original.WithResourcePolicy(b =>
+ b.SetPriority(ProcessPriorityClass.High)
+ .SetAffinity(0x1)
+ .SetMinWorkingSet(1024)
+ .SetMaxWorkingSet(2048)
+ );
+
+ // Assert
+ original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.ResourcePolicy));
+ original.ResourcePolicy.Should().NotBe(modified.ResourcePolicy);
+ modified
+ .ResourcePolicy.Should()
+ .BeEquivalentTo(new ResourcePolicy(ProcessPriorityClass.High, 0x1, 1024, 2048));
+ }
+
[Fact]
public void I_can_configure_the_user_credentials()
{
diff --git a/CliWrap.Tests/CredentialsSpecs.cs b/CliWrap.Tests/CredentialsSpecs.cs
index ebc14d46..8fd0b9e5 100644
--- a/CliWrap.Tests/CredentialsSpecs.cs
+++ b/CliWrap.Tests/CredentialsSpecs.cs
@@ -58,7 +58,7 @@ public async Task I_can_try_to_execute_a_command_as_a_different_user_and_get_an_
{
Skip.If(
RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
- "Starting a process as another user is only supported on Windows."
+ "Starting a process as another user is fully supported on Windows."
);
// Arrange
diff --git a/CliWrap.Tests/PipingSpecs.cs b/CliWrap.Tests/PipingSpecs.cs
index 0f39dd48..81be5e57 100644
--- a/CliWrap.Tests/PipingSpecs.cs
+++ b/CliWrap.Tests/PipingSpecs.cs
@@ -1,4 +1,5 @@
using System;
+using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Text;
@@ -138,6 +139,30 @@ public async Task I_can_execute_a_command_and_pipe_the_stdin_from_another_comman
result.StandardOutput.Trim().Should().Be("100000");
}
+ [Fact(Timeout = 15000)]
+ public async Task I_can_execute_a_command_and_pipe_the_stdin_from_another_command_with_a_transform()
+ {
+ // Arrange
+ var cmd =
+ PipeSource.FromCommand(
+ Cli.Wrap(Dummy.Program.FilePath)
+ .WithArguments(["generate binary", "--length", "100000"]),
+ // Transform: take the first 5000 bytes and discard the rest
+ async (source, destination, cancellationToken) =>
+ {
+ using var buffer = MemoryPool.Shared.Rent(5000);
+ await source.ReadAtLeastAsync(buffer.Memory, 5000, false, cancellationToken);
+ await destination.WriteAsync(buffer.Memory[..5000], cancellationToken);
+ }
+ ) | Cli.Wrap(Dummy.Program.FilePath).WithArguments("length stdin");
+
+ // Act
+ var result = await cmd.ExecuteBufferedAsync();
+
+ // Assert
+ result.StandardOutput.Trim().Should().Be("5000");
+ }
+
[Fact(Timeout = 15000)]
public async Task I_can_execute_a_command_and_pipe_the_stdin_from_a_chain_of_commands()
{
diff --git a/CliWrap.Tests/ResourcePolicySpecs.cs b/CliWrap.Tests/ResourcePolicySpecs.cs
new file mode 100644
index 00000000..7b537f24
--- /dev/null
+++ b/CliWrap.Tests/ResourcePolicySpecs.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Xunit;
+
+namespace CliWrap.Tests;
+
+public class ResourcePolicySpecs
+{
+ [SkippableFact(Timeout = 15000)]
+ public async Task I_can_execute_a_command_with_a_custom_process_priority()
+ {
+ // Process priority is supported on other platforms, but setting it requires elevated permissions,
+ // which we cannot guarantee in a CI environment. Therefore, we only test this on Windows.
+ Skip.IfNot(
+ RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
+ "Starting a process with a custom priority is only supported on Windows."
+ );
+
+ // Arrange
+ var cmd = Cli.Wrap(Dummy.Program.FilePath)
+ .WithResourcePolicy(p => p.SetPriority(ProcessPriorityClass.High));
+
+ // Act
+ var result = await cmd.ExecuteAsync();
+
+ // Assert
+ result.ExitCode.Should().Be(0);
+ }
+
+ [SkippableFact(Timeout = 15000)]
+ public async Task I_can_execute_a_command_with_a_custom_core_affinity()
+ {
+ Skip.IfNot(
+ RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ || RuntimeInformation.IsOSPlatform(OSPlatform.Linux),
+ "Starting a process with a custom core affinity is only supported on Windows and Linux."
+ );
+
+ // Arrange
+ var cmd = Cli.Wrap(Dummy.Program.FilePath).WithResourcePolicy(p => p.SetAffinity(0b1010)); // Cores 1 and 3
+
+ // Act
+ var result = await cmd.ExecuteAsync();
+
+ // Assert
+ result.ExitCode.Should().Be(0);
+ }
+
+ [SkippableFact(Timeout = 15000)]
+ public async Task I_can_execute_a_command_with_a_custom_working_set_limit()
+ {
+ Skip.IfNot(
+ RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
+ "Starting a process with a custom working set limit is only supported on Windows."
+ );
+
+ // Arrange
+ var cmd = Cli.Wrap(Dummy.Program.FilePath)
+ .WithResourcePolicy(p =>
+ p.SetMinWorkingSet(1024 * 1024) // 1 MB
+ .SetMaxWorkingSet(1024 * 1024 * 10) // 10 MB
+ );
+
+ // Act
+ var result = await cmd.ExecuteAsync();
+
+ // Assert
+ result.ExitCode.Should().Be(0);
+ }
+
+ [SkippableFact(Timeout = 15000)]
+ public async Task I_can_try_to_execute_a_command_with_a_custom_resource_policy_and_get_an_error_if_the_operating_system_does_not_support_it()
+ {
+ Skip.If(
+ RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
+ "Starting a process with a custom resource policy is fully supported on Windows."
+ );
+
+ // Arrange
+ var cmd = Cli.Wrap(Dummy.Program.FilePath)
+ .WithResourcePolicy(p => p.SetMinWorkingSet(1024 * 1024));
+
+ // Act & assert
+ await Assert.ThrowsAsync(() => cmd.ExecuteAsync());
+ }
+}
diff --git a/CliWrap/Builders/ResourcePolicyBuilder.cs b/CliWrap/Builders/ResourcePolicyBuilder.cs
new file mode 100644
index 00000000..8ee91807
--- /dev/null
+++ b/CliWrap/Builders/ResourcePolicyBuilder.cs
@@ -0,0 +1,56 @@
+using System.Diagnostics;
+
+namespace CliWrap.Builders;
+
+///
+/// Builder that helps configure resource policy.
+///
+public class ResourcePolicyBuilder
+{
+ private ProcessPriorityClass _priority = ProcessPriorityClass.Normal;
+ private nint? _affinity;
+ private nint? _minWorkingSet;
+ private nint? _maxWorkingSet;
+
+ ///
+ /// Sets the priority class of the process.
+ ///
+ public ResourcePolicyBuilder SetPriority(ProcessPriorityClass priority)
+ {
+ _priority = priority;
+ return this;
+ }
+
+ ///
+ /// Sets the processor core affinity mask of the process.
+ /// For example, to set the affinity to cores 1 and 3 out of 4, pass 0b1010.
+ ///
+ public ResourcePolicyBuilder SetAffinity(nint? affinity)
+ {
+ _affinity = affinity;
+ return this;
+ }
+
+ ///
+ /// Sets the minimum working set size of the process.
+ ///
+ public ResourcePolicyBuilder SetMinWorkingSet(nint? minWorkingSet)
+ {
+ _minWorkingSet = minWorkingSet;
+ return this;
+ }
+
+ ///
+ /// Sets the maximum working set size of the process.
+ ///
+ public ResourcePolicyBuilder SetMaxWorkingSet(nint? maxWorkingSet)
+ {
+ _maxWorkingSet = maxWorkingSet;
+ return this;
+ }
+
+ ///
+ /// Builds the resulting resource policy.
+ ///
+ public ResourcePolicy Build() => new(_priority, _affinity, _minWorkingSet, _maxWorkingSet);
+}
diff --git a/CliWrap/CliWrap.csproj b/CliWrap/CliWrap.csproj
index 23397bca..feb4e464 100644
--- a/CliWrap/CliWrap.csproj
+++ b/CliWrap/CliWrap.csproj
@@ -23,12 +23,12 @@
-
-
+
+
-
+
diff --git a/CliWrap/Command.Execution.cs b/CliWrap/Command.Execution.cs
index 35e31930..2c2ca5f5 100644
--- a/CliWrap/Command.Execution.cs
+++ b/CliWrap/Command.Execution.cs
@@ -115,7 +115,7 @@ private ProcessStartInfo CreateStartInfo()
{
throw new NotSupportedException(
"Cannot start a process using the provided credentials. "
- + "Setting custom domain, password, or loading user profile is only supported on Windows.",
+ + "Setting custom domain, username, password, and/or loading the user profile is not supported on this platform.",
ex
);
}
@@ -308,10 +308,36 @@ CancellationToken gracefulCancellationToken
{
var process = new ProcessEx(CreateStartInfo());
- // This method may fail and we want to propagate the exceptions immediately instead
+ // This method may fail, and we want to propagate the exceptions immediately instead
// of wrapping them in a task, so it needs to be executed in a synchronous context.
// https://github.com/Tyrrrz/CliWrap/issues/139
- process.Start();
+ process.Start(p =>
+ {
+ try
+ {
+ // Disable CA1416 because we're handling an exception that is thrown by the property setters
+#pragma warning disable CA1416
+ p.PriorityClass = ResourcePolicy.Priority;
+
+ if (ResourcePolicy.Affinity is not null)
+ p.ProcessorAffinity = ResourcePolicy.Affinity.Value;
+
+ if (ResourcePolicy.MinWorkingSet is not null)
+ p.MinWorkingSet = ResourcePolicy.MinWorkingSet.Value;
+
+ if (ResourcePolicy.MaxWorkingSet is not null)
+ p.MaxWorkingSet = ResourcePolicy.MaxWorkingSet.Value;
+#pragma warning restore CA1416
+ }
+ catch (NotSupportedException ex)
+ {
+ throw new NotSupportedException(
+ "Cannot set resource policy for the process. "
+ + "Setting custom priority, affinity, and/or working set limits is not supported on this platform.",
+ ex
+ );
+ }
+ });
// Extract the process ID before calling ExecuteAsync(), because the process may
// already be disposed by then.
diff --git a/CliWrap/Command.cs b/CliWrap/Command.cs
index 27bed170..07524a20 100644
--- a/CliWrap/Command.cs
+++ b/CliWrap/Command.cs
@@ -14,6 +14,7 @@ public partial class Command(
string targetFilePath,
string arguments,
string workingDirPath,
+ ResourcePolicy resourcePolicy,
Credentials credentials,
IReadOnlyDictionary environmentVariables,
CommandResultValidation validation,
@@ -30,6 +31,7 @@ public Command(string targetFilePath)
targetFilePath,
string.Empty,
Directory.GetCurrentDirectory(),
+ ResourcePolicy.Default,
Credentials.Default,
new Dictionary(),
CommandResultValidation.ZeroExitCode,
@@ -47,6 +49,9 @@ public Command(string targetFilePath)
///
public string WorkingDirPath { get; } = workingDirPath;
+ ///
+ public ResourcePolicy ResourcePolicy { get; } = resourcePolicy;
+
///
public Credentials Credentials { get; } = credentials;
@@ -75,6 +80,7 @@ public Command WithTargetFile(string targetFilePath) =>
targetFilePath,
Arguments,
WorkingDirPath,
+ ResourcePolicy,
Credentials,
EnvironmentVariables,
Validation,
@@ -96,6 +102,7 @@ public Command WithArguments(string arguments) =>
TargetFilePath,
arguments,
WorkingDirPath,
+ ResourcePolicy,
Credentials,
EnvironmentVariables,
Validation,
@@ -142,6 +149,25 @@ public Command WithWorkingDirectory(string workingDirPath) =>
TargetFilePath,
Arguments,
workingDirPath,
+ ResourcePolicy,
+ Credentials,
+ EnvironmentVariables,
+ Validation,
+ StandardInputPipe,
+ StandardOutputPipe,
+ StandardErrorPipe
+ );
+
+ ///
+ /// Creates a copy of this command, setting the resource policy to the specified value.
+ ///
+ [Pure]
+ public Command WithResourcePolicy(ResourcePolicy resourcePolicy) =>
+ new(
+ TargetFilePath,
+ Arguments,
+ WorkingDirPath,
+ resourcePolicy,
Credentials,
EnvironmentVariables,
Validation,
@@ -150,6 +176,19 @@ public Command WithWorkingDirectory(string workingDirPath) =>
StandardErrorPipe
);
+ ///
+ /// Creates a copy of this command, setting the resource policy to the value
+ /// configured by the specified delegate.
+ ///
+ [Pure]
+ public Command WithResourcePolicy(Action configure)
+ {
+ var builder = new ResourcePolicyBuilder();
+ configure(builder);
+
+ return WithResourcePolicy(builder.Build());
+ }
+
///
/// Creates a copy of this command, setting the user credentials to the specified value.
///
@@ -159,6 +198,7 @@ public Command WithCredentials(Credentials credentials) =>
TargetFilePath,
Arguments,
WorkingDirPath,
+ ResourcePolicy,
credentials,
EnvironmentVariables,
Validation,
@@ -191,6 +231,7 @@ public Command WithEnvironmentVariables(
TargetFilePath,
Arguments,
WorkingDirPath,
+ ResourcePolicy,
Credentials,
environmentVariables,
Validation,
@@ -221,6 +262,7 @@ public Command WithValidation(CommandResultValidation validation) =>
TargetFilePath,
Arguments,
WorkingDirPath,
+ ResourcePolicy,
Credentials,
EnvironmentVariables,
validation,
@@ -238,6 +280,7 @@ public Command WithStandardInputPipe(PipeSource source) =>
TargetFilePath,
Arguments,
WorkingDirPath,
+ ResourcePolicy,
Credentials,
EnvironmentVariables,
Validation,
@@ -255,6 +298,7 @@ public Command WithStandardOutputPipe(PipeTarget target) =>
TargetFilePath,
Arguments,
WorkingDirPath,
+ ResourcePolicy,
Credentials,
EnvironmentVariables,
Validation,
@@ -272,6 +316,7 @@ public Command WithStandardErrorPipe(PipeTarget target) =>
TargetFilePath,
Arguments,
WorkingDirPath,
+ ResourcePolicy,
Credentials,
EnvironmentVariables,
Validation,
diff --git a/CliWrap/ICommandConfiguration.cs b/CliWrap/ICommandConfiguration.cs
index 5a101d0b..0f72fc8e 100644
--- a/CliWrap/ICommandConfiguration.cs
+++ b/CliWrap/ICommandConfiguration.cs
@@ -22,6 +22,11 @@ public interface ICommandConfiguration
///
string WorkingDirPath { get; }
+ ///
+ /// Resource policy set for the underlying process.
+ ///
+ ResourcePolicy ResourcePolicy { get; }
+
///
/// User credentials set for the underlying process.
///
diff --git a/CliWrap/PipeSource.cs b/CliWrap/PipeSource.cs
index 7a8faca4..3bb679a5 100644
--- a/CliWrap/PipeSource.cs
+++ b/CliWrap/PipeSource.cs
@@ -132,12 +132,34 @@ public static PipeSource FromString(string str, Encoding encoding) =>
///
/// Creates a pipe source that reads from the standard output of the specified command.
///
- public static PipeSource FromCommand(Command command) =>
+ public static PipeSource FromCommand(
+ Command command,
+ Func copyStreamAsync
+ ) =>
+ // cmdA | | cmdB
Create(
- async (destination, cancellationToken) =>
+ // Destination -> cmdB's standard input
+ async (destination, destinationCancellationToken) =>
await command
- .WithStandardOutputPipe(PipeTarget.ToStream(destination))
- .ExecuteAsync(cancellationToken)
+ .WithStandardOutputPipe(
+ PipeTarget.Create(
+ // Source -> cmdA's standard output
+ async (source, sourceCancellationToken) =>
+ await copyStreamAsync(source, destination, sourceCancellationToken)
+ .ConfigureAwait(false)
+ )
+ )
+ .ExecuteAsync(destinationCancellationToken)
.ConfigureAwait(false)
);
+
+ ///
+ /// Creates a pipe source that reads from the standard output of the specified command.
+ ///
+ public static PipeSource FromCommand(Command command) =>
+ FromCommand(
+ command,
+ async (source, destination, cancellationToken) =>
+ await source.CopyToAsync(destination, cancellationToken).ConfigureAwait(false)
+ );
}
diff --git a/CliWrap/ResourcePolicy.cs b/CliWrap/ResourcePolicy.cs
new file mode 100644
index 00000000..943824b9
--- /dev/null
+++ b/CliWrap/ResourcePolicy.cs
@@ -0,0 +1,42 @@
+using System.Diagnostics;
+
+namespace CliWrap;
+
+///
+/// Resource policy assigned to a process.
+///
+public partial class ResourcePolicy(
+ ProcessPriorityClass priority = ProcessPriorityClass.Normal,
+ nint? affinity = null,
+ nint? minWorkingSet = null,
+ nint? maxWorkingSet = null
+)
+{
+ ///
+ /// Priority class of the process.
+ ///
+ public ProcessPriorityClass Priority { get; } = priority;
+
+ ///
+ /// Processor core affinity mask of the process.
+ ///
+ public nint? Affinity { get; } = affinity;
+
+ ///
+ /// Minimum working set size of the process.
+ ///
+ public nint? MinWorkingSet { get; } = minWorkingSet;
+
+ ///
+ /// Maximum working set size of the process.
+ ///
+ public nint? MaxWorkingSet { get; } = maxWorkingSet;
+}
+
+public partial class ResourcePolicy
+{
+ ///
+ /// Default resource policy.
+ ///
+ public static ResourcePolicy Default { get; } = new();
+}
diff --git a/CliWrap/Utils/ProcessEx.cs b/CliWrap/Utils/ProcessEx.cs
index 8fd53594..91ccc93a 100644
--- a/CliWrap/Utils/ProcessEx.cs
+++ b/CliWrap/Utils/ProcessEx.cs
@@ -42,7 +42,7 @@ internal class ProcessEx(ProcessStartInfo startInfo) : IDisposable
public int ExitCode => _nativeProcess.ExitCode;
- public void Start()
+ public void Start(Action? configureProcess = null)
{
// Hook up events
_nativeProcess.EnableRaisingEvents = true;
@@ -64,6 +64,9 @@ public void Start()
}
StartTime = DateTimeOffset.Now;
+
+ // Apply custom configurations
+ configureProcess?.Invoke(_nativeProcess);
}
catch (Win32Exception ex)
{
diff --git a/Readme.md b/Readme.md
index 1f1f59a2..4b5ec50c 100644
--- a/Readme.md
+++ b/Readme.md
@@ -269,6 +269,41 @@ var cmd = Cli.Wrap("git")
> Environment variables configured using `WithEnvironmentVariables(...)` are applied on top of those inherited from the parent process.
> If you need to remove an inherited variable, set the corresponding value to `null`.
+#### `WithResourcePolicy(...)`
+
+Sets the system resource management policy for the child process.
+
+**Default**: default policy.
+
+**Examples**:
+
+- Set resource policy using a builder:
+
+```csharp
+var cmd = Cli.Wrap("git")
+ .WithResourcePolicy(policy => policy
+ .SetPriority(ProcessPriorityClass.High)
+ .SetAffinity(0b1010)
+ .SetMinWorkingSet(1024)
+ .SetMaxWorkingSet(4096)
+ );
+```
+
+- Set resource policy directly:
+
+```csharp
+var cmd = Cli.Wrap("git")
+ .WithResourcePolicy(new ResourcePolicy(
+ priority: ProcessPriorityClass.High,
+ affinity: 0b1010,
+ minWorkingSet: 1024,
+ maxWorkingSet: 4096
+ ));
+```
+
+> **Warning**:
+> Resource policy options have varying support across different platforms.
+
#### `WithCredentials(...)`
Sets domain, name and password of the user, under whom the child process should be started.