using Serilog; using System; using System.Diagnostics; using System.IO; using System.Net.Sockets; using System.Threading.Tasks; namespace RunRedis.Services { public class RedisProcessManager { private readonly RedisSetting _settings; private Process _redisProcess; private int _restartAttempts = 0; public RedisProcessManager(RedisSetting settings) { _settings = settings ?? throw new ArgumentNullException(nameof(settings)); } public bool IsRedisRunning() { try { // First check if process is running if (!IsRedisProcessRunning()) return false; // Then check if Redis is responding on the configured port using (var tcpClient = new TcpClient()) { var connectTask = tcpClient.ConnectAsync(_settings.Host, int.Parse(_settings.Port)); var timeoutTask = Task.Delay(_settings.ConnectionTimeoutSeconds * 1000); var completedTask = Task.WaitAny(connectTask, timeoutTask); if (completedTask == 0 && tcpClient.Connected) { return true; } } return false; } catch (Exception ex) { Log.Debug("Error checking Redis status: {Error}", ex.Message); return false; } } public bool IsRedisProcessRunning() { try { var processes = Process.GetProcessesByName("redis-server"); return processes.Length > 0; } catch { return false; } } public async Task StartRedisAsync() { try { if (IsRedisRunning()) { Log.Information("Redis is already running"); return true; } // Prepare for startup await PrepareForStartupAsync(); // Determine Redis executable path string redisExecutable = GetRedisExecutablePath(); if (!File.Exists(redisExecutable)) { Log.Error("Redis executable not found at: {Path}", redisExecutable); return false; } // Create process start info var startInfo = new ProcessStartInfo { FileName = redisExecutable, WorkingDirectory = Path.GetDirectoryName(redisExecutable), CreateNoWindow = true, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true }; // Add configuration file if exists string configPath = GetRedisConfigPath(); if (File.Exists(configPath)) { startInfo.Arguments = $"\"{configPath}\""; Log.Information("Starting Redis with config file: {ConfigPath}", configPath); } else { // Use command line arguments startInfo.Arguments = BuildRedisArguments(); Log.Information("Starting Redis with command line arguments"); } _redisProcess = Process.Start(startInfo); if (_redisProcess != null) { Log.Information("Redis process started with PID: {ProcessId}", _redisProcess.Id); // Wait a moment for the process to initialize await Task.Delay(5000); // Verify the process is still running and Redis is responding if (!_redisProcess.HasExited && IsRedisRunning()) { _restartAttempts = 0; // Reset restart attempts on successful start Log.Information("Redis started successfully"); return true; } else { Log.Error("Redis process exited immediately after start or is not responding"); return false; } } else { Log.Error("Failed to start Redis process"); return false; } } catch (Exception ex) { Log.Error("Error starting Redis: {Error}", ex.Message); return false; } } public async Task StopRedisAsync(bool graceful = true) { try { if (!IsRedisProcessRunning()) { Log.Information("Redis is not running"); return true; } if (graceful) { // Try graceful shutdown using redis-cli await ExecuteRedisCliCommandAsync("SHUTDOWN SAVE"); await Task.Delay(5000); // Wait for graceful shutdown } // If still running, force kill var processes = Process.GetProcessesByName("redis-server"); foreach (var process in processes) { try { if (!process.HasExited) { process.Kill(); await process.WaitForExitAsync(); Log.Information("Redis process {ProcessId} terminated", process.Id); } } finally { process.Dispose(); } } return true; } catch (Exception ex) { Log.Error("Error stopping Redis: {Error}", ex.Message); return false; } } public async Task RestartRedisAsync() { if (_restartAttempts >= _settings.MaxRestartAttempts) { Log.Error("Maximum restart attempts ({MaxAttempts}) reached. Manual intervention required.", _settings.MaxRestartAttempts); return false; } _restartAttempts++; Log.Information("Attempting Redis restart (attempt {Attempt}/{MaxAttempts})", _restartAttempts, _settings.MaxRestartAttempts); // Stop Redis await StopRedisAsync(graceful: true); // Wait before restart await Task.Delay(_settings.RestartDelaySeconds * 1000); // Start Redis return await StartRedisAsync(); } private async Task PrepareForStartupAsync() { try { // Ensure data directory exists if (!string.IsNullOrEmpty(_settings.DataDirectory)) { Directory.CreateDirectory(_settings.DataDirectory); } // Ensure log directory exists if (!string.IsNullOrEmpty(_settings.LogDirectory)) { Directory.CreateDirectory(_settings.LogDirectory); } // Clean up any stale lock files or temporary files await CleanupStaleFilesAsync(); } catch (Exception ex) { Log.Warning("Error during startup preparation: {Error}", ex.Message); } } private Task CleanupStaleFilesAsync() { try { // Remove Redis dump.rdb lock files if they exist var lockFiles = new[] { "dump.rdb.lock", "redis.pid" }; foreach (var lockFile in lockFiles) { var lockPath = Path.Combine(_settings.DataDirectory, lockFile); if (File.Exists(lockPath)) { File.Delete(lockPath); Log.Information("Removed stale lock file: {LockFile}", lockPath); } } } catch (Exception ex) { Log.Warning("Error cleaning up stale files: {Error}", ex.Message); } return Task.CompletedTask; } private string GetRedisExecutablePath() { // Try configured path first if (!string.IsNullOrEmpty(_settings.RedisExecutablePath) && File.Exists(_settings.RedisExecutablePath)) { return _settings.RedisExecutablePath; } // Try current directory var currentDirPath = Path.Combine(Directory.GetCurrentDirectory(), "redis-server.exe"); if (File.Exists(currentDirPath)) { return currentDirPath; } // Try PATH environment variable return "redis-server.exe"; // Let the system find it } private string GetRedisConfigPath() { // Try configured path first if (!string.IsNullOrEmpty(_settings.RedisConfigPath) && File.Exists(_settings.RedisConfigPath)) { return _settings.RedisConfigPath; } // Try current directory var currentDirPath = Path.Combine(Directory.GetCurrentDirectory(), "redis.conf"); if (File.Exists(currentDirPath)) { return currentDirPath; } return null; // No config file found } private string BuildRedisArguments() { var args = new System.Collections.Generic.List(); // Basic configuration args.Add($"--port {_settings.Port}"); args.Add($"--bind {_settings.Host}"); if (!string.IsNullOrEmpty(_settings.Auth)) { args.Add($"--requirepass {_settings.Auth}"); } if (!string.IsNullOrEmpty(_settings.DataDirectory)) { args.Add($"--dir \"{_settings.DataDirectory}\""); } // Memory settings if (_settings.MaxMemoryUsageMB > 0) { args.Add($"--maxmemory {_settings.MaxMemoryUsageMB}mb"); args.Add($"--maxmemory-policy {_settings.MaxMemoryPolicy}"); } // Persistence settings if (_settings.EnablePersistence) { args.Add($"--save {_settings.SaveConfiguration}"); } else { args.Add("--save \"\""); } // Database settings args.Add($"--databases {_settings.DatabaseCount}"); args.Add($"--timeout {_settings.ClientTimeoutSeconds}"); // Slow log settings if (_settings.EnableSlowLog) { args.Add($"--slowlog-max-len {_settings.SlowLogMaxLength}"); args.Add($"--slowlog-log-slower-than {_settings.SlowLogSlowerThanMicroseconds}"); } return string.Join(" ", args); } public async Task ExecuteRedisCliCommandAsync(string command) { try { string redisCliPath = GetRedisCliPath(); var processStartInfo = new ProcessStartInfo { FileName = redisCliPath, Arguments = $"-h {_settings.Host} -p {_settings.Port}", CreateNoWindow = true, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true }; if (!string.IsNullOrEmpty(_settings.Auth)) { processStartInfo.Arguments += $" -a {_settings.Auth}"; } processStartInfo.Arguments += $" {command}"; using (var process = Process.Start(processStartInfo)) { var output = await process.StandardOutput.ReadToEndAsync(); var error = await process.StandardError.ReadToEndAsync(); await process.WaitForExitAsync(); if (process.ExitCode == 0) { Log.Debug("Redis CLI command executed successfully: {Command}", command); return output.Trim(); } else { Log.Warning("Redis CLI command failed: {Command}, Error: {Error}", command, error); return null; } } } catch (Exception ex) { Log.Error("Error executing Redis CLI command: {Command}, Error: {Error}", command, ex.Message); return null; } } private string GetRedisCliPath() { // Try configured path first if (!string.IsNullOrEmpty(_settings.RedisCliPath) && File.Exists(_settings.RedisCliPath)) { return _settings.RedisCliPath; } // Try current directory var currentDirPath = Path.Combine(Directory.GetCurrentDirectory(), "redis-cli.exe"); if (File.Exists(currentDirPath)) { return currentDirPath; } // Try PATH environment variable return "redis-cli.exe"; // Let the system find it } public void Dispose() { _redisProcess?.Dispose(); } } }