Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class BackwardsCompatProgramBase
{
public static async Task<int> Main()
{
NonBlockingConsoleWriter.Install();
using var cancellationTokenSource = new CancellationTokenSource(SettingsHelper.GetTestTimeout());
using var registration = cancellationTokenSource.Token.Register(() =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
using System.Text;
using System.Threading;

namespace Halibut.TestUtils.SampleProgram.Base.LogUtils
{
/// <summary>
/// The test reads our stdout through a pipe. If it stops reading, a blocking Console.Write
/// freezes Halibut mid-exchange, so writes are queued and dropped when the queue is full.
/// </summary>
public class NonBlockingConsoleWriter : TextWriter

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude made this, seems legit.

If we hate it in the future, we can simply delete it and end up in a slightly worse place if we decide we don't want to maintain this.

{
const int MaxQueuedWrites = 10_000;

readonly TextWriter destination;
readonly BlockingCollection<string> queue = new(MaxQueuedWrites);
long droppedWrites;

NonBlockingConsoleWriter(Stream destination)
{
this.destination = new StreamWriter(destination, new UTF8Encoding(false)) { AutoFlush = false };
new Thread(Drain) { IsBackground = true, Name = "NonBlockingConsoleWriter" }.Start();
}

public static void Install()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not a big deal, but if the process exists, nothing will drain what we currently have in th equeue.

We could hook into the AppDomain.CurrentDomain.ProcessExit (and maybe UnhandledException), and then call queue.CompleteAdding() and wait for the drain thread with a short timeout.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to consider overriding FlushAsync as well, as this is called in the console app. Even if all it does is comment that we rely on the thread.

{
Console.SetOut(new NonBlockingConsoleWriter(OpenStandardStream(1, Console.OpenStandardOutput)));
Console.SetError(new NonBlockingConsoleWriter(OpenStandardStream(2, Console.OpenStandardError)));
}

// On Unix the console stream locks Console.Out while writing, so a stalled pipe would
// still block every Console.WriteLine. Write to the raw file descriptor instead.
static Stream OpenStandardStream(int fileDescriptor, Func<Stream> openConsoleStream)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return openConsoleStream();
return new FileStream(new SafeFileHandle((IntPtr)fileDescriptor, ownsHandle: false), FileAccess.Write, 1);
}

public override Encoding Encoding => destination.Encoding;

public override void Write(char value) => Enqueue(value.ToString());

public override void Write(string? value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider overriding Write(char[], int, int) as well for completeness.

{
if (value is not null) Enqueue(value);
}

public override void WriteLine(string? value) => Enqueue(value + new string(CoreNewLine));

void Enqueue(string value)
{
if (!queue.TryAdd(value)) Interlocked.Increment(ref droppedWrites);
}

void Drain()
{
foreach (var value in queue.GetConsumingEnumerable())
{
var dropped = Interlocked.Exchange(ref droppedWrites, 0);
if (dropped > 0) destination.WriteLine($"[NonBlockingConsoleWriter dropped {dropped} writes]");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we write the drops here, then the text will appear well after the drop actually happened (likely 10000 lines out of place).

We could do this closer to when it happened, when the TryAdd succeeds and dropppedWrites is > 0.


destination.Write(value);
if (queue.Count == 0) destination.Flush();
}
}
}
}