mirror of
https://github.com/Stone-Red-Code/RemoteExec.git
synced 2026-09-06 07:46:30 +02:00
Add authentication and improve server side configuration
This commit is contained in:
@@ -22,21 +22,13 @@ public partial class RemoteExecutor : IAsyncDisposable
|
|||||||
private CancellationTokenSource distributorCts = new();
|
private CancellationTokenSource distributorCts = new();
|
||||||
private Task? distributorTask;
|
private Task? distributorTask;
|
||||||
|
|
||||||
private readonly RemoteExecutorOptions options = new();
|
private readonly RemoteExecutorOptions options;
|
||||||
private readonly ILogger logger;
|
private readonly ILogger logger;
|
||||||
|
|
||||||
private bool disposedValue;
|
private bool disposedValue;
|
||||||
|
|
||||||
public event EventHandler<ServerMetricsUpdatedEventArgs>? MetricsUpdated;
|
public event EventHandler<ServerMetricsUpdatedEventArgs>? MetricsUpdated;
|
||||||
|
|
||||||
public RemoteExecutor(string url) : this([url], new RemoteExecutorOptions(), NullLogger.Instance)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public RemoteExecutor(string url, ILogger logger) : this([url], new RemoteExecutorOptions(), logger)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public RemoteExecutor(string url, Action<RemoteExecutorOptions> configure) : this([url], NullLogger.Instance, configure)
|
public RemoteExecutor(string url, Action<RemoteExecutorOptions> configure) : this([url], NullLogger.Instance, configure)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -45,14 +37,6 @@ public partial class RemoteExecutor : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public RemoteExecutor(string[] urls) : this(urls, new RemoteExecutorOptions(), NullLogger.Instance)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public RemoteExecutor(string[] urls, ILogger logger) : this(urls, new RemoteExecutorOptions(), logger)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public RemoteExecutor(string[] urls, Action<RemoteExecutorOptions> configure) : this(urls, NullLogger.Instance, configure)
|
public RemoteExecutor(string[] urls, Action<RemoteExecutorOptions> configure) : this(urls, NullLogger.Instance, configure)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -81,13 +65,21 @@ public partial class RemoteExecutor : IAsyncDisposable
|
|||||||
|
|
||||||
private void InitializeServers(string[] urls)
|
private void InitializeServers(string[] urls)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(options.ApiKey))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("API Key is required. Configure it in RemoteExecutorOptions.");
|
||||||
|
}
|
||||||
|
|
||||||
foreach (string url in urls)
|
foreach (string url in urls)
|
||||||
{
|
{
|
||||||
Uri baseUri = new(url);
|
Uri baseUri = new(url);
|
||||||
Uri signalRUri = new(baseUri, "/remote");
|
Uri signalRUri = new(baseUri, "/remote");
|
||||||
|
|
||||||
HubConnection connection = new HubConnectionBuilder()
|
HubConnection connection = new HubConnectionBuilder()
|
||||||
.WithUrl(signalRUri)
|
.WithUrl(signalRUri, httpOptions =>
|
||||||
|
{
|
||||||
|
httpOptions.Headers["X-API-Key"] = options.ApiKey;
|
||||||
|
})
|
||||||
.ConfigureLogging(logging =>
|
.ConfigureLogging(logging =>
|
||||||
{
|
{
|
||||||
_ = logging.AddProvider(new RemoteExecLoggerProvider(logger));
|
_ = logging.AddProvider(new RemoteExecLoggerProvider(logger));
|
||||||
@@ -99,6 +91,8 @@ public partial class RemoteExecutor : IAsyncDisposable
|
|||||||
BaseAddress = baseUri
|
BaseAddress = baseUri
|
||||||
};
|
};
|
||||||
|
|
||||||
|
httpClient.DefaultRequestHeaders.Add("X-API-Key", options.ApiKey);
|
||||||
|
|
||||||
ServerConnection serverConnection = new ServerConnection(connection, httpClient);
|
ServerConnection serverConnection = new ServerConnection(connection, httpClient);
|
||||||
servers.Add(serverConnection);
|
servers.Add(serverConnection);
|
||||||
serverAssignedTasks[serverConnection] = new ConcurrentDictionary<Guid, PendingTask>();
|
serverAssignedTasks[serverConnection] = new ConcurrentDictionary<Guid, PendingTask>();
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ public class RemoteExecutorOptions
|
|||||||
{
|
{
|
||||||
public ILoadBalancingStrategy Strategy { get; set; } = new ResourceAwareStrategy();
|
public ILoadBalancingStrategy Strategy { get; set; } = new ResourceAwareStrategy();
|
||||||
|
|
||||||
|
public string ApiKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
// How long to wait for a result before throwing a TimeoutException
|
// How long to wait for a result before throwing a TimeoutException
|
||||||
public TimeSpan ExecutionTimeout { get; set; } = TimeSpan.FromMinutes(5);
|
public TimeSpan ExecutionTimeout { get; set; } = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace RemoteExec.Server.Configuration;
|
||||||
|
|
||||||
|
public class ApiKeyConfiguration
|
||||||
|
{
|
||||||
|
public required string Key { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace RemoteExec.Server.Configuration;
|
||||||
|
|
||||||
|
public class AuthenticationConfiguration
|
||||||
|
{
|
||||||
|
public List<ApiKeyConfiguration> ApiKeys { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace RemoteExec.Server.Configuration;
|
||||||
|
|
||||||
|
public class ExecutionConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of concurrent tasks. Default is ProcessorCount * 2.
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxConcurrentTasks { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timeout for assembly loading requests in seconds. Default is 30 seconds.
|
||||||
|
/// </summary>
|
||||||
|
public int AssemblyLoadTimeoutSeconds { get; set; } = 30;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace RemoteExec.Server.Configuration;
|
||||||
|
|
||||||
|
public class MetricsConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum CPU usage difference (in percentage points) to trigger a metrics broadcast. Default is 1.0.
|
||||||
|
/// </summary>
|
||||||
|
public double CpuDifferenceThreshold { get; set; } = 1.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum memory usage difference (in bytes) to trigger a metrics broadcast. Default is 10 MB.
|
||||||
|
/// </summary>
|
||||||
|
public long MemoryDifferenceThreshold { get; set; } = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Broadcast interval in milliseconds. Default is 500 ms.
|
||||||
|
/// </summary>
|
||||||
|
public int BroadcastIntervalMs { get; set; } = 500;
|
||||||
|
}
|
||||||
@@ -6,5 +6,5 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
[assembly: SuppressMessage("Minor Code Smell", "S2325:Methods and properties that don't access instance data should be static", Justification = "<Pending>", Scope = "type", Target = "~T:RemoteExec.Server.Hubs.RemoteExecutionHub")]
|
[assembly: SuppressMessage("Minor Code Smell", "S2325:Methods and properties that don't access instance data should be static", Justification = "<Pending>", Scope = "type", Target = "~T:RemoteExec.Server.Hubs.RemoteExecutionHub")]
|
||||||
[assembly: SuppressMessage("Major Code Smell", "S2139:Exceptions should be either logged or rethrown but not both", Justification = "<Pending>", Scope = "member", Target = "~M:RemoteExec.Server.Hubs.RemoteExecutionHub.RequestAssemblyAsync(System.String)~System.Threading.Tasks.Task{System.Reflection.Assembly}")]
|
|
||||||
[assembly: SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "<Pending>", Scope = "member", Target = "~M:RemoteExec.Server.Hubs.RemoteExecutionHub.Execute(RemoteExec.Shared.RemoteExecutionRequest)~System.Threading.Tasks.Task{RemoteExec.Shared.RemoteExecutionResult}")]
|
[assembly: SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "<Pending>", Scope = "member", Target = "~M:RemoteExec.Server.Hubs.RemoteExecutionHub.Execute(RemoteExec.Shared.RemoteExecutionRequest)~System.Threading.Tasks.Task{RemoteExec.Shared.RemoteExecutionResult}")]
|
||||||
|
[assembly: SuppressMessage("Major Code Smell", "S3010:Static fields should not be updated in constructors", Justification = "<Pending>")]
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
using RemoteExec.Server.Configuration;
|
||||||
using RemoteExec.Shared;
|
using RemoteExec.Shared;
|
||||||
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
@@ -9,7 +11,7 @@ using System.Text.Json;
|
|||||||
|
|
||||||
namespace RemoteExec.Server.Hubs;
|
namespace RemoteExec.Server.Hubs;
|
||||||
|
|
||||||
public class RemoteExecutionHub(ILogger<RemoteExecutionHub> logger) : Hub
|
public class RemoteExecutionHub : Hub
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<string, RemoteJobAssemblyLoadContext> connections = new();
|
private static readonly ConcurrentDictionary<string, RemoteJobAssemblyLoadContext> connections = new();
|
||||||
|
|
||||||
@@ -23,8 +25,29 @@ public class RemoteExecutionHub(ILogger<RemoteExecutionHub> logger) : Hub
|
|||||||
private static TimeSpan lastTotalProcessorTime;
|
private static TimeSpan lastTotalProcessorTime;
|
||||||
|
|
||||||
private static int activeTasks = 0;
|
private static int activeTasks = 0;
|
||||||
private static readonly int maxConcurrentTasks = Environment.ProcessorCount * 2;
|
private static int maxConcurrentTasks;
|
||||||
private static readonly SemaphoreSlim taskSemaphore = new SemaphoreSlim(maxConcurrentTasks, maxConcurrentTasks);
|
private static SemaphoreSlim taskSemaphore = null!;
|
||||||
|
|
||||||
|
private static int assemblyLoadTimeoutSeconds;
|
||||||
|
private static double cpuDifferenceThreshold;
|
||||||
|
private static long memoryDifferenceThreshold;
|
||||||
|
|
||||||
|
private readonly ILogger<RemoteExecutionHub> logger;
|
||||||
|
|
||||||
|
public RemoteExecutionHub(ILogger<RemoteExecutionHub> logger, IOptions<ExecutionConfiguration> executionOptions, IOptions<MetricsConfiguration> metricsOptions)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
|
||||||
|
// Initialize static configuration values once
|
||||||
|
if (taskSemaphore is null)
|
||||||
|
{
|
||||||
|
maxConcurrentTasks = executionOptions.Value.MaxConcurrentTasks ?? (Environment.ProcessorCount * 2);
|
||||||
|
taskSemaphore = new SemaphoreSlim(maxConcurrentTasks, maxConcurrentTasks);
|
||||||
|
assemblyLoadTimeoutSeconds = executionOptions.Value.AssemblyLoadTimeoutSeconds;
|
||||||
|
cpuDifferenceThreshold = metricsOptions.Value.CpuDifferenceThreshold;
|
||||||
|
memoryDifferenceThreshold = metricsOptions.Value.MemoryDifferenceThreshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override Task OnConnectedAsync()
|
public override Task OnConnectedAsync()
|
||||||
{
|
{
|
||||||
@@ -234,7 +257,7 @@ public class RemoteExecutionHub(ILogger<RemoteExecutionHub> logger) : Hub
|
|||||||
int tasksDiff = Math.Abs(metrics.ActiveTasks - lastMetrics.ActiveTasks);
|
int tasksDiff = Math.Abs(metrics.ActiveTasks - lastMetrics.ActiveTasks);
|
||||||
int maxTasksDiff = Math.Abs(metrics.MaxConcurrentTasks - lastMetrics.MaxConcurrentTasks);
|
int maxTasksDiff = Math.Abs(metrics.MaxConcurrentTasks - lastMetrics.MaxConcurrentTasks);
|
||||||
|
|
||||||
if (cpuDiff < 1.0 && memoryDiff < 10 * 1024 * 1024 && connectionsDiff == 0 && tasksDiff == 0 && maxTasksDiff == 0)
|
if (cpuDiff < cpuDifferenceThreshold && memoryDiff < memoryDifferenceThreshold && connectionsDiff == 0 && tasksDiff == 0 && maxTasksDiff == 0)
|
||||||
{
|
{
|
||||||
return; // No significant change
|
return; // No significant change
|
||||||
}
|
}
|
||||||
@@ -296,7 +319,7 @@ public class RemoteExecutionHub(ILogger<RemoteExecutionHub> logger) : Hub
|
|||||||
|
|
||||||
await Clients.Caller.SendAsync("RequestAssembly", key, guid);
|
await Clients.Caller.SendAsync("RequestAssembly", key, guid);
|
||||||
|
|
||||||
byte[] assemblyBytes = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(30));
|
byte[] assemblyBytes = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(assemblyLoadTimeoutSeconds));
|
||||||
|
|
||||||
using MemoryStream ms = new MemoryStream(assemblyBytes);
|
using MemoryStream ms = new MemoryStream(assemblyBytes);
|
||||||
return assemblyLoadContext.LoadFromStream(ms);
|
return assemblyLoadContext.LoadFromStream(ms);
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
using RemoteExec.Server.Configuration;
|
||||||
|
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace RemoteExec.Server.Middleware;
|
||||||
|
|
||||||
|
public class ApiKeyAuthenticationMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<ApiKeyAuthenticationMiddleware> _logger;
|
||||||
|
private readonly ConcurrentDictionary<string, ApiKeyConfiguration> _apiKeys;
|
||||||
|
|
||||||
|
public ApiKeyAuthenticationMiddleware(
|
||||||
|
RequestDelegate next,
|
||||||
|
IOptionsMonitor<AuthenticationConfiguration> authOptions,
|
||||||
|
ILogger<ApiKeyAuthenticationMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Build lookup dictionary from configuration
|
||||||
|
_apiKeys = new ConcurrentDictionary<string, ApiKeyConfiguration>();
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
LoadApiKeys(authOptions.CurrentValue);
|
||||||
|
|
||||||
|
// Watch for configuration changes
|
||||||
|
_ = authOptions.OnChange(LoadApiKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadApiKeys(AuthenticationConfiguration config)
|
||||||
|
{
|
||||||
|
_apiKeys.Clear();
|
||||||
|
|
||||||
|
if (config.ApiKeys == null || config.ApiKeys.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("No API keys configured. All requests will be rejected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (ApiKeyConfiguration apiKey in config.ApiKeys.Where(k => k.Enabled))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(apiKey.Key))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Skipping empty API key configuration");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_apiKeys.TryAdd(apiKey.Key, apiKey))
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Registered API key: {Description}",
|
||||||
|
apiKey.Description ?? "No description");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Duplicate API key found and skipped: {Description}",
|
||||||
|
apiKey.Description ?? "No description");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Loaded {Count} active API keys", _apiKeys.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
// Skip authentication for health checks
|
||||||
|
if (context.Request.Path.StartsWithSegments("/health"))
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any API keys are configured
|
||||||
|
if (_apiKeys.IsEmpty)
|
||||||
|
{
|
||||||
|
_logger.LogError("No API keys configured. Rejecting request to {Path}", context.Request.Path);
|
||||||
|
|
||||||
|
context.Response.StatusCode = 503;
|
||||||
|
|
||||||
|
await context.Response.WriteAsync("Service is not properly configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for API key in header
|
||||||
|
if (!context.Request.Headers.TryGetValue("X-API-Key", out StringValues extractedApiKey))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("API Key missing from request to {Path} from {RemoteIp}", context.Request.Path, context.Connection.RemoteIpAddress);
|
||||||
|
|
||||||
|
context.Response.StatusCode = 401;
|
||||||
|
|
||||||
|
await context.Response.WriteAsync("API Key is missing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string providedKey = extractedApiKey.ToString();
|
||||||
|
|
||||||
|
// Validate API key
|
||||||
|
if (!_apiKeys.TryGetValue(providedKey, out ApiKeyConfiguration? apiKeyConfig))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Invalid API Key provided for request to {Path} from {RemoteIp}",
|
||||||
|
context.Request.Path,
|
||||||
|
context.Connection.RemoteIpAddress);
|
||||||
|
context.Response.StatusCode = 401;
|
||||||
|
await context.Response.WriteAsync("Invalid API Key");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store API key info in HttpContext for potential use in controllers
|
||||||
|
context.Items["ApiKeyDescription"] = apiKeyConfig.Description;
|
||||||
|
context.Items["ApiKey"] = providedKey;
|
||||||
|
|
||||||
|
_logger.LogDebug("Authenticated request to {Path} using key: {Description}",
|
||||||
|
context.Request.Path,
|
||||||
|
apiKeyConfig.Description ?? "No description");
|
||||||
|
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,26 @@
|
|||||||
|
using RemoteExec.Server.Configuration;
|
||||||
using RemoteExec.Server.Hubs;
|
using RemoteExec.Server.Hubs;
|
||||||
|
using RemoteExec.Server.Middleware;
|
||||||
using RemoteExec.Server.Services;
|
using RemoteExec.Server.Services;
|
||||||
|
|
||||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddSignalR();
|
builder.Services.AddSignalR();
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
builder.Services.AddHealthChecks();
|
builder.Services.AddHealthChecks();
|
||||||
|
|
||||||
// Add metrics broadcast background service
|
builder.Services.Configure<AuthenticationConfiguration>(builder.Configuration.GetSection("Authentication"));
|
||||||
|
builder.Services.Configure<ExecutionConfiguration>(builder.Configuration.GetSection("Execution"));
|
||||||
|
builder.Services.Configure<MetricsConfiguration>(builder.Configuration.GetSection("Metrics"));
|
||||||
|
|
||||||
builder.Services.AddHostedService<MetricsBroadcastService>();
|
builder.Services.AddHostedService<MetricsBroadcastService>();
|
||||||
|
|
||||||
WebApplication app = builder.Build();
|
WebApplication app = builder.Build();
|
||||||
|
|
||||||
|
app.UseMiddleware<ApiKeyAuthenticationMiddleware>();
|
||||||
|
|
||||||
app.MapHub<RemoteExecutionHub>("/remote");
|
app.MapHub<RemoteExecutionHub>("/remote");
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
@@ -23,7 +29,7 @@ if (app.Environment.IsDevelopment())
|
|||||||
_ = app.MapOpenApi();
|
_ = app.MapOpenApi();
|
||||||
}
|
}
|
||||||
|
|
||||||
//app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
using RemoteExec.Server.Configuration;
|
||||||
using RemoteExec.Server.Hubs;
|
using RemoteExec.Server.Hubs;
|
||||||
|
|
||||||
namespace RemoteExec.Server.Services;
|
namespace RemoteExec.Server.Services;
|
||||||
|
|
||||||
public class MetricsBroadcastService(IHubContext<RemoteExecutionHub> hubContext, ILogger<MetricsBroadcastService> logger) : BackgroundService
|
public class MetricsBroadcastService(IHubContext<RemoteExecutionHub> hubContext, ILogger<MetricsBroadcastService> logger, IOptions<MetricsConfiguration> metricsOptions) : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly TimeSpan _broadcastInterval = TimeSpan.FromMilliseconds(500);
|
private readonly TimeSpan broadcastInterval = TimeSpan.FromMilliseconds(metricsOptions.Value.BroadcastIntervalMs);
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Metrics broadcast service started");
|
logger.LogInformation("Metrics broadcast service started with interval {Interval}ms", broadcastInterval.TotalMilliseconds);
|
||||||
|
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(_broadcastInterval, stoppingToken);
|
await Task.Delay(broadcastInterval, stoppingToken);
|
||||||
|
|
||||||
await RemoteExecutionHub.BroadcastMetricsAsync(hubContext);
|
await RemoteExecutionHub.BroadcastMetricsAsync(hubContext);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*",
|
||||||
|
"Authentication": {
|
||||||
|
"ApiKeys": [
|
||||||
|
{
|
||||||
|
"Key": "dev-api-key-1",
|
||||||
|
"Description": "Dev client 1",
|
||||||
|
"Enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key": "dev-api-key-2",
|
||||||
|
"Description": "Dev client 2",
|
||||||
|
"Enabled": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,49 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*",
|
||||||
|
|
||||||
|
// Authentication configuration
|
||||||
|
"Authentication": {
|
||||||
|
// List of API keys for authenticating clients
|
||||||
|
// Each entry should contain key identification and validation information
|
||||||
|
"ApiKeys": [
|
||||||
|
// Example:
|
||||||
|
// {
|
||||||
|
// "Key": "your-secret-api-key-here",
|
||||||
|
// "Name": "Client1",
|
||||||
|
// "Enabled": true
|
||||||
|
// }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remote execution configuration
|
||||||
|
"Execution": {
|
||||||
|
// Maximum number of tasks that can execute concurrently on the server
|
||||||
|
// null = uses Environment.ProcessorCount * 2 (default)
|
||||||
|
// Increase for CPU-light tasks, decrease for resource-intensive operations
|
||||||
|
"MaxConcurrentTasks": null,
|
||||||
|
|
||||||
|
// Timeout in seconds for loading assemblies from clients
|
||||||
|
// If a client doesn't respond with assembly bytes within this time, the request fails
|
||||||
|
// Default: 30 seconds
|
||||||
|
"AssemblyLoadTimeoutSeconds": 30
|
||||||
|
},
|
||||||
|
|
||||||
|
// Server metrics broadcasting configuration
|
||||||
|
"Metrics": {
|
||||||
|
// Minimum CPU usage difference (in percentage points) required to trigger a metrics broadcast
|
||||||
|
// Lower values = more frequent updates, higher values = less network traffic
|
||||||
|
// Default: 1.0 (1%)
|
||||||
|
"CpuDifferenceThreshold": 1.0,
|
||||||
|
|
||||||
|
// Minimum memory usage difference (in bytes) required to trigger a metrics broadcast
|
||||||
|
// Default: 10485760 (10 MB)
|
||||||
|
"MemoryDifferenceThreshold": 10485760,
|
||||||
|
|
||||||
|
// Interval in milliseconds between metrics broadcasts to all connected clients
|
||||||
|
// Lower values = more real-time updates, higher values = less server load
|
||||||
|
// Default: 5000 (5 seconds)
|
||||||
|
"BroadcastIntervalMs": 5000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
using RemoteExec.Client;
|
using RemoteExec.Client;
|
||||||
|
|
||||||
// Single host example
|
// Single host example
|
||||||
RemoteExecutor singleHostExecutor = new RemoteExecutor("https://localhost:5001/remote");
|
RemoteExecutor singleHostExecutor = new RemoteExecutor("https://localhost:5001/remote", configure =>
|
||||||
|
{
|
||||||
|
configure.ApiKey = "dev-api-key-1";
|
||||||
|
});
|
||||||
|
|
||||||
singleHostExecutor.MetricsUpdated += (sender, e) =>
|
singleHostExecutor.MetricsUpdated += (sender, e) =>
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user