using System; using System.Collections.Concurrent; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace CtrEditor.Services { /// /// Servidor TCP que captura y retransmite mensajes de debug console /// Permite al MCP Proxy escuchar los logs de debug de CtrEditor /// public class DebugConsoleServer : IDisposable { private readonly int _port; private TcpListener _tcpListener; private bool _isRunning; private CancellationTokenSource _cancellationTokenSource; private readonly object _lockObject = new object(); private readonly ConcurrentQueue _messageQueue = new ConcurrentQueue(); private readonly ConcurrentBag _connectedClients = new ConcurrentBag(); // Custom TraceListener para capturar Debug.WriteLine private DebugTraceListener _traceListener; // Control de buffer para evitar acumulación excesiva private const int MAX_BUFFER_SIZE = 1000; private int _messageCount = 0; private readonly object _bufferLock = new object(); public DebugConsoleServer(int port = 5007) { _port = port; _cancellationTokenSource = new CancellationTokenSource(); } /// /// Inicia el servidor de debug console /// public void Start() { if (_isRunning) return; try { _tcpListener = new TcpListener(IPAddress.Loopback, _port); _tcpListener.Start(); _isRunning = true; // Instalar el trace listener personalizado solo para Trace // Nota: Debug.Listeners no existe en .NET Core/.NET 8 _traceListener = new DebugTraceListener(this); Trace.Listeners.Add(_traceListener); Trace.WriteLine($"[Debug Console Server] Servidor de debug iniciado en puerto {_port}"); // Procesar conexiones en background _ = Task.Run(async () => await AcceptConnectionsAsync(_cancellationTokenSource.Token)); // Procesar cola de mensajes _ = Task.Run(async () => await ProcessMessageQueueAsync(_cancellationTokenSource.Token)); } catch (Exception ex) { Debug.WriteLine($"[Debug Console Server] Error al iniciar servidor: {ex.Message}"); throw; } } /// /// Detiene el servidor de debug console /// public void Stop() { if (!_isRunning) return; lock (_lockObject) { if (!_isRunning) return; _isRunning = false; _cancellationTokenSource?.Cancel(); // Remover el trace listener de Trace if (_traceListener != null) { Trace.Listeners.Remove(_traceListener); _traceListener = null; } _tcpListener?.Stop(); // Cerrar todas las conexiones de clientes foreach (var client in _connectedClients) { try { client?.Close(); } catch { } } Debug.WriteLine("[Debug Console Server] Servidor de debug detenido"); } } /// /// Acepta conexiones de clientes /// private async Task AcceptConnectionsAsync(CancellationToken cancellationToken) { try { while (_isRunning && !cancellationToken.IsCancellationRequested) { try { // Implementar timeout y cancelación manual para evitar freeze var acceptTask = _tcpListener.AcceptTcpClientAsync(); var delayTask = Task.Delay(1000, cancellationToken); // Check every 1s var completedTask = await Task.WhenAny(acceptTask, delayTask); if (completedTask == acceptTask && !cancellationToken.IsCancellationRequested) { var tcpClient = await acceptTask; _connectedClients.Add(tcpClient); Debug.WriteLine("[Debug Console Server] Cliente debug conectado"); // Manejar cliente en background _ = Task.Run(async () => await HandleClientAsync(tcpClient, cancellationToken)); } else if (cancellationToken.IsCancellationRequested) { break; } // Si completedTask == delayTask, simplemente continúa el loop para verificar estado } catch (ObjectDisposedException) { // TcpListener fue cerrado, salir silenciosamente break; } catch (Exception ex) { Debug.WriteLine($"[Debug Console Server] Error aceptando conexión: {ex.Message}"); if (!_isRunning) break; await Task.Delay(1000, cancellationToken); // Esperar antes de reintentar } } } catch (OperationCanceledException) { // Cancelación normal, no hacer nada } catch (Exception ex) { if (_isRunning) { Debug.WriteLine($"[Debug Console Server] Error en AcceptConnectionsAsync: {ex.Message}"); } } } /// /// Maneja un cliente conectado /// private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken) { try { var stream = client.GetStream(); // Enviar mensaje de bienvenida var welcomeMessage = $"[Debug Console Server] Conectado al servidor de debug de CtrEditor en puerto {_port}\n"; await SendMessageToClientAsync(stream, welcomeMessage); // Mantener conexión viva hasta cancelación while (_isRunning && client.Connected && !cancellationToken.IsCancellationRequested) { await Task.Delay(1000, cancellationToken); } } catch (Exception ex) { Debug.WriteLine($"[Debug Console Server] Error en cliente debug: {ex.Message}"); } finally { try { client?.Close(); } catch { } Debug.WriteLine("[Debug Console Server] Cliente debug desconectado"); } } /// /// Procesa la cola de mensajes y los envía a todos los clientes conectados /// private async Task ProcessMessageQueueAsync(CancellationToken cancellationToken) { try { while (_isRunning && !cancellationToken.IsCancellationRequested) { if (_messageQueue.TryDequeue(out string message)) { lock (_bufferLock) { _messageCount = Math.Max(0, _messageCount - 1); } await BroadcastMessageAsync(message); } else { await Task.Delay(50, cancellationToken); // Pequeña pausa si no hay mensajes } } } catch (Exception ex) { if (_isRunning) { Debug.WriteLine($"[Debug Console Server] Error procesando cola de mensajes: {ex.Message}"); } } } /// /// Envía un mensaje a todos los clientes conectados /// private async Task BroadcastMessageAsync(string message) { var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); var formattedMessage = $"[{timestamp}] {message}\n"; var clientsToRemove = new List(); foreach (var client in _connectedClients) { try { if (client.Connected) { var stream = client.GetStream(); await SendMessageToClientAsync(stream, formattedMessage); } else { clientsToRemove.Add(client); } } catch { clientsToRemove.Add(client); } } // Remover clientes desconectados // Nota: ConcurrentBag no permite eliminación directa, pero no es crítico // Los clientes desconectados se manejan en HandleClientAsync } /// /// Envía un mensaje a un cliente específico /// private async Task SendMessageToClientAsync(NetworkStream stream, string message) { var data = Encoding.UTF8.GetBytes(message); await stream.WriteAsync(data, 0, data.Length); await stream.FlushAsync(); } /// /// Agrega un mensaje a la cola para transmisión /// public void QueueMessage(string message) { if (_isRunning) { lock (_bufferLock) { _messageQueue.Enqueue(message); _messageCount++; // Si el buffer se está llenando demasiado, hacer un flush automático if (_messageCount > MAX_BUFFER_SIZE) { FlushMessageBuffer(); } } } } /// /// Limpia el buffer de mensajes para evitar acumulación excesiva /// public void FlushMessageBuffer() { lock (_bufferLock) { // Mantener solo los últimos 100 mensajes var messagesToKeep = new List(); var count = 0; while (_messageQueue.TryDequeue(out string message) && count < 100) { messagesToKeep.Insert(0, message); // Insertar al inicio para mantener orden count++; } // Limpiar completamente la cola while (_messageQueue.TryDequeue(out _)) { } // Volver a agregar los mensajes que queremos mantener foreach (var message in messagesToKeep) { _messageQueue.Enqueue(message); } _messageCount = messagesToKeep.Count; Debug.WriteLine($"[Debug Console Server] Buffer flushed. Messages kept: {_messageCount}"); } } /// /// Obtiene estadísticas del buffer de mensajes /// public (int messageCount, int maxBufferSize) GetBufferStats() { lock (_bufferLock) { return (_messageCount, MAX_BUFFER_SIZE); } } public void Dispose() { Stop(); _cancellationTokenSource?.Dispose(); } } /// /// TraceListener personalizado que captura mensajes de Trace.WriteLine /// y los envía al servidor de debug console /// Nota: Debug.WriteLine no está disponible en .NET Core/.NET 8 /// internal class DebugTraceListener : TraceListener { private readonly DebugConsoleServer _server; public DebugTraceListener(DebugConsoleServer server) { _server = server; } public override void Write(string message) { _server?.QueueMessage(message); } public override void WriteLine(string message) { _server?.QueueMessage(message); } } }