// Copyright Epic Games, Inc. All Rights Reserved. using System.Collections.Concurrent; using EpicGames.Core; using EpicGames.Horde.Users; using HordeServer.Users; namespace HordeServer.Configuration { /// /// Information about the updated config /// public record class ConfigUpdateInfo(List Status, HashSet Authors, Exception? Exception); /// /// Source for reading config files /// public interface IConfigSource { /// /// URI scheme for this config source /// string Scheme { get; } /// /// Update interval for this source /// TimeSpan UpdateInterval { get; } /// /// Reads a config file from this source /// /// Locations of the config files to query /// Cancellation token for the operation /// Config file data Task GetFilesAsync(Uri[] uris, CancellationToken cancellationToken); /// /// Gets summary infomration for sending notifications /// /// New files for the configuration /// Previous set of files for the configuration /// Information about the update /// Cancellation token for the operation Task GetUpdateInfoAsync(IReadOnlyDictionary files, IReadOnlyDictionary? prevFiles, ConfigUpdateInfo updateInfo, CancellationToken cancellationToken); } /// /// Extension methods for /// public static class ConfigSource { /// /// Gets a single config file from a source /// /// Source to query /// Location of the config file to query /// Cancellation token for the operation /// Config file data public static async Task GetAsync(this IConfigSource source, Uri uri, CancellationToken cancellationToken) { IConfigFile[] result = await source.GetFilesAsync(new[] { uri }, cancellationToken); return result[0]; } } /// /// In-memory config file source /// public sealed class InMemoryConfigSource : IConfigSource { class ConfigFileRevisionImpl : IConfigFile { public Uri Uri { get; } public string Revision { get; } public ReadOnlyMemory Data { get; } public IUser? Author => null; public ConfigFileRevisionImpl(Uri uri, string version, ReadOnlyMemory data) { Uri = uri; Revision = version; Data = data; } public ValueTask> ReadAsync(CancellationToken cancellationToken) => new ValueTask>(Data); } readonly Dictionary _files = new Dictionary(); /// /// Name of the scheme for this source /// public const string Scheme = "memory"; /// string IConfigSource.Scheme => Scheme; /// public TimeSpan UpdateInterval => TimeSpan.FromSeconds(1.0); /// /// Manually adds a new config file /// /// Path to the config file /// Config file data public void Add(Uri path, ReadOnlyMemory data) { _files.Add(path, new ConfigFileRevisionImpl(path, "v1", data)); } /// public Task GetFilesAsync(Uri[] uris, CancellationToken cancellationToken) { IConfigFile[] result = new IConfigFile[uris.Length]; for (int idx = 0; idx < uris.Length; idx++) { ConfigFileRevisionImpl? configFile; if (!_files.TryGetValue(uris[idx], out configFile)) { throw new FileNotFoundException($"Config file {uris[idx]} not found."); } result[idx] = configFile; } return Task.FromResult(result); } /// public Task GetUpdateInfoAsync(IReadOnlyDictionary files, IReadOnlyDictionary? prevFiles, ConfigUpdateInfo updateInfo, CancellationToken cancellationToken) => Task.CompletedTask; } /// /// Config file source which reads from the filesystem /// public sealed class FileConfigSource : IConfigSource { class ConfigFileImpl : IConfigFile { public Uri Uri { get; } public string Revision { get; } public DateTime LastWriteTimeUtc { get; } public ReadOnlyMemory Data { get; } public IUser? Author => null; public ConfigFileImpl(Uri uri, DateTime lastWriteTimeUtc, ReadOnlyMemory data) { Uri = uri; Revision = $"timestamp={lastWriteTimeUtc.Ticks}"; LastWriteTimeUtc = lastWriteTimeUtc; Data = data; } public ValueTask> ReadAsync(CancellationToken cancellationToken) => new ValueTask>(Data); } /// /// Name of the scheme for this source /// public const string Scheme = "file"; /// string IConfigSource.Scheme => Scheme; /// public TimeSpan UpdateInterval => TimeSpan.FromSeconds(5.0); readonly DirectoryReference _baseDir; readonly ConcurrentDictionary _files = new ConcurrentDictionary(); /// /// Constructor /// public FileConfigSource() : this(DirectoryReference.GetCurrentDirectory()) { } /// /// Constructor /// /// Base directory for resolving relative paths public FileConfigSource(DirectoryReference baseDir) { _baseDir = baseDir; } /// public async Task GetFilesAsync(Uri[] uris, CancellationToken cancellationToken) { IConfigFile[] files = new IConfigFile[uris.Length]; for (int idx = 0; idx < uris.Length; idx++) { Uri uri = uris[idx]; FileReference localPath = FileReference.Combine(_baseDir, uri.LocalPath); ConfigFileImpl? file; for (; ; ) { if (_files.TryGetValue(localPath, out file)) { if (FileReference.GetLastWriteTimeUtc(localPath) == file.LastWriteTimeUtc) { break; } else { _files.TryRemove(new KeyValuePair(localPath, file)); } } using (FileStream stream = FileReference.Open(localPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { using MemoryStream memoryStream = new MemoryStream(); await stream.CopyToAsync(memoryStream, cancellationToken); DateTime lastWriteTime = FileReference.GetLastWriteTimeUtc(localPath); file = new ConfigFileImpl(uri, lastWriteTime, memoryStream.ToArray()); } if (_files.TryAdd(localPath, file)) { break; } } files[idx] = file; } return files; } /// public Task GetUpdateInfoAsync(IReadOnlyDictionary files, IReadOnlyDictionary? prevFiles, ConfigUpdateInfo updateInfo, CancellationToken cancellationToken) => Task.CompletedTask; } }