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