// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; using EpicGames.Core; using Microsoft.Extensions.Logging; namespace HordeServer.Server { /// /// Runs a local instance of redis /// public sealed class RedisProcess : IAsyncDisposable { readonly DirectoryReference _tempDir; readonly ILogger _logger; int? _port; ManagedProcessGroup? _processGroup; ManagedProcess? _process; BackgroundTask? _relayTask; /// /// Path to the redis executable /// [SupportedOSPlatform("windows")] public static FileReference RedisExe => FileReference.Combine(ServerApp.AppDir, "ThirdParty", "Redis", "redis-server.exe"); /// /// Selected port for the service /// public int Port => _port ?? throw new Exception("Redis process has not been started"); /// /// Constructor /// public RedisProcess(ILogger logger) { _tempDir = DirectoryReference.Combine(ServerApp.DataDir, "Redis"); _logger = logger; } /// public async ValueTask DisposeAsync() { await StopAsync(); } /// /// Starts the redis process /// [SupportedOSPlatform("windows")] public void Start(string arguments) { if (_process != null) { throw new Exception("Redis process has already been started"); } FileReference redisExe = RedisExe; if (!FileReference.Exists(redisExe)) { throw new Exception($"Couldn't find bundled Redis executable at {redisExe}"); } // For some reason Redis takes quite a while to shut down, and is in a Zombie state until it is. Copy it to a temp directory first. DeleteTempFiles(); DirectoryReference.CreateDirectory(_tempDir); FileReference tempRedisExe = FileReference.Combine(_tempDir, $"redis-server.{Guid.NewGuid():N}.exe"); FileReference.Copy(redisExe, tempRedisExe); // Find a free port on the local machine _port = GetAvailablePort(); // Launch the child process _processGroup = new ManagedProcessGroup(); _process = new ManagedProcess(_processGroup, tempRedisExe.FullName, $"{arguments} --bind 127.0.0.1 --port {_port} --save \"\" --appendonly no", redisExe.Directory.FullName, null, ProcessPriorityClass.Normal); _process.StdIn.Close(); _relayTask = BackgroundTask.StartNew(RelayOutputAsync); } /// /// Stops the current process /// public async ValueTask StopAsync() { _logger.LogInformation("Stopping Redis..."); if (_processGroup != null) { _processGroup.Dispose(); _processGroup = null; } if (_process != null) { _process.Dispose(); _process = null; } if (_relayTask != null) { await _relayTask.DisposeAsync(); _relayTask = null; } DeleteTempFiles(); _logger.LogInformation("Done."); } [DllImport("kernel32.dll", SetLastError = true)] static extern uint DeleteFile(string lpFileName); void DeleteTempFiles() { if (DirectoryReference.Exists(_tempDir)) { foreach (FileReference file in DirectoryReference.EnumerateFiles(_tempDir, "*.exe", System.IO.SearchOption.TopDirectoryOnly)) { _ = DeleteFile(file.FullName); } } } /// /// Gets an unused port that can host the redis server /// static int GetAvailablePort() { using TcpListener listener = new(IPAddress.Loopback, 0); listener.Start(); int port = ((IPEndPoint)listener.LocalEndpoint).Port; listener.Stop(); return port; } /// /// Copies output from the redis process to the logger /// async Task RelayOutputAsync(CancellationToken cancellationToken) { for (; ; ) { string? line = await _process!.ReadLineAsync(cancellationToken); if (line == null) { break; } if (line.Length > 0) { _logger.Log(LogLevel.Information, "{Output}", line); } } } } }