377 lines
13 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|