add project
This commit is contained in:
931
RedisManagerService.cs
Normal file
931
RedisManagerService.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user