diff --git a/.travis.yml b/.travis.yml index 38e1a220ac..b5de2be669 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,97 @@ branches: jobs: include: + - env: + - DoxGeneration=false + - DockerBuild=false + - DMAPI=false + - CONFIG=Release + - TGS4_TEST_DATABASE_TYPE=MySql + - TGS4_TEST_CONNECTION_STRING="server=127.0.0.1;uid=root;pwd=;database=tgs_test" + name: "MySql Integration Test" + language: csharp + mono: none + services: + - mysql + dotnet: 3.1 + cache: + directories: + - $HOME/.nuget/packages: + addons: + apt: + packages: + - libc6-i386 + - libstdc++6:i386 + - env: + - DoxGeneration=false + - DockerBuild=false + - DMAPI=false + - CONFIG=Release + - TGS4_TEST_DATABASE_TYPE=Sqlite + - TGS4_TEST_CONNECTION_STRING="Data Source=TravisTestDB.sqlite3;Mode=ReadWriteCreate" + name: "Sqlite Integration Test" + language: csharp + mono: none + dotnet: 3.1 + cache: + directories: + - $HOME/.nuget/packages: + addons: + apt: + 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 + - CONFIG=Debug + name: "Debug Unit Tests" + language: csharp + mono: none + dotnet: 3.1 + cache: + directories: + - $HOME/.nuget/packages: + - env: + - DoxGeneration=false + - DockerBuild=false + - DMAPI=false + - CONFIG=Release + name: "Release Unit Tests" + language: csharp + mono: none + dotnet: 3.1 + cache: + directories: + - $HOME/.nuget/packages: + - env: + - DoxGeneration=false + - DockerBuild=true + name: "Docker Build" + services: + - docker - env: - DoxGeneration=true name: "Dox Generation" @@ -30,56 +121,11 @@ jobs: cache: directories: - $HOME/BYOND-${BYOND_MAJOR}.${BYOND_MINOR} - addons: apt: packages: - libc6-i386 - libstdc++6:i386 - - env: - - DoxGeneration=false - - DockerBuild=false - - DMAPI=false - - CONFIG=Debug - name: "Test Server Debug" - language: csharp - mono: none - dotnet: 3.1 - services: - - mysql - cache: - directories: - - $HOME/.nuget/packages: - addons: - apt: - packages: - - libc6-i386 - - libstdc++6:i386 - - env: - - DoxGeneration=false - - DockerBuild=false - - DMAPI=false - - CONFIG=Release - name: "Test Server Release" - language: csharp - mono: none - dotnet: 3.1 - services: - - mysql - cache: - directories: - - $HOME/.nuget/packages: - addons: - apt: - packages: - - libc6-i386 - - libstdc++6:i386 - - env: - - DoxGeneration=false - - DockerBuild=true - name: "Docker Build" - services: - - docker install: - if [ $DoxGeneration = false ] && [ $DockerBuild = false ] && [ $DMAPI = true ]; then build/install_byond.sh; fi diff --git a/appveyor.yml b/appveyor.yml index 3ba2f056f9..13d2da0ad0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -73,7 +73,7 @@ test_script: - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Host.Watchdog.Tests*]*" -output:".\watchdog_coverage.xml" -oldstyle - ps: $wc = New-Object 'System.Net.WebClient' - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Host.Watchdog.Tests\TestResults\results.trx)) - - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Tests*]* -[Tgstation.Server.Host]Tgstation.Server.Host.Database.Migrations..*" -output:".\server_coverage.xml" -oldstyle + - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx --logger:console;noprogress=true /p:DebugType=full tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Tests*]* -[Tgstation.Server.Host]Tgstation.Server.Host.Database.Migrations..*" -output:".\server_coverage.xml" -oldstyle - ps: $wc = New-Object 'System.Net.WebClient' - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Tests\TestResults\results.trx)) - lint-openapi -p -c build/OpenApiValidationSettings.json C:/swagger.json diff --git a/build/Version.props b/build/Version.props index 3d1ad36d6e..bda24b2315 100644 --- a/build/Version.props +++ b/build/Version.props @@ -2,10 +2,10 @@ - 4.2.8 - 6.4.1 - 7.0.1 - 5.2.1 + 4.3.0 + 6.5.0 + 7.1.0 + 5.2.2 0.4.0 1.1.0 diff --git a/build/integration_test.sh b/build/integration_test.sh new file mode 100755 index 0000000000..8492301359 --- /dev/null +++ b/build/integration_test.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +export TGS4_TEST_DISCORD_CHANNEL=493119635319947269 +export TGS4_TEST_IRC_CHANNEL=\#botbus +export TGS4_TEST_TEMP_DIRECTORY=~/tgs4_test +#token set in CI settings + +cd tests/Tgstation.Server.Tests + +dotnet build -c $CONFIG + +$HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp3.1/Tgstation.Server.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build --logger:\"console;noprogress=true\"" --format opencover --output "../../TestResults/integration_test.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Tests*]*" --exclude "[Tgstation.Server.Host]Tgstation.Server.Host.Database.Migrations.*" + +cd ../../TestResults + +bash <(curl -s https://codecov.io/bash) -f integration_test.xml -F integration diff --git a/build/test_core.sh b/build/test_core.sh index 59577f26b6..0775ff7246 100755 --- a/build/test_core.sh +++ b/build/test_core.sh @@ -7,6 +7,11 @@ mkdir TestResults source ~/.nvm/nvm.sh && nvm install 10 +if [[ ! -z "${TGS4_TEST_CONNECTION_STRING}" ]]; then + build/integration_test.sh + exit +fi + cd tests/Tgstation.Server.Api.Tests dotnet build -c $CONFIG /p:CopyLocalLockFileAssemblies=true @@ -32,22 +37,6 @@ cd ../Tgstation.Server.Host.Console.Tests dotnet build -c $CONFIG /p:CopyLocalLockFileAssemblies=true $HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp3.1/Tgstation.Server.Host.Console.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build" --format opencover --output "../../TestResults/console.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Host.Console.Tests*]*" -cd ../Tgstation.Server.Tests -export TGS4_TEST_DATABASE_TYPE=MySql -export TGS4_TEST_DISCORD_CHANNEL=493119635319947269 -export TGS4_TEST_CONNECTION_STRING="server=127.0.0.1;uid=root;pwd=;database=tgs_test" -export TGS4_TEST_IRC_CHANNEL=\#botbus -export TGS4_TEST_TEMP_DIRECTORY=~/tgs4_test -#token set in CI settings -dotnet build -c $CONFIG /p:CopyLocalLockFileAssemblies=true - -$HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp3.1/Tgstation.Server.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build" --format opencover --output "../../TestResults/servermy.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Tests*]*" --exclude "[Tgstation.Server.Host]Tgstation.Server.Host.Database.Migrations.*" - -#Run again for Sqlite -export TGS4_TEST_DATABASE_TYPE=Sqlite -export TGS4_TEST_CONNECTION_STRING="Data Source=TravisTestDB.sqlite3;Mode=ReadWriteCreate" -$HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp3.1/Tgstation.Server.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build" --format opencover --output "../../TestResults/serversl.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Tests*]*" --exclude "[Tgstation.Server.Host]Tgstation.Server.Host.Database.Migrations.*" - cd ../../TestResults bash <(curl -s https://codecov.io/bash) -f api.xml -F unittests @@ -55,5 +44,3 @@ bash <(curl -s https://codecov.io/bash) -f client.xml -F unittests bash <(curl -s https://codecov.io/bash) -f host.xml -F unittests bash <(curl -s https://codecov.io/bash) -f watchdog.xml -F unittests bash <(curl -s https://codecov.io/bash) -f console.xml -F unittests -bash <(curl -s https://codecov.io/bash) -f servermy.xml -F integration -bash <(curl -s https://codecov.io/bash) -f serversl.xml -F integration diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm index 7c0811ec59..4493aef72c 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -1,6 +1,6 @@ // tgstation-server DMAPI -#define TGS_DMAPI_VERSION "5.2.1" +#define TGS_DMAPI_VERSION "5.2.2" // All functions and datums outside this document are subject to change with any version and should not be relied on. @@ -127,6 +127,7 @@ * Call this when your initializations are complete and your game is ready to play before any player interactions happen. * * This may use [/world/var/sleep_offline] to make this happen so ensure no changes are made to it while this call is running. + * Afterwards, consider explicitly setting it to what you want to avoid this BYOND bug: http://www.byond.com/forum/post/2575184 * Before this point, note that any static files or directories may be in use by another server. Your code should account for this. * This function should not be called before ..() in [/world/proc/New]. */ diff --git a/src/DMAPI/tgs/v4/api.dm b/src/DMAPI/tgs/v4/api.dm index 1dc98f811e..5e7f3c60be 100644 --- a/src/DMAPI/tgs/v4/api.dm +++ b/src/DMAPI/tgs/v4/api.dm @@ -114,12 +114,14 @@ /datum/tgs_api/v4/OnInitializationComplete() Export(TGS4_COMM_SERVER_PRIMED) - var/tgs4_secret_sleep_offline_sauce = 24051994 + var/tgs4_secret_sleep_offline_sauce = 29051994 var/old_sleep_offline = world.sleep_offline world.sleep_offline = tgs4_secret_sleep_offline_sauce sleep(1) if(world.sleep_offline == tgs4_secret_sleep_offline_sauce) //if not someone changed it world.sleep_offline = old_sleep_offline + else + TGS_WARNING_LOG("world.sleep_offline unexpectedly changed!") /datum/tgs_api/v4/OnTopic(T) var/list/params = params2list(T) diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm index 8ca85faf05..3791cbde84 100644 --- a/src/DMAPI/tgs/v5/api.dm +++ b/src/DMAPI/tgs/v5/api.dm @@ -16,7 +16,7 @@ var/list/chat_channels /datum/tgs_api/v5/ApiVersion() - return new /datum/tgs_version("5.2.1") + return new /datum/tgs_version("5.2.2") /datum/tgs_api/v5/OnWorldNew(minimum_required_security_level) server_port = world.params[DMAPI5_PARAM_SERVER_PORT] @@ -94,6 +94,8 @@ sleep(1) if(world.sleep_offline == tgs4_secret_sleep_offline_sauce) //if not someone changed it world.sleep_offline = old_sleep_offline + else + TGS_WARNING_LOG("world.sleep_offline unexpectedly changed!") /datum/tgs_api/v5/proc/TopicResponse(error_message = null) var/list/response = list() diff --git a/src/Tgstation.Server.Api/Models/RevisionInformation.cs b/src/Tgstation.Server.Api/Models/RevisionInformation.cs index 1d05a22755..8e115fb922 100644 --- a/src/Tgstation.Server.Api/Models/RevisionInformation.cs +++ b/src/Tgstation.Server.Api/Models/RevisionInformation.cs @@ -18,6 +18,6 @@ namespace Tgstation.Server.Api.Models /// /// The s made from the /// - public ICollection? CompileJobs { get; set; } + public ICollection? CompileJobs { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/User.cs b/src/Tgstation.Server.Api/Models/User.cs index e6f50acb27..60ecb7225d 100644 --- a/src/Tgstation.Server.Api/Models/User.cs +++ b/src/Tgstation.Server.Api/Models/User.cs @@ -16,6 +16,6 @@ /// /// The who created this /// - public User? CreatedBy { get; set; } + public Internal.User? CreatedBy { get; set; } } } \ No newline at end of file diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index 539b3ce319..b9443b8897 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -162,22 +162,9 @@ namespace Tgstation.Server.Client var headersToUse = tokenRefresh ? tokenRefreshHeaders! : headers; headersToUse.SetRequestHeaders(request.Headers, instanceId); - // This is meant to be a gate against token refresh operations - await semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); - if(!tokenRefresh) - semaphoreSlim.Release(); + await Task.WhenAll(requestLoggers.Select(x => x.LogRequest(request, cancellationToken))).ConfigureAwait(false); - try - { - await Task.WhenAll(requestLoggers.Select(x => x.LogRequest(request, cancellationToken))).ConfigureAwait(false); - - response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); - } - finally - { - if (tokenRefresh) - semaphoreSlim.Release(); - } + response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); } using (response) @@ -214,8 +201,13 @@ namespace Tgstation.Server.Client if (tokenRefreshHeaders == null) return false; + var startingToken = headers.Token; + await semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); try { + if (startingToken != headers.Token) + return true; + var token = await RunRequest(Routes.Root, new object(), HttpMethod.Post, null, true, cancellationToken); headers = new ApiHeaders(headers.UserAgent!, token.Bearer!); } @@ -223,6 +215,10 @@ namespace Tgstation.Server.Client { return false; } + finally + { + semaphoreSlim.Release(); + } return true; } diff --git a/src/Tgstation.Server.Host/.config/dotnet-tools.json b/src/Tgstation.Server.Host/.config/dotnet-tools.json index 22ec38229b..1bbb1a456c 100644 --- a/src/Tgstation.Server.Host/.config/dotnet-tools.json +++ b/src/Tgstation.Server.Host/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "3.1.3", + "version": "3.1.4", "commands": [ "dotnet-ef" ] diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index 94e01c8af5..dd4caa7835 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -620,13 +620,61 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public Task SendUpdateMessage(string message, CancellationToken cancellationToken) + public async Task> SendDeploymentMessage( + Models.RevisionInformation revisionInformation, + Version byondVersion, + DateTimeOffset? estimatedCompletionTime, + string gitHubOwner, + string gitHubRepo, + bool localCommitPushed, + CancellationToken cancellationToken) { List wdChannels; - message = String.Format(CultureInfo.InvariantCulture, "DM: {0}", message); lock (mappedChannels) // so it doesn't change while we're using it wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList(); - return SendMessage(message, wdChannels, cancellationToken); + + logger.LogTrace("Sending deployment message for RevisionInformation: {0}", revisionInformation.Id); + + var callbacks = new List>(); + + await Task.WhenAll( + wdChannels.Select( + async x => + { + ChannelMapping channelMapping; + lock (mappedChannels) + if (!mappedChannels.TryGetValue(x, out channelMapping)) + return; + IProvider provider; + lock (providers) + if (!providers.TryGetValue(channelMapping.ProviderId, out provider)) + return; + try + { + var callback = await provider.SendUpdateMessage( + revisionInformation, + byondVersion, + estimatedCompletionTime, + gitHubOwner, + gitHubRepo, + channelMapping.ProviderChannelId, + localCommitPushed, + cancellationToken) + .ConfigureAwait(false); + + callbacks.Add(callback); + } + catch (Exception ex) + { + logger.LogWarning( + "Error sending deploy message to provider {0}! Exception: {1}", + channelMapping.ProviderId, + ex); + } + })) + .ConfigureAwait(false); + + return (errorMessage, dreamMakerOutput) => Task.WhenAll(callbacks.Select(x => x(errorMessage, dreamMakerOutput))); } /// diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs index 15e992da83..5e63fa62a3 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs @@ -76,14 +76,21 @@ namespace Tgstation.Server.Host.Components.Chat.Commands head = repo.Head; } - await databaseContextFactory.UseContext(async db => results = await db.RevisionInformations.Where(x => x.Instance.Id == instance.Id && x.CommitSha == head) - .SelectMany(x => x.ActiveTestMerges) - .Select(x => x.TestMerge) - .Select(x => new Models.TestMerge - { - Number = x.Number, - PullRequestRevision = x.PullRequestRevision - }).ToListAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + await databaseContextFactory.UseContext( + async db => results = await db + .RevisionInformations + .AsQueryable() + .Where(x => x.Instance.Id == instance.Id && x.CommitSha == head) + .SelectMany(x => x.ActiveTestMerges) + .Select(x => x.TestMerge) + .Select(x => new Models.TestMerge + { + Number = x.Number, + PullRequestRevision = x.PullRequestRevision + }) + .ToListAsync(cancellationToken) + .ConfigureAwait(false)) + .ConfigureAwait(false); } else { diff --git a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs index 2c9b286c30..062e8fe1bd 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs @@ -62,12 +62,24 @@ namespace Tgstation.Server.Host.Components.Chat Task SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken); /// - /// Send a chat to configured update channels + /// Send the message for a deployment to configured deployment channels. /// - /// The message being sent - /// The for the operation - /// A representing the running operation - Task SendUpdateMessage(string message, CancellationToken cancellationToken); + /// The of the deployment. + /// The BYOND of the deployment. + /// The optional the deployment is expected to be completed at. + /// The repository GitHub owner, if any. + /// The repository GitHub name, if any. + /// if the local deployment commit was pushed to the remote repository. + /// The for the operation. + /// A resulting in a to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. + Task> SendDeploymentMessage( + Models.RevisionInformation revisionInformation, + Version byondVersion, + DateTimeOffset? estimatedCompletionTime, + string gitHubOwner, + string gitHubRepo, + bool localCommitPushed, + CancellationToken cancellationToken); /// /// Start tracking s and s. diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 9e7052bcfb..ba23173db1 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components.Chat.Providers { @@ -28,6 +30,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } } + /// + /// The for the . + /// + readonly IAssemblyInformationProvider assemblyInformationProvider; + /// /// The for the /// @@ -53,12 +60,18 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// Construct a /// + /// The value of . /// The value of /// The value of /// The initial reconnect interval in minutes. - public DiscordProvider(ILogger logger, string botToken, uint reconnectInterval) + public DiscordProvider( + IAssemblyInformationProvider assemblyInformationProvider, + ILogger logger, + string botToken, + uint reconnectInterval) : base(logger, reconnectInterval) { + this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken)); client = new DiscordSocketClient(); client.MessageReceived += Client_MessageReceived; @@ -260,5 +273,138 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Logger.LogWarning("Error sending discord message: {0}", e); } } + + /// + public override async Task> SendUpdateMessage( + RevisionInformation revisionInformation, + Version byondVersion, + DateTimeOffset? estimatedCompletionTime, + string gitHubOwner, + string gitHubRepo, + ulong channelId, + bool localCommitPushed, + CancellationToken cancellationToken) + { + bool gitHub = gitHubOwner != null && gitHubRepo != null; + + localCommitPushed |= revisionInformation.CommitSha == revisionInformation.OriginCommitSha; + + var fields = new List + { + new EmbedFieldBuilder + { + Name = "BYOND Version", + Value = $"{byondVersion.Major}.{byondVersion.Minor}", + IsInline = true + }, + new EmbedFieldBuilder + { + Name = "Local Commit", + Value = localCommitPushed && gitHub + ? $"[{revisionInformation.CommitSha.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.CommitSha})" + : revisionInformation.CommitSha.Substring(0, 7), + IsInline = true + }, + new EmbedFieldBuilder + { + Name = "Branch Commit", + Value = gitHub + ? $"[{revisionInformation.OriginCommitSha.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.OriginCommitSha})" + : revisionInformation.OriginCommitSha.Substring(0, 7), + IsInline = true + } + }; + + fields.AddRange((revisionInformation.ActiveTestMerges ?? Enumerable.Empty()) + .Select(x => x.TestMerge) + .Select(x => new EmbedFieldBuilder + { + Name = $"#{x.Number}", + Value = $"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.PullRequestRevision.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.PullRequestRevision}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}" + })); + + var builder = new EmbedBuilder + { + Author = new EmbedAuthorBuilder + { + Name = assemblyInformationProvider.VersionPrefix, + Url = "https://github.com/tgstation/tgstation-server", + IconUrl = "https://avatars0.githubusercontent.com/u/1363778?s=280&v=4" + }, + Color = Color.Gold, + Description = "TGS has begun deploying active repository code to production.", + Fields = fields, + Title = "Code Deployment", + Footer = new EmbedFooterBuilder + { + Text = "In progress... ETA" + }, + Timestamp = estimatedCompletionTime + }; + + Logger.LogTrace("Attempting to post deploy embed to channel {0}...", channelId); + if (!(client.GetChannel(channelId) is IMessageChannel channel)) + { + Logger.LogTrace("Channel ID {0} does not exist or is not an IMessageChannel!", channelId); + return (errorMessage, dreamMakerOutput) => Task.CompletedTask; + } + + var message = await channel.SendMessageAsync( + String.Empty, + false, + builder.Build(), + new RequestOptions + { + CancelToken = cancellationToken + }) + .ConfigureAwait(false); + + return async (errorMessage, dreamMakerOutput) => + { + builder.Footer.Text = errorMessage == null ? "Succeeded" : "Failed"; + builder.Color = errorMessage == null ? Color.Green : Color.Red; + builder.Timestamp = DateTimeOffset.Now; + builder.Description = errorMessage == null + ? "The deployment completed successfully and will be available at the next server reboot." + : "The deployment failed."; + + if (dreamMakerOutput != null) + builder.AddField(new EmbedFieldBuilder + { + Name = "DreamMaker Output", + Value = $"```{Environment.NewLine}{dreamMakerOutput}{Environment.NewLine}```" + }); + + if (errorMessage != null) + builder.AddField(new EmbedFieldBuilder + { + Name = "Error Message", + Value = errorMessage + }); + + try + { + await message.ModifyAsync( + props => props.Embed = builder.Build()) + .ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning("Updating deploy embed {0} failed, attempting new post! Exception: {1}", message.Id, ex); + try + { + await channel.SendMessageAsync( + String.Empty, + false, + builder.Build()) + .ConfigureAwait(false); + } + catch (Exception ex2) + { + Logger.LogWarning("Posting completion deploy embed failed! Exception: {0}", ex2); + } + } + }; + } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs index d4dd515150..c06c22844f 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components.Chat.Providers { @@ -65,5 +66,27 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// The reconnection interval in minutes. /// A representing the running operation. Task SetReconnectInterval(uint reconnectInterval); + + /// + /// Send the message for a deployment. + /// + /// The of the deployment. + /// The BYOND of the deployment. + /// The optional the deployment is expected to be completed at. + /// The repository GitHub owner, if any. + /// The repository GitHub name, if any. + /// The to send to + /// if the local deployment commit was pushed to the remote repository. + /// The for the operation. + /// A resulting in a to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. + Task> SendUpdateMessage( + RevisionInformation revisionInformation, + Version byondVersion, + DateTimeOffset? estimatedCompletionTime, + string gitHubOwner, + string gitHubRepo, + ulong channelId, + bool localCommitPushed, + CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 9d66fc0f38..67e85e152a 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -467,5 +467,55 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Logger.LogWarning("Unable to send to channel: {0}", e); } }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + + /// + public override async Task> SendUpdateMessage( + Models.RevisionInformation revisionInformation, + Version byondVersion, + DateTimeOffset? estimatedCompletionTime, + string gitHubOwner, + string gitHubRepo, + ulong channelId, + bool localCommitPushed, + CancellationToken cancellationToken) + { + var commitInsert = revisionInformation.CommitSha.Substring(0, 7); + string remoteCommitInsert; + if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha) + { + commitInsert = String.Format(CultureInfo.InvariantCulture, localCommitPushed ? "^{0}" : "{0}", commitInsert); + remoteCommitInsert = String.Empty; + } + else + remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7)); + + var testmergeInsert = (revisionInformation.ActiveTestMerges?.Count ?? 0) == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})", + String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => + { + var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7)); + if (x.Comment != null) + result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment); + return result; + }))); + + await SendMessage( + channelId, + String.Format( + CultureInfo.InvariantCulture, + "DM: Deploying revision: {0}{1}{2} BYOND Version: {3}{4}", + commitInsert, + testmergeInsert, + remoteCommitInsert, + byondVersion, + estimatedCompletionTime.HasValue + ? $" ETA: {estimatedCompletionTime - DateTimeOffset.Now}" + : String.Empty), + cancellationToken).ConfigureAwait(false); + + return (errorMessage, dreamMakerOutput) => SendMessage( + channelId, + $"DM: Deployment {(errorMessage == null ? "complete" : "failed")}!", + cancellationToken); + } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs index 9a4fcae488..9f686bedca 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components.Chat.Providers { @@ -180,5 +181,16 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// public abstract Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken); + + /// + public abstract Task> SendUpdateMessage( + RevisionInformation revisionInformation, + Version byondVersion, + DateTimeOffset? estimatedCompletionTime, + string gitHubOwner, + string gitHubRepo, + ulong channelId, + bool localCommitPushed, + CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs index 8d0bc2c832..3a23549cba 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs @@ -56,7 +56,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers return new IrcProvider(assemblyInformationProvider, asyncDelayer, loggerFactory.CreateLogger(), ircBuilder.Address, ircBuilder.Port.Value, ircBuilder.Nickname, ircBuilder.Password, ircBuilder.PasswordType, settings.ReconnectionInterval.Value, ircBuilder.UseSsl.Value); case ChatProvider.Discord: var discordBuilder = (DiscordConnectionStringBuilder)builder; - return new DiscordProvider(loggerFactory.CreateLogger(), discordBuilder.BotToken, settings.ReconnectionInterval.Value); + return new DiscordProvider( + assemblyInformationProvider, + loggerFactory.CreateLogger(), + discordBuilder.BotToken, + settings.ReconnectionInterval.Value); default: throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider)); } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index 281bb892e9..978586af70 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -197,11 +197,17 @@ namespace Tgstation.Server.Host.Components.Deployment // ensure we have the entire compile job tree logger.LogTrace("Loading compile job {0}...", compileJob.Id); - await databaseContextFactory.UseContext(async db => compileJob = await db.CompileJobs.Where(x => x.Id == compileJob.Id) - .Include(x => x.Job).ThenInclude(x => x.StartedBy) - .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy) - .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy) - .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); // can't wait to see that query + await databaseContextFactory.UseContext( + async db => compileJob = await db + .CompileJobs + .AsQueryable() + .Where(x => x.Id == compileJob.Id) + .Include(x => x.Job).ThenInclude(x => x.StartedBy) + .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy) + .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy) + .FirstAsync(cancellationToken) + .ConfigureAwait(false)) + .ConfigureAwait(false); // can't wait to see that query if (!compileJob.Job.StoppedAt.HasValue) { @@ -269,8 +275,12 @@ namespace Tgstation.Server.Host.Components.Deployment // find the uids of locked directories await databaseContextFactory.UseContext(async db => { - jobUidsToNotErase = (await db.CompileJobs.Where( - x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id)) + jobUidsToNotErase = (await db + .CompileJobs + .AsQueryable() + .Where( + x => x.Job.Instance.Id == instance.Id + && jobIdsToSkip.Contains(x.Id)) .Select(x => x.DirectoryName.Value) .ToListAsync(cancellationToken) .ConfigureAwait(false)) diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index aab15c7d26..e35cfd5b15 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -14,7 +14,6 @@ using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Session; -using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; @@ -82,11 +81,6 @@ namespace Tgstation.Server.Host.Components.Deployment /// readonly IProcessExecutor processExecutor; - /// - /// The for - /// - readonly IWatchdog watchdog; - /// /// The for . /// @@ -113,14 +107,18 @@ namespace Tgstation.Server.Host.Components.Deployment readonly Api.Models.Instance metadata; /// - /// for . + /// for . /// - readonly object compilingLock; + readonly object deploymentLock; + + Func currentChatCallback; + + string currentDreamMakerOutput; /// /// If a compile job is running /// - bool compiling; + bool deploying; /// /// Construct @@ -132,7 +130,6 @@ namespace Tgstation.Server.Host.Components.Deployment /// The value of /// The value of /// The value of - /// The value of /// The value of . /// The value of . /// The value of . @@ -146,7 +143,6 @@ namespace Tgstation.Server.Host.Components.Deployment IEventConsumer eventConsumer, IChatManager chatManager, IProcessExecutor processExecutor, - IWatchdog watchdog, IGitHubClientFactory gitHubClientFactory, ICompileJobSink compileJobConsumer, IRepositoryManager repositoryManager, @@ -160,14 +156,13 @@ namespace Tgstation.Server.Host.Components.Deployment this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); this.chatManager = chatManager ?? throw new ArgumentNullException(nameof(chatManager)); this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); - this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog)); this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer)); this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); - compilingLock = new object(); + deploymentLock = new object(); } /// @@ -289,7 +284,7 @@ namespace Tgstation.Server.Host.Components.Deployment cancellationToken.ThrowIfCancellationRequested(); logger.LogDebug("DreamMaker exit code: {0}", exitCode); - job.Output = dm.GetCombinedOutput(); + currentDreamMakerOutput = job.Output = dm.GetCombinedOutput(); logger.LogDebug("DreamMaker output: {0}{1}", Environment.NewLine, job.Output); return exitCode; } @@ -352,13 +347,10 @@ namespace Tgstation.Server.Host.Components.Deployment /// Cleans up a failed compile /// /// The running - /// If the was cancelled - /// The for the operation /// A representing the running operation - async Task CleanupFailedCompile(Models.CompileJob job, bool cancelled, CancellationToken cancellationToken) + async Task CleanupFailedCompile(Models.CompileJob job) { logger.LogTrace("Cleaning compile directory..."); - var chatTask = chatManager.SendUpdateMessage(cancelled ? "Deploy cancelled!" : "Deploy failed!", cancellationToken); var jobPath = job.DirectoryName.ToString(); try { @@ -368,66 +360,6 @@ namespace Tgstation.Server.Host.Components.Deployment { logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(jobPath), e); } - - await chatTask.ConfigureAwait(false); - } - - /// - /// Send a message to about a deployment - /// - /// The for the deployment - /// The for the deployment - /// The for the operation - /// A representing the running operation - async Task SendDeploymentMessage(Models.RevisionInformation revisionInformation, IByondExecutableLock byondLock, CancellationToken cancellationToken) - { - var commitInsert = revisionInformation.CommitSha.Substring(0, 7); - string remoteCommitInsert; - if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha) - { - commitInsert = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert); - remoteCommitInsert = String.Empty; - } - else - remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7)); - - var testmergeInsert = (revisionInformation.ActiveTestMerges?.Count ?? 0) == 0 - ? String.Empty - : String.Format( - CultureInfo.InvariantCulture, - "{0}Test Merges:{1}", - Environment.NewLine, - String.Join( - Environment.NewLine, - revisionInformation - .ActiveTestMerges - .Select(x => x.TestMerge) - .Select(x => - { - var result = String.Format( - CultureInfo.InvariantCulture, - "- #{0} at {1}", - x.Number, - x.PullRequestRevision.Substring(0, 7)); - - if (x.Comment != null) - result += $": {x.Comment}"; - - return result; - }))); - - await chatManager.SendUpdateMessage( - String.Format( - CultureInfo.InvariantCulture, - "*Deployment Triggered*{0}Revision: {1}{2}{3}{0}BYOND Version: {4}.{5}", - Environment.NewLine, - commitInsert, - testmergeInsert, - remoteCommitInsert, - byondLock.Version.Major, - byondLock.Version.Minor), - cancellationToken) - .ConfigureAwait(false); } /// @@ -523,9 +455,9 @@ namespace Tgstation.Server.Host.Components.Deployment logger.LogDebug("Compile complete!"); } - catch (Exception e) + catch { - await CleanupFailedCompile(job, e is OperationCanceledException, cancellationToken).ConfigureAwait(false); + await CleanupFailedCompile(job).ConfigureAwait(false); throw; } } @@ -547,174 +479,209 @@ namespace Tgstation.Server.Host.Components.Deployment if (progressReporter == null) throw new ArgumentNullException(nameof(progressReporter)); - string repoOwner = null; - string repoName = null; - TimeSpan? averageSpan = null; - Models.RepositorySettings repositorySettings = null; - Models.DreamDaemonSettings ddSettings = null; - DreamMakerSettings dreamMakerSettings = null; - IRepository repo = null; + lock (deploymentLock) + { + if (deploying) + throw new JobException(ErrorCode.DreamMakerCompileJobInProgress); + deploying = true; + } + + currentChatCallback = null; + currentDreamMakerOutput = null; Models.CompileJob compileJob = null; - Models.RevisionInformation revInfo = null; - await databaseContextFactory.UseContext( - async databaseContext => - { - averageSpan = await CalculateExpectedDeploymentTime(databaseContext, cancellationToken).ConfigureAwait(false); - - ddSettings = await databaseContext - .DreamDaemonSettings - .Where(x => x.InstanceId == metadata.Id) - .Select(x => new Models.DreamDaemonSettings - { - StartupTimeout = x.StartupTimeout, - }) - .FirstOrDefaultAsync(cancellationToken) - .ConfigureAwait(false); - if (ddSettings == default) - throw new JobException(ErrorCode.InstanceMissingDreamDaemonSettings); - - dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false); - if (dreamMakerSettings == default) - throw new JobException(ErrorCode.InstanceMissingDreamMakerSettings); - - repositorySettings = await databaseContext - .RepositorySettings - .Where(x => x.InstanceId == metadata.Id) - .Select(x => new Models.RepositorySettings - { - AccessToken = x.AccessToken, - ShowTestMergeCommitters = x.ShowTestMergeCommitters, - PushTestMergeCommits = x.PushTestMergeCommits, - PostTestMergeComment = x.PostTestMergeComment - }) - .FirstOrDefaultAsync(cancellationToken) - .ConfigureAwait(false); - if (repositorySettings == default) - throw new JobException(ErrorCode.InstanceMissingRepositorySettings); - - repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false); - try - { - if (repo == null) - throw new JobException(ErrorCode.RepoMissing); - - if (repo.IsGitHubRepository) - { - repoOwner = repo.GitHubOwner; - repoName = repo.GitHubRepoName; - } - - var repoSha = repo.Head; - revInfo = await databaseContext - .RevisionInformations - .Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id) - .Include(x => x.ActiveTestMerges) - .ThenInclude(x => x.TestMerge) - .ThenInclude(x => x.MergedBy) - .FirstOrDefaultAsync(cancellationToken) - .ConfigureAwait(false); - - if (revInfo == default) - { - revInfo = new Models.RevisionInformation - { - CommitSha = repoSha, - OriginCommitSha = repoSha, - Instance = new Models.Instance - { - Id = metadata.Id - }, - ActiveTestMerges = new List() - }; - - logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha); - databaseContext.Instances.Attach(revInfo.Instance); - await databaseContext.Save(cancellationToken).ConfigureAwait(false); - } - } - catch - { - repo.Dispose(); - throw; - } - }) - .ConfigureAwait(false); - - using (repo) - compileJob = await Compile( - revInfo, - dreamMakerSettings, - ddSettings.StartupTimeout.Value, - repo, - progressReporter, - averageSpan, - cancellationToken) - .ConfigureAwait(false); - - var activeCompileJob = compileJobConsumer.LatestCompileJob(); try { + string repoOwner = null; + string repoName = null; + TimeSpan? averageSpan = null; + Models.RepositorySettings repositorySettings = null; + Models.DreamDaemonSettings ddSettings = null; + DreamMakerSettings dreamMakerSettings = null; + IRepository repo = null; + Models.RevisionInformation revInfo = null; await databaseContextFactory.UseContext( async databaseContext => { - compileJob.Job = new Models.Job - { - Id = job.Id - }; - compileJob.RevisionInformation = new Models.RevisionInformation - { - Id = revInfo.Id - }; + averageSpan = await CalculateExpectedDeploymentTime(databaseContext, cancellationToken).ConfigureAwait(false); - databaseContext.Jobs.Attach(compileJob.Job); - databaseContext.RevisionInformations.Attach(compileJob.RevisionInformation); - databaseContext.CompileJobs.Add(compileJob); + ddSettings = await databaseContext + .DreamDaemonSettings + .AsQueryable() + .Where(x => x.InstanceId == metadata.Id) + .Select(x => new Models.DreamDaemonSettings + { + StartupTimeout = x.StartupTimeout, + }) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + if (ddSettings == default) + throw new JobException(ErrorCode.InstanceMissingDreamDaemonSettings); - // The difficulty with compile jobs is they have a two part commit - await databaseContext.Save(cancellationToken).ConfigureAwait(false); + dreamMakerSettings = await databaseContext + .DreamMakerSettings + .AsQueryable() + .Where(x => x.InstanceId == metadata.Id) + .FirstAsync(cancellationToken) + .ConfigureAwait(false); + if (dreamMakerSettings == default) + throw new JobException(ErrorCode.InstanceMissingDreamMakerSettings); + + repositorySettings = await databaseContext + .RepositorySettings + .AsQueryable() + .Where(x => x.InstanceId == metadata.Id) + .Select(x => new Models.RepositorySettings + { + AccessToken = x.AccessToken, + ShowTestMergeCommitters = x.ShowTestMergeCommitters, + PushTestMergeCommits = x.PushTestMergeCommits, + PostTestMergeComment = x.PostTestMergeComment + }) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + if (repositorySettings == default) + throw new JobException(ErrorCode.InstanceMissingRepositorySettings); + + repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false); try { - await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false); + if (repo == null) + throw new JobException(ErrorCode.RepoMissing); + + if (repo.IsGitHubRepository) + { + repoOwner = repo.GitHubOwner; + repoName = repo.GitHubRepoName; + } + + var repoSha = repo.Head; + revInfo = await databaseContext + .RevisionInformations + .AsQueryable() + .Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id) + .Include(x => x.ActiveTestMerges) + .ThenInclude(x => x.TestMerge) + .ThenInclude(x => x.MergedBy) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + + if (revInfo == default) + { + revInfo = new Models.RevisionInformation + { + CommitSha = repoSha, + OriginCommitSha = repoSha, + Instance = new Models.Instance + { + Id = metadata.Id + }, + ActiveTestMerges = new List() + }; + + logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha); + databaseContext.Instances.Attach(revInfo.Instance); + await databaseContext.Save(cancellationToken).ConfigureAwait(false); + } } catch { - // So we need to un-commit the compile job if the above throws - databaseContext.CompileJobs.Remove(compileJob); - await databaseContext.Save(default).ConfigureAwait(false); + repo.Dispose(); throw; } }) .ConfigureAwait(false); + + var likelyPushedTestMergeCommit = + repositorySettings.PushTestMergeCommits.Value + && repositorySettings.AccessToken != null + && repositorySettings.AccessUser != null; + using (repo) + compileJob = await Compile( + revInfo, + dreamMakerSettings, + ddSettings.StartupTimeout.Value, + repo, + progressReporter, + averageSpan, + likelyPushedTestMergeCommit, + cancellationToken) + .ConfigureAwait(false); + + var activeCompileJob = compileJobConsumer.LatestCompileJob(); + try + { + await databaseContextFactory.UseContext( + async databaseContext => + { + compileJob.Job = new Models.Job + { + Id = job.Id + }; + compileJob.RevisionInformation = new Models.RevisionInformation + { + Id = revInfo.Id + }; + + databaseContext.Jobs.Attach(compileJob.Job); + databaseContext.RevisionInformations.Attach(compileJob.RevisionInformation); + databaseContext.CompileJobs.Add(compileJob); + + // The difficulty with compile jobs is they have a two part commit + await databaseContext.Save(cancellationToken).ConfigureAwait(false); + try + { + await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false); + } + catch + { + // So we need to un-commit the compile job if the above throws + databaseContext.CompileJobs.Remove(compileJob); + await databaseContext.Save(default).ConfigureAwait(false); + throw; + } + }) + .ConfigureAwait(false); + } + catch + { + await CleanupFailedCompile(compileJob).ConfigureAwait(false); + throw; + } + + var commentsTask = PostDeploymentComments( + revInfo, + activeCompileJob?.RevisionInformation, + repositorySettings, + repoOwner, + repoName); + + var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, null, cancellationToken); + + var chatTask = currentChatCallback(null, compileJob.Output); + currentChatCallback = null; + + try + { + await Task.WhenAll(commentsTask, eventTask, chatTask).ConfigureAwait(false); + } + catch (Exception ex) + { + throw new JobException(ErrorCode.PostDeployFailure, ex); + } } catch (Exception ex) { - await CleanupFailedCompile(compileJob, ex is OperationCanceledException, default).ConfigureAwait(false); + if (currentChatCallback != null) + await currentChatCallback( + ex.Message, + currentDreamMakerOutput) + .ConfigureAwait(false); + throw; } - - var commentsTask = PostDeploymentComments( - revInfo, - activeCompileJob?.RevisionInformation, - repositorySettings, - repoOwner, - repoName); - - var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, null, cancellationToken); - - var chatTask = chatManager.SendUpdateMessage( - String.Format( - CultureInfo.InvariantCulture, - "Deployment complete! Changes will be applied when DreamDaemon {0}.", - watchdog.Running ? "reboots" : "is launched"), - cancellationToken); - - try + finally { - await Task.WhenAll(commentsTask, eventTask, chatTask).ConfigureAwait(false); - } - catch (Exception ex) - { - throw new JobException(ErrorCode.PostDeployFailure, ex); + deploying = false; } } #pragma warning restore CA1506 @@ -727,7 +694,9 @@ namespace Tgstation.Server.Host.Components.Deployment /// A resulting in the average of the 10 previous deployments or if there are none. async Task CalculateExpectedDeploymentTime(IDatabaseContext databaseContext, CancellationToken cancellationToken) { - var previousCompileJobs = await databaseContext.CompileJobs + var previousCompileJobs = await databaseContext + .CompileJobs + .AsQueryable() .Where(x => x.Job.Instance.Id == metadata.Id) .OrderByDescending(x => x.Job.StoppedAt) .Take(10) @@ -751,23 +720,32 @@ namespace Tgstation.Server.Host.Components.Deployment return averageSpan; } - async Task Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken) + async Task Compile( + Models.RevisionInformation revisionInformation, + Api.Models.DreamMaker dreamMakerSettings, + uint apiValidateTimeout, + IRepository repository, + Action progressReporter, + TimeSpan? estimatedDuration, + bool localCommitExistsOnRemote, + CancellationToken cancellationToken) { logger.LogTrace("Begin Compile"); - lock (compilingLock) - { - if (compiling) - throw new JobException(ErrorCode.DreamMakerCompileJobInProgress); - compiling = true; - } - using var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var progressTask = estimatedDuration.HasValue ? ProgressTask(progressReporter, estimatedDuration.Value, progressCts.Token) : Task.CompletedTask; try { using var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false); - await SendDeploymentMessage(revisionInformation, byondLock, cancellationToken).ConfigureAwait(false); + currentChatCallback = await chatManager.SendDeploymentMessage( + revisionInformation, + byondLock.Version, + DateTimeOffset.Now + estimatedDuration, + repository.GitHubOwner, + repository.GitHubRepoName, + localCommitExistsOnRemote, + cancellationToken) + .ConfigureAwait(false); var job = new Models.CompileJob { @@ -788,7 +766,6 @@ namespace Tgstation.Server.Host.Components.Deployment } finally { - compiling = false; progressCts.Cancel(); await progressTask.ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index d1c281352b..2782ee9934 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -162,6 +162,7 @@ namespace Tgstation.Server.Host.Components await databaseContextFactory.UseContext( async (db) => user = await db .Users + .AsQueryable() .Where(x => x.CanonicalName == User.CanonicalizeName(Api.Models.User.AdminName)) .FirstAsync(cancellationToken) .ConfigureAwait(false)) @@ -189,7 +190,11 @@ namespace Tgstation.Server.Host.Components await databaseContextFactory.UseContext( async databaseContext => { - var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken); + var repositorySettingsTask = databaseContext + .RepositorySettings + .AsQueryable() + .Where(x => x.InstanceId == metadata.Id) + .FirstAsync(jobCancellationToken); const int NumSteps = 3; var doneSteps = 0; @@ -225,6 +230,7 @@ namespace Tgstation.Server.Host.Components bool hasDbChanges = false; Task LoadRevInfo() => databaseContext.RevisionInformations + .AsQueryable() .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id) .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge) .FirstOrDefaultAsync(cancellationToken); @@ -294,6 +300,7 @@ namespace Tgstation.Server.Host.Components var currentHead = repo.Head; currentRevInfo = await databaseContext.RevisionInformations + .AsQueryable() .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id) .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 9be0802cc7..23aa1da429 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -262,7 +262,6 @@ namespace Tgstation.Server.Host.Components eventConsumer, chatManager, processExecutor, - watchdog, gitHubClientFactory, dmbFactory, repoManager, diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 3e61925a27..062dca8163 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -213,10 +213,14 @@ namespace Tgstation.Server.Host.Components var tasks = new List(); await databaseContextFactory.UseContext(async db => { - var jobs = db.Jobs.Where(x => x.Instance.Id == metadata.Id).Select(x => new Models.Job - { - Id = x.Id - }).ToAsyncEnumerable(); + var jobs = db + .Jobs + .AsQueryable() + .Where(x => x.Instance.Id == metadata.Id) + .Select(x => new Models.Job + { + Id = x.Id + }); await jobs.ForEachAsync(job => { lock (tasks) @@ -271,7 +275,10 @@ namespace Tgstation.Server.Host.Components var factoryStartup = instanceFactory.StartAsync(cancellationToken); await databaseContext.Initialize(cancellationToken).ConfigureAwait(false); await jobManager.StartAsync(cancellationToken).ConfigureAwait(false); - var dbInstances = databaseContext.Instances.Where(x => x.Online.Value) + var dbInstances = databaseContext + .Instances + .AsQueryable() + .Where(x => x.Online.Value) .Include(x => x.RepositorySettings) .Include(x => x.ChatSettings) .ThenInclude(x => x.Channels) diff --git a/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs b/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs index 125938305b..d91d5bdbf7 100644 --- a/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs +++ b/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs @@ -33,7 +33,7 @@ namespace Tgstation.Server.Host.Components.Interop /// /// The DMAPI being used. /// - public static readonly Version Version = new Version(5, 2, 1); + public static readonly Version Version = new Version(5, 2, 2); /// /// for use when communicating with the DMAPI. diff --git a/src/Tgstation.Server.Host/Components/Session/ReattachInfoHandler.cs b/src/Tgstation.Server.Host/Components/Session/ReattachInfoHandler.cs index c194aad5b0..001f0a1087 100644 --- a/src/Tgstation.Server.Host/Components/Session/ReattachInfoHandler.cs +++ b/src/Tgstation.Server.Host/Components/Session/ReattachInfoHandler.cs @@ -56,7 +56,11 @@ namespace Tgstation.Server.Host.Components.Session logger.LogDebug("Saving reattach information: {0}...", reattachInformation); - var deleteTask = db.WatchdogReattachInformations.Where(x => x.InstanceId == metadata.Id).DeleteAsync(cancellationToken); + var deleteTask = db + .WatchdogReattachInformations + .AsQueryable() + .Where(x => x.InstanceId == metadata.Id) + .DeleteAsync(cancellationToken); Models.ReattachInformation ConvertReattachInfo(ReattachInformation wdInfo) { @@ -93,7 +97,9 @@ namespace Tgstation.Server.Host.Components.Session Models.DualReattachInformation result = null; await databaseContextFactory.UseContext(async (db) => { - var instance = await db.Instances.Where(x => x.Id == metadata.Id) + var instance = await db.Instances + .AsQueryable() + .Where(x => x.Id == metadata.Id) .Include(x => x.WatchdogReattachInformation).ThenInclude(x => x.Alpha).ThenInclude(x => x.CompileJob) .Include(x => x.WatchdogReattachInformation).ThenInclude(x => x.Bravo).ThenInclude(x => x.CompileJob) .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 13c96f350a..e3e8968fd8 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -192,7 +192,7 @@ namespace Tgstation.Server.Host.Components.Watchdog }; /// - protected override void DisposeAndNullControllers() + protected override void DisposeAndNullControllersImpl() { Server?.Dispose(); Server = null; diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs index 02daf7d157..6547ef6d62 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs @@ -392,10 +392,8 @@ namespace Tgstation.Server.Host.Components.Watchdog } #pragma warning restore CA1502 - /// - /// Call on and and set them to - /// - protected override void DisposeAndNullControllers() + /// + protected override void DisposeAndNullControllersImpl() { alphaServer?.Dispose(); alphaServer = null; diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 74b3e06371..d6eafdef32 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -112,6 +112,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IRestartRegistration restartRegistration; + /// + /// used for . + /// + readonly object controllerDisposeLock; + /// /// If the should in /// @@ -201,6 +206,7 @@ namespace Tgstation.Server.Host.Components.Watchdog ActiveLaunchParameters = initialLaunchParameters; releaseServers = false; ActiveParametersUpdated = new TaskCompletionSource(); + controllerDisposeLock = new object(); restartRegistration = serverControl.RegisterForRestart(this); try @@ -463,7 +469,16 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Call and null the fields for all s and set to . /// - protected abstract void DisposeAndNullControllers(); + protected abstract void DisposeAndNullControllersImpl(); + + /// + /// Wrapper for under a locked context. + /// + protected void DisposeAndNullControllers() + { + lock (controllerDisposeLock) + DisposeAndNullControllersImpl(); + } /// /// Get the active . @@ -857,6 +872,7 @@ namespace Tgstation.Server.Host.Components.Watchdog await databaseContextFactory.UseContext( async db => adminUserId = await db .Users + .AsQueryable() .Where(x => x.CanonicalName == Models.User.CanonicalizeName(Api.Models.User.AdminName)) .Select(x => x.Id) .FirstAsync(cancellationToken) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index 895347f4a6..730ced06b9 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -101,9 +101,9 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - protected override void DisposeAndNullControllers() + protected override void DisposeAndNullControllersImpl() { - base.DisposeAndNullControllers(); + base.DisposeAndNullControllersImpl(); // If we reach this point, we can guarantee PrepServerForLaunch will be called before starting again. activeSwappable = null; diff --git a/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs b/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs index a02f70256e..abe1a6d5df 100644 --- a/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json.Converters; namespace Tgstation.Server.Host.Configuration { /// - /// Configuration options for the + /// Configuration options for the /// sealed class DatabaseConfiguration { diff --git a/src/Tgstation.Server.Host/Configuration/DatabaseType.cs b/src/Tgstation.Server.Host/Configuration/DatabaseType.cs index e370e40c68..78c4c75362 100644 --- a/src/Tgstation.Server.Host/Configuration/DatabaseType.cs +++ b/src/Tgstation.Server.Host/Configuration/DatabaseType.cs @@ -24,5 +24,10 @@ /// Use Sqlite /// Sqlite, + + /// + /// Use PostgresSql + /// + PostgresSql, } } diff --git a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs index 9b5f34f5f6..39c4ac068c 100644 --- a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs @@ -14,6 +14,11 @@ namespace Tgstation.Server.Host.Configuration /// public const string Section = "General"; + /// + /// The default value of . + /// + public const ushort DefaultApiPort = 5000; + /// /// The default value for . /// @@ -37,7 +42,12 @@ namespace Tgstation.Server.Host.Configuration /// /// The default value for /// - const int DefaultRestartTimeout = 10000; + const int DefaultRestartTimeout = 60000; + + /// + /// The port the TGS API listens on. + /// + public ushort ApiPort { get; set; } /// /// A GitHub personal access token to use for bypassing rate limits on requests. Requires no scopes diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index c021746687..a60949643f 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -163,7 +163,7 @@ namespace Tgstation.Server.Host.Controllers if (ApiHeaders != null) Logger.LogDebug( - "Request made by User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}{5} to Instance {6}", + "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, diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 321a004b71..153406d210 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -80,6 +80,7 @@ namespace Tgstation.Server.Host.Controllers var countOfExistingBotsInInstance = await DatabaseContext .ChatBots + .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .CountAsync(cancellationToken) .ConfigureAwait(false); @@ -140,7 +141,14 @@ namespace Tgstation.Server.Host.Controllers public async Task Delete(long id, CancellationToken cancellationToken) { var instance = instanceManager.GetInstance(Instance); - await Task.WhenAll(instance.Chat.DeleteConnection(id, cancellationToken), DatabaseContext.ChatBots.Where(x => x.Id == id).DeleteAsync(cancellationToken)).ConfigureAwait(false); + await Task.WhenAll( + instance.Chat.DeleteConnection(id, cancellationToken), + DatabaseContext + .ChatBots + .AsQueryable() + .Where(x => x.Id == id) + .DeleteAsync(cancellationToken)) + .ConfigureAwait(false); return Ok(); } @@ -156,7 +164,11 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(IEnumerable), 200)] public async Task List(CancellationToken cancellationToken) { - var query = DatabaseContext.ChatBots.Where(x => x.InstanceId == Instance.Id).Include(x => x.Channels); + var query = DatabaseContext + .ChatBots + .AsQueryable() + .Where(x => x.InstanceId == Instance.Id) + .Include(x => x.Channels); var results = await query.ToListAsync(cancellationToken).ConfigureAwait(false); @@ -183,7 +195,10 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(410)] public async Task GetId(long id, CancellationToken cancellationToken) { - var query = DatabaseContext.ChatBots.Where(x => x.Id == id).Include(x => x.Channels); + var query = DatabaseContext.ChatBots + .AsQueryable() + .Where(x => x.Id == id) + .Include(x => x.Channels); var results = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (results == default) @@ -221,7 +236,11 @@ namespace Tgstation.Server.Host.Controllers if (earlyOut != null) return earlyOut; - var query = DatabaseContext.ChatBots.Where(x => x.InstanceId == Instance.Id && x.Id == model.Id).Include(x => x.Channels); + var query = DatabaseContext + .ChatBots + .AsQueryable() + .Where(x => x.InstanceId == Instance.Id && x.Id == model.Id) + .Include(x => x.Channels); var current = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index bae044577b..f2ed58b9d8 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -109,7 +109,13 @@ namespace Tgstation.Server.Host.Controllers if (settings == null) { - settings = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => x.DreamDaemonSettings).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + settings = await DatabaseContext + .Instances + .AsQueryable() + .Where(x => x.Id == Instance.Id) + .Select(x => x.DreamDaemonSettings) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); if (settings == default) return StatusCode((int)HttpStatusCode.Gone); } @@ -191,7 +197,13 @@ namespace Tgstation.Server.Host.Controllers return BadRequest(new ErrorMessage(ErrorCode.DreamDaemonDoubleSoft)); // alias for changing DD settings - var current = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => x.DreamDaemonSettings).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + var current = await DatabaseContext + .Instances + .AsQueryable() + .Where(x => x.Id == Instance.Id) + .Select(x => x.DreamDaemonSettings) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); if (current == default) return StatusCode((int)HttpStatusCode.Gone); diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index 3f011a9cea..bf2354fc98 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -61,7 +61,12 @@ namespace Tgstation.Server.Host.Controllers public async Task Read(CancellationToken cancellationToken) { var instance = instanceManager.GetInstance(Instance); - var dreamMakerSettings = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + var dreamMakerSettings = await DatabaseContext + .DreamMakerSettings + .AsQueryable() + .Where(x => x.InstanceId == Instance.Id) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); return Json(dreamMakerSettings.ToApi()); } @@ -79,7 +84,9 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(404)] public async Task GetId(long id, CancellationToken cancellationToken) { - var compileJob = await DatabaseContext.CompileJobs + var compileJob = await DatabaseContext + .CompileJobs + .AsQueryable() .Where(x => x.Id == id && x.Job.Instance.Id == Instance.Id) .Include(x => x.Job).ThenInclude(x => x.StartedBy) .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy) @@ -101,10 +108,17 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(List), 200)] public async Task List(CancellationToken cancellationToken) { - var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StoppedAt).Select(x => new EntityId - { - Id = x.Id - }).ToListAsync(cancellationToken).ConfigureAwait(false); + var compileJobs = await DatabaseContext + .CompileJobs + .AsQueryable() + .Where(x => x.Job.Instance.Id == Instance.Id) + .OrderByDescending(x => x.Job.StoppedAt) + .Select(x => new EntityId + { + Id = x.Id + }) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); return Json(compileJobs); } @@ -159,7 +173,12 @@ namespace Tgstation.Server.Host.Controllers if (model.ApiValidationPort == 0) throw new InvalidOperationException("ApiValidationPort cannot be 0!"); - var hostModel = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + var hostModel = await DatabaseContext + .DreamMakerSettings + .AsQueryable() + .Where(x => x.InstanceId == Instance.Id) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); if (hostModel == null) return StatusCode((int)HttpStatusCode.Gone); diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index eff10e36d1..b10307a232 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -196,12 +196,12 @@ namespace Tgstation.Server.Host.Controllers using (systemIdentity) { // Get the user from the database - IQueryable query; + IQueryable query = DatabaseContext.Users.AsQueryable(); string canonicalName = Models.User.CanonicalizeName(ApiHeaders.Username); if (systemIdentity == null) - query = DatabaseContext.Users.Where(x => x.CanonicalName == canonicalName); + query = query.Where(x => x.CanonicalName == canonicalName); else - query = DatabaseContext.Users.Where(x => x.CanonicalName == canonicalName || x.SystemIdentifier == systemIdentity.Uid); + query = query.Where(x => x.CanonicalName == canonicalName || x.SystemIdentifier == systemIdentity.Uid); var users = await query.Select(x => new User { Id = x.Id, diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index ed76728410..fe2dbb1233 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -174,18 +174,25 @@ namespace Tgstation.Server.Host.Controllers var newCancellationToken = cts.Token; try { - await DatabaseContext.Instances.ForEachAsync( - otherInstance => + await DatabaseContext + .Instances + .AsQueryable() + .Select(x => new Models.Instance { - if (++countOfOtherInstances >= generalConfiguration.InstanceLimit) - earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceLimitReached)); - else if (InstanceIsChildOf(otherInstance.Path)) - earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath)); + Path = x.Path + }) + .ForEachAsync( + otherInstance => + { + if (++countOfOtherInstances >= generalConfiguration.InstanceLimit) + earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceLimitReached)); + else if (InstanceIsChildOf(otherInstance.Path)) + earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath)); - if (earlyOut != null && !newCancellationToken.IsCancellationRequested) - cts.Cancel(); - }, - newCancellationToken) + if (earlyOut != null && !newCancellationToken.IsCancellationRequested) + cts.Cancel(); + }, + newCancellationToken) .ConfigureAwait(false); } catch (OperationCanceledException) @@ -312,7 +319,10 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(410)] public async Task Delete(long id, CancellationToken cancellationToken) { - var originalModel = await DatabaseContext.Instances.Where(x => x.Id == id) + var originalModel = await DatabaseContext + .Instances + .AsQueryable() + .Where(x => x.Id == id) .Include(x => x.WatchdogReattachInformation) .Include(x => x.WatchdogReattachInformation.Alpha) .Include(x => x.WatchdogReattachInformation.Bravo) @@ -358,7 +368,10 @@ namespace Tgstation.Server.Host.Controllers if (model == null) throw new ArgumentNullException(nameof(model)); - IQueryable InstanceQuery() => DatabaseContext.Instances.Where(x => x.Id == model.Id); + IQueryable InstanceQuery() => DatabaseContext + .Instances + .AsQueryable() + .Where(x => x.Id == model.Id); var moveJob = await InstanceQuery() .SelectMany(x => x.Jobs). @@ -435,6 +448,7 @@ namespace Tgstation.Server.Host.Controllers { var countOfExistingChatBots = await DatabaseContext .ChatBots + .AsQueryable() .Where(x => x.InstanceId == originalModel.Id) .CountAsync(cancellationToken) .ConfigureAwait(false); @@ -582,7 +596,10 @@ namespace Tgstation.Server.Host.Controllers var cantList = !AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List); IQueryable QueryForUser() { - var query = DatabaseContext.Instances.Where(x => x.Id == id); + var query = DatabaseContext + .Instances + .AsQueryable() + .Where(x => x.Id == id); if (cantList) query = query.Include(x => x.InstanceUsers); diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs index 6b0e2237cd..edd642fab8 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs @@ -101,7 +101,14 @@ namespace Tgstation.Server.Host.Controllers if (earlyOut != null) return earlyOut; - var originalUser = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == model.UserId).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + var originalUser = await DatabaseContext + .Instances + .AsQueryable() + .Where(x => x.Id == Instance.Id) + .SelectMany(x => x.InstanceUsers) + .Where(x => x.UserId == model.UserId) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); if (originalUser == null) return StatusCode((int)HttpStatusCode.Gone); @@ -141,7 +148,13 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(IEnumerable), 200)] public async Task List(CancellationToken cancellationToken) { - var users = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).ToListAsync(cancellationToken).ConfigureAwait(false); + var users = await DatabaseContext + .Instances + .AsQueryable() + .Where(x => x.Id == Instance.Id) + .SelectMany(x => x.InstanceUsers) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); return Json(users.Select(x => x.ToApi())); } @@ -160,7 +173,14 @@ namespace Tgstation.Server.Host.Controllers public async Task GetId(long id, CancellationToken cancellationToken) { // this functions as userId - var user = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + var user = await DatabaseContext + .Instances + .AsQueryable() + .Where(x => x.Id == Instance.Id) + .SelectMany(x => x.InstanceUsers) + .Where(x => x.UserId == id) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); if (user == default) return StatusCode((int)HttpStatusCode.Gone); return Json(user.ToApi()); @@ -178,7 +198,14 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(204)] public async Task Delete(long id, CancellationToken cancellationToken) { - await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).DeleteAsync(cancellationToken).ConfigureAwait(false); + await DatabaseContext + .Instances + .AsQueryable() + .Where(x => x.Id == Instance.Id) + .SelectMany(x => x.InstanceUsers) + .Where(x => x.UserId == id) + .DeleteAsync(cancellationToken) + .ConfigureAwait(false); return NoContent(); } } diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs index f912483f85..483d9ed476 100644 --- a/src/Tgstation.Server.Host/Controllers/JobController.cs +++ b/src/Tgstation.Server.Host/Controllers/JobController.cs @@ -49,7 +49,13 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(IEnumerable), 200)] public async Task Read(CancellationToken cancellationToken) { - var result = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue).OrderByDescending(x => x.StartedAt).ToListAsync(cancellationToken).ConfigureAwait(false); + var result = await DatabaseContext + .Jobs + .AsQueryable() + .Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue) + .OrderByDescending(x => x.StartedAt) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); return Json(result.Select(x => x.ToApi())); } @@ -65,10 +71,17 @@ namespace Tgstation.Server.Host.Controllers public async Task List(CancellationToken cancellationToken) { // you KNOW this will need pagination eventually right? - var jobs = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id).OrderByDescending(x => x.StartedAt).Select(x => new Api.Models.EntityId - { - Id = x.Id - }).ToListAsync(cancellationToken).ConfigureAwait(false); + var jobs = await DatabaseContext + .Jobs + .AsQueryable() + .Where(x => x.Instance.Id == Instance.Id) + .OrderByDescending(x => x.StartedAt) + .Select(x => new Api.Models.EntityId + { + Id = x.Id + }) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); return Json(jobs); } @@ -89,7 +102,12 @@ namespace Tgstation.Server.Host.Controllers public async Task Delete(long id, CancellationToken cancellationToken) { // don't care if an instance post or not at this point - var job = await DatabaseContext.Jobs.Where(x => x.Id == id && x.Instance.Id == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + var job = await DatabaseContext + .Jobs + .AsQueryable() + .Where(x => x.Id == id && x.Instance.Id == Instance.Id) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); if (job == default(Job)) return NotFound(); @@ -117,7 +135,13 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(404)] public async Task GetId(long id, CancellationToken cancellationToken) { - var job = await DatabaseContext.Jobs.Where(x => x.Id == id && x.Instance.Id == Instance.Id).Include(x => x.StartedBy).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + var job = await DatabaseContext + .Jobs + .AsQueryable() + .Where(x => x.Id == id && x.Instance.Id == Instance.Id) + .Include(x => x.StartedBy) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); if (job == default) return NotFound(); var api = job.ToApi(); diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 08bc40afbd..287a9bc26a 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -156,7 +156,12 @@ namespace Tgstation.Server.Host.Controllers if (model.AccessUser == null ^ model.AccessToken == null) return BadRequest(ErrorCode.RepoMismatchUserAndAccessToken); - var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + var currentModel = await DatabaseContext + .RepositorySettings + .AsQueryable() + .Where(x => x.InstanceId == Instance.Id) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); if (currentModel == default) return StatusCode((int)HttpStatusCode.Gone); @@ -236,7 +241,12 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(410)] public async Task Delete(CancellationToken cancellationToken) { - var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + var currentModel = await DatabaseContext + .RepositorySettings + .AsQueryable() + .Where(x => x.InstanceId == Instance.Id) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); if (currentModel == default) return StatusCode((int)HttpStatusCode.Gone); @@ -275,7 +285,12 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(410)] public async Task Read(CancellationToken cancellationToken) { - var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + var currentModel = await DatabaseContext + .RepositorySettings + .AsQueryable() + .Where(x => x.InstanceId == Instance.Id) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); if (currentModel == default) return StatusCode((int)HttpStatusCode.Gone, new ErrorMessage(ErrorCode.RepoMissing)); @@ -345,6 +360,7 @@ namespace Tgstation.Server.Host.Controllers var currentModel = await DatabaseContext .RepositorySettings + .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); @@ -653,6 +669,7 @@ namespace Tgstation.Server.Host.Controllers await databaseContextFactory.UseContext( async databaseContext => dbPull = await databaseContext.RevisionInformations + .AsQueryable() .Where(x => x.Instance.Id == Instance.Id && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha && x.ActiveTestMerges.Count <= model.NewTestMerges.Count diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 820f043ac3..6f0a1d1c0c 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -192,7 +192,10 @@ namespace Tgstation.Server.Host.Controllers var originalUser = passwordEditOnly ? AuthenticationContext.User - : await DatabaseContext.Users.Where(x => x.Id == model.Id) + : await DatabaseContext + .Users + .AsQueryable() + .Where(x => x.Id == model.Id) .Include(x => x.CreatedBy) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); @@ -301,6 +304,7 @@ namespace Tgstation.Server.Host.Controllers return Forbid(); var user = await DatabaseContext.Users + .AsQueryable() .Where(x => x.Id == id) .Include(x => x.CreatedBy) .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 7117a95628..c7ed253c9c 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -202,7 +202,7 @@ namespace Tgstation.Server.Host.Core // CORS conditionally enabled later services.AddCors(); - void AddTypedContext() where TContext : DatabaseContext + void AddTypedContext() where TContext : DatabaseContext { services.AddDbContext(builder => { @@ -226,6 +226,9 @@ namespace Tgstation.Server.Host.Core case DatabaseType.Sqlite: AddTypedContext(); break; + case DatabaseType.PostgresSql: + AddTypedContext(); + break; default: throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType)); } @@ -354,6 +357,8 @@ namespace Tgstation.Server.Host.Core // Final point where we wrap exceptions in a 500 (ErrorMessage) response applicationBuilder.UseServerErrorHandling(); + applicationBuilder.UseRequestCounting(); + // 503 requests made while the application is starting applicationBuilder.UseAsyncInitialization(async (cancellationToken) => { diff --git a/src/Tgstation.Server.Host/Core/ServerPortProivder.cs b/src/Tgstation.Server.Host/Core/ServerPortProivder.cs index a71dc72a29..04c009949d 100644 --- a/src/Tgstation.Server.Host/Core/ServerPortProivder.cs +++ b/src/Tgstation.Server.Host/Core/ServerPortProivder.cs @@ -1,6 +1,9 @@ using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System; using System.Linq; +using Tgstation.Server.Host.Configuration; namespace Tgstation.Server.Host.Core { @@ -8,14 +11,25 @@ namespace Tgstation.Server.Host.Core sealed class ServerPortProivder : IServerPortProvider { /// - public ushort HttpApiPort { get; } + public ushort HttpApiPort => generalConfiguration.ApiPort; + + /// + /// The for the . + /// + readonly GeneralConfiguration generalConfiguration; /// /// Initializes a new instance of the . /// + /// The containing the value of . /// The to use. - public ServerPortProivder(IConfiguration configuration) + /// The to use. + public ServerPortProivder( + IOptions generalConfigurationOptions, + IConfiguration configuration, + ILogger logger) { + generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); if (configuration == null) throw new ArgumentNullException(nameof(configuration)); @@ -26,8 +40,13 @@ namespace Tgstation.Server.Host.Core .GetSection("Url") .Value; - if (httpEndpoint == null) - throw new InvalidOperationException("Missing required configuration option Kestrel:EndPoints:Http:Url!"); + if (generalConfiguration.ApiPort == default && httpEndpoint == null) + throw new InvalidOperationException("Missing required configuration option General:ApiPort!"); + + if (generalConfiguration.ApiPort != default) + return; + + logger.LogWarning("The \"Kestrel\" configuration section is deprecated! Please set your API port using the \"General:ApiPort\" configuration option!"); var splits = httpEndpoint.Split(":", StringSplitOptions.RemoveEmptyEntries); var portString = splits.Last(); @@ -36,7 +55,7 @@ namespace Tgstation.Server.Host.Core if (!UInt16.TryParse(portString, out var result)) throw new InvalidOperationException($"Failed to parse HTTP EndPoint port: {httpEndpoint}"); - HttpApiPort = result; + generalConfiguration.ApiPort = result; } } } diff --git a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs index cadd4c3e5f..fa4c191023 100644 --- a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs +++ b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs @@ -133,6 +133,14 @@ namespace Tgstation.Server.Host.Core swaggerGenOptions.DocumentFilter(); swaggerGenOptions.SchemaFilter(); + swaggerGenOptions.CustomSchemaIds(type => + { + if (type == typeof(Api.Models.Internal.User)) + return "ShallowUser"; + + return type.Name; + }); + swaggerGenOptions.AddSecurityDefinition(PasswordSecuritySchemeId, new OpenApiSecurityScheme { In = ParameterLocation.Header, diff --git a/src/Tgstation.Server.Host/Database/DatabaseCollection.cs b/src/Tgstation.Server.Host/Database/DatabaseCollection.cs index 16652b38f3..e2ebf973e8 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseCollection.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseCollection.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading; -using System.Threading.Tasks; namespace Tgstation.Server.Host.Database { @@ -48,9 +47,7 @@ namespace Tgstation.Server.Host.Database public void Attach(TModel model) => dbSet.Attach(model); /// - public Task ForEachAsync(Action action, CancellationToken cancellationToken) => dbSet - .AsAsyncEnumerable() - .ForEachAsync(action, cancellationToken); + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => dbSet.AsAsyncEnumerable().GetAsyncEnumerator(); /// public IEnumerator GetEnumerator() => dbSet.AsQueryable().GetEnumerator(); @@ -61,9 +58,6 @@ namespace Tgstation.Server.Host.Database /// public void RemoveRange(IEnumerable models) => dbSet.RemoveRange(models); - /// - public Task> ToListAsync(CancellationToken cancellationToken) => dbSet.AsQueryable().ToListAsync(cancellationToken); - /// IEnumerator IEnumerable.GetEnumerator() => dbSet.AsQueryable().GetEnumerator(); } diff --git a/src/Tgstation.Server.Host/Database/DatabaseConnectionFactory.cs b/src/Tgstation.Server.Host/Database/DatabaseConnectionFactory.cs index bc0f3f6963..037418fbdc 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseConnectionFactory.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseConnectionFactory.cs @@ -3,6 +3,7 @@ using System.Data.Common; using System.Data.SqlClient; using Microsoft.Data.Sqlite; using MySql.Data.MySqlClient; +using Npgsql; using Tgstation.Server.Host.Configuration; namespace Tgstation.Server.Host.Database @@ -34,6 +35,11 @@ namespace Tgstation.Server.Host.Database { ConnectionString = connectionString }; + case DatabaseType.PostgresSql: + return new NpgsqlConnection + { + ConnectionString = connectionString + }; default: throw new ArgumentOutOfRangeException(nameof(databaseType), databaseType, "Invalid DatabaseType!"); } diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs index fd86c5ca35..10dddb039d 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs @@ -18,95 +18,94 @@ namespace Tgstation.Server.Host.Database /// /// Backend abstract implementation of /// - /// The child used to implement a backend. #pragma warning disable CA1506 // TODO: Decomplexify - abstract class DatabaseContext : DbContext, IDatabaseContext where TParentContext : DbContext + abstract class DatabaseContext : DbContext, IDatabaseContext { /// public DatabaseType DatabaseType => DatabaseConfiguration.DatabaseType; /// - /// The s in the . + /// The s in the . /// public DbSet Users { get; set; } /// - /// The s in the . + /// The s in the . /// public DbSet Instances { get; set; } /// - /// The s in the . + /// The s in the . /// public DbSet CompileJobs { get; set; } /// - /// The s in the . + /// The s in the . /// public DbSet RevisionInformations { get; set; } /// - /// The in the . + /// The in the . /// public DbSet DreamMakerSettings { get; set; } /// - /// The s in the . + /// The s in the . /// public DbSet ChatBots { get; set; } /// - /// The in the . + /// The in the . /// public DbSet DreamDaemonSettings { get; set; } /// - /// The in the . + /// The in the . /// public DbSet RepositorySettings { get; set; } /// - /// The s in the . + /// The s in the . /// public DbSet InstanceUsers { get; set; } /// - /// The s in the . + /// The s in the . /// public DbSet ChatChannels { get; set; } /// - /// The s in the . + /// The s in the . /// public DbSet Jobs { get; set; } /// - /// The s in the . + /// The s in the . /// public DbSet ReattachInformations { get; set; } /// - /// The s in the . + /// The s in the . /// public DbSet WatchdogReattachInformations { get; set; } /// - /// The s in the + /// The s in the /// public DbSet TestMerges { get; set; } /// - /// The s om the + /// The s om the /// public DbSet RevInfoTestMerges { get; set; } /// - /// The for the + /// The for the /// protected ILogger Logger { get; } /// - /// The for the + /// The for the /// protected DatabaseConfiguration DatabaseConfiguration { get; } @@ -150,7 +149,7 @@ namespace Tgstation.Server.Host.Database IDatabaseCollection IDatabaseContext.WatchdogReattachInformations => watchdogReattachInformationsCollection; /// - /// The for the + /// The for the /// readonly IDatabaseSeeder databaseSeeder; @@ -220,13 +219,13 @@ namespace Tgstation.Server.Host.Database readonly IDatabaseCollection watchdogReattachInformationsCollection; /// - /// Construct a + /// Construct a /// - /// The for the + /// The for the . /// The containing the value of /// The value of /// The value of - public DatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions) + public DatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions) { DatabaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions)); this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder)); @@ -354,9 +353,14 @@ namespace Tgstation.Server.Host.Database // Update this with new migrations as they are made string targetMigration = null; + + if (DatabaseType == DatabaseType.PostgresSql && version < new Version(4, 3, 0)) + throw new NotSupportedException("Cannot migrate below version 4.3.0 with PostgresSql!"); + if (version < new Version(4, 1, 0)) throw new NotSupportedException("Cannot migrate below version 4.1.0!"); - else if (version < new Version(4, 2, 0)) + + if (version < new Version(4, 2, 0)) targetMigration = DatabaseType == DatabaseType.Sqlite ? nameof(SLRebuild) : nameof(MSFixCascadingDelete); if (targetMigration == null) @@ -379,6 +383,9 @@ namespace Tgstation.Server.Host.Database case DatabaseType.Sqlite: migrationSubstitution = "SL{0}"; break; + case DatabaseType.PostgresSql: + migrationSubstitution = "PG{0}"; + break; default: throw new InvalidOperationException($"Invalid DatabaseType: {DatabaseType}"); } @@ -402,7 +409,7 @@ namespace Tgstation.Server.Host.Database } /// - /// Ensure the is correct for the . + /// Ensure the is correct for the . /// protected abstract void ValidateDatabaseType(); } diff --git a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs index ec15fb3e3c..e7173b0067 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs @@ -14,12 +14,12 @@ namespace Tgstation.Server.Host.Database sealed class DatabaseSeeder : IDatabaseSeeder { /// - /// The for the + /// The for the /// readonly ICryptographySuite cryptographySuite; /// - /// The for the . + /// The for the . /// readonly IPlatformIdentifier platformIdentifier; @@ -76,7 +76,11 @@ namespace Tgstation.Server.Host.Database if (platformIdentifier.IsWindows) { // normalize backslashes to forward slashes - var allInstances = await databaseContext.Instances.ToListAsync(cancellationToken).ConfigureAwait(false); + var allInstances = await databaseContext + .Instances + .AsQueryable() + .ToListAsync(cancellationToken) + .ConfigureAwait(false); foreach (var instance in allInstances) instance.Path = instance.Path.Replace('\\', '/'); } @@ -107,6 +111,7 @@ namespace Tgstation.Server.Host.Database { var admin = await databaseContext .Users + .AsQueryable() .Where(x => x.CanonicalName == User.CanonicalizeName(Api.Models.User.AdminName)) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs b/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs index 9699c86a16..98f71aeaaf 100644 --- a/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs +++ b/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs @@ -4,7 +4,7 @@ using Tgstation.Server.Host.Configuration; namespace Tgstation.Server.Host.Database.Design { /// - /// Contains helpers for creating design time s + /// Contains helpers for creating design time s /// static class DesignTimeDbContextFactoryHelpers { @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Database.Design /// The . /// The . /// The for the - public static IOptions GetDbContextOptions(DatabaseType databaseType, string connectionString) + public static IOptions GetDatabaseConfiguration(DatabaseType databaseType, string connectionString) { var dbConfig = new DatabaseConfiguration { diff --git a/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs index 61f2cfd8df..5b39d9e63d 100644 --- a/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs +++ b/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs @@ -20,7 +20,7 @@ namespace Tgstation.Server.Host.Database.Design using var loggerFactory = new LoggerFactory(); return new MySqlDatabaseContext( new DbContextOptions(), - DesignTimeDbContextFactoryHelpers.GetDbContextOptions( + DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration( DatabaseType.MariaDB, "Server=127.0.0.1;User Id=root;Password=fake;Database=TGS_Design"), new DatabaseSeeder( diff --git a/src/Tgstation.Server.Host/Database/Design/PostgresSqlDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/PostgresSqlDesignTimeDbContextFactory.cs new file mode 100644 index 0000000000..2af7d7c939 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Design/PostgresSqlDesignTimeDbContextFactory.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Logging; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Security; +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Database.Design +{ + /// + sealed class PostgresSqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory + { + /// + public PostgresSqlDatabaseContext CreateDbContext(string[] args) + { + using var loggerFactory = new LoggerFactory(); + return new PostgresSqlDatabaseContext( + new DbContextOptions(), + DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration( + DatabaseType.PostgresSql, + "Application Name=tgstation-server;Host=127.0.0.1;Password=qCkWimNgLfWwpr7TnUHs;Username=postgres;Database=TGS_Design"), + new DatabaseSeeder( + new CryptographySuite( + new PasswordHasher()), + new PlatformIdentifier()), + loggerFactory.CreateLogger()); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs index 68cc43ed2f..427d84ba8a 100644 --- a/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs +++ b/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs @@ -20,7 +20,7 @@ namespace Tgstation.Server.Host.Database.Design using var loggerFactory = new LoggerFactory(); return new SqlServerDatabaseContext( new DbContextOptions(), - DesignTimeDbContextFactoryHelpers.GetDbContextOptions( + DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration( DatabaseType.SqlServer, "Data Source=fake;Initial Catalog=TGS_Design;Integrated Security=True;Application Name=tgstation-server"), new DatabaseSeeder( diff --git a/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs index ba5690ec75..91de1284f2 100644 --- a/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs +++ b/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs @@ -20,7 +20,7 @@ namespace Tgstation.Server.Host.Database.Design using var loggerFactory = new LoggerFactory(); return new SqliteDatabaseContext( new DbContextOptions(), - DesignTimeDbContextFactoryHelpers.GetDbContextOptions( + DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration( DatabaseType.Sqlite, "Data Source=tgs_design.sqlite3;Mode=ReadWriteCreate"), new DatabaseSeeder( diff --git a/src/Tgstation.Server.Host/Database/IDatabaseCollection.cs b/src/Tgstation.Server.Host/Database/IDatabaseCollection.cs index 9c3318e35d..a9e1fe0e03 100644 --- a/src/Tgstation.Server.Host/Database/IDatabaseCollection.cs +++ b/src/Tgstation.Server.Host/Database/IDatabaseCollection.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Threading; -using System.Threading.Tasks; namespace Tgstation.Server.Host.Database { @@ -10,7 +7,7 @@ namespace Tgstation.Server.Host.Database /// Represents a database table. /// /// The type of model. - public interface IDatabaseCollection : IQueryable + public interface IDatabaseCollection : IQueryable, IAsyncEnumerable { /// /// An of s prioritizing in the working set. @@ -46,20 +43,5 @@ namespace Tgstation.Server.Host.Database /// /// An of s to remove. void RemoveRange(IEnumerable models); - - /// - /// Asyncronously run a given on the . - /// - /// The to run. - /// The for the operation. - /// A representing the running operation. - Task ForEachAsync(Action action, CancellationToken cancellationToken); - - /// - /// Retrieve all the s in the table. - /// - /// The for the operation. - /// A resulting in a of all in the table. - Task> ToListAsync(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.Designer.cs new file mode 100644 index 0000000000..36d63b62c3 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.Designer.cs @@ -0,0 +1,816 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(PostgresSqlDatabaseContext))] + [Migration("20200516111712_PGCreate")] + partial class PGCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ChannelLimit") + .HasColumnType("integer"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + 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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("IrcChannel") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Tag") + .HasColumnType("character varying(10000)") + .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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("text"); + + b.Property("DMApiMajorVersion") + .HasColumnType("integer"); + + b.Property("DMApiMinorVersion") + .HasColumnType("integer"); + + b.Property("DMApiPatchVersion") + .HasColumnType("integer"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("integer"); + + b.Property("Output") + .IsRequired() + .HasColumnType("text"); + + 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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("HeartbeatSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PrimaryPort") + .HasColumnType("integer"); + + b.Property("SecondaryPort") + .HasColumnType("integer"); + + b.Property("SecurityLevel") + .HasColumnType("integer"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ApiValidationPort") + .HasColumnType("integer"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("integer"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("character varying(10000)") + .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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AlphaId") + .HasColumnType("bigint"); + + b.Property("AlphaIsActive") + .HasColumnType("boolean"); + + 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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("integer"); + + b.Property("ConfigurationType") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ByondRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ChatBotRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstanceUserRights") + .HasColumnType("numeric(20,0)"); + + b.Property("RepositoryRights") + .HasColumnType("numeric(20,0)"); + + 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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CancelRight") + .HasColumnType("numeric(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("numeric(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("text"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("timestamp with time zone"); + + 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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("ProcessId") + .HasColumnType("integer"); + + b.Property("RebootState") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccessToken") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + 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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("PullRequestRevision") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + 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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdministrationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("InstanceManagerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("LastPasswordUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("SystemIdentifier") + .HasColumnType("text"); + + 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.ClientNoAction) + .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/20200516111712_PGCreate.cs b/src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.cs new file mode 100644 index 0000000000..af77ee5d60 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.cs @@ -0,0 +1,638 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + /// Create initial schema for PostgreSQL. + /// + public partial class PGCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.CreateTable( + name: "Instances", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(maxLength: 10000, nullable: false), + Path = table.Column(nullable: false), + Online = table.Column(nullable: false), + ConfigurationType = table.Column(nullable: false), + AutoUpdateInterval = table.Column(nullable: false), + ChatBotLimit = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Instances", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Enabled = table.Column(nullable: false), + CreatedAt = table.Column(nullable: false), + SystemIdentifier = table.Column(nullable: true), + Name = table.Column(maxLength: 10000, nullable: false), + AdministrationRights = table.Column(nullable: false), + InstanceManagerRights = table.Column(nullable: false), + PasswordHash = table.Column(nullable: true), + CreatedById = table.Column(nullable: true), + CanonicalName = table.Column(nullable: false), + LastPasswordUpdate = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + table.ForeignKey( + name: "FK_Users_Users_CreatedById", + column: x => x.CreatedById, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ChatBots", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(maxLength: 100, nullable: false), + Enabled = table.Column(nullable: true), + ReconnectionInterval = table.Column(nullable: false), + ChannelLimit = table.Column(nullable: false), + Provider = table.Column(nullable: false), + ConnectionString = table.Column(maxLength: 10000, nullable: false), + InstanceId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ChatBots", x => x.Id); + table.ForeignKey( + name: "FK_ChatBots_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "DreamDaemonSettings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AllowWebClient = table.Column(nullable: false), + SecurityLevel = table.Column(nullable: false), + PrimaryPort = table.Column(nullable: false), + SecondaryPort = table.Column(nullable: false), + StartupTimeout = table.Column(nullable: false), + HeartbeatSeconds = table.Column(nullable: false), + AutoStart = table.Column(nullable: false), + InstanceId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DreamDaemonSettings", x => x.Id); + table.ForeignKey( + name: "FK_DreamDaemonSettings_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "DreamMakerSettings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectName = table.Column(maxLength: 10000, nullable: true), + ApiValidationPort = table.Column(nullable: false), + ApiValidationSecurityLevel = table.Column(nullable: false), + InstanceId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DreamMakerSettings", x => x.Id); + table.ForeignKey( + name: "FK_DreamMakerSettings_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RepositorySettings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CommitterName = table.Column(maxLength: 10000, nullable: false), + CommitterEmail = table.Column(maxLength: 10000, nullable: false), + AccessUser = table.Column(maxLength: 10000, nullable: true), + AccessToken = table.Column(maxLength: 10000, nullable: true), + PushTestMergeCommits = table.Column(nullable: false), + ShowTestMergeCommitters = table.Column(nullable: false), + AutoUpdatesKeepTestMerges = table.Column(nullable: false), + AutoUpdatesSynchronize = table.Column(nullable: false), + PostTestMergeComment = table.Column(nullable: false), + InstanceId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RepositorySettings", x => x.Id); + table.ForeignKey( + name: "FK_RepositorySettings_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RevisionInformations", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CommitSha = table.Column(maxLength: 40, nullable: false), + OriginCommitSha = table.Column(maxLength: 40, nullable: false), + InstanceId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RevisionInformations", x => x.Id); + table.ForeignKey( + name: "FK_RevisionInformations_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "InstanceUsers", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(nullable: false), + InstanceUserRights = table.Column(nullable: false), + ByondRights = table.Column(nullable: false), + DreamDaemonRights = table.Column(nullable: false), + DreamMakerRights = table.Column(nullable: false), + RepositoryRights = table.Column(nullable: false), + ChatBotRights = table.Column(nullable: false), + ConfigurationRights = table.Column(nullable: false), + InstanceId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InstanceUsers", x => x.Id); + table.ForeignKey( + name: "FK_InstanceUsers_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_InstanceUsers_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Jobs", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Description = table.Column(nullable: false), + ErrorCode = table.Column(nullable: true), + ExceptionDetails = table.Column(nullable: true), + StartedAt = table.Column(nullable: false), + StoppedAt = table.Column(nullable: true), + Cancelled = table.Column(nullable: false), + CancelRightsType = table.Column(nullable: true), + CancelRight = table.Column(nullable: true), + StartedById = table.Column(nullable: false), + CancelledById = table.Column(nullable: true), + InstanceId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Jobs", x => x.Id); + table.ForeignKey( + name: "FK_Jobs_Users_CancelledById", + column: x => x.CancelledById, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Jobs_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Jobs_Users_StartedById", + column: x => x.StartedById, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ChatChannels", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + IrcChannel = table.Column(maxLength: 100, nullable: true), + DiscordChannelId = table.Column(nullable: true), + IsAdminChannel = table.Column(nullable: false), + IsWatchdogChannel = table.Column(nullable: false), + IsUpdatesChannel = table.Column(nullable: false), + Tag = table.Column(maxLength: 10000, nullable: true), + ChatSettingsId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ChatChannels", x => x.Id); + table.ForeignKey( + name: "FK_ChatChannels_ChatBots_ChatSettingsId", + column: x => x.ChatSettingsId, + principalTable: "ChatBots", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "TestMerges", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Number = table.Column(nullable: false), + PullRequestRevision = table.Column(maxLength: 40, nullable: false), + Comment = table.Column(maxLength: 10000, nullable: true), + TitleAtMerge = table.Column(nullable: false), + BodyAtMerge = table.Column(nullable: false), + Url = table.Column(nullable: false), + Author = table.Column(nullable: false), + MergedAt = table.Column(nullable: false), + MergedById = table.Column(nullable: false), + PrimaryRevisionInformationId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TestMerges", x => x.Id); + table.ForeignKey( + name: "FK_TestMerges_Users_MergedById", + column: x => x.MergedById, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_TestMerges_RevisionInformations_PrimaryRevisionInformationId", + column: x => x.PrimaryRevisionInformationId, + principalTable: "RevisionInformations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "CompileJobs", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + DmeName = table.Column(nullable: false), + Output = table.Column(nullable: false), + DirectoryName = table.Column(nullable: false), + MinimumSecurityLevel = table.Column(nullable: false), + JobId = table.Column(nullable: false), + RevisionInformationId = table.Column(nullable: false), + ByondVersion = table.Column(nullable: false), + DMApiMajorVersion = table.Column(nullable: true), + DMApiMinorVersion = table.Column(nullable: true), + DMApiPatchVersion = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CompileJobs", x => x.Id); + table.ForeignKey( + name: "FK_CompileJobs_Jobs_JobId", + column: x => x.JobId, + principalTable: "Jobs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_CompileJobs_RevisionInformations_RevisionInformationId", + column: x => x.RevisionInformationId, + principalTable: "RevisionInformations", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "RevInfoTestMerges", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + TestMergeId = table.Column(nullable: false), + RevisionInformationId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RevInfoTestMerges", x => x.Id); + table.ForeignKey( + name: "FK_RevInfoTestMerges_RevisionInformations_RevisionInformationId", + column: x => x.RevisionInformationId, + principalTable: "RevisionInformations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_RevInfoTestMerges_TestMerges_TestMergeId", + column: x => x.TestMergeId, + principalTable: "TestMerges", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ReattachInformations", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AccessIdentifier = table.Column(nullable: false), + ProcessId = table.Column(nullable: false), + IsPrimary = table.Column(nullable: false), + Port = table.Column(nullable: false), + RebootState = table.Column(nullable: false), + LaunchSecurityLevel = table.Column(nullable: false), + CompileJobId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ReattachInformations", x => x.Id); + table.ForeignKey( + name: "FK_ReattachInformations_CompileJobs_CompileJobId", + column: x => x.CompileJobId, + principalTable: "CompileJobs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "WatchdogReattachInformations", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AlphaIsActive = table.Column(nullable: false), + InstanceId = table.Column(nullable: false), + AlphaId = table.Column(nullable: true), + BravoId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_WatchdogReattachInformations", x => x.Id); + table.ForeignKey( + name: "FK_WatchdogReattachInformations_ReattachInformations_AlphaId", + column: x => x.AlphaId, + principalTable: "ReattachInformations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_WatchdogReattachInformations_ReattachInformations_BravoId", + column: x => x.BravoId, + principalTable: "ReattachInformations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_WatchdogReattachInformations_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ChatBots_InstanceId_Name", + table: "ChatBots", + columns: new[] { "InstanceId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ChatChannels_ChatSettingsId_DiscordChannelId", + table: "ChatChannels", + columns: new[] { "ChatSettingsId", "DiscordChannelId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ChatChannels_ChatSettingsId_IrcChannel", + table: "ChatChannels", + columns: new[] { "ChatSettingsId", "IrcChannel" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CompileJobs_DirectoryName", + table: "CompileJobs", + column: "DirectoryName"); + + migrationBuilder.CreateIndex( + name: "IX_CompileJobs_JobId", + table: "CompileJobs", + column: "JobId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CompileJobs_RevisionInformationId", + table: "CompileJobs", + column: "RevisionInformationId"); + + migrationBuilder.CreateIndex( + name: "IX_DreamDaemonSettings_InstanceId", + table: "DreamDaemonSettings", + column: "InstanceId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_DreamMakerSettings_InstanceId", + table: "DreamMakerSettings", + column: "InstanceId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Instances_Path", + table: "Instances", + column: "Path", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_InstanceUsers_InstanceId", + table: "InstanceUsers", + column: "InstanceId"); + + migrationBuilder.CreateIndex( + name: "IX_InstanceUsers_UserId_InstanceId", + table: "InstanceUsers", + columns: new[] { "UserId", "InstanceId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Jobs_CancelledById", + table: "Jobs", + column: "CancelledById"); + + migrationBuilder.CreateIndex( + name: "IX_Jobs_InstanceId", + table: "Jobs", + column: "InstanceId"); + + migrationBuilder.CreateIndex( + name: "IX_Jobs_StartedById", + table: "Jobs", + column: "StartedById"); + + migrationBuilder.CreateIndex( + name: "IX_ReattachInformations_CompileJobId", + table: "ReattachInformations", + column: "CompileJobId"); + + migrationBuilder.CreateIndex( + name: "IX_RepositorySettings_InstanceId", + table: "RepositorySettings", + column: "InstanceId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_RevInfoTestMerges_RevisionInformationId", + table: "RevInfoTestMerges", + column: "RevisionInformationId"); + + migrationBuilder.CreateIndex( + name: "IX_RevInfoTestMerges_TestMergeId", + table: "RevInfoTestMerges", + column: "TestMergeId"); + + migrationBuilder.CreateIndex( + name: "IX_RevisionInformations_InstanceId_CommitSha", + table: "RevisionInformations", + columns: new[] { "InstanceId", "CommitSha" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_TestMerges_MergedById", + table: "TestMerges", + column: "MergedById"); + + migrationBuilder.CreateIndex( + name: "IX_TestMerges_PrimaryRevisionInformationId", + table: "TestMerges", + column: "PrimaryRevisionInformationId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_CanonicalName", + table: "Users", + column: "CanonicalName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_CreatedById", + table: "Users", + column: "CreatedById"); + + migrationBuilder.CreateIndex( + name: "IX_Users_SystemIdentifier", + table: "Users", + column: "SystemIdentifier", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_WatchdogReattachInformations_AlphaId", + table: "WatchdogReattachInformations", + column: "AlphaId"); + + migrationBuilder.CreateIndex( + name: "IX_WatchdogReattachInformations_BravoId", + table: "WatchdogReattachInformations", + column: "BravoId"); + + migrationBuilder.CreateIndex( + name: "IX_WatchdogReattachInformations_InstanceId", + table: "WatchdogReattachInformations", + column: "InstanceId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder == null) + throw new ArgumentNullException(nameof(migrationBuilder)); + + migrationBuilder.DropTable( + name: "ChatChannels"); + + migrationBuilder.DropTable( + name: "DreamDaemonSettings"); + + migrationBuilder.DropTable( + name: "DreamMakerSettings"); + + migrationBuilder.DropTable( + name: "InstanceUsers"); + + migrationBuilder.DropTable( + name: "RepositorySettings"); + + migrationBuilder.DropTable( + name: "RevInfoTestMerges"); + + migrationBuilder.DropTable( + name: "WatchdogReattachInformations"); + + migrationBuilder.DropTable( + name: "ChatBots"); + + migrationBuilder.DropTable( + name: "TestMerges"); + + migrationBuilder.DropTable( + name: "ReattachInformations"); + + migrationBuilder.DropTable( + name: "CompileJobs"); + + migrationBuilder.DropTable( + name: "Jobs"); + + migrationBuilder.DropTable( + name: "RevisionInformations"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Instances"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs new file mode 100644 index 0000000000..fd44242293 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs @@ -0,0 +1,814 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Tgstation.Server.Host.Database; + +namespace Tgstation.Server.Host.Migrations +{ + [DbContext(typeof(PostgresSqlDatabaseContext))] + partial class PostgresSqlDatabaseContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ChannelLimit") + .HasColumnType("integer"); + + b.Property("ConnectionString") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + 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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("IrcChannel") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Tag") + .HasColumnType("character varying(10000)") + .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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("text"); + + b.Property("DMApiMajorVersion") + .HasColumnType("integer"); + + b.Property("DMApiMinorVersion") + .HasColumnType("integer"); + + b.Property("DMApiPatchVersion") + .HasColumnType("integer"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("integer"); + + b.Property("Output") + .IsRequired() + .HasColumnType("text"); + + 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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("HeartbeatSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PrimaryPort") + .HasColumnType("integer"); + + b.Property("SecondaryPort") + .HasColumnType("integer"); + + b.Property("SecurityLevel") + .HasColumnType("integer"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ApiValidationPort") + .HasColumnType("integer"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("integer"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasColumnType("character varying(10000)") + .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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AlphaId") + .HasColumnType("bigint"); + + b.Property("AlphaIsActive") + .HasColumnType("boolean"); + + 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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("integer"); + + b.Property("ConfigurationType") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("Online") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ByondRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ChatBotRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstanceUserRights") + .HasColumnType("numeric(20,0)"); + + b.Property("RepositoryRights") + .HasColumnType("numeric(20,0)"); + + 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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CancelRight") + .HasColumnType("numeric(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("numeric(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("text"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("timestamp with time zone"); + + 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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("ProcessId") + .HasColumnType("integer"); + + b.Property("RebootState") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccessToken") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AccessUser") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CommitterEmail") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("CommitterName") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + 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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasColumnType("character varying(40)") + .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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("MergedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("PullRequestRevision") + .IsRequired() + .HasColumnType("character varying(40)") + .HasMaxLength(40); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + 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") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdministrationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("CanonicalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("InstanceManagerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("LastPasswordUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("character varying(10000)") + .HasMaxLength(10000); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("SystemIdentifier") + .HasColumnType("text"); + + 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.ClientNoAction) + .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/MySqlDatabaseContext.cs b/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs index 009ab4463b..c18d4e723e 100644 --- a/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs @@ -9,17 +9,17 @@ using Tgstation.Server.Host.Configuration; namespace Tgstation.Server.Host.Database { /// - /// for MySQL + /// for MySQL /// - sealed class MySqlDatabaseContext : DatabaseContext + sealed class MySqlDatabaseContext : DatabaseContext { /// /// Construct a /// - /// The for the - /// The of for the - /// The for the - /// The for the + /// The for the + /// The of for the + /// The for the + /// The for the public MySqlDatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger) { } diff --git a/src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs b/src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs new file mode 100644 index 0000000000..111f3918ed --- /dev/null +++ b/src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Diagnostics; +using Tgstation.Server.Host.Configuration; + +namespace Tgstation.Server.Host.Database +{ + /// + /// for PostgresSQL. + /// + sealed class PostgresSqlDatabaseContext : DatabaseContext + { + /// + /// Construct a + /// + /// The for the + /// The of for the + /// The for the + /// The for the + public PostgresSqlDatabaseContext( + DbContextOptions dbContextOptions, + IOptions databaseConfiguration, + IDatabaseSeeder databaseSeeder, + ILogger logger) + : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger) + { } + + /// + protected override void OnConfiguring(DbContextOptionsBuilder options) + { + base.OnConfiguring(options); + options.UseNpgsql(DatabaseConfiguration.ConnectionString, x => x.EnableRetryOnFailure()); + } + + /// + 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/Database/SqlServerDatabaseContext.cs b/src/Tgstation.Server.Host/Database/SqlServerDatabaseContext.cs index 77826e7dbe..42c6aedc4e 100644 --- a/src/Tgstation.Server.Host/Database/SqlServerDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/SqlServerDatabaseContext.cs @@ -7,17 +7,17 @@ using Tgstation.Server.Host.Configuration; namespace Tgstation.Server.Host.Database { /// - /// for Sqlserver + /// for Sqlserver /// - sealed class SqlServerDatabaseContext : DatabaseContext + sealed class SqlServerDatabaseContext : DatabaseContext { /// /// Construct a /// - /// The for the - /// The of for the - /// The for the - /// The for the + /// The for the + /// The of for the + /// The for the + /// The for the public SqlServerDatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger) { } @@ -25,7 +25,7 @@ namespace Tgstation.Server.Host.Database protected override void OnConfiguring(DbContextOptionsBuilder options) { base.OnConfiguring(options); - options.UseSqlServer(DatabaseConfiguration.ConnectionString); + options.UseSqlServer(DatabaseConfiguration.ConnectionString, x => x.EnableRetryOnFailure()); } /// diff --git a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs index 2e71317a65..4d9fecf844 100644 --- a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs @@ -9,17 +9,17 @@ using Tgstation.Server.Host.Configuration; namespace Tgstation.Server.Host.Database { /// - /// for MySQL + /// for MySQL /// - sealed class SqliteDatabaseContext : DatabaseContext + sealed class SqliteDatabaseContext : DatabaseContext { /// /// Construct a /// - /// The for the - /// The of for the - /// The for the - /// The for the + /// The for the + /// The of for the + /// The for the + /// The for the public SqliteDatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger) { } diff --git a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs index f155aff190..2fe00098db 100644 --- a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs @@ -107,5 +107,32 @@ namespace Tgstation.Server.Host.Extensions } }); } + + /// + /// Add middleware for logging the request number. + /// + /// The to configure. + public static void UseRequestCounting(this IApplicationBuilder applicationBuilder) + { + if (applicationBuilder == null) + throw new ArgumentNullException(nameof(applicationBuilder)); + + ulong requestCounter = 0; + + applicationBuilder.Use(async (context, next) => + { + var logger = GetLogger(context); + var requestNumber = ++requestCounter; + logger.LogTrace("Starting request #{0}...", requestNumber); + try + { + await next().ConfigureAwait(false); + } + finally + { + logger.LogTrace("Finished request #{0}", requestNumber); + } + }); + } } } diff --git a/src/Tgstation.Server.Host/Extensions/DatabaseContextExtensions.cs b/src/Tgstation.Server.Host/Extensions/DatabaseContextExtensions.cs index 1fdae2abba..2aaf72f6aa 100644 --- a/src/Tgstation.Server.Host/Extensions/DatabaseContextExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/DatabaseContextExtensions.cs @@ -30,6 +30,7 @@ namespace Tgstation.Server.Host.Extensions return databaseContext .CompileJobs + .AsQueryable() .Where(x => x.Job.Instance.Id == instance.Id) .OrderByDescending(x => x.Job.StoppedAt) .FirstOrDefaultAsync(cancellationToken); diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobManager.cs index c071c946ed..5f01fcbf5e 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobManager.cs @@ -186,7 +186,13 @@ namespace Tgstation.Server.Host.Jobs await databaseContextFactory.UseContext(async databaseContext => { // mark all jobs as cancelled - var badJobs = await databaseContext.Jobs.Where(y => !y.StoppedAt.HasValue).Select(y => y.Id).ToListAsync(cancellationToken).ConfigureAwait(false); + var badJobs = await databaseContext + .Jobs + .AsQueryable() + .Where(y => !y.StoppedAt.HasValue) + .Select(y => y.Id) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); if (badJobs.Count > 0) { logger.LogTrace("Cleaning {0} unfinished jobs...", badJobs.Count); diff --git a/src/Tgstation.Server.Host/Models/RevisionInformation.cs b/src/Tgstation.Server.Host/Models/RevisionInformation.cs index 17c676990b..02dd5edef3 100644 --- a/src/Tgstation.Server.Host/Models/RevisionInformation.cs +++ b/src/Tgstation.Server.Host/Models/RevisionInformation.cs @@ -48,9 +48,9 @@ namespace Tgstation.Server.Host.Models OriginCommitSha = OriginCommitSha, PrimaryTestMerge = PrimaryTestMerge?.ToApi(), ActiveTestMerges = ActiveTestMerges.Select(x => x.TestMerge.ToApi()).ToList(), - CompileJobs = CompileJobs.Select(x => new Api.Models.CompileJob + CompileJobs = CompileJobs.Select(x => new Api.Models.EntityId { - Id = x.Id // anti recursion measure + Id = x.Id }).ToList() }; } diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs index 070642c105..aa019be91f 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs @@ -55,7 +55,10 @@ namespace Tgstation.Server.Host.Security if (CurrentAuthenticationContext != null) throw new InvalidOperationException("Authentication context has already been loaded"); - var user = await databaseContext.Users.Where(x => x.Id == userId) + var user = await databaseContext + .Users + .AsQueryable() + .Where(x => x.Id == userId) .Include(x => x.CreatedBy) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); @@ -85,6 +88,7 @@ namespace Tgstation.Server.Host.Security if (instanceId.HasValue) { instanceUser = await databaseContext.InstanceUsers + .AsQueryable() .Where(x => x.UserId == userId && x.InstanceId == instanceId && x.Instance.Online.Value) .Include(x => x.Instance) .FirstOrDefaultAsync(cancellationToken) diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index 5a8fd3ee84..45b824bd39 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -299,7 +299,10 @@ namespace Tgstation.Server.Host { await eventsTask.ConfigureAwait(false); } - catch (OperationCanceledException) { } + catch (OperationCanceledException) + { + logger.LogError("Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!"); + } catch (Exception e) { logger.LogError("Restart handlers error! Exception: {0}", e); diff --git a/src/Tgstation.Server.Host/ServerFactory.cs b/src/Tgstation.Server.Host/ServerFactory.cs index 32289426bc..6bb31a8948 100644 --- a/src/Tgstation.Server.Host/ServerFactory.cs +++ b/src/Tgstation.Server.Host/ServerFactory.cs @@ -1,10 +1,12 @@ using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Setup; @@ -58,8 +60,17 @@ namespace Tgstation.Server.Host } var hostBuilder = CreateDefaultBuilder() - .ConfigureWebHostDefaults(webHostBuilder => + .ConfigureWebHost(webHostBuilder => webHostBuilder + .UseKestrel(kestrelOptions => + { + var serverPortProvider = kestrelOptions.ApplicationServices.GetRequiredService(); + kestrelOptions.ListenAnyIP( + serverPortProvider.HttpApiPort, + listenOptions => listenOptions.Protocols = HttpProtocols.Http1AndHttp2); + }) + .UseIIS() + .UseIISIntegration() .UseApplication(postSetupServices) .SuppressStatusMessages(true) .UseShutdownTimeout(TimeSpan.FromMinutes(1))); diff --git a/src/Tgstation.Server.Host/Setup/SetupWizard.cs b/src/Tgstation.Server.Host/Setup/SetupWizard.cs index 779f20493d..9e54ac8f00 100644 --- a/src/Tgstation.Server.Host/Setup/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Setup/SetupWizard.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MySql.Data.MySqlClient; using Newtonsoft.Json; +using Npgsql; using System; using System.Collections.Generic; using System.Data.Common; @@ -140,7 +141,11 @@ namespace Tgstation.Server.Host.Setup do { - await console.WriteAsync("API Port (leave blank for default): ", false, cancellationToken).ConfigureAwait(false); + await console.WriteAsync( + $"API Port (leave blank for default of {GeneralConfiguration.DefaultApiPort}): ", + false, + cancellationToken) + .ConfigureAwait(false); var portString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false); if (String.IsNullOrWhiteSpace(portString)) return null; @@ -275,34 +280,38 @@ namespace Tgstation.Server.Host.Setup /// /// Prompt the user for the . /// + /// If this is the user's first time here. /// The for the operation. /// A resulting in the input . - async Task PromptDatabaseType(CancellationToken cancellationToken) + async Task PromptDatabaseType(bool firstTime, CancellationToken cancellationToken) { - await console.WriteAsync(String.Empty, true, cancellationToken).ConfigureAwait(false); - await console.WriteAsync( - "NOTE: It is HIGHLY reccommended that TGS runs on a complete relational database, specfically *NOT* Sqlite.", - true, - cancellationToken) - .ConfigureAwait(false); - await console.WriteAsync( - "Sqlite, by nature cannot perform several DDL operations. Because of this future compatiblility cannot be guaranteed.", - true, - cancellationToken) - .ConfigureAwait(false); - await console.WriteAsync( - "This means that you may not be able to update to the next minor version of TGS4 without a clean re-installation!", - true, - cancellationToken) - .ConfigureAwait(false); - await console.WriteAsync( - "Please consider taking the time to set up a relational database if this is meant to be a long-standing server.", - true, - cancellationToken) - .ConfigureAwait(false); - await console.WriteAsync(String.Empty, true, cancellationToken).ConfigureAwait(false); + if (firstTime) + { + await console.WriteAsync(String.Empty, true, cancellationToken).ConfigureAwait(false); + await console.WriteAsync( + "NOTE: It is HIGHLY reccommended that TGS runs on a complete relational database, specfically *NOT* Sqlite.", + true, + cancellationToken) + .ConfigureAwait(false); + await console.WriteAsync( + "Sqlite, by nature cannot perform several DDL operations. Because of this future compatiblility cannot be guaranteed.", + true, + cancellationToken) + .ConfigureAwait(false); + await console.WriteAsync( + "This means that you may not be able to update to the next minor version of TGS4 without a clean re-installation!", + true, + cancellationToken) + .ConfigureAwait(false); + await console.WriteAsync( + "Please consider taking the time to set up a relational database if this is meant to be a long-standing server.", + true, + cancellationToken) + .ConfigureAwait(false); + await console.WriteAsync(String.Empty, true, cancellationToken).ConfigureAwait(false); - await asyncDelayer.Delay(TimeSpan.FromSeconds(3), cancellationToken).ConfigureAwait(false); + await asyncDelayer.Delay(TimeSpan.FromSeconds(3), cancellationToken).ConfigureAwait(false); + } await console.WriteAsync("What SQL database type will you be using?", true, cancellationToken).ConfigureAwait(false); do @@ -310,10 +319,13 @@ 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}, or {3}: ", DatabaseType.MariaDB, DatabaseType.MySql, +#pragma warning disable SA1515 // Single-line comment should be preceded by blank line + // DatabaseType.PostgresSql, DatabaseType.SqlServer, +#pragma warning restore SA1515 // Single-line comment should be preceded by blank line DatabaseType.Sqlite), false, cancellationToken) @@ -332,19 +344,22 @@ namespace Tgstation.Server.Host.Setup /// /// The for the operation /// A resulting in the new + #pragma warning disable CA1502 // TODO: Decomplexify async Task ConfigureDatabase(CancellationToken cancellationToken) { + bool firstTime = true; do { await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); var databaseConfiguration = new DatabaseConfiguration { - DatabaseType = await PromptDatabaseType(cancellationToken).ConfigureAwait(false) + DatabaseType = await PromptDatabaseType(firstTime, cancellationToken).ConfigureAwait(false) }; + firstTime = false; string serverAddress = null; - uint? mySQLServerPort = null; + ushort? serverPort = null; bool isSqliteDB = databaseConfiguration.DatabaseType == DatabaseType.Sqlite; if (!isSqliteDB) @@ -353,15 +368,17 @@ namespace Tgstation.Server.Host.Setup await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); await console.WriteAsync("Enter the server's address and port [: or ] (blank for local): ", false, cancellationToken).ConfigureAwait(false); serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false); - if (!String.IsNullOrWhiteSpace(serverAddress) && databaseConfiguration.DatabaseType == DatabaseType.SqlServer) + if (String.IsNullOrWhiteSpace(serverAddress)) + serverAddress = null; + else if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer) { var match = Regex.Match(serverAddress, @"^(?.+):(?.+)$"); if (match.Success) { serverAddress = match.Groups["server"].Value; var portString = match.Groups["port"].Value; - if (uint.TryParse(portString, out uint port)) - mySQLServerPort = port; + if (UInt16.TryParse(portString, out var port)) + serverPort = port; else { await console.WriteAsync($"Failed to parse port \"{portString}\", please try again.", true, cancellationToken).ConfigureAwait(false); @@ -434,53 +451,84 @@ namespace Tgstation.Server.Host.Setup connectionString, databaseConfiguration.DatabaseType); - if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer) + switch (databaseConfiguration.DatabaseType) { - var csb = new SqlConnectionStringBuilder - { - ApplicationName = assemblyInformationProvider.VersionPrefix, - DataSource = serverAddress ?? "(local)" - }; + case DatabaseType.SqlServer: + { + var csb = new SqlConnectionStringBuilder + { + ApplicationName = assemblyInformationProvider.VersionPrefix, + DataSource = serverAddress ?? "(local)" + }; - if (useWinAuth) - csb.IntegratedSecurity = true; - else - { - csb.UserID = username; - csb.Password = password; - } + if (useWinAuth) + csb.IntegratedSecurity = true; + else + { + csb.UserID = username; + csb.Password = password; + } - CreateTestConnection(csb.ConnectionString); - csb.InitialCatalog = databaseName; - databaseConfiguration.ConnectionString = csb.ConnectionString; - } - else if(databaseConfiguration.DatabaseType == DatabaseType.Sqlite) - { - var csb = new SqliteConnectionStringBuilder - { - DataSource = databaseName, - Mode = dbExists ? SqliteOpenMode.ReadOnly : SqliteOpenMode.ReadWriteCreate - }; + CreateTestConnection(csb.ConnectionString); + csb.InitialCatalog = databaseName; + databaseConfiguration.ConnectionString = csb.ConnectionString; + } - CreateTestConnection(csb.ConnectionString); - databaseConfiguration.ConnectionString = csb.ConnectionString; - } - else - { - // MySQL/MariaDB - var csb = new MySqlConnectionStringBuilder - { - Server = serverAddress ?? "127.0.0.1", - UserID = username, - Password = password - }; + break; + case DatabaseType.MariaDB: + case DatabaseType.MySql: + { + // MySQL/MariaDB + var csb = new MySqlConnectionStringBuilder + { + Server = serverAddress ?? "127.0.0.1", + UserID = username, + Password = password + }; - if (mySQLServerPort.HasValue) - csb.Port = mySQLServerPort.Value; + if (serverPort.HasValue) + csb.Port = serverPort.Value; - CreateTestConnection(csb.ConnectionString); - csb.Database = databaseName; - databaseConfiguration.ConnectionString = csb.ConnectionString; + CreateTestConnection(csb.ConnectionString); + csb.Database = databaseName; + databaseConfiguration.ConnectionString = csb.ConnectionString; + } + + break; + case DatabaseType.Sqlite: + { + var csb = new SqliteConnectionStringBuilder + { + DataSource = databaseName, + Mode = dbExists ? SqliteOpenMode.ReadOnly : SqliteOpenMode.ReadWriteCreate + }; + + CreateTestConnection(csb.ConnectionString); + databaseConfiguration.ConnectionString = csb.ConnectionString; + } + + break; + case DatabaseType.PostgresSql: + { + var csb = new NpgsqlConnectionStringBuilder + { + ApplicationName = assemblyInformationProvider.VersionPrefix, + Host = serverAddress ?? "127.0.0.1", + Password = password, + Username = username + }; + + if (serverPort.HasValue) + csb.Port = serverPort.Value; + + CreateTestConnection(csb.ConnectionString); + csb.Database = databaseName; + databaseConfiguration.ConnectionString = csb.ConnectionString; + } + + break; + default: + throw new InvalidOperationException("Invalid DatabaseType!"); } try @@ -502,6 +550,7 @@ namespace Tgstation.Server.Host.Setup } while (true); } + #pragma warning restore CA1502 /// /// Prompts the user to create a @@ -687,6 +736,7 @@ namespace Tgstation.Server.Host.Setup { await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken).ConfigureAwait(false); + newGeneralConfiguration.ApiPort = hostingPort ?? GeneralConfiguration.DefaultApiPort; var map = new Dictionary() { { DatabaseConfiguration.Section, databaseConfiguration }, @@ -695,18 +745,6 @@ namespace Tgstation.Server.Host.Setup { ControlPanelConfiguration.Section, controlPanelConfiguration } }; - if (hostingPort.HasValue) - map.Add("Kestrel", new - { - EndPoints = new - { - Http = new - { - Url = String.Format(CultureInfo.InvariantCulture, "http://0.0.0.0:{0}", hostingPort) - } - } - }); - var json = JsonConvert.SerializeObject(map, Formatting.Indented); var configBytes = Encoding.UTF8.GetBytes(json); diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 4e7ce431c4..4382558c26 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -48,25 +48,26 @@ - - + + all runtime; build; native; contentfiles; analyzers - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -82,10 +83,10 @@ - + - + diff --git a/src/Tgstation.Server.Host/appsettings.json b/src/Tgstation.Server.Host/appsettings.json index 2457e64d56..55786ab050 100644 --- a/src/Tgstation.Server.Host/appsettings.json +++ b/src/Tgstation.Server.Host/appsettings.json @@ -1,10 +1,11 @@ { "General": { + "ApiPort": 5000, "MinimumPasswordLength": 15, "GitHubAccessToken": null, "SetupWizardMode": "AutoDetect", "ByondTopicTimeout": 5000, - "RestartTimeout": 10000, + "RestartTimeout": 60000, "UseExperimentalWatchdog": false, "UseBasicWatchdogOnWindows": false, "UserLimit": 100, @@ -17,13 +18,6 @@ "LogLevel": "Debug", "MicrosoftLogLevel": "Warning" }, - "Kestrel": { - "EndPoints": { - "Http": { - "Url": "http://0.0.0.0:5000" - } - } - }, "Logging": { "IncludeScopes": false, "Debug": { diff --git a/tests/DMAPI/BasicOperation/Test.dm b/tests/DMAPI/BasicOperation/Test.dm index 21ba9e6226..a603a099be 100644 --- a/tests/DMAPI/BasicOperation/Test.dm +++ b/tests/DMAPI/BasicOperation/Test.dm @@ -25,6 +25,10 @@ world.log << "You really shouldn't be able to read this" +/world/Export(url) + log << "Export: [url]" + return ..() + /world/Topic(T, Addr, Master, Keys) world.log << "Topic: [T]" . = HandleTopic(T) diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm index 631dcfccae..8930774de6 100644 --- a/tests/DMAPI/LongRunning/Test.dm +++ b/tests/DMAPI/LongRunning/Test.dm @@ -1,4 +1,8 @@ +/world + sleep_offline = FALSE + /world/New() + log << "Initial value of sleep_offline: [sleep_offline]" TgsNew(new /datum/tgs_event_handler/impl, TGS_SECURITY_ULTRASAFE) StartAsync() @@ -9,17 +13,16 @@ /proc/Run() sleep(60) world.TgsChatBroadcast("World Initialized") - world.TgsInitializationComplete() + // world.TgsInitializationComplete() /world/Topic(T, Addr, Master, Keys) - world.log << "Topic: [T]" + log << "Topic: [T]" . = HandleTopic(T) - world.log << "Response: [.]" + log << "Response: [.]" /world/proc/HandleTopic(T) TGS_TOPIC - world.sleep_offline = FALSE TgsChatBroadcast("Recieved non-tgs topic: [T]") var/list/data = params2list(T) @@ -40,9 +43,14 @@ world.TgsChatBroadcast("Recieved event: [json_encode(args)]") +/world/Export(url) + log << "Export: [url]" + return ..() + /proc/RebootAsync() set waitfor = FALSE - world.sleep_offline = FALSE world.TgsChatBroadcast("Rebooting after 3 seconds"); + world.log << "About to sleep. sleep_offline: [world.sleep_offline]" sleep(30) + world.log << "Done sleep, calling Reboot" world.Reboot() diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs index 8725718aa3..8cf32df3e6 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs @@ -4,6 +4,7 @@ using Moq; using System; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components.Chat.Providers.Tests { @@ -21,23 +22,23 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests [TestMethod] public void TestConstructionAndDisposal() { - Assert.ThrowsException(() => new DiscordProvider(null, null, 1)); + Assert.ThrowsException(() => new DiscordProvider(null, null, null, 1)); + var mockAss = new Mock(); + Assert.ThrowsException(() => new DiscordProvider(mockAss.Object, null, null, 1)); var mockLogger = new Mock>(); - Assert.ThrowsException(() => new DiscordProvider(mockLogger.Object, null, 1)); + Assert.ThrowsException(() => new DiscordProvider(mockAss.Object, mockLogger.Object, null, 1)); var mockToken = "asdf"; - Assert.ThrowsException(() => new DiscordProvider(mockLogger.Object, mockToken, 0)); - new DiscordProvider(mockLogger.Object, mockToken, 1).Dispose(); + Assert.ThrowsException(() => new DiscordProvider(mockAss.Object, mockLogger.Object, mockToken, 0)); + new DiscordProvider(mockAss.Object, mockLogger.Object, mockToken, 1).Dispose(); } [TestMethod] public async Task TestConnectWithFakeTokenFails() { var mockLogger = new Mock>(); - using (var provider = new DiscordProvider(mockLogger.Object, "asdf", 1)) - { - Assert.IsFalse(await provider.Connect(default).ConfigureAwait(false)); - Assert.IsFalse(provider.Connected); - } + using var provider = new DiscordProvider(Mock.Of(), mockLogger.Object, "asdf", 1); + Assert.IsFalse(await provider.Connect(default).ConfigureAwait(false)); + Assert.IsFalse(provider.Connected); } [Ignore("Broken due to dependency issues after first call to .Connect()")] @@ -49,37 +50,32 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests var mockLogger = new Mock>(); - using (var provider = new DiscordProvider(mockLogger.Object, testToken1, 1)) - { - Assert.IsFalse(provider.Connected); - await provider.Disconnect(default).ConfigureAwait(false); - Assert.IsFalse(provider.Connected); - Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false)); - Assert.IsTrue(provider.Connected); - Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false)); - Assert.IsTrue(provider.Connected); + using var provider = new DiscordProvider(Mock.Of(), mockLogger.Object, testToken1, 1); + Assert.IsFalse(provider.Connected); + await provider.Disconnect(default).ConfigureAwait(false); + Assert.IsFalse(provider.Connected); + Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false)); + Assert.IsTrue(provider.Connected); + Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false)); + Assert.IsTrue(provider.Connected); - await provider.Disconnect(default).ConfigureAwait(false); - Assert.IsFalse(provider.Connected); - await provider.Disconnect(default).ConfigureAwait(false); - Assert.IsFalse(provider.Connected); + await provider.Disconnect(default).ConfigureAwait(false); + Assert.IsFalse(provider.Connected); + await provider.Disconnect(default).ConfigureAwait(false); + Assert.IsFalse(provider.Connected); - //now try it with cancellationTokens - using (var cts = new CancellationTokenSource()) - { - cts.Cancel(); - var cancellationToken = cts.Token; - await Assert.ThrowsExceptionAsync(() => provider.Connect(cancellationToken)).ConfigureAwait(false); - Assert.IsFalse(provider.Connected); - Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false)); - Assert.IsTrue(provider.Connected); - await Assert.ThrowsExceptionAsync(() => provider.Disconnect(cancellationToken)).ConfigureAwait(false); - Assert.IsTrue(provider.Connected); - await provider.Disconnect(default).ConfigureAwait(false); - Assert.IsFalse(provider.Connected); - } - - } + //now try it with cancellationTokens + using var cts = new CancellationTokenSource(); + cts.Cancel(); + var cancellationToken = cts.Token; + await Assert.ThrowsExceptionAsync(() => provider.Connect(cancellationToken)).ConfigureAwait(false); + Assert.IsFalse(provider.Connected); + Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false)); + Assert.IsTrue(provider.Connected); + await Assert.ThrowsExceptionAsync(() => provider.Disconnect(cancellationToken)).ConfigureAwait(false); + Assert.IsTrue(provider.Connected); + await provider.Disconnect(default).ConfigureAwait(false); + Assert.IsFalse(provider.Connected); } } } diff --git a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs index 5d11ac34dc..0b49bbc5d6 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs @@ -48,14 +48,12 @@ namespace Tgstation.Server.Host.Components.Repository.Tests tempDir, default); - using (var repo = await TestRepoLoading(tempDir)) - { - var gitObject = repo.Lookup("f636418bf47d238d33b0e4a34f0072b23a8aad0e"); - Assert.IsNotNull(gitObject); - var commit = gitObject.Peel(); + using var repo = await TestRepoLoading(tempDir); + var gitObject = repo.Lookup("f636418bf47d238d33b0e4a34f0072b23a8aad0e"); + Assert.IsNotNull(gitObject); + var commit = gitObject.Peel(); - Assert.AreEqual("Update Test.md", commit.Message); - } + Assert.AreEqual("Update Test.md", commit.Message); } finally { diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestAsyncDelayer.cs b/tests/Tgstation.Server.Host.Tests/Core/TestAsyncDelayer.cs index c4927e3dee..8cfb81e0dc 100644 --- a/tests/Tgstation.Server.Host.Tests/Core/TestAsyncDelayer.cs +++ b/tests/Tgstation.Server.Host.Tests/Core/TestAsyncDelayer.cs @@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Core.Tests { var delayer = new AsyncDelayer(); var startDelay = delayer.Delay(TimeSpan.FromSeconds(1), default); - var checkDelay = Task.Delay(TimeSpan.FromSeconds(1) - TimeSpan.FromMilliseconds(10), default); + var checkDelay = Task.Delay(TimeSpan.FromSeconds(1) - TimeSpan.FromMilliseconds(100), default); await startDelay.ConfigureAwait(false); Assert.IsTrue(checkDelay.IsCompleted); } @@ -22,11 +22,9 @@ namespace Tgstation.Server.Host.Core.Tests public async Task TestCancel() { var delayer = new AsyncDelayer(); - using (var cts = new CancellationTokenSource()) - { - cts.Cancel(); - await Assert.ThrowsExceptionAsync(() => delayer.Delay(TimeSpan.FromSeconds(1), cts.Token)).ConfigureAwait(false); - } + using var cts = new CancellationTokenSource(); + cts.Cancel(); + await Assert.ThrowsExceptionAsync(() => delayer.Delay(TimeSpan.FromSeconds(1), cts.Token)).ConfigureAwait(false); } } } diff --git a/tests/Tgstation.Server.Tests/Instance/ChatTest.cs b/tests/Tgstation.Server.Tests/Instance/ChatTest.cs index cbfa660636..57b9470e23 100644 --- a/tests/Tgstation.Server.Tests/Instance/ChatTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/ChatTest.cs @@ -127,7 +127,7 @@ namespace Tgstation.Server.Tests.Instance new ChatChannel { IsAdminChannel = true, - IsUpdatesChannel = false, + IsUpdatesChannel = true, IsWatchdogChannel = true, Tag = "butt", DiscordChannelId = channelId @@ -140,7 +140,7 @@ namespace Tgstation.Server.Tests.Instance Assert.IsNotNull(updatedBot.Channels); Assert.AreEqual(1, updatedBot.Channels.Count); Assert.AreEqual(true, updatedBot.Channels.First().IsAdminChannel); - Assert.AreEqual(false, updatedBot.Channels.First().IsUpdatesChannel); + Assert.AreEqual(true, updatedBot.Channels.First().IsUpdatesChannel); Assert.AreEqual(true, updatedBot.Channels.First().IsWatchdogChannel); Assert.AreEqual("butt", updatedBot.Channels.First().Tag); Assert.AreEqual(channelId, updatedBot.Channels.First().DiscordChannelId); diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index f7a8ed3e55..9955bc1af4 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -29,6 +29,7 @@ namespace Tgstation.Server.Tests.Instance public async Task Run(CancellationToken cancellationToken) { + global::System.Console.WriteLine("TEST: START WATCHDOG TESTS"); // Increase startup timeout, disable heartbeats await instanceClient.DreamDaemon.Update(new DreamDaemon { @@ -46,17 +47,18 @@ namespace Tgstation.Server.Tests.Instance // await RunLongRunningTestThenUpdate(cancellationToken); // await RunLongRunningTestThenUpdateWithByondVersionSwitch(cancellationToken); - // Remove this deploy when the above tests are reenabled await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Trusted, cancellationToken); await RunHeartbeatTest(cancellationToken); await StartAndLeaveRunning(cancellationToken); + global::System.Console.WriteLine("TEST: END WATCHDOG TESTS"); } async Task RunBasicTest(CancellationToken cancellationToken) { + global::System.Console.WriteLine("TEST: WATCHDOG BASIC TEST"); var daemonStatus = await DeployTestDme("BasicOperation/basic_operation_test", DreamDaemonSecurity.Ultrasafe, cancellationToken); Assert.IsFalse(daemonStatus.Running.Value); @@ -84,6 +86,7 @@ namespace Tgstation.Server.Tests.Instance async Task RunHeartbeatTest(CancellationToken cancellationToken) { + global::System.Console.WriteLine("TEST: WATCHDOG HEARTBEAT TEST"); // enable heartbeats await instanceClient.DreamDaemon.Update(new DreamDaemon { @@ -144,6 +147,7 @@ namespace Tgstation.Server.Tests.Instance async Task RunLongRunningTestThenUpdate(CancellationToken cancellationToken) { + global::System.Console.WriteLine("TEST: WATCHDOG LONG RUNNING TEST"); const string DmeName = "LongRunning/long_running_test"; var daemonStatus = await DeployTestDme(DmeName, DreamDaemonSecurity.Trusted, cancellationToken); @@ -184,6 +188,7 @@ namespace Tgstation.Server.Tests.Instance async Task RunLongRunningTestThenUpdateWithByondVersionSwitch(CancellationToken cancellationToken) { + global::System.Console.WriteLine("TEST: WATCHDOG BYOND VERSION UPDATE TEST"); var versionToInstall = new Version(511, 1384, 0); var byondInstallJobTask = instanceClient.Byond.SetActiveVersion( new Api.Models.Byond @@ -232,6 +237,7 @@ namespace Tgstation.Server.Tests.Instance public async Task StartAndLeaveRunning(CancellationToken cancellationToken) { + global::System.Console.WriteLine("TEST: WATCHDOG ENDLESS TEST"); var dd = await instanceClient.DreamDaemon.Read(cancellationToken); if(dd.ActiveCompileJob == null) await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Trusted, cancellationToken); @@ -253,18 +259,16 @@ namespace Tgstation.Server.Tests.Instance try { + global::System.Console.WriteLine("TEST: Sending world reboot topic..."); var result = await bts.SendTopic(IPAddress.Loopback, "tgs_integration_test_special_tactics=1", 1337, cancellationToken); Assert.AreEqual("ack", result.StringData); - await Task.Delay(7000, cancellationToken); + await Task.Delay(10000, cancellationToken); } catch (OperationCanceledException) { throw; } - catch - { - } } async Task DeployTestDme(string dmeName, DreamDaemonSecurity deploymentSecurity, CancellationToken cancellationToken) diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 523b101a15..aeff0bfffa 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -118,7 +118,7 @@ namespace Tgstation.Server.Tests using var hardTimeoutCts = new CancellationTokenSource(); hardTimeoutCts.CancelAfter(new TimeSpan(0, 9, 45)); var hardTimeoutCancellationToken = hardTimeoutCts.Token; - hardTimeoutCancellationToken.Register(() => Console.WriteLine("TEST TIMEOUT HARD!")); + hardTimeoutCancellationToken.Register(() => Console.WriteLine($"[{DateTimeOffset.Now}] TEST TIMEOUT HARD!")); using var softTimeoutCts = CancellationTokenSource.CreateLinkedTokenSource(hardTimeoutCancellationToken); softTimeoutCts.CancelAfter(new TimeSpan(0, 9, 15)); @@ -127,7 +127,7 @@ namespace Tgstation.Server.Tests softTimeoutCancellationToken.Register(() => { if (!tooLateForSoftTimeout) - Console.WriteLine("TEST TIMEOUT SOFT!"); + Console.WriteLine($"[{DateTimeOffset.Now}] TEST TIMEOUT SOFT!"); }); using var serverCts = CancellationTokenSource.CreateLinkedTokenSource(softTimeoutCancellationToken); @@ -297,7 +297,7 @@ namespace Tgstation.Server.Tests } catch (Exception ex) { - Console.WriteLine($"TEST ERROR: {ex.GetType()} in flight!"); + Console.WriteLine($"[{DateTimeOffset.Now}] TEST ERROR: {ex}"); throw; } finally diff --git a/tests/Tgstation.Server.Tests/TestingServer.cs b/tests/Tgstation.Server.Tests/TestingServer.cs index 7b7df505cc..c420bebd7f 100644 --- a/tests/Tgstation.Server.Tests/TestingServer.cs +++ b/tests/Tgstation.Server.Tests/TestingServer.cs @@ -72,7 +72,7 @@ namespace Tgstation.Server.Tests var args = new List() { String.Format(CultureInfo.InvariantCulture, "Database:DropDatabase={0}", true), - String.Format(CultureInfo.InvariantCulture, "Kestrel:EndPoints:Http:Url={0}", UrlString), + String.Format(CultureInfo.InvariantCulture, "General:ApiPort={0}", 5010), String.Format(CultureInfo.InvariantCulture, "Database:DatabaseType={0}", DatabaseType), String.Format(CultureInfo.InvariantCulture, "Database:ConnectionString={0}", connectionString), String.Format(CultureInfo.InvariantCulture, "General:SetupWizardMode={0}", SetupWizardMode.Never),