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

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 LBPUnion.ProjectLighthouse.Configuration.ConfigurationCategories;
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.Types; using LBPUnion.ProjectLighthouse.Types;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace LBPUnion.ProjectLighthouse.Configuration; namespace LBPUnion.ProjectLighthouse.Configuration;
[Serializable] public class ServerConfiguration : ConfigurationBase<ServerConfiguration>
public class ServerConfiguration
{ {
// HEY, YOU! // HEY, YOU!
// THIS VALUE MUST BE INCREMENTED FOR EVERY CONFIG CHANGE! // 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. // 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. // If you are modifying anything here, 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.
//
// Thanks for listening~ // Thanks for listening~
public const int CurrentConfigVersion = 16; public override int ConfigVersion { get; set; } = 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 string ConfigName { get; set; } = "lighthouse.yml";
public string WebsiteListenUrl { get; set; } = "http://localhost:10060"; public string WebsiteListenUrl { get; set; } = "http://localhost:10060";
public string GameApiListenUrl { get; set; } = "http://localhost:10061"; public string GameApiListenUrl { get; set; } = "http://localhost:10061";
public string ApiListenUrl { get; set; } = "http://localhost:10062"; 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 RedisConnectionString { get; set; } = "redis://localhost:6379";
public string ExternalUrl { get; set; } = "http://localhost:10060"; public string ExternalUrl { get; set; } = "http://localhost:10060";
public string GameApiExternalUrl { get; set; } = "http://localhost:10060/LITTLEBIGPLANETPS3_XML"; public string GameApiExternalUrl { get; set; } = "http://localhost:10060/LITTLEBIGPLANETPS3_XML";
public bool ConfigReloading { get; set; }
public string EulaText { get; set; } = ""; public string EulaText { get; set; } = "";
#if !DEBUG #if !DEBUG
public string AnnounceText { get; set; } = "You are now logged in as %user."; 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 CustomizationConfiguration Customization { get; set; } = new();
public RateLimitConfiguration RateLimitConfiguration { get; set; } = new(); public RateLimitConfiguration RateLimitConfiguration { get; set; } = new();
public TwoFactorConfiguration TwoFactorConfiguration { 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() static VersionHelper()
{ {
string commitNumber = "invalid";
try try
{ {
CommitHash = ResourceHelper.ReadManifestFile("gitVersion.txt"); CommitHash = ResourceHelper.ReadManifestFile("gitVersion.txt");
Branch = ResourceHelper.ReadManifestFile("gitBranch.txt"); Branch = ResourceHelper.ReadManifestFile("gitBranch.txt");
commitNumber = $"{CommitHash}_{Build}"; string commitNumber = $"{CommitHash}_{Build}";
FullRevision = (Branch == "main") ? $"r{commitNumber}" : $"{Branch}_r{commitNumber}"; FullRevision = Branch == "main" ? $"r{commitNumber}" : $"{Branch}_r{commitNumber}";
string remotesFile = ResourceHelper.ReadManifestFile("gitRemotes.txt"); string remotesFile = ResourceHelper.ReadManifestFile("gitRemotes.txt");
@ -35,7 +34,6 @@ public static class VersionHelper
Logger.Error Logger.Error
( (
"Project Lighthouse was built incorrectly. Please make sure git is available when building.", "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 LogArea.Startup
); );
CommitHash = "invalid"; CommitHash = "invalid";
@ -43,16 +41,15 @@ public static class VersionHelper
CanCheckForUpdates = false; CanCheckForUpdates = false;
} }
if (IsDirty) if (!IsDirty) return;
{
Logger.Warn Logger.Warn
( (
"This is a modified version of Project Lighthouse. " + "This is a modified version of Project Lighthouse. " +
"Please make sure you are properly disclosing the source code to any users who may be using this instance.", "Please make sure you are properly disclosing the source code to any users who may be using this instance.",
LogArea.Startup LogArea.Startup
); );
CanCheckForUpdates = false; CanCheckForUpdates = false;
}
} }
public static string CommitHash { get; set; } public static string CommitHash { get; set; }

View file

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

View file

@ -8,55 +8,22 @@ public class ConsoleLogger : ILogger
public void Log(LogLine logLine) public void Log(LogLine logLine)
{ {
ConsoleColor oldForegroundColor = Console.ForegroundColor; ConsoleColor oldForegroundColor = Console.ForegroundColor;
ConsoleColor logColor = logLine.Level.ToColor();
foreach (string line in logLine.Message.Split('\n')) 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"); string time = DateTime.Now.ToString("MM/dd/yyyy-HH:mm:ss.fff");
Console.ForegroundColor = ConsoleColor.White; string trace = "";
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(' ');
if (logLine.Trace.Name != null) if (logLine.Trace.Name != null)
{ {
Console.ForegroundColor = ConsoleColor.White; trace += logLine.Trace.Name;
Console.Write('<'); if (logLine.Trace.Section != null) trace += ":" + logLine.Trace.Section;
Console.ForegroundColor = logLine.Level.ToColor(); trace = "[" + trace + "]";
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;
} }
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 #nullable enable
using System;
using System.Xml.Serialization; using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Serialization;
namespace LBPUnion.ProjectLighthouse.Configuration; namespace LBPUnion.ProjectLighthouse.PlayerData;
[XmlRoot("privacySettings")] [XmlRoot("privacySettings")]
[XmlType("privacySettings")] [XmlType("privacySettings")]

View file

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Threading;
using LBPUnion.ProjectLighthouse.Administration; using LBPUnion.ProjectLighthouse.Administration;
using LBPUnion.ProjectLighthouse.Administration.Maintenance; using LBPUnion.ProjectLighthouse.Administration.Maintenance;
using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Configuration;
@ -32,10 +34,16 @@ public static class StartupTasks
Logger.Instance.AddLogger(new FileLogger()); Logger.Instance.AddLogger(new FileLogger());
Logger.Info($"Welcome to the Project Lighthouse {serverType.ToString()}!", LogArea.Startup); 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.Info("Loading configurations...", LogArea.Startup);
Logger.Success("Loaded config file version " + ServerConfiguration.Instance.ConfigVersion, 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); Logger.Info("Connecting to the database...", LogArea.Startup);
bool dbConnected = ServerStatics.DbConnected; bool dbConnected = ServerStatics.DbConnected;
@ -55,7 +63,7 @@ public static class StartupTasks
if (serverType == ServerType.GameServer) if (serverType == ServerType.GameServer)
#endif #endif
migrateDatabase(database); migrateDatabase(database);
if (ServerConfiguration.Instance.InfluxDB.InfluxEnabled) if (ServerConfiguration.Instance.InfluxDB.InfluxEnabled)
{ {
Logger.Info("Influx logging is enabled. Starting influx logging...", LogArea.Startup); Logger.Info("Influx logging is enabled. Starting influx logging...", LogArea.Startup);
@ -84,10 +92,10 @@ public static class StartupTasks
{ {
FileHelper.ConvertAllTexturesToPng(); FileHelper.ConvertAllTexturesToPng();
} }
Logger.Info("Initializing Redis...", LogArea.Startup); Logger.Info("Initializing Redis...", LogArea.Startup);
RedisDatabase.Initialize().Wait(); RedisDatabase.Initialize().Wait();
Logger.Info("Initializing repeating tasks...", LogArea.Startup); Logger.Info("Initializing repeating tasks...", LogArea.Startup);
RepeatingTaskHandler.Initialize(); RepeatingTaskHandler.Initialize();
@ -111,6 +119,41 @@ public static class StartupTasks
Logger.Success($"Ready! Startup took {stopwatch.ElapsedMilliseconds}ms. Passing off control to ASP.NET...", LogArea.Startup); 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) private static void migrateDatabase(Database database)
{ {
Logger.Info("Migrating database...", LogArea.Database); Logger.Info("Migrating database...", LogArea.Database);