From bc86a1ba44c9508509b0965beee5aa74fde62cd3 Mon Sep 17 00:00:00 2001 From: Kyle | Guitar <26614720+TheGuitarleader@users.noreply.github.com> Date: Tue, 19 Aug 2025 04:38:37 -0500 Subject: [PATCH] Requests to the service are ready to go. Code should adjust for system parameters --- Parallel.Core.Net/ServerResponse.cs | 4 +- Parallel.Service/RequestHandler.cs | 42 ++++++++------- Parallel.Service/Requests/BaseRequest.cs | 39 +++++++++++++- Parallel.Service/Requests/HelpRequest.cs | 37 ++++++++++++- Parallel.Service/Requests/LoginRequest.cs | 22 -------- Parallel.Service/Requests/PingRequest.cs | 3 +- Parallel.Service/Responses/ErrorResponse.cs | 18 +++++++ Parallel.Service/Responses/IResponse.cs | 1 + Parallel.Service/Responses/MessageResponse.cs | 8 +-- Parallel.Service/Responses/ObjectResponse.cs | 4 +- .../Services/TcpRequestService.cs | 52 +++++++++++++++---- 11 files changed, 166 insertions(+), 64 deletions(-) delete mode 100644 Parallel.Service/Requests/LoginRequest.cs create mode 100644 Parallel.Service/Responses/ErrorResponse.cs diff --git a/Parallel.Core.Net/ServerResponse.cs b/Parallel.Core.Net/ServerResponse.cs index 78e90a9..d3b6a99 100644 --- a/Parallel.Core.Net/ServerResponse.cs +++ b/Parallel.Core.Net/ServerResponse.cs @@ -7,7 +7,7 @@ namespace Parallel.Core.Net.Connections public class ServerResponse { public ServerRequest Request { get; } - public bool IsSuccess { get; } = false; + public bool Success { get; } = false; public JToken? Data { get; } public ServerResponse(ServerRequest request) @@ -18,7 +18,7 @@ public ServerResponse(ServerRequest request) private ServerResponse(ServerRequest request, JToken? data, bool isSuccess) { Request = request; - IsSuccess = isSuccess; + Success = isSuccess; Data = data; } diff --git a/Parallel.Service/RequestHandler.cs b/Parallel.Service/RequestHandler.cs index 118dcf5..a121c6e 100644 --- a/Parallel.Service/RequestHandler.cs +++ b/Parallel.Service/RequestHandler.cs @@ -9,21 +9,17 @@ namespace Parallel.Service { public class RequestHandler { - private readonly Dictionary _requests; + public Dictionary Requests { get; } public RequestHandler() { Type[] types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).Where(t => typeof(BaseRequest).IsAssignableFrom(t) && !t.IsAbstract).ToArray(); - _requests = types.ToDictionary(t => t.Name.Replace("Request", ""), t => t, StringComparer.OrdinalIgnoreCase); + Requests = types.ToDictionary(t => t.Name.Replace("Request", ""), t => t, StringComparer.OrdinalIgnoreCase); - // Logs if all requests registered. - if (_requests.Count == types.Length) + // Logs if any requests failed + if (Requests.Count != types.Length) { - Log.Information($"Successfully registered all {types.Length} requests"); - } - else - { - int remaining = types.Length - _requests.Count; + int remaining = types.Length - Requests.Count; Log.Warning($"Failed to register {remaining} requests"); } } @@ -33,29 +29,37 @@ public RequestHandler() /// /// The name of the request. /// The corresponding . If none was found a help request will be returned. - public IRequest CreateNew(ServerRequest request) + public IRequest? CreateNew(ServerRequest request) { - if (!_requests.TryGetValue(request.Name, out Type? requestType)) + Dictionary headers = new Dictionary(request.Parameters, StringComparer.OrdinalIgnoreCase); + if (!Requests.TryGetValue(request.Name, out Type? requestType)) { Log.Warning($"Unknown command: {request.Name}"); - throw new InvalidOperationException($"Unknown command: {request.Name}"); + return null; } // Instantiate the request object object? instance = Activator.CreateInstance(requestType); - if (instance is not IRequest requestInstance) - throw new InvalidOperationException($"Type '{requestType.Name}' does not implement IRequest."); + if (instance is not IRequest requestInstance) return null; // Map parameters to object properties - foreach (PropertyInfo? prop in requestType.GetProperties()) + foreach (PropertyInfo prop in requestType.GetProperties()) { - if (request.Parameters.TryGetValue(prop.Name, out string? value)) + if (headers.TryGetValue(prop.Name, out string? value)) { - object? converted = Convert.ChangeType(value, prop.PropertyType); - prop.SetValue(instance, converted); + try + { + object? converted = Convert.ChangeType(value, prop.PropertyType); + prop.SetValue(instance, converted); + } + catch (Exception ex) + { + Log.Warning($"Failed to convert '{value}' to {prop.PropertyType.Name} for property '{prop.Name}': {ex.Message}"); + } } } + // Validate required properties List? validationResults = new List(); ValidationContext? context = new ValidationContext(instance, serviceProvider: null, items: null); @@ -63,7 +67,7 @@ public IRequest CreateNew(ServerRequest request) { string? errors = string.Join("; ", validationResults.Select(r => r.ErrorMessage)); Log.Warning($"Validation failed for '{request.Name}': {errors}"); - throw new InvalidOperationException($"Validation failed: {errors}"); + return null; } return requestInstance; diff --git a/Parallel.Service/Requests/BaseRequest.cs b/Parallel.Service/Requests/BaseRequest.cs index 52dfe05..f515432 100644 --- a/Parallel.Service/Requests/BaseRequest.cs +++ b/Parallel.Service/Requests/BaseRequest.cs @@ -20,9 +20,44 @@ public virtual void Dispose() GC.SuppressFinalize(this); } - protected ObjectResponse Success() + public static MessageResponse Ok() { - return new ObjectResponse("Success"); + return new MessageResponse("Success", 200); + } + + public static MessageResponse Ok(string message) + { + return new MessageResponse(message, 200); + } + + public static ObjectResponse Json(object data) + { + return new ObjectResponse(data, 200); + } + + public static MessageResponse BadRequest(string message) + { + return new MessageResponse(message, 401); + } + + public static MessageResponse Unauthorized() + { + return new MessageResponse("Unauthorized", 401); + } + + public static MessageResponse Forbidden() + { + return new MessageResponse("Forbidden", 403); + } + + public static ErrorResponse InternalServerError(Exception exception) + { + return new ErrorResponse(exception, 500); + } + + public static MessageResponse NotImplemented() + { + return new MessageResponse("Function not implemented", 501); } } } \ No newline at end of file diff --git a/Parallel.Service/Requests/HelpRequest.cs b/Parallel.Service/Requests/HelpRequest.cs index 18c28f4..7d4ee43 100644 --- a/Parallel.Service/Requests/HelpRequest.cs +++ b/Parallel.Service/Requests/HelpRequest.cs @@ -1,7 +1,9 @@ // Copyright 2025 Kyle Ebbinga using System.ComponentModel; -using Parallel.Core.Net.Sockets; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using Newtonsoft.Json.Linq; using Parallel.Service.Responses; namespace Parallel.Service.Requests @@ -11,7 +13,38 @@ public class HelpRequest : BaseRequest { public override Task ExecuteAsync() { - throw new NotImplementedException(); + RequestHandler handler = new RequestHandler(); + + JArray jsonArray = new JArray(); + foreach (KeyValuePair request in handler.Requests.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase)) + { + Type type = request.Value; + DescriptionAttribute? descAttr = type.GetCustomAttribute(); + string description = descAttr?.Description ?? "No description provided."; + + JArray parameters = new JArray(); + foreach (PropertyInfo prop in type.GetProperties()) + { + parameters.Add(new JObject + { + ["name"] = prop.Name, + ["type"] = prop.PropertyType.Name, + ["required"] = prop.GetCustomAttribute() != null + }); + } + + // Build JObject for this request + JObject summary = new JObject + { + ["name"] = request.Key, + ["description"] = description, + ["parameters"] = parameters + }; + + jsonArray.Add(summary); + } + + return Task.FromResult(Json(jsonArray)); } } } \ No newline at end of file diff --git a/Parallel.Service/Requests/LoginRequest.cs b/Parallel.Service/Requests/LoginRequest.cs deleted file mode 100644 index 20e4f75..0000000 --- a/Parallel.Service/Requests/LoginRequest.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using Parallel.Core.Net.Sockets; -using Parallel.Service.Responses; - -namespace Parallel.Service.Requests -{ - [Description("Logins into the server.")] - public class LoginRequest : BaseRequest - { - [Required] public string Username { get; set; } - - [Required] public string Password { get; set; } - - public override Task ExecuteAsync() - { - return Task.FromResult(Success()); - } - } -} \ No newline at end of file diff --git a/Parallel.Service/Requests/PingRequest.cs b/Parallel.Service/Requests/PingRequest.cs index 643ef2a..ea918b6 100644 --- a/Parallel.Service/Requests/PingRequest.cs +++ b/Parallel.Service/Requests/PingRequest.cs @@ -1,6 +1,5 @@ // Copyright 2025 Kyle Ebbinga -using Parallel.Core.Net.Sockets; using Parallel.Service.Responses; namespace Parallel.Service.Requests @@ -9,7 +8,7 @@ public class PingRequest : BaseRequest { public override Task ExecuteAsync() { - return Task.FromResult(Success()); + return Task.FromResult(Ok()); } } } \ No newline at end of file diff --git a/Parallel.Service/Responses/ErrorResponse.cs b/Parallel.Service/Responses/ErrorResponse.cs new file mode 100644 index 0000000..a986445 --- /dev/null +++ b/Parallel.Service/Responses/ErrorResponse.cs @@ -0,0 +1,18 @@ +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Service.Responses +{ + public class ErrorResponse : IResponse + { + public int Status { get; } + public string? Exception { get; } + public string Message { get; } + + public ErrorResponse(Exception exception, int status) + { + Status = status; + Exception = exception.GetType().FullName; + Message = exception.Message; + } + } +} \ No newline at end of file diff --git a/Parallel.Service/Responses/IResponse.cs b/Parallel.Service/Responses/IResponse.cs index 84473ba..f48de5e 100644 --- a/Parallel.Service/Responses/IResponse.cs +++ b/Parallel.Service/Responses/IResponse.cs @@ -4,5 +4,6 @@ namespace Parallel.Service.Responses { public interface IResponse { + int Status { get; } } } \ No newline at end of file diff --git a/Parallel.Service/Responses/MessageResponse.cs b/Parallel.Service/Responses/MessageResponse.cs index 5f6c383..c7773b4 100644 --- a/Parallel.Service/Responses/MessageResponse.cs +++ b/Parallel.Service/Responses/MessageResponse.cs @@ -2,13 +2,15 @@ namespace Parallel.Service.Responses { - public class MessageResponse + public class MessageResponse : IResponse { - public string Message { get; set; } + public string Message { get; } + public int Status { get; } - public MessageResponse(string message) + public MessageResponse(string message, int status) { Message = message; + Status = status; } } } \ No newline at end of file diff --git a/Parallel.Service/Responses/ObjectResponse.cs b/Parallel.Service/Responses/ObjectResponse.cs index 20ff8ce..1e3577b 100644 --- a/Parallel.Service/Responses/ObjectResponse.cs +++ b/Parallel.Service/Responses/ObjectResponse.cs @@ -4,10 +4,12 @@ namespace Parallel.Service.Responses { public sealed class ObjectResponse : IResponse { + public int Status { get; } public object? Data { get; } - public ObjectResponse(object? data) + public ObjectResponse(object? data, int status) { + Status = status; Data = data; } } diff --git a/Parallel.Service/Services/TcpRequestService.cs b/Parallel.Service/Services/TcpRequestService.cs index ceabb94..7b45152 100644 --- a/Parallel.Service/Services/TcpRequestService.cs +++ b/Parallel.Service/Services/TcpRequestService.cs @@ -57,23 +57,53 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) private void StartHandlingRequests(Socket socket, CancellationToken token) { TcpSocketHandler handler = new(socket); - Task handlerTask = Task.Run(() => AcceptRequestAsync(handler).ContinueWith(t => + Task handleTask = AcceptRequestAsync(handler); + Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(30), token); + + Task wrappedTask = Task.Run(async () => { - t.Dispose(); - }, token), token); + Task completed = await Task.WhenAny(handleTask, timeoutTask); + IResponse response; + + if (completed == handleTask) + { + try + { + response = await handleTask; + } + catch (OperationCanceledException) + { + _logger.LogInformation($"[{handler.RemoteEndPoint}]: Request cancelled."); + response = new MessageResponse("Request cancelled", 503); + } + catch (Exception ex) + { + _logger.LogError(ex, $"[{handler.RemoteEndPoint}]: Handler failed."); + response = new ErrorResponse(ex, 500); + } + } + else + { + _logger.LogWarning($"[{handler.RemoteEndPoint}]: Timed out after 30 seconds."); + response = new MessageResponse("Request timed out", 408); + } + + await handler.RespondAsync(response); + handler.Close(); + }, token); - _requestPool.Add(handlerTask); + _requestPool.Add(wrappedTask); } - private async Task AcceptRequestAsync(ISocketHandler handler) + private async Task AcceptRequestAsync(ISocketHandler handler) { - ServerRequest request = handler.Parse(); - Log.Debug($"Received request '{handler.RawData}' from '{handler.RemoteEndPoint}' ({_requestPool.Count} active request{(_requestPool.Count == 1 ? string.Empty : "s")})"); - IRequest requestInstance = _requests.CreateNew(request); + ServerRequest? request = handler.Parse(); + if (request == null) return new MessageResponse("Unable to parse request", 401); - IResponse response = await requestInstance.ExecuteAsync(); - await handler.RespondAsync(response); - Log.Debug($"Responding to '{handler.RemoteEndPoint}' with '{JsonConvert.SerializeObject(response)}' ({_requestPool.Count} active request{(_requestPool.Count == 1 ? string.Empty : "s")})"); + Log.Debug($"Received request '{handler.RawData}' from '{handler.RemoteEndPoint}' ({_requestPool.Count} active request{(_requestPool.Count == 1 ? string.Empty : "s")})"); + IRequest? requestInstance = _requests.CreateNew(request); + if (requestInstance == null) return new MessageResponse("Required fields are missing", 401); + return await requestInstance.ExecuteAsync(); } public override async Task StopAsync(CancellationToken cancellationToken)