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
{
///
/// 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.
///
public class RedisManagerService
{
private TcpListener _listener;
private readonly ConcurrentDictionary _clients = new();
private Config _config;
private readonly object _configLock = new();
// Track Valkey/Redis server processes started by the daemon
private readonly ConcurrentDictionary _instanceProcesses = new();
private bool _isRunning = false;
private readonly int _port = 6380; // Port for service communication
///
/// Initializes a new instance of the RedisManagerService.
/// Loads configuration and prepares the service for operation.
///
public RedisManagerService()
{
_config = ConfigManager.LoadConfig();
}
///
/// Starts the TCP server and begins accepting client connections.
/// The server runs indefinitely until StopAsync is called.
///
/// A task that represents the asynchronous start operation
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 instancesSnapshot;
lock (_configLock)
{
instancesSnapshot = new List(_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
}
}
}
///
/// Stops the TCP server and closes all client connections.
///
/// A task that represents the asynchronous stop operation
public async Task StopAsync()
{
if (!_isRunning) return;
_isRunning = false;
_listener?.Stop();
Console.WriteLine("RedisManager Service stopped");
}
///
/// Handles a single client connection asynchronously.
/// Processes JSON-formatted command requests and sends back responses.
///
/// The TCP client connection to handle
/// A task that represents the asynchronous client handling operation
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();
}
}
///
/// Executes a Redis command from a JSON-formatted request.
///
/// JSON string containing the command request
/// A ServiceResponse object with the command execution results
private async Task ExecuteCommandAsync(string commandJson)
{
try
{
var request = JsonSerializer.Deserialize(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}");
}
}
///
/// Executes Redis commands by parsing arguments and routing to appropriate command handlers.
/// Captures console output and returns it as a string.
///
/// Command line arguments to execute
/// The captured console output from command execution
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();
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(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 ", "ARGUMENT_ERROR", new { Command = "get", Args = filteredArgs });
break;
case "set":
if (filteredArgs.Count < 3)
return (false, "'set' command requires at least 2 arguments: set [EX seconds] [PX milliseconds] [NX|XX]", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
// Validate optional flags for set
var setFlags = new HashSet(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 [key2 ...]", "ARGUMENT_ERROR", new { Command = "del", Args = filteredArgs });
break;
case "hget":
if (filteredArgs.Count != 3)
return (false, "'hget' command requires exactly 2 arguments: hget ", "ARGUMENT_ERROR", new { Command = "hget", Args = filteredArgs });
break;
case "hset":
if (filteredArgs.Count != 4)
return (false, "'hset' command requires exactly 3 arguments: hset ", "ARGUMENT_ERROR", new { Command = "hset", Args = filteredArgs });
break;
case "mget":
if (filteredArgs.Count < 2)
return (false, "'mget' command requires at least 1 argument: mget [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 [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} [value2 ...]", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
break;
case "lrange":
if (filteredArgs.Count != 4)
return (false, "'lrange' command requires exactly 3 arguments: lrange ", "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 [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 [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 [key2 ...]", "ARGUMENT_ERROR", new { Command = "exists", Args = filteredArgs });
break;
case "expire":
if (filteredArgs.Count != 3)
return (false, "'expire' command requires exactly 2 arguments: expire ", "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 ", "ARGUMENT_ERROR", new { Command = "rename", Args = filteredArgs });
break;
case "select":
if (filteredArgs.Count != 2)
return (false, "'select' command requires exactly 1 argument: select ", "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 ", "ARGUMENT_ERROR", new { Command = "auth", Args = filteredArgs });
break;
case "keys":
if (filteredArgs.Count != 2)
return (false, "'keys' command requires exactly 1 argument: keys ", "ARGUMENT_ERROR", new { Command = "keys", Args = filteredArgs });
break;
case "ttl":
if (filteredArgs.Count != 2)
return (false, "'ttl' command requires exactly 1 argument: ttl ", "ARGUMENT_ERROR", new { Command = "ttl", Args = filteredArgs });
break;
case "type":
if (filteredArgs.Count != 2)
return (false, "'type' command requires exactly 1 argument: type ", "ARGUMENT_ERROR", new { Command = "type", Args = filteredArgs });
break;
case "persist":
if (filteredArgs.Count != 2)
return (false, "'persist' command requires exactly 1 argument: persist ", "ARGUMENT_ERROR", new { Command = "persist", Args = filteredArgs });
break;
case "scard":
if (filteredArgs.Count != 2)
return (false, "'scard' command requires exactly 1 argument: scard ", "ARGUMENT_ERROR", new { Command = "scard", Args = filteredArgs });
break;
case "smembers":
if (filteredArgs.Count != 2)
return (false, "'smembers' command requires exactly 1 argument: smembers ", "ARGUMENT_ERROR", new { Command = "smembers", Args = filteredArgs });
break;
case "sismember":
if (filteredArgs.Count != 3)
return (false, "'sismember' command requires exactly 2 arguments: sismember ", "ARGUMENT_ERROR", new { Command = "sismember", Args = filteredArgs });
break;
case "srem":
if (filteredArgs.Count < 3)
return (false, "'srem' command requires at least 2 arguments: srem [member2 ...]", "ARGUMENT_ERROR", new { Command = "srem", Args = filteredArgs });
break;
case "zcard":
if (filteredArgs.Count != 2)
return (false, "'zcard' command requires exactly 1 argument: zcard ", "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} [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 ", "ARGUMENT_ERROR", new { Command = "zscore", Args = filteredArgs });
break;
case "zrem":
if (filteredArgs.Count < 3)
return (false, "'zrem' command requires at least 2 arguments: zrem [member2 ...]", "ARGUMENT_ERROR", new { Command = "zrem", Args = filteredArgs });
break;
case "zcount":
if (filteredArgs.Count != 4)
return (false, "'zcount' command requires exactly 3 arguments: zcount ", "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} ", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
break;
case "hincrby":
if (filteredArgs.Count != 4)
return (false, "'hincrby' command requires exactly 3 arguments: hincrby ", "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 ", "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 [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 [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 [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} ", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
break;
case "move":
if (filteredArgs.Count != 3)
return (false, "'move' command requires exactly 2 arguments: move ", "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 ", "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