using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; using RemoteExec.Shared; using System.Collections.Concurrent; using System.Reflection; namespace RemoteExec.Client; public partial class RemoteExecutor { private void RegisterEventHandlers(ServerConnection server) { // Unregister existing handlers to prevent duplicates UnregisterEventHandlers(server); _ = server.Connection.On("MetricsUpdated", metrics => OnMetricsUpdated(server, metrics)); _ = server.Connection.On("TaskResult", (taskId, result) => OnTaskResult(server, taskId, result)); _ = server.Connection.On("RequestAssembly", async (assemblyName, requestId) => await OnRequestAssemblyAsync(server, assemblyName, requestId)); // Store the handler so it can be unregistered later server.ClosedEventHandler = async (error) => await OnConnectionClosedAsync(server, error); server.Connection.Closed += server.ClosedEventHandler; } private static void UnregisterEventHandlers(ServerConnection server) { server.Connection.Remove("MetricsUpdated"); server.Connection.Remove("TaskResult"); server.Connection.Remove("RequestAssembly"); // Unregister the Closed event handler if it exists if (server.ClosedEventHandler != null) { server.Connection.Closed -= server.ClosedEventHandler; server.ClosedEventHandler = null; } } private void OnMetricsUpdated(ServerConnection server, ServerMetrics metrics) { server.Metrics = metrics; MetricsUpdated?.Invoke(this, new ServerMetricsUpdatedEventArgs(server.Connection, metrics)); } private void OnTaskResult(ServerConnection server, Guid taskId, RemoteExecutionResult result) { // Remove completed task from server's assigned tasks if (serverAssignedTasks.TryGetValue(server, out ConcurrentDictionary? taskDictionary)) { _ = taskDictionary.TryRemove(taskId, out _); } if (pendingResults.TryRemove(taskId, out TaskCompletionSource? tcs)) { tcs.SetResult(result); } } private async Task OnRequestAssemblyAsync(ServerConnection server, string assemblyName, Guid requestId) { Assembly? assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().FullName == assemblyName) ?? Assembly.Load(new AssemblyName(assemblyName)); if (string.IsNullOrEmpty(assembly.Location)) { logger.LogWarning("Cannot provide assembly {AssemblyName}: Location is null or empty", assemblyName); return; } byte[] dllBytes = await File.ReadAllBytesAsync(assembly.Location); ByteArrayContent content = new(dllBytes); content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream"); _ = await server.HttpClient.PostAsync($"/provide-assembly?requestId={requestId}", content); } private async Task OnConnectionClosedAsync(ServerConnection server, Exception? error) { if (error != null) { logger.LogWarning(error, "Server connection closed unexpectedly: {ServerUrl}", server.HttpClient.BaseAddress); } else { logger.LogInformation("Server connection closed gracefully: {ServerUrl}", server.HttpClient.BaseAddress); } // Requeue all tasks assigned to this server await RequeuePendingTasksForServer(server); if (!distributorCts.IsCancellationRequested) { _ = Task.Run(() => AttemptReconnectionAsync(server, distributorCts.Token)); } } }