// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using EpicGames.Core; using EpicGames.Redis; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace HordeServer.Server { /// /// Manages the lifetime of a bundled Redis instance /// public sealed class RedisService : IRedisService, IAsyncDisposable { /// /// Default Redis port /// const int RedisPort = 6379; /// /// Connection pool /// public RedisConnectionPool ConnectionPool { get; } /// /// Flag for whether the connection is read-only /// public bool ReadOnlyMode { get; } RedisProcess? _redisProcess; readonly ILogger _logger; /// /// Constructor /// public RedisService(IOptions options, ILogger logger) : this(options.Value.RedisConnectionString, -1, logger) { ReadOnlyMode = options.Value.RedisReadOnlyMode; } /// /// Constructor /// /// Redis connection string. If null, we will start a temporary redis instance on the local machine. /// Override for the database to use. Set to -1 to use the default from the connection string. /// public RedisService(string? connectionString, int dbNum, ILogger logger) { _logger = logger; if (connectionString == null) { if (IsRunningOnDefaultPort()) { connectionString = $"127.0.0.1:{RedisPort}"; } else if (TryStartRedisProcess()) { connectionString = $"127.0.0.1:{_redisProcess!.Port},allowAdmin=true"; } else { throw new Exception($"Unable to connect to Redis. Please set {nameof(ServerSettings.RedisConnectionString)} in {ServerApp.ServerConfigFile}"); } } ConnectionPool = new RedisConnectionPool(20, connectionString, dbNum); } /// public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { try { TimeSpan span = await ConnectionPool.GetDatabase().PingAsync(); return HealthCheckResult.Healthy(data: new Dictionary { ["Latency"] = span.ToString() }); } catch (Exception ex) { return HealthCheckResult.Unhealthy("Unable to ping Redis", ex); } } /// public async ValueTask DisposeAsync() { if (_redisProcess != null) { _logger.LogInformation("Sending shutdown command..."); ConnectionPool.GetConnection().GetServers().FirstOrDefault()?.Shutdown(); } ConnectionPool.Dispose(); if (_redisProcess != null) { await _redisProcess.DisposeAsync(); _redisProcess = null; } } /// /// Checks if Redis is already running on the default port /// /// static bool IsRunningOnDefaultPort() { IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); IPEndPoint[] listeners = ipGlobalProperties.GetActiveTcpListeners(); if (listeners.Any(x => x.Port == RedisPort)) { return true; } return false; } /// /// Attempts to start a local instance of Redis /// /// bool TryStartRedisProcess() { if (_redisProcess != null) { return true; } if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return false; } FileReference redisConfigFile = FileReference.Combine(RedisProcess.RedisExe.Directory, "redis.conf"); _redisProcess = new RedisProcess(_logger); _redisProcess.Start($"\"{redisConfigFile}\""); return true; } } }