To start a process and get the output, it might be simple.

(Do NOT COPY!!! Wrong code!!!)

        var process = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = bin,
                Arguments = arg,
                CreateNoWindow = true,
                WindowStyle = ProcessWindowStyle.Hidden,
                WorkingDirectory = path,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
            }
        };
        process.Start();

        await process.WaitForExitAsync();
        var output = process.StandardOutput.ReadToEnd();
        var error = process.StandardError.ReadToEnd();
        return (process.ExitCode, output, error);
    }

Those code might be working. But when I test it with the following code:

    [TestMethod]
    public async Task TestLargeOutput()
    {
        var service = new CommandService();
        var testDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
        _ = await service.RunCommandAsync("git", "clone https://github.com/ediwang/moonglade.git --bare --filter=tree:0 .", testDirectory);
        var (code, output, error) = await service.RunCommandAsync("git", "--no-pager log --pretty=format:\"%H\" --max-count=2000", testDirectory);
        Assert.AreEqual(0, code);
        Assert.IsTrue(string.IsNullOrEmpty(error));
        // Total Lines:
        Assert.AreEqual(2000, output.Split('\n').Length);
        
        // Clean
        FolderDeleter.DeleteByForce(testDirectory);
    }

It never quits! The test keeps running until timeout!

How could this be?

The output stream of a process needs to be consumed

The key issue is that the output stream of a process needs to be consumed. In the above code, my program is waiting for the process to exit, which is not a problem. However, at this moment, if the process generates a large amount of output, this output will accumulate in the standard output stream without anyone reading it.

Based on my testing, the buffer of the standard output stream is only 4KB. Once the 4KB buffer is full, the program cannot continue writing to the standard output stream. Therefore, Git will be stuck in an infinite wait. To get it to continue and exit, you just need to touch its output stream using the following command:

ps -aux | grep git
cd /proc/1234
cd fd
cat 1
cat 2

The above code will read its output stream and empty the buffer, allowing the program to behave normally.

This means that the correct way to run a process is to keep reading it's output stream.

Modify to get the correct code

public class CommandService
{
    public async Task<(int code, string output, string error)> RunCommandAsync(string bin, string arg, string path,
        TimeSpan? timeout = null)
    {
        if (!Directory.Exists(path)) Directory.CreateDirectory(path);
        timeout ??= TimeSpan.FromMinutes(2);

        var process = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = bin,
                Arguments = arg,
                CreateNoWindow = true,
                WindowStyle = ProcessWindowStyle.Hidden,
                WorkingDirectory = path,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
            }
        };
        process.Start();

        var outputMemoryStream = new MemoryStream();
        var errorMemoryStream = new MemoryStream();
        var programTask = Task.WhenAll(
            process.StandardOutput.BaseStream.CopyToAsync(outputMemoryStream),
            process.StandardError.BaseStream.CopyToAsync(errorMemoryStream), 
            process.WaitForExitAsync()
        );
        await Task.WhenAny(
            Task.Delay(timeout.Value),
            programTask);
        if (!programTask.IsCompleted)
        {
            throw new TimeoutException($@"Execute command: {bin} {arg} at {path} was time out! Timeout is {timeout}.");
        }

        var output = Encoding.UTF8.GetString(outputMemoryStream.ToArray());
        var error = Encoding.UTF8.GetString(errorMemoryStream.ToArray());
        return (process.ExitCode, output, error);
    }
}

Now we keep reading the streams while waiting for the program to exit.

Let's test it!

using System.ComponentModel;
using System.Runtime.InteropServices;
using Aiursoft.CSTools.Services;
using Aiursoft.CSTools.Tools;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Aiursoft.CSTools.Tests.Services;

[TestClass]
public class CommandServiceTests
{
    private readonly string _testCommand =
        RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "-n 2 baidu.com" : "-c 2 baidu.com";
    
    [TestMethod]
    public async Task TestPing()
    {
        var service = new CommandService();
        var (code, output, error) = await service.RunCommandAsync("ping", _testCommand, Environment.CurrentDirectory);
        Assert.IsTrue(output.Contains("baidu.com"));
        Assert.IsTrue(string.IsNullOrEmpty(error));
        Assert.AreEqual(0, code);
    }
    
    [TestMethod]
    public async Task TestProgramNotExist()
    {
        var service = new CommandService();
        await Assert.ThrowsExceptionAsync<Win32Exception>(async () =>
        {
            await service.RunCommandAsync("notexist", string.Empty, Environment.CurrentDirectory);
        });
    }
    
    [TestMethod]
    public async Task TestProgramTimeout()
    {
        var service = new CommandService();
        await Assert.ThrowsExceptionAsync<TimeoutException>(async () =>
        {
            await service.RunCommandAsync("ping", _testCommand, Environment.CurrentDirectory, TimeSpan.FromMilliseconds(1));
        });
    }
    
    [TestMethod]
    public async Task TestProgramError()
    {
        var service = new CommandService();
        var (code, output, error) = await service.RunCommandAsync("ping", "-n 2 notexist", Environment.CurrentDirectory);
        Assert.IsTrue(output.ToLower().Contains("ping") || error.ToLower().Contains("ping")); 
        Assert.IsTrue(code > 0);
    }
    
    [TestMethod]
    public async Task TestLargeOutput()
    {
        var service = new CommandService();
        var testDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
        _ = await service.RunCommandAsync("git", "clone https://github.com/ediwang/moonglade.git --bare --filter=tree:0 .", testDirectory);
        var (code, output, error) = await service.RunCommandAsync("git", "--no-pager log --pretty=format:\"%H\" --max-count=2000", testDirectory);
        Assert.AreEqual(0, code);
        Assert.IsTrue(string.IsNullOrEmpty(error));
        // Total Lines:
        Assert.AreEqual(2000, output.Split('\n').Length);
        
        // Clean
        FolderDeleter.DeleteByForce(testDirectory);
    }
}

file

Use the code above from nuget

Of course you can use my way to run a process from Nuget!

Download it here https://www.nuget.org/packages/Aiursoft.CSTools

Or run:

dotnet add package Aiursoft.CSTools --version 7.0.6