Merge pull request #1029 from tgstation/dev [TGSDeploy]

v4.3.0
This commit is contained in:
Jordan Brown
2020-05-28 23:03:05 -04:00
committed by GitHub
83 changed files with 3666 additions and 674 deletions
+91 -45
View File
@@ -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
+1 -1
View File
@@ -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
+4 -4
View File
@@ -2,10 +2,10 @@
<PropertyGroup>
<!-- This is the authorative version list -->
<!-- Integration tests will ensure they match across the board -->
<TgsCoreVersion>4.2.8</TgsCoreVersion>
<TgsApiVersion>6.4.1</TgsApiVersion>
<TgsClientVersion>7.0.1</TgsClientVersion>
<TgsDmapiVersion>5.2.1</TgsDmapiVersion>
<TgsCoreVersion>4.3.0</TgsCoreVersion>
<TgsApiVersion>6.5.0</TgsApiVersion>
<TgsClientVersion>7.1.0</TgsClientVersion>
<TgsDmapiVersion>5.2.2</TgsDmapiVersion>
<TgsControlPanelVersion>0.4.0</TgsControlPanelVersion>
<TgsHostWatchdogVersion>1.1.0</TgsHostWatchdogVersion>
</PropertyGroup>
+17
View File
@@ -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
+5 -18
View File
@@ -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
+2 -1
View File
@@ -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].
*/
+3 -1
View File
@@ -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)
+3 -1
View File
@@ -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()
@@ -18,6 +18,6 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The <see cref="CompileJob"/>s made from the <see cref="RevisionInformation"/>
/// </summary>
public ICollection<CompileJob>? CompileJobs { get; set; }
public ICollection<EntityId>? CompileJobs { get; set; }
}
}
+1 -1
View File
@@ -16,6 +16,6 @@
/// <summary>
/// The <see cref="User"/> who created this <see cref="User"/>
/// </summary>
public User? CreatedBy { get; set; }
public Internal.User? CreatedBy { get; set; }
}
}
+11 -15
View File
@@ -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<Token>(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;
}
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "3.1.3",
"version": "3.1.4",
"commands": [
"dotnet-ef"
]
@@ -620,13 +620,61 @@ namespace Tgstation.Server.Host.Components.Chat
}
/// <inheritdoc />
public Task SendUpdateMessage(string message, CancellationToken cancellationToken)
public async Task<Func<string, string, Task>> SendDeploymentMessage(
Models.RevisionInformation revisionInformation,
Version byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
bool localCommitPushed,
CancellationToken cancellationToken)
{
List<ulong> 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<Func<string, string, Task>>();
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)));
}
/// <inheritdoc />
@@ -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
{
@@ -62,12 +62,24 @@ namespace Tgstation.Server.Host.Components.Chat
Task SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken);
/// <summary>
/// Send a chat <paramref name="message"/> to configured update channels
/// Send the message for a deployment to configured deployment channels.
/// </summary>
/// <param name="message">The message being sent</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task SendUpdateMessage(string message, CancellationToken cancellationToken);
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> of the deployment.</param>
/// <param name="byondVersion">The BYOND <see cref="Version"/> of the deployment.</param>
/// <param name="estimatedCompletionTime">The optional <see cref="DateTimeOffset"/> the deployment is expected to be completed at.</param>
/// <param name="gitHubOwner">The repository GitHub owner, if any.</param>
/// <param name="gitHubRepo">The repository GitHub name, if any.</param>
/// <param name="localCommitPushed"><see langword="true"/> if the local deployment commit was pushed to the remote repository.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="Func{T1, T2, TResult}"/> to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any.</returns>
Task<Func<string, string, Task>> SendDeploymentMessage(
Models.RevisionInformation revisionInformation,
Version byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
bool localCommitPushed,
CancellationToken cancellationToken);
/// <summary>
/// Start tracking <see cref="Commands.CustomCommand"/>s and <see cref="ChannelRepresentation"/>s.
@@ -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
}
}
/// <summary>
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="DiscordProvider"/>.
/// </summary>
readonly IAssemblyInformationProvider assemblyInformationProvider;
/// <summary>
/// The <see cref="DiscordSocketClient"/> for the <see cref="DiscordProvider"/>
/// </summary>
@@ -53,12 +60,18 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <summary>
/// Construct a <see cref="DiscordProvider"/>
/// </summary>
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
/// <param name="logger">The value of <see cref="Logger"/></param>
/// <param name="botToken">The value of <see cref="botToken"/></param>
/// <param name="reconnectInterval">The initial reconnect interval in minutes.</param>
public DiscordProvider(ILogger<DiscordProvider> logger, string botToken, uint reconnectInterval)
public DiscordProvider(
IAssemblyInformationProvider assemblyInformationProvider,
ILogger<DiscordProvider> 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);
}
}
/// <inheritdoc />
public override async Task<Func<string, string, 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<EmbedFieldBuilder>
{
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<RevInfoTestMerge>())
.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);
}
}
};
}
}
}
@@ -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
/// <param name="reconnectInterval">The reconnection interval in minutes.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task SetReconnectInterval(uint reconnectInterval);
/// <summary>
/// Send the message for a deployment.
/// </summary>
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> of the deployment.</param>
/// <param name="byondVersion">The BYOND <see cref="Version"/> of the deployment.</param>
/// <param name="estimatedCompletionTime">The optional <see cref="DateTimeOffset"/> the deployment is expected to be completed at.</param>
/// <param name="gitHubOwner">The repository GitHub owner, if any.</param>
/// <param name="gitHubRepo">The repository GitHub name, if any.</param>
/// <param name="channelId">The <see cref="ChannelRepresentation.RealId"/> to send to</param>
/// <param name="localCommitPushed"><see langword="true"/> if the local deployment commit was pushed to the remote repository.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="Func{T1, T2, TResult}"/> to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any.</returns>
Task<Func<string, string, Task>> SendUpdateMessage(
RevisionInformation revisionInformation,
Version byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
ulong channelId,
bool localCommitPushed,
CancellationToken cancellationToken);
}
}
@@ -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);
/// <inheritdoc />
public override async Task<Func<string, string, 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);
}
}
}
@@ -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
/// <inheritdoc />
public abstract Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken);
/// <inheritdoc />
public abstract Task<Func<string, string, Task>> SendUpdateMessage(
RevisionInformation revisionInformation,
Version byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
ulong channelId,
bool localCommitPushed,
CancellationToken cancellationToken);
}
}
@@ -56,7 +56,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
return new IrcProvider(assemblyInformationProvider, asyncDelayer, loggerFactory.CreateLogger<IrcProvider>(), 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<DiscordProvider>(), discordBuilder.BotToken, settings.ReconnectionInterval.Value);
return new DiscordProvider(
assemblyInformationProvider,
loggerFactory.CreateLogger<DiscordProvider>(),
discordBuilder.BotToken,
settings.ReconnectionInterval.Value);
default:
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider));
}
@@ -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))
@@ -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
/// </summary>
readonly IProcessExecutor processExecutor;
/// <summary>
/// The <see cref="IWatchdog"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IWatchdog watchdog;
/// <summary>
/// The <see cref="IRepositoryManager"/> for <see cref="DreamMaker"/>.
/// </summary>
@@ -113,14 +107,18 @@ namespace Tgstation.Server.Host.Components.Deployment
readonly Api.Models.Instance metadata;
/// <summary>
/// <see langword="lock"/> <see cref="object"/> for <see cref="compiling"/>.
/// <see langword="lock"/> <see cref="object"/> for <see cref="deploying"/>.
/// </summary>
readonly object compilingLock;
readonly object deploymentLock;
Func<string, string, Task> currentChatCallback;
string currentDreamMakerOutput;
/// <summary>
/// If a compile job is running
/// </summary>
bool compiling;
bool deploying;
/// <summary>
/// Construct <see cref="DreamMaker"/>
@@ -132,7 +130,6 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/></param>
/// <param name="chatManager">The value of <see cref="chatManager"/></param>
/// <param name="processExecutor">The value of <see cref="processExecutor"/></param>
/// <param name="watchdog">The value of <see cref="watchdog"/></param>
/// <param name="gitHubClientFactory">The value of <see cref="gitHubClientFactory"/>.</param>
/// <param name="compileJobConsumer">The value of <see cref="compileJobConsumer"/>.</param>
/// <param name="repositoryManager">The value of <see cref="repositoryManager"/>.</param>
@@ -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();
}
/// <summary>
@@ -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 <paramref name="job"/>
/// </summary>
/// <param name="job">The running <see cref="CompileJob"/></param>
/// <param name="cancelled">If the <paramref name="job"/> was cancelled</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
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);
}
/// <summary>
/// Send a message to <see cref="chatManager"/> about a deployment
/// </summary>
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> for the deployment</param>
/// <param name="byondLock">The <see cref="IByondExecutableLock"/> for the deployment</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
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);
}
/// <summary>
@@ -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<RevInfoTestMerge>()
};
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<RevInfoTestMerge>()
};
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
/// <returns>A <see cref="Task{TResult}"/> resulting in the average <see cref="TimeSpan"/> of the 10 previous deployments or <see langword="null"/> if there are none.</returns>
async Task<TimeSpan?> 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<Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action<int> progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
async Task<Models.CompileJob> Compile(
Models.RevisionInformation revisionInformation,
Api.Models.DreamMaker dreamMakerSettings,
uint apiValidateTimeout,
IRepository repository,
Action<int> 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);
}
@@ -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<RevisionInformation> 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);
@@ -262,7 +262,6 @@ namespace Tgstation.Server.Host.Components
eventConsumer,
chatManager,
processExecutor,
watchdog,
gitHubClientFactory,
dmbFactory,
repoManager,
@@ -213,10 +213,14 @@ namespace Tgstation.Server.Host.Components
var tasks = new List<Task>();
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)
@@ -33,7 +33,7 @@ namespace Tgstation.Server.Host.Components.Interop
/// <summary>
/// The DMAPI <see cref="Version"/> being used.
/// </summary>
public static readonly Version Version = new Version(5, 2, 1);
public static readonly Version Version = new Version(5, 2, 2);
/// <summary>
/// <see cref="JsonSerializerSettings"/> for use when communicating with the DMAPI.
@@ -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);
@@ -192,7 +192,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
};
/// <inheritdoc />
protected override void DisposeAndNullControllers()
protected override void DisposeAndNullControllersImpl()
{
Server?.Dispose();
Server = null;
@@ -392,10 +392,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
}
#pragma warning restore CA1502
/// <summary>
/// Call <see cref="IDisposable.Dispose"/> on <see cref="alphaServer"/> and <see cref="bravoServer"/> and set them to <see langword="null"/>
/// </summary>
protected override void DisposeAndNullControllers()
/// <inheritdoc />
protected override void DisposeAndNullControllersImpl()
{
alphaServer?.Dispose();
alphaServer = null;
@@ -112,6 +112,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
readonly IRestartRegistration restartRegistration;
/// <summary>
/// <see langword="lock"/> <see cref="object"/> used for <see cref="DisposeAndNullControllers"/>.
/// </summary>
readonly object controllerDisposeLock;
/// <summary>
/// If the <see cref="WatchdogBase"/> should <see cref="LaunchImplNoLock(bool, bool, DualReattachInformation, CancellationToken)"/> in <see cref="StartAsync(CancellationToken)"/>
/// </summary>
@@ -201,6 +206,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
ActiveLaunchParameters = initialLaunchParameters;
releaseServers = false;
ActiveParametersUpdated = new TaskCompletionSource<object>();
controllerDisposeLock = new object();
restartRegistration = serverControl.RegisterForRestart(this);
try
@@ -463,7 +469,16 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <summary>
/// Call <see cref="IDisposable.Dispose"/> and null the fields for all <see cref="ISessionController"/>s and set <see cref="Running"/> to <see langword="false"/>.
/// </summary>
protected abstract void DisposeAndNullControllers();
protected abstract void DisposeAndNullControllersImpl();
/// <summary>
/// Wrapper for <see cref="DisposeAndNullControllersImpl"/> under a locked context.
/// </summary>
protected void DisposeAndNullControllers()
{
lock (controllerDisposeLock)
DisposeAndNullControllersImpl();
}
/// <summary>
/// Get the active <see cref="ISessionController"/>.
@@ -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)
@@ -101,9 +101,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
}
/// <inheritdoc />
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;
@@ -4,7 +4,7 @@ using Newtonsoft.Json.Converters;
namespace Tgstation.Server.Host.Configuration
{
/// <summary>
/// Configuration options for the <see cref="Database.DatabaseContext{TParentContext}"/>
/// Configuration options for the <see cref="Database.DatabaseContext"/>
/// </summary>
sealed class DatabaseConfiguration
{
@@ -24,5 +24,10 @@
/// Use Sqlite
/// </summary>
Sqlite,
/// <summary>
/// Use PostgresSql
/// </summary>
PostgresSql,
}
}
@@ -14,6 +14,11 @@ namespace Tgstation.Server.Host.Configuration
/// </summary>
public const string Section = "General";
/// <summary>
/// The default value of <see cref="ApiPort"/>.
/// </summary>
public const ushort DefaultApiPort = 5000;
/// <summary>
/// The default value for <see cref="ServerInformation.MinimumPasswordLength"/>.
/// </summary>
@@ -37,7 +42,12 @@ namespace Tgstation.Server.Host.Configuration
/// <summary>
/// The default value for <see cref="RestartTimeout"/>
/// </summary>
const int DefaultRestartTimeout = 10000;
const int DefaultRestartTimeout = 60000;
/// <summary>
/// The port the TGS API listens on.
/// </summary>
public ushort ApiPort { get; set; }
/// <summary>
/// A GitHub personal access token to use for bypassing rate limits on requests. Requires no scopes
@@ -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,
@@ -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<IActionResult> 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<Api.Models.ChatBot>), 200)]
public async Task<IActionResult> 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<IActionResult> 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);
@@ -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);
@@ -61,7 +61,12 @@ namespace Tgstation.Server.Host.Controllers
public async Task<IActionResult> 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<IActionResult> 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<EntityId>), 200)]
public async Task<IActionResult> 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);
@@ -196,12 +196,12 @@ namespace Tgstation.Server.Host.Controllers
using (systemIdentity)
{
// Get the user from the database
IQueryable<User> query;
IQueryable<User> 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,
@@ -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<IActionResult> 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<Models.Instance> InstanceQuery() => DatabaseContext.Instances.Where(x => x.Id == model.Id);
IQueryable<Models.Instance> 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<Models.Instance> 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);
@@ -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<Api.Models.InstanceUser>), 200)]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}
@@ -49,7 +49,13 @@ namespace Tgstation.Server.Host.Controllers
[ProducesResponseType(typeof(IEnumerable<Api.Models.Job>), 200)]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
@@ -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<IActionResult> 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<IActionResult> 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
@@ -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);
@@ -202,7 +202,7 @@ namespace Tgstation.Server.Host.Core
// CORS conditionally enabled later
services.AddCors();
void AddTypedContext<TContext>() where TContext : DatabaseContext<TContext>
void AddTypedContext<TContext>() where TContext : DatabaseContext
{
services.AddDbContext<TContext>(builder =>
{
@@ -226,6 +226,9 @@ namespace Tgstation.Server.Host.Core
case DatabaseType.Sqlite:
AddTypedContext<SqliteDatabaseContext>();
break;
case DatabaseType.PostgresSql:
AddTypedContext<PostgresSqlDatabaseContext>();
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) =>
{
@@ -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
{
/// <inheritdoc />
public ushort HttpApiPort { get; }
public ushort HttpApiPort => generalConfiguration.ApiPort;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="ServerPortProivder"/>.
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// Initializes a new instance of the <see cref="ServerPortProivder"/> <see langword="class"/>.
/// </summary>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
/// <param name="configuration">The <see cref="IConfiguration"/> to use.</param>
public ServerPortProivder(IConfiguration configuration)
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
public ServerPortProivder(
IOptions<GeneralConfiguration> generalConfigurationOptions,
IConfiguration configuration,
ILogger<ServerPortProivder> 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;
}
}
}
@@ -133,6 +133,14 @@ namespace Tgstation.Server.Host.Core
swaggerGenOptions.DocumentFilter<SwaggerConfiguration>();
swaggerGenOptions.SchemaFilter<SwaggerConfiguration>();
swaggerGenOptions.CustomSchemaIds(type =>
{
if (type == typeof(Api.Models.Internal.User))
return "ShallowUser";
return type.Name;
});
swaggerGenOptions.AddSecurityDefinition(PasswordSecuritySchemeId, new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
@@ -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);
/// <inheritdoc />
public Task ForEachAsync(Action<TModel> action, CancellationToken cancellationToken) => dbSet
.AsAsyncEnumerable()
.ForEachAsync(action, cancellationToken);
public IAsyncEnumerator<TModel> GetAsyncEnumerator(CancellationToken cancellationToken = default) => dbSet.AsAsyncEnumerable().GetAsyncEnumerator();
/// <inheritdoc />
public IEnumerator<TModel> GetEnumerator() => dbSet.AsQueryable().GetEnumerator();
@@ -61,9 +58,6 @@ namespace Tgstation.Server.Host.Database
/// <inheritdoc />
public void RemoveRange(IEnumerable<TModel> models) => dbSet.RemoveRange(models);
/// <inheritdoc />
public Task<List<TModel>> ToListAsync(CancellationToken cancellationToken) => dbSet.AsQueryable().ToListAsync(cancellationToken);
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => dbSet.AsQueryable().GetEnumerator();
}
@@ -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!");
}
@@ -18,95 +18,94 @@ namespace Tgstation.Server.Host.Database
/// <summary>
/// Backend abstract implementation of <see cref="IDatabaseContext"/>
/// </summary>
/// <typeparam name="TParentContext">The child <see cref="Type"/> used to implement a backend.</typeparam>
#pragma warning disable CA1506 // TODO: Decomplexify
abstract class DatabaseContext<TParentContext> : DbContext, IDatabaseContext where TParentContext : DbContext
abstract class DatabaseContext : DbContext, IDatabaseContext
{
/// <inheritdoc />
public DatabaseType DatabaseType => DatabaseConfiguration.DatabaseType;
/// <summary>
/// The <see cref="User"/>s in the <see cref="DatabaseContext{TParentContext}"/>.
/// The <see cref="User"/>s in the <see cref="DatabaseContext"/>.
/// </summary>
public DbSet<User> Users { get; set; }
/// <summary>
/// The <see cref="Instance"/>s in the <see cref="DatabaseContext{TParentContext}"/>.
/// The <see cref="Instance"/>s in the <see cref="DatabaseContext"/>.
/// </summary>
public DbSet<Instance> Instances { get; set; }
/// <summary>
/// The <see cref="CompileJob"/>s in the <see cref="DatabaseContext{TParentContext}"/>.
/// The <see cref="CompileJob"/>s in the <see cref="DatabaseContext"/>.
/// </summary>
public DbSet<CompileJob> CompileJobs { get; set; }
/// <summary>
/// The <see cref="RevisionInformation"/>s in the <see cref="DatabaseContext{TParentContext}"/>.
/// The <see cref="RevisionInformation"/>s in the <see cref="DatabaseContext"/>.
/// </summary>
public DbSet<RevisionInformation> RevisionInformations { get; set; }
/// <summary>
/// The <see cref="Models.DreamMakerSettings"/> in the <see cref="DatabaseContext{TParentContext}"/>.
/// The <see cref="Models.DreamMakerSettings"/> in the <see cref="DatabaseContext"/>.
/// </summary>
public DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
/// <summary>
/// The <see cref="ChatBot"/>s in the <see cref="DatabaseContext{TParentContext}"/>.
/// The <see cref="ChatBot"/>s in the <see cref="DatabaseContext"/>.
/// </summary>
public DbSet<ChatBot> ChatBots { get; set; }
/// <summary>
/// The <see cref="Models.DreamDaemonSettings"/> in the <see cref="DatabaseContext{TParentContext}"/>.
/// The <see cref="Models.DreamDaemonSettings"/> in the <see cref="DatabaseContext"/>.
/// </summary>
public DbSet<DreamDaemonSettings> DreamDaemonSettings { get; set; }
/// <summary>
/// The <see cref="Models.RepositorySettings"/> in the <see cref="DatabaseContext{TParentContext}"/>.
/// The <see cref="Models.RepositorySettings"/> in the <see cref="DatabaseContext"/>.
/// </summary>
public DbSet<RepositorySettings> RepositorySettings { get; set; }
/// <summary>
/// The <see cref="InstanceUser"/>s in the <see cref="DatabaseContext{TParentContext}"/>.
/// The <see cref="InstanceUser"/>s in the <see cref="DatabaseContext"/>.
/// </summary>
public DbSet<InstanceUser> InstanceUsers { get; set; }
/// <summary>
/// The <see cref="ChatChannel"/>s in the <see cref="DatabaseContext{TParentContext}"/>.
/// The <see cref="ChatChannel"/>s in the <see cref="DatabaseContext"/>.
/// </summary>
public DbSet<ChatChannel> ChatChannels { get; set; }
/// <summary>
/// The <see cref="Job"/>s in the <see cref="DatabaseContext{TParentContext}"/>.
/// The <see cref="Job"/>s in the <see cref="DatabaseContext"/>.
/// </summary>
public DbSet<Job> Jobs { get; set; }
/// <summary>
/// The <see cref="ReattachInformation"/>s in the <see cref="DatabaseContext{TParentContext}"/>.
/// The <see cref="ReattachInformation"/>s in the <see cref="DatabaseContext"/>.
/// </summary>
public DbSet<ReattachInformation> ReattachInformations { get; set; }
/// <summary>
/// The <see cref="DualReattachInformation"/>s in the <see cref="DatabaseContext{TParentContext}"/>.
/// The <see cref="DualReattachInformation"/>s in the <see cref="DatabaseContext"/>.
/// </summary>
public DbSet<DualReattachInformation> WatchdogReattachInformations { get; set; }
/// <summary>
/// The <see cref="TestMerge"/>s in the <see cref="DatabaseContext{TParentContext}"/>
/// The <see cref="TestMerge"/>s in the <see cref="DatabaseContext"/>
/// </summary>
public DbSet<TestMerge> TestMerges { get; set; }
/// <summary>
/// The <see cref="RevInfoTestMerge"/>s om the <see cref="DatabaseContext{TParentContext}"/>
/// The <see cref="RevInfoTestMerge"/>s om the <see cref="DatabaseContext"/>
/// </summary>
public DbSet<RevInfoTestMerge> RevInfoTestMerges { get; set; }
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="DatabaseContext{TParentContext}"/>
/// The <see cref="ILogger"/> for the <see cref="DatabaseContext"/>
/// </summary>
protected ILogger Logger { get; }
/// <summary>
/// The <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext{TParentContext}"/>
/// The <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext"/>
/// </summary>
protected DatabaseConfiguration DatabaseConfiguration { get; }
@@ -150,7 +149,7 @@ namespace Tgstation.Server.Host.Database
IDatabaseCollection<DualReattachInformation> IDatabaseContext.WatchdogReattachInformations => watchdogReattachInformationsCollection;
/// <summary>
/// The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext{TParentContext}"/>
/// The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext"/>
/// </summary>
readonly IDatabaseSeeder databaseSeeder;
@@ -220,13 +219,13 @@ namespace Tgstation.Server.Host.Database
readonly IDatabaseCollection<DualReattachInformation> watchdogReattachInformationsCollection;
/// <summary>
/// Construct a <see cref="DatabaseContext{TParentContext}"/>
/// Construct a <see cref="DatabaseContext"/>
/// </summary>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TParentContext}"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="dbContextOptions">The <see cref="DbContextOptions"/> for the <see cref="DatabaseContext"/>.</param>
/// <param name="databaseConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="DatabaseConfiguration"/></param>
/// <param name="databaseSeeder">The value of <see cref="databaseSeeder"/></param>
/// <param name="logger">The value of <see cref="Logger"/></param>
public DatabaseContext(DbContextOptions<TParentContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions)
public DatabaseContext(DbContextOptions dbContextOptions, IOptions<DatabaseConfiguration> 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
}
/// <summary>
/// Ensure the <see cref="DatabaseType"/> is correct for the <see cref="DatabaseContext{TParentContext}"/>.
/// Ensure the <see cref="DatabaseType"/> is correct for the <see cref="DatabaseContext"/>.
/// </summary>
protected abstract void ValidateDatabaseType();
}
@@ -14,12 +14,12 @@ namespace Tgstation.Server.Host.Database
sealed class DatabaseSeeder : IDatabaseSeeder
{
/// <summary>
/// The <see cref="ICryptographySuite"/> for the <see cref="DatabaseContext{TParentContext}"/>
/// The <see cref="ICryptographySuite"/> for the <see cref="DatabaseSeeder"/>
/// </summary>
readonly ICryptographySuite cryptographySuite;
/// <summary>
/// The <see cref="IPlatformIdentifier"/> for the <see cref="DatabaseContext{TParentContext}"/>.
/// The <see cref="IPlatformIdentifier"/> for the <see cref="DatabaseSeeder"/>.
/// </summary>
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);
@@ -4,7 +4,7 @@ using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.Database.Design
{
/// <summary>
/// Contains helpers for creating design time <see cref="DatabaseContext{TParentContext}"/>s
/// Contains helpers for creating design time <see cref="DatabaseContext"/>s
/// </summary>
static class DesignTimeDbContextFactoryHelpers
{
@@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Database.Design
/// <param name="databaseType">The <see cref="DatabaseConfiguration.DatabaseType"/>.</param>
/// <param name="connectionString">The <see cref="DatabaseConfiguration.ConnectionString"/>.</param>
/// <returns>The <see cref="IOptions{TOptions}"/> for the <see cref="DatabaseConfiguration"/></returns>
public static IOptions<DatabaseConfiguration> GetDbContextOptions(DatabaseType databaseType, string connectionString)
public static IOptions<DatabaseConfiguration> GetDatabaseConfiguration(DatabaseType databaseType, string connectionString)
{
var dbConfig = new DatabaseConfiguration
{
@@ -20,7 +20,7 @@ namespace Tgstation.Server.Host.Database.Design
using var loggerFactory = new LoggerFactory();
return new MySqlDatabaseContext(
new DbContextOptions<MySqlDatabaseContext>(),
DesignTimeDbContextFactoryHelpers.GetDbContextOptions(
DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration(
DatabaseType.MariaDB,
"Server=127.0.0.1;User Id=root;Password=fake;Database=TGS_Design"),
new DatabaseSeeder(
@@ -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
{
/// <inheritdoc />
sealed class PostgresSqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory<PostgresSqlDatabaseContext>
{
/// <inheritdoc />
public PostgresSqlDatabaseContext CreateDbContext(string[] args)
{
using var loggerFactory = new LoggerFactory();
return new PostgresSqlDatabaseContext(
new DbContextOptions<PostgresSqlDatabaseContext>(),
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<User>()),
new PlatformIdentifier()),
loggerFactory.CreateLogger<PostgresSqlDatabaseContext>());
}
}
}
@@ -20,7 +20,7 @@ namespace Tgstation.Server.Host.Database.Design
using var loggerFactory = new LoggerFactory();
return new SqlServerDatabaseContext(
new DbContextOptions<SqlServerDatabaseContext>(),
DesignTimeDbContextFactoryHelpers.GetDbContextOptions(
DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration(
DatabaseType.SqlServer,
"Data Source=fake;Initial Catalog=TGS_Design;Integrated Security=True;Application Name=tgstation-server"),
new DatabaseSeeder(
@@ -20,7 +20,7 @@ namespace Tgstation.Server.Host.Database.Design
using var loggerFactory = new LoggerFactory();
return new SqliteDatabaseContext(
new DbContextOptions<SqliteDatabaseContext>(),
DesignTimeDbContextFactoryHelpers.GetDbContextOptions(
DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration(
DatabaseType.Sqlite,
"Data Source=tgs_design.sqlite3;Mode=ReadWriteCreate"),
new DatabaseSeeder(
@@ -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.
/// </summary>
/// <typeparam name="TModel">The type of model.</typeparam>
public interface IDatabaseCollection<TModel> : IQueryable<TModel>
public interface IDatabaseCollection<TModel> : IQueryable<TModel>, IAsyncEnumerable<TModel>
{
/// <summary>
/// An <see cref="IEnumerable{T}"/> of <typeparamref name="TModel"/>s prioritizing in the working set.
@@ -46,20 +43,5 @@ namespace Tgstation.Server.Host.Database
/// </summary>
/// <param name="models">An <see cref="IEnumerable{T}"/> of <typeparamref name="TModel"/>s to remove.</param>
void RemoveRange(IEnumerable<TModel> models);
/// <summary>
/// Asyncronously run a given <paramref name="action"/> on the <see cref="IDatabaseCollection{TModel}"/>.
/// </summary>
/// <param name="action">The <see cref="Action{T}"/> to run.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task ForEachAsync(Action<TModel> action, CancellationToken cancellationToken);
/// <summary>
/// Retrieve all the <typeparamref name="TModel"/>s in the table.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="List{T}"/> of all <typeparamref name="TModel"/> in the table.</returns>
Task<List<TModel>> ToListAsync(CancellationToken cancellationToken);
}
}
@@ -0,0 +1,816 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ChannelLimit")
.HasColumnType("integer");
b.Property<string>("ConnectionString")
.IsRequired()
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<bool?>("Enabled")
.HasColumnType("boolean");
b.Property<long>("InstanceId")
.HasColumnType("bigint");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("character varying(100)")
.HasMaxLength(100);
b.Property<int>("Provider")
.HasColumnType("integer");
b.Property<long>("ReconnectionInterval")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("InstanceId", "Name")
.IsUnique();
b.ToTable("ChatBots");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<long>("ChatSettingsId")
.HasColumnType("bigint");
b.Property<decimal?>("DiscordChannelId")
.HasColumnType("numeric(20,0)");
b.Property<string>("IrcChannel")
.HasColumnType("character varying(100)")
.HasMaxLength(100);
b.Property<bool?>("IsAdminChannel")
.IsRequired()
.HasColumnType("boolean");
b.Property<bool?>("IsUpdatesChannel")
.IsRequired()
.HasColumnType("boolean");
b.Property<bool?>("IsWatchdogChannel")
.IsRequired()
.HasColumnType("boolean");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("ByondVersion")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("DMApiMajorVersion")
.HasColumnType("integer");
b.Property<int?>("DMApiMinorVersion")
.HasColumnType("integer");
b.Property<int?>("DMApiPatchVersion")
.HasColumnType("integer");
b.Property<Guid?>("DirectoryName")
.IsRequired()
.HasColumnType("uuid");
b.Property<string>("DmeName")
.IsRequired()
.HasColumnType("text");
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<int>("MinimumSecurityLevel")
.HasColumnType("integer");
b.Property<string>("Output")
.IsRequired()
.HasColumnType("text");
b.Property<long>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<bool?>("AllowWebClient")
.IsRequired()
.HasColumnType("boolean");
b.Property<bool?>("AutoStart")
.IsRequired()
.HasColumnType("boolean");
b.Property<long>("HeartbeatSeconds")
.HasColumnType("bigint");
b.Property<long>("InstanceId")
.HasColumnType("bigint");
b.Property<int>("PrimaryPort")
.HasColumnType("integer");
b.Property<int>("SecondaryPort")
.HasColumnType("integer");
b.Property<int>("SecurityLevel")
.HasColumnType("integer");
b.Property<long>("StartupTimeout")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("InstanceId")
.IsUnique();
b.ToTable("DreamDaemonSettings");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiValidationPort")
.HasColumnType("integer");
b.Property<int>("ApiValidationSecurityLevel")
.HasColumnType("integer");
b.Property<long>("InstanceId")
.HasColumnType("bigint");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<long?>("AlphaId")
.HasColumnType("bigint");
b.Property<bool>("AlphaIsActive")
.HasColumnType("boolean");
b.Property<long?>("BravoId")
.HasColumnType("bigint");
b.Property<long>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<long>("AutoUpdateInterval")
.HasColumnType("bigint");
b.Property<int>("ChatBotLimit")
.HasColumnType("integer");
b.Property<int>("ConfigurationType")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<bool?>("Online")
.IsRequired()
.HasColumnType("boolean");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Path")
.IsUnique();
b.ToTable("Instances");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<decimal>("ByondRights")
.HasColumnType("numeric(20,0)");
b.Property<decimal>("ChatBotRights")
.HasColumnType("numeric(20,0)");
b.Property<decimal>("ConfigurationRights")
.HasColumnType("numeric(20,0)");
b.Property<decimal>("DreamDaemonRights")
.HasColumnType("numeric(20,0)");
b.Property<decimal>("DreamMakerRights")
.HasColumnType("numeric(20,0)");
b.Property<long>("InstanceId")
.HasColumnType("bigint");
b.Property<decimal>("InstanceUserRights")
.HasColumnType("numeric(20,0)");
b.Property<decimal>("RepositoryRights")
.HasColumnType("numeric(20,0)");
b.Property<long?>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<decimal?>("CancelRight")
.HasColumnType("numeric(20,0)");
b.Property<decimal?>("CancelRightsType")
.HasColumnType("numeric(20,0)");
b.Property<bool?>("Cancelled")
.IsRequired()
.HasColumnType("boolean");
b.Property<long?>("CancelledById")
.HasColumnType("bigint");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("ErrorCode")
.HasColumnType("bigint");
b.Property<string>("ExceptionDetails")
.HasColumnType("text");
b.Property<long>("InstanceId")
.HasColumnType("bigint");
b.Property<DateTimeOffset?>("StartedAt")
.IsRequired()
.HasColumnType("timestamp with time zone");
b.Property<long>("StartedById")
.HasColumnType("bigint");
b.Property<DateTimeOffset?>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("AccessIdentifier")
.IsRequired()
.HasColumnType("text");
b.Property<long>("CompileJobId")
.HasColumnType("bigint");
b.Property<bool>("IsPrimary")
.HasColumnType("boolean");
b.Property<int>("LaunchSecurityLevel")
.HasColumnType("integer");
b.Property<int>("Port")
.HasColumnType("integer");
b.Property<int>("ProcessId")
.HasColumnType("integer");
b.Property<int>("RebootState")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("CompileJobId");
b.ToTable("ReattachInformations");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("AccessToken")
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<string>("AccessUser")
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<bool?>("AutoUpdatesKeepTestMerges")
.IsRequired()
.HasColumnType("boolean");
b.Property<bool?>("AutoUpdatesSynchronize")
.IsRequired()
.HasColumnType("boolean");
b.Property<string>("CommitterEmail")
.IsRequired()
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<string>("CommitterName")
.IsRequired()
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<long>("InstanceId")
.HasColumnType("bigint");
b.Property<bool?>("PostTestMergeComment")
.IsRequired()
.HasColumnType("boolean");
b.Property<bool?>("PushTestMergeCommits")
.IsRequired()
.HasColumnType("boolean");
b.Property<bool?>("ShowTestMergeCommitters")
.IsRequired()
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("InstanceId")
.IsUnique();
b.ToTable("RepositorySettings");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<long>("RevisionInformationId")
.HasColumnType("bigint");
b.Property<long>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("CommitSha")
.IsRequired()
.HasColumnType("character varying(40)")
.HasMaxLength(40);
b.Property<long>("InstanceId")
.HasColumnType("bigint");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Author")
.IsRequired()
.HasColumnType("text");
b.Property<string>("BodyAtMerge")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Comment")
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<DateTimeOffset>("MergedAt")
.HasColumnType("timestamp with time zone");
b.Property<long>("MergedById")
.HasColumnType("bigint");
b.Property<int>("Number")
.HasColumnType("integer");
b.Property<long?>("PrimaryRevisionInformationId")
.IsRequired()
.HasColumnType("bigint");
b.Property<string>("PullRequestRevision")
.IsRequired()
.HasColumnType("character varying(40)")
.HasMaxLength(40);
b.Property<string>("TitleAtMerge")
.IsRequired()
.HasColumnType("text");
b.Property<string>("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<long?>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<decimal>("AdministrationRights")
.HasColumnType("numeric(20,0)");
b.Property<string>("CanonicalName")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset?>("CreatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone");
b.Property<long?>("CreatedById")
.HasColumnType("bigint");
b.Property<bool?>("Enabled")
.IsRequired()
.HasColumnType("boolean");
b.Property<decimal>("InstanceManagerRights")
.HasColumnType("numeric(20,0)");
b.Property<DateTimeOffset?>("LastPasswordUpdate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("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
}
}
}
@@ -0,0 +1,638 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Tgstation.Server.Host.Database.Migrations
{
/// <summary>
/// Create initial schema for PostgreSQL.
/// </summary>
public partial class PGCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder == null)
throw new ArgumentNullException(nameof(migrationBuilder));
migrationBuilder.CreateTable(
name: "Instances",
columns: table => new
{
Id = table.Column<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(maxLength: 10000, nullable: false),
Path = table.Column<string>(nullable: false),
Online = table.Column<bool>(nullable: false),
ConfigurationType = table.Column<int>(nullable: false),
AutoUpdateInterval = table.Column<long>(nullable: false),
ChatBotLimit = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Instances", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Enabled = table.Column<bool>(nullable: false),
CreatedAt = table.Column<DateTimeOffset>(nullable: false),
SystemIdentifier = table.Column<string>(nullable: true),
Name = table.Column<string>(maxLength: 10000, nullable: false),
AdministrationRights = table.Column<decimal>(nullable: false),
InstanceManagerRights = table.Column<decimal>(nullable: false),
PasswordHash = table.Column<string>(nullable: true),
CreatedById = table.Column<long>(nullable: true),
CanonicalName = table.Column<string>(nullable: false),
LastPasswordUpdate = table.Column<DateTimeOffset>(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<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(maxLength: 100, nullable: false),
Enabled = table.Column<bool>(nullable: true),
ReconnectionInterval = table.Column<long>(nullable: false),
ChannelLimit = table.Column<int>(nullable: false),
Provider = table.Column<int>(nullable: false),
ConnectionString = table.Column<string>(maxLength: 10000, nullable: false),
InstanceId = table.Column<long>(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<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
AllowWebClient = table.Column<bool>(nullable: false),
SecurityLevel = table.Column<int>(nullable: false),
PrimaryPort = table.Column<int>(nullable: false),
SecondaryPort = table.Column<int>(nullable: false),
StartupTimeout = table.Column<long>(nullable: false),
HeartbeatSeconds = table.Column<long>(nullable: false),
AutoStart = table.Column<bool>(nullable: false),
InstanceId = table.Column<long>(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<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ProjectName = table.Column<string>(maxLength: 10000, nullable: true),
ApiValidationPort = table.Column<int>(nullable: false),
ApiValidationSecurityLevel = table.Column<int>(nullable: false),
InstanceId = table.Column<long>(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<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
CommitterName = table.Column<string>(maxLength: 10000, nullable: false),
CommitterEmail = table.Column<string>(maxLength: 10000, nullable: false),
AccessUser = table.Column<string>(maxLength: 10000, nullable: true),
AccessToken = table.Column<string>(maxLength: 10000, nullable: true),
PushTestMergeCommits = table.Column<bool>(nullable: false),
ShowTestMergeCommitters = table.Column<bool>(nullable: false),
AutoUpdatesKeepTestMerges = table.Column<bool>(nullable: false),
AutoUpdatesSynchronize = table.Column<bool>(nullable: false),
PostTestMergeComment = table.Column<bool>(nullable: false),
InstanceId = table.Column<long>(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<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
CommitSha = table.Column<string>(maxLength: 40, nullable: false),
OriginCommitSha = table.Column<string>(maxLength: 40, nullable: false),
InstanceId = table.Column<long>(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<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<long>(nullable: false),
InstanceUserRights = table.Column<decimal>(nullable: false),
ByondRights = table.Column<decimal>(nullable: false),
DreamDaemonRights = table.Column<decimal>(nullable: false),
DreamMakerRights = table.Column<decimal>(nullable: false),
RepositoryRights = table.Column<decimal>(nullable: false),
ChatBotRights = table.Column<decimal>(nullable: false),
ConfigurationRights = table.Column<decimal>(nullable: false),
InstanceId = table.Column<long>(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<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Description = table.Column<string>(nullable: false),
ErrorCode = table.Column<long>(nullable: true),
ExceptionDetails = table.Column<string>(nullable: true),
StartedAt = table.Column<DateTimeOffset>(nullable: false),
StoppedAt = table.Column<DateTimeOffset>(nullable: true),
Cancelled = table.Column<bool>(nullable: false),
CancelRightsType = table.Column<decimal>(nullable: true),
CancelRight = table.Column<decimal>(nullable: true),
StartedById = table.Column<long>(nullable: false),
CancelledById = table.Column<long>(nullable: true),
InstanceId = table.Column<long>(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<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
IrcChannel = table.Column<string>(maxLength: 100, nullable: true),
DiscordChannelId = table.Column<decimal>(nullable: true),
IsAdminChannel = table.Column<bool>(nullable: false),
IsWatchdogChannel = table.Column<bool>(nullable: false),
IsUpdatesChannel = table.Column<bool>(nullable: false),
Tag = table.Column<string>(maxLength: 10000, nullable: true),
ChatSettingsId = table.Column<long>(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<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Number = table.Column<int>(nullable: false),
PullRequestRevision = table.Column<string>(maxLength: 40, nullable: false),
Comment = table.Column<string>(maxLength: 10000, nullable: true),
TitleAtMerge = table.Column<string>(nullable: false),
BodyAtMerge = table.Column<string>(nullable: false),
Url = table.Column<string>(nullable: false),
Author = table.Column<string>(nullable: false),
MergedAt = table.Column<DateTimeOffset>(nullable: false),
MergedById = table.Column<long>(nullable: false),
PrimaryRevisionInformationId = table.Column<long>(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<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
DmeName = table.Column<string>(nullable: false),
Output = table.Column<string>(nullable: false),
DirectoryName = table.Column<Guid>(nullable: false),
MinimumSecurityLevel = table.Column<int>(nullable: false),
JobId = table.Column<long>(nullable: false),
RevisionInformationId = table.Column<long>(nullable: false),
ByondVersion = table.Column<string>(nullable: false),
DMApiMajorVersion = table.Column<int>(nullable: true),
DMApiMinorVersion = table.Column<int>(nullable: true),
DMApiPatchVersion = table.Column<int>(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<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
TestMergeId = table.Column<long>(nullable: false),
RevisionInformationId = table.Column<long>(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<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
AccessIdentifier = table.Column<string>(nullable: false),
ProcessId = table.Column<int>(nullable: false),
IsPrimary = table.Column<bool>(nullable: false),
Port = table.Column<int>(nullable: false),
RebootState = table.Column<int>(nullable: false),
LaunchSecurityLevel = table.Column<int>(nullable: false),
CompileJobId = table.Column<long>(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<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
AlphaIsActive = table.Column<bool>(nullable: false),
InstanceId = table.Column<long>(nullable: false),
AlphaId = table.Column<long>(nullable: true),
BravoId = table.Column<long>(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);
}
/// <inheritdoc />
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");
}
}
}
@@ -0,0 +1,814 @@
// <auto-generated />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ChannelLimit")
.HasColumnType("integer");
b.Property<string>("ConnectionString")
.IsRequired()
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<bool?>("Enabled")
.HasColumnType("boolean");
b.Property<long>("InstanceId")
.HasColumnType("bigint");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("character varying(100)")
.HasMaxLength(100);
b.Property<int>("Provider")
.HasColumnType("integer");
b.Property<long>("ReconnectionInterval")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("InstanceId", "Name")
.IsUnique();
b.ToTable("ChatBots");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<long>("ChatSettingsId")
.HasColumnType("bigint");
b.Property<decimal?>("DiscordChannelId")
.HasColumnType("numeric(20,0)");
b.Property<string>("IrcChannel")
.HasColumnType("character varying(100)")
.HasMaxLength(100);
b.Property<bool?>("IsAdminChannel")
.IsRequired()
.HasColumnType("boolean");
b.Property<bool?>("IsUpdatesChannel")
.IsRequired()
.HasColumnType("boolean");
b.Property<bool?>("IsWatchdogChannel")
.IsRequired()
.HasColumnType("boolean");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("ByondVersion")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("DMApiMajorVersion")
.HasColumnType("integer");
b.Property<int?>("DMApiMinorVersion")
.HasColumnType("integer");
b.Property<int?>("DMApiPatchVersion")
.HasColumnType("integer");
b.Property<Guid?>("DirectoryName")
.IsRequired()
.HasColumnType("uuid");
b.Property<string>("DmeName")
.IsRequired()
.HasColumnType("text");
b.Property<long>("JobId")
.HasColumnType("bigint");
b.Property<int>("MinimumSecurityLevel")
.HasColumnType("integer");
b.Property<string>("Output")
.IsRequired()
.HasColumnType("text");
b.Property<long>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<bool?>("AllowWebClient")
.IsRequired()
.HasColumnType("boolean");
b.Property<bool?>("AutoStart")
.IsRequired()
.HasColumnType("boolean");
b.Property<long>("HeartbeatSeconds")
.HasColumnType("bigint");
b.Property<long>("InstanceId")
.HasColumnType("bigint");
b.Property<int>("PrimaryPort")
.HasColumnType("integer");
b.Property<int>("SecondaryPort")
.HasColumnType("integer");
b.Property<int>("SecurityLevel")
.HasColumnType("integer");
b.Property<long>("StartupTimeout")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("InstanceId")
.IsUnique();
b.ToTable("DreamDaemonSettings");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ApiValidationPort")
.HasColumnType("integer");
b.Property<int>("ApiValidationSecurityLevel")
.HasColumnType("integer");
b.Property<long>("InstanceId")
.HasColumnType("bigint");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<long?>("AlphaId")
.HasColumnType("bigint");
b.Property<bool>("AlphaIsActive")
.HasColumnType("boolean");
b.Property<long?>("BravoId")
.HasColumnType("bigint");
b.Property<long>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<long>("AutoUpdateInterval")
.HasColumnType("bigint");
b.Property<int>("ChatBotLimit")
.HasColumnType("integer");
b.Property<int>("ConfigurationType")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<bool?>("Online")
.IsRequired()
.HasColumnType("boolean");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Path")
.IsUnique();
b.ToTable("Instances");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<decimal>("ByondRights")
.HasColumnType("numeric(20,0)");
b.Property<decimal>("ChatBotRights")
.HasColumnType("numeric(20,0)");
b.Property<decimal>("ConfigurationRights")
.HasColumnType("numeric(20,0)");
b.Property<decimal>("DreamDaemonRights")
.HasColumnType("numeric(20,0)");
b.Property<decimal>("DreamMakerRights")
.HasColumnType("numeric(20,0)");
b.Property<long>("InstanceId")
.HasColumnType("bigint");
b.Property<decimal>("InstanceUserRights")
.HasColumnType("numeric(20,0)");
b.Property<decimal>("RepositoryRights")
.HasColumnType("numeric(20,0)");
b.Property<long?>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<decimal?>("CancelRight")
.HasColumnType("numeric(20,0)");
b.Property<decimal?>("CancelRightsType")
.HasColumnType("numeric(20,0)");
b.Property<bool?>("Cancelled")
.IsRequired()
.HasColumnType("boolean");
b.Property<long?>("CancelledById")
.HasColumnType("bigint");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("ErrorCode")
.HasColumnType("bigint");
b.Property<string>("ExceptionDetails")
.HasColumnType("text");
b.Property<long>("InstanceId")
.HasColumnType("bigint");
b.Property<DateTimeOffset?>("StartedAt")
.IsRequired()
.HasColumnType("timestamp with time zone");
b.Property<long>("StartedById")
.HasColumnType("bigint");
b.Property<DateTimeOffset?>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("AccessIdentifier")
.IsRequired()
.HasColumnType("text");
b.Property<long>("CompileJobId")
.HasColumnType("bigint");
b.Property<bool>("IsPrimary")
.HasColumnType("boolean");
b.Property<int>("LaunchSecurityLevel")
.HasColumnType("integer");
b.Property<int>("Port")
.HasColumnType("integer");
b.Property<int>("ProcessId")
.HasColumnType("integer");
b.Property<int>("RebootState")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("CompileJobId");
b.ToTable("ReattachInformations");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("AccessToken")
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<string>("AccessUser")
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<bool?>("AutoUpdatesKeepTestMerges")
.IsRequired()
.HasColumnType("boolean");
b.Property<bool?>("AutoUpdatesSynchronize")
.IsRequired()
.HasColumnType("boolean");
b.Property<string>("CommitterEmail")
.IsRequired()
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<string>("CommitterName")
.IsRequired()
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<long>("InstanceId")
.HasColumnType("bigint");
b.Property<bool?>("PostTestMergeComment")
.IsRequired()
.HasColumnType("boolean");
b.Property<bool?>("PushTestMergeCommits")
.IsRequired()
.HasColumnType("boolean");
b.Property<bool?>("ShowTestMergeCommitters")
.IsRequired()
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("InstanceId")
.IsUnique();
b.ToTable("RepositorySettings");
});
modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<long>("RevisionInformationId")
.HasColumnType("bigint");
b.Property<long>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("CommitSha")
.IsRequired()
.HasColumnType("character varying(40)")
.HasMaxLength(40);
b.Property<long>("InstanceId")
.HasColumnType("bigint");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Author")
.IsRequired()
.HasColumnType("text");
b.Property<string>("BodyAtMerge")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Comment")
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<DateTimeOffset>("MergedAt")
.HasColumnType("timestamp with time zone");
b.Property<long>("MergedById")
.HasColumnType("bigint");
b.Property<int>("Number")
.HasColumnType("integer");
b.Property<long?>("PrimaryRevisionInformationId")
.IsRequired()
.HasColumnType("bigint");
b.Property<string>("PullRequestRevision")
.IsRequired()
.HasColumnType("character varying(40)")
.HasMaxLength(40);
b.Property<string>("TitleAtMerge")
.IsRequired()
.HasColumnType("text");
b.Property<string>("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<long?>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<decimal>("AdministrationRights")
.HasColumnType("numeric(20,0)");
b.Property<string>("CanonicalName")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset?>("CreatedAt")
.IsRequired()
.HasColumnType("timestamp with time zone");
b.Property<long?>("CreatedById")
.HasColumnType("bigint");
b.Property<bool?>("Enabled")
.IsRequired()
.HasColumnType("boolean");
b.Property<decimal>("InstanceManagerRights")
.HasColumnType("numeric(20,0)");
b.Property<DateTimeOffset?>("LastPasswordUpdate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("character varying(10000)")
.HasMaxLength(10000);
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("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
}
}
}
@@ -9,17 +9,17 @@ using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.Database
{
/// <summary>
/// <see cref="DatabaseContext{TParentContext}"/> for MySQL
/// <see cref="DatabaseContext"/> for MySQL
/// </summary>
sealed class MySqlDatabaseContext : DatabaseContext<MySqlDatabaseContext>
sealed class MySqlDatabaseContext : DatabaseContext
{
/// <summary>
/// Construct a <see cref="MySqlDatabaseContext"/>
/// </summary>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="DatabaseContext"/></param>
public MySqlDatabaseContext(DbContextOptions<MySqlDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<MySqlDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
{ }
@@ -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
{
/// <summary>
/// <see cref="DatabaseContext"/> for PostgresSQL.
/// </summary>
sealed class PostgresSqlDatabaseContext : DatabaseContext
{
/// <summary>
/// Construct a <see cref="SqlServerDatabaseContext"/>
/// </summary>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="DatabaseContext"/></param>
public PostgresSqlDatabaseContext(
DbContextOptions<PostgresSqlDatabaseContext> dbContextOptions,
IOptions<DatabaseConfiguration> databaseConfiguration,
IDatabaseSeeder databaseSeeder,
ILogger<PostgresSqlDatabaseContext> logger)
: base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
{ }
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
base.OnConfiguring(options);
options.UseNpgsql(DatabaseConfiguration.ConnectionString, x => x.EnableRetryOnFailure());
}
/// <inheritdoc />
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!");
}
}
}
@@ -7,17 +7,17 @@ using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.Database
{
/// <summary>
/// <see cref="DatabaseContext{TParentContext}"/> for Sqlserver
/// <see cref="DatabaseContext"/> for Sqlserver
/// </summary>
sealed class SqlServerDatabaseContext : DatabaseContext<SqlServerDatabaseContext>
sealed class SqlServerDatabaseContext : DatabaseContext
{
/// <summary>
/// Construct a <see cref="SqlServerDatabaseContext"/>
/// </summary>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="DatabaseContext"/></param>
public SqlServerDatabaseContext(DbContextOptions<SqlServerDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<SqlServerDatabaseContext> 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());
}
/// <inheritdoc />
@@ -9,17 +9,17 @@ using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.Database
{
/// <summary>
/// <see cref="DatabaseContext{TParentContext}"/> for MySQL
/// <see cref="DatabaseContext"/> for MySQL
/// </summary>
sealed class SqliteDatabaseContext : DatabaseContext<SqliteDatabaseContext>
sealed class SqliteDatabaseContext : DatabaseContext
{
/// <summary>
/// Construct a <see cref="MySqlDatabaseContext"/>
/// </summary>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext"/></param>
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="DatabaseContext"/></param>
public SqliteDatabaseContext(DbContextOptions<SqliteDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<SqliteDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
{ }
@@ -107,5 +107,32 @@ namespace Tgstation.Server.Host.Extensions
}
});
}
/// <summary>
/// Add middleware for logging the request number.
/// </summary>
/// <param name="applicationBuilder">The <see cref="IApplicationBuilder"/> to configure.</param>
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);
}
});
}
}
}
@@ -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);
+7 -1
View File
@@ -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);
@@ -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()
};
}
@@ -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)
+4 -1
View File
@@ -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);
+12 -1
View File
@@ -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<IServerPortProvider>();
kestrelOptions.ListenAnyIP(
serverPortProvider.HttpApiPort,
listenOptions => listenOptions.Protocols = HttpProtocols.Http1AndHttp2);
})
.UseIIS()
.UseIISIntegration()
.UseApplication(postSetupServices)
.SuppressStatusMessages(true)
.UseShutdownTimeout(TimeSpan.FromMinutes(1)));
+122 -84
View File
@@ -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
/// <summary>
/// Prompt the user for the <see cref="DatabaseType"/>.
/// </summary>
/// <param name="firstTime">If this is the user's first time here.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the input <see cref="DatabaseType"/>.</returns>
async Task<DatabaseType> PromptDatabaseType(CancellationToken cancellationToken)
async Task<DatabaseType> 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
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the new <see cref="DatabaseConfiguration"/></returns>
#pragma warning disable CA1502 // TODO: Decomplexify
async Task<DatabaseConfiguration> 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 [<server>:<port> or <server>] (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, @"^(?<server>.+):(?<port>.+)$");
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
/// <summary>
/// Prompts the user to create a <see cref="GeneralConfiguration"/>
@@ -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<string, object>()
{
{ 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);
@@ -48,25 +48,26 @@
<PackageReference Include="Cyberboss.SmartIrc4net.Standard" Version="0.4.6" />
<PackageReference Include="Discord.Net.WebSocket" Version="2.2.0" />
<PackageReference Include="LibGit2Sharp" Version="0.27.0-preview-0034" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.4" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.3">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.8" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.3" />
<PackageReference Include="Octokit" Version="0.47.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
<!-- If this is updated, be sure to update the reference in the README.md -->
@@ -82,10 +83,10 @@
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="5.4.1" />
<PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="4.7.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.5.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.6.0" />
<PackageReference Include="System.Management" Version="4.7.0" />
<PackageReference Include="Wangkanai.Detection.Browser" Version="2.0.0" />
<PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="3.0.49" />
<PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="3.0.50" />
</ItemGroup>
<ItemGroup>
+2 -8
View File
@@ -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": {
+4
View File
@@ -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)
+13 -5
View File
@@ -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()
@@ -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<ArgumentNullException>(() => new DiscordProvider(null, null, 1));
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(null, null, null, 1));
var mockAss = new Mock<IAssemblyInformationProvider>();
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockAss.Object, null, null, 1));
var mockLogger = new Mock<ILogger<DiscordProvider>>();
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockLogger.Object, null, 1));
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockAss.Object, mockLogger.Object, null, 1));
var mockToken = "asdf";
Assert.ThrowsException<ArgumentOutOfRangeException>(() => new DiscordProvider(mockLogger.Object, mockToken, 0));
new DiscordProvider(mockLogger.Object, mockToken, 1).Dispose();
Assert.ThrowsException<ArgumentOutOfRangeException>(() => 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<ILogger<DiscordProvider>>();
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<IAssemblyInformationProvider>(), 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<ILogger<DiscordProvider>>();
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<IAssemblyInformationProvider>(), 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<OperationCanceledException>(() => provider.Connect(cancellationToken)).ConfigureAwait(false);
Assert.IsFalse(provider.Connected);
Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false));
Assert.IsTrue(provider.Connected);
await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => 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<OperationCanceledException>(() => provider.Connect(cancellationToken)).ConfigureAwait(false);
Assert.IsFalse(provider.Connected);
Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false));
Assert.IsTrue(provider.Connected);
await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => provider.Disconnect(cancellationToken)).ConfigureAwait(false);
Assert.IsTrue(provider.Connected);
await provider.Disconnect(default).ConfigureAwait(false);
Assert.IsFalse(provider.Connected);
}
}
}
@@ -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<Commit>();
using var repo = await TestRepoLoading(tempDir);
var gitObject = repo.Lookup("f636418bf47d238d33b0e4a34f0072b23a8aad0e");
Assert.IsNotNull(gitObject);
var commit = gitObject.Peel<Commit>();
Assert.AreEqual("Update Test.md", commit.Message);
}
Assert.AreEqual("Update Test.md", commit.Message);
}
finally
{
@@ -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<TaskCanceledException>(() => delayer.Delay(TimeSpan.FromSeconds(1), cts.Token)).ConfigureAwait(false);
}
using var cts = new CancellationTokenSource();
cts.Cancel();
await Assert.ThrowsExceptionAsync<TaskCanceledException>(() => delayer.Delay(TimeSpan.FromSeconds(1), cts.Token)).ConfigureAwait(false);
}
}
}
@@ -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);
@@ -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<DreamDaemon> DeployTestDme(string dmeName, DreamDaemonSecurity deploymentSecurity, CancellationToken cancellationToken)
@@ -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
@@ -72,7 +72,7 @@ namespace Tgstation.Server.Tests
var args = new List<string>()
{
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),