Abstract config design and update logging format (#624)

* Abstract config design and update logging pattern
Also drops legacy config support

* Fix unit tests

* Make backup of config file when upgrading

* Use shared process semaphore to fix migration race conditions

* Use mutex because semaphore isn't supported

* Make startup wait for configs to load

* Move mutex to config load instead of constructor

* Add debug logging

* Make mutex static

* Change mutex name format

* Make mutex use global namespace

* Remove debug logging and fix config upgrading

* Rename lambda variable
This commit is contained in:
Josh 2023-01-10 17:29:47 -06:00 committed by GitHub
parent 7179574e43
commit c86d2a11b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 310 additions and 443 deletions

View file

@ -10,10 +10,7 @@ public sealed class DatabaseFactAttribute : FactAttribute
public DatabaseFactAttribute()
{
ServerConfiguration.Instance = new ServerConfiguration
{
DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse",
};
ServerConfiguration.Instance.DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse";
if (!ServerStatics.DbConnected) this.Skip = "Database not available";
else
lock (migrateLock)

View file

@ -0,0 +1,231 @@
#nullable enable
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Threading;
using LBPUnion.ProjectLighthouse.Logging;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
// ReSharper disable VirtualMemberCallInConstructor
// ReSharper disable StaticMemberInGenericType
namespace LBPUnion.ProjectLighthouse.Configuration;
[Serializable]
public abstract class ConfigurationBase<T> where T : class, new()
{
private static readonly Lazy<T> sInstance = new(CreateInstanceOfT);
public static T Instance => sInstance.Value;
// ReSharper disable once UnusedMember.Global
// Used with reflection to determine if the config was successfully loaded
public static bool IsConfigured => sInstance.IsValueCreated;
// Used to prevent an infinite loop of the config trying to load itself when deserializing
// This is intentionally not released in the constructor
private static readonly SemaphoreSlim constructorLock = new(1, 1);
// Global mutex for synchronizing processes so that only one process can read a config at a time
// Mostly useful for migrations so only one server will try to rewrite the config file
private static Mutex? _configFileMutex;
[YamlIgnore]
public abstract string ConfigName { get; set; }
[YamlMember(Alias = "configVersionDoNotModifyOrYouWillBeSlapped", Order = -2)]
public abstract int ConfigVersion { get; set; }
[YamlIgnore]
// Used to indicate whether the config will be generated with a .configme extension or not
public virtual bool NeedsConfiguration { get; set; } = true;
[YamlMember(Order = -1)]
public bool ConfigReloading { get; set; } = false;
// Used to listen for changes to the config file
private static FileSystemWatcher? _fileWatcher;
internal ConfigurationBase()
{
// Deserializing this class will call this constructor and we don't want it to actually load the config
// So each subsequent time this constructor is called we want to exit early
if (constructorLock.CurrentCount == 0)
{
return;
}
constructorLock.Wait();
if (ServerStatics.IsUnitTesting)
return; // Unit testing, we don't want to read configurations here since the tests will provide their own
// Trim ConfigName by 4 to remove the .yml
string mutexName = $"Global\\LighthouseConfig-{this.ConfigName[..^4]}";
_configFileMutex = new Mutex(false, mutexName);
this.loadStoredConfig();
if (!this.ConfigReloading) return;
_fileWatcher = new FileSystemWatcher
{
Path = Environment.CurrentDirectory,
Filter = this.ConfigName,
NotifyFilter = NotifyFilters.LastWrite, // only watch for writes to config file
};
_fileWatcher.Changed += this.onConfigChanged; // add event handler
_fileWatcher.EnableRaisingEvents = true; // begin watching
}
internal void onConfigChanged(object sender, FileSystemEventArgs e)
{
if (_fileWatcher == null) return;
try
{
_fileWatcher.EnableRaisingEvents = false;
Debug.Assert(e.Name == this.ConfigName);
Logger.Info("Configuration file modified, reloading config...", LogArea.Config);
Logger.Warn("Some changes may not apply; they will require a restart of Lighthouse.", LogArea.Config);
this.loadStoredConfig();
Logger.Success("Successfully reloaded the configuration!", LogArea.Config);
}
finally
{
_fileWatcher.EnableRaisingEvents = true;
}
}
private void loadStoredConfig()
{
try
{
_configFileMutex?.WaitOne();
ConfigurationBase<T>? storedConfig;
if (File.Exists(this.ConfigName) && (storedConfig = this.fromFile(this.ConfigName)) != null)
{
if (storedConfig.ConfigVersion < GetVersion())
{
int newVersion = GetVersion();
Logger.Info($"Upgrading config file from version {storedConfig.ConfigVersion} to version {newVersion}", LogArea.Config);
storedConfig.writeConfig(this.ConfigName + ".bak");
this.loadConfig(storedConfig);
this.ConfigVersion = newVersion;
this.writeConfig(this.ConfigName);
}
else
{
this.loadConfig(storedConfig);
}
}
else if (!File.Exists(this.ConfigName))
{
if (this.NeedsConfiguration)
{
Logger.Warn("The configuration file was not found. " +
"A blank configuration file has been created for you at " +
$"{Path.Combine(Environment.CurrentDirectory, this.ConfigName + ".configme")}",
LogArea.Config);
this.writeConfig(this.ConfigName + ".configme");
this.ConfigVersion = -1;
}
else
{
this.writeConfig(this.ConfigName);
}
}
}
finally
{
_configFileMutex?.ReleaseMutex();
}
}
/// <summary>
/// Uses reflection to set all values of this class to the values of another class
/// </summary>
/// <param name="otherConfig">The config to be loaded</param>
private void loadConfig(ConfigurationBase<T> otherConfig)
{
foreach (PropertyInfo propertyInfo in otherConfig.GetType().GetProperties())
{
object? value = propertyInfo.GetValue(otherConfig);
PropertyInfo? local = this.GetType().GetProperty(propertyInfo.Name);
if (value == null || local == null || Attribute.IsDefined(local, typeof(YamlIgnoreAttribute)))
{
continue;
}
local.SetValue(this, value);
}
}
private ConfigurationBase<T>? fromFile(string path)
{
IDeserializer deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.Build();
try
{
string text = File.ReadAllText(path);
if (text.StartsWith("configVersionDoNotModifyOrYouWillBeSlapped"))
return this.Deserialize(deserializer, text);
}
catch (Exception e)
{
Logger.Error($"Error while deserializing config: {e}", LogArea.Config);
return null;
}
Logger.Error($"Unable to load config for {this.GetType().Name}", LogArea.Config);
return null;
}
public abstract ConfigurationBase<T> Deserialize(IDeserializer deserializer, string text);
private string serializeConfig() => new SerializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build().Serialize(this);
private void writeConfig(string path) => File.WriteAllText(path, this.serializeConfig());
public void Dispose()
{
_configFileMutex?.Dispose();
_fileWatcher?.Dispose();
constructorLock.Dispose();
}
public static int GetVersion() => version.Value;
private static readonly Lazy<int> version = new(fetchVersion);
// Obtain a fresh version of the class to get the coded config version
private static int fetchVersion()
{
object instance = CreateInstanceOfT();
int? ver = instance.GetType().GetProperty("ConfigVersion")?.GetValue(instance) as int?;
return ver.GetValueOrDefault();
}
private static T CreateInstanceOfT()
{
try
{
return Activator.CreateInstance(typeof(T), true) as T ?? throw new InvalidOperationException();
}
catch (Exception e)
{
Logger.Error($"Failed to create instance of {typeof(T).Name}: {e}", LogArea.Config);
return new T();
}
}
}

View file

@ -1,213 +0,0 @@
using System.IO;
using System.Text.Json;
using LBPUnion.ProjectLighthouse.Configuration.ConfigurationCategories;
using LBPUnion.ProjectLighthouse.Types;
namespace LBPUnion.ProjectLighthouse.Configuration.Legacy;
#nullable enable
internal class LegacyServerSettings
{
#region Meta
public const string ConfigFileName = "lighthouse.config.json";
#endregion
#region InfluxDB
public bool InfluxEnabled { get; set; }
public bool InfluxLoggingEnabled { get; set; }
public string InfluxOrg { get; set; } = "lighthouse";
public string InfluxBucket { get; set; } = "lighthouse";
public string InfluxToken { get; set; } = "";
public string InfluxUrl { get; set; } = "http://localhost:8086";
#endregion
public string EulaText { get; set; } = "";
#if !DEBUG
public string AnnounceText { get; set; } = "You are now logged in as %user.";
#else
public string AnnounceText { get; set; } = "You are now logged in as %user (id: %id).";
#endif
public string DbConnectionString { get; set; } = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse";
public string ExternalUrl { get; set; } = "http://localhost:10060";
public string ServerDigestKey { get; set; } = "";
public string AlternateDigestKey { get; set; } = "";
public bool UseExternalAuth { get; set; }
public bool CheckForUnsafeFiles { get; set; } = true;
public bool RegistrationEnabled { get; set; } = true;
#region UGC Limits
/// <summary>
/// The maximum amount of slots allowed on users' earth
/// </summary>
public int EntitledSlots { get; set; } = 50;
public int ListsQuota { get; set; } = 50;
public int PhotosQuota { get; set; } = 500;
public bool ProfileCommentsEnabled { get; set; } = true;
public bool LevelCommentsEnabled { get; set; } = true;
public bool LevelReviewsEnabled { get; set; } = true;
#endregion
#region Google Analytics
public bool GoogleAnalyticsEnabled { get; set; }
public string GoogleAnalyticsId { get; set; } = "";
#endregion
public bool BlockDeniedUsers { get; set; } = true;
public bool BooingEnabled { get; set; } = true;
public FilterMode UserInputFilterMode { get; set; } = FilterMode.None;
#region Discord Webhook
public bool DiscordWebhookEnabled { get; set; }
public string DiscordWebhookUrl { get; set; } = "";
#endregion
public bool ConfigReloading { get; set; } = true;
public string MissingIconHash { get; set; } = "";
#region HCaptcha
public bool HCaptchaEnabled { get; set; }
public string HCaptchaSiteKey { get; set; } = "";
public string HCaptchaSecret { get; set; } = "";
#endregion
public string ServerListenUrl { get; set; } = "http://localhost:10060";
public bool ConvertAssetsOnStartup { get; set; } = true;
#region SMTP
public bool SMTPEnabled { get; set; }
public string SMTPHost { get; set; } = "";
public int SMTPPort { get; set; } = 587;
public string SMTPFromAddress { get; set; } = "lighthouse@example.com";
public string SMTPFromName { get; set; } = "Project Lighthouse";
public string SMTPPassword { get; set; } = "";
public bool SMTPSsl { get; set; } = true;
#endregion
internal static LegacyServerSettings? FromFile(string path)
{
string data = File.ReadAllText(path);
return JsonSerializer.Deserialize<LegacyServerSettings>(data);
}
internal ServerConfiguration ToNewConfiguration()
{
ServerConfiguration configuration = new();
configuration.ConfigReloading = this.ConfigReloading;
configuration.AnnounceText = this.AnnounceText;
configuration.EulaText = this.EulaText;
configuration.ExternalUrl = this.ExternalUrl;
configuration.DbConnectionString = this.DbConnectionString;
configuration.CheckForUnsafeFiles = this.CheckForUnsafeFiles;
configuration.UserInputFilterMode = this.UserInputFilterMode;
// configuration categories
configuration.InfluxDB = new InfluxDBConfiguration
{
InfluxEnabled = this.InfluxEnabled,
LoggingEnabled = this.InfluxLoggingEnabled,
Bucket = this.InfluxBucket,
Organization = this.InfluxOrg,
Token = this.InfluxToken,
Url = this.InfluxUrl,
};
configuration.Authentication = new AuthenticationConfiguration
{
RegistrationEnabled = this.RegistrationEnabled,
};
configuration.Captcha = new CaptchaConfiguration
{
CaptchaEnabled = this.HCaptchaEnabled,
SiteKey = this.HCaptchaSiteKey,
Secret = this.HCaptchaSecret,
};
configuration.Mail = new MailConfiguration
{
MailEnabled = this.SMTPEnabled,
Host = this.SMTPHost,
Password = this.SMTPPassword,
Port = this.SMTPPort,
FromAddress = this.SMTPFromAddress,
FromName = this.SMTPFromName,
UseSSL = this.SMTPSsl,
};
configuration.DigestKey = new DigestKeyConfiguration
{
PrimaryDigestKey = this.ServerDigestKey,
AlternateDigestKey = this.AlternateDigestKey,
};
configuration.DiscordIntegration = new DiscordIntegrationConfiguration
{
DiscordIntegrationEnabled = this.DiscordWebhookEnabled,
Url = this.DiscordWebhookUrl,
};
configuration.GoogleAnalytics = new GoogleAnalyticsConfiguration
{
AnalyticsEnabled = this.GoogleAnalyticsEnabled,
Id = this.GoogleAnalyticsId,
};
configuration.UserGeneratedContentLimits = new UserGeneratedContentLimitConfiguration
{
BooingEnabled = this.BooingEnabled,
EntitledSlots = this.EntitledSlots,
ListsQuota = this.ListsQuota,
PhotosQuota = this.PhotosQuota,
LevelCommentsEnabled = this.LevelCommentsEnabled,
LevelReviewsEnabled = this.LevelReviewsEnabled,
ProfileCommentsEnabled = this.ProfileCommentsEnabled,
};
configuration.WebsiteConfiguration = new WebsiteConfiguration
{
MissingIconHash = this.MissingIconHash,
ConvertAssetsOnStartup = this.ConvertAssetsOnStartup,
};
return configuration;
}
}

View file

@ -1,174 +1,20 @@
#nullable enable
using System;
using System.Diagnostics;
using System.IO;
using LBPUnion.ProjectLighthouse.Configuration.ConfigurationCategories;
using LBPUnion.ProjectLighthouse.Configuration.Legacy;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Configuration.ConfigurationCategories;
using LBPUnion.ProjectLighthouse.Types;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace LBPUnion.ProjectLighthouse.Configuration;
[Serializable]
public class ServerConfiguration
public class ServerConfiguration : ConfigurationBase<ServerConfiguration>
{
// HEY, YOU!
// THIS VALUE MUST BE INCREMENTED FOR EVERY CONFIG CHANGE!
//
// This is so Lighthouse can properly identify outdated configurations and update them with newer settings accordingly.
// If you are modifying anything here that isn't outside of a method, this value MUST be incremented.
// It is also strongly recommended to not remove any items, else it will cause deserialization errors.
// You can use an ObsoleteAttribute instead. Make sure you set it to error, though.
//
// If you are modifying anything here, this value MUST be incremented.
// Thanks for listening~
public const int CurrentConfigVersion = 16;
#region Meta
public static ServerConfiguration Instance;
[YamlMember(Alias = "configVersionDoNotModifyOrYouWillBeSlapped")]
public int ConfigVersion { get; set; } = CurrentConfigVersion;
public const string ConfigFileName = "lighthouse.yml";
public const string LegacyConfigFileName = LegacyServerSettings.ConfigFileName;
#endregion Meta
#region Setup
private static readonly FileSystemWatcher fileWatcher;
// ReSharper disable once NotNullMemberIsNotInitialized
#pragma warning disable CS8618
static ServerConfiguration()
{
if (ServerStatics.IsUnitTesting) return; // Unit testing, we don't want to read configurations here since the tests will provide their own
Logger.Info("Loading config...", LogArea.Config);
ServerConfiguration? tempConfig;
// If a valid YML configuration is available!
if (File.Exists(ConfigFileName) && (tempConfig = fromFile(ConfigFileName)) != null)
{
Instance = tempConfig;
if (Instance.ConfigVersion < CurrentConfigVersion)
{
Logger.Info($"Upgrading config file from version {Instance.ConfigVersion} to version {CurrentConfigVersion}", LogArea.Config);
Instance.ConfigVersion = CurrentConfigVersion;
Instance.writeConfig(ConfigFileName);
}
}
// If we have a valid legacy configuration we can migrate, let's do it now.
else if (File.Exists(LegacyConfigFileName))
{
Logger.Warn("This version of Project Lighthouse now uses YML instead of JSON to store configuration.", LogArea.Config);
Logger.Warn
("As such, the config will now be migrated to use YML. Do not modify the original JSON file; changes will not be kept.", LogArea.Config);
Logger.Info($"The new configuration is stored at {ConfigFileName}.", LogArea.Config);
LegacyServerSettings? legacyConfig = LegacyServerSettings.FromFile(LegacyConfigFileName);
Debug.Assert(legacyConfig != null);
Instance = legacyConfig.ToNewConfiguration();
Instance.writeConfig(ConfigFileName);
Logger.Success("The configuration migration completed successfully.", LogArea.Config);
}
// If there is no valid YML configuration available,
// generate a blank one and ask the server operator to configure it, then exit.
else
{
new ServerConfiguration().writeConfig(ConfigFileName + ".configme");
Logger.Warn
(
"The configuration file was not found. " +
"A blank configuration file has been created for you at " +
$"{Path.Combine(Environment.CurrentDirectory, ConfigFileName + ".configme")}",
LogArea.Config
);
Environment.Exit(1);
}
// Set up reloading
if (!Instance.ConfigReloading) return;
Logger.Info("Setting up config reloading...", LogArea.Config);
fileWatcher = new FileSystemWatcher
{
Path = Environment.CurrentDirectory,
Filter = ConfigFileName,
NotifyFilter = NotifyFilters.LastWrite, // only watch for writes to config file
};
fileWatcher.Changed += onConfigChanged; // add event handler
fileWatcher.EnableRaisingEvents = true; // begin watching
}
#pragma warning restore CS8618
private static void onConfigChanged(object sender, FileSystemEventArgs e)
{
try
{
fileWatcher.EnableRaisingEvents = false;
Debug.Assert(e.Name == ConfigFileName);
Logger.Info("Configuration file modified, reloading config...", LogArea.Config);
Logger.Warn("Some changes may not apply; they will require a restart of Lighthouse.", LogArea.Config);
ServerConfiguration? configuration = fromFile(ConfigFileName);
if (configuration == null)
{
Logger.Warn("The new configuration was unable to be loaded for some reason. The old config has been kept.", LogArea.Config);
return;
}
Instance = configuration;
Logger.Success("Successfully reloaded the configuration!", LogArea.Config);
}
finally
{
fileWatcher.EnableRaisingEvents = true;
}
}
private static INamingConvention namingConvention = CamelCaseNamingConvention.Instance;
private static ServerConfiguration? fromFile(string path)
{
IDeserializer deserializer = new DeserializerBuilder().WithNamingConvention(namingConvention).IgnoreUnmatchedProperties().Build();
string text;
try
{
text = File.ReadAllText(path);
}
catch
{
return null;
}
return deserializer.Deserialize<ServerConfiguration>(text);
}
private void writeConfig(string path)
{
ISerializer serializer = new SerializerBuilder().WithNamingConvention(namingConvention).Build();
File.WriteAllText(path, serializer.Serialize(this));
}
#endregion
public override int ConfigVersion { get; set; } = 16;
public override string ConfigName { get; set; } = "lighthouse.yml";
public string WebsiteListenUrl { get; set; } = "http://localhost:10060";
public string GameApiListenUrl { get; set; } = "http://localhost:10061";
public string ApiListenUrl { get; set; } = "http://localhost:10062";
@ -177,7 +23,6 @@ public class ServerConfiguration
public string RedisConnectionString { get; set; } = "redis://localhost:6379";
public string ExternalUrl { get; set; } = "http://localhost:10060";
public string GameApiExternalUrl { get; set; } = "http://localhost:10060/LITTLEBIGPLANETPS3_XML";
public bool ConfigReloading { get; set; }
public string EulaText { get; set; } = "";
#if !DEBUG
public string AnnounceText { get; set; } = "You are now logged in as %user.";
@ -201,4 +46,6 @@ public class ServerConfiguration
public CustomizationConfiguration Customization { get; set; } = new();
public RateLimitConfiguration RateLimitConfiguration { get; set; } = new();
public TwoFactorConfiguration TwoFactorConfiguration { get; set; } = new();
public override ConfigurationBase<ServerConfiguration> Deserialize(IDeserializer deserializer, string text) => deserializer.Deserialize<ServerConfiguration>(text);
}

View file

@ -8,13 +8,12 @@ public static class VersionHelper
{
static VersionHelper()
{
string commitNumber = "invalid";
try
{
CommitHash = ResourceHelper.ReadManifestFile("gitVersion.txt");
Branch = ResourceHelper.ReadManifestFile("gitBranch.txt");
commitNumber = $"{CommitHash}_{Build}";
FullRevision = (Branch == "main") ? $"r{commitNumber}" : $"{Branch}_r{commitNumber}";
string commitNumber = $"{CommitHash}_{Build}";
FullRevision = Branch == "main" ? $"r{commitNumber}" : $"{Branch}_r{commitNumber}";
string remotesFile = ResourceHelper.ReadManifestFile("gitRemotes.txt");
@ -35,7 +34,6 @@ public static class VersionHelper
Logger.Error
(
"Project Lighthouse was built incorrectly. Please make sure git is available when building.",
// "Because of this, you will not be notified of updates.",
LogArea.Startup
);
CommitHash = "invalid";
@ -43,8 +41,8 @@ public static class VersionHelper
CanCheckForUpdates = false;
}
if (IsDirty)
{
if (!IsDirty) return;
Logger.Warn
(
"This is a modified version of Project Lighthouse. " +
@ -53,7 +51,6 @@ public static class VersionHelper
);
CanCheckForUpdates = false;
}
}
public static string CommitHash { get; set; }
public static string Branch { get; set; }

View file

@ -5,7 +5,7 @@ using IAspLogger = Microsoft.Extensions.Logging.ILogger;
namespace LBPUnion.ProjectLighthouse.Logging.Loggers.AspNet;
[ProviderAlias("Kettu")]
public class AspNetToLighthouseLoggerProvider : ILoggerProvider, IDisposable
public class AspNetToLighthouseLoggerProvider : ILoggerProvider
{
public void Dispose()
{

View file

@ -8,55 +8,22 @@ public class ConsoleLogger : ILogger
public void Log(LogLine logLine)
{
ConsoleColor oldForegroundColor = Console.ForegroundColor;
ConsoleColor logColor = logLine.Level.ToColor();
foreach (string line in logLine.Message.Split('\n'))
{
// The following is scuffed.
// Beware~
string time = DateTime.Now.ToString("MM/dd/yyyy-HH:mm:ss.fff");
Console.ForegroundColor = ConsoleColor.White;
Console.Write('[');
Console.ForegroundColor = logLine.Level.ToColor();
Console.Write(time);
Console.ForegroundColor = ConsoleColor.White;
Console.Write(']');
Console.Write(' ');
// Write the level! [Success]
Console.ForegroundColor = ConsoleColor.White;
Console.Write('[');
Console.ForegroundColor = logLine.Level.ToColor();
Console.Write(logLine.Area);
Console.ForegroundColor = ConsoleColor.White;
Console.Write(':');
Console.ForegroundColor = logLine.Level.ToColor();
Console.Write(logLine.Level);
Console.ForegroundColor = ConsoleColor.White;
Console.Write(']');
Console.ForegroundColor = oldForegroundColor;
Console.Write(' ');
string trace = "";
if (logLine.Trace.Name != null)
{
Console.ForegroundColor = ConsoleColor.White;
Console.Write('<');
Console.ForegroundColor = logLine.Level.ToColor();
Console.Write(logLine.Trace.Name);
if (logLine.Trace.Section != null)
{
Console.ForegroundColor = ConsoleColor.White;
Console.Write(':');
Console.ForegroundColor = logLine.Level.ToColor();
Console.Write(logLine.Trace.Section);
}
Console.ForegroundColor = ConsoleColor.White;
Console.Write('>');
Console.Write(' ');
Console.ForegroundColor = oldForegroundColor;
trace += logLine.Trace.Name;
if (logLine.Trace.Section != null) trace += ":" + logLine.Trace.Section;
trace = "[" + trace + "]";
}
Console.WriteLine(line);
Console.ForegroundColor = logColor;
Console.WriteLine(@$"[{time}] [{logLine.Area}/{logLine.Level.ToString().ToUpper()}] {trace}: {line}");
Console.ForegroundColor = oldForegroundColor;
}
}
}

View file

@ -1,10 +1,8 @@
#nullable enable
using System;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Serialization;
namespace LBPUnion.ProjectLighthouse.Configuration;
namespace LBPUnion.ProjectLighthouse.PlayerData;
[XmlRoot("privacySettings")]
[XmlType("privacySettings")]

View file

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
using LBPUnion.ProjectLighthouse.Administration;
using LBPUnion.ProjectLighthouse.Administration.Maintenance;
using LBPUnion.ProjectLighthouse.Configuration;
@ -32,10 +34,16 @@ public static class StartupTasks
Logger.Instance.AddLogger(new FileLogger());
Logger.Info($"Welcome to the Project Lighthouse {serverType.ToString()}!", LogArea.Startup);
Logger.Info($"You are running version {VersionHelper.FullVersion}", LogArea.Startup);
// Referencing ServerConfiguration.Instance here loads the config, see ServerConfiguration.cs for more information
Logger.Success("Loaded config file version " + ServerConfiguration.Instance.ConfigVersion, LogArea.Startup);
Logger.Info("Loading configurations...", LogArea.Startup);
if (!loadConfigurations())
{
Logger.Error("Failed to load one or more configurations", LogArea.Config);
Environment.Exit(1);
}
// Version info depends on ServerConfig
Logger.Info($"You are running version {VersionHelper.FullVersion}", LogArea.Startup);
Logger.Info("Connecting to the database...", LogArea.Startup);
bool dbConnected = ServerStatics.DbConnected;
@ -111,6 +119,41 @@ public static class StartupTasks
Logger.Success($"Ready! Startup took {stopwatch.ElapsedMilliseconds}ms. Passing off control to ASP.NET...", LogArea.Startup);
}
private static bool loadConfigurations()
{
Assembly assembly = Assembly.GetAssembly(typeof(ConfigurationBase<>));
if (assembly == null) return false;
bool didLoad = true;
foreach (Type type in assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract && t.BaseType?.Name == "ConfigurationBase`1"))
{
if (type.BaseType == null) continue;
if (type.BaseType.GetProperty("Instance") != null)
{
// force create lazy instance
type.BaseType.GetProperty("Instance")?.GetValue(null);
bool isConfigured = false;
while (!isConfigured)
{
isConfigured = (bool)(type.BaseType.GetProperty("IsConfigured")?.GetValue(null) ?? false);
Thread.Sleep(10);
}
}
object objRef = type.BaseType.GetProperty("Instance")?.GetValue(null);
int configVersion = ((int?)type.GetProperty("ConfigVersion")?.GetValue(objRef)).GetValueOrDefault();
if (configVersion <= 0)
{
didLoad = false;
}
else
{
Logger.Success($"Successfully loaded {type.Name} version {configVersion}", LogArea.Startup);
}
}
return didLoad;
}
private static void migrateDatabase(Database database)
{
Logger.Info("Migrating database...", LogArea.Database);