Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Parallel.Core.Net/ServerResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -18,7 +18,7 @@ public ServerResponse(ServerRequest request)
private ServerResponse(ServerRequest request, JToken? data, bool isSuccess)
{
Request = request;
IsSuccess = isSuccess;
Success = isSuccess;
Data = data;
}

Expand Down
42 changes: 23 additions & 19 deletions Parallel.Service/RequestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,17 @@ namespace Parallel.Service
{
public class RequestHandler
{
private readonly Dictionary<string, Type> _requests;
public Dictionary<string, Type> 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");
}
}
Expand All @@ -33,37 +29,45 @@ public RequestHandler()
/// </summary>
/// <param name="request">The name of the request.</param>
/// <returns>The corresponding <see cref="IRequest"/>. If none was found a help request will be returned.</returns>
public IRequest CreateNew(ServerRequest request)
public IRequest? CreateNew(ServerRequest request)
{
if (!_requests.TryGetValue(request.Name, out Type? requestType))
Dictionary<string, string> headers = new Dictionary<string, string>(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<ValidationResult>? validationResults = new List<ValidationResult>();
ValidationContext? context = new ValidationContext(instance, serviceProvider: null, items: null);
if (!Validator.TryValidateObject(instance, context, validationResults, validateAllProperties: true))
{
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;
Expand Down
39 changes: 37 additions & 2 deletions Parallel.Service/Requests/BaseRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
37 changes: 35 additions & 2 deletions Parallel.Service/Requests/HelpRequest.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,7 +13,38 @@ public class HelpRequest : BaseRequest
{
public override Task<IResponse> ExecuteAsync()
{
throw new NotImplementedException();
RequestHandler handler = new RequestHandler();

JArray jsonArray = new JArray();
foreach (KeyValuePair<string, Type> request in handler.Requests.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase))
{
Type type = request.Value;
DescriptionAttribute? descAttr = type.GetCustomAttribute<DescriptionAttribute>();
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<RequiredAttribute>() != null
});
}

// Build JObject for this request
JObject summary = new JObject
{
["name"] = request.Key,
["description"] = description,
["parameters"] = parameters
};

jsonArray.Add(summary);
}

return Task.FromResult<IResponse>(Json(jsonArray));
}
}
}
22 changes: 0 additions & 22 deletions Parallel.Service/Requests/LoginRequest.cs

This file was deleted.

3 changes: 1 addition & 2 deletions Parallel.Service/Requests/PingRequest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2025 Kyle Ebbinga

using Parallel.Core.Net.Sockets;
using Parallel.Service.Responses;

namespace Parallel.Service.Requests
Expand All @@ -9,7 +8,7 @@ public class PingRequest : BaseRequest
{
public override Task<IResponse> ExecuteAsync()
{
return Task.FromResult<IResponse>(Success());
return Task.FromResult<IResponse>(Ok());
}
}
}
18 changes: 18 additions & 0 deletions Parallel.Service/Responses/ErrorResponse.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
1 change: 1 addition & 0 deletions Parallel.Service/Responses/IResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ namespace Parallel.Service.Responses
{
public interface IResponse
{
int Status { get; }
}
}
8 changes: 5 additions & 3 deletions Parallel.Service/Responses/MessageResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
4 changes: 3 additions & 1 deletion Parallel.Service/Responses/ObjectResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
52 changes: 41 additions & 11 deletions Parallel.Service/Services/TcpRequestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IResponse> 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<IResponse> 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<Task> StopAsync(CancellationToken cancellationToken)
Expand Down
Loading