Last active
December 15, 2022 07:08
-
-
Save ycherkes/bec5f8b08f8cefdb147c6b373cb6eafa to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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