top of page

ProcessX — Simplify call an external process with the async streams in C# 8.0.


The only way to handle external processes in C# is with the Process class, but this API is very old, and asynchronous support would require a lot of flag and boilerplate code.

So I created a library that “just throws a line of string like writing a shell,” “receives the result in a C# 8.0 asynchronous stream, which also only requires a line of await foreach,” and “handles ExitCode, StdError, etc. appropriately.

using Cysharp.Diagnostics; 
// using namespace
// async iterate.
awaitforeach (string iteminProcessX.StartAsync("dotnet --info"))
{
    Console.WriteLine(item);
 }

There are other wrapper libraries for Process. However, assuming C# 8.0 Async Streams, ProcessX makes it the simplest.

What would normally take about 30 lines to write in Process, only takes one line.

Process’s design has not changed in any way since the .NET Framework 1.0, and it is clearly outdated. It’s very difficult to use, even though it hides a half-baked, low-level process. It’s a mess, especially asynchronous processing.

var pi = new ProcessStartInfo
{
    // Separated with FileName and Arguments
    FileName ="dotnet",
    Arguments ="--info",
    // Many bool flags...
    UseShellExecute = false,
    CreateNoWindow = true,
    ErrorDialog = false,
    RedirectStandardError = true,
    RedirectStandardOutput = true,
 };

using (varprocess=newProcess()
{
    StartInfo=pi,
    // more bool flag
    EnableRaisingEvents=true
 })
 {
     process.OutputDataReceived += (sender, e) =>    
     {
     // come null terminated, require null handling
     if (e.Data != null)        
     {
         Console.WriteLine(e.Data);        
     }    
     };
     process.Exited+= (sender, e) =>    
     {
         // In many cases, when Exited is called, OutputDataReceived is 
             still being loaded
         // So if you're handling with Exit, you'll need a proper         
             standby code here    
     };
     
     process.Start();
     // You need to call this explicitly after Start
     process.BeginOutputReadLine();
     
     // If you touch anything Process-related after Process disposes, 
     you die.
     // But if you do WaitForExit, it is same as synchronous,
     // so to make it truly asynchronous, you'll need to work on it 
     from here
     // process.WaitForExit();
     }

Now, the nice thing about await foreach is that you can use try-catch for exception handling as is, so if ExitCode is non-zero (or if you receive a StdError), ProcessErrorException will be raised.

try
{
    await foreach (variteminProcessX.StartAsync("dotnet --foo --bar")) 
    { }
}
catch (ProcessErrorExceptionex)
{
    // int .ExitCode
    // string[] .ErrorOutput
    Console.WriteLine(ex.ToString());
}

If you want to wait for synchronization and get all the results, like WaitForExit, you can use ToTask.

// receive buffered result(similar as WaitForExit).
string[] result=await ProcessX.StartAsync("dotnet --info").ToTask();

As for cancellation, you can still use WithCancellation for asynchronous streams. If the process is still intact when canceling, kill it with the cancellation to ensure it’s killed.

awaitforeach (variteminProcessX.StartAsync
    ("dotnet --info").WithCancellation(cancellationToken))
    {
        Console.WriteLine(item);
     }

The timeout can be used because the CancelationTokenSource itself has the option of firing with time.

using (varcts = newCancellationTokenSource(TimeSpan.FromSeconds(1)))
{
    awaitforeach (variteminProcessX.StartAsync
        ("dotnet --info").WithCancellation(cts.Token))    
    {
        Console.WriteLine(item);    
        }}

We also have a ProcessX.StartAsync overload that allows you to set the working directory, environment variables, and encoding, so you should be able to implement most things without problems.

StartAsync(string command, 
    string ? workingDirectory=null, 
    IDictionary<string, string>?environmentVariable=null, 
    Encoding ? encoding=null)
StartAsync(string fileName, 
    string? arguments, 
    string? workingDirectory=null, 
    IDictionary<string, string>?environmentVariable=null, 
    Encoding? encoding=null)
StartAsync(ProcessStartInfoprocessStartInfo)

Task<string[]>ToTask(CancellationTokencancellationToken=default)

Process itself is the C# 1.0 generation (10 years ago!). It’s worthwhile to provide a properly designed modern library again. Both the design techniques and the language itself are far more advanced, so a proper update is necessary. We offer a library called ConsoleAppFramework, which is also a modern interpretation of CLI tools/command line parsing. We will be the ones to evolve C# in the .NET Core era


Source : Medium


The Tech Platform

0 comments
bottom of page