Merge branch 'dev' into 1037-FixAPI

This commit is contained in:
Jordan Brown
2020-06-09 20:42:43 -04:00
committed by GitHub
59 changed files with 1555 additions and 527 deletions
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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)).
+1 -1
View File
@@ -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()
+2 -2
View File
@@ -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 -1
View File
@@ -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
{
+1 -1
View File
@@ -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>
+1 -6
View File
@@ -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>
+1 -1
View File
@@ -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>
+8 -8
View File
@@ -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>
+10 -3
View File
@@ -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);
@@ -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(
@@ -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");
}
}
}
@@ -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
}
}
@@ -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);
});
+57 -55
View File
@@ -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 -1
View File
@@ -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.
+1 -1
View File
@@ -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; }
+2 -2
View File
@@ -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>
+1 -1
View File
@@ -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(
+16 -8
View File
@@ -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" />
+1 -1
View File
@@ -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!");
}