mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-30 16:39:21 +01:00
More CI cleanups
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user