mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-05-14 05:42:27 +00:00
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:
parent
7179574e43
commit
c86d2a11b5
9 changed files with 310 additions and 443 deletions
|
@ -10,13 +10,10 @@ 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)
|
||||
lock (migrateLock)
|
||||
{
|
||||
using Database database = new();
|
||||
database.Database.Migrate();
|
||||
|
|
231
ProjectLighthouse/Configuration/ConfigurationBase.cs
Normal file
231
ProjectLighthouse/Configuration/ConfigurationBase.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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,16 +41,15 @@ public static class VersionHelper
|
|||
CanCheckForUpdates = false;
|
||||
}
|
||||
|
||||
if (IsDirty)
|
||||
{
|
||||
Logger.Warn
|
||||
(
|
||||
"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.",
|
||||
LogArea.Startup
|
||||
);
|
||||
CanCheckForUpdates = false;
|
||||
}
|
||||
if (!IsDirty) return;
|
||||
|
||||
Logger.Warn
|
||||
(
|
||||
"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.",
|
||||
LogArea.Startup
|
||||
);
|
||||
CanCheckForUpdates = false;
|
||||
}
|
||||
|
||||
public static string CommitHash { get; set; }
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")]
|
|
@ -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;
|
||||
|
@ -55,7 +63,7 @@ public static class StartupTasks
|
|||
if (serverType == ServerType.GameServer)
|
||||
#endif
|
||||
migrateDatabase(database);
|
||||
|
||||
|
||||
if (ServerConfiguration.Instance.InfluxDB.InfluxEnabled)
|
||||
{
|
||||
Logger.Info("Influx logging is enabled. Starting influx logging...", LogArea.Startup);
|
||||
|
@ -84,10 +92,10 @@ public static class StartupTasks
|
|||
{
|
||||
FileHelper.ConvertAllTexturesToPng();
|
||||
}
|
||||
|
||||
|
||||
Logger.Info("Initializing Redis...", LogArea.Startup);
|
||||
RedisDatabase.Initialize().Wait();
|
||||
|
||||
|
||||
Logger.Info("Initializing repeating tasks...", LogArea.Startup);
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue