XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Collections.Concurrent;

namespace XFEToolBox.Client.Core.Console;

/// <summary>
/// A thread-safe, single-consumer buffer that preserves console Write/WriteLine semantics.
/// </summary>
/// <typeparam name="TMetadata">Metadata needed by the output renderer.</typeparam>
public sealed class ConsoleOutputBuffer<TMetadata>
{
    private readonly ConcurrentQueue<BufferedConsoleOutput<TMetadata>> queue = new();
    private readonly object enqueueLock = new();
    private bool lastLineIsEnded = true;
    private long generation;
    private int count;

    /// <summary>
    /// Gets the number of entries waiting to be consumed.
    /// </summary>
    public int Count => Volatile.Read(ref count);

    /// <summary>
    /// Gets whether no entries are waiting to be consumed.
    /// </summary>
    public bool IsEmpty => Count == 0;

    /// <summary>
    /// Gets the current buffer generation. Clearing the buffer starts a new generation.
    /// </summary>
    public long Generation => Volatile.Read(ref generation);

    /// <summary>
    /// Enqueues output and atomically determines whether it starts a new logical line.
    /// </summary>
    public BufferedConsoleOutput<TMetadata> Enqueue(
        string text,
        TMetadata metadata,
        bool isLineEnd,
        bool forceNewLine = false) =>
        Enqueue(_ => text, metadata, isLineEnd, forceNewLine);

    /// <summary>
    /// Enqueues output using a factory that can add a prefix only when a new line starts.
    /// </summary>
    public BufferedConsoleOutput<TMetadata> Enqueue(
        Func<bool, string> textFactory,
        TMetadata metadata,
        bool isLineEnd,
        bool forceNewLine = false)
    {
        ArgumentNullException.ThrowIfNull(textFactory);

        lock (enqueueLock)
        {
            var startsNewLine = forceNewLine || lastLineIsEnded;
            var output = new BufferedConsoleOutput<TMetadata>(
                textFactory(startsNewLine),
                metadata,
                startsNewLine,
                isLineEnd,
                generation);

            queue.Enqueue(output);
            Interlocked.Increment(ref count);
            lastLineIsEnded = isLineEnd;
            return output;
        }
    }

    /// <summary>
    /// Attempts to read the next entry. This method is intended for a single consumer.
    /// </summary>
    public bool TryDequeue(out BufferedConsoleOutput<TMetadata> output)
    {
        if (!queue.TryDequeue(out output))
            return false;

        Interlocked.Decrement(ref count);
        return true;
    }

    /// <summary>
    /// Returns whether an entry belongs to the current, non-cleared generation.
    /// </summary>
    public bool IsCurrent(BufferedConsoleOutput<TMetadata> output) => output.Generation == Generation;

    /// <summary>
    /// Clears pending output, resets Write/WriteLine state, and returns discarded entries.
    /// </summary>
    public IReadOnlyList<BufferedConsoleOutput<TMetadata>> Clear()
    {
        var discarded = new List<BufferedConsoleOutput<TMetadata>>();

        lock (enqueueLock)
        {
            Interlocked.Increment(ref generation);
            lastLineIsEnded = true;

            while (queue.TryDequeue(out var output))
            {
                Interlocked.Decrement(ref count);
                discarded.Add(output);
            }
        }

        return discarded;
    }
}

/// <summary>
/// One buffered console write operation.
/// </summary>
public readonly record struct BufferedConsoleOutput<TMetadata>(
    string Text,
    TMetadata Metadata,
    bool StartsNewLine,
    bool IsLineEnd,
    long Generation);