diff --git a/README.md b/README.md index 1280ec519c..aa7a1bba4c 100644 --- a/README.md +++ b/README.md @@ -57,15 +57,15 @@ Note although `/app/lib` is specified as a volume mount point in the `Dockerfile ### Configuring -Create an `appsettings.Production.json` file next to `appsettings.json`. This will override the default settings in appsettings.json with your production settings. There are a few keys meant to be changed by hosts: - -- `General:LogFileDirectory`: Override the default directory where server logs are stored. Default is C:/ProgramData/tgstation-server/logs on Windows, /usr/share/tgstation-server/logs otherwise +Create an `appsettings.Production.json` file next to `appsettings.json`. This will override the default settings in appsettings.json with your production settings. There are a few keys meant to be changed by hosts. Note these are all case-sensitive: - `General:MinimumPasswordLength`: Minimum password length requirement for database users - `General:GitHubAccessToken`: Specify a GitHub personal access token with no scopes here to highly mitigate the possiblity of 429 response codes from GitHub requests -- `General:LogFileLevel`: Can be one of `Trace`, `Debug`, `Information`, `Warning`, `Error`, or `Critical`. Restricts what is put into the log files. Currently `Debug` is reccommended for help with error reporting. +- `FileLogging:Directory`: Override the default directory where server logs are stored. Default is C:/ProgramData/tgstation-server/logs on Windows, /usr/share/tgstation-server/logs otherwise + +- `FileLogging:LogLevel`: Can be one of `Trace`, `Debug`, `Information`, `Warning`, `Error`, or `Critical`. Restricts what is put into the log files. Currently `Debug` is reccommended for help with error reporting. - `Kestrel:Endpoints:Http:Url`: The URL (i.e. interface and ports) your application should listen on. General use case should be `http://localhost:` for restricted local connections. See the Remote Access section for configuring public access to the World Wide Web. This doesn't need to be changed using the docker setup and should be mapped with the `-p` option instead diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index af7da0a334..e11d311609 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -39,6 +39,11 @@ namespace Tgstation.Server.Host.Components.Chat /// readonly IRestartRegistration restartRegistration; + /// + /// The for the + /// + readonly ILoggerFactory loggerFactory; + /// /// The for the /// @@ -104,17 +109,19 @@ namespace Tgstation.Server.Host.Components.Chat /// /// The value of /// The value of + /// The value of /// The value of /// The value of /// The to populate with /// The used to populate - public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, ILogger logger, IEnumerable initialChatBots) + public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, ILoggerFactory loggerFactory, ILogger logger, IEnumerable initialChatBots) { this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory)); if (serverControl == null) throw new ArgumentNullException(nameof(serverControl)); + this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.initialChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots)); @@ -263,7 +270,7 @@ namespace Tgstation.Server.Host.Components.Chat if (splits.Count == 0) { var allCommands = builtinCommands.Select(x => x.Value).ToList(); - var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken)); + var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken)).ToList(); await Task.WhenAll(tasks).ConfigureAwait(false); allCommands.AddRange(tasks.SelectMany(x => x.Result)); helpText = String.Format(CultureInfo.InvariantCulture, "Available commands (Type '?' or 'help' and then a command name for more details): {0}", String.Join(", ", allCommands.Select(x => x.Name))); @@ -529,7 +536,7 @@ namespace Tgstation.Server.Host.Components.Chat if (customCommandHandler == null) throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!"); JsonTrackingContext context = null; - context = new JsonTrackingContext(ioManager, customCommandHandler, () => + context = new JsonTrackingContext(ioManager, customCommandHandler, loggerFactory.CreateLogger(), () => { lock (trackingContexts) trackingContexts.Remove(context); diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs index 8b3db001ce..28e1323dde 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs @@ -39,6 +39,6 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public IChat CreateChat(IIOManager ioManager, ICommandFactory commandFactory, IEnumerable initialChatBots) => new Chat(providerFactory, ioManager, commandFactory, serverControl, loggerFactory.CreateLogger(), initialChatBots); + public IChat CreateChat(IIOManager ioManager, ICommandFactory commandFactory, IEnumerable initialChatBots) => new Chat(providerFactory, ioManager, commandFactory, serverControl, loggerFactory, loggerFactory.CreateLogger(), initialChatBots); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs b/src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs index 778b6d0d3c..1a5ec65b7f 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IJsonTrackingContext.cs @@ -11,6 +11,11 @@ namespace Tgstation.Server.Host.Components.Chat /// public interface IJsonTrackingContext : IDisposable { + /// + /// If the should be used for + /// + bool Active { get; set; } + /// /// Read s from the /// diff --git a/src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs b/src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs index f5bbcd558f..1efa732ef9 100644 --- a/src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs +++ b/src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; @@ -14,8 +15,20 @@ namespace Tgstation.Server.Host.Components.Chat /// sealed class JsonTrackingContext : IJsonTrackingContext { + /// + public bool Active + { + get => active; + set + { + active = true; + logger.LogDebug("Tracking {0}activated", !active ? "de" : String.Empty); + } + } + readonly IIOManager ioManager; readonly ICustomCommandHandler customCommandHandler; + readonly ILogger logger; readonly Action onDispose; readonly string commandsPath; @@ -23,52 +36,71 @@ namespace Tgstation.Server.Host.Components.Chat readonly SemaphoreSlim channelsSemaphore; - public JsonTrackingContext(IIOManager ioManager, ICustomCommandHandler customCommandHandler, Action onDispose, string commandsPath, string channelsPath) + bool active; + + public JsonTrackingContext(IIOManager ioManager, ICustomCommandHandler customCommandHandler, ILogger logger, Action onDispose, string commandsPath, string channelsPath) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); this.commandsPath = commandsPath ?? throw new ArgumentNullException(nameof(commandsPath)); this.channelsPath = channelsPath ?? throw new ArgumentNullException(nameof(channelsPath)); channelsSemaphore = new SemaphoreSlim(1); + active = false; + + logger.LogTrace("Created tracking context for {0} and {1}", commandsPath, channelsPath); } /// - public void Dispose() => onDispose(); + public void Dispose() + { + logger.LogTrace("Disposing..."); + onDispose(); + } /// public async Task> GetCustomCommands(CancellationToken cancellationToken) { try { - var resultBytes = await ioManager.ReadAllBytes(commandsPath, cancellationToken).ConfigureAwait(false); - var resultJson = Encoding.UTF8.GetString(resultBytes); - var result = JsonConvert.DeserializeObject>(resultJson, new JsonSerializerSettings + if (Active && await ioManager.FileExists(commandsPath, cancellationToken).ConfigureAwait(false)) { - ContractResolver = new DefaultContractResolver + var resultBytes = await ioManager.ReadAllBytes(commandsPath, cancellationToken).ConfigureAwait(false); + var resultJson = Encoding.UTF8.GetString(resultBytes); + logger.LogTrace("Read commands JSON: {0}", resultJson); + var result = JsonConvert.DeserializeObject>(resultJson, new JsonSerializerSettings { - NamingStrategy = new SnakeCaseNamingStrategy() - } - }); - foreach (var I in result) - I.SetHandler(customCommandHandler); - return result; + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + } + }); + foreach (var I in result) + I.SetHandler(customCommandHandler); + return result; + } } - catch + catch (Exception e) { - return new List(); + logger.LogWarning("Error retrieving custom commands! Exception: {0}", e); } + return new List(); } /// public async Task SetChannels(IEnumerable channels, CancellationToken cancellationToken) { using (await SemaphoreSlimContext.Lock(channelsSemaphore, cancellationToken).ConfigureAwait(false)) - await ioManager.WriteAllBytes(channelsPath, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(channels, Formatting.Indented, new JsonSerializerSettings + { + var json = JsonConvert.SerializeObject(channels, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() - })), cancellationToken).ConfigureAwait(false); + }); + logger.LogTrace("Writing channels JSON: {0}", json); + await ioManager.WriteAllBytes(channelsPath, Encoding.UTF8.GetBytes(json), cancellationToken).ConfigureAwait(false); + } } } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs index 97e19b271d..6b21bbd6ca 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs @@ -88,5 +88,10 @@ namespace Tgstation.Server.Host.Components.Watchdog /// Changes to without telling the DMAPI /// void ResetRebootState(); + + /// + /// Enables the reading of custom chat commands from the + /// + void EnableCustomChatCommands(); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs index eeacaa941a..dddee90d3b 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs @@ -200,6 +200,8 @@ namespace Tgstation.Server.Host.Components.Watchdog rebootTcs = new TaskCompletionSource(); + process.Lifetime.ContinueWith(x => chatJsonTrackingContext.Active = false, TaskScheduler.Current); + async Task GetLaunchResult() { var startTime = DateTimeOffset.Now; @@ -353,6 +355,7 @@ namespace Tgstation.Server.Host.Components.Watchdog case Constants.DMCommandWorldReboot: if (ClosePortOnReboot) { + chatJsonTrackingContext.Active = false; content = new Dictionary { { Constants.DMParameterData, 0 } }; portClosedForReboot = true; } @@ -372,7 +375,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var response = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicInteropResponse), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json)), overrideResponsePort, cancellationToken).ConfigureAwait(false); if (response != Constants.DMResponseSuccess) - logger.LogWarning("Recieved error response while responding to interop: {0}", response); + logger.LogWarning("Received error response while responding to interop: {0}", response); postRespond?.Invoke(); } @@ -386,6 +389,9 @@ namespace Tgstation.Server.Host.Components.Watchdog throw new ObjectDisposedException(nameof(SessionController)); } + /// + public void EnableCustomChatCommands() => chatJsonTrackingContext.Active = true; + /// public ReattachInformation Release() { diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs index d4af485ee8..118c1020b1 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs @@ -175,7 +175,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var interopJsonFile = JsonFile("interop"); - var interopJson = JsonConvert.SerializeObject(interopInfo, Formatting.Indented, new JsonSerializerSettings + var interopJson = JsonConvert.SerializeObject(interopInfo, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver(), ReferenceLoopHandling = ReferenceLoopHandling.Ignore diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index f2c1f12673..3cfff5c9bc 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -275,6 +275,7 @@ namespace Tgstation.Server.Host.Components.Watchdog monitorState.ActiveServer = monitorState.InactiveServer; monitorState.InactiveServer = tmp; AlphaIsActive = !AlphaIsActive; + monitorState.ActiveServer.EnableCustomChatCommands(); return true; } @@ -598,10 +599,11 @@ namespace Tgstation.Server.Host.Components.Watchdog } catch (Exception e) { - logger.LogError("Monitor crashed! Iteration: {0}, State: {1}", iteration, JsonConvert.SerializeObject(monitorState)); + logger.LogError("Monitor crashed! Iteration: {0}, State: {1}, Exception: {2}", iteration, JsonConvert.SerializeObject(monitorState), e); await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Monitor crashed, this should NEVER happen! Please report this, full details in logs! Restarting monitor... Error: {0}", e.Message), cancellationToken).ConfigureAwait(false); } } + logger.LogTrace("Monitor exiting..."); } async Task StopMonitor() @@ -716,7 +718,10 @@ namespace Tgstation.Server.Host.Components.Watchdog //both servers are now running, alpha is the active server(unless reattach), huzzah AlphaIsActive = doReattach ? reattachInfo?.AlphaIsActive ?? true : true; LastLaunchResult = alphaLrt.Result; - (AlphaIsActive ? alphaServer : bravoServer).ClosePortOnReboot = true; + + var activeServer = AlphaIsActive ? alphaServer : bravoServer; + activeServer.EnableCustomChatCommands(); + activeServer.ClosePortOnReboot = true; logger.LogInformation("Launched servers successfully"); Running = true; diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 7466235fd9..095649224a 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -201,15 +201,13 @@ namespace Tgstation.Server.Host.Core var databaseConfiguration = databaseConfigurationSection.Get(); - void ConfigureDatabase(DbContextOptionsBuilder builder) - { - if (hostingEnvironment.IsDevelopment()) - builder.EnableSensitiveDataLogging(); - }; - void AddTypedContext() where TContext : DatabaseContext { - services.AddDbContext(ConfigureDatabase); + services.AddDbContext(builder => + { + if (hostingEnvironment.IsDevelopment()) + builder.EnableSensitiveDataLogging(); + }); services.AddScoped(x => x.GetRequiredService()); } @@ -297,6 +295,8 @@ namespace Tgstation.Server.Host.Core throw new ArgumentNullException(nameof(applicationBuilder)); if (logger == null) throw new ArgumentNullException(nameof(logger)); + if (serverControl == null) + throw new ArgumentNullException(nameof(serverControl)); logger.LogInformation(VersionString);