Merge branch 'master' into dev

This commit is contained in:
tgstation-server
2020-11-05 20:38:13 +00:00
6 changed files with 131 additions and 26 deletions
+15 -13
View File
@@ -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 <tgs port>:80 \
-p 0.0.0.0:<public game port>:<public game port> \
-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:<release version> #replace this with <your tag name> 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 <tgs port>:<port configured with setup wizard> \ # Port bridge for accessing TGS
-p 0.0.0.0:<public game port>:<public game port> \ # 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[:<release version>]
```
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.
+1
View File
@@ -9,6 +9,7 @@
<TgsClientVersion>8.4.0</TgsClientVersion>
<TgsDmapiVersion>5.2.7</TgsDmapiVersion>
<TgsHostWatchdogVersion>1.1.0</TgsHostWatchdogVersion>
<TgsContainerScriptVersion>1.1.0</TgsContainerScriptVersion>
</PropertyGroup>
</Project>
@@ -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
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// A <see cref="TaskCompletionSource{TResult}"/> that will complete when the <see cref="IConfiguration"/> is reloaded.
/// </summary>
TaskCompletionSource<object> reloadTcs;
/// <summary>
/// Construct a <see cref="SetupWizard"/>
/// </summary>
@@ -83,6 +89,7 @@ namespace Tgstation.Server.Host.Setup
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/></param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/></param>
/// <param name="applicationLifetime">The value of <see cref="applicationLifetime"/>.</param>
/// <param name="configuration">The <see cref="IConfiguration"/> in use.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/></param>
public SetupWizard(
IIOManager ioManager,
@@ -93,6 +100,7 @@ namespace Tgstation.Server.Host.Setup
IPlatformIdentifier platformIdentifier,
IAsyncDelayer asyncDelayer,
IHostApplicationLifetime applicationLifetime,
IConfiguration configuration,
IOptions<GeneralConfiguration> 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);
}
/// <summary>
@@ -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<object>();
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)
{
@@ -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
}
}
@@ -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<ArgumentNullException>(() => new SetupWizard(null, null, null, null, null, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(null, null, null, null, null, null, null, null, null, null));
var mockIOManager = new Mock<IIOManager>();
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, null, null, null, null, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, null, null, null, null, null, null, null, null, null));
var mockConsole = new Mock<IConsole>();
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, null, null, null, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, null, null, null, null, null, null, null, null));
var mockHostingEnvironment = new Mock<IWebHostEnvironment>();
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, null, null, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, null, null, null, null, null, null, null));
var mockAssemblyInfoProvider = new Mock<IAssemblyInformationProvider>();
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, null, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, null, null, null, null, null, null));
var mockDBConnectionFactory = new Mock<IDatabaseConnectionFactory>();
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, null, null, null, null, null));
var mockPlatformIdentifier = new Mock<IPlatformIdentifier>();
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, null, null, null, null));
var mockAsyncDelayer = new Mock<IAsyncDelayer>();
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, null, null, null));
var mockLifetime = new Mock<IHostApplicationLifetime>();
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, null));
Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, null, null));
var mockConfiguration = new Mock<IConfiguration>();
mockConfiguration.Setup(x => x.GetReloadToken()).Returns(Mock.Of<IChangeToken>());
Assert.ThrowsException<ArgumentNullException>(() => 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<IOptions<GeneralConfiguration>>();
var mockPlatformIdentifier = new Mock<IPlatformIdentifier>();
var mockAsyncDelayer = new Mock<IAsyncDelayer>();
var mockConfiguration = new Mock<IConfiguration>();
var mockChangeToken = new Mock<IChangeToken>();
object configReloadCallbackState = null;
Action<object> configReloadCallback = null;
mockChangeToken
.Setup(x => x.RegisterChangeCallback(It.IsNotNull<Action<object>>(), null))
.Callback<Action<object>, 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<TimeSpan>(), It.IsAny<CancellationToken>())).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<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(true)).Verifiable();
mockIOManager.Setup(x => x.ReadAllBytes(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(Encoding.UTF8.GetBytes("less profane"))).Verifiable();
mockIOManager.Setup(x => x.WriteAllBytes(It.IsNotNull<string>(), It.IsNotNull<byte[]>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
mockIOManager
.Setup(x => x.WriteAllBytes(It.IsNotNull<string>(), It.IsNotNull<byte[]>(), It.IsAny<CancellationToken>()))
.Callback(() => configReloadCallback(configReloadCallbackState))
.Returns(Task.CompletedTask)
.Verifiable();
var mockSuccessCommand = new Mock<DbCommand>();
mockSuccessCommand.Setup(x => x.ExecuteNonQueryAsync(It.IsAny<CancellationToken>())).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<string>(), It.IsNotNull<byte[]>(), It.IsAny<CancellationToken>())).Throws(new Exception()).Verifiable();
mockIOManager
.Setup(x => x.WriteAllBytes(It.IsNotNull<string>(), It.IsNotNull<byte[]>(), It.IsAny<CancellationToken>()))
.Throws(new Exception())
.Verifiable();
var firstRun = true;
mockIOManager.Setup(x => x.CreateDirectory(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(() =>
{
@@ -265,6 +295,8 @@ namespace Tgstation.Server.Host.Setup.Tests
mockAssemblyInfoProvider.VerifyAll();
mockPlatformIdentifier.VerifyAll();
mockAsyncDelayer.VerifyAll();
mockConfiguration.VerifyAll();
mockChangeToken.VerifyAll();
}
}
}
+13 -1
View File
@@ -3,12 +3,12 @@ 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
@@ -131,5 +131,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);
}
}
}