Skip to content

Instantly share code, notes, and snippets.

@ycherkes
Last active December 15, 2022 07:08
Show Gist options
  • Save ycherkes/bec5f8b08f8cefdb147c6b373cb6eafa to your computer and use it in GitHub Desktop.
Save ycherkes/bec5f8b08f8cefdb147c6b373cb6eafa to your computer and use it in GitHub Desktop.
using Medallion.Shell;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
// Correct behavior
// Sends CTRL+C signal via external signaler process because
// Medallion creates a process with the option CreateNoWindow = true
// So the new process doesn't share the console with the parent
await RunAndTerminateGracefullyWithExternalSignaler();
// Wrong behavior
// Sends CTRL+C signal via builtin (in-proc) signaler
// As a result, both child processes received a CTRL+C event
// and therefore terminated
await RunAndTerminateGracefullyWithBuiltinSignaler();
async Task RunAndTerminateGracefullyWithExternalSignaler()
{
var cmd1 = Command.Run("ping", "-t", "142.251.208.174");
var pipeStdOutTask1 = Task.Run(async () =>
{
using var reader = new StreamReader(cmd1.StandardOutput.BaseStream, Console.OutputEncoding, false, 1024, true);
await foreach (var line in reader.ReadAllLinesAsync())
{
Console.WriteLine($"child {cmd1.ProcessId}: {line}");
}
});
var cmd2 = Command.Run("ping", "-t", "142.251.208.174");
var pipeStdOutTask2 = Task.Run(async () =>
{
using var reader = new StreamReader(cmd2.StandardOutput.BaseStream, Console.OutputEncoding, false, 1024, true);
await foreach (var line in reader.ReadAllLinesAsync())
{
Console.WriteLine($"child {cmd2.ProcessId}: {line}");
}
});
await Task.Delay(6000);
await cmd1.TrySignalAsync(CommandSignal.ControlC);
await Task.Delay(10000);
await cmd2.TrySignalAsync(CommandSignal.ControlC);
await Task.WhenAll(pipeStdOutTask1, pipeStdOutTask2);
await Task.WhenAll(cmd1.Task, cmd2.Task);
}
async Task RunAndTerminateGracefullyWithBuiltinSignaler()
{
using var process1 = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "ping",
Arguments = "-t 142.251.208.174", // google.com
UseShellExecute = false,
RedirectStandardOutput = true,
// Uncomment the line below to get separate termination working
// CreateNoWindow = true
}
};
using var process2 = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "ping",
Arguments = "-t 142.251.208.174", // google.com
UseShellExecute = false,
RedirectStandardOutput = true,
// Uncomment the line below to get separate termination working
// CreateNoWindow = true
}
};
process1.OutputDataReceived += (_, e) => Console.WriteLine($"child {process1.Id}: {e.Data}");
process1.Start();
process1.BeginOutputReadLine();
process2.OutputDataReceived += (_, e) => Console.WriteLine($"child {process2.Id}: {e.Data}");
process2.Start();
process2.BeginOutputReadLine();
Command.TryAttachToProcess(process1.Id, (opts) => { }, out var cmd1);
Command.TryAttachToProcess(process2.Id, (opts) => { }, out var cmd2);
await Task.Delay(6000);
await cmd1.TrySignalAsync(CommandSignal.ControlC);
await Task.Delay(10000);
await cmd2.TrySignalAsync(CommandSignal.ControlC);
await Task.WhenAll(cmd1.Task, cmd2.Task);
}
internal static class StreamExtensions
{
public static async IAsyncEnumerable<string> ReadAllLinesAsync(
this StreamReader reader,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var stringBuilder = new StringBuilder();
using var buffer = MemoryPool<char>.Shared.Rent(1024);
// Following sequences are treated as individual linebreaks:
// - \r
// - \n
// - \r\n
// Even though \r and \n are linebreaks on their own, \r\n together
// should not yield two lines. To ensure that, we keep track of the
// previous char and check if it's part of a sequence.
var prevSeqChar = (char?)null;
int charsRead;
while ((charsRead = await reader.ReadAsync(buffer.Memory, cancellationToken).ConfigureAwait(false)) > 0)
{
for (var i = 0; i < charsRead; i++)
{
var curChar = buffer.Memory.Span[i];
// If current char and last char are part of a line break sequence,
// skip over the current char and move on.
// The buffer was already yielded in the previous iteration, so there's
// nothing left to do.
if (prevSeqChar == '\r' && curChar == '\n')
{
prevSeqChar = null;
continue;
}
// If current char is \n or \r, yield the buffer (even if it is empty)
if (curChar is '\n' or '\r')
{
yield return stringBuilder.ToString();
stringBuilder.Clear();
}
// For any other char, just append it to the buffer
else
{
stringBuilder.Append(curChar);
}
prevSeqChar = curChar;
}
}
// Yield what's remaining in the buffer
if (stringBuilder.Length > 0)
yield return stringBuilder.ToString();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment