// 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;
}
}
}