add project

This commit is contained in:
GuilhermeStrice
2025-07-09 19:31:34 +01:00
parent 8d2e88edf4
commit f37078157d
44 changed files with 7680 additions and 0 deletions

931
RedisManagerService.cs Normal file
View File

@ -0,0 +1,931 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using CommandLine;
using RedisManager.Commands;
namespace RedisManager
{
/// <summary>
/// TCP server that provides Redis command execution as a service.
/// Accepts client connections and executes Redis commands on their behalf.
/// Clients send JSON-formatted requests and receive JSON responses.
/// </summary>
public class RedisManagerService
{
private TcpListener _listener;
private readonly ConcurrentDictionary<string, DateTime> _clients = new();
private Config _config;
private readonly object _configLock = new();
// Track Valkey/Redis server processes started by the daemon
private readonly ConcurrentDictionary<string, System.Diagnostics.Process> _instanceProcesses = new();
private bool _isRunning = false;
private readonly int _port = 6380; // Port for service communication
/// <summary>
/// Initializes a new instance of the RedisManagerService.
/// Loads configuration and prepares the service for operation.
/// </summary>
public RedisManagerService()
{
_config = ConfigManager.LoadConfig();
}
/// <summary>
/// Starts the TCP server and begins accepting client connections.
/// The server runs indefinitely until StopAsync is called.
/// </summary>
/// <returns>A task that represents the asynchronous start operation</returns>
public Task StartAsync()
{
if (_isRunning) return Task.CompletedTask;
try
{
_listener = new TcpListener(IPAddress.Any, _port);
_listener.Start();
_isRunning = true;
}
catch (SocketException ex)
{
Console.WriteLine($"Error starting service on port {_port}: {ex.Message}");
Console.WriteLine("Another instance of the service may be running, or the port is in use.");
_isRunning = false;
return Task.CompletedTask; // Exit if we can't start the listener
}
Console.WriteLine($"RedisManager Service started on port {_port}");
// Start Valkey/Redis servers for all configured instances at startup
Task.Run(async () => {
bool anySuccess = false;
List<InstanceConfig> instancesSnapshot;
lock (_configLock)
{
instancesSnapshot = new List<InstanceConfig>(_config.Instances);
}
foreach (var instance in instancesSnapshot)
{
var result = await StartValkeyServerIfNeeded(instance);
if (result.Success)
{
Console.WriteLine($"[Daemon] Started Valkey/Redis for instance '{instance.Name}' on port {instance.Port}.");
anySuccess = true;
}
else
{
Console.WriteLine($"[Daemon] Failed to start Valkey/Redis for instance '{instance.Name}' on port {instance.Port}: {result.Output}");
}
}
if (!anySuccess)
{
Console.WriteLine("[Daemon] FATAL: Could not start any Valkey/Redis server. Check your ServerBinaryPath configuration and ensure the binary exists and is executable.");
Environment.Exit(1);
}
});
// Run the client accepting loop in the background
_ = AcceptClientsAsync();
return Task.CompletedTask;
}
private async Task AcceptClientsAsync()
{
while (_isRunning)
{
try
{
var client = await _listener.AcceptTcpClientAsync();
_ = Task.Run(() => HandleClientAsync(client));
}
catch (Exception ex) when (!_isRunning)
{
// Expected when stopping
break;
}
catch (Exception ex)
{
Console.WriteLine($"[Daemon] Error accepting client: {ex}");
// Continue loop; do not exit
}
}
}
/// <summary>
/// Stops the TCP server and closes all client connections.
/// </summary>
/// <returns>A task that represents the asynchronous stop operation</returns>
public async Task StopAsync()
{
if (!_isRunning) return;
_isRunning = false;
_listener?.Stop();
Console.WriteLine("RedisManager Service stopped");
}
/// <summary>
/// Handles a single client connection asynchronously.
/// Processes JSON-formatted command requests and sends back responses.
/// </summary>
/// <param name="client">The TCP client connection to handle</param>
/// <returns>A task that represents the asynchronous client handling operation</returns>
private async Task HandleClientAsync(TcpClient client)
{
var clientId = Guid.NewGuid().ToString();
_clients[clientId] = DateTime.UtcNow;
Console.WriteLine($"[Daemon] Client connected: {clientId}");
try
{
using var stream = client.GetStream();
using var reader = new StreamReader(stream, new UTF8Encoding(false));
using var writer = new StreamWriter(stream, new UTF8Encoding(false)) { AutoFlush = true };
while (client.Connected)
{
try
{
var line = await reader.ReadLineAsync();
if (line == null) break;
Console.WriteLine($"[Daemon] Received command: {line}");
var response = await ExecuteCommandAsync(line);
var responseJson = JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = false });
await writer.WriteLineAsync(responseJson);
}
catch (Exception ex)
{
Console.WriteLine($"[Daemon] Error processing client command: {ex}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[Daemon] Error handling client: {ex}");
}
finally
{
_clients.TryRemove(clientId, out _);
Console.WriteLine($"[Daemon] Client disconnected: {clientId}");
client.Close();
}
}
/// <summary>
/// Executes a Redis command from a JSON-formatted request.
/// </summary>
/// <param name="commandJson">JSON string containing the command request</param>
/// <returns>A ServiceResponse object with the command execution results</returns>
private async Task<ServiceResponse> ExecuteCommandAsync(string commandJson)
{
try
{
var request = JsonSerializer.Deserialize<ServiceRequest>(commandJson);
if (request == null)
{
return new ServiceResponse
{
Success = false,
Data = null,
Error = "Invalid request format",
ErrorCode = "INVALID_REQUEST",
ErrorDetails = new { RawInput = commandJson }
};
}
// Special handling for reload-config
if (request.Arguments != null && request.Arguments.Length > 0 && string.Equals(request.Arguments[0], "reload-config", StringComparison.OrdinalIgnoreCase))
{
if (request.Arguments.Length > 1)
{
return new ServiceResponse
{
Success = false,
Data = null,
Error = "'reload-config' takes no arguments",
ErrorCode = "ARGUMENT_ERROR",
ErrorDetails = new { Command = "reload-config", Args = request.Arguments }
};
}
try
{
var newConfig = ConfigManager.LoadConfig();
lock (_configLock)
{
_config = newConfig;
}
Console.WriteLine($"[Daemon] Config reloaded from disk at {DateTime.Now:O}.");
return new ServiceResponse
{
Success = true,
Data = "Configuration reloaded successfully.",
Error = null,
ErrorCode = null,
ErrorDetails = null
};
}
catch (Exception ex)
{
Console.WriteLine($"[Daemon] ERROR: Failed to reload config: {ex.Message}");
return new ServiceResponse
{
Success = false,
Data = null,
Error = $"Failed to reload configuration: {ex.Message}",
ErrorCode = "CONFIG_RELOAD_FAILED",
ErrorDetails = new { ExceptionType = ex.GetType().Name, ExceptionMessage = ex.Message }
};
}
}
var (success, output) = await ExecuteRedisCommandAsync(request.Arguments);
if (!success)
{
return new ServiceResponse
{
Success = false,
Data = null,
Error = output,
ErrorCode = "COMMAND_EXECUTION_FAILED",
ErrorDetails = new { Arguments = request.Arguments }
};
}
return new ServiceResponse
{
Success = true,
Data = output,
Error = null,
ErrorCode = null,
ErrorDetails = null
};
}
catch (Exception ex)
{
return new ServiceResponse
{
Success = false,
Data = null,
Error = ex.Message,
ErrorCode = "EXCEPTION",
ErrorDetails = new { ExceptionType = ex.GetType().Name, ExceptionMessage = ex.Message }
};
}
}
// Helper: Check if a TCP port is open
private bool IsPortOpen(string host, int port)
{
try
{
using var client = new TcpClient();
var result = client.BeginConnect(host, port, null, null);
bool success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(300));
return success && client.Connected;
}
catch { return false; }
}
// Helper: Check if a Redis/Valkey server is running on host:port
private bool IsRedisServer(string host, int port)
{
try
{
using var client = new TcpClient(host, port);
using var stream = client.GetStream();
var ping = Encoding.ASCII.GetBytes("*1\r\n$4\r\nPING\r\n");
stream.Write(ping, 0, ping.Length);
stream.Flush();
var buffer = new byte[64];
int read = stream.Read(buffer, 0, buffer.Length);
var response = Encoding.ASCII.GetString(buffer, 0, read);
return response.Contains("PONG");
}
catch { return false; }
}
// Helper: Start Valkey/Redis server for an instance if not running
private async Task<(bool Success, string Output)> StartValkeyServerIfNeeded(InstanceConfig instance)
{
// Already started by us?
if (_instanceProcesses.TryGetValue(instance.Name, out var existingProc) && !existingProc.HasExited)
return (true, "");
// Is port open?
if (IsPortOpen(instance.Host, instance.Port))
{
if (IsRedisServer(instance.Host, instance.Port))
return (true, "");
else
return (false, $"Port {instance.Port} is in use but is not a Redis/Valkey server.");
}
// Determine binary path
string binaryPath = instance.ServerBinaryPath ?? _config.ServerBinaryPath;
if (string.IsNullOrWhiteSpace(binaryPath) || !File.Exists(binaryPath))
return (false, $"Redis/Valkey server binary not found at '{binaryPath}'. Please set 'ServerBinaryPath' in redismanager.json to the correct path for your Valkey or Redis server binary.");
// Start the server
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = binaryPath,
Arguments = $"--port {instance.Port}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
try
{
var proc = System.Diagnostics.Process.Start(psi);
if (proc == null)
return (false, $"Failed to start Valkey/Redis server for instance '{instance.Name}'.");
_instanceProcesses[instance.Name] = proc;
// Wait briefly for the server to start
await Task.Delay(500);
if (!IsPortOpen(instance.Host, instance.Port))
return (false, $"Valkey/Redis server did not start on port {instance.Port}.");
// Apply custom config options after server is confirmed up
await CustomConfigApplier.ApplyCustomConfigAsync(instance);
return (true, $"Started Valkey/Redis server for instance '{instance.Name}'.");
}
catch (Exception ex)
{
return (false, $"Exception starting Valkey/Redis server: {ex.Message}");
}
}
/// <summary>
/// Executes Redis commands by parsing arguments and routing to appropriate command handlers.
/// Captures console output and returns it as a string.
/// </summary>
/// <param name="args">Command line arguments to execute</param>
/// <returns>The captured console output from command execution</returns>
private async Task<(bool Success, string Output, string ErrorCode, object ErrorDetails)> ValidateArguments(string[] args)
{
// Special validation for reload-config
if (args.Length > 0 && string.Equals(args[0], "reload-config", StringComparison.OrdinalIgnoreCase))
{
if (args.Length == 1)
return (true, null, null, null);
else
return (false, "'reload-config' takes no arguments", "ARGUMENT_ERROR", new { Command = "reload-config", Args = args });
}
if (args == null || args.Length == 0)
{
return (false, "No command arguments provided.", "ARGUMENT_ERROR", new { Args = args });
}
// Instance flag validation
string instanceName = null;
var filteredArgs = new List<string>();
for (int i = 0; i < args.Length; i++)
{
if ((args[i] == "-i" || args[i] == "--instance") && i + 1 < args.Length && !string.IsNullOrWhiteSpace(args[i + 1]))
{
instanceName = args[i + 1];
i++; // Skip instance name
}
else
{
filteredArgs.Add(args[i]);
}
}
// Whitelist of commands that do NOT require instance
var noInstanceRequired = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"list-instances", "add-instance", "update-instance", "delete-instance", "reload-config", "help", "version"
};
string commandName = filteredArgs.Count > 0 ? filteredArgs[0].ToLowerInvariant() : null;
bool needsInstance = !noInstanceRequired.Contains(commandName);
if (needsInstance && string.IsNullOrEmpty(instanceName))
{
return (false, "Instance name must be specified with --instance or -i.", "INSTANCE_REQUIRED", new { Command = commandName, Args = args });
}
if (!string.IsNullOrEmpty(instanceName))
{
var instanceConfig = _config.Instances.Find(x => x.Name == instanceName);
if (instanceConfig == null)
{
return (false, $"Instance '{instanceName}' not found in config.", "ARGUMENT_ERROR", new { Instance = instanceName });
}
}
// Command-specific validation
if (filteredArgs.Count > 0)
{
string cmd = filteredArgs[0].ToLowerInvariant();
switch (cmd)
{
case "get":
if (filteredArgs.Count != 2)
return (false, "'get' command requires exactly 1 argument: get <key>", "ARGUMENT_ERROR", new { Command = "get", Args = filteredArgs });
break;
case "set":
if (filteredArgs.Count < 3)
return (false, "'set' command requires at least 2 arguments: set <key> <value> [EX seconds] [PX milliseconds] [NX|XX]", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
// Validate optional flags for set
var setFlags = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "EX", "PX", "NX", "XX" };
bool seenNX = false, seenXX = false;
int iSet = 3;
while (iSet < filteredArgs.Count)
{
string flag = filteredArgs[iSet];
if (flag.Equals("EX", StringComparison.OrdinalIgnoreCase))
{
if (iSet + 1 >= filteredArgs.Count || !int.TryParse(filteredArgs[iSet + 1], out _))
return (false, "'set' EX flag must be followed by an integer (seconds)", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
iSet += 2;
}
else if (flag.Equals("PX", StringComparison.OrdinalIgnoreCase))
{
if (iSet + 1 >= filteredArgs.Count || !int.TryParse(filteredArgs[iSet + 1], out _))
return (false, "'set' PX flag must be followed by an integer (milliseconds)", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
iSet += 2;
}
else if (flag.Equals("NX", StringComparison.OrdinalIgnoreCase))
{
if (seenXX)
return (false, "'set' cannot have both NX and XX flags", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
seenNX = true;
iSet++;
}
else if (flag.Equals("XX", StringComparison.OrdinalIgnoreCase))
{
if (seenNX)
return (false, "'set' cannot have both NX and XX flags", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
seenXX = true;
iSet++;
}
else
{
return (false, $"Unknown or misplaced flag '{flag}' in 'set' command", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
}
}
break;
case "del":
if (filteredArgs.Count < 2)
return (false, "'del' command requires at least 1 argument: del <key1> [key2 ...]", "ARGUMENT_ERROR", new { Command = "del", Args = filteredArgs });
break;
case "hget":
if (filteredArgs.Count != 3)
return (false, "'hget' command requires exactly 2 arguments: hget <hash> <field>", "ARGUMENT_ERROR", new { Command = "hget", Args = filteredArgs });
break;
case "hset":
if (filteredArgs.Count != 4)
return (false, "'hset' command requires exactly 3 arguments: hset <hash> <field> <value>", "ARGUMENT_ERROR", new { Command = "hset", Args = filteredArgs });
break;
case "mget":
if (filteredArgs.Count < 2)
return (false, "'mget' command requires at least 1 argument: mget <key1> [key2 ...]", "ARGUMENT_ERROR", new { Command = "mget", Args = filteredArgs });
break;
case "mset":
if (filteredArgs.Count < 3 || filteredArgs.Count % 2 != 1)
return (false, "'mset' command requires an even number of arguments ≥ 2: mset <key1> <value1> [key2 value2 ...]", "ARGUMENT_ERROR", new { Command = "mset", Args = filteredArgs });
break;
case "lpush":
case "rpush":
if (filteredArgs.Count < 3)
return (false, $"'{cmd}' command requires at least 2 arguments: {cmd} <list> <value1> [value2 ...]", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
break;
case "lrange":
if (filteredArgs.Count != 4)
return (false, "'lrange' command requires exactly 3 arguments: lrange <list> <start> <stop>", "ARGUMENT_ERROR", new { Command = "lrange", Args = filteredArgs });
break;
case "zadd":
if (filteredArgs.Count < 4 || (filteredArgs.Count - 2) % 2 != 0)
return (false, "'zadd' command requires at least 3 arguments and pairs: zadd <zset> <score1> <member1> [score2 member2 ...]", "ARGUMENT_ERROR", new { Command = "zadd", Args = filteredArgs });
// Validate that all scores are valid numbers
for (int i = 2; i < filteredArgs.Count; i += 2)
{
if (!double.TryParse(filteredArgs[i], out _))
return (false, $"'zadd' score argument at position {i} must be a valid number", "ARGUMENT_ERROR", new { Command = "zadd", Args = filteredArgs });
}
break;
case "sadd":
if (filteredArgs.Count < 3)
return (false, "'sadd' command requires at least 2 arguments: sadd <set> <member1> [member2 ...]", "ARGUMENT_ERROR", new { Command = "sadd", Args = filteredArgs });
break;
case "exists":
if (filteredArgs.Count < 2)
return (false, "'exists' command requires at least 1 argument: exists <key1> [key2 ...]", "ARGUMENT_ERROR", new { Command = "exists", Args = filteredArgs });
break;
case "expire":
if (filteredArgs.Count != 3)
return (false, "'expire' command requires exactly 2 arguments: expire <key> <seconds>", "ARGUMENT_ERROR", new { Command = "expire", Args = filteredArgs });
if (!int.TryParse(filteredArgs[2], out _))
return (false, "'expire' seconds argument must be a valid integer", "ARGUMENT_ERROR", new { Command = "expire", Args = filteredArgs });
break;
case "rename":
if (filteredArgs.Count != 3)
return (false, "'rename' command requires exactly 2 arguments: rename <key> <newkey>", "ARGUMENT_ERROR", new { Command = "rename", Args = filteredArgs });
break;
case "select":
if (filteredArgs.Count != 2)
return (false, "'select' command requires exactly 1 argument: select <dbindex>", "ARGUMENT_ERROR", new { Command = "select", Args = filteredArgs });
if (!int.TryParse(filteredArgs[1], out int dbidx) || dbidx < 0)
return (false, "'select' dbindex must be a non-negative integer", "ARGUMENT_ERROR", new { Command = "select", Args = filteredArgs });
break;
case "flushdb":
case "flushall":
case "dbsize":
case "info":
case "ping":
case "quit":
if (filteredArgs.Count != 1)
return (false, $"'{cmd}' command takes no arguments", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
break;
case "auth":
if (filteredArgs.Count != 2)
return (false, "'auth' command requires exactly 1 argument: auth <password>", "ARGUMENT_ERROR", new { Command = "auth", Args = filteredArgs });
break;
case "keys":
if (filteredArgs.Count != 2)
return (false, "'keys' command requires exactly 1 argument: keys <pattern>", "ARGUMENT_ERROR", new { Command = "keys", Args = filteredArgs });
break;
case "ttl":
if (filteredArgs.Count != 2)
return (false, "'ttl' command requires exactly 1 argument: ttl <key>", "ARGUMENT_ERROR", new { Command = "ttl", Args = filteredArgs });
break;
case "type":
if (filteredArgs.Count != 2)
return (false, "'type' command requires exactly 1 argument: type <key>", "ARGUMENT_ERROR", new { Command = "type", Args = filteredArgs });
break;
case "persist":
if (filteredArgs.Count != 2)
return (false, "'persist' command requires exactly 1 argument: persist <key>", "ARGUMENT_ERROR", new { Command = "persist", Args = filteredArgs });
break;
case "scard":
if (filteredArgs.Count != 2)
return (false, "'scard' command requires exactly 1 argument: scard <set>", "ARGUMENT_ERROR", new { Command = "scard", Args = filteredArgs });
break;
case "smembers":
if (filteredArgs.Count != 2)
return (false, "'smembers' command requires exactly 1 argument: smembers <set>", "ARGUMENT_ERROR", new { Command = "smembers", Args = filteredArgs });
break;
case "sismember":
if (filteredArgs.Count != 3)
return (false, "'sismember' command requires exactly 2 arguments: sismember <set> <member>", "ARGUMENT_ERROR", new { Command = "sismember", Args = filteredArgs });
break;
case "srem":
if (filteredArgs.Count < 3)
return (false, "'srem' command requires at least 2 arguments: srem <set> <member1> [member2 ...]", "ARGUMENT_ERROR", new { Command = "srem", Args = filteredArgs });
break;
case "zcard":
if (filteredArgs.Count != 2)
return (false, "'zcard' command requires exactly 1 argument: zcard <zset>", "ARGUMENT_ERROR", new { Command = "zcard", Args = filteredArgs });
break;
case "zrange":
case "zrevrange":
if (filteredArgs.Count != 4 && filteredArgs.Count != 5)
return (false, $"'{cmd}' command requires 3 or 4 arguments: {cmd} <zset> <start> <stop> [WITHSCORES]", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
if (filteredArgs.Count == 5 && !filteredArgs[4].Equals("WITHSCORES", StringComparison.OrdinalIgnoreCase))
return (false, $"'{cmd}' 5th argument must be 'WITHSCORES' if present.", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
break;
case "zscore":
if (filteredArgs.Count != 3)
return (false, "'zscore' command requires exactly 2 arguments: zscore <zset> <member>", "ARGUMENT_ERROR", new { Command = "zscore", Args = filteredArgs });
break;
case "zrem":
if (filteredArgs.Count < 3)
return (false, "'zrem' command requires at least 2 arguments: zrem <zset> <member1> [member2 ...]", "ARGUMENT_ERROR", new { Command = "zrem", Args = filteredArgs });
break;
case "zcount":
if (filteredArgs.Count != 4)
return (false, "'zcount' command requires exactly 3 arguments: zcount <zset> <min> <max>", "ARGUMENT_ERROR", new { Command = "zcount", Args = filteredArgs });
break;
case "zrank":
case "zrevrank":
if (filteredArgs.Count != 3)
return (false, $"'{cmd}' command requires exactly 2 arguments: {cmd} <zset> <member>", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
break;
case "hincrby":
if (filteredArgs.Count != 4)
return (false, "'hincrby' command requires exactly 3 arguments: hincrby <hash> <field> <increment>", "ARGUMENT_ERROR", new { Command = "hincrby", Args = filteredArgs });
if (!int.TryParse(filteredArgs[3], out _))
return (false, "'hincrby' increment must be an integer", "ARGUMENT_ERROR", new { Command = "hincrby", Args = filteredArgs });
break;
case "hincrbyfloat":
if (filteredArgs.Count != 4)
return (false, "'hincrbyfloat' command requires exactly 3 arguments: hincrbyfloat <hash> <field> <increment>", "ARGUMENT_ERROR", new { Command = "hincrbyfloat", Args = filteredArgs });
if (!double.TryParse(filteredArgs[3], out _))
return (false, "'hincrbyfloat' increment must be a number", "ARGUMENT_ERROR", new { Command = "hincrbyfloat", Args = filteredArgs });
break;
case "hdel":
if (filteredArgs.Count < 3)
return (false, "'hdel' command requires at least 2 arguments: hdel <hash> <field1> [field2 ...]", "ARGUMENT_ERROR", new { Command = "hdel", Args = filteredArgs });
break;
case "hmget":
if (filteredArgs.Count < 3)
return (false, "'hmget' command requires at least 2 arguments: hmget <hash> <field1> [field2 ...]", "ARGUMENT_ERROR", new { Command = "hmget", Args = filteredArgs });
break;
case "hmset":
if (filteredArgs.Count < 4 || (filteredArgs.Count - 2) % 2 != 0)
return (false, "'hmset' command requires at least 2 field-value pairs: hmset <hash> <field1> <value1> [field2 value2 ...]", "ARGUMENT_ERROR", new { Command = "hmset", Args = filteredArgs });
break;
case "hkeys":
case "hvals":
case "hlen":
if (filteredArgs.Count != 2)
return (false, $"'{cmd}' command requires exactly 1 argument: {cmd} <hash>", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
break;
case "move":
if (filteredArgs.Count != 3)
return (false, "'move' command requires exactly 2 arguments: move <key> <dbindex>", "ARGUMENT_ERROR", new { Command = "move", Args = filteredArgs });
break;
case "randomkey":
if (filteredArgs.Count != 1)
return (false, "'randomkey' command takes no arguments", "ARGUMENT_ERROR", new { Command = "randomkey", Args = filteredArgs });
break;
case "echo":
if (filteredArgs.Count != 2)
return (false, "'echo' command requires exactly 1 argument: echo <message>", "ARGUMENT_ERROR", new { Command = "echo", Args = filteredArgs });
break;
}
}
return (true, null, null, null);
}
private async Task<(bool Success, string Output)> ExecuteRedisCommandAsync(string[] args)
{
// (reload-config now handled in ExecuteCommandAsync)
// Validate input arguments
var validation = await ValidateArguments(args);
if (!validation.Success)
{
return (false, validation.Output);
}
// Extract instance name if present in args (-i or --instance)
string instanceName = null;
for (int i = 0; i < args.Length - 1; i++)
{
if ((args[i] == "-i" || args[i] == "--instance") && !string.IsNullOrWhiteSpace(args[i + 1]))
{
instanceName = args[i + 1];
break;
}
}
if (!string.IsNullOrEmpty(instanceName))
{
var instanceConfig = _config.Instances.Find(x => x.Name == instanceName);
if (instanceConfig == null)
return (false, $"Instance '{instanceName}' not found in config.");
// Check/start Valkey server if needed
var startResult = await StartValkeyServerIfNeeded(instanceConfig);
if (!startResult.Success)
return (false, startResult.Output);
}
try
{
// Capture console output
var originalOut = Console.Out;
var stringWriter = new StringWriter();
Console.SetOut(stringWriter);
// Always use the full in-memory config for all command handlers
Config config = _config;
bool commandRan = false;
bool parseError = false;
var parserResult = Parser.Default.ParseArguments(args, GetCommandTypes());
parserResult
.WithParsed<object>(o =>
{
commandRan = true;
switch (o)
{
case StatusOptions statusOpts: StatusCommand.RunStatus(statusOpts, config); break;
case ListInstancesOptions listInstancesOpts: InstanceCommands.RunListInstances(config); break;
case AddInstanceOptions addInstanceOpts: InstanceCommands.RunAddInstance(addInstanceOpts, config); break;
case UpdateInstanceOptions updateInstanceOpts: InstanceCommands.RunUpdateInstance(updateInstanceOpts, config); break;
case DeleteInstanceOptions deleteInstanceOpts: InstanceCommands.RunDeleteInstance(deleteInstanceOpts, config); break;
case GetOptions getOpts: StringCommands.RunGet(getOpts, config); break;
case SetOptions setOpts: StringCommands.RunSet(setOpts, config); break;
case DelOptions delOpts: StringCommands.RunDel(delOpts, config); break;
case HGetOptions hgetOpts: HashCommands.RunHGet(hgetOpts, config); break;
case HSetOptions hsetOpts: HashCommands.RunHSet(hsetOpts, config); break;
case HDelOptions hdelOpts: HashCommands.RunHDel(hdelOpts, config); break;
case HGetAllOptions hgetAllOpts: HashCommands.RunHGetAll(hgetAllOpts, config); break;
case HKeysOptions hkeysOpts: HashCommands.RunHKeys(hkeysOpts, config); break;
case HValsOptions hvalsOpts: HashCommands.RunHVals(hvalsOpts, config); break;
case HLenOptions hlenOpts: HashCommands.RunHLen(hlenOpts, config); break;
case HExistsOptions hexistsOpts: HashCommands.RunHExists(hexistsOpts, config); break;
case HIncrByOptions hincrByOpts: HashCommands.RunHIncrBy(hincrByOpts, config); break;
case HIncrByFloatOptions hincrByFloatOpts: HashCommands.RunHIncrByFloat(hincrByFloatOpts, config); break;
case HMSetOptions hmsetOpts: HashCommands.RunHMSet(hmsetOpts, config); break;
case HMGetOptions hmgetOpts: HashCommands.RunHMGet(hmgetOpts, config); break;
case HSetNxOptions hsetnxOpts: HashCommands.RunHSetNx(hsetnxOpts, config); break;
case HStrLenOptions hstrlenOpts: HashCommands.RunHStrLen(hstrlenOpts, config); break;
case HScanOptions hscanOpts: HashCommands.RunHScan(hscanOpts, config); break;
case PFAddOptions pfaddOpts: HyperLogLogCommands.RunPFAdd(pfaddOpts, config); break;
case PFCountOptions pfcountOpts: HyperLogLogCommands.RunPFCount(pfcountOpts, config); break;
case PFMergeOptions pfmergeOpts: HyperLogLogCommands.RunPFMerge(pfmergeOpts, config); break;
case GeoAddOptions geoaddOpts: GeoCommands.RunGeoAdd(geoaddOpts, config); break;
case GeoDistOptions geodistOpts: GeoCommands.RunGeoDist(geodistOpts, config); break;
case GeoHashOptions geohashOpts: GeoCommands.RunGeoHash(geohashOpts, config); break;
case GeoPosOptions geoposOpts: GeoCommands.RunGeoPos(geoposOpts, config); break;
case GeoRadiusOptions georadiusOpts: GeoCommands.RunGeoRadius(georadiusOpts, config); break;
case BitCountOptions bitcountOpts: BitCommands.RunBitCount(bitcountOpts, config); break;
case BitFieldOptions bitfieldOpts: BitCommands.RunBitField(bitfieldOpts, config); break;
case BitOpOptions bitopOpts: BitCommands.RunBitOp(bitopOpts, config); break;
case BitPosOptions bitposOpts: BitCommands.RunBitPos(bitposOpts, config); break;
case GetBitOptions getbitOpts: BitCommands.RunGetBit(getbitOpts, config); break;
case SetBitOptions setbitOpts: BitCommands.RunSetBit(setbitOpts, config); break;
case ModuleListOptions moduleListOpts: ModuleCommands.RunModuleList(moduleListOpts, config); break;
case ModuleLoadOptions moduleLoadOpts: ModuleCommands.RunModuleLoad(moduleLoadOpts, config); break;
case ModuleUnloadOptions moduleUnloadOpts: ModuleCommands.RunModuleUnload(moduleUnloadOpts, config); break;
case LPushOptions lpushOpts: ListCommands.RunLPush(lpushOpts, config); break;
case RPushOptions rpushOpts: ListCommands.RunRPush(rpushOpts, config); break;
case LLenOptions llenOpts: ListCommands.RunLLen(llenOpts, config); break;
case LRangeOptions lrangeOpts: ListCommands.RunLRange(lrangeOpts, config); break;
case LIndexOptions lindexOpts: ListCommands.RunLIndex(lindexOpts, config); break;
case LSetOptions lsetOpts: ListCommands.RunLSet(lsetOpts, config); break;
case LInsertOptions linsertOpts: ListCommands.RunLInsert(linsertOpts, config); break;
case LRemOptions lremOpts: ListCommands.RunLRem(lremOpts, config); break;
case LTrimOptions ltrimOpts: ListCommands.RunLTrim(ltrimOpts, config); break;
case LPopOptions lpopOpts: ListCommands.RunLPop(lpopOpts, config); break;
case RPopOptions rpopOpts: ListCommands.RunRPop(rpopOpts, config); break;
case BLPopOptions blpopOpts: ListCommands.RunBLPop(blpopOpts, config); break;
case BRPopOptions brpopOpts: ListCommands.RunBRPop(brpopOpts, config); break;
case RPopLPushOptions rpoplpushOpts: ListCommands.RunRPopLPush(rpoplpushOpts, config); break;
case SAddOptions saddOpts: SetCommands.RunSAdd(saddOpts, config); break;
case SMembersOptions smembersOpts: SetCommands.RunSMembers(smembersOpts, config); break;
case SIsMemberOptions sismemberOpts: SetCommands.RunSIsMember(sismemberOpts, config); break;
case SCardOptions scardOpts: SetCommands.RunSCard(scardOpts, config); break;
case SPopOptions spopOpts: SetCommands.RunSPop(spopOpts, config); break;
case SRandMemberOptions srandmemberOpts: SetCommands.RunSRandMember(srandmemberOpts, config); break;
case SRemOptions sremOpts: SetCommands.RunSRem(sremOpts, config); break;
case SInterOptions sinterOpts: SetCommands.RunSInter(sinterOpts, config); break;
case SUnionOptions sunionOpts: SetCommands.RunSUnion(sunionOpts, config); break;
case SDiffOptions sdiffOpts: SetCommands.RunSDiff(sdiffOpts, config); break;
case SInterStoreOptions sinterstoreOpts: SetCommands.RunSInterStore(sinterstoreOpts, config); break;
case SUnionStoreOptions sunionstoreOpts: SetCommands.RunSUnionStore(sunionstoreOpts, config); break;
case SDiffStoreOptions sdiffstoreOpts: SetCommands.RunSDiffStore(sdiffstoreOpts, config); break;
case SScanOptions sscanOpts: SetCommands.RunSScan(sscanOpts, config); break;
case SMoveOptions smoveOpts: SetCommands.RunSMove(smoveOpts, config); break;
case ZAddOptions zaddOpts: SortedSetCommands.RunZAdd(zaddOpts, config); break;
case ZRemOptions zremOpts: SortedSetCommands.RunZRem(zremOpts, config); break;
case ZRangeOptions zrangeOpts: SortedSetCommands.RunZRange(zrangeOpts, config); break;
case ZRevRangeOptions zrevrangeOpts: SortedSetCommands.RunZRevRange(zrevrangeOpts, config); break;
case ZRangeByScoreOptions zrangebyscoreOpts: SortedSetCommands.RunZRangeByScore(zrangebyscoreOpts, config); break;
case ZCardOptions zcardOpts: SortedSetCommands.RunZCard(zcardOpts, config); break;
case ZScoreOptions zscoreOpts: SortedSetCommands.RunZScore(zscoreOpts, config); break;
case ZRankOptions zrankOpts: SortedSetCommands.RunZRank(zrankOpts, config); break;
case ZRevRankOptions zrevrankOpts: SortedSetCommands.RunZRevRank(zrevrankOpts, config); break;
case ZIncrByOptions zincrbyOpts: SortedSetCommands.RunZIncrBy(zincrbyOpts, config); break;
case ZRevRangeByScoreOptions zrevrangebyscoreOpts: SortedSetCommands.RunZRevRangeByScore(zrevrangebyscoreOpts, config); break;
case ZCountOptions zcountOpts: SortedSetCommands.RunZCount(zcountOpts, config); break;
case ZUnionStoreOptions zunionstoreOpts: SortedSetCommands.RunZUnionStore(zunionstoreOpts, config); break;
case ZInterStoreOptions zinterstoreOpts: SortedSetCommands.RunZInterStore(zinterstoreOpts, config); break;
case ZScanOptions zscanOpts: SortedSetCommands.RunZScan(zscanOpts, config); break;
case ZPopMaxOptions zpopmaxOpts: SortedSetCommands.RunZPopMax(zpopmaxOpts, config); break;
case ZPopMinOptions zpopminOpts: SortedSetCommands.RunZPopMin(zpopminOpts, config); break;
case ZRemRangeByRankOptions zremrangebyrankOpts: SortedSetCommands.RunZRemRangeByRank(zremrangebyrankOpts, config); break;
case ZRemRangeByScoreOptions zremrangebyscoreOpts: SortedSetCommands.RunZRemRangeByScore(zremrangebyscoreOpts, config); break;
case FlushDbOptions flushdbOpts: DatabaseCommands.RunFlushDb(flushdbOpts, config); break;
case DbSizeOptions dbsizeOpts: DatabaseCommands.RunDbSize(dbsizeOpts, config); break;
case SelectOptions selectOpts: DatabaseCommands.RunSelect(selectOpts, config); break;
case FlushAllOptions flushallOpts: DatabaseCommands.RunFlushAll(flushallOpts, config); break;
case ScanOptions scanOpts: KeyCommands.RunScan(scanOpts, config); break;
case KeysOptions keysOpts: KeyCommands.RunKeys(keysOpts, config); break;
case ExistsOptions existsOpts: KeyCommands.RunExists(existsOpts, config); break;
case TypeOptions typeOpts: KeyCommands.RunType(typeOpts, config); break;
case TtlOptions ttlOpts: KeyCommands.RunTtl(ttlOpts, config); break;
case ExpireOptions expireOpts: KeyCommands.RunExpire(expireOpts, config); break;
case PersistOptions persistOpts: KeyCommands.RunPersist(persistOpts, config); break;
case RenameOptions renameOpts: KeyCommands.RunRename(renameOpts, config); break;
case ConfigOptions configOpts: ServerCommands.RunConfig(configOpts, config); break;
case AuthOptions authOpts: ConnectionCommands.RunAuth(authOpts, config); break;
case QuitOptions quitOpts: ConnectionCommands.RunQuit(quitOpts, config); break;
case ClientListOptions clientListOpts: ConnectionCommands.RunClientList(clientListOpts, config); break;
case ClientKillOptions clientKillOpts: ConnectionCommands.RunClientKill(clientKillOpts, config); break;
case AppendOptions appendOpts: AdvancedStringCommands.RunAppend(appendOpts, config); break;
case IncrOptions incrOpts: AdvancedStringCommands.RunIncr(incrOpts, config); break;
case DecrOptions decrOpts: AdvancedStringCommands.RunDecr(decrOpts, config); break;
case IncrByOptions incrbyOpts: AdvancedStringCommands.RunIncrBy(incrbyOpts, config); break;
case DecrByOptions decrbyOpts: AdvancedStringCommands.RunDecrBy(decrbyOpts, config); break;
case IncrByFloatOptions incrbyfloatOpts: AdvancedStringCommands.RunIncrByFloat(incrbyfloatOpts, config); break;
case GetRangeOptions getrangeOpts: AdvancedStringCommands.RunGetRange(getrangeOpts, config); break;
case SetRangeOptions setrangeOpts: AdvancedStringCommands.RunSetRange(setrangeOpts, config); break;
case StrLenOptions strlenOpts: AdvancedStringCommands.RunStrLen(strlenOpts, config); break;
case MGetOptions mgetOpts: AdvancedStringCommands.RunMGet(mgetOpts, config); break;
case MSetOptions msetOpts: AdvancedStringCommands.RunMSet(msetOpts, config); break;
case XAddOptions xaddOpts: StreamCommands.RunXAdd(xaddOpts, config); break;
case XRangeOptions xrangeOpts: StreamCommands.RunXRange(xrangeOpts, config); break;
case XLenOptions xlenOpts: StreamCommands.RunXLen(xlenOpts, config); break;
case XDelOptions xdelOpts: StreamCommands.RunXDel(xdelOpts, config); break;
case MultiOptions multiOpts: TransactionCommands.RunMulti(multiOpts, config); break;
default: break;
}
});
parserResult
.WithNotParsed(errors =>
{
parseError = true;
});
Console.SetOut(originalOut);
var output = stringWriter.ToString();
if (!commandRan || parseError)
{
string attemptedCommand = args.Length > 0 ? args[0] : "<none>";
return (false, $"Unknown or invalid command: '{attemptedCommand}'. Arguments: [{string.Join(", ", args)}]");
}
return (true, output);
}
catch (Exception ex)
{
return (false, $"[Daemon] Exception: {ex}");
}
}
/// <summary>
/// Returns an array of all command option types supported by the service.
/// This array is used by the command line parser to determine which commands are available.
/// </summary>
/// <returns>Array of Type objects representing all supported command options</returns>
private Type[] GetCommandTypes()
{
return new Type[] {
typeof(StatusOptions),
typeof(ListInstancesOptions), typeof(AddInstanceOptions), typeof(UpdateInstanceOptions), typeof(DeleteInstanceOptions),
typeof(GetOptions), typeof(SetOptions), typeof(DelOptions),
typeof(HGetOptions), typeof(HSetOptions), typeof(HDelOptions), typeof(HGetAllOptions), typeof(HKeysOptions), typeof(HValsOptions), typeof(HLenOptions), typeof(HExistsOptions), typeof(HIncrByOptions), typeof(HIncrByFloatOptions), typeof(HMSetOptions), typeof(HMGetOptions), typeof(HSetNxOptions), typeof(HStrLenOptions), typeof(HScanOptions),
typeof(PFAddOptions), typeof(PFCountOptions), typeof(PFMergeOptions),
typeof(GeoAddOptions), typeof(GeoDistOptions), typeof(GeoHashOptions), typeof(GeoPosOptions), typeof(GeoRadiusOptions),
typeof(BitCountOptions), typeof(BitFieldOptions), typeof(BitOpOptions), typeof(BitPosOptions), typeof(GetBitOptions), typeof(SetBitOptions),
typeof(ModuleListOptions), typeof(ModuleLoadOptions), typeof(ModuleUnloadOptions),
typeof(LPushOptions), typeof(RPushOptions), typeof(LLenOptions), typeof(LRangeOptions), typeof(LIndexOptions), typeof(LSetOptions), typeof(LInsertOptions), typeof(LRemOptions), typeof(LTrimOptions), typeof(LPopOptions), typeof(RPopOptions), typeof(BLPopOptions), typeof(BRPopOptions), typeof(RPopLPushOptions),
typeof(SAddOptions), typeof(SMembersOptions), typeof(SIsMemberOptions), typeof(SCardOptions), typeof(SPopOptions), typeof(SRandMemberOptions), typeof(SRemOptions), typeof(SInterOptions), typeof(SUnionOptions), typeof(SDiffOptions), typeof(SInterStoreOptions), typeof(SUnionStoreOptions), typeof(SDiffStoreOptions), typeof(SScanOptions), typeof(SMoveOptions),
typeof(ZAddOptions), typeof(ZRemOptions), typeof(ZRangeOptions), typeof(ZRevRangeOptions), typeof(ZRangeByScoreOptions), typeof(ZCardOptions), typeof(ZScoreOptions), typeof(ZRankOptions), typeof(ZRevRankOptions), typeof(ZIncrByOptions), typeof(ZRevRangeByScoreOptions), typeof(ZCountOptions), typeof(ZUnionStoreOptions), typeof(ZInterStoreOptions), typeof(ZScanOptions), typeof(ZPopMaxOptions), typeof(ZPopMinOptions), typeof(ZRemRangeByRankOptions), typeof(ZRemRangeByScoreOptions),
typeof(FlushDbOptions), typeof(DbSizeOptions), typeof(SelectOptions), typeof(FlushAllOptions),
typeof(ScanOptions), typeof(KeysOptions), typeof(ExistsOptions), typeof(TypeOptions), typeof(TtlOptions), typeof(ExpireOptions), typeof(PersistOptions), typeof(RenameOptions),
typeof(ConfigOptions),
typeof(AuthOptions), typeof(QuitOptions), typeof(ClientListOptions), typeof(ClientKillOptions),
typeof(AppendOptions), typeof(IncrOptions), typeof(DecrOptions), typeof(IncrByOptions), typeof(DecrByOptions), typeof(IncrByFloatOptions), typeof(GetRangeOptions), typeof(SetRangeOptions), typeof(StrLenOptions), typeof(MGetOptions), typeof(MSetOptions),
typeof(XAddOptions), typeof(XRangeOptions), typeof(XLenOptions), typeof(XDelOptions),
typeof(MultiOptions)
};
}
}
/// <summary>
/// Represents a command request sent from a client to the RedisManager service.
/// Contains the command name and arguments to be executed.
/// </summary>
public class ServiceRequest
{
/// <summary>
/// Gets or sets the name of the command to execute.
/// </summary>
public string Command { get; set; }
/// <summary>
/// Gets or sets the array of arguments for the command.
/// </summary>
public string[] Arguments { get; set; }
}
/// <summary>
/// Represents a response from the RedisManager service to a client request.
/// Contains the success status, result data, and any error information.
/// </summary>
public class ServiceResponse
{
/// <summary>
/// Gets or sets whether the command execution was successful.
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Gets or sets the result data from the command execution.
/// Contains the output when Success is true.
/// </summary>
public string Data { get; set; }
/// <summary>
/// Gets or sets the error message if the command execution failed.
/// Contains the error details when Success is false.
/// </summary>
public string Error { get; set; }
/// <summary>
/// Gets or sets the error code (e.g., INVALID_REQUEST, REDIS_ERROR, INTERNAL_ERROR).
/// </summary>
public string ErrorCode { get; set; }
/// <summary>
/// Gets or sets additional error details (optional, can be any object).
/// </summary>
public object ErrorDetails { get; set; }
}
}