using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace io.github.hatayama.uLoopMCP
{
// Related classes:
// - McpServerController: Manages the lifecycle of this server.
// - UnityCommandExecutor: Executes commands received from clients.
// - JsonRpcProcessor: Handles JSON-RPC 2.0 message processing.
///
/// Represents a connected client
///
public class ConnectedClient
{
public readonly string Endpoint;
public readonly string ClientName;
public readonly DateTime ConnectedAt;
public readonly int Port;
public readonly NetworkStream Stream;
public ConnectedClient(string endpoint, NetworkStream stream, int port, string clientName = McpConstants.UNKNOWN_CLIENT_NAME)
{
Endpoint = endpoint;
Stream = stream; // Allow null stream for UI display purposes
ClientName = clientName;
ConnectedAt = DateTime.Now;
Port = port;
}
// Private constructor for WithClientName to preserve ConnectedAt
private ConnectedClient(string endpoint, NetworkStream stream, int port, string clientName, DateTime connectedAt)
{
Endpoint = endpoint;
Stream = stream; // Allow null stream for UI display purposes
ClientName = clientName;
ConnectedAt = connectedAt;
Port = port;
}
public ConnectedClient WithClientName(string clientName)
{
return new ConnectedClient(Endpoint, Stream, Port, clientName, ConnectedAt);
}
}
///
/// Unity MCP Bridge TCP/IP Server.
/// Accepts connections from the TypeScript MCP Server and handles JSON-RPC 2.0 communication.
///
public class McpBridgeServer : IDisposable
{
// Note: Domain reload progress is now tracked via McpSessionManager
// Events for server lifecycle notifications
public static event System.Action OnServerStopping;
public static event System.Action OnServerStarted;
// Events for individual tool management
public static event System.Action OnToolConnected;
public static event System.Action OnToolDisconnected;
public static event System.Action OnAllToolsCleared;
// Fired from thread pool when ServerLoopAsync exits while _isRunning is still true.
// Subscribers must marshal to main thread before accessing Unity APIs.
public static event System.Action OnServerLoopExited;
// HResult error codes for normal disconnection detection
private static readonly HashSet NormalDisconnectionHResults = new()
{
unchecked((int)0x800703E3), // ERROR_OPERATION_ABORTED
unchecked((int)0x80070040), // ERROR_NETNAME_DELETED
unchecked((int)0x80072745), // ERROR_CONNECTION_ABORTED
unchecked((int)0x80072746) // ERROR_CONNECTION_RESET
};
private TcpListener _tcpListener;
private CancellationTokenSource _cancellationTokenSource;
private Task _serverTask;
private readonly ConcurrentDictionary _clientTasks = new();
// Read from thread pool (ServerLoopAsync), written from main thread (StopServer)
private volatile bool _isRunning = false;
// Guard against concurrent cleanup from ServerLoopAsync finally + external disposal
private int _unexpectedExitCleanupStarted = 0;
// Client management for broadcasting notifications
private readonly ConcurrentDictionary _connectedClients = new();
///
/// Whether the server is running.
///
public bool IsRunning => _isRunning;
///
/// The server's port number.
///
public int Port { get; private set; } = McpEditorSettings.GetCustomPort();
///
/// Event on client connection.
///
public event Action OnClientConnected;
///
/// Event on client disconnection.
///
public event Action OnClientDisconnected;
///
/// Event on error.
///
public event Action OnError;
///
/// Generate unique client key using Endpoint
///
private string GenerateClientKey(string endpoint)
{
// Use endpoint as unique identifier
return endpoint;
}
///
/// Get list of connected clients sorted by name
///
public IReadOnlyCollection GetConnectedClients()
{
return _connectedClients.Values.OrderBy(client => client.ClientName).ToArray();
}
///
/// Update client name for a connected client
///
public void UpdateClientName(string clientEndpoint, string clientName)
{
// Find client by endpoint (backward compatibility)
ConnectedClient targetClient = _connectedClients.Values
.FirstOrDefault(c => c.Endpoint == clientEndpoint);
if (targetClient != null)
{
string clientKey = GenerateClientKey(targetClient.Endpoint);
ConnectedClient updatedClient = targetClient.WithClientName(clientName);
bool updateResult = _connectedClients.TryUpdate(clientKey, updatedClient, targetClient);
// Clear reconnecting flags when client name is successfully set (client is now fully connected)
if (updateResult && clientName != McpConstants.UNKNOWN_CLIENT_NAME)
{
McpServerController.ClearReconnectingFlag();
// Notify tool connected
OnToolConnected?.Invoke(updatedClient);
}
}
}
///
/// Checks if the specified port is in use.
/// Delegates to NetworkUtility for consistent port checking behavior.
///
/// The port number to check.
/// True if the port is in use.
public static bool IsPortInUse(int port)
{
return NetworkUtility.IsPortInUse(port);
}
///
/// Starts the server.
///
///
/// The port number to bind to. Use -1 to fall back to the saved custom port
/// from . Defaults to -1.
///
public void StartServer(int port = -1, bool clearServerStartingLockWhenReady = true)
{
if (_isRunning)
{
return;
}
Port = port == -1 ? McpEditorSettings.GetCustomPort() : port;
_cancellationTokenSource = new CancellationTokenSource();
_unexpectedExitCleanupStarted = 0;
try
{
_tcpListener = new TcpListener(IPAddress.Loopback, Port);
_tcpListener.Start();
_isRunning = true;
TcpListener tcpListener = _tcpListener;
CancellationToken cancellationToken = _cancellationTokenSource.Token;
_serverTask = Task.Run(() => ServerLoopAsync(tcpListener, cancellationToken));
// Safety net: log if the server task faults unexpectedly.
// Primary detection is in ServerLoopAsync's finally block; this catches unhandled exceptions in Task.Run itself.
_serverTask.ContinueWith(task =>
{
if (task.IsFaulted)
{
VibeLogger.LogError(
"server_task_faulted",
$"Server task faulted unexpectedly: {task.Exception?.GetBaseException().Message}",
new { exceptionType = task.Exception?.GetBaseException().GetType().Name }
);
}
}, TaskScheduler.Default);
// Server is now ready to accept connections - clean up compilation/reload locks.
CompilationLockService.DeleteLockFile();
DomainReloadDetectionService.DeleteLockFile();
if (clearServerStartingLockWhenReady)
{
ServerStartingLockService.DeleteLockFile();
}
// Notify that server has started
OnServerStarted?.Invoke();
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
{
_isRunning = false;
string errorMessage = $"Port {Port} is already in use. Please choose a different port.";
OnError?.Invoke(errorMessage);
throw new InvalidOperationException(errorMessage, ex);
}
catch (Exception ex)
{
_isRunning = false;
string errorMessage = $"Failed to start MCP Server: {ex.Message}";
OnError?.Invoke(errorMessage);
throw;
}
}
///
/// Stops the server.
///
public void StopServer()
{
StopServerCore(ServerStopMode.Normal);
}
///
/// Domain reload can suspend editor-thread continuations before the server task observes shutdown.
///
public void StopServerBeforeDomainReload()
{
StopServerCore(ServerStopMode.BeforeDomainReload);
}
private void StopServerCore(ServerStopMode mode)
{
bool wasRunning = _isRunning;
CancellationTokenSource cancellationTokenSource = Interlocked.Exchange(ref _cancellationTokenSource, null);
TcpListener tcpListener = Interlocked.Exchange(ref _tcpListener, null);
Task serverTask = mode == ServerStopMode.BeforeDomainReload
? _serverTask
: Interlocked.Exchange(ref _serverTask, null);
if (!wasRunning &&
cancellationTokenSource == null &&
tcpListener == null &&
serverTask == null)
{
return;
}
if (wasRunning && mode != ServerStopMode.UnexpectedLoopExit)
{
ServerShutdownReason shutdownReason = McpEditorSettings.GetIsDomainReloadInProgress()
? ServerShutdownReason.DomainReload
: ServerShutdownReason.EditorQuit;
SendShutdownNotification(shutdownReason);
OnServerStopping?.Invoke();
}
_isRunning = false;
DisconnectAllClientsCore(notifyLifecycleEvents: mode != ServerStopMode.UnexpectedLoopExit);
cancellationTokenSource?.Cancel();
tcpListener?.Stop();
if (mode == ServerStopMode.BeforeDomainReload)
{
return;
}
if (mode == ServerStopMode.UnexpectedLoopExit)
{
cancellationTokenSource?.Dispose();
return;
}
serverTask?.Wait(TimeSpan.FromSeconds(McpServerConfig.SHUTDOWN_TIMEOUT_SECONDS));
DisposeCancellationTokenSourceAfterClientTasks(cancellationTokenSource);
}
private void DisposeCancellationTokenSourceAfterClientTasks(
CancellationTokenSource cancellationTokenSource)
{
Task[] clientTasks = _clientTasks.Values.ToArray();
if (clientTasks.Length == 0)
{
cancellationTokenSource?.Dispose();
return;
}
if (MainThreadSwitcher.IsMainThread)
{
_ = ObserveClientTasksThenDisposeCancellationTokenSourceAsync(
clientTasks,
cancellationTokenSource);
return;
}
WaitForClientTasksToComplete(clientTasks);
cancellationTokenSource?.Dispose();
}
private static void WaitForClientTasksToComplete(Task[] clientTasks)
{
Task completionTask = CreateObservedClientTaskCompletionTask(clientTasks);
bool completed = completionTask.Wait(TimeSpan.FromSeconds(McpServerConfig.SHUTDOWN_TIMEOUT_SECONDS));
if (!completed)
{
LogClientTaskShutdownTimeout(clientTasks.Length);
}
}
private static async Task ObserveClientTasksThenDisposeCancellationTokenSourceAsync(
Task[] clientTasks,
CancellationTokenSource cancellationTokenSource)
{
try
{
Task completionTask = CreateObservedClientTaskCompletionTask(clientTasks);
Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(McpServerConfig.SHUTDOWN_TIMEOUT_SECONDS));
Task finishedTask = await Task.WhenAny(completionTask, timeoutTask);
if (!ReferenceEquals(finishedTask, completionTask))
{
LogClientTaskShutdownTimeout(clientTasks.Length);
}
}
finally
{
cancellationTokenSource?.Dispose();
}
}
private static Task CreateObservedClientTaskCompletionTask(Task[] clientTasks)
{
return Task.WhenAll(clientTasks).ContinueWith(task =>
{
if (task.IsFaulted)
{
_ = task.Exception;
}
}, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
private static void LogClientTaskShutdownTimeout(int activeClientTasks)
{
VibeLogger.LogWarning(
"client_tasks_shutdown_timeout",
"Timed out waiting for client handlers to finish during normal server shutdown.",
new { activeClientTasks }
);
}
///
/// Explicitly disconnect all connected clients
/// This ensures TypeScript clients receive proper close events
///
private void DisconnectAllClients()
{
DisconnectAllClientsCore(notifyLifecycleEvents: true);
}
///
/// OnAllToolsCleared subscribers (UI components) require the main thread.
/// Background-thread callers (CleanupAfterUnexpectedLoopExit) must suppress
/// lifecycle events to avoid cross-thread Unity API violations.
///
private void DisconnectAllClientsCore(bool notifyLifecycleEvents)
{
if (_connectedClients.IsEmpty)
{
return;
}
List clientsToRemove = new List();
foreach (KeyValuePair client in _connectedClients)
{
try
{
// Close the NetworkStream to send proper close event to TypeScript client
if (client.Value.Stream != null && client.Value.Stream.CanWrite)
{
client.Value.Stream.Close();
}
clientsToRemove.Add(client.Key);
}
catch (Exception ex)
{
OnError?.Invoke($"Error disconnecting client {client.Key}: {ex.Message}");
clientsToRemove.Add(client.Key); // Remove even if disconnect failed
}
}
// Remove all clients from the connected clients list
foreach (string clientKey in clientsToRemove)
{
_connectedClients.TryRemove(clientKey, out _);
}
// Lifecycle events require main thread — only fire when explicitly requested
if (notifyLifecycleEvents && !McpEditorSettings.GetIsDomainReloadInProgress())
{
OnAllToolsCleared?.Invoke();
}
}
///
/// StopServer() guards on _isRunning==true, but by the time this runs _isRunning may already
/// be false or the normal shutdown path may race with the finally block.
/// A separate cleanup path that skips the _isRunning guard is needed.
/// Lifecycle events are deferred to OnServerLoopExited → EditorApplication.delayCall
/// because this runs on the thread pool where Unity APIs are unsafe.
///
private void CleanupAfterUnexpectedLoopExit()
{
if (Interlocked.Exchange(ref _unexpectedExitCleanupStarted, 1) != 0)
{
return;
}
StopServerCore(ServerStopMode.UnexpectedLoopExit);
}
///
/// The server's main loop.
///
private async Task ServerLoopAsync(TcpListener listener, CancellationToken cancellationToken)
{
System.Diagnostics.Debug.Assert(listener != null, "TcpListener must be captured before starting the server loop.");
try
{
while (!cancellationToken.IsCancellationRequested && _isRunning)
{
try
{
TcpClient client = await AcceptTcpClientAsync(listener, cancellationToken);
if (client != null)
{
string clientEndpoint = client.Client.RemoteEndPoint?.ToString() ?? McpServerConfig.UNKNOWN_CLIENT_ENDPOINT;
OnClientConnected?.Invoke(clientEndpoint);
Task clientTask = Task.Run(() => HandleClientAsync(client, cancellationToken));
TrackClientTask(clientTask);
}
}
catch (ObjectDisposedException)
{
// Expected when StopServer() disposes TcpListener while accept is pending.
// If _isRunning is still true here, this is an unexpected disposal — finally block handles state cleanup.
if (_isRunning)
{
VibeLogger.LogWarning(
"server_loop_disposed_while_running",
"TcpListener disposed while server was still marked as running. Exiting loop."
);
}
break;
}
catch (ThreadAbortException ex)
{
// Log and re-throw ThreadAbortException
if (!McpEditorSettings.GetIsDomainReloadInProgress())
{
OnError?.Invoke($"Unexpected thread abort: {ex.Message}");
}
throw;
}
catch (Exception ex)
{
if (!cancellationToken.IsCancellationRequested)
{
string errorMessage = $"Server loop error: {ex.Message}";
OnError?.Invoke(errorMessage);
}
}
}
}
finally
{
bool wasUnexpectedExit = ShouldTreatLoopExitAsUnexpected(_isRunning, cancellationToken);
if (wasUnexpectedExit)
{
VibeLogger.LogWarning(
"server_loop_unexpected_exit",
"ServerLoopAsync exited while _isRunning was still true. Cleaning up and triggering recovery.",
new { cancellationRequested = cancellationToken.IsCancellationRequested }
);
CleanupAfterUnexpectedLoopExit();
OnServerLoopExited?.Invoke();
}
}
}
private static bool ShouldTreatLoopExitAsUnexpected(
bool isRunning,
CancellationToken cancellationToken)
{
// A canceled loop belongs to an intentional shutdown and must not recover a newer server.
return isRunning && !cancellationToken.IsCancellationRequested;
}
///
/// Asynchronously accepts a client from the TcpListener.
///
private async Task AcceptTcpClientAsync(TcpListener listener, CancellationToken cancellationToken)
{
try
{
return await AcceptTcpClientAsyncCore(listener, cancellationToken);
}
catch (ThreadAbortException ex)
{
// Log and re-throw ThreadAbortException
if (!McpEditorSettings.GetIsDomainReloadInProgress())
{
OnError?.Invoke($"Unexpected thread abort: {ex.Message}");
}
throw;
}
catch (OperationCanceledException)
{
return null;
}
}
internal static Task AcceptTcpClientAsyncForTests(
TcpListener listener,
CancellationToken cancellationToken)
{
return AcceptTcpClientAsyncCore(listener, cancellationToken);
}
internal int GetActiveClientTaskCountForTests()
{
return _clientTasks.Count;
}
internal static bool ShouldTreatLoopExitAsUnexpectedForTests(
bool isRunning,
CancellationToken cancellationToken)
{
return ShouldTreatLoopExitAsUnexpected(isRunning, cancellationToken);
}
internal void TrackClientTaskForTests(Task clientTask)
{
TrackClientTask(clientTask);
}
private static async Task AcceptTcpClientAsyncCore(
TcpListener listener,
CancellationToken cancellationToken)
{
System.Diagnostics.Debug.Assert(listener != null, "TcpListener must be available while accepting clients.");
if (listener == null || cancellationToken.IsCancellationRequested)
{
return null;
}
using CancellationTokenRegistration cancellationRegistration =
cancellationToken.Register(() => listener.Stop());
try
{
return await listener.AcceptTcpClientAsync();
}
catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested)
{
return null;
}
catch (SocketException) when (cancellationToken.IsCancellationRequested)
{
return null;
}
catch (InvalidOperationException) when (cancellationToken.IsCancellationRequested)
{
return null;
}
}
private void TrackClientTask(Task clientTask)
{
System.Diagnostics.Debug.Assert(clientTask != null, "Client handler task must be created before tracking.");
if (clientTask == null)
{
return;
}
bool added = _clientTasks.TryAdd(clientTask.Id, clientTask);
System.Diagnostics.Debug.Assert(added, "Client handler task id should be unique while tracking.");
clientTask.ContinueWith(completedTask =>
{
_clientTasks.TryRemove(completedTask.Id, out _);
if (completedTask.IsFaulted)
{
Exception exception = completedTask.Exception?.GetBaseException();
OnError?.Invoke($"Client handler task faulted: {exception?.Message}");
}
}, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
///
/// Handles communication with the client using Content-Length framing.
///
private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
{
string clientEndpoint = client.Client.RemoteEndPoint?.ToString() ?? McpServerConfig.UNKNOWN_CLIENT_ENDPOINT;
// Initialize new components for Content-Length framing
DynamicBufferManager bufferManager = null;
MessageReassembler messageReassembler = null;
try
{
using (client)
using (NetworkStream stream = client.GetStream())
{
// Check for existing connection from same endpoint and close it
string clientKey = GenerateClientKey(clientEndpoint);
if (_connectedClients.TryGetValue(clientKey, out ConnectedClient existingClient))
{
existingClient.Stream?.Close();
_connectedClients.TryRemove(clientKey, out _);
// Notify tool disconnected
OnToolDisconnected?.Invoke(existingClient.ClientName);
}
// Add new client to connected clients for notification broadcasting
int clientPort = (client.Client.RemoteEndPoint as System.Net.IPEndPoint)?.Port ?? 0;
ConnectedClient connectedClient = new ConnectedClient(clientEndpoint, stream, clientPort);
_connectedClients.TryAdd(clientKey, connectedClient);
// Initialize new framing components
bufferManager = new DynamicBufferManager();
messageReassembler = new MessageReassembler(bufferManager);
// Start with initial buffer size
byte[] buffer = bufferManager.GetBuffer(BufferConfig.INITIAL_BUFFER_SIZE);
while (!cancellationToken.IsCancellationRequested && client.Connected)
{
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
if (bytesRead == 0)
{
break; // Client disconnected.
}
// Add received data to message reassembler
messageReassembler.AddData(buffer, bytesRead);
// Extract any complete messages
string[] completeJsonMessages = messageReassembler.ExtractCompleteMessages();
foreach (string requestJson in completeJsonMessages)
{
if (string.IsNullOrWhiteSpace(requestJson)) continue;
// JSON-RPC processing and response sending with client context
string responseJson = await JsonRpcProcessor.ProcessRequest(requestJson, clientEndpoint);
// Only send response if it's not null (notifications return null)
if (!string.IsNullOrEmpty(responseJson))
{
// Check stream and client state before attempting write
if (!stream.CanWrite || !client.Connected || cancellationToken.IsCancellationRequested)
{
return; // Skip the write operation
}
// Send response with Content-Length framing
string framedResponse = CreateContentLengthFrame(responseJson);
byte[] responseData = Encoding.UTF8.GetBytes(framedResponse);
await stream.WriteAsync(responseData, 0, responseData.Length, cancellationToken);
}
}
// Validate reassembler state and clear if needed
if (!messageReassembler.ValidateState())
{
}
}
}
}
catch (ThreadAbortException)
{
// Treat as normal behavior if a domain reload is in progress.
// No need to log thread aborts during domain reload
}
catch (OperationCanceledException)
{
// Normal cancellation during server shutdown or domain reload
// No logging needed as this is expected behavior during Unity Editor operations
}
catch (IOException ex)
{
// I/O errors are usually normal disconnections - only log as info instead of warning
if (IsNormalDisconnectionException(ex))
{
// Log normal disconnections as info level
}
}
catch (Exception ex)
{
OnError?.Invoke(ex.Message);
}
finally
{
// Dispose of framing components
try
{
messageReassembler?.Dispose();
bufferManager?.Dispose();
}
catch (Exception ex)
{
OnError?.Invoke($"Error during client disposal: {ex.Message}");
}
// Remove client from connected clients list
// Find client by endpoint to get the correct key
ConnectedClient clientToRemove = _connectedClients.Values
.FirstOrDefault(c => c.Endpoint == clientEndpoint);
if (clientToRemove != null)
{
string clientKey = GenerateClientKey(clientToRemove.Endpoint);
_connectedClients.TryRemove(clientKey, out _);
// Notify tool disconnected
OnToolDisconnected?.Invoke(clientToRemove.ClientName);
}
client.Close();
OnClientDisconnected?.Invoke(clientEndpoint);
}
}
///
/// Sends a pre-formatted JSON-RPC notification to all connected clients using Content-Length framing.
///
/// The complete JSON-RPC notification string
public void SendNotificationToClients(string notificationJson)
{
if (_connectedClients.IsEmpty)
{
return;
}
// Frame the notification with Content-Length header
string framedNotification = CreateContentLengthFrame(notificationJson);
byte[] notificationData = Encoding.UTF8.GetBytes(framedNotification);
SendNotificationDataAsync(notificationData).Forget();
}
///
/// Sends a shutdown notification to all connected clients.
/// This should be called before disconnecting clients so TypeScript side can
/// differentiate between domain reload (temporary) and editor quit (permanent).
///
/// The reason for server shutdown
public void SendShutdownNotification(ServerShutdownReason reason)
{
if (_connectedClients.IsEmpty)
{
return;
}
// Create JSON-RPC 2.0 notification with shutdown reason
string notificationJson = $"{{\"jsonrpc\":\"2.0\",\"method\":\"notifications/server/shutdown\",\"params\":{{\"reason\":\"{reason}\"}}}}";
// Frame the notification with Content-Length header
string framedNotification = CreateContentLengthFrame(notificationJson);
byte[] notificationData = Encoding.UTF8.GetBytes(framedNotification);
// Send synchronously to ensure it's sent before connection closes
SendNotificationDataSync(notificationData);
}
///
/// Send notification data to all connected clients synchronously.
/// Used for shutdown notifications to ensure delivery before connection closes.
///
private void SendNotificationDataSync(byte[] notificationData)
{
foreach (KeyValuePair client in _connectedClients)
{
try
{
if (client.Value.Stream?.CanWrite == true)
{
client.Value.Stream.Write(notificationData, 0, notificationData.Length);
client.Value.Stream.Flush();
}
}
catch (Exception)
{
// Ignore errors during shutdown notification - client may already be disconnected
}
}
}
///
/// Send notification data to all connected clients
///
private async Task SendNotificationDataAsync(byte[] notificationData)
{
List clientsToRemove = new List();
foreach (KeyValuePair client in _connectedClients)
{
try
{
if (client.Value.Stream?.CanWrite == true)
{
await client.Value.Stream.WriteAsync(notificationData, 0, notificationData.Length);
}
else
{
clientsToRemove.Add(client.Key);
}
}
catch (Exception ex)
{
OnError?.Invoke($"Error writing notification to client {client.Key}: {ex.Message}");
clientsToRemove.Add(client.Key);
}
}
// Remove disconnected clients
foreach (string clientKey in clientsToRemove)
{
if (_connectedClients.TryRemove(clientKey, out ConnectedClient removedClient))
{
// Notify tool disconnected
OnToolDisconnected?.Invoke(removedClient.ClientName);
}
}
}
///
/// Creates a Content-Length framed message for JSON-RPC 2.0 communication.
///
/// The JSON content to frame
/// The framed message with Content-Length header
private string CreateContentLengthFrame(string jsonContent)
{
if (string.IsNullOrEmpty(jsonContent))
{
return string.Empty;
}
// Calculate content length in bytes (UTF-8 encoding)
int contentLength = Encoding.UTF8.GetByteCount(jsonContent);
// Create the framed message: Content-Length: \r\n\r\n
return $"Content-Length: {contentLength}\r\n\r\n{jsonContent}";
}
///
/// Determines if the given exception represents a normal client disconnection.
///
/// The exception to evaluate
/// True if the exception represents a normal disconnection, false otherwise
private static bool IsNormalDisconnectionException(Exception ex)
{
switch (ex)
{
case SocketException sockEx:
return sockEx.SocketErrorCode is SocketError.ConnectionReset or
SocketError.ConnectionAborted or
SocketError.OperationAborted or
SocketError.Shutdown or
SocketError.NotConnected;
case ObjectDisposedException:
return true;
case IOException ioEx when ioEx.InnerException is SocketException innerSockEx:
return innerSockEx.SocketErrorCode is SocketError.ConnectionReset or
SocketError.ConnectionAborted or
SocketError.OperationAborted or
SocketError.Shutdown or
SocketError.NotConnected;
case IOException ioEx:
// Check HResult codes for common disconnection scenarios
return NormalDisconnectionHResults.Contains(ioEx.HResult) ||
IsNormalDisconnectionByInnerException(ioEx);
default:
return false;
}
}
///
/// Recursively checks inner exceptions for normal disconnection scenarios
///
/// The exception to check
/// True if any inner exception indicates a normal disconnection
private static bool IsNormalDisconnectionByInnerException(Exception ex)
{
Exception innerEx = ex.InnerException;
while (innerEx != null)
{
if (IsNormalDisconnectionException(innerEx))
{
return true;
}
innerEx = innerEx.InnerException;
}
return false;
}
///
/// Releases resources.
///
public void Dispose()
{
StopServer();
}
private enum ServerStopMode
{
Normal,
BeforeDomainReload,
UnexpectedLoopExit
}
}
}