| File: Interaction\ConsoleLogBufferContext.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Aspire.Cli.Interaction; /// <summary> /// Shared context that buffers console log output while interactive prompts are active. /// Registered as a singleton so the logger provider and interaction service share the same state. /// </summary> internal sealed class ConsoleLogBufferContext { // Cap the buffer to prevent unbounded memory growth when a prompt stays open // during heavy logging. Once the cap is reached, oldest messages are dropped. internal const int MaxBufferedMessages = 1000; // Guards prompt depth, buffer state, and direct writes. Holding the lock during // I/O is acceptable because Console.Error is already internally serialized and // each write is a single short log line (sub-millisecond). private readonly object _logBufferLock = new(); private readonly Queue<(TextWriter Writer, string Message)> _bufferedMessages = new(); private int _interactivePromptDepth; /// <summary> /// Begins an interactive prompt scope. Logs are buffered until the outermost scope ends. /// </summary> internal IDisposable BeginInteractivePromptScope() { lock (_logBufferLock) { _interactivePromptDepth++; } return new InteractivePromptScope(this); } /// <summary> /// Writes the message immediately if no prompt scope is active, otherwise buffers it. /// The decision and write are performed atomically under the same lock so no log line /// can slip into an active prompt window. /// </summary> internal void WriteOrBuffer(TextWriter output, string message) { lock (_logBufferLock) { if (_interactivePromptDepth > 0) { if (_bufferedMessages.Count >= MaxBufferedMessages) { // Drop the oldest message to stay within the cap. _bufferedMessages.Dequeue(); } _bufferedMessages.Enqueue((output, message)); return; } output.WriteLine(message); } } private void EndInteractivePromptScope() { lock (_logBufferLock) { if (_interactivePromptDepth > 0) { _interactivePromptDepth--; } if (_interactivePromptDepth > 0) { return; } // Flush all buffered messages under the lock. This is acceptable because // each message is a single short log line (sub-millisecond write) and // Console.Error is already internally serialized. Writing under lock // eliminates the need for a flush loop or _isFlushing flag entirely. while (_bufferedMessages.Count > 0) { var (writer, msg) = _bufferedMessages.Dequeue(); writer.WriteLine(msg); } } } private sealed class InteractivePromptScope(ConsoleLogBufferContext context) : IDisposable { private int _disposed; public void Dispose() { // Ensure scope close is applied only once for idempotent disposal. if (Interlocked.Exchange(ref _disposed, 1) == 0) { context.EndInteractivePromptScope(); } } } }