CtrEditor/Services/DebugConsoleServer.cs

377 lines
13 KiB
C#

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
{
/// <summary>
/// Servidor TCP que captura y retransmite mensajes de debug console
/// Permite al MCP Proxy escuchar los logs de debug de CtrEditor
/// </summary>
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<string> _messageQueue = new ConcurrentQueue<string>();
private readonly ConcurrentBag<TcpClient> _connectedClients = new ConcurrentBag<TcpClient>();
// 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();
}
/// <summary>
/// Inicia el servidor de debug console
/// </summary>
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;
}
}
/// <summary>
/// Detiene el servidor de debug console
/// </summary>
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");
}
}
/// <summary>
/// Acepta conexiones de clientes
/// </summary>
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}");
}
}
}
/// <summary>
/// Maneja un cliente conectado
/// </summary>
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");
}
}
/// <summary>
/// Procesa la cola de mensajes y los envía a todos los clientes conectados
/// </summary>
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}");
}
}
}
/// <summary>
/// Envía un mensaje a todos los clientes conectados
/// </summary>
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<TcpClient>();
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
}
/// <summary>
/// Envía un mensaje a un cliente específico
/// </summary>
private async Task SendMessageToClientAsync(NetworkStream stream, string message)
{
var data = Encoding.UTF8.GetBytes(message);
await stream.WriteAsync(data, 0, data.Length);
await stream.FlushAsync();
}
/// <summary>
/// Agrega un mensaje a la cola para transmisión
/// </summary>
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();
}
}
}
}
/// <summary>
/// Limpia el buffer de mensajes para evitar acumulación excesiva
/// </summary>
public void FlushMessageBuffer()
{
lock (_bufferLock)
{
// Mantener solo los últimos 100 mensajes
var messagesToKeep = new List<string>();
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}");
}
}
/// <summary>
/// Obtiene estadísticas del buffer de mensajes
/// </summary>
public (int messageCount, int maxBufferSize) GetBufferStats()
{
lock (_bufferLock)
{
return (_messageCount, MAX_BUFFER_SIZE);
}
}
public void Dispose()
{
Stop();
_cancellationTokenSource?.Dispose();
}
}
/// <summary>
/// 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
/// </summary>
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);
}
}
}