GithubHelp home page GithubHelp logo

tyrrrz / cliwrap Goto Github PK

View Code? Open in Web Editor NEW
4.1K 47.0 259.0 898 KB

Library for running command-line processes

License: MIT License

C# 100.00%
command-line cli piping stdout event-stream shell process pipe-stdout stderr pipe-stderr

cliwrap's Introduction

CliWrap

Status Made in Ukraine Build Coverage Version Downloads Discord Fuck Russia

Development of this project is entirely funded by the community. Consider donating to support!

Icon

CliWrap is a library for interacting with external command-line interfaces. It provides a convenient model for launching processes, redirecting input and output streams, awaiting completion, handling cancellation, and more.

Terms of use[?]

By using this project or its source code, for any purpose and in any shape or form, you grant your implicit agreement to all the following statements:

  • You condemn Russia and its military aggression against Ukraine
  • You recognize that Russia is an occupant that unlawfully invaded a sovereign state
  • You support Ukraine's territorial integrity, including its claims over temporarily occupied territories of Crimea and Donbas
  • You reject false narratives perpetuated by Russian state propaganda

To learn more about the war and how you can help, click here. Glory to Ukraine! 🇺🇦

Install

  • 📦 NuGet: dotnet add package CliWrap

Features

  • Airtight abstraction over System.Diagnostics.Process
  • Fluent configuration interface
  • Flexible support for piping
  • Fully asynchronous and cancellation-aware API
  • Graceful cancellation using interrupt signals
  • Designed with strict immutability in mind
  • Provides safety against typical deadlock scenarios
  • Tested on Windows, Linux, and macOS
  • Targets .NET Standard 2.0+, .NET Core 3.0+, .NET Framework 4.6.2+
  • No external dependencies

Usage

Video guides

You can watch one of these videos to learn how to use the library:

Intro to CliWrap

Stop using the Process class for CLI interactions in .NET

Quick overview

Similarly to a shell, CliWrap's base unit of work is a command — an object that encapsulates instructions for running a process. To build a command, start by calling Cli.Wrap(...) with the executable path, and then use the provided fluent interface to configure arguments, working directory, or other options. Once the command is configured, you can run it by calling ExecuteAsync():

using CliWrap;

var result = await Cli.Wrap("path/to/exe")
    .WithArguments(["--foo", "bar"])
    .WithWorkingDirectory("work/dir/path")
    .ExecuteAsync();

// Result contains:
// -- result.IsSuccess       (bool)
// -- result.ExitCode        (int)
// -- result.StartTime       (DateTimeOffset)
// -- result.ExitTime        (DateTimeOffset)
// -- result.RunTime         (TimeSpan)

The code above spawns a child process with the configured command-line arguments and working directory, and then asynchronously waits for it to exit. After the task has completed, it resolves to a CommandResult object that contains the process exit code and other relevant information.

Warning: CliWrap will throw an exception if the underlying process returns a non-zero exit code, as it usually indicates an error. You can override this behavior by disabling result validation using WithValidation(CommandResultValidation.None).

By default, the process's standard input, output and error streams are routed to CliWrap's equivalent of a null device, which represents an empty source and a target that discards all data. You can change this by calling WithStandardInputPipe(...), WithStandardOutputPipe(...), or WithStandardErrorPipe(...) to configure pipes for the corresponding streams:

using CliWrap;

var stdOutBuffer = new StringBuilder();
var stdErrBuffer = new StringBuilder();

var result = await Cli.Wrap("path/to/exe")
    .WithArguments(["--foo", "bar"])
    .WithWorkingDirectory("work/dir/path")
    // This can be simplified with `ExecuteBufferedAsync()`
    .WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdOutBuffer))
    .WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuffer))
    .ExecuteAsync();

// Access stdout & stderr buffered in-memory as strings
var stdOut = stdOutBuffer.ToString();
var stdErr = stdErrBuffer.ToString();

This example command is configured to decode the data written to the standard output and error streams as text, and append it to the corresponding StringBuilder buffers. Once the execution is complete, these buffers can be inspected to see what the process has printed to the console.

Handling command output is a very common use case, so CliWrap offers a few high-level execution models to make these scenarios simpler. In particular, the same thing shown above can also be achieved more succinctly with the ExecuteBufferedAsync() extension method:

using CliWrap;
using CliWrap.Buffered;

// Calling `ExecuteBufferedAsync()` instead of `ExecuteAsync()`
// implicitly configures pipes that write to in-memory buffers.
var result = await Cli.Wrap("path/to/exe")
    .WithArguments(["--foo", "bar"])
    .WithWorkingDirectory("work/dir/path")
    .ExecuteBufferedAsync();

// Result contains:
// -- result.IsSuccess       (bool)
// -- result.StandardOutput  (string)
// -- result.StandardError   (string)
// -- result.ExitCode        (int)
// -- result.StartTime       (DateTimeOffset)
// -- result.ExitTime        (DateTimeOffset)
// -- result.RunTime         (TimeSpan)

Warning: Be mindful when using ExecuteBufferedAsync(). Programs can write arbitrary data (including binary) to the output and error streams, and storing it in-memory may be impractical. For more advanced scenarios, CliWrap also provides other piping options, which are covered in the piping section.

Command configuration

The fluent interface provided by the command object allows you to configure various aspects of its execution. This section covers all available configuration methods and their usage.

Note: Command is an immutable object — all configuration methods listed here create a new instance instead of modifying the existing one.

WithArguments(...)

Sets the command-line arguments passed to the child process.

Default: empty.

Examples:

  • Set arguments using an array:
var cmd = Cli.Wrap("git")
    // Each element is formatted as a separate argument.
    // Equivalent to: `git commit -m "my commit"`
    .WithArguments(["commit", "-m", "my commit"]);
  • Set arguments using a builder:
var cmd = Cli.Wrap("git")
    // Each Add(...) call takes care of formatting automatically.
    // Equivalent to: `git clone https://github.com/Tyrrrz/CliWrap --depth 20`
    .WithArguments(args => args
        .Add("clone")
        .Add("https://github.com/Tyrrrz/CliWrap")
        .Add("--depth")
        .Add(20)
    );
var forcePush = true;

var cmd = Cli.Wrap("git")
    // Arguments can also be constructed in an imperative fashion.
    // Equivalent to: `git push --force`
    .WithArguments(args => 
    {
        args.Add("push");

        if (forcePush)
            args.Add("--force");
    });

Note: The builder overload allows you to define custom extension methods for reusable argument patterns. Learn more.

  • Set arguments directly:
var cmd = Cli.Wrap("git")
    // Avoid using this overload unless you really have to.
    // Equivalent to: `git commit -m "my commit"`
    .WithArguments("commit -m \"my commit\"");

Warning: Unless you absolutely have to, avoid setting command line arguments directly from a string. This method expects all arguments to be correctly escaped and formatted ahead of time — which can be cumbersome to do yourself. Formatting errors may result in unexpected bugs and security vulnerabilities.

WithWorkingDirectory(...)

Sets the working directory of the child process.

Default: current working directory, i.e. Directory.GetCurrentDirectory().

Example:

var cmd = Cli.Wrap("git")
    .WithWorkingDirectory("c:/projects/my project/");

WithEnvironmentVariables(...)

Sets additional environment variables exposed to the child process.

Default: empty.

Examples:

  • Set environment variables using a builder:
var cmd = Cli.Wrap("git")
    .WithEnvironmentVariables(env => env
        .Set("GIT_AUTHOR_NAME", "John")
        .Set("GIT_AUTHOR_EMAIL", "[email protected]")
    );
  • Set environment variables directly:
var cmd = Cli.Wrap("git")
    .WithEnvironmentVariables(new Dictionary<string, string?>
    {
        ["GIT_AUTHOR_NAME"] = "John",
        ["GIT_AUTHOR_EMAIL"] = "[email protected]"
    });

Note: 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.

WithCredentials(...)

Sets domain, name and password of the user, under whom the child process should be started.

Default: no credentials.

Examples:

  • Set credentials using a builder:
var cmd = Cli.Wrap("git")
    .WithCredentials(creds => creds
       .SetDomain("some_workspace")
       .SetUserName("johndoe")
       .SetPassword("securepassword123")
       .LoadUserProfile()
    );
  • Set credentials directly:
var cmd = Cli.Wrap("git")
    .WithCredentials(new Credentials(
        domain: "some_workspace",
        userName: "johndoe",
        password: "securepassword123",
        loadUserProfile: true
    ));

Warning: Running a process under a different username is supported across all platforms, but other options are only available on Windows.

WithValidation(...)

Sets the strategy for validating the result of an execution.

Accepted values:

  • CommandResultValidation.None — no validation
  • CommandResultValidation.ZeroExitCode — ensures zero exit code when the process exits

Default: CommandResultValidation.ZeroExitCode.

Examples:

  • Enable validation:
var cmd = Cli.Wrap("git")
    .WithValidation(CommandResultValidation.ZeroExitCode);
  • Disable validation:
var cmd = Cli.Wrap("git")
    .WithValidation(CommandResultValidation.None);

If you want to throw a custom exception when the process exits with a non-zero exit code, don't disable result validation, but instead catch the default CommandExecutionException and re-throw it inside your own exception. This way you can preserve the information provided by the original exception, while extending it with additional context:

try
{
    await Cli.Wrap("git").ExecuteAsync();
}
catch (CommandExecutionException ex)
{
    // Re-throw the original exception to preserve additional information
    // about the command that failed (exit code, arguments, etc.).
    throw new MyException("Failed to run the git command-line tool.", ex);
}

WithStandardInputPipe(...)

Sets the pipe source that will be used for the standard input stream of the process.

Default: PipeSource.Null.

Read more about this method in the piping section.

WithStandardOutputPipe(...)

Sets the pipe target that will be used for the standard output stream of the process.

Default: PipeTarget.Null.

Read more about this method in the piping section.

WithStandardErrorPipe(...)

Sets the pipe target that will be used for the standard error stream of the process.

Default: PipeTarget.Null.

Read more about this method in the piping section.

Piping

CliWrap provides a very powerful and flexible piping model that allows you to redirect process's streams, transform input and output data, and even chain multiple commands together with minimal effort. At its core, it's based on two abstractions: PipeSource which provides data for the standard input stream, and PipeTarget which reads data coming from the standard output stream or the standard error stream.

By default, a command's input pipe is set to PipeSource.Null and the output and error pipes are set to PipeTarget.Null. These objects effectively represent no-op stubs that provide empty input and discard all output respectively.

You can specify your own PipeSource and PipeTarget instances by calling the corresponding configuration methods on the command:

await using var input = File.OpenRead("input.txt");
await using var output = File.Create("output.txt");

await Cli.Wrap("foo")
    .WithStandardInputPipe(PipeSource.FromStream(input))
    .WithStandardOutputPipe(PipeTarget.ToStream(output))
    .ExecuteAsync();

Alternatively, pipes can also be configured in a slightly terser way using pipe operators:

await using var input = File.OpenRead("input.txt");
await using var output = File.Create("output.txt");

await (input | Cli.Wrap("foo") | output).ExecuteAsync();

Both PipeSource and PipeTarget have many factory methods that let you create pipe implementations for different scenarios:

  • PipeSource:
    • PipeSource.Null — represents an empty pipe source
    • PipeSource.FromStream(...) — pipes data from any readable stream
    • PipeSource.FromFile(...) — pipes data from a file
    • PipeSource.FromBytes(...) — pipes data from a byte array
    • PipeSource.FromString(...) — pipes data from a text string
    • PipeSource.FromCommand(...) — pipes data from the standard output of another command
  • PipeTarget:
    • PipeTarget.Null — represents a pipe target that discards all data
    • PipeTarget.ToStream(...) — pipes data to any writable stream
    • PipeTarget.ToFile(...) — pipes data to a file
    • PipeTarget.ToStringBuilder(...) — pipes data as text into a StringBuilder
    • PipeTarget.ToDelegate(...) — pipes data as text, line-by-line, into an Action<string>, or a Func<string, Task>, or a Func<string, CancellationToken, Task> delegate
    • PipeTarget.Merge(...) — merges multiple outbound pipes by replicating the same data across all of them

Warning: Using PipeTarget.Null results in the corresponding stream (stdout or stderr) not being opened for the underlying process at all. In the vast majority of cases, this behavior should be functionally equivalent to piping to a null stream, but without the performance overhead of consuming and discarding unneeded data. This may be undesirable in certain situations — in which case it's recommended to pipe to a null stream explicitly using PipeTarget.ToStream(Stream.Null).

Below you can see some examples of what you can achieve with the help of CliWrap's piping feature:

  • Pipe a string into stdin:
var cmd = "Hello world" | Cli.Wrap("foo");
await cmd.ExecuteAsync();
  • Pipe stdout as text into a StringBuilder:
var stdOutBuffer = new StringBuilder();

var cmd = Cli.Wrap("foo") | stdOutBuffer;
await cmd.ExecuteAsync();
  • Pipe a binary HTTP stream into stdin:
using var httpClient = new HttpClient();
await using var input = await httpClient.GetStreamAsync("https://example.com/image.png");

var cmd = input | Cli.Wrap("foo");
await cmd.ExecuteAsync();
  • Pipe stdout of one command into stdin of another:
var cmd = Cli.Wrap("foo") | Cli.Wrap("bar") | Cli.Wrap("baz");
await cmd.ExecuteAsync();
  • Pipe stdout and stderr into stdout and stderr of the parent process:
await using var stdOut = Console.OpenStandardOutput();
await using var stdErr = Console.OpenStandardError();

var cmd = Cli.Wrap("foo") | (stdOut, stdErr);
await cmd.ExecuteAsync();
  • Pipe stdout into a delegate:
var cmd = Cli.Wrap("foo") | Debug.WriteLine;
await cmd.ExecuteAsync();
  • Pipe stdout into a file and stderr into a StringBuilder:
var buffer = new StringBuilder();

var cmd = Cli.Wrap("foo") |
    (PipeTarget.ToFile("output.txt"), PipeTarget.ToStringBuilder(buffer));

await cmd.ExecuteAsync();
  • Pipe stdout into multiple files simultaneously:
var target = PipeTarget.Merge(
    PipeTarget.ToFile("file1.txt"),
    PipeTarget.ToFile("file2.txt"),
    PipeTarget.ToFile("file3.txt")
);

var cmd = Cli.Wrap("foo") | target;
await cmd.ExecuteAsync();
  • Pipe a string into stdin of one command, stdout of that command into stdin of another command, and then stdout and stderr of the last command into stdout and stderr of the parent process:
var cmd =
    "Hello world" |
    Cli.Wrap("foo").WithArguments(["aaa"]) |
    Cli.Wrap("bar").WithArguments(["bbb"]) |
    (Console.WriteLine, Console.Error.WriteLine);

await cmd.ExecuteAsync();

Execution models

CliWrap provides a few high-level execution models that offer alternative ways to reason about commands. These are essentially just extension methods that work by leveraging the piping feature shown earlier.

Buffered execution

This execution model lets you run a process while buffering its standard output and error streams in-memory as text. The buffered data can then be accessed after the command finishes executing.

In order to execute a command with buffering, call the ExecuteBufferedAsync() extension method:

using CliWrap;
using CliWrap.Buffered;

var result = await Cli.Wrap("foo")
    .WithArguments(["bar"])
    .ExecuteBufferedAsync();

var exitCode = result.ExitCode;
var stdOut = result.StandardOutput;
var stdErr = result.StandardError;

By default, ExecuteBufferedAsync() assumes that the underlying process uses the default encoding (Console.OutputEncoding) for writing text to the console. To override this, specify the encoding explicitly by using one of the available overloads:

// Treat both stdout and stderr as UTF8-encoded text streams
var result = await Cli.Wrap("foo")
    .WithArguments(["bar"])
    .ExecuteBufferedAsync(Encoding.UTF8);

// Treat stdout as ASCII-encoded and stderr as UTF8-encoded
var result = await Cli.Wrap("foo")
    .WithArguments(["bar"])
    .ExecuteBufferedAsync(Encoding.ASCII, Encoding.UTF8);

Note: If the underlying process returns a non-zero exit code, ExecuteBufferedAsync() will throw an exception similarly to ExecuteAsync(), but the exception message will also include the standard error data.

Pull-based event stream

Besides executing a command as a task, CliWrap also supports an alternative model, in which the execution is represented as an event stream. This lets you start a process and react to the events it produces in real-time.

Those events are:

  • StartedCommandEvent — received just once, when the command starts executing (contains the process ID)
  • StandardOutputCommandEvent — received every time the underlying process writes a new line to the output stream (contains the text as a string)
  • StandardErrorCommandEvent — received every time the underlying process writes a new line to the error stream (contains the text as a string)
  • ExitedCommandEvent — received just once, when the command finishes executing (contains the exit code)

To execute a command as a pull-based event stream, use the ListenAsync() extension method:

using CliWrap;
using CliWrap.EventStream;

var cmd = Cli.Wrap("foo").WithArguments(["bar"]);

await foreach (var cmdEvent in cmd.ListenAsync())
{
    switch (cmdEvent)
    {
        case StartedCommandEvent started:
            _output.WriteLine($"Process started; ID: {started.ProcessId}");
            break;
        case StandardOutputCommandEvent stdOut:
            _output.WriteLine($"Out> {stdOut.Text}");
            break;
        case StandardErrorCommandEvent stdErr:
            _output.WriteLine($"Err> {stdErr.Text}");
            break;
        case ExitedCommandEvent exited:
            _output.WriteLine($"Process exited; Code: {exited.ExitCode}");
            break;
    }
}

The ListenAsync() method starts the command and returns an object of type IAsyncEnumerable<CommandEvent>, which you can iterate using the await foreach construct introduced in C# 8. When using this execution model, back pressure is facilitated by locking the pipes between each iteration of the loop, preventing unnecessary buffering of data in-memory.

Note: Just like with ExecuteBufferedAsync(), you can specify custom encoding for ListenAsync() using one of its overloads.

Push-based event stream

Similarly to the pull-based stream, you can also execute a command as a push-based event stream instead:

using System.Reactive;
using CliWrap;
using CliWrap.EventStream;

var cmd = Cli.Wrap("foo").WithArguments(["bar"]);

await cmd.Observe().ForEachAsync(cmdEvent =>
{
    switch (cmdEvent)
    {
        case StartedCommandEvent started:
            _output.WriteLine($"Process started; ID: {started.ProcessId}");
            break;
        case StandardOutputCommandEvent stdOut:
            _output.WriteLine($"Out> {stdOut.Text}");
            break;
        case StandardErrorCommandEvent stdErr:
            _output.WriteLine($"Err> {stdErr.Text}");
            break;
        case ExitedCommandEvent exited:
            _output.WriteLine($"Process exited; Code: {exited.ExitCode}");
            break;
    }
});

In this case, Observe() returns a cold IObservable<CommandEvent> that represents an observable stream of command events. You can use the set of extensions provided by Rx.NET to transform, filter, throttle, or otherwise manipulate this stream.

Unlike the pull-based event stream, this execution model does not involve any back pressure, meaning that the data is pushed to the observer at the rate that it becomes available.

Note: Similarly to ExecuteBufferedAsync(), you can specify custom encoding for Observe() using one of its overloads.

Combining execution models with custom pipes

The different execution models shown above are based on the piping model, but those two concepts are not mutually exclusive. When running a command using one of the built-in execution models, existing pipe configurations are preserved and extended using PipeTarget.Merge(...).

This means that you can, for example, pipe a command to a file and simultaneously execute it as an event stream:

var cmd =
    PipeSource.FromFile("input.txt") |
    Cli.Wrap("foo") |
    PipeTarget.ToFile("output.txt");

// Iterate as an event stream and pipe to a file at the same time
// (execution models preserve configured pipes)
await foreach (var cmdEvent in cmd.ListenAsync())
{
    // ...
}

Timeout and cancellation

Command execution is asynchronous in nature as it involves a completely separate process. In many cases, it may be useful to implement an abortion mechanism to stop the execution before it finishes, either through a manual trigger or a timeout.

To do that, issue the corresponding CancellationToken and include it when calling ExecuteAsync():

using System.Threading;
using CliWrap;

using var cts = new CancellationTokenSource();

// Cancel after a timeout of 10 seconds
cts.CancelAfter(TimeSpan.FromSeconds(10));

var result = await Cli.Wrap("foo").ExecuteAsync(cts.Token);

In the event of a cancellation request, the underlying process will be killed and ExecuteAsync() will throw an exception of type OperationCanceledException (or its derivative, TaskCanceledException). You will need to catch this exception in your code to recover from cancellation:

try
{
    await Cli.Wrap("foo").ExecuteAsync(cts.Token);
}
catch (OperationCanceledException)
{
    // Command was canceled
}

Besides outright killing the process, you can also request cancellation in a more graceful way by sending an interrupt signal. To do that, pass an additional cancellation token to ExecuteAsync() that corresponds to that request:

using var forcefulCts = new CancellationTokenSource();
using var gracefulCts = new CancellationTokenSource();

// Cancel forcefully after a timeout of 10 seconds.
// This serves as a fallback in case graceful cancellation
// takes too long.
forcefulCts.CancelAfter(TimeSpan.FromSeconds(10));

// Cancel gracefully after a timeout of 7 seconds.
// If the process takes too long to respond to graceful
// cancellation, it will get killed by forceful cancellation
// 3 seconds later (as configured above).
gracefulCts.CancelAfter(TimeSpan.FromSeconds(7));

var result = await Cli.Wrap("foo").ExecuteAsync(forcefulCts.Token, gracefulCts.Token);

Requesting graceful cancellation in CliWrap is functionally equivalent to pressing Ctrl+C in the console window. The underlying process may handle this signal to perform last-minute critical work before finally exiting on its own terms.

Graceful cancellation is inherently cooperative, so it's possible that the process may take too long to fulfill the request or choose to ignore it altogether. In the above example, this risk is mitigated by additionally scheduling a delayed forceful cancellation that prevents the command from hanging.

If you are executing a command inside a method and don't want to expose those implementation details to the caller, you can rely on the following pattern to use the provided token for graceful cancellation and extend it with a forceful fallback:

public async Task GitPushAsync(CancellationToken cancellationToken = default)
{
    using var forcefulCts = new CancellationTokenSource();

    // When the cancellation token is triggered,
    // schedule forceful cancellation as a fallback.
    await using var link = cancellationToken.Register(() =>
        forcefulCts.CancelAfter(TimeSpan.FromSeconds(3))
    );

    await Cli.Wrap("git")
        .WithArguments(["push"])
        .ExecuteAsync(forcefulCts.Token, cancellationToken);
}

Note: Similarly to ExecuteAsync(), cancellation is also supported by ExecuteBufferedAsync(), ListenAsync(), and Observe().

Retrieving process-related information

The task returned by ExecuteAsync() and ExecuteBufferedAsync() is, in fact, not a regular Task<T>, but an instance of CommandTask<T>. This is a specialized awaitable object that contains additional information about the process associated with the executing command:

var task = Cli.Wrap("foo")
    .WithArguments(["bar"])
    .ExecuteAsync();

// Get the process ID
var processId = task.ProcessId;

// Wait for the task to complete
await task;

cliwrap's People

Contributors

0xced avatar alirezanet avatar bav2212 avatar brunozell avatar daniel15 avatar georg-jung avatar ian-g-holm-intel avatar iron9light avatar jakoss avatar koryphaee avatar loremfoobar avatar m-patrone avatar maartenba avatar md-v avatar mihazupan avatar moh-hassan avatar pchinery avatar piedone avatar pipe01 avatar simoncropp avatar tompazourek avatar tyrrrz avatar vipentti avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cliwrap's Issues

Parsing percentage status (no new lines)

Hi again Alexey,
Please, may you suggest an approach using CliWrap to parse the status as show the application below:
status
Such status will be show in a WinForm.
I tried something similar to the following method
public async Task I_can_execute_a_command_that_pipes_its_stdout_into_an_async_delegate() at PipingSpec.cs, but I just get the calling when there is a new line, at the end of the status, i.e. 100%.
Any guidance on which approach use is appreciated.
Thanks

Send command after execution

Let's suppose I have a long-running command-line app that exits on Ctrl+C. How would I model this with CliWrap? Startup is simple but once it runs how can I send it a Ctrl+C command? Would writing a line to a stream piped onto stdin work even after ExecuteAsync()?

Thank you.

PipeTarget.ToDelegate splits output into smaller chunks when merged with another pipe

Create a simple console app that prints one, long, continuous string:

	class Program
	{
		static void Main(string[] args)
		{
			Console.Out.Write(new String('x', 100000));
		}
	}

Now, create two unit tests:

[TestFixture]
public class Tests
{

	[Test]
	public async Task Test1()
	{
		var outputBuilder = new StringBuilder();

		await Cli.Wrap(@"<pathToTestApp>")
			.WithStandardOutputPipe(PipeTarget.ToStringBuilder(outputBuilder))
			.ExecuteAsync();

		var output = outputBuilder.ToString();

		Assert.False(output.Contains(Environment.NewLine));
	}

	[Test]
	public async Task Test2()
	{
		var outputBuilder = new StringBuilder();

		await Cli.Wrap(@"<pathToTestApp>")
			.WithStandardOutputPipe(PipeTarget.ToDelegate(line => {outputBuilder.AppendLine(line); }))
			.ExecuteAsync();

		var output = outputBuilder.ToString();

		Assert.False(output.Contains(Environment.NewLine));
	}
}

The first will pass, as expected. The second will fail.

It appears that the delegate PipeTarget is splitting a single, long line in to multiple lines. We're using the delegate target because we want to do some other things to the output than just writing to the StringBuilder (i.e. logging).

Create TaskCompletionSource with TaskCreationOptions.RunContinuationsAsynchronously

After looking for a good library doing basically what CliWrap does I think it's by far the best solution I've seen so far, thanks for the great work!

While reading some source code stumbled upon this line

private readonly TaskCompletionSource<object?> _exitTcs = new TaskCompletionSource<object?>();

and had to think of David Fowler's async guidance. I don't know if you considered this and I'm not at all deep enough into your code to know if it might not make sense in this case, but I thought I'd drop a note.

System.InvalidOperationException: No process is associated with this object

I'm getting System.InvalidOperationException while trying to start the process.

System.InvalidOperationException: No process is associated with this object.
   at System.Diagnostics.Process.EnsureState(State state)
   at System.Diagnostics.Process.get_HasExited()
   at CliWrap.Internal.ProcessEx.Dispose()
   at CliWrap.Command.ExecuteAsync(ProcessEx process, CancellationToken cancellationToken)

What it actually means is that targetFilePath pointed to non-existing exe and the process didn't even start. Which is fine, but it throws an exception in wrong place and the exception message is misleading.

Could we have a check whether the process actually started?

Interface for Cli class

Hello Sir, thank you for the great library.

It will be great to have an interface for Cli as it will be much more easy to create unit tests for classes which use Cli class.
E.g.

public interface ICli
{
    string FilePath { get; }
    CliSettings Settings { get; }
    
    void CancelAll();
    ExecutionOutput Execute(ExecutionInput input, CancellationToken cancellationToken = default(CancellationToken), IBufferHandler bufferHandler = null);
    ExecutionOutput Execute(string arguments, CancellationToken cancellationToken = default(CancellationToken), IBufferHandler bufferHandler = null);
    ExecutionOutput Execute(CancellationToken cancellationToken = default(CancellationToken), IBufferHandler bufferHandler = null);
    void ExecuteAndForget(ExecutionInput input);
    void ExecuteAndForget(string arguments);
    void ExecuteAndForget();
    Task<ExecutionOutput> ExecuteAsync(ExecutionInput input, CancellationToken cancellationToken = default(CancellationToken), IBufferHandler bufferHandler = null);
    Task<ExecutionOutput> ExecuteAsync(string arguments, CancellationToken cancellationToken = default(CancellationToken), IBufferHandler bufferHandler = null);
    Task<ExecutionOutput> ExecuteAsync(CancellationToken cancellationToken = default(CancellationToken), IBufferHandler bufferHandler = null);
}

Regards,

Stdin piping doesn't work with a stream that never resolves

Hello,

In our CLI program, we are invoking git mergetool on the user's behalf, and we want to pipe input and output unaltered between the parent process, and the target process (git) for the duration of the target command execution.

We have tried various variations of the following:

await using var stdIn = Console.OpenStandardInput();
await using var stdOut = Console.OpenStandardOutput();
await using var stdErr = Console.OpenStandardError();

var cmd =
	Cli.Wrap("git")
		.WithWorkingDirectory(@"C:\SomeRepository")
		.WithArguments("mergetool")
		.WithStandardInputPipe(PipeSource.FromStream(stdIn))
		.WithStandardOutputPipe(PipeTarget.ToStream(stdOut))
		.WithStandardErrorPipe(PipeTarget.ToStream(stdErr));

var result = await cmd.ExecuteAsync();

The piping of stdout and stderr seem to work as expected. However, input from stdin does not seem to get passed to the target process, because it does absolutely nothing in response to the input while prompting.

To illustrate:
image

What are we doing wrong? Any advice would be greatly appreciated.

ExecuteAsync throwing error even though command runs fine

Hey, so I just updated CliWrap on my App and changed the usage to use SetArguments.

I'm having trouble now as it throws an error on my ExecuteAsync() line even though the command it executes works 100%. It's using ffmpeg to copy a video.

In the temp folder it outputs to, the file is there and works completely fine. Can't figure out whats erroring here

Error:

  HResult=0x80131500
  Message=Underlying process reported an error:
ffmpeg version N-90649-g9825f77ac7 Copyright (c) 2000-2018 the FFmpeg developers
  built with gcc 7.3.0 (GCC)
  configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-bzlib --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth
  libavutil      56. 13.100 / 56. 13.100
  libavcodec     58. 17.100 / 58. 17.100
  libavformat    58. 11.101 / 58. 11.101
  libavdevice    58.  2.100 / 58.  2.100
  libavfilter     7. 14.100 /  7. 14.100
  libswscale      5.  0.102 /  5.  0.102
  libswresample   3.  0.101 /  3.  0.101
  libpostproc    55.  0.100 / 55.  0.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'C:\Users\njord\Videos\nVidia Share\Rocket League\rage_quit.mp4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isommp42
    creation_time   : 2018-09-29T10:20:37.000000Z
    date            : 2018
  Duration: 00:02:00.23, start: 0.000000, bitrate: 46971 kb/s
    Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, smpte170m/smpte170m/bt470m), 1920x1080 [SAR 1:1 DAR 16:9], 46769 kb/s, 59.89 fps, 60 tbr, 90k tbn, 120 tbc (default)
    Metadata:
      creation_time   : 2018-09-29T10:20:37.000000Z
      handler_name    : VideoHandle
    Stream #0:1(und): Audio: aac (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 195 kb/s (default)
    Metadata:
      creation_time   : 2018-09-29T10:20:37.000000Z
      handler_name    : SoundHandle
[mp4 @ 00000219fa0f44c0] track 1: codec frame size is not set
Output #0, mp4, to 'C:\Users\njord\Videos\temp\fbc9413e-a396-4f6b-8101-861cc592f16b.mp4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isommp42
    date            : 2018
    encoder         : Lavf58.11.101
    Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, smpte170m/smpte170m/bt470m), 1920x1080 [SAR 1:1 DAR 16:9], q=2-31, 46769 kb/s, 59.89 fps, 60 tbr, 90k tbn, 90k tbc (default)
    Metadata:
      creation_time   : 2018-09-29T10:20:37.000000Z
      handler_name    : VideoHandle
    Stream #0:1(und): Audio: aac (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 195 kb/s (default)
    Metadata:
      creation_time   : 2018-09-29T10:20:37.000000Z
      handler_name    : SoundHandle
Stream mapping:
  Stream #0:0 -> #0:0 (copy)
  Stream #0:1 -> #0:1 (copy)
Press [q] to stop, [?] for help
frame= 5810 fps=0.0 q=-1.0 size=  556800kB time=00:01:37.79 bitrate=46641.5kbits/s speed= 195x    
frame= 7200 fps=0.0 q=-1.0 Lsize=  689458kB time=00:02:00.21 bitrate=46983.7kbits/s speed= 198x    
video:686409kB audio:2871kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.025804%

  Source=CliWrap
  StackTrace:
   at CliWrap.Cli.<ExecuteAsync>d__28.MoveNext()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at YouTubeTool.ViewModels.CutVideoViewModel.<Go>d__46.MoveNext() in C:\Source\GitHub\remiX-\YouTubeTool\src\ViewModels\CutVideoViewModel.cs:line 147```

Use IObserver<T>/IObservable<T> for stdin/stdout/stderr

Title of the issue is self-explanatory: as a suggestion, stdin might be more flexibly represented by having CliWrap provide an IObserver<String> . In this vein, your library would then expose stdout and stderr as each an instance of the push-oriented interface IObservable<String>.

These changes would allow for better integration with other standard techniques, tools, and libraries such as Reactive Extensions.

Send SIGINT (Ctrl+C) on cancellation

The current cancellation token kills that application which is needed in some cases but it would also be great to have one that could close the application so the application has time to close gracefully and cleanup. I have a few processes there it would be ideal that i could close them and only kill them as a last resort if the close does not work after a short period of time.

ObjectDisposedException when using SetStandardOutputCallback

Getting:

Unhandled Exception: 
Unhandled Exception:System.ObjectDisposedException: The semaphore has been disposed.
   at System.Threading.SemaphoreSlim.CheckDispose()
   at System.Threading.SemaphoreSlim.Release(Int32 releaseCount)
   at CliWrap.Internal.Signal.Release()
   at CliWrap.Internal.CliProcess.<>c__DisplayClass24_0.<.ctor>b__2(Object sender, DataReceivedEventArgs args)
   at System.Diagnostics.Process.ErrorReadNotifyUser(String data)
   at System.Diagnostics.AsyncStreamReader.FlushMessageQueue()
   at System.Diagnostics.AsyncStreamReader.ReadBuffer(IAsyncResult ar)
   at System.IO.Stream.ReadWriteTask.InvokeAsyncCallback(Object completedTask)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.IO.Stream.ReadWriteTask.System.Threading.Tasks.ITaskCompletionAction.Invoke(Task completingTask)
   at System.Threading.Tasks.Task.FinishContinuations()
   at System.Threading.Tasks.Task.FinishStageThree()
   at System.Threading.Tasks.Task.FinishStageTwo()
   at System.Threading.Tasks.Task.Finish(Boolean bUserDelegateExecuted)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)
   at System.Threading.Tasks.Task.ExecuteEntry(Boolean bPreventDoubleExecution)
   at System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

It appears the events are still getting fired after the Signals are disposed.

The fix appears to be disconnecting the event handlers before disposing of the semaphores.

PR coming soon.

CancellationToken not always being honored

Hi, thank you for your work here. I've enjoyed using it for over a year now.

I have generally observed no issues at all with your library around clean-up, signal management, output redirection, native handles, etc.

But the cancellations are often not picked up until after the process has exited and well beyond the original time of cancellation. So I am guessing that's this location:

_cancellationToken.ThrowIfCancellationRequested();

Sometimes it works, though (immediate cancellation).

The output callbacks keep getting invoked after cancellation and while the process is still running. That's what I'd expect would happen because we don't want to lose process output, but I thought I'd mention it.

This particular invocation is a build script that starts processes of its own (npm, etc.).

Do you have any suspicion what might cause this behavior? Is the cancellation callback guaranteed to get an execution slot or could it be stuck waiting for the ExecuteAsync task to yield control?

I noticed that your TryKill code is relatively simple and doesn't look at exceptions and other outcomes too hard . May I suggest additions of the following nature? This is taken from some process management code I wrote at some point. Not sure how the API surface maps to what's available on .NET Standard, but I think the general ideas apply.
The important idea being: Check if the termination succeeded or not. And if not handle it in some way.

bool closed;
try
{
    if (!p.HasExited)
    {
        p.CloseMainWindow();
        closed = p.WaitForExit(2000);
    }
    else
    {
        closed = true;
    }
}
catch
{
    closed = false;
}

if (!closed)
{
    try
    {
        if (!p.HasExited)
        {
            p.Kill();

            if (!p.WaitForExit(10000))
            {
                throw new SomeException(
                    $"Most likely failed to terminate process (process ID {processId})'. Termination reason: {reason}. Started with:\n{call}"
                );
            }
        }
    }
    catch (InvalidOperationException) when (process.HasExited)
    {
    }
    catch (Exception ex)
    {
      // ?
    }
}

Setup:

CliWrap 2.3.0 in a NetCoreApp 2.2 project running on Windows directly as a Kestrel host.

// in this instance: powershell -File ".\build_client.ps1"
var cli = Cli.Wrap(executable)
    .EnableExitCodeValidation(isEnabled: false)
    .EnableStandardErrorValidation(isEnabled: false)
    .SetWorkingDirectory(workingDirectory.FullName)
    .SetArguments(command.Arguments ?? "")
    .SetCancellationToken(cancellationToken);

cli
    .SetStandardOutputCallback(async stdOutLine =>
    {
        if (httpTracing)
        {
            await logAsync(stdOutLine);
        }
        lock (writeLock1)
        {
            // StreamWriter backed by a file stream
            writerStdOut.WriteLine(GetLine(stdOutLine));
        }
    })
    .SetStandardErrorCallback(async stdErrLine =>
    {
        if (httpTracing)
        {
            await logAsync(stdErrLine);
        }
        lock (writeLock2)
        {
            // StreamWriter backed by a file stream
            writerStdErr.WriteLine(GetLine(stdErrLine));
        }
    });

output = await cli.ExecuteAsync();

Set environment variables

First of all, just wanted to say good job on this project, it's awesome and I will undoubtedly use it on any of my upcoming projects!

My request is to be able to set environment variables for the process. Exposing ProcessStartInfo.EnvironmentVariables would be enough for the job.

How to handle apps that do not give an exit code?

First, very nice library!
Now to my problem, I use it to merge MKV files with mkvmerge (v46) but i have the problem that mkvmerge finishes but gives appenrently no exit code so my app hangs and i need to restart it.

How do you handle this case?

Console.ReadKey problems

Hi, thanks for the library. I'm trying to use it to automate something.

I have a 'client' application that has the following code:

while (true)
{
	try
	{
		switch (Console.ReadKey(true).Key)
		{
		}
	} catch {}
}

Because of this, I get as fast as it can the following errors:

Out> 2020-04-15 16:44:12.9287  [1] ERROR Command.ProcessConsoleInput Error processing action System.InvalidOperationException: Cannot read keys when either application does not have a console or when console input has been redirected. Try Console.Read.
Out>    at System.ConsolePal.ReadKey(Boolean intercept)
Out>    at System.Console.ReadKey(Boolean intercept)
Out>    at ClientApplication.Command.ProcessConsoleInput() in Command.cs:line 45

Can something be done by giving the client application a 'pipe' to read from?

The most beautiful solution would be if this could be redirected from the host application, thanks!

ThrowIfError message should include the StandardError text

When ExecutionOutput.ThrowIfError() is called, the exception message seen in the output log looks something like this:

Unhandled Exception: System.AggregateException: One or more errors occurred.
(Underlying process reported an error. Inspect [StandardError] property for more information.)
---> CliWrap.Exceptions.StandardErrorException:
Underlying process reported an error. Inspect [StandardError] property for more information.

This is "ok" in a development/debug environment, but won't be helpful in a production environment.

The ThrowIfError message should include the StandardError text.

How to interact with prompts

Is it possible to answer multiple command prompts using CliWrap?
image
I'd like to automate configuring RClone but the challenge is that there are multiple questions in the setup and long delays in between each steps (while API calls are made in the background).

Obviously I can figure out the answers to all the prompts ahead of time, I'm just not sure how I'd accomplish triggering the prompt answers with CliWrap.

How to setup "ProcessStartInfo.StandardOutputEncoding"

I would like to start a CSharp app via cmd which prints UTF-8 Text.
To work correctly I need to execute chcp 65001 first or start it with a non ANS OutputEncoding.

This would be possible by setting StandardOutputEncoding of the class ProcessStartInfo.
The construction of this class is unfortunately handled in a private method. Could you make these internals somehow available? Best would be something like an "OnAdjustProcessStartInfo" event.

I have seen that you have an overload ExecuteAsync which affects the interpretation of the binary output... But it does not "inform" the application about that encoding.

Missing / too much string escaping?

Hello.

The following code (contains RANDOM string):

var cmdTest = Cli.Wrap("myexe")
    .WithArguments(a => a
        .Add(new[]
            {
                "/superstring",
                "ogq77a3auubgQaVHUXhKpVa*NR!YTsEjn8MA2^9$gZwb2D2z3EU^Fk7d8pS&psPg9F8M*jbfyCDWsewF$5osz9LmcsGUf5jV^@WarHuYPxZXN@GU5&AyhE!3t%W&f@ra"
                }));

produces this command argument - no quotes around the superstring while I expect it to be escaped:

/superstring ogq77a3auubgQaVHUXhKpVa*NR!YTsEjn8MA2^9$gZwb2D2z3EU^Fk7d8pS&psPg9F8M*jbfyCDWsewF$5osz9LmcsGUf5jV^@WarHuYPxZXN@GU5&AyhE!3t%W&f@ra

But, if i want to add a single quotation mark with this code:

var cmdTest = Cli.Wrap("myexe")
	.WithArguments(a => a
		.Add(new[]
		{
			"/superstring",
			"\"ogq77a3auubgQaVHUXhKpVa*NR!YTsEjn8MA2^9$gZwb2D2z3EU^Fk7d8pS&psPg9F8M*jbfyCDWsewF$5osz9LmcsGUf5jV^@WarHuYPxZXN@GU5&AyhE!3t%W&f@ra\""
		}));

I get the following command arguments - too many quotes:

/superstring "\"ogq77a3auubgQaVHUXhKpVa*NR!YTsEjn8MA2^9$gZwb2D2z3EU^Fk7d8pS&psPg9F8M*jbfyCDWsewF$5osz9LmcsGUf5jV^@WarHuYPxZXN@GU5&AyhE!3t%W&f@ra\""

My way to overcome this is to create the arguments by myself with StringBuilder and send it to the WithArguments method.

Is there another way to solve it?

Thanks!

How to run an embedded resource executable?

Hello,
Sorry for beginner question.
I would like to use this library to run an embedded resource executable. I cannot find any documentation or reference.
The idea is to hide the application from the user.
Thanks!

Pass arguments as list

I've transformed some Process.Start() calls into Cli.Wrap calls. I've noticed i have to get pretty funky with quotation marks to make it work, at least with one specific application.

Previously i used ProcessStartInfo.ArgumentList (which i guess was introduced with netstandard?).

For one process all i had to do is cli.SetArguments(string.Join(" ", argumentList)) and it worked.

For the other, problematic one i had to try a lot and ended up with a) putting almost (!) every argument set in quotation marks and b) also triple-quotation marks around a "string".
"\"-D RENDER=\"\"\"SINGLE\"\"\"\"" what previously was just "-D RENDER=\"SINGLE\"".

The argument parser in the other application may be buggy. But still, i don't know why it worked with ProcessStartInfo.ArgumentList before.

Looking into your implementation i see you're using "plain old" ProcessStartInfo.Arguments. Maybe using the ProcessStartInfo.ArgumentList and adding an API like ICli.AddArgument would help for these cases.

Doesn't work on Ubuntu

System.ComponentModel.Win32Exception (0x80004005): Unable to retrieve the specified information about the process or thread. It may have exited or may be privileged.
at System.Diagnostics.Process.GetStat()
at System.Diagnostics.Process.get_StartTimeCore()
at System.Diagnostics.Process.get_StartTime()
at CliWrap.Cli.Execute(ExecutionInput input, CancellationToken cancellationToken, IBufferHandler bufferHandler)

I tried executing my app with sudo and without, gave file execute permission, I don't really know if it's my problem or the library, although I had it working while using the Process class.

I need Process Id exposed through CLI/ICLI so I can kill off the underlying long running process

Lets say I am doing an infinite ping.exe -t (actually something more like sqlite3.exe on a SQL script which does some pretty long queries or large updates/inserts) so:
"TokenSource?.Cancel(); Task?.Wait();" on Window Close/Exit doesn't take effect immediately but waits around and takes a long time if the currently executing query line is a long running one before finishing;
Whilst:
"if (this.Thread?.IsAlive == true) this.Thread?.Abort();" is both rather nasty and whilst it appearing immediate actually leaves a stray ping.exe (or sqlite3.exe) sub-process still up and running to cause havoc and locks or multi writer corruption on SQLite files for example.

I need to kill off the ping.exe (or sqlite3.exe) sub-process explicitly using its Process.Id so I am now compiling your code rather than using the DLL through NuGet so I am going to make the change here which I estimate at around a half dozen lines of code plus an external Kill(int Id) routine of my own.

Let me know if you would like the results to reincorporate back into your code case once I get this up tested and working and then I too can switch back to your newer NuGet DLL down the track!

Sound like a plan?

BTW: I moved to CliWrap from another library and this is the only thing missing. The Stdout and Stderr callbacks work like a treat: I couldn't see any easy way of doing line by line output without this and with any other library it has already made my life significantly easier. SUGGESTION: Make EnableExitCodeValidation and EnableStandardErrorValidation false by default as I started looking at MedallionShell in the beginning when I mistakenly thought that your library like the first I tried wasnt working and was giving me grief at first.

Cancellation kills Process finishing actions

I'm using the ExecuteAsync() in a start-function and call the cts.Cancel() from a stop-function. The Cli wraps ffmpeg.exe and the hard Process.Kill() prevents the application to finish recording or converting of videofiles.

Maybe you could add a flag like "UseSoftProcessKill" and send the CTRL+C Command this way:
public async Task StopScreenRecordAsync() { if (AttachConsole((uint)ffmpegProcess.Id)) { SetConsoleCtrlHandler(null, true); try { if (!GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0)) { return; } while (!ffmpegProcess.HasExited) { await Task.Delay(200); } } finally { FreeConsole(); SetConsoleCtrlHandler(null, false); } } }

Thank you!

Older net-framework versions support

Hey, any chance to release support for old netframework versions for tools like installers that need to aim for atleast net3.5 that comes by default with windows 7?

It doesn't have to have async methods, so you can reduce the overhead with BCL libs.

Regards, Eli.

Provide some way to get Process Id

Hi there! Thanks for this greaty utility! It makes my life a lot easier :)
Is there a chance to get some after process Start Callback in order to get the process Id right after the start? I'd do a PR if desired :)

How to cancel ongoing process?

Sorry for beginner question, I used this library to create an automated Build (msbuild.exe), my question is let's say for some reason it's taking too long, and I want to cancel the process. How do I do that?

My current app is created using SignalR so every user can see the terminal output on real time.

var cli = Cli.Wrap(command).WithValidation(CommandResultValidation.None).WithArguments(arguments);
            var output = "";

            await foreach (var cmdEvent in cli.ListenAsync())
            {
                switch (cmdEvent)
                {
                    case StartedCommandEvent started:
                        output = "ProcessID : " + started.ProcessId;
                        break;
                    case StandardOutputCommandEvent stdOut:
                        output = stdOut.Text;
                        break;
                    case StandardErrorCommandEvent stdErr:
                        output = stdErr.Text;
                        break;
                    case ExitedCommandEvent exited:
                        output = "ExitCode : " + exited.ExitCode;
                        break;
                }

                //send to all client
                await Clients.All.SendAsync("BuildOutput", output);
            }

DebuggerDisplay for command events

A tiny improvement would be to add [DebuggerDisplay("{Text}")] to StandardOutputCommandEvent and StandardErrorCommandEvent. This way when you're looking at command outputs in the debugger you'd see their contents right away, not just after opening the Text property.

What do you think? I can submit a PR.

Exception out of try-catch scope?

Hi
I'm using CliWrap to running multiple ffmpeg (about 300 in one moment) processes all the time (each process lives for like 5 seconds). Recently i got this exception on my global error handler dispite the fact that every call to Cli.Execute is wrapped by try-catch. Any idea why this might be the case? It's like super rare, but still..

System.AggregateException: One or more errors occurred. ---> System.ComponentModel.Win32Exception: Access is denied
   at System.Diagnostics.Process.Kill()
   at CliWrap.Internal.Extensions.KillIfRunning(Process process)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.CancellationCallbackInfo.ExecuteCallback()
   at System.Threading.CancellationTokenSource.CancellationCallbackCoreWork(CancellationCallbackCoreWorkArguments args)
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)
   --- End of inner exception stack trace ---
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)
   at System.Threading.CancellationTokenSource.NotifyCancellation(Boolean throwOnFirstException)
   at System.Threading.CancellationTokenSource.TimerCallbackLogic(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.TimerQueueTimer.CallCallback()
   at System.Threading.TimerQueueTimer.Fire()
   at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
---> (Inner Exception #0) System.ComponentModel.Win32Exception (0x80004005): Access is denied
   at System.Diagnostics.Process.Kill()
   at CliWrap.Internal.Extensions.KillIfRunning(Process process)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.CancellationCallbackInfo.ExecuteCallback()
   at System.Threading.CancellationTokenSource.CancellationCallbackCoreWork(CancellationCallbackCoreWorkArguments args)
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)<---

System.ComponentModel.Win32Exception (0x80004005): Access is denied
   at System.Diagnostics.Process.Kill()
   at CliWrap.Internal.Extensions.KillIfRunning(Process process)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.CancellationCallbackInfo.ExecuteCallback()
   at System.Threading.CancellationTokenSource.CancellationCallbackCoreWork(CancellationCallbackCoreWorkArguments args)
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)

Piping ffmpeg into another process doesn't seem to do anything

I am trying to achieve the following pipe with CliWrap:

ffmpeg.exe -y -i "C:\input.mp4" -pix_fmt yuv420p -f yuv4mpegpipe - | aomenc.exe -v -w 1920 -h 1080 --passes=2 --pass=1 --fpf=".\out.stats" -t 1 --cq-level=30 --end-usage=q -o ".\out.ivf" -

This command works as it should in Command Prompt and PowerShell.

To do this, I am using CliWrap like so:

var ffProcess = Cli.Wrap( "C:\\ffmpeg.exe" ).WithArguments( ffArgs );
var aomProcess = Cli.Wrap( "C:\\aomenc.exe" ).WithArguments( aomArgs );
await ( ffProcess | aomProcess ).ExecuteBufferedAsync();

This launches the processes, but seems to just sit there until terminated. When launched manually, these two processes take up 50% CPU, but with CliWrap, they just sit there at 0%. This happens with both ExecuteAsync() and ExecuteBufferedAsync().

Ideally, I would like to use ListenAsync() so that I can read from the StdErr of aomenc.exe and parse the progress it outputs, but that doesn't seem to work either.

Invalid IL code exception with Xamarin.mac

Youtube.Converter fails in Xamarin.mac, it's not a runtime issue with code, it appears to be something with mono.

{System.InvalidProgramException: Invalid IL code in YoutubeExplode.Converter.Internal.FFmpeg/<ConvertAsync>d__2:MoveNext (): IL_01c0: stloc.2   


  at System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start[TStateMachine] (TStateMachine& stateMachine) [0x0002c] in /Library/Frameworks/Xamarin.Mac.framework/Versions/Current/src/Xamarin.Mac/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/AsyncMethodBuilder.cs:316 
  at YoutubeExplode.Converter.Internal.FFmpeg.ConvertAsync (System.String outputFilePath, System.Collections.Generic.IReadOnlyList`1[T] inputFilePaths, System.String format, System.String preset, System.Boolean avoidTranscoding, System.IProgress`1[T] progress, System.Threading.CancellationToken cancellationToken) [0x00048] in <941c5308801d480e8150dab71e27de93>:0 
  at YoutubeExplode.Converter.YoutubeConverter.DownloadAndProcessMediaStreamsAsync (System.Collections.Generic.IReadOnlyList`1[T] streamInfos, System.String filePath, System.String format, YoutubeExplode.Converter.ConversionPreset preset, System.IProgress`1[T] progress, System.Threading.CancellationToken cancellationToken) [0x002b5] in /YoutubeExplode.Converter/YoutubeExplode.Converter/YoutubeConverter.cs:91 }

The following code is what is causing the issue:

            await Cli.Wrap(_ffmpegFilePath)
                .WithArguments(arguments.Build())
                .WithStandardErrorPipe(stdErrPipe)
                .ExecuteAsync();

The constructor won't even get called because that code is there, replacing it with:

            var process = new Process
            {
                StartInfo = new ProcessStartInfo
                {
                    FileName = _ffmpegFilePath,
                    Arguments = arguments.Build(),
                    RedirectStandardOutput = true,
                    UseShellExecute = false,
                    CreateNoWindow = true
                }
            };

            process.Start();

            string result = process.StandardOutput.ReadToEnd();

            process.WaitForExit();

The conversion will take place, its a very strange issue, I hoping a work around might be added to your library to make this work

Doesn't work with git.exe

Thanks for creating CliWrap :-)

When I run git.exe it successfully completes, but ExecutionOutput.StandardError is still filled up with text and the ExecutionOutput.HasError is true. Is this a bug in CliWrap or is there something wrong with git.exe?

How to reproduce:

  1. On Windows 10, download and install Git-2.16.2-64-bit.exe from https://git-scm.com/download/win
  2. Make sure C:\temp folder exists.
  3. Run this code:
using (var cli = new Cli(@"C:\Program Files\Git\bin\git.exe", new CliSettings { WorkingDirectory = @"C:\temp" })) {
    var output = await cli.ExecuteAsync("clone -b \"1.8.2\" --single-branch --depth 1 https://github.com/Tyrrrz/CliWrap.git");
    var errortext = output.StandardError;
}

BTW, the StandardError contains this text:

Cloning into 'CliWrap'...
remote: Counting objects: 38, done.
remote: Compressing objects: 100% (34/34), done.
remote: Total 38 (delta 1), reused 18 (delta 0), pack-reused 0
Unpacking objects: 100% (38/38), done.
Note: checking out 'a4f337c2e4549ae5866656f8249cb21d51794f17'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

Note: This is also an issue when running on Ubuntu 16.04.4

Bad transition to task state

This happens sometimes, not sure why.

System.AggregateException: One or more errors occurred. ---> System.AggregateException: One or more errors occurred. ---> System.InvalidOperationException: An attempt was made to transition a task to a final state when it had already completed.
   at System.Threading.Tasks.TaskCompletionSource`1.SetCanceled()
   at CliWrap.Cli.<>c__DisplayClass22_1.<ExecuteAsync>b__3()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.CancellationCallbackInfo.ExecuteCallback()
   at System.Threading.CancellationTokenSource.CancellationCallbackCoreWork(CancellationCallbackCoreWorkArguments args)
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)
   --- End of inner exception stack trace ---
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)
   at System.Threading.CancellationTokenSource.NotifyCancellation(Boolean throwOnFirstException)
   at System.Threading.CancellationTokenSource.LinkedTokenCancelDelegate(Object source)
   at System.Threading.CancellationCallbackInfo.ExecuteCallback()
   at System.Threading.CancellationTokenSource.CancellationCallbackCoreWork(CancellationCallbackCoreWorkArguments args)
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)
   --- End of inner exception stack trace ---
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)
   at System.Threading.CancellationTokenSource.NotifyCancellation(Boolean throwOnFirstException)
   at System.Threading.CancellationTokenSource.TimerCallbackLogic(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.TimerQueueTimer.CallCallback()
   at System.Threading.TimerQueueTimer.Fire()
   at System.Threading.TimerQueue.FireNextTimers()
---> (Inner Exception #0) System.AggregateException: One or more errors occurred. ---> System.InvalidOperationException: An attempt was made to transition a task to a final state when it had already completed.
   at System.Threading.Tasks.TaskCompletionSource`1.SetCanceled()
   at CliWrap.Cli.<>c__DisplayClass22_1.<ExecuteAsync>b__3()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.CancellationCallbackInfo.ExecuteCallback()
   at System.Threading.CancellationTokenSource.CancellationCallbackCoreWork(CancellationCallbackCoreWorkArguments args)
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)
   --- End of inner exception stack trace ---
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)
   at System.Threading.CancellationTokenSource.NotifyCancellation(Boolean throwOnFirstException)
   at System.Threading.CancellationTokenSource.LinkedTokenCancelDelegate(Object source)
   at System.Threading.CancellationCallbackInfo.ExecuteCallback()
   at System.Threading.CancellationTokenSource.CancellationCallbackCoreWork(CancellationCallbackCoreWorkArguments args)
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)
---> (Inner Exception #0) System.InvalidOperationException: An attempt was made to transition a task to a final state when it had already completed.
   at System.Threading.Tasks.TaskCompletionSource`1.SetCanceled()
   at CliWrap.Cli.<>c__DisplayClass22_1.<ExecuteAsync>b__3()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.CancellationCallbackInfo.ExecuteCallback()
   at System.Threading.CancellationTokenSource.CancellationCallbackCoreWork(CancellationCallbackCoreWorkArguments args)
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)<---
<---

System.AggregateException: One or more errors occurred. ---> System.InvalidOperationException: An attempt was made to transition a task to a final state when it had already completed.
   at System.Threading.Tasks.TaskCompletionSource`1.SetCanceled()
   at CliWrap.Cli.<>c__DisplayClass22_1.<ExecuteAsync>b__3()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.CancellationCallbackInfo.ExecuteCallback()
   at System.Threading.CancellationTokenSource.CancellationCallbackCoreWork(CancellationCallbackCoreWorkArguments args)
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)
   --- End of inner exception stack trace ---
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)
   at System.Threading.CancellationTokenSource.NotifyCancellation(Boolean throwOnFirstException)
   at System.Threading.CancellationTokenSource.LinkedTokenCancelDelegate(Object source)
   at System.Threading.CancellationCallbackInfo.ExecuteCallback()
   at System.Threading.CancellationTokenSource.CancellationCallbackCoreWork(CancellationCallbackCoreWorkArguments args)
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)
---> (Inner Exception #0) System.InvalidOperationException: An attempt was made to transition a task to a final state when it had already completed.
   at System.Threading.Tasks.TaskCompletionSource`1.SetCanceled()
   at CliWrap.Cli.<>c__DisplayClass22_1.<ExecuteAsync>b__3()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.CancellationCallbackInfo.ExecuteCallback()
   at System.Threading.CancellationTokenSource.CancellationCallbackCoreWork(CancellationCallbackCoreWorkArguments args)
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)<---

System.InvalidOperationException: An attempt was made to transition a task to a final state when it had already completed.
   at System.Threading.Tasks.TaskCompletionSource`1.SetCanceled()
   at CliWrap.Cli.<>c__DisplayClass22_1.<ExecuteAsync>b__3()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.CancellationCallbackInfo.ExecuteCallback()
   at System.Threading.CancellationTokenSource.CancellationCallbackCoreWork(CancellationCallbackCoreWorkArguments args)
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)
System.InvalidOperationException: An attempt was made to transition a task to a final state when it had already completed.
   at System.Threading.Tasks.TaskCompletionSource`1.SetCanceled()
   at CliWrap.Cli.<>c__DisplayClass22_1.<ExecuteAsync>b__3()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.CancellationCallbackInfo.ExecuteCallback()
   at System.Threading.CancellationTokenSource.CancellationCallbackCoreWork(CancellationCallbackCoreWorkArguments args)
   at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)

also, i can be connected to other exception:

System.InvalidOperationException: An attempt was made to transition a task to a final state when it had already completed.
   at System.Threading.Tasks.TaskCompletionSource`1.SetResult(TResult result)
   at System.Diagnostics.Process.ErrorReadNotifyUser(String data)
   at System.Diagnostics.AsyncStreamReader.FlushMessageQueue()
   at System.Diagnostics.AsyncStreamReader.ReadBuffer(IAsyncResult ar)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.IO.Stream.ReadWriteTask.System.Threading.Tasks.ITaskCompletionAction.Invoke(Task completingTask)
   at System.Threading.Tasks.Task.FinishContinuations()
   at System.Threading.Tasks.Task.Finish(Boolean bUserDelegateExecuted)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)
   at System.Threading.Tasks.Task.ExecuteEntry(Boolean bPreventDoubleExecution)
   at System.Threading.ThreadPoolWorkQueue.Dispatch()

Can't execute ipconfig

Здравствуйте. С помощью вашей библиотеки я хочу выполнить запросы в консоли cmd. Но у меня не выполняются команды.
Вот пример кода:
string cmd = "ipconfig";
var result = Cli.Wrap("cmd.exe").SetArguments(cmd).Execute();
Console.WriteLine(result.StandardOutput);
Console.ReadKey();
Я хочу выполнить команду ipconfig, но результат работы я не получаю.
И поправьте меня, если я делаю что-то не так.

Deadlocking issue with lots of stdout and stderr

We're using CliWrap to invoke R scripts for data modelling (R is a stats/data analysis language). I'm using the event stream usage example that you have posted on Github like this:

           await cmd.Observe().ForEachAsync(cmdEvent => {
                    switch (cmdEvent) {
                        case StandardOutputCommandEvent stdOut:
                            sbOut.AppendLine($"{stdOut.Text}");
                            break;
                        case StandardErrorCommandEvent stdErr:
                            sbErr.AppendLine($"{stdErr.Text}");
                            break;
                        case ExitedCommandEvent exited:
                            //handle exit-code etc
                            break;
                    }
                });

All I do for StandardOutputCommandEvent and StandardErrorCommandEvent is capture the text to 2 different string builders which I dump to a log periodically so we can actually see the script progress in real time. What seems to be happening though is that something locks up and the event stream stops capturing. So I get no further output and the ExitedCommandEvent never fires so it never runs my completion and cleanup code, to my program it just appears as if it is still running. The thing is, the script actually does complete it's execution because at the end it writes out several files and I've verified that those are being written even when the code above hangs.

The other clue is that when I was testing this, it was very sporadic: sometimes it would run to completion, sometimes it would hang-up. As best I can tell the issue seems to be with high volume messages in quick succession. When I would test with a small dataset, the RScript will rip though the numbers very quickly (only 1-2 mins) and since it is quite verbose in it's messaging, you'll get a lot of output happening quickly. But with the large datasets it takes R much longer to execute each step in the script so the messages come though much slower over 30-40 mins which doesn't seem to cause an issue. Not sure if this is a deadlocking issue, but I did see a mention of that in the last release notes. Thank you

Consider maintaining changelog

Hey,
Great library you have here. I would suggest you to keep some kind of changelog for users, so we can keep up with what is new. Probably simple CHANGELOG.md would be good enough

Trouble running within Windows Service

I have been testing a Windows Service that uses CliWrap and on my DEV machine it works fine.

However, when I try copying the binaries (including all the assemblies that VS2019 included in the output), I get this stack trace:
System.MissingMethodException: Method not found: 'System.Collections.Generic.IDictionary2<System.String,System.String> System.Diagnostics.ProcessStartInfo.get_Environment()'.
at CliWrap.Command.GetStartInfo()
at CliWrap.Command.ExecuteAsync(CancellationToken cancellationToken)`

Can you help me figure out what dependency CliWrap is looking for? I can't find anything missing as far as I've looked!

FWIW: Building/Targeting .Net 4.6.1

Thanks.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.