Merge pull request #1045 from tgstation/MovingOnUp

Various fixes
This commit is contained in:
Jordan Brown
2020-06-08 14:47:09 -04:00
committed by GitHub
36 changed files with 503 additions and 412 deletions
+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 -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; }
}
@@ -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
@@ -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>
@@ -123,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)
{
@@ -326,6 +326,7 @@ namespace Tgstation.Server.Host.Components.Session
var sessionController = new SessionController(
reattachInformation,
instance,
process,
byondLock,
byondTopicSender,
@@ -391,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);
}
}
@@ -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;
@@ -161,26 +162,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>
@@ -475,7 +475,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
@@ -582,7 +582,7 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// Get a specific <see cref="Api.Models.Instance"/>.
/// </summary>
/// <param name="id">The <see cref="Api.Models.Instance.Id"/> to retrieve.</param>
/// <param name="id">The instance <see cref="EntityId.Id"/> to retrieve.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the request.</returns>
/// <response code="200">Retrieved <see cref="Api.Models.Instance"/> successfully.</response>
@@ -133,10 +133,12 @@ namespace Tgstation.Server.Host.Core
var logEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.LogLevel);
var formatter = new MessageTemplateTextFormatter(
"{Timestamp:o} {RequestId,13} [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}",
"{Timestamp:o} "
+ ServiceCollectionExtensions.SerilogContextTemplate
+ ": [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}",
null);
logPath = IOManager.ConcatPath(logPath, "tgs-{Date}.log");
logPath = IOManager.ConcatPath(logPath, "tgs-.log");
var rollingFileConfig = sinkConfig.File(
formatter,
logPath,
@@ -167,6 +169,7 @@ namespace Tgstation.Server.Host.Core
};
});
// WARNING: STATIC CODE
// fucking prevents converting 'sub' to M$ bs
// can't be done in the above lambda, that's too late
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
@@ -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
@@ -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; }
@@ -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>
@@ -68,7 +68,7 @@
<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="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" />