From 9922c8f5a81519ed342ffba23708ab21bc49f06c Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 5 Nov 2020 14:54:07 -0500 Subject: [PATCH] Docker cleanup - Add container script to master version list - Setup wizard now waits for a guaranteed config reload Fixes #1138 --- README.md | 28 +++++----- build/Version.props | 1 + .../Setup/SetupWizard.cs | 24 ++++++++ .../apspsettings.Development.json | 34 +++++++++++ .../Setup/TestSetupWizard.cs | 56 +++++++++++++++---- tests/Tgstation.Server.Tests/VersionsTest.cs | 16 +++++- 6 files changed, 132 insertions(+), 27 deletions(-) create mode 100644 src/Tgstation.Server.Host/apspsettings.Development.json diff --git a/README.md b/README.md index bce40b776f..a79e7e8f93 100644 --- a/README.md +++ b/README.md @@ -51,22 +51,24 @@ tgstation-server supports running in a docker container and is the recommended d To create a container run ```sh docker run \ - -ti \ #start interactive for manual configuration - --restart=always \ #if you want maximum uptime - --network="host" \ #if your sql server is on the same machine - --name="tgs" \ #or whatever else you wanna call it - --cap-add=sys_nice \ #allows tgs to schedule DreamDaemon as a higher priority process - --init \ #reaps potential zombie processes - -p :80 \ - -p 0.0.0.0:: \ - -v /path/to/your/configfile/directory:/config_data \ #only if you want to use manual configuration - -v /path/to/store/instances:/tgs4_instances \ - -v /path/to/your/log/folder:/tgs_logs \ - tgstation/server: #replace this with if you built the image locally + -ti \ # Start with interactive terminal the first time to run the setup wizard + --restart=always \ # Recommended for maximum uptime + --network="host" \ # Not recommended, eases networking setup if your sql server is on the same machine + --name="tgs" \ # Name for the container + --cap-add=sys_nice \ # Recommended, allows tgs to schedule DreamDaemon as a higher priority process + --init \ #Highly recommended, reaps potential zombie processes + -p : \ # Port bridge for accessing TGS + -p 0.0.0.0:: \ # Port bridge for accessing DreamDaemon + -v /path/to/your/configfile/directory:/config_data \ # Recommended, create a volume mapping for server configuration + -v /path/to/store/instances:/tgs4_instances \ # Recommended, create a volume mapping for server instances + -v /path/to/your/log/folder:/tgs_logs \ # Recommended, create a volume mapping for server logs + tgstation/server[:] ``` with any additional options you desire (i.e. You'll have to expose more game ports in order to host more than one instance). -- Important note about port exposure: The internal port used by DreamDaemon _**MUST**_ match the port you want users to connect on. If it doesn't, you'll still be able to have them connect HOWEVER links from the BYOND hub will point at what DreamDaemon thinks the port is. +When launching the container for the first time, you'll be prompted with the setup wizard. + +Important note about port exposure: The internal port used by DreamDaemon _**MUST**_ match the port you want users to connect on. If it doesn't, you'll still be able to have them connect HOWEVER links from the BYOND hub will point at what DreamDaemon thinks the port is (the internal port). Note although `/app/lib` is specified as a volume mount point in the `Dockerfile`, unless you REALLY know what you're doing. Do not mount any volumes over this for fear of breaking your container. diff --git a/build/Version.props b/build/Version.props index e6845264db..7910b7779c 100644 --- a/build/Version.props +++ b/build/Version.props @@ -9,5 +9,6 @@ 5.2.7 0.4.0 1.1.0 + 1.1.0 diff --git a/src/Tgstation.Server.Host/Setup/SetupWizard.cs b/src/Tgstation.Server.Host/Setup/SetupWizard.cs index 31b934258e..efecf6cd04 100644 --- a/src/Tgstation.Server.Host/Setup/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Setup/SetupWizard.cs @@ -1,4 +1,5 @@ using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -72,6 +73,11 @@ namespace Tgstation.Server.Host.Setup /// readonly GeneralConfiguration generalConfiguration; + /// + /// A that will complete when the is reloaded. + /// + TaskCompletionSource reloadTcs; + /// /// Construct a /// @@ -83,6 +89,7 @@ namespace Tgstation.Server.Host.Setup /// The value of /// The value of /// The value of . + /// The in use. /// The containing the value of public SetupWizard( IIOManager ioManager, @@ -93,6 +100,7 @@ namespace Tgstation.Server.Host.Setup IPlatformIdentifier platformIdentifier, IAsyncDelayer asyncDelayer, IHostApplicationLifetime applicationLifetime, + IConfiguration configuration, IOptions generalConfigurationOptions) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); @@ -103,7 +111,16 @@ namespace Tgstation.Server.Host.Setup this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.applicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime)); + if (configuration == null) + throw new ArgumentNullException(nameof(configuration)); + generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + + configuration + .GetReloadToken() + .RegisterChangeCallback( + state => reloadTcs?.TrySetResult(null), + null); } /// @@ -758,9 +775,16 @@ namespace Tgstation.Server.Host.Setup var json = JsonConvert.SerializeObject(map, Formatting.Indented); var configBytes = Encoding.UTF8.GetBytes(json); + reloadTcs = new TaskCompletionSource(); + try { await ioManager.WriteAllBytes(userConfigFileName, configBytes, cancellationToken).ConfigureAwait(false); + + // Ensure the reload + if (generalConfiguration.SetupWizardMode != SetupWizardMode.Only) + using (cancellationToken.Register(() => reloadTcs.TrySetCanceled())) + await reloadTcs.Task.ConfigureAwait(false); } catch (OperationCanceledException) { diff --git a/src/Tgstation.Server.Host/apspsettings.Development.json b/src/Tgstation.Server.Host/apspsettings.Development.json new file mode 100644 index 0000000000..0f44fffaec --- /dev/null +++ b/src/Tgstation.Server.Host/apspsettings.Development.json @@ -0,0 +1,34 @@ +{ + "Database": { + "DatabaseType": "SqlServer", + "ResetAdminPassword": false, + "ConnectionString": "Data Source=(local);Initial Catalog=TGS_Debug2341;Integrated Security=True;Application Name=tgstation-server", + "DropDatabase": false, + "DesignTime": false, + "ServerVersion": null + }, + "General": { + "ConfigVersion": "2.0.0", + "ApiPort": 5000, + "GitHubAccessToken": null, + "SetupWizardMode": "Never", + "ByondTopicTimeout": 5000, + "RestartTimeout": 60000, + "UseBasicWatchdog": false, + "MinimumPasswordLength": 15, + "InstanceLimit": 10, + "UserLimit": 100, + "ValidInstancePaths": null + }, + "FileLogging": { + "Directory": null, + "Disable": false, + "LogLevel": "Trace", + "MicrosoftLogLevel": "Warning" + }, + "ControlPanel": { + "Enable": true, + "AllowAnyOrigin": true, + "AllowedOrigins": null + } +} diff --git a/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs b/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs index 3d99525bdf..f45af4db4b 100644 --- a/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs +++ b/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Moq.Protected; @@ -24,23 +26,26 @@ namespace Tgstation.Server.Host.Setup.Tests [TestMethod] public void TestConstructionThrows() { - Assert.ThrowsException(() => new SetupWizard(null, null, null, null, null, null, null, null, null)); + Assert.ThrowsException(() => new SetupWizard(null, null, null, null, null, null, null, null, null, null)); var mockIOManager = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, null, null, null, null, null, null, null, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, null, null, null, null, null, null, null, null, null)); var mockConsole = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, null, null, null, null, null, null, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, null, null, null, null, null, null, null, null)); var mockHostingEnvironment = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, null, null, null, null, null, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, null, null, null, null, null, null, null)); var mockAssemblyInfoProvider = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, null, null, null, null, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, null, null, null, null, null, null)); var mockDBConnectionFactory = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, null, null, null, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, null, null, null, null, null)); var mockPlatformIdentifier = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, null, null, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, null, null, null, null)); var mockAsyncDelayer = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, null, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, null, null, null)); var mockLifetime = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, null, null)); + var mockConfiguration = new Mock(); + mockConfiguration.Setup(x => x.GetReloadToken()).Returns(Mock.Of()); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, mockConfiguration.Object, null)); } [TestMethod] @@ -55,6 +60,23 @@ namespace Tgstation.Server.Host.Setup.Tests var mockGeneralConfigurationOptions = new Mock>(); var mockPlatformIdentifier = new Mock(); var mockAsyncDelayer = new Mock(); + var mockConfiguration = new Mock(); + var mockChangeToken = new Mock(); + + object configReloadCallbackState = null; + Action configReloadCallback = null; + mockChangeToken + .Setup(x => x.RegisterChangeCallback(It.IsNotNull>(), null)) + .Callback, object>((callback, state) => + { + configReloadCallback = callback; + configReloadCallbackState = state; + }); + + mockConfiguration + .Setup(x => x.GetReloadToken()) + .Returns(mockChangeToken.Object) + .Verifiable(); var testGeneralConfig = new GeneralConfiguration { @@ -62,7 +84,8 @@ namespace Tgstation.Server.Host.Setup.Tests }; mockGeneralConfigurationOptions.SetupGet(x => x.Value).Returns(testGeneralConfig).Verifiable(); - var wizard = new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, mockGeneralConfigurationOptions.Object); + var wizard = new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, mockConfiguration.Object, mockGeneralConfigurationOptions.Object); + mockPlatformIdentifier.SetupGet(x => x.IsWindows).Returns(true).Verifiable(); mockAsyncDelayer.Setup(x => x.Delay(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask).Verifiable(); @@ -78,7 +101,11 @@ namespace Tgstation.Server.Host.Setup.Tests mockConsole.SetupGet(x => x.Available).Returns(true).Verifiable(); mockIOManager.Setup(x => x.FileExists(It.IsNotNull(), It.IsAny())).Returns(Task.FromResult(true)).Verifiable(); mockIOManager.Setup(x => x.ReadAllBytes(It.IsNotNull(), It.IsAny())).Returns(Task.FromResult(Encoding.UTF8.GetBytes("less profane"))).Verifiable(); - mockIOManager.Setup(x => x.WriteAllBytes(It.IsNotNull(), It.IsNotNull(), It.IsAny())).Returns(Task.CompletedTask).Verifiable(); + mockIOManager + .Setup(x => x.WriteAllBytes(It.IsNotNull(), It.IsNotNull(), It.IsAny())) + .Callback(() => configReloadCallback(configReloadCallbackState)) + .Returns(Task.CompletedTask) + .Verifiable(); var mockSuccessCommand = new Mock(); mockSuccessCommand.Setup(x => x.ExecuteNonQueryAsync(It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); @@ -238,7 +265,10 @@ namespace Tgstation.Server.Host.Setup.Tests //third run testGeneralConfig.SetupWizardMode = SetupWizardMode.Autodetect; - mockIOManager.Setup(x => x.WriteAllBytes(It.IsNotNull(), It.IsNotNull(), It.IsAny())).Throws(new Exception()).Verifiable(); + mockIOManager + .Setup(x => x.WriteAllBytes(It.IsNotNull(), It.IsNotNull(), It.IsAny())) + .Throws(new Exception()) + .Verifiable(); var firstRun = true; mockIOManager.Setup(x => x.CreateDirectory(It.IsNotNull(), It.IsAny())).Returns(() => { @@ -265,6 +295,8 @@ namespace Tgstation.Server.Host.Setup.Tests mockAssemblyInfoProvider.VerifyAll(); mockPlatformIdentifier.VerifyAll(); mockAsyncDelayer.VerifyAll(); + mockConfiguration.VerifyAll(); + mockChangeToken.VerifyAll(); } } } diff --git a/tests/Tgstation.Server.Tests/VersionsTest.cs b/tests/Tgstation.Server.Tests/VersionsTest.cs index e3523c3d0d..b6b6e8e6a7 100644 --- a/tests/Tgstation.Server.Tests/VersionsTest.cs +++ b/tests/Tgstation.Server.Tests/VersionsTest.cs @@ -1,14 +1,14 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json.Linq; using System; using System.IO; using System.Linq; +using System.Threading.Tasks; using System.Xml.Linq; using Tgstation.Server.Api; using Tgstation.Server.Client; using Tgstation.Server.Host; using Tgstation.Server.Host.Components.Interop; -using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Configuration; namespace Tgstation.Server.Tests @@ -127,5 +127,17 @@ namespace Tgstation.Server.Tests Assert.AreEqual(expected.Build, actual.Build); Assert.AreEqual(-1, actual.Revision); } + + [TestMethod] + public async Task TestContainerScriptVersion() + { + var versionString = versionsPropertyGroup.Element(xmlNamespace + "TgsContainerScriptVersion").Value; + Assert.IsNotNull(versionString); + Assert.IsTrue(Version.TryParse(versionString, out var expected)); + var scriptLines = await File.ReadAllLinesAsync("../../../../../build/tgs.docker.sh"); + + var line = scriptLines.FirstOrDefault(x => x.Trim().Contains($"SCRIPT_VERSION=\"{expected.Semver()}\"")); + Assert.IsNotNull(line); + } } }