More CI cleanups

This commit is contained in:
Jordan Brown
2020-07-25 12:59:47 -04:00
parent 5ac7ad2131
commit b158bfbd1e
23 changed files with 409 additions and 234 deletions
@@ -131,13 +131,13 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
Mention = NormalizeMentions(e.Author.Mention)
}
};
EnqueueMessage(result);
}
/// <inheritdoc />
protected override async Task Connect(CancellationToken cancellationToken)
{
Logger.LogTrace("Connecting...");
try
{
await client.LoginAsync(TokenType.Bot, BotToken, true).ConfigureAwait(false);
@@ -150,14 +150,22 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
Logger.LogTrace("Started.");
var channelsAvailable = new TaskCompletionSource<object>();
client.Ready += () =>
Task ReadyCallback()
{
channelsAvailable.TrySetResult(null);
return Task.CompletedTask;
};
using (cancellationToken.Register(() => channelsAvailable.SetCanceled()))
await channelsAvailable.Task.ConfigureAwait(false);
Logger.LogDebug("Connection established!");
}
client.Ready += ReadyCallback;
try
{
using (cancellationToken.Register(() => channelsAvailable.SetCanceled()))
await channelsAvailable.Task.ConfigureAwait(false);
}
finally
{
client.Ready -= ReadyCallback;
}
}
catch (OperationCanceledException)
{
@@ -172,13 +180,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
protected override async Task DisconnectImpl(CancellationToken cancellationToken)
{
Logger.LogTrace("Disconnecting...");
if (!Connected)
{
Logger.LogTrace("Already disconnected not doing disconnection attempt!");
return;
}
try
{
cancellationToken.ThrowIfCancellationRequested();
@@ -254,7 +255,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
var channel = client.GetChannel(channelId) as IMessageChannel;
await (channel?.SendMessageAsync(message, false, null, new RequestOptions
{
CancelToken = cancellationToken
CancelToken = cancellationToken,
Timeout = 10000 // prevent stupid long hold ups from this
}) ?? Task.CompletedTask).ConfigureAwait(false);
}
catch (OperationCanceledException)
@@ -20,6 +20,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// </summary>
sealed class IrcProvider : Provider
{
/// <summary>
/// Number of seconds used for several IRC related timeouts.
/// </summary>
const int TimeoutSeconds = 5;
/// <inheritdoc />
@@ -149,7 +152,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync().ConfigureAwait(false);
await HardDisconnect().ConfigureAwait(false);
// DCT: None available
await HardDisconnect(default).ConfigureAwait(false);
}
/// <summary>
@@ -239,6 +244,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
cancellationToken.ThrowIfCancellationRequested();
Logger.LogTrace("Authenticating ({0})...", passwordType);
switch (passwordType)
{
case IrcPasswordType.Server:
@@ -259,24 +265,50 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
cancellationToken.ThrowIfCancellationRequested();
client.Listen(false);
Logger.LogTrace("Processing initial messages...");
await NonBlockingListen(cancellationToken).ConfigureAwait(false);
listenTask = Task.Factory.StartNew(() =>
var nickCheckCompleteTcs = new TaskCompletionSource<object>();
using (cancellationToken.Register(() => nickCheckCompleteTcs.TrySetCanceled()))
{
while (!disconnecting && client.IsConnected && client.Nickname != nickname)
listenTask = Task.Factory.StartNew(
async () =>
{
client.ListenOnce(true);
if (disconnecting || !client.IsConnected)
break;
client.Listen(false);
Logger.LogTrace("Entering nick check loop");
while (!disconnecting && client.IsConnected && client.Nickname != nickname)
{
client.ListenOnce(true);
if (disconnecting || !client.IsConnected)
break;
await NonBlockingListen(cancellationToken).ConfigureAwait(false);
// ensure we have the correct nick
if (client.GetIrcUser(nickname) == null)
client.RfcNick(nickname);
}
// ensure we have the correct nick
if (client.GetIrcUser(nickname) == null)
client.RfcNick(nickname);
}
client.Listen();
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
nickCheckCompleteTcs.TrySetResult(null);
Logger.LogTrace("Starting blocking listen...");
try
{
client.Listen();
}
catch (Exception ex)
{
Logger.LogWarning("IRC Listen Error: {0}", ex);
}
Logger.LogTrace("Exiting listening task...");
},
cancellationToken,
TaskCreationOptions.LongRunning,
TaskScheduler.Current);
await nickCheckCompleteTcs.Task.ConfigureAwait(false);
}
Logger.LogTrace("Connection established!");
}
catch (OperationCanceledException)
{
@@ -288,6 +320,28 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
}
/// <summary>
/// Perform a non-blocking <see cref="IrcConnection.Listen(bool)"/>.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task NonBlockingListen(CancellationToken cancellationToken) => Task.Factory.StartNew(
() =>
{
try
{
client.Listen(false);
}
catch (Exception ex)
{
Logger.LogWarning("IRC Listen Error: {0}", ex);
}
},
cancellationToken,
TaskCreationOptions.None,
TaskScheduler.Current)
.WithToken(cancellationToken);
/// <summary>
/// Run SASL authentication on <see cref="client"/>.
/// </summary>
@@ -297,6 +351,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
{
client.WriteLine("CAP REQ :sasl", Priority.Critical); // needs to be put in the buffer before anything else
cancellationToken.ThrowIfCancellationRequested();
Logger.LogTrace("Logging in...");
client.Login(nickname, nickname, 0, nickname);
cancellationToken.ThrowIfCancellationRequested();
@@ -312,69 +368,72 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
recievedPlus = true;
}
Logger.LogTrace("Performing handshake...");
client.OnReadLine += AuthenticationDelegate;
try
{
using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
timeoutCts.CancelAfter(TimeSpan.FromSeconds(TimeoutSeconds));
var timeoutToken = timeoutCts.Token;
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromSeconds(TimeoutSeconds));
var timeoutToken = timeoutCts.Token;
var listenTimeSpan = TimeSpan.FromMilliseconds(10);
for (; !recievedAck;
await asyncDelayer.Delay(listenTimeSpan, timeoutToken).ConfigureAwait(false))
client.Listen(false);
var listenTimeSpan = TimeSpan.FromMilliseconds(10);
for (; !recievedAck;
await asyncDelayer.Delay(listenTimeSpan, timeoutToken).ConfigureAwait(false))
await NonBlockingListen(cancellationToken).ConfigureAwait(false);
client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical);
timeoutToken.ThrowIfCancellationRequested();
client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical);
timeoutToken.ThrowIfCancellationRequested();
for (; !recievedPlus;
await asyncDelayer.Delay(listenTimeSpan, timeoutToken).ConfigureAwait(false))
client.Listen(false);
}
cancellationToken.ThrowIfCancellationRequested();
// Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196
var authString = String.Format(
CultureInfo.InvariantCulture,
"{0}{1}{0}{1}{2}",
nickname,
'\0',
password);
var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
var authLine = $"AUTHENTICATE {b64}";
client.WriteLine(authLine, Priority.Critical);
cancellationToken.ThrowIfCancellationRequested();
client.WriteLine("CAP END", Priority.Critical);
for (; !recievedPlus;
await asyncDelayer.Delay(listenTimeSpan, timeoutToken).ConfigureAwait(false))
await NonBlockingListen(cancellationToken).ConfigureAwait(false);
}
finally
{
client.OnReadLine -= AuthenticationDelegate;
}
cancellationToken.ThrowIfCancellationRequested();
// Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196
Logger.LogTrace("Sending credentials...");
var authString = String.Format(
CultureInfo.InvariantCulture,
"{0}{1}{0}{1}{2}",
nickname,
'\0',
password);
var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
var authLine = $"AUTHENTICATE {b64}";
client.WriteLine(authLine, Priority.Critical);
cancellationToken.ThrowIfCancellationRequested();
Logger.LogTrace("Finishing authentication...");
client.WriteLine("CAP END", Priority.Critical);
}
/// <inheritdoc />
protected override async Task DisconnectImpl(CancellationToken cancellationToken)
{
if (!Connected)
return;
try
{
await Task.Factory.StartNew(() =>
{
try
await Task.Factory.StartNew(
() =>
{
client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); // priocritical otherwise it wont go through
}
catch (Exception e)
{
Logger.LogWarning("Error quitting IRC: {0}", e);
}
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
await HardDisconnect().ConfigureAwait(false);
try
{
client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); // priocritical otherwise it wont go through
}
catch (Exception e)
{
Logger.LogWarning("Error quitting IRC: {0}", e);
}
},
cancellationToken,
TaskCreationOptions.LongRunning,
TaskScheduler.Current)
.ConfigureAwait(false);
await HardDisconnect(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -386,16 +445,42 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
}
async Task HardDisconnect()
async Task HardDisconnect(CancellationToken cancellationToken)
{
if (!Connected)
{
Logger.LogTrace("Not hard disconnecting, already offline");
return;
}
Logger.LogTrace("Hard disconnect");
disconnecting = true;
client.Disconnect();
if(listenTask != null)
await listenTask.ConfigureAwait(false);
// This call blocks permanently randomly sometimes
// Frankly I don't give a shit
var disconnectTask = Task.Factory.StartNew(
() =>
{
try
{
client.Disconnect();
}
catch (Exception e)
{
Logger.LogWarning("Error disconnecting IRC: {0}", e);
}
},
cancellationToken,
TaskCreationOptions.None,
TaskScheduler.Current);
await Task.WhenAny(
Task.WhenAll(
disconnectTask,
listenTask ?? Task.CompletedTask),
asyncDelayer.Delay(TimeSpan.FromSeconds(TimeoutSeconds), cancellationToken))
.ConfigureAwait(false);
}
/// <inheritdoc />
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
@@ -119,8 +120,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
public async Task Disconnect(CancellationToken cancellationToken)
{
if(Connected)
if (Connected)
{
await DisconnectImpl(cancellationToken).ConfigureAwait(false);
Logger.LogTrace("Disconnected");
}
await StopReconnectionTimer().ConfigureAwait(false);
}
@@ -130,16 +135,17 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
public async Task<Message> NextMessage(CancellationToken cancellationToken)
{
var cancelTcs = new TaskCompletionSource<object>();
using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
await Task.WhenAny(nextMessage.Task, cancelTcs.Task).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
lock (messageQueue)
while (true)
{
var result = messageQueue.Dequeue();
if (messageQueue.Count == 0)
nextMessage = new TaskCompletionSource<object>();
return result;
await nextMessage.Task.WithToken(cancellationToken).ConfigureAwait(false);
lock (messageQueue)
if (messageQueue.Count > 0)
{
var result = messageQueue.Dequeue();
if (messageQueue.Count == 0)
nextMessage = new TaskCompletionSource<object>();
return result;
}
}
}
@@ -211,8 +217,17 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
job,
async (core, databaseContextFactory, paramJob, progressReporter, jobCancellationToken) =>
{
await DisconnectImpl(jobCancellationToken).ConfigureAwait(false);
if (Connected)
{
Logger.LogTrace("Disconnecting...");
await DisconnectImpl(jobCancellationToken).ConfigureAwait(false);
}
else
Logger.LogTrace("Already disconnected not doing disconnection attempt!");
Logger.LogTrace("Connecting...");
await Connect(jobCancellationToken).ConfigureAwait(false);
Logger.LogTrace("Connected successfully");
EnqueueMessage(null);
},
cancellationToken)
@@ -1,4 +1,4 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
@@ -202,26 +202,32 @@ namespace Tgstation.Server.Host.Components.Deployment
if (compileJob == null)
throw new ArgumentNullException(nameof(compileJob));
// ensure we have the entire compile job tree
// ensure we have the entire metadata tree
logger.LogTrace("Loading compile job {0}...", compileJob.Id);
await databaseContextFactory.UseContext(
async db => compileJob = await db
.CompileJobs
.AsQueryable()
.Where(x => x.Id == compileJob.Id)
.Include(x => x.Job).ThenInclude(x => x.StartedBy)
.Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
.Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
.Include(x => x.Job)
.ThenInclude(x => x.StartedBy)
.Include(x => x.RevisionInformation)
.ThenInclude(x => x.PrimaryTestMerge)
.ThenInclude(x => x.MergedBy)
.Include(x => x.RevisionInformation)
.ThenInclude(x => x.ActiveTestMerges)
.ThenInclude(x => x.TestMerge)
.ThenInclude(x => x.MergedBy)
.FirstAsync(cancellationToken)
.ConfigureAwait(false))
.ConfigureAwait(false); // can't wait to see that query
if (!compileJob.Job.StoppedAt.HasValue)
{
// This happens if we're told to load the compile job that is currently finished up
// It can constitute an API violation if it's returned by the DreamDaemonController so just set it here
// Bit of a hack, but it should work out to be the same value
logger.LogTrace("Setting missing StoppedAt for CompileJob job...");
// This happens when we're told to load the compile job that is currently finished up
// It constitutes an API violation if it's returned by the DreamDaemonController so just set it here
// Bit of a hack, but it works out to be nearly if not the same value that's put in the DB
logger.LogTrace("Setting missing StoppedAt for CompileJob.Job #{0}...", compileJob.Job.Id);
compileJob.Job.StoppedAt = DateTimeOffset.Now;
}
@@ -284,9 +290,9 @@ namespace Tgstation.Server.Host.Components.Deployment
else
jobLockCounts[compileJob.Id] = ++value;
logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value);
providerSubmitted = true;
logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value);
return newProvider;
}
}
@@ -1,4 +1,4 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Octokit;
using System;
@@ -633,6 +633,7 @@ namespace Tgstation.Server.Host.Components.Deployment
// The difficulty with compile jobs is they have a two part commit
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
logger.LogTrace("Created CompileJob {0}", compileJob.Id);
try
{
await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false);
@@ -471,7 +471,7 @@ namespace Tgstation.Server.Host.Components
// race condition, just quit
if (timerTask != null)
{
logger.LogDebug("Aborting auto update interval change due to race condition!");
logger.LogWarning("Aborting auto update interval change due to race condition!");
return;
}
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using System;
using System.Globalization;
using System.Linq;
@@ -116,10 +116,11 @@ namespace Tgstation.Server.Host.Components.Session
/// Check if a given <paramref name="port"/> can be bound to.
/// </summary>
/// <param name="port">The port number to test.</param>
static void PortBindTest(ushort port)
void PortBindTest(ushort port)
{
try
{
logger.LogTrace("Bind test: {0}", port);
SocketExtensions.BindTest(port, false);
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
@@ -159,12 +159,12 @@ namespace Tgstation.Server.Host.Components.Watchdog
protected override async Task DisposeAndNullControllersImpl()
{
var disposeTask = Server?.DisposeAsync();
gracefulRebootRequired = false;
if (!disposeTask.HasValue)
return;
await disposeTask.Value.ConfigureAwait(false);
Server = null;
gracefulRebootRequired = false;
}
/// <inheritdoc />
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -56,11 +56,6 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="InstanceController"/>
/// </summary>
readonly IAssemblyInformationProvider assemblyInformationProvider;
/// <summary>
/// The <see cref="IPlatformIdentifier"/> for the <see cref="InstanceController"/>
/// </summary>
@@ -79,7 +74,6 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/></param>
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/></param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
@@ -89,7 +83,6 @@ namespace Tgstation.Server.Host.Controllers
IJobManager jobManager,
IInstanceManager instanceManager,
IIOManager ioManager,
IAssemblyInformationProvider assemblyInformationProvider,
IPlatformIdentifier platformIdentifier,
IOptions<GeneralConfiguration> generalConfigurationOptions,
ILogger<InstanceController> logger)
@@ -102,11 +95,51 @@ namespace Tgstation.Server.Host.Controllers
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
}
Models.Instance CreateDefaultInstance(Api.Models.Instance initialSettings)
=> new Models.Instance
{
ConfigurationType = initialSettings.ConfigurationType ?? ConfigurationType.Disallowed,
DreamDaemonSettings = new DreamDaemonSettings
{
AllowWebClient = false,
AutoStart = false,
Port = 1337,
SecurityLevel = DreamDaemonSecurity.Safe,
StartupTimeout = 60,
HeartbeatSeconds = 60,
TopicRequestTimeout = generalConfiguration.ByondTopicTimeout
},
DreamMakerSettings = new DreamMakerSettings
{
ApiValidationPort = 1339,
ApiValidationSecurityLevel = DreamDaemonSecurity.Safe,
RequireDMApiValidation = true
},
Name = initialSettings.Name,
Online = false,
Path = initialSettings.Path,
AutoUpdateInterval = initialSettings.AutoUpdateInterval ?? 0,
ChatBotLimit = initialSettings.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit,
RepositorySettings = new RepositorySettings
{
CommitterEmail = "tgstation-server@users.noreply.github.com",
CommitterName = "tgstation-server",
PushTestMergeCommits = false,
ShowTestMergeCommitters = false,
AutoUpdatesKeepTestMerges = false,
AutoUpdatesSynchronize = false,
PostTestMergeComment = false
},
InstanceUsers = new List<Models.InstanceUser> // give this user full privileges on the instance
{
InstanceAdminUser(null)
}
};
string NormalizePath(string path)
{
if (path == null)
@@ -243,45 +276,7 @@ namespace Tgstation.Server.Host.Controllers
else
attached = true;
var newInstance = new Models.Instance
{
ConfigurationType = model.ConfigurationType ?? ConfigurationType.Disallowed,
DreamDaemonSettings = new DreamDaemonSettings
{
AllowWebClient = false,
AutoStart = false,
Port = 1337,
SecurityLevel = DreamDaemonSecurity.Safe,
StartupTimeout = 60,
HeartbeatSeconds = 60,
TopicRequestTimeout = generalConfiguration.ByondTopicTimeout
},
DreamMakerSettings = new DreamMakerSettings
{
ApiValidationPort = 1339,
ApiValidationSecurityLevel = DreamDaemonSecurity.Safe,
RequireDMApiValidation = true
},
Name = model.Name,
Online = false,
Path = model.Path,
AutoUpdateInterval = model.AutoUpdateInterval ?? 0,
ChatBotLimit = model.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit,
RepositorySettings = new RepositorySettings
{
CommitterEmail = "tgstation-server@users.noreply.github.com",
CommitterName = assemblyInformationProvider.VersionPrefix,
PushTestMergeCommits = false,
ShowTestMergeCommitters = false,
AutoUpdatesKeepTestMerges = false,
AutoUpdatesSynchronize = false,
PostTestMergeComment = false
},
InstanceUsers = new List<Models.InstanceUser> // give this user full privileges on the instance
{
InstanceAdminUser(null)
}
};
var newInstance = CreateDefaultInstance(model);
DatabaseContext.Instances.Add(newInstance);
try
@@ -1,4 +1,4 @@
using Cyberboss.AspNetCore.AsyncInitializer;
using Cyberboss.AspNetCore.AsyncInitializer;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
@@ -98,6 +98,10 @@ namespace Tgstation.Server.Host.Core
// enable options which give us config reloading
services.AddOptions();
// Set the timeout for IHostedService.StopAsync
services.Configure<HostOptions>(
opts => opts.ShutdownTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.RestartTimeout));
static LogEventLevel? ConvertSeriLogLevel(LogLevel logLevel) =>
logLevel switch
{
@@ -441,4 +445,4 @@ namespace Tgstation.Server.Host.Core
// 404 anything that gets this far
}
}
}
}
@@ -1,4 +1,4 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
using System;
namespace Tgstation.Server.Host.Database.Migrations
@@ -55,8 +55,7 @@ namespace Tgstation.Server.Host.Database.Migrations
migrationBuilder.AddColumn<string>(
name: "ServerCommandsJson",
table: "ReattachInformations",
nullable: false,
defaultValue: "server_commands.tgs.json");
nullable: false);
}
}
}
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
+4 -6
View File
@@ -111,6 +111,7 @@ namespace Tgstation.Server.Host.Jobs
await activationTcs.Task.WithToken(cancellationToken).ConfigureAwait(false);
logger.LogTrace("Starting job...");
await operation(
instanceCoreProvider.Value.GetInstance(oldJob.Instance),
databaseContextFactory,
@@ -201,7 +202,7 @@ namespace Tgstation.Server.Host.Jobs
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description);
logger.LogDebug("Registering job {0}: {1}...", job.Id, job.Description);
var jobHandler = new JobHandler(jobCancellationToken => RunJob(job, operation, jobCancellationToken));
try
{
@@ -283,15 +284,12 @@ namespace Tgstation.Server.Host.Jobs
await databaseContextFactory.UseContext(async databaseContext =>
{
if (user == null)
{
user = await databaseContext.Users.GetTgsUser(cancellationToken).ConfigureAwait(false);
databaseContext.Users.Attach(user);
}
var updatedJob = new Job { Id = job.Id };
databaseContext.Jobs.Attach(job);
databaseContext.Jobs.Attach(updatedJob);
var attachedUser = new User { Id = user.Id };
databaseContext.Users.Attach(user);
databaseContext.Users.Attach(attachedUser);
updatedJob.CancelledBy = attachedUser;
// let either startup or cancellation set job.cancelled
+22 -22
View File
@@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -290,29 +290,29 @@ namespace Tgstation.Server.Host
}
if (exception == null)
using (var cts = new CancellationTokenSource())
{
logger.LogInformation("Restarting server...");
var cancellationToken = cts.Token;
var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken)).ToList());
{
logger.LogInformation("Restarting server...");
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(generalConfiguration.RestartTimeout));
var cancellationToken = cts.Token;
var eventsTask = Task.WhenAll(
restartHandlers.Select(
x => x.HandleRestart(newVersion, cancellationToken))
.ToList());
var expiryTask = Task.Delay(TimeSpan.FromMilliseconds(generalConfiguration.RestartTimeout));
await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false);
logger.LogTrace("Joining restart handlers...");
cts.Cancel();
try
{
await eventsTask.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
logger.LogError("Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!");
}
catch (Exception e)
{
logger.LogError("Restart handlers error! Exception: {0}", e);
}
logger.LogTrace("Joining restart handlers...");
try
{
await eventsTask.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
logger.LogError("Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!");
}
catch (Exception e)
{
logger.LogError("Restart handlers error! Exception: {0}", e);
}
}
logger.LogTrace("Stopping host...");
cancellationTokenSource.Cancel();
+27 -5
View File
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Text;
@@ -10,6 +10,11 @@ namespace Tgstation.Server.Host.System
/// <inheritdoc />
sealed class Process : IProcess
{
/// <summary>
/// Maximum time to wait in a call to <see cref="global::System.Diagnostics.Process.WaitForExit(int)"/>.
/// </summary>
const int MaximumWaitMilliseconds = 30000;
/// <inheritdoc />
public int Id { get; }
@@ -31,6 +36,11 @@ namespace Tgstation.Server.Host.System
readonly global::System.Diagnostics.Process handle;
/// <summary>
/// A <see cref="TaskCompletionSource{TResult}"/> so that we can complete <see cref="Lifetime"/> if the <see cref="handle"/> becomes unresponsive.
/// </summary>
readonly TaskCompletionSource<object> emergencyLifetimeTcs;
readonly StringBuilder outputStringBuilder;
readonly StringBuilder errorStringBuilder;
readonly StringBuilder combinedStringBuilder;
@@ -65,6 +75,7 @@ namespace Tgstation.Server.Host.System
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
emergencyLifetimeTcs = new TaskCompletionSource<object>();
Lifetime = WrapLifetimeTask(lifetime ?? throw new ArgumentNullException(nameof(lifetime)));
Id = handle.Id;
@@ -92,9 +103,12 @@ namespace Tgstation.Server.Host.System
async Task<int> WrapLifetimeTask(Task<int> lifetimeTask)
{
var result = await lifetimeTask.ConfigureAwait(false);
logger.LogTrace("PID {0} ended with code {1}", Id, result);
return result;
await Task.WhenAny(lifetimeTask, emergencyLifetimeTcs.Task).ConfigureAwait(false);
if (lifetimeTask.IsCompleted)
return await lifetimeTask.ConfigureAwait(false);
logger.LogTrace("Using exit code -1 for hung PID {0}.", Id);
return -1;
}
/// <inheritdoc />
@@ -130,7 +144,15 @@ namespace Tgstation.Server.Host.System
{
logger.LogTrace("Terminating PID {0}...", Id);
handle.Kill();
handle.WaitForExit();
if (!handle.WaitForExit(MaximumWaitMilliseconds))
{
logger.LogError(
"PID {0} hasn't exited in {1} seconds! This may cause issues with port reuse.",
Id,
TimeSpan.FromMilliseconds(MaximumWaitMilliseconds).TotalSeconds);
emergencyLifetimeTcs.TrySetResult(null);
}
}
catch (Exception e)
{
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using System;
using System.Text;
using System.Threading.Tasks;
@@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.System
/// </summary>
/// <param name="handle">The <see cref="global::System.Diagnostics.Process"/> to attach the <see cref="Task{TResult}"/> for</param>
/// <returns>A new <see cref="Task{TResult}"/> resulting in the exit code of <paramref name="handle"/></returns>
static Task<int> AttachExitHandler(global::System.Diagnostics.Process handle)
Task<int> AttachExitHandler(global::System.Diagnostics.Process handle)
{
handle.EnableRaisingEvents = true;
var tcs = new TaskCompletionSource<int>();
@@ -45,7 +45,8 @@ namespace Tgstation.Server.Host.System
}
// Try because this can be invoked twice for weird reasons
tcs.TrySetResult(exitCode);
if (tcs.TrySetResult(exitCode))
logger.LogTrace("Process exit event completed");
};
return tcs.Task;
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../../build/Version.props" />
<PropertyGroup>
@@ -69,7 +69,7 @@
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.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" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.2" />
<!-- If this is updated, be sure to update the reference in the README.md -->
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />