diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index fe3b8ef960..87a518e410 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at #coderbus on irc.rizon.net:6667. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at the [#coderbus discord](https://discord.gg/Vh8TJp9). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. diff --git a/.travis.yml b/.travis.yml index b5de2be669..d5196bdd03 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,27 +51,29 @@ jobs: packages: - libc6-i386 - libstdc++6:i386 -# - env: -# - DoxGeneration=false -# - DockerBuild=false -# - DMAPI=false -# - CONFIG=Release -# - TGS4_TEST_DATABASE_TYPE=PostgresSql -# - TGS4_TEST_CONNECTION_STRING="Application Name=tgstation-server;Host=127.0.0.1;Username=postgres;Password=;Database=TGS_Test" -# name: "PostgresSql Integration Test" -# language: csharp -# mono: none -# dotnet: 3.1 -# services: -# - postgresql -# cache: -# directories: -# - $HOME/.nuget/packages: -# addons: -# apt: -# packages: -# - libc6-i386 -# - libstdc++6:i386 + - env: + - DoxGeneration=false + - DockerBuild=false + - DMAPI=false + - POSTGRESFIX=true + - CONFIG=Release + - TGS4_TEST_DATABASE_TYPE=PostgresSql + - TGS4_TEST_CONNECTION_STRING="Application Name=tgstation-server;Port=5433;Host=127.0.0.1;Username=testuser;Password=password;Database=TGS_Test" + - PGPORT=5433 + name: "PostgresSql Integration Test" + language: csharp + mono: none + dotnet: 3.1 + cache: + directories: + - $HOME/.nuget/packages: + addons: + postgresql: "12" + apt: + packages: + - postgresql-12 + - libc6-i386 + - libstdc++6:i386 - env: - DoxGeneration=false - DockerBuild=false @@ -127,6 +129,10 @@ jobs: - libc6-i386 - libstdc++6:i386 +before_install: + - if [ $POSTGRESFIX = true ]; then sudo -u postgres psql -c "CREATE USER testuser WITH PASSWORD 'password'"; fi + - if [ $POSTGRESFIX = true ]; then sudo -u postgres psql -c "ALTER ROLE testuser SUPERUSER"; fi + install: - if [ $DoxGeneration = false ] && [ $DockerBuild = false ] && [ $DMAPI = true ]; then build/install_byond.sh; fi - if [ $DoxGeneration = false ] && [ $DockerBuild = false ] && [ $DMAPI = false ]; then dotnet restore tgstation-server.sln; fi diff --git a/README.md b/README.md index cd2898ed40..16bf6e737d 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ Create an `appsettings.Production.json` file next to `appsettings.json`. This wi - `Database:DatabaseType`: Can be one of `SqlServer`, `MariaDB`, `MySql`, or `Sqlite`. -- `Database:MySqlServerVersion`: The version of MySql/MariaDB the database resides on, can be left as null for attempted auto detection. Used by the MySQL/MariaDB provider for selection of [certain features](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/blob/2.2.6/src/EFCore.MySql/Storage/Internal/ServerVersion.cs) ignore at your own risk. A string in the form `..` +- `Database:ServerVersion`: The version of the database server. Used by the MySQL/MariaDB and Postgres providers for selection of certain features, ignore at your own risk. A string in the form `..` for MySQL/MariaDB or `.` for Postgres. - `Database:ConnectionString`: Connection string for your database. Click [here](https://www.developerfusion.com/tools/sql-connection-string/) for an SQL Server generator or see [here](https://www.connectionstrings.com/mysql/) for a MySQL guide ([You should probably use '127.0.0.1' instead of 'localhost'](https://stackoverflow.com/questions/19712307/mysql-localhost-127-0-0-1)). diff --git a/src/DMAPI/tgs/core/datum.dm b/src/DMAPI/tgs/core/datum.dm index ef6d3a4fe5..4d37ed662d 100644 --- a/src/DMAPI/tgs/core/datum.dm +++ b/src/DMAPI/tgs/core/datum.dm @@ -50,7 +50,7 @@ TGS_PROTECT_DATUM(/datum/tgs_api) /datum/tgs_api/proc/ChatTargetedBroadcast(message, admin_only) return TGS_UNIMPLEMENTED -/datum/tgs_api/proc/ChatPrivateMessage(message, admin_only) +/datum/tgs_api/proc/ChatPrivateMessage(message, datum/tgs_chat_user/user) return TGS_UNIMPLEMENTED /datum/tgs_api/proc/SecurityLevel() diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs index 69d6fa32e6..3758ec9b34 100644 --- a/src/Tgstation.Server.Api/ApiHeaders.cs +++ b/src/Tgstation.Server.Api/ApiHeaders.cs @@ -62,7 +62,7 @@ namespace Tgstation.Server.Api public static readonly Version Version = AssemblyName.Version.Semver(); /// - /// The being accessed + /// The instance being accessed /// public long? InstanceId { get; set; } @@ -255,7 +255,7 @@ namespace Tgstation.Server.Api /// Set using the . This initially clears /// /// The to set - /// The for the request + /// The instance for the request public void SetRequestHeaders(HttpRequestHeaders headers, long? instanceId = null) { if (headers == null) diff --git a/src/Tgstation.Server.Api/Models/EntityId.cs b/src/Tgstation.Server.Api/Models/EntityId.cs index bf9ce0883f..85d5cd0487 100644 --- a/src/Tgstation.Server.Api/Models/EntityId.cs +++ b/src/Tgstation.Server.Api/Models/EntityId.cs @@ -1,7 +1,7 @@ namespace Tgstation.Server.Api.Models { /// - /// Common base of s and s. + /// Common base of s, s, and s. /// public class EntityId { diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index e9aa740161..4f579c752e 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -325,7 +325,7 @@ namespace Tgstation.Server.Api.Models /// /// The DMAPI never validated itself /// - [Description("DreamDaemon exited without validating the DMAPI@")] + [Description("DreamDaemon exited without validating the DMAPI!")] DreamMakerNeverValidated, /// diff --git a/src/Tgstation.Server.Api/Models/Instance.cs b/src/Tgstation.Server.Api/Models/Instance.cs index d3f1a78115..60856b07ea 100644 --- a/src/Tgstation.Server.Api/Models/Instance.cs +++ b/src/Tgstation.Server.Api/Models/Instance.cs @@ -6,13 +6,8 @@ namespace Tgstation.Server.Api.Models /// /// Metadata about a server instance /// - public class Instance + public class Instance : EntityId { - /// - /// The id of the . Not modifiable - /// - public long Id { get; set; } - /// /// The name of the /// diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index b9443b8897..69d7d0f79a 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -138,7 +138,7 @@ namespace Tgstation.Server.Client /// The route to run /// The body of the request /// The method of the request - /// The optional for the request + /// The optional instance for the request /// If this is a token refresh operation. /// The for the operation /// A resulting in the response on success diff --git a/src/Tgstation.Server.Client/IApiClient.cs b/src/Tgstation.Server.Client/IApiClient.cs index ab833b1731..b1c8519892 100644 --- a/src/Tgstation.Server.Client/IApiClient.cs +++ b/src/Tgstation.Server.Client/IApiClient.cs @@ -105,7 +105,7 @@ namespace Tgstation.Server.Client /// The type of the response body /// The server route to make the request to /// The request body - /// The to make the request to + /// The instance to make the request to /// The for the operation /// A resulting in the response body as a Task Create(string route, TBody body, long instanceId, CancellationToken cancellationToken); @@ -115,7 +115,7 @@ namespace Tgstation.Server.Client /// /// The type of the response body /// The server route to make the request to - /// The to make the request to + /// The instance to make the request to /// The for the operation /// A resulting in the response body as a Task Create(string route, long instanceId, CancellationToken cancellationToken); @@ -125,7 +125,7 @@ namespace Tgstation.Server.Client /// /// The type of the response body /// The server route to make the request to - /// The to make the request to + /// The instance to make the request to /// The for the operation /// A resulting in the response body as a Task Patch(string route, long instanceId, CancellationToken cancellationToken); @@ -135,7 +135,7 @@ namespace Tgstation.Server.Client /// /// The type of the response body /// The server route to make the request to - /// The to make the request to + /// The instance to make the request to /// The for the operation /// A resulting in the response body as a Task Read(string route, long instanceId, CancellationToken cancellationToken); @@ -147,7 +147,7 @@ namespace Tgstation.Server.Client /// The type of the response body /// The server route to make the request to /// The request body - /// The to make the request to + /// The instance to make the request to /// The for the operation /// A resulting in the response body as a Task Update(string route, TBody body, long instanceId, CancellationToken cancellationToken); @@ -156,7 +156,7 @@ namespace Tgstation.Server.Client /// Run an HTTP DELETE request /// /// The server route to make the request to - /// The to make the request to + /// The instance to make the request to /// The for the operation /// A representing the running operation Task Delete(string route, long instanceId, CancellationToken cancellationToken); @@ -167,7 +167,7 @@ namespace Tgstation.Server.Client /// The type to of the request body /// The server route to make the request to /// The request body - /// The to make the request to + /// The instance to make the request to /// The for the operation /// A representing the running operation Task Delete(string route, TBody body, long instanceId, CancellationToken cancellationToken); @@ -177,7 +177,7 @@ namespace Tgstation.Server.Client /// /// The type of the response body /// The server route to make the request to - /// The to make the request to + /// The instance to make the request to /// The for the operation /// A resulting in the response body as a Task Delete(string route, long instanceId, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Client/IInstanceManagerClient.cs b/src/Tgstation.Server.Client/IInstanceManagerClient.cs index 73d60a3893..57030b1deb 100644 --- a/src/Tgstation.Server.Client/IInstanceManagerClient.cs +++ b/src/Tgstation.Server.Client/IInstanceManagerClient.cs @@ -21,7 +21,7 @@ namespace Tgstation.Server.Client /// /// Create or attach an /// - /// The to create. will be ignored + /// The to create. will be ignored /// The for the operation /// A resulting in the created or attached Task CreateOrAttach(Instance instance, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index dd4caa7835..8a6a3c0071 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using Serilog.Context; using System; using System.Collections.Generic; using System.Globalization; @@ -111,6 +112,11 @@ namespace Tgstation.Server.Host.Components.Chat /// ulong channelIdCounter; + /// + /// The number of s processed. + /// + long messagesProcessed; + /// /// If has been called /// @@ -407,7 +413,9 @@ namespace Tgstation.Server.Host.Components.Chat foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList()) { var message = await I.Value.ConfigureAwait(false); - await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false); + var messageNumber = Interlocked.Increment(ref messagesProcessed); + using (LogContext.PushProperty("ChatMessage", messageNumber)) + await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false); messageTasks.Remove(I.Key); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Message.cs b/src/Tgstation.Server.Host/Components/Chat/Message.cs index 24e18962e4..5f661fd4f7 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Message.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Message.cs @@ -11,7 +11,7 @@ public string Content { get; set; } /// - /// The who sent the + /// The who sent the /// public ChatUser User { get; set; } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index c73f5ad8d8..3a2eb928a0 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -586,7 +586,7 @@ namespace Tgstation.Server.Host.Components.Deployment } catch { - repo.Dispose(); + repo?.Dispose(); throw; } }) diff --git a/src/Tgstation.Server.Host/Components/IInstance.cs b/src/Tgstation.Server.Host/Components/IInstance.cs index ee975d13a8..475c625482 100644 --- a/src/Tgstation.Server.Host/Components/IInstance.cs +++ b/src/Tgstation.Server.Host/Components/IInstance.cs @@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Components /// /// For interacting with the instance services /// - public interface IInstance : ILatestCompileJobProvider, IHostedService, IDisposable + public interface IInstance : ILatestCompileJobProvider, IHostedService, IRenameNotifyee, IDisposable { /// /// The for the @@ -45,12 +45,6 @@ namespace Tgstation.Server.Host.Components /// IConfiguration Configuration { get; } - /// - /// Rename the - /// - /// The new name for the - void Rename(string newName); - /// /// Change the for the /// diff --git a/src/Tgstation.Server.Host/Components/IRenameNotifyee.cs b/src/Tgstation.Server.Host/Components/IRenameNotifyee.cs new file mode 100644 index 0000000000..d40afc0023 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/IRenameNotifyee.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components +{ + /// + /// Handler for an instance being renamed. + /// + public interface IRenameNotifyee + { + /// + /// Called when the owning is renamed. + /// + /// The new . + /// The for the operation. + /// A representing the running operation. + Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 76d67ce595..62d5545faa 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Serilog.Context; using System; using System.Collections.Generic; using System.Linq; @@ -134,12 +135,15 @@ namespace Tgstation.Server.Host.Components /// public void Dispose() { - timerCts?.Dispose(); - Configuration.Dispose(); - Chat.Dispose(); - Watchdog.Dispose(); - dmbFactory.Dispose(); - RepositoryManager.Dispose(); + using (LogContext.PushProperty("Instance", metadata.Id)) + { + timerCts?.Dispose(); + Configuration.Dispose(); + Chat.Dispose(); + Watchdog.Dispose(); + dmbFactory.Dispose(); + RepositoryManager.Dispose(); + } } /// @@ -151,7 +155,7 @@ namespace Tgstation.Server.Host.Components #pragma warning disable CA1502 // TODO: Decomplexify async Task TimerLoop(uint minutes, CancellationToken cancellationToken) { - logger.LogTrace("Entering auto-update loop"); + logger.LogDebug("Entering auto-update loop"); while (true) try { @@ -393,17 +397,20 @@ namespace Tgstation.Server.Host.Components #pragma warning restore CA1502 /// - public void Rename(string newName) + public Task InstanceRenamed(string newName, CancellationToken cancellationToken) { if (String.IsNullOrWhiteSpace(newName)) throw new ArgumentNullException(nameof(newName)); metadata.Name = newName; + return Watchdog.InstanceRenamed(newName, cancellationToken); } /// public async Task StartAsync(CancellationToken cancellationToken) { - await Task.WhenAll( + using (LogContext.PushProperty("Instance", metadata.Id)) + { + await Task.WhenAll( SetAutoUpdateInterval(metadata.AutoUpdateInterval.Value), Configuration.StartAsync(cancellationToken), ByondManager.StartAsync(cancellationToken), @@ -411,23 +418,27 @@ namespace Tgstation.Server.Host.Components dmbFactory.StartAsync(cancellationToken)) .ConfigureAwait(false); - // dependent on so many things, its just safer this way - await Watchdog.StartAsync(cancellationToken).ConfigureAwait(false); + // dependent on so many things, its just safer this way + await Watchdog.StartAsync(cancellationToken).ConfigureAwait(false); - await dmbFactory.CleanUnusedCompileJobs(cancellationToken).ConfigureAwait(false); + await dmbFactory.CleanUnusedCompileJobs(cancellationToken).ConfigureAwait(false); + } } /// public async Task StopAsync(CancellationToken cancellationToken) { - await SetAutoUpdateInterval(0).ConfigureAwait(false); - await Watchdog.StopAsync(cancellationToken).ConfigureAwait(false); - await Task.WhenAll( - Configuration.StopAsync(cancellationToken), - ByondManager.StopAsync(cancellationToken), - Chat.StopAsync(cancellationToken), - dmbFactory.StopAsync(cancellationToken)) - .ConfigureAwait(false); + using (LogContext.PushProperty("Instance", metadata.Id)) + { + await SetAutoUpdateInterval(0).ConfigureAwait(false); + await Watchdog.StopAsync(cancellationToken).ConfigureAwait(false); + await Task.WhenAll( + Configuration.StopAsync(cancellationToken), + ByondManager.StopAsync(cancellationToken), + Chat.StopAsync(cancellationToken), + dmbFactory.StopAsync(cancellationToken)) + .ConfigureAwait(false); + } } /// @@ -438,8 +449,12 @@ namespace Tgstation.Server.Host.Components { if (timerTask != null) { + logger.LogTrace("Cancelling auto-update task"); timerCts.Cancel(); + timerCts.Dispose(); toWait = timerTask; + timerTask = null; + timerCts = null; } else toWait = Task.CompletedTask; @@ -447,13 +462,20 @@ namespace Tgstation.Server.Host.Components await toWait.ConfigureAwait(false); if (newInterval == 0) + { + logger.LogTrace("New auto-update interval is 0. Not starting task."); return; + } + lock (timerLock) { // race condition, just quit if (timerTask != null) + { + logger.LogDebug("Aborting auto update interval change due to race condition!"); return; - timerCts?.Dispose(); + } + timerCts = new CancellationTokenSource(); timerTask = TimerLoop(newInterval, timerCts.Token); } diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 062dca8163..e0045ccbc9 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Configuration; @@ -72,7 +73,7 @@ namespace Tgstation.Server.Host.Components readonly ILogger logger; /// - /// Map of s to respective s. Also used as a . + /// Map of instance s to respective s. Also used as a . /// readonly IDictionary instances; diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/IBridgeHandler.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/IBridgeHandler.cs index 5ca5347e3d..5d262a277e 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/IBridgeHandler.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/IBridgeHandler.cs @@ -1,7 +1,4 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace Tgstation.Server.Host.Components.Interop.Bridge +namespace Tgstation.Server.Host.Components.Interop.Bridge { /// interface IBridgeHandler : IBridgeDispatcher @@ -10,13 +7,5 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge /// The for the . /// DMApiParameters DMApiParameters { get; } - - /// - /// Called when the owning is renamed. - /// - /// The new . - /// The for the operation. - /// A representing the running operation. - Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Session/DeadSessionController.cs b/src/Tgstation.Server.Host/Components/Session/DeadSessionController.cs index a49c9f7738..2f5ff7f5c4 100644 --- a/src/Tgstation.Server.Host/Components/Session/DeadSessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/DeadSessionController.cs @@ -120,5 +120,8 @@ namespace Tgstation.Server.Host.Components.Session /// public void Resume() => throw new NotSupportedException(); + + /// + public Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken) => Task.CompletedTask; } } diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs index c598d9a331..fdc9536ec5 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs @@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Components.Session /// /// Handles communication with a DreamDaemon /// - interface ISessionController : IProcessBase + interface ISessionController : IRenameNotifyee, IProcessBase { /// /// A that completes when DreamDaemon starts pumping the windows message queue after loading a .dmb or when it crashes diff --git a/src/Tgstation.Server.Host/Components/Session/ReattachInfoHandler.cs b/src/Tgstation.Server.Host/Components/Session/ReattachInfoHandler.cs index 001f0a1087..b7d377ef8a 100644 --- a/src/Tgstation.Server.Host/Components/Session/ReattachInfoHandler.cs +++ b/src/Tgstation.Server.Host/Components/Session/ReattachInfoHandler.cs @@ -56,11 +56,12 @@ namespace Tgstation.Server.Host.Components.Session logger.LogDebug("Saving reattach information: {0}...", reattachInformation); - var deleteTask = db + await db .WatchdogReattachInformations .AsQueryable() .Where(x => x.InstanceId == metadata.Id) - .DeleteAsync(cancellationToken); + .DeleteAsync(cancellationToken) + .ConfigureAwait(false); Models.ReattachInformation ConvertReattachInfo(ReattachInformation wdInfo) { @@ -79,8 +80,6 @@ namespace Tgstation.Server.Host.Components.Session }; } - await deleteTask.ConfigureAwait(false); - db.WatchdogReattachInformations.Add(new Models.DualReattachInformation { Alpha = ConvertReattachInfo(reattachInformation.Alpha), diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 3d1590cc4e..dca20a3a14 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -1,6 +1,7 @@ using Byond.TopicSender; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using Serilog.Context; using System; using System.Collections.Generic; using System.Globalization; @@ -105,6 +106,11 @@ namespace Tgstation.Server.Host.Components.Session /// readonly ReattachInformation reattachInformation; + /// + /// The metadata. + /// + readonly Api.Models.Instance metadata; + /// /// A used for the topic send operation made on reattaching. /// @@ -194,6 +200,7 @@ namespace Tgstation.Server.Host.Components.Session /// Construct a /// /// The value of + /// The owning . /// The value of /// The value of /// The value of @@ -206,6 +213,7 @@ namespace Tgstation.Server.Host.Components.Session /// If this is a reattached session. public SessionController( ReattachInformation reattachInformation, + Api.Models.Instance metadata, IProcess process, IByondExecutableLock byondLock, ITopicClient byondTopicSender, @@ -218,6 +226,7 @@ namespace Tgstation.Server.Host.Components.Session bool reattached) { this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); + this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); this.process = process ?? throw new ArgumentNullException(nameof(process)); this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); @@ -364,141 +373,145 @@ namespace Tgstation.Server.Host.Components.Session if (parameters == null) throw new ArgumentNullException(nameof(parameters)); - var response = new BridgeResponse(); - switch (parameters.CommandType) + using (LogContext.PushProperty("Instance", metadata.Id)) { - case BridgeCommandType.ChatSend: - if (parameters.ChatMessage == null) - return new BridgeResponse - { - ErrorMessage = "Missing chatMessage field!" - }; - - if (parameters.ChatMessage.ChannelIds == null) - return new BridgeResponse - { - ErrorMessage = "Missing channelIds field in chatMessage!" - }; - - if(parameters.ChatMessage.ChannelIds.Any(channelIdString => !UInt64.TryParse(channelIdString, out var _))) - return new BridgeResponse - { - ErrorMessage = "Invalid channelIds in chatMessage!" - }; - - if (parameters.ChatMessage.Text == null) - return new BridgeResponse - { - ErrorMessage = "Missing message field in chatMessage!" - }; - - await chat.SendMessage( - parameters.ChatMessage.Text, - parameters.ChatMessage.ChannelIds.Select(UInt64.Parse), - cancellationToken).ConfigureAwait(false); - break; - case BridgeCommandType.Prime: - var oldPrimeTcs = primeTcs; - primeTcs = new TaskCompletionSource(); - oldPrimeTcs.SetResult(null); - break; - case BridgeCommandType.Kill: - logger.LogInformation("Bridge requested process termination!"); - TerminationWasRequested = true; - process.Terminate(); - break; - case BridgeCommandType.PortUpdate: - lock (synchronizationLock) - { - if (!parameters.CurrentPort.HasValue) - { - /////UHHHH - logger.LogWarning("DreamDaemon sent new port command without providing it's own!"); + logger.LogTrace("Handling bridge request..."); + var response = new BridgeResponse(); + switch (parameters.CommandType) + { + case BridgeCommandType.ChatSend: + if (parameters.ChatMessage == null) return new BridgeResponse { - ErrorMessage = "Missing stringified port as data parameter!" + ErrorMessage = "Missing chatMessage field!" }; + + if (parameters.ChatMessage.ChannelIds == null) + return new BridgeResponse + { + ErrorMessage = "Missing channelIds field in chatMessage!" + }; + + if (parameters.ChatMessage.ChannelIds.Any(channelIdString => !UInt64.TryParse(channelIdString, out var _))) + return new BridgeResponse + { + ErrorMessage = "Invalid channelIds in chatMessage!" + }; + + if (parameters.ChatMessage.Text == null) + return new BridgeResponse + { + ErrorMessage = "Missing message field in chatMessage!" + }; + + await chat.SendMessage( + parameters.ChatMessage.Text, + parameters.ChatMessage.ChannelIds.Select(UInt64.Parse), + cancellationToken).ConfigureAwait(false); + break; + case BridgeCommandType.Prime: + var oldPrimeTcs = primeTcs; + primeTcs = new TaskCompletionSource(); + oldPrimeTcs.SetResult(null); + break; + case BridgeCommandType.Kill: + logger.LogInformation("Bridge requested process termination!"); + TerminationWasRequested = true; + process.Terminate(); + break; + case BridgeCommandType.PortUpdate: + lock (synchronizationLock) + { + if (!parameters.CurrentPort.HasValue) + { + /////UHHHH + logger.LogWarning("DreamDaemon sent new port command without providing it's own!"); + return new BridgeResponse + { + ErrorMessage = "Missing stringified port as data parameter!" + }; + } + + var currentPort = parameters.CurrentPort.Value; + if (!nextPort.HasValue) + reattachInformation.Port = parameters.CurrentPort.Value; // not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to + else + { + // nextPort is ready, tell DD to switch to that + // if it fails it'll kill itself + response.NewPort = nextPort.Value; + reattachInformation.Port = nextPort.Value; + nextPort = null; + + // we'll also get here from SetPort so complete that task + var tmpTcs = portAssignmentTcs; + portAssignmentTcs = null; + tmpTcs.SetResult(true); + } + + portClosedForReboot = false; } - var currentPort = parameters.CurrentPort.Value; - if (!nextPort.HasValue) - reattachInformation.Port = parameters.CurrentPort.Value; // not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to - else - { - // nextPort is ready, tell DD to switch to that - // if it fails it'll kill itself - response.NewPort = nextPort.Value; - reattachInformation.Port = nextPort.Value; - nextPort = null; + break; + case BridgeCommandType.Startup: + apiValidationStatus = ApiValidationStatus.BadValidationRequest; + if (parameters.Version == null) + return new BridgeResponse + { + ErrorMessage = "Missing dmApiVersion field!" + }; - // we'll also get here from SetPort so complete that task - var tmpTcs = portAssignmentTcs; - portAssignmentTcs = null; - tmpTcs.SetResult(true); + DMApiVersion = parameters.Version; + switch (parameters.MinimumSecurityLevel) + { + case DreamDaemonSecurity.Ultrasafe: + apiValidationStatus = ApiValidationStatus.RequiresUltrasafe; + break; + case DreamDaemonSecurity.Safe: + apiValidationStatus = ApiValidationStatus.RequiresSafe; + break; + case DreamDaemonSecurity.Trusted: + apiValidationStatus = ApiValidationStatus.RequiresTrusted; + break; + case null: + return new BridgeResponse + { + ErrorMessage = "Missing minimumSecurityLevel field!" + }; + default: + return new BridgeResponse + { + ErrorMessage = "Invalid minimumSecurityLevel!" + }; } - portClosedForReboot = false; - } + response.RuntimeInformation = reattachInformation.RuntimeInformation; - break; - case BridgeCommandType.Startup: - apiValidationStatus = ApiValidationStatus.BadValidationRequest; - if (parameters.Version == null) - return new BridgeResponse + // Load custom commands + chatTrackingContext.CustomCommands = parameters.CustomCommands; + break; + case BridgeCommandType.Reboot: + if (ClosePortOnReboot) { - ErrorMessage = "Missing dmApiVersion field!" - }; + chatTrackingContext.Active = false; + response.NewPort = 0; + portClosedForReboot = true; + } - DMApiVersion = parameters.Version; - switch (parameters.MinimumSecurityLevel) - { - case DreamDaemonSecurity.Ultrasafe: - apiValidationStatus = ApiValidationStatus.RequiresUltrasafe; - break; - case DreamDaemonSecurity.Safe: - apiValidationStatus = ApiValidationStatus.RequiresSafe; - break; - case DreamDaemonSecurity.Trusted: - apiValidationStatus = ApiValidationStatus.RequiresTrusted; - break; - case null: - return new BridgeResponse - { - ErrorMessage = "Missing minimumSecurityLevel field!" - }; - default: - return new BridgeResponse - { - ErrorMessage = "Invalid minimumSecurityLevel!" - }; - } + var oldRebootTcs = rebootTcs; + rebootTcs = new TaskCompletionSource(); + oldRebootTcs.SetResult(null); + break; + case null: + response.ErrorMessage = "Missing commandType!"; + break; + default: + response.ErrorMessage = "Requested commandType not supported!"; + break; + } - response.RuntimeInformation = reattachInformation.RuntimeInformation; - - // Load custom commands - chatTrackingContext.CustomCommands = parameters.CustomCommands; - break; - case BridgeCommandType.Reboot: - if (ClosePortOnReboot) - { - chatTrackingContext.Active = false; - response.NewPort = 0; - portClosedForReboot = true; - } - - var oldRebootTcs = rebootTcs; - rebootTcs = new TaskCompletionSource(); - oldRebootTcs.SetResult(null); - break; - case null: - response.ErrorMessage = "Missing commandType!"; - break; - default: - response.ErrorMessage = "Requested commandType not supported!"; - break; + return response; } - - return response; } /// diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index e5d61c60d1..b1861108dc 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Linq; using System.Net; using System.Net.Sockets; +using System.Text; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; @@ -122,7 +123,7 @@ namespace Tgstation.Server.Host.Components.Session try { - socket.Bind(new IPEndPoint(IPAddress.Loopback, port)); + socket.Bind(new IPEndPoint(IPAddress.Any, port)); } catch (Exception ex) { @@ -233,12 +234,18 @@ namespace Tgstation.Server.Host.Components.Session var visibility = apiValidate ? "invisible" : "public"; // important to run on all ports to allow port changing - var arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} -ports 1-65535 {2}-close -{3} -{4} -public -params \"{5}\"", + Guid? logFileGuid = null; + var arguments = String.Format( + CultureInfo.InvariantCulture, + "{0} -port {1} -ports 1-65535 {2}-close -{3} -{4}{5} -public -params \"{6}\"", dmbProvider.DmbName, portToUse, launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty, SecurityWord(launchParameters.SecurityLevel.Value), visibility, + platformIdentifier.IsWindows + ? $" -log {logFileGuid = Guid.NewGuid()}" + : String.Empty, // Just use stdout on linux parameters); // See https://github.com/tgstation/tgstation-server/issues/719 @@ -253,16 +260,52 @@ namespace Tgstation.Server.Host.Components.Session noShellExecute, noShellExecute: noShellExecute); - if (noShellExecute) + async Task GetDDOutput() { - // Log DD output - _ = process.Lifetime.ContinueWith( - x => logger.LogTrace( - "DreamDaemon Output:{0}{1}", - Environment.NewLine, process.GetCombinedOutput()), - TaskScheduler.Current); + if (!platformIdentifier.IsWindows) + return process.GetCombinedOutput(); + + var logFilePath = ioManager.ConcatPath(basePath, logFileGuid.ToString()); + try + { + var dreamDaemonLogBytes = await ioManager.ReadAllBytes( + logFilePath, + default) + .ConfigureAwait(false); + + return Encoding.UTF8.GetString(dreamDaemonLogBytes); + } + finally + { + try + { + await ioManager.DeleteFile(logFilePath, default).ConfigureAwait(false); + } + catch (Exception ex) + { + logger.LogWarning("Failed to delete DreamDaemon log file {0}: {1}", logFilePath, ex); + } + } } + // Log DD output + _ = process.Lifetime.ContinueWith( + async x => + { + try + { + var ddOutput = await GetDDOutput().ConfigureAwait(false); + logger.LogTrace( + "DreamDaemon Output:{0}{1}", + Environment.NewLine, ddOutput); + } + catch (Exception ex) + { + logger.LogWarning("Error reading DreamDaemon output: {0}", ex); + } + }, + TaskScheduler.Current); + try { networkPromptReaper.RegisterProcess(process); @@ -283,6 +326,7 @@ namespace Tgstation.Server.Host.Components.Session var sessionController = new SessionController( reattachInformation, + instance, process, byondLock, byondTopicSender, @@ -348,6 +392,7 @@ namespace Tgstation.Server.Host.Components.Session var controller = new SessionController( reattachInformation, + instance, process, byondLock, byondTopicSender, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 1722376728..96fbbbdf30 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -346,5 +346,9 @@ namespace Tgstation.Server.Host.Components.Watchdog await LaunchNoLock(true, false, null, cancellationToken).ConfigureAwait(false); await chatTask.ConfigureAwait(false); } + + /// + public sealed override Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken) + => Server?.InstanceRenamed(newInstanceName, cancellationToken) ?? Task.CompletedTask; } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs index 07c73f7376..a399db4e8d 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs @@ -583,5 +583,11 @@ namespace Tgstation.Server.Host.Components.Watchdog Alpha = alphaServer?.Release(), Bravo = bravoServer?.Release() }; + + /// + public override Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken) + => Task.WhenAll( + alphaServer?.InstanceRenamed(newInstanceName, cancellationToken) ?? Task.CompletedTask, + bravoServer?.InstanceRenamed(newInstanceName, cancellationToken) ?? Task.CompletedTask); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs index 536096c499..6c40b1bd97 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs @@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Runs and monitors the twin server controllers /// - public interface IWatchdog : IHostedService, IDisposable, IEventConsumer + public interface IWatchdog : IHostedService, IDisposable, IEventConsumer, IRenameNotifyee { /// /// If the watchdog is running diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index d2400a8170..6f2b0341d1 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using Serilog.Context; using System; using System.Collections.Generic; using System.Linq; @@ -289,10 +290,10 @@ namespace Tgstation.Server.Host.Components.Watchdog switch (++heartbeatsMissed) { case 1: - Logger.LogDebug("DEFCON 4: Watchdog missed first heartbeat!"); + Logger.LogDebug("DEFCON 4: DreamDaemon missed first heartbeat!"); break; case 2: - var message2 = "DEFCON 3: Watchdog has missed 2 heartbeats!"; + var message2 = "DEFCON 3: DreamDaemon has missed 2 heartbeats!"; Logger.LogInformation(message2); await Chat.SendWatchdogMessage(message2, true, cancellationToken).ConfigureAwait(false); break; @@ -300,7 +301,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var actionToTake = shouldShutdown ? "shutdown" : "be restarted"; - var message3 = $"DEFCON 2: Watchdog has missed 3 heartbeats! If DreamDaemon does not respond to the next one, the watchdog will {actionToTake}!"; + var message3 = $"DEFCON 2: DreamDaemon has missed 3 heartbeats! If it does not respond to the next one, the watchdog will {actionToTake}!"; Logger.LogWarning(message3); await Chat.SendWatchdogMessage(message3, false, cancellationToken).ConfigureAwait(false); break; @@ -565,137 +566,138 @@ namespace Tgstation.Server.Host.Components.Watchdog try { for (var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration) - try - { - Logger.LogDebug("Iteration {0} of monitor loop", iteration); - - // load the activation tasks into local variables - var serverTasks = GetMonitoredServerTasks(monitorState); - if (serverTasks.Count != 5) - throw new InvalidOperationException("Expected 5 monitored server tasks!"); - - var activeServerLifetime = serverTasks[MonitorActivationReason.ActiveServerCrashed]; - var activeServerReboot = serverTasks[MonitorActivationReason.ActiveServerRebooted]; - var inactiveServerLifetime = serverTasks[MonitorActivationReason.InactiveServerCrashed]; - var inactiveServerReboot = serverTasks[MonitorActivationReason.InactiveServerRebooted]; - var inactiveStartupComplete = serverTasks[MonitorActivationReason.InactiveServerStartupComplete]; - - Task activeLaunchParametersChanged = ActiveParametersUpdated.Task; - var newDmbAvailable = DmbFactory.OnNewerDmb; - - var heartbeatSeconds = ActiveLaunchParameters.HeartbeatSeconds.Value; - var heartbeat = heartbeatSeconds == 0 - ? Extensions.TaskExtensions.InfiniteTask() - : Task.Delay(TimeSpan.FromSeconds(heartbeatSeconds)); - - // cancel waiting if requested - var cancelTcs = new TaskCompletionSource(); - var toWaitOn = Task.WhenAny( - activeServerLifetime, - activeServerReboot, - inactiveServerLifetime, - inactiveServerReboot, - inactiveStartupComplete, - heartbeat, - newDmbAvailable, - cancelTcs.Task, - activeLaunchParametersChanged); - - // wait for something to happen - using (cancellationToken.Register(() => cancelTcs.SetCanceled())) - await toWaitOn.ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); - Logger.LogTrace("Monitor activated"); - - // always run HandleMonitorWakeup from the context of the semaphore lock - using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) + using (LogContext.PushProperty("Monitor", iteration)) + try { - // multiple things may have happened, handle them one at a time - for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);) - { - MonitorActivationReason activationReason = default; // this will always be assigned before being used + Logger.LogTrace("Iteration {0} of monitor loop", iteration); - bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason) + // load the activation tasks into local variables + var serverTasks = GetMonitoredServerTasks(monitorState); + if (serverTasks.Count != 5) + throw new InvalidOperationException("Expected 5 monitored server tasks!"); + + var activeServerLifetime = serverTasks[MonitorActivationReason.ActiveServerCrashed]; + var activeServerReboot = serverTasks[MonitorActivationReason.ActiveServerRebooted]; + var inactiveServerLifetime = serverTasks[MonitorActivationReason.InactiveServerCrashed]; + var inactiveServerReboot = serverTasks[MonitorActivationReason.InactiveServerRebooted]; + var inactiveStartupComplete = serverTasks[MonitorActivationReason.InactiveServerStartupComplete]; + + Task activeLaunchParametersChanged = ActiveParametersUpdated.Task; + var newDmbAvailable = DmbFactory.OnNewerDmb; + + var heartbeatSeconds = ActiveLaunchParameters.HeartbeatSeconds.Value; + var heartbeat = heartbeatSeconds == 0 + ? Extensions.TaskExtensions.InfiniteTask() + : Task.Delay(TimeSpan.FromSeconds(heartbeatSeconds)); + + // cancel waiting if requested + var cancelTcs = new TaskCompletionSource(); + var toWaitOn = Task.WhenAny( + activeServerLifetime, + activeServerReboot, + inactiveServerLifetime, + inactiveServerReboot, + inactiveStartupComplete, + heartbeat, + newDmbAvailable, + cancelTcs.Task, + activeLaunchParametersChanged); + + // wait for something to happen + using (cancellationToken.Register(() => cancelTcs.SetCanceled())) + await toWaitOn.ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + Logger.LogTrace("Monitor activated"); + + // always run HandleMonitorWakeup from the context of the semaphore lock + using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) + { + // multiple things may have happened, handle them one at a time + for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);) { - var taskCompleted = task?.IsCompleted == true; - task = null; - if (monitorState.NextAction == MonitorAction.Skip) - monitorState.NextAction = MonitorAction.Continue; - else if (taskCompleted) + MonitorActivationReason activationReason = default; // this will always be assigned before being used + + bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason) { - activationReason = testActivationReason; - return true; + var taskCompleted = task?.IsCompleted == true; + task = null; + if (monitorState.NextAction == MonitorAction.Skip) + monitorState.NextAction = MonitorAction.Continue; + else if (taskCompleted) + { + activationReason = testActivationReason; + return true; + } + + return false; } - return false; - } + // process the tasks in this order and call HandlerMonitorWakup for each depending on the new monitorState + var anyActivation = CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed) + || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted) + || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable) + || CheckActivationReason(ref inactiveServerLifetime, MonitorActivationReason.InactiveServerCrashed) + || CheckActivationReason(ref inactiveServerReboot, MonitorActivationReason.InactiveServerRebooted) + || CheckActivationReason(ref inactiveStartupComplete, MonitorActivationReason.InactiveServerStartupComplete) + || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated) + || CheckActivationReason(ref heartbeat, MonitorActivationReason.Heartbeat); - // process the tasks in this order and call HandlerMonitorWakup for each depending on the new monitorState - var anyActivation = CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed) - || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted) - || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable) - || CheckActivationReason(ref inactiveServerLifetime, MonitorActivationReason.InactiveServerCrashed) - || CheckActivationReason(ref inactiveServerReboot, MonitorActivationReason.InactiveServerRebooted) - || CheckActivationReason(ref inactiveStartupComplete, MonitorActivationReason.InactiveServerStartupComplete) - || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated) - || CheckActivationReason(ref heartbeat, MonitorActivationReason.Heartbeat); - - if (!anyActivation) - moreActivationsToProcess = false; - else - { - Logger.LogTrace("Reason: {0}", activationReason); - if (activationReason == MonitorActivationReason.Heartbeat) - monitorState.NextAction = await HandleHeartbeat( - monitorState.ActiveServer, - cancellationToken) - .ConfigureAwait(false); + if (!anyActivation) + moreActivationsToProcess = false; else - await HandleMonitorWakeup( - activationReason, - monitorState, - cancellationToken) - .ConfigureAwait(false); + { + Logger.LogTrace("Reason: {0}", activationReason); + if (activationReason == MonitorActivationReason.Heartbeat) + monitorState.NextAction = await HandleHeartbeat( + monitorState.ActiveServer, + cancellationToken) + .ConfigureAwait(false); + else + await HandleMonitorWakeup( + activationReason, + monitorState, + cancellationToken) + .ConfigureAwait(false); + } } } + + Logger.LogTrace("Next monitor action is to {0}", monitorState.NextAction); + + // Restart if requested + if (monitorState.NextAction == MonitorAction.Restart) + monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(false); } + catch (OperationCanceledException) + { + // let this bubble, other exceptions caught below + throw; + } + catch (Exception e) + { + // really, this should NEVER happen + Logger.LogError( + "Monitor crashed! Iteration: {0}, Monitor State: {1}, Exception: {2}", + iteration, + JsonConvert.SerializeObject(monitorState), + e); - Logger.LogTrace("Next monitor action is to {0}", monitorState.NextAction); + var nextActionMessage = monitorState.NextAction != MonitorAction.Exit + ? "Restarting" + : "Shutting down"; + var chatTask = Chat.SendWatchdogMessage( + $"Monitor crashed, this should NEVER happen! Please report this, full details in logs! {nextActionMessage}. Error: {e.Message}", + false, + cancellationToken); - // Restart if requested - if (monitorState.NextAction == MonitorAction.Restart) - monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // let this bubble, other exceptions caught below - throw; - } - catch (Exception e) - { - // really, this should NEVER happen - Logger.LogError( - "Monitor crashed! Iteration: {0}, Monitor State: {1}, Exception: {2}", - iteration, - JsonConvert.SerializeObject(monitorState), - e); + if (disposed) + monitorState.NextAction = MonitorAction.Exit; + else if (monitorState.NextAction != MonitorAction.Exit) + monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(false); - var nextActionMessage = monitorState.NextAction != MonitorAction.Exit - ? "Restarting" - : "Shutting down"; - var chatTask = Chat.SendWatchdogMessage( - $"Monitor crashed, this should NEVER happen! Please report this, full details in logs! {nextActionMessage}. Error: {e.Message}", - false, - cancellationToken); - - if (disposed) - monitorState.NextAction = MonitorAction.Exit; - else if (monitorState.NextAction != MonitorAction.Exit) - monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(false); - - await chatTask.ConfigureAwait(false); - } + await chatTask.ConfigureAwait(false); + } } catch (OperationCanceledException) { @@ -914,5 +916,8 @@ namespace Tgstation.Server.Host.Components.Watchdog if (Running) await Chat.SendWatchdogMessage("Detaching...", false, cancellationToken).ConfigureAwait(false); } + + /// + public abstract Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs b/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs index abe1a6d5df..92fd68237d 100644 --- a/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs @@ -40,8 +40,8 @@ namespace Tgstation.Server.Host.Configuration public bool DesignTime { get; set; } /// - /// The form of the of a target MySQL/MariaDB server + /// The form of the of the target server /// - public string MySqlServerVersion { get; set; } + public string ServerVersion { get; set; } } } diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index 821a093bf7..8c664fe4e0 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; +using Serilog.Context; using System; using System.Globalization; using System.Linq; @@ -179,26 +180,26 @@ namespace Tgstation.Server.Host.Controllers ModelState.Clear(); } - if (ApiHeaders != null) - Logger.LogDebug( - "Request details: User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}{5} to Instance {6}", - AuthenticationContext?.User.Id.Value.ToString(CultureInfo.InvariantCulture), - ApiHeaders.ApiVersion.Semver(), - ApiHeaders.RawUserAgent, - Request.Method, - Request.Path, - Request.QueryString, - ApiHeaders.InstanceId); - - try + using (ApiHeaders?.InstanceId != null + ? LogContext.PushProperty("Instance", ApiHeaders.InstanceId) + : null) + using (AuthenticationContext != null + ? LogContext.PushProperty("User", AuthenticationContext.User.Id) + : null) + using (LogContext.PushProperty("Request", $"{Request.Method} {Request.Path}")) { + if (ApiHeaders != null) + Logger.LogDebug( + "Starting API Request: Version: {1}. User-Agent: {2}", + AuthenticationContext?.User.Id.Value.ToString(CultureInfo.InvariantCulture), + ApiHeaders.ApiVersion.Semver(), + ApiHeaders.RawUserAgent, + Request.Method, + Request.Path, + Request.QueryString, + ApiHeaders.InstanceId); await base.OnActionExecutionAsync(context, next).ConfigureAwait(false); } - catch (OperationCanceledException e) - { - Logger.LogDebug("Request cancelled! Exception: {0}", e); - throw; - } } #pragma warning restore CA1506 } diff --git a/src/Tgstation.Server.Host/Controllers/BridgeController.cs b/src/Tgstation.Server.Host/Controllers/BridgeController.cs index 9733a7081f..3ee3155cd8 100644 --- a/src/Tgstation.Server.Host/Controllers/BridgeController.cs +++ b/src/Tgstation.Server.Host/Controllers/BridgeController.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using Serilog.Context; using System; using System.Net; using System.Threading; @@ -18,6 +20,11 @@ namespace Tgstation.Server.Host.Controllers [Produces(ApiHeaders.ApplicationJson)] public class BridgeController : Controller { + /// + /// Static counter for the number of requests processed. + /// + static long requestsProcessed; + /// /// The for the /// @@ -32,11 +39,17 @@ namespace Tgstation.Server.Host.Controllers /// Initializes a new instance of the . /// /// The value of . + /// The of the server. /// The value of . - public BridgeController(IBridgeDispatcher bridgeDispatcher, ILogger logger) + public BridgeController(IBridgeDispatcher bridgeDispatcher, IHostApplicationLifetime applicationLifetime, ILogger logger) { this.bridgeDispatcher = bridgeDispatcher ?? throw new ArgumentNullException(nameof(bridgeDispatcher)); + if (applicationLifetime == null) + throw new ArgumentNullException(nameof(applicationLifetime)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + applicationLifetime.ApplicationStopped.Register(() => requestsProcessed = 0); } /// @@ -52,26 +65,29 @@ namespace Tgstation.Server.Host.Controllers if (!IPAddress.IsLoopback(Request.HttpContext.Connection.RemoteIpAddress)) return NotFound(); - BridgeParameters request; - try + using (LogContext.PushProperty("Bridge", Interlocked.Increment(ref requestsProcessed))) { - request = JsonConvert.DeserializeObject(data, DMApiConstants.SerializerSettings); + BridgeParameters request; + try + { + request = JsonConvert.DeserializeObject(data, DMApiConstants.SerializerSettings); + } + catch + { + logger.LogWarning("Error deserializing bridge request: {0}", data); + return BadRequest(); + } + + logger.LogTrace("Bridge Request: {0}", data); + + var response = await bridgeDispatcher.ProcessBridgeRequest(request, cancellationToken).ConfigureAwait(false); + if (response == null) + Forbid(); + + var responseJson = JsonConvert.SerializeObject(response, DMApiConstants.SerializerSettings); + logger.LogTrace("Bridge Response: {0}", responseJson); + return Content(responseJson, ApiHeaders.ApplicationJson); } - catch - { - logger.LogWarning("Error deserializing bridge request: {0}", data); - return BadRequest(); - } - - logger.LogTrace("Bridge Request: {0}", data); - - var response = await bridgeDispatcher.ProcessBridgeRequest(request, cancellationToken).ConfigureAwait(false); - if (response == null) - Forbid(); - - var responseJson = JsonConvert.SerializeObject(response, DMApiConstants.SerializerSettings); - logger.LogTrace("Bridge Response: {0}", responseJson); - return Content(responseJson, ApiHeaders.ApplicationJson); } } } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 26c86508b3..c55405bc2e 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -308,7 +308,7 @@ namespace Tgstation.Server.Host.Controllers /// /// Detach an with the given . /// - /// The to detach. + /// The of the instance to detach. /// The for the operation. /// A resulting in the of the request. /// Instance detatched successfully. @@ -472,7 +472,7 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); if (renamed) - instanceManager.GetInstance(originalModel).Rename(originalModel.Name); + await instanceManager.GetInstance(originalModel).InstanceRenamed(originalModel.Name, cancellationToken).ConfigureAwait(false); var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart; try @@ -579,7 +579,7 @@ namespace Tgstation.Server.Host.Controllers /// /// Get a specific . /// - /// The to retrieve. + /// The instance to retrieve. /// The for the operation. /// A resulting in the of the request. /// Retrieved successfully. diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 7f486eeb43..8bc5133bc8 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -133,10 +133,12 @@ namespace Tgstation.Server.Host.Core var logEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.LogLevel); var formatter = new MessageTemplateTextFormatter( - "{Timestamp:o} {RequestId,13} [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}", + "{Timestamp:o} " + + ServiceCollectionExtensions.SerilogContextTemplate + + ": [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}", null); - logPath = IOManager.ConcatPath(logPath, "tgs-{Date}.log"); + logPath = IOManager.ConcatPath(logPath, "tgs-.log"); var rollingFileConfig = sinkConfig.File( formatter, logPath, @@ -167,6 +169,7 @@ namespace Tgstation.Server.Host.Core }; }); + // WARNING: STATIC CODE // fucking prevents converting 'sub' to M$ bs // can't be done in the above lambda, that's too late JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); @@ -351,7 +354,11 @@ namespace Tgstation.Server.Host.Core // attempt to restart the server if the configuration changes if (serverControl.WatchdogPresent) - ChangeToken.OnChange(Configuration.GetReloadToken, () => serverControl.Restart()); + ChangeToken.OnChange(Configuration.GetReloadToken, () => + { + logger.LogInformation("Configuration change detected"); + serverControl.Restart(); + }); // setup the HTTP request pipeline // Final point where we wrap exceptions in a 500 (ErrorMessage) response diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs index 10dddb039d..b75baf3e09 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs @@ -109,6 +109,11 @@ namespace Tgstation.Server.Host.Database /// protected DatabaseConfiguration DatabaseConfiguration { get; } + /// + /// The for the / foreign key. + /// + protected virtual DeleteBehavior RevInfoCompileJobDeleteBehavior => DeleteBehavior.ClientNoAction; + /// IDatabaseCollection IDatabaseContext.Users => usersCollection; @@ -271,7 +276,8 @@ namespace Tgstation.Server.Host.Database // rev info takes care of the rest // Break the link here so the db doesn't shit itself complaining about cascading deletes // EF will handle making the right query to destroy everything - revInfo.HasMany(x => x.CompileJobs).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.ClientNoAction); + // UPDATE: I fuck with this constantly in hopes of eliminating FK issues on instance detack + revInfo.HasMany(x => x.CompileJobs).WithOne(x => x.RevisionInformation).OnDelete(RevInfoCompileJobDeleteBehavior); // Also break the link between ritm and testmerge so it doesn't cycle in a triangle with rev info modelBuilder.Entity().HasMany(x => x.RevisonInformations).WithOne(x => x.TestMerge).OnDelete(DeleteBehavior.ClientNoAction); diff --git a/src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.Designer.cs index 36d63b62c3..e2e75c64e2 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.Designer.cs @@ -674,7 +674,7 @@ namespace Tgstation.Server.Host.Database.Migrations b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") .WithMany("CompileJobs") .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.ClientNoAction) + .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); diff --git a/src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.cs b/src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.cs index af77ee5d60..d76ef1939f 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.cs @@ -346,7 +346,8 @@ namespace Tgstation.Server.Host.Database.Migrations name: "FK_CompileJobs_RevisionInformations_RevisionInformationId", column: x => x.RevisionInformationId, principalTable: "RevisionInformations", - principalColumn: "Id"); + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( diff --git a/src/Tgstation.Server.Host/Database/Migrations/20200529171541_MYFixForeignKey.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20200529171541_MYFixForeignKey.Designer.cs new file mode 100644 index 0000000000..9302a18764 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20200529171541_MYFixForeignKey.Designer.cs @@ -0,0 +1,808 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20200529171541_MYFixForeignKey")] + partial class MYFixForeignKey + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.4") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("bigint unsigned"); + + b.Property("IrcChannel") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Tag") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("char(36)"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("HeartbeatSeconds") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PrimaryPort") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("SecondaryPort") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DualReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AlphaId") + .HasColumnType("bigint"); + + b.Property("AlphaIsActive") + .HasColumnType("tinyint(1)"); + + b.Property("BravoId") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("AlphaId"); + + b.HasIndex("BravoId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("WatchdogReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ByondRights") + .HasColumnType("bigint unsigned"); + + b.Property("ChatBotRights") + .HasColumnType("bigint unsigned"); + + b.Property("ConfigurationRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamDaemonRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamMakerRights") + .HasColumnType("bigint unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstanceUserRights") + .HasColumnType("bigint unsigned"); + + b.Property("RepositoryRights") + .HasColumnType("bigint unsigned"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CancelRight") + .HasColumnType("bigint unsigned"); + + b.Property("CancelRightsType") + .HasColumnType("bigint unsigned"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("ErrorCode") + .HasColumnType("int unsigned"); + + b.Property("ExceptionDetails") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("smallint unsigned"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessToken") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Author") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Comment") + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("datetime(6)"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("PullRequestRevision") + .IsRequired() + .HasColumnType("varchar(40) CHARACTER SET utf8mb4") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdministrationRights") + .HasColumnType("bigint unsigned"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("InstanceManagerRights") + .HasColumnType("bigint unsigned"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(10000); + + b.Property("PasswordHash") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("SystemIdentifier") + .HasColumnType("varchar(255) CHARACTER SET utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DualReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Alpha") + .WithMany() + .HasForeignKey("AlphaId"); + + b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Bravo") + .WithMany() + .HasForeignKey("BravoId"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", null) + .WithOne("WatchdogReattachInformation") + .HasForeignKey("Tgstation.Server.Host.Models.DualReattachInformation", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", null) + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20200529171541_MYFixForeignKey.cs b/src/Tgstation.Server.Host/Database/Migrations/20200529171541_MYFixForeignKey.cs new file mode 100644 index 0000000000..3f2e2c658f --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20200529171541_MYFixForeignKey.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Fix the CompileJob/RevisionInformation foreign key for MySQL. + /// + public partial class MYFixForeignKey : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.DropForeignKey( + name: "FK_CompileJobs_RevisionInformations_RevisionInformationId", + table: "CompileJobs"); + + migrationBuilder.AddForeignKey( + name: "FK_CompileJobs_RevisionInformations_RevisionInformationId", + table: "CompileJobs", + column: "RevisionInformationId", + principalTable: "RevisionInformations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.DropForeignKey( + name: "FK_CompileJobs_RevisionInformations_RevisionInformationId", + table: "CompileJobs"); + + migrationBuilder.AddForeignKey( + name: "FK_CompileJobs_RevisionInformations_RevisionInformationId", + table: "CompileJobs", + column: "RevisionInformationId", + principalTable: "RevisionInformations", + principalColumn: "Id"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs index 27515b7376..d098601eed 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs @@ -12,7 +12,7 @@ namespace Tgstation.Server.Host.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.3") + .HasAnnotation("ProductVersion", "3.1.4") .HasAnnotation("Relational:MaxIdentifierLength", 64); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => @@ -223,6 +223,36 @@ namespace Tgstation.Server.Host.Database.Migrations b.ToTable("DreamMakerSettings"); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.DualReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AlphaId") + .HasColumnType("bigint"); + + b.Property("AlphaIsActive") + .HasColumnType("tinyint(1)"); + + b.Property("BravoId") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("AlphaId"); + + b.HasIndex("BravoId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("WatchdogReattachInformations"); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => { b.Property("Id") @@ -604,36 +634,6 @@ namespace Tgstation.Server.Host.Database.Migrations b.ToTable("Users"); }); - modelBuilder.Entity("Tgstation.Server.Host.Models.WatchdogReattachInformation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - b.Property("AlphaId") - .HasColumnType("bigint"); - - b.Property("AlphaIsActive") - .HasColumnType("tinyint(1)"); - - b.Property("BravoId") - .HasColumnType("bigint"); - - b.Property("InstanceId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("AlphaId"); - - b.HasIndex("BravoId"); - - b.HasIndex("InstanceId") - .IsUnique(); - - b.ToTable("WatchdogReattachInformations"); - }); - modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => { b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") @@ -663,7 +663,7 @@ namespace Tgstation.Server.Host.Database.Migrations b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") .WithMany("CompileJobs") .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.ClientNoAction) + .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); @@ -685,6 +685,23 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired(); }); + modelBuilder.Entity("Tgstation.Server.Host.Models.DualReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Alpha") + .WithMany() + .HasForeignKey("AlphaId"); + + b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Bravo") + .WithMany() + .HasForeignKey("BravoId"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", null) + .WithOne("WatchdogReattachInformation") + .HasForeignKey("Tgstation.Server.Host.Models.DualReattachInformation", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => { b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") @@ -782,23 +799,6 @@ namespace Tgstation.Server.Host.Database.Migrations .WithMany("CreatedUsers") .HasForeignKey("CreatedById"); }); - - modelBuilder.Entity("Tgstation.Server.Host.Models.WatchdogReattachInformation", b => - { - b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Alpha") - .WithMany() - .HasForeignKey("AlphaId"); - - b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Bravo") - .WithMany() - .HasForeignKey("BravoId"); - - b.HasOne("Tgstation.Server.Host.Models.Instance", null) - .WithOne("WatchdogReattachInformation") - .HasForeignKey("Tgstation.Server.Host.Models.WatchdogReattachInformation", "InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); #pragma warning restore 612, 618 } } diff --git a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs index fd44242293..1cf3d0ce67 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs @@ -672,7 +672,7 @@ namespace Tgstation.Server.Host.Migrations b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") .WithMany("CompileJobs") .HasForeignKey("RevisionInformationId") - .OnDelete(DeleteBehavior.ClientNoAction) + .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); diff --git a/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs b/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs index c18d4e723e..78e936352c 100644 --- a/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs @@ -13,6 +13,9 @@ namespace Tgstation.Server.Host.Database /// sealed class MySqlDatabaseContext : DatabaseContext { + /// + protected override DeleteBehavior RevInfoCompileJobDeleteBehavior => DeleteBehavior.Cascade; + /// /// Construct a /// @@ -39,9 +42,9 @@ namespace Tgstation.Server.Host.Database { mySqlOptions.EnableRetryOnFailure(); - if (!String.IsNullOrEmpty(DatabaseConfiguration.MySqlServerVersion)) + if (!String.IsNullOrEmpty(DatabaseConfiguration.ServerVersion)) mySqlOptions.ServerVersion( - Version.Parse(DatabaseConfiguration.MySqlServerVersion), + Version.Parse(DatabaseConfiguration.ServerVersion), DatabaseConfiguration.DatabaseType == DatabaseType.MariaDB ? ServerType.MariaDb : ServerType.MySql); diff --git a/src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs b/src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs index 111f3918ed..6514bc5333 100644 --- a/src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; -using System.Diagnostics; using Tgstation.Server.Host.Configuration; namespace Tgstation.Server.Host.Database @@ -12,6 +11,9 @@ namespace Tgstation.Server.Host.Database /// sealed class PostgresSqlDatabaseContext : DatabaseContext { + /// + protected override DeleteBehavior RevInfoCompileJobDeleteBehavior => DeleteBehavior.Cascade; + /// /// Construct a /// @@ -31,15 +33,19 @@ namespace Tgstation.Server.Host.Database protected override void OnConfiguring(DbContextOptionsBuilder options) { base.OnConfiguring(options); - options.UseNpgsql(DatabaseConfiguration.ConnectionString, x => x.EnableRetryOnFailure()); + options.UseNpgsql(DatabaseConfiguration.ConnectionString, options => + { + options.EnableRetryOnFailure(); + + if (!String.IsNullOrEmpty(DatabaseConfiguration.ServerVersion)) + options.SetPostgresVersion( + Version.Parse(DatabaseConfiguration.ServerVersion)); + }); } /// protected override void ValidateDatabaseType() { - if (!Debugger.IsAttached) - throw new NotImplementedException("PostgresSQL implementation is not complete yet!"); - if (DatabaseType != DatabaseType.PostgresSql) throw new InvalidOperationException("Invalid DatabaseType for PostgresSqlDatabaseContext!"); } diff --git a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs index 2e6a5d1326..8ff326c398 100644 --- a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs @@ -15,6 +15,11 @@ namespace Tgstation.Server.Host.Extensions /// static class ServiceCollectionExtensions { + /// + /// Common template used for adding our custom log context to serilog. + /// + public const string SerilogContextTemplate = "(Instance:{Instance}|Job:{Job}|Request:{Request}|User:{User}|Monitor:{Monitor}|Bridge:{Bridge}|Chat:{ChatMessage})"; + /// /// Add a standard binding /// @@ -67,11 +72,14 @@ namespace Tgstation.Server.Host.Extensions configurationAction?.Invoke(configuration); configuration + .Enrich.FromLogContext() .WriteTo .Async(sinkConfiguration => { sinkConfiguration.Console( - outputTemplate: "[{Timestamp:HH:mm:ss}] {Level:w3}: {SourceContext:l}{NewLine} {Message:lj}{NewLine}{Exception}"); + outputTemplate: "[{Timestamp:HH:mm:ss}] {Level:w3}: {SourceContext:l} " + + SerilogContextTemplate + + "{NewLine} {Message:lj}{NewLine}{Exception}"); sinkConfigurationAction?.Invoke(sinkConfiguration); }); diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobManager.cs index 5f01fcbf5e..5cfa3d94f9 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobManager.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Serilog.Context; using System; using System.Collections.Generic; using System.Linq; @@ -77,67 +78,68 @@ namespace Tgstation.Server.Host.Jobs /// A representing the running operation async Task RunJob(Job job, Func operation, CancellationToken cancellationToken) { - try - { - void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails); + using (LogContext.PushProperty("Job", job.Id)) try { - var oldJob = job; - job = new Job { Id = oldJob.Id }; - - await operation(job, databaseContextFactory, cancellationToken).ConfigureAwait(false); - - logger.LogDebug("Job {0} completed!", job.Id); - } - catch (OperationCanceledException) - { - logger.LogDebug("Job {0} cancelled!", job.Id); - job.Cancelled = true; - } - catch (JobException e) - { - job.ErrorCode = e.ErrorCode; - job.ExceptionDetails = e.Message; - LogRegularException(); - if (e.InnerException != null) - logger.LogDebug( - "Inner exception for job {0}: {1}", - job.Id, - e.InnerException is JobException - ? e.InnerException.Message - : e.InnerException.ToString()); - } - catch (Exception e) - { - job.ExceptionDetails = e.ToString(); - LogRegularException(); - } - - await databaseContextFactory.UseContext(async databaseContext => - { - var attachedJob = new Job + void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails); + try { - Id = job.Id - }; + var oldJob = job; + job = new Job { Id = oldJob.Id }; - databaseContext.Jobs.Attach(attachedJob); - attachedJob.StoppedAt = DateTimeOffset.Now; - attachedJob.ExceptionDetails = job.ExceptionDetails; - attachedJob.ErrorCode = job.ErrorCode; - attachedJob.Cancelled = job.Cancelled; + await operation(job, databaseContextFactory, cancellationToken).ConfigureAwait(false); - await databaseContext.Save(default).ConfigureAwait(false); - }).ConfigureAwait(false); - } - finally - { - lock (synchronizationLock) - { - var handler = jobs[job.Id]; - jobs.Remove(job.Id); - handler.Dispose(); + logger.LogDebug("Job {0} completed!", job.Id); + } + catch (OperationCanceledException) + { + logger.LogDebug("Job {0} cancelled!", job.Id); + job.Cancelled = true; + } + catch (JobException e) + { + job.ErrorCode = e.ErrorCode; + job.ExceptionDetails = e.Message; + LogRegularException(); + if (e.InnerException != null) + logger.LogDebug( + "Inner exception for job {0}: {1}", + job.Id, + e.InnerException is JobException + ? e.InnerException.Message + : e.InnerException.ToString()); + } + catch (Exception e) + { + job.ExceptionDetails = e.ToString(); + LogRegularException(); + } + + await databaseContextFactory.UseContext(async databaseContext => + { + var attachedJob = new Job + { + Id = job.Id + }; + + databaseContext.Jobs.Attach(attachedJob); + attachedJob.StoppedAt = DateTimeOffset.Now; + attachedJob.ExceptionDetails = job.ExceptionDetails; + attachedJob.ErrorCode = job.ErrorCode; + attachedJob.Cancelled = job.Cancelled; + + await databaseContext.Save(default).ConfigureAwait(false); + }).ConfigureAwait(false); + } + finally + { + lock (synchronizationLock) + { + var handler = jobs[job.Id]; + jobs.Remove(job.Id); + handler.Dispose(); + } } - } } /// diff --git a/src/Tgstation.Server.Host/Jobs/README.md b/src/Tgstation.Server.Host/Jobs/README.md index 48ec91bddd..53bf47f8fc 100644 --- a/src/Tgstation.Server.Host/Jobs/README.md +++ b/src/Tgstation.Server.Host/Jobs/README.md @@ -1,5 +1,5 @@ # Jobs Subsystem - [IJobManager](./IJobManager.cs) and [implementation](./JobManager.cs) is where the bulk of the magic happens. The `RegisterOperation()` call is what takes a work unit and sets it to run asynchronously while being tracked through the API. -- [JobException] is a special .NET Exception implementation that is able to carry API `ErrorCode`s and other additional data. +- [JobException](./JobException.cs) is a special .NET Exception implementation that is able to carry API `ErrorCode`s and other additional data. - [JobHandler](./JobHandler.cs) carries the [CancellationTokenSource](https://stackoverflow.com/questions/20638952/cancellationtoken-and-cancellationtokensource-how-to-use-it) for a given job in a disposable context. diff --git a/src/Tgstation.Server.Host/Models/ChatBot.cs b/src/Tgstation.Server.Host/Models/ChatBot.cs index 27cdcfaa89..b34876bb40 100644 --- a/src/Tgstation.Server.Host/Models/ChatBot.cs +++ b/src/Tgstation.Server.Host/Models/ChatBot.cs @@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Models public const ushort DefaultChannelLimit = 100; /// - /// The + /// The instance /// public long InstanceId { get; set; } diff --git a/src/Tgstation.Server.Host/Models/DreamDaemonSettings.cs b/src/Tgstation.Server.Host/Models/DreamDaemonSettings.cs index 1285c4c49e..d7b49c1c76 100644 --- a/src/Tgstation.Server.Host/Models/DreamDaemonSettings.cs +++ b/src/Tgstation.Server.Host/Models/DreamDaemonSettings.cs @@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.Models public long Id { get; set; } /// - /// The + /// The /// public long InstanceId { get; set; } diff --git a/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs index 0137be1678..2d92d57fa1 100644 --- a/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs +++ b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs @@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.Models public long Id { get; set; } /// - /// The + /// The instance /// public long InstanceId { get; set; } diff --git a/src/Tgstation.Server.Host/Models/DualReattachInformation.cs b/src/Tgstation.Server.Host/Models/DualReattachInformation.cs index 763e5d10a3..0669e4f88c 100644 --- a/src/Tgstation.Server.Host/Models/DualReattachInformation.cs +++ b/src/Tgstation.Server.Host/Models/DualReattachInformation.cs @@ -11,7 +11,7 @@ public long Id { get; set; } /// - /// The of the the belongs to + /// The of the the belongs to /// public long InstanceId { get; set; } diff --git a/src/Tgstation.Server.Host/Models/Instance.cs b/src/Tgstation.Server.Host/Models/Instance.cs index c272b00324..0356bf0f2e 100644 --- a/src/Tgstation.Server.Host/Models/Instance.cs +++ b/src/Tgstation.Server.Host/Models/Instance.cs @@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.Models public RepositorySettings RepositorySettings { get; set; } /// - /// The for the + /// The for the /// public DualReattachInformation WatchdogReattachInformation { get; set; } @@ -48,7 +48,7 @@ namespace Tgstation.Server.Host.Models public List RevisionInformations { get; set; } /// - /// The in the + /// The s in the /// public List Jobs { get; set; } diff --git a/src/Tgstation.Server.Host/Models/InstanceUser.cs b/src/Tgstation.Server.Host/Models/InstanceUser.cs index 7b9b8491d1..2aa74e5042 100644 --- a/src/Tgstation.Server.Host/Models/InstanceUser.cs +++ b/src/Tgstation.Server.Host/Models/InstanceUser.cs @@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.Models public long Id { get; set; } /// - /// The of + /// The of /// public long InstanceId { get; set; } diff --git a/src/Tgstation.Server.Host/Models/RepositorySettings.cs b/src/Tgstation.Server.Host/Models/RepositorySettings.cs index 5493ae0fbe..df1b51305e 100644 --- a/src/Tgstation.Server.Host/Models/RepositorySettings.cs +++ b/src/Tgstation.Server.Host/Models/RepositorySettings.cs @@ -12,7 +12,7 @@ namespace Tgstation.Server.Host.Models public long Id { get; set; } /// - /// The + /// The instance /// public long InstanceId { get; set; } diff --git a/src/Tgstation.Server.Host/Models/RevisionInformation.cs b/src/Tgstation.Server.Host/Models/RevisionInformation.cs index 02dd5edef3..f1d3a2c970 100644 --- a/src/Tgstation.Server.Host/Models/RevisionInformation.cs +++ b/src/Tgstation.Server.Host/Models/RevisionInformation.cs @@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Models public long Id { get; set; } /// - /// The + /// The instance /// public long InstanceId { get; set; } diff --git a/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs index 39afcb925c..da9ea886de 100644 --- a/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs @@ -18,7 +18,7 @@ namespace Tgstation.Server.Host.Security /// Create an to populate /// /// The of the - /// The of the operation + /// The of the operation /// The the resulting 's password must be valid after /// The for the operation /// A representing the running operation diff --git a/src/Tgstation.Server.Host/ServerFactory.cs b/src/Tgstation.Server.Host/ServerFactory.cs index 6bb31a8948..9be5679521 100644 --- a/src/Tgstation.Server.Host/ServerFactory.cs +++ b/src/Tgstation.Server.Host/ServerFactory.cs @@ -73,7 +73,7 @@ namespace Tgstation.Server.Host .UseIISIntegration() .UseApplication(postSetupServices) .SuppressStatusMessages(true) - .UseShutdownTimeout(TimeSpan.FromMinutes(1))); + .UseShutdownTimeout(TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.RestartTimeout))); if (updatePath != null) hostBuilder.UseContentRoot( diff --git a/src/Tgstation.Server.Host/Setup/SetupWizard.cs b/src/Tgstation.Server.Host/Setup/SetupWizard.cs index 9e54ac8f00..89c0a2eacb 100644 --- a/src/Tgstation.Server.Host/Setup/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Setup/SetupWizard.cs @@ -180,15 +180,25 @@ namespace Tgstation.Server.Host.Setup await console.WriteAsync("Connection successful!", true, cancellationToken).ConfigureAwait(false); if (databaseConfiguration.DatabaseType == DatabaseType.MariaDB - || databaseConfiguration.DatabaseType == DatabaseType.MySql) + || databaseConfiguration.DatabaseType == DatabaseType.MySql + || databaseConfiguration.DatabaseType == DatabaseType.PostgresSql) { - await console.WriteAsync("Checking MySQL/MariaDB version...", true, cancellationToken).ConfigureAwait(false); + await console.WriteAsync($"Checking {databaseConfiguration.DatabaseType} version...", true, cancellationToken).ConfigureAwait(false); using var command = testConnection.CreateCommand(); command.CommandText = "SELECT VERSION()"; var fullVersion = (string)await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Found {0}", fullVersion), true, cancellationToken).ConfigureAwait(false); - var splits = fullVersion.Split('-'); - databaseConfiguration.MySqlServerVersion = splits.First(); + + if (databaseConfiguration.DatabaseType == DatabaseType.PostgresSql) + { + var splits = fullVersion.Split(' '); + databaseConfiguration.ServerVersion = splits[1].TrimEnd(','); + } + else + { + var splits = fullVersion.Split('-'); + databaseConfiguration.ServerVersion = splits.First(); + } } if (!isSqliteDB && !dbExists) @@ -319,13 +329,11 @@ namespace Tgstation.Server.Host.Setup await console.WriteAsync( String.Format( CultureInfo.InvariantCulture, - "Please enter one of {0}, {1}, {2}, or {3}: ", + "Please enter one of {0}, {1}, {2}, {3} or {4}: ", DatabaseType.MariaDB, DatabaseType.MySql, -#pragma warning disable SA1515 // Single-line comment should be preceded by blank line - // DatabaseType.PostgresSql, + DatabaseType.PostgresSql, DatabaseType.SqlServer, -#pragma warning restore SA1515 // Single-line comment should be preceded by blank line DatabaseType.Sqlite), false, cancellationToken) diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 4382558c26..56ba42cec1 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -67,8 +67,8 @@ - - + + diff --git a/src/Tgstation.Server.Host/appsettings.json b/src/Tgstation.Server.Host/appsettings.json index 55786ab050..2a2d1c3b41 100644 --- a/src/Tgstation.Server.Host/appsettings.json +++ b/src/Tgstation.Server.Host/appsettings.json @@ -47,7 +47,7 @@ "DropDatabase": false, "DatabaseType": "SqlServer", "ResetAdminPassword": false, - "MySqlServerVersion": null, + "ServerVersion": null, "ConnectionString": "Data Source=(local);Initial Catalog=TGS;Integrated Security=True" } } diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index aeff0bfffa..eeb4562a50 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -91,6 +91,11 @@ namespace Tgstation.Server.Tests await serverTask.ConfigureAwait(false); } catch (OperationCanceledException) { } + catch (AggregateException ex) + { + if (ex.InnerException is NotSupportedException notSupportedException) + Assert.Inconclusive(notSupportedException.Message); + } } Assert.IsTrue(server.RestartRequested, "Server not requesting restart!"); }