Chungus refactors

- Added a new ErrorCode for when chat bots fail to connect
- Made IProvider IAsyncDispsable
- Removed IDatabaseContextFactory dependency from watchdogs
- Jobs will no longer start until instance manager startup is complete
- Chat provider connection is now done as a job, there is no way to retrieve directly this job through the API currently
- Fixed disabled chat bots connecting when their instance onlined
- Fixed issue with chat provider reconnection channel remapping
- IJobManager can now accept a null user in most cases which it will substitute for the TGS user
- Fixed some watchdog issues relating to accessing disposed session controllers
This commit is contained in:
Jordan Brown
2020-07-19 17:27:47 -04:00
parent af9df2130e
commit 7c0030180c
28 changed files with 401 additions and 345 deletions
+9 -3
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.ComponentModel;
namespace Tgstation.Server.Api.Models
@@ -557,6 +557,12 @@ namespace Tgstation.Server.Api.Models
/// Attempted to perform an instance operation with an offline instance.
/// </summary>
[Description("The instance associated with the operation is currently offline!")]
InstanceOffline
InstanceOffline,
/// <summary>
/// An attempt to connect a chat bot failed.
/// </summary>
[Description("Failed to connect chat bot!")]
ChatCannotConnectProvider
}
}
}
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Serilog.Context;
using System;
@@ -117,11 +117,6 @@ namespace Tgstation.Server.Host.Components.Chat
/// </summary>
long messagesProcessed;
/// <summary>
/// If <see cref="StartAsync(CancellationToken)"/> has been called
/// </summary>
bool started;
/// <summary>
/// Construct a <see cref="ChatManager"/>
/// </summary>
@@ -159,13 +154,13 @@ namespace Tgstation.Server.Host.Components.Chat
}
/// <inheritdoc />
public void Dispose()
public async ValueTask DisposeAsync()
{
logger.LogTrace("Disposing...");
restartRegistration.Dispose();
handlerCts.Dispose();
foreach (var I in providers)
I.Value.Dispose();
await I.Value.DisposeAsync().ConfigureAwait(false);
}
/// <summary>
@@ -206,6 +201,21 @@ namespace Tgstation.Server.Host.Components.Chat
return provider;
}
async Task RemapProvider(IProvider provider, CancellationToken cancellationToken)
{
logger.LogTrace("Remapping channels for provider reconnection...");
IEnumerable<Api.Models.ChatChannel> channelsToMap;
long providerId;
lock (providers)
providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
lock (activeChatBots)
channelsToMap = activeChatBots.FirstOrDefault(x => x.Id == providerId)?.Channels;
if (channelsToMap?.Any() ?? false)
await ChangeChannels(providerId, channelsToMap, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Processes a <paramref name="message"/>
/// </summary>
@@ -213,26 +223,20 @@ namespace Tgstation.Server.Host.Components.Chat
/// <param name="message">The <see cref="Message"/> to process. If <see langword="null"/>, this indicates the provider reconnected.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
#pragma warning disable CA1502
#pragma warning disable CA1502
async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
#pragma warning restore CA1502
#pragma warning restore CA1502
{
if (!provider.Connected)
{
logger.LogTrace("Abort message processing because provider is disconnected!");
return;
}
// provider reconnected, remap channels.
if (message == null)
{
logger.LogTrace("Remapping channels for provider reconnection...");
IEnumerable<Api.Models.ChatChannel> channelsToMap;
lock (activeChatBots)
channelsToMap = activeChatBots.FirstOrDefault()?.Channels;
if (channelsToMap?.Any() ?? false)
{
long providerId;
lock (providers)
providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
await ChangeChannels(providerId, channelsToMap, cancellationToken).ConfigureAwait(false);
}
await RemapProvider(provider, cancellationToken).ConfigureAwait(false);
return;
}
@@ -243,9 +247,6 @@ namespace Tgstation.Server.Host.Components.Chat
var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
if (message.User.Channel.IsPrivateChannel)
lock (mappedChannels)
{
if (!provider.Connected)
return;
if (!enumerable.Any())
{
ulong newId;
@@ -267,7 +268,6 @@ namespace Tgstation.Server.Host.Components.Chat
}
else
message.User.Channel.RealId = enumerable.First().Key;
}
else
{
// need to add tag and isAdminChannel
@@ -285,7 +285,9 @@ namespace Tgstation.Server.Host.Components.Chat
address = address.ToUpperInvariant();
var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
var addressed =
address == CommonMention.ToUpperInvariant()
|| address == provider.BotMention.ToUpperInvariant();
// no mention
if (!addressed && !message.User.Channel.IsPrivateChannel)
@@ -406,7 +408,7 @@ namespace Tgstation.Server.Host.Components.Chat
while (!cancellationToken.IsCancellationRequested)
{
// prune disconnected providers
foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList())
foreach (var I in messageTasks.Where(x => !x.Key.Disposed).ToList())
messageTasks.Remove(I.Key);
// add new ones
@@ -415,7 +417,7 @@ namespace Tgstation.Server.Host.Components.Chat
updatedTask = connectionsUpdated.Task;
lock (providers)
foreach (var I in providers)
if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
if (!messageTasks.ContainsKey(I.Value))
messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
if (messageTasks.Count == 0)
@@ -521,7 +523,7 @@ namespace Tgstation.Server.Host.Components.Chat
}
/// <inheritdoc />
public async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
public async Task ChangeSettings(Models.ChatBot newSettings, CancellationToken cancellationToken)
{
if (newSettings == null)
throw new ArgumentNullException(nameof(newSettings));
@@ -537,7 +539,7 @@ namespace Tgstation.Server.Host.Components.Chat
}
finally
{
p.Dispose();
await p.DisposeAsync().ConfigureAwait(false);
}
}
@@ -565,30 +567,23 @@ namespace Tgstation.Server.Host.Components.Chat
await disconnectTask.ConfigureAwait(false);
if (started)
lock (synchronizationLock)
{
if (newSettings.Enabled.Value)
await provider.Connect(cancellationToken).ConfigureAwait(false);
lock (synchronizationLock)
{
// same thread shennanigans
var oldOne = connectionsUpdated;
connectionsUpdated = new TaskCompletionSource<object>();
oldOne.SetResult(null);
}
// same thread shennanigans
var oldOne = connectionsUpdated;
connectionsUpdated = new TaskCompletionSource<object>();
oldOne.SetResult(null);
}
Task reconnectionUpdateTask = Task.CompletedTask;
var reconnectionUpdateTask = provider?.SetReconnectInterval(
newSettings.ReconnectionInterval.Value,
newSettings.Enabled.Value)
?? Task.CompletedTask;
lock (activeChatBots)
{
var originalChatBot = activeChatBots.FirstOrDefault(bot => bot.Id == newSettings.Id);
if (originalChatBot != null)
{
if (originalChatBot.ReconnectionInterval != newSettings.ReconnectionInterval)
reconnectionUpdateTask = provider.SetReconnectInterval(newSettings.ReconnectionInterval.Value);
activeChatBots.Remove(originalChatBot);
}
activeChatBots.Add(new Models.ChatBot
{
@@ -716,10 +711,8 @@ namespace Tgstation.Server.Host.Components.Chat
builtinCommands.Add(I.Name.ToUpperInvariant(), I);
var initialChatBots = activeChatBots.ToList();
await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
chatHandler = MonitorMessages(handlerCts.Token);
started = true;
}
/// <inheritdoc />
@@ -774,7 +767,7 @@ namespace Tgstation.Server.Host.Components.Chat
}
finally
{
provider.Dispose();
await provider.DisposeAsync().ConfigureAwait(false);
}
}
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using Tgstation.Server.Host.Components.Chat.Commands;
using Tgstation.Server.Host.Components.Chat.Providers;
using Tgstation.Server.Host.Core;
@@ -38,7 +39,11 @@ namespace Tgstation.Server.Host.Components.Chat
/// <param name="serverControl">The value of <see cref="serverControl"/></param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/></param>
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/></param>
public ChatManagerFactory(IProviderFactory providerFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory)
public ChatManagerFactory(
IProviderFactory providerFactory,
IServerControl serverControl,
IAsyncDelayer asyncDelayer,
ILoggerFactory loggerFactory)
{
this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
@@ -47,6 +52,18 @@ namespace Tgstation.Server.Host.Components.Chat
}
/// <inheritdoc />
public IChatManager CreateChatManager(IIOManager ioManager, ICommandFactory commandFactory, IEnumerable<Models.ChatBot> initialChatBots) => new ChatManager(providerFactory, ioManager, commandFactory, serverControl, asyncDelayer, loggerFactory, loggerFactory.CreateLogger<ChatManager>(), initialChatBots);
public IChatManager CreateChatManager(
IIOManager ioManager,
ICommandFactory commandFactory,
IEnumerable<Models.ChatBot> initialChatBots)
=> new ChatManager(
providerFactory,
ioManager,
commandFactory,
serverControl,
asyncDelayer,
loggerFactory,
loggerFactory.CreateLogger<ChatManager>(),
initialChatBots.Where(x => x.Enabled.Value));
}
}
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Threading;
@@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// <summary>
/// For managing connected chat services
/// </summary>
public interface IChatManager : IHostedService, IDisposable
public interface IChatManager : IHostedService, IAsyncDisposable
{
/// <summary>
/// Registers a <paramref name="customCommandHandler"/> to use
@@ -21,10 +21,10 @@ namespace Tgstation.Server.Host.Components.Chat
/// <summary>
/// Change chat settings. If the <see cref="ChatBot.Id"/> is not currently in use, a new connection will be made instead
/// </summary>
/// <param name="newSettings">The new <see cref="ChatBot"/></param>
/// <param name="newSettings">The new <see cref="Models.ChatBot"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation. Will complete immediately if the <see cref="ChatBot.Enabled"/> property of <paramref name="newSettings"/> is <see langword="false"/></returns>
Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken);
Task ChangeSettings(Models.ChatBot newSettings, CancellationToken cancellationToken);
/// <summary>
/// Disconnects and deletes a given connection
@@ -87,4 +87,4 @@ namespace Tgstation.Server.Host.Components.Chat
/// <returns>A new <see cref="IChatTrackingContext"/>.</returns>
IChatTrackingContext CreateTrackingContext();
}
}
}
@@ -1,4 +1,4 @@
using Discord;
using Discord;
using Discord.WebSocket;
using Microsoft.Extensions.Logging;
using System;
@@ -6,6 +6,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.System;
@@ -30,6 +32,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
}
/// <summary>
/// Gets the Discord bot token.
/// </summary>
string BotToken => ChatBot.ConnectionString;
/// <summary>
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="DiscordProvider"/>.
/// </summary>
@@ -40,11 +47,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// </summary>
readonly DiscordSocketClient client;
/// <summary>
/// The token used for connecting to discord
/// </summary>
readonly string botToken;
/// <summary>
/// <see cref="List{T}"/> of mapped <see cref="ITextChannel"/> <see cref="IEntity{TId}.Id"/>s
/// </summary>
@@ -60,30 +62,28 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <summary>
/// Construct a <see cref="DiscordProvider"/>
/// </summary>
/// <param name="jobManager">The <see cref="IJobManager"/> for the <see cref="Provider"/>.</param>
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
/// <param name="logger">The value of <see cref="Logger"/></param>
/// <param name="botToken">The value of <see cref="botToken"/></param>
/// <param name="reconnectInterval">The initial reconnect interval in minutes.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="Provider"/>.</param>
/// <param name="chatBot">The <see cref="ChatBot"/> for the <see cref="Provider"/>.</param>
public DiscordProvider(
IJobManager jobManager,
IAssemblyInformationProvider assemblyInformationProvider,
ILogger<DiscordProvider> logger,
string botToken,
uint reconnectInterval)
: base(logger, reconnectInterval)
Models.ChatBot chatBot)
: base(jobManager, logger, chatBot)
{
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken));
client = new DiscordSocketClient();
client.MessageReceived += Client_MessageReceived;
mappedChannels = new List<ulong>();
}
/// <inheritdoc />
public override void Dispose()
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync().ConfigureAwait(false);
client.Dispose();
base.Dispose();
}
/// <summary>
@@ -135,18 +135,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
/// <inheritdoc />
public override async Task<bool> Connect(CancellationToken cancellationToken)
protected override async Task Connect(CancellationToken cancellationToken)
{
Logger.LogTrace("Connecting...");
if (Connected)
{
Logger.LogTrace("Already connected not doing connection attempt!");
return true;
}
try
{
await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false);
await client.LoginAsync(TokenType.Bot, BotToken, true).ConfigureAwait(false);
Logger.LogTrace("Logged in.");
cancellationToken.ThrowIfCancellationRequested();
@@ -171,11 +165,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
catch (Exception e)
{
Logger.LogWarning("Error connecting to Discord: {0}", e);
return false;
throw new JobException(ErrorCode.ChatCannotConnectProvider, e);
}
return true;
}
/// <inheritdoc />
@@ -190,9 +181,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
try
{
cancellationToken.ThrowIfCancellationRequested();
await client.StopAsync().ConfigureAwait(false);
Logger.LogTrace("Stopped.");
cancellationToken.ThrowIfCancellationRequested();
await client.LogoutAsync().ConfigureAwait(false);
Logger.LogDebug("Disconnected!");
}
@@ -278,7 +269,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
public override async Task<Func<string, string, Task>> SendUpdateMessage(
RevisionInformation revisionInformation,
Models.RevisionInformation revisionInformation,
Version byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -9,13 +9,18 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <summary>
/// For interacting with a chat service
/// </summary>
interface IProvider : IDisposable
interface IProvider : IAsyncDisposable
{
/// <summary>
/// If the <see cref="IProvider"/>
/// If the <see cref="IProvider"/> is currently connected.
/// </summary>
bool Connected { get; }
/// <summary>
/// If the <see cref="IProvider"/> was disposed.
/// </summary>
bool Disposed { get; }
/// <summary>
/// The <see cref="string"/> that indicates the <see cref="IProvider"/> was mentioned
/// </summary>
@@ -29,13 +34,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <remarks>Note that private messages will come in the form of <see cref="ChannelRepresentation"/>s not returned in <see cref="MapChannels(IEnumerable{Api.Models.ChatChannel}, CancellationToken)"/>. Do not <see cref="IDisposable.Dispose"/> the <see cref="IProvider"/> on continuations run from the returned <see cref="Task"/>.</remarks>
Task<Message> NextMessage(CancellationToken cancellationToken);
/// <summary>
/// Attempt to connect the <see cref="IProvider"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> on success, <see langword="false"/> otherwise</returns>
Task<bool> Connect(CancellationToken cancellationToken);
/// <summary>
/// Gracefully disconnects the provider. Permanently stops the reconnection timer.
/// </summary>
@@ -61,11 +59,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken);
/// <summary>
/// Set the interval at which the provider tries to reconnect.
/// Set the interval at which the provider starts jobs to try to reconnect.
/// </summary>
/// <param name="reconnectInterval">The reconnection interval in minutes.</param>
/// <param name="connectNow">If a connection attempt should be made now.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task SetReconnectInterval(uint reconnectInterval);
Task SetReconnectInterval(uint reconnectInterval, bool connectNow);
/// <summary>
/// Send the message for a deployment.
@@ -1,4 +1,4 @@
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components.Chat.Providers
{
@@ -1,4 +1,4 @@
using Meebey.SmartIrc4net;
using Meebey.SmartIrc4net;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.Components.Chat.Providers
@@ -90,45 +91,33 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <summary>
/// Construct an <see cref="IrcProvider"/>
/// </summary>
/// <param name="jobManager">The <see cref="IJobManager"/> for the provider.</param>
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> to get the <see cref="IAssemblyInformationProvider.VersionString"/> from</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/></param>
/// <param name="logger">The value of logger</param>
/// <param name="address">The value of <see cref="address"/></param>
/// <param name="port">The value of <see cref="port"/></param>
/// <param name="nickname">The value of <see cref="nickname"/></param>
/// <param name="password">The value of <see cref="password"/></param>
/// <param name="passwordType">The value of <see cref="passwordType"/></param>
/// <param name="reconnectInterval">The initial reconnect interval in minutes.</param>
/// <param name="useSsl">If <see cref="IrcConnection.UseSsl"/> should be used</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="Provider"/>.</param>
/// <param name="chatBot">The <see cref="Models.ChatBot"/> for the <see cref="Provider"/>.</param>
public IrcProvider(
IJobManager jobManager,
IAssemblyInformationProvider assemblyInformationProvider,
IAsyncDelayer asyncDelayer,
ILogger<IrcProvider> logger,
string address,
ushort port,
string nickname,
string password,
IrcPasswordType? passwordType,
uint reconnectInterval,
bool useSsl)
: base(logger, reconnectInterval)
Models.ChatBot chatBot)
: base(jobManager, logger, chatBot)
{
if (assemblyInformationProvider == null)
throw new ArgumentNullException(nameof(assemblyInformationProvider));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.address = address ?? throw new ArgumentNullException(nameof(address));
this.port = port;
this.nickname = nickname ?? throw new ArgumentNullException(nameof(nickname));
var builder = chatBot.CreateConnectionStringBuilder();
if (builder == null || !builder.Valid || !(builder is IrcConnectionStringBuilder ircBuilder))
throw new InvalidOperationException("Invalid ChatConnectionStringBuilder!");
if (passwordType.HasValue && password == null)
throw new ArgumentNullException(nameof(password));
address = ircBuilder.Address;
port = ircBuilder.Port.Value;
nickname = ircBuilder.Nickname;
if (password != null && !passwordType.HasValue)
throw new ArgumentNullException(nameof(passwordType));
this.password = password;
this.passwordType = passwordType;
password = ircBuilder.Password;
passwordType = ircBuilder.PasswordType.Value;
client = new IrcFeatures
{
@@ -143,9 +132,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
ActiveChannelSyncing = true,
AutoNickHandling = true,
CtcpVersion = assemblyInformationProvider.VersionString,
UseSsl = useSsl
UseSsl = ircBuilder.UseSsl.Value
};
if (useSsl)
if (ircBuilder.UseSsl.Value)
client.ValidateServerCertificate = true; // dunno if it defaults to that or what
client.OnChannelMessage += Client_OnChannelMessage;
@@ -154,19 +143,13 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
channelIdMap = new Dictionary<ulong, string>();
queryChannelIdMap = new Dictionary<ulong, string>();
channelIdCounter = 1;
disconnecting = false;
}
/// <inheritdoc />
public override void Dispose()
public override async ValueTask DisposeAsync()
{
if (Connected)
{
disconnecting = true;
client.Disconnect(); // just closes the socket
}
base.Dispose();
await base.DisposeAsync().ConfigureAwait(false);
await HardDisconnect().ConfigureAwait(false);
}
/// <summary>
@@ -247,7 +230,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
void Client_OnChannelMessage(object sender, IrcEventArgs e) => HandleMessage(e, false);
/// <inheritdoc />
public override Task<bool> Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
protected override Task Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
disconnecting = false;
lock (client)
@@ -338,11 +321,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
catch (Exception e)
{
Logger.LogWarning("Unable to connect to IRC: {0}", e);
return false;
throw new JobException(ErrorCode.ChatCannotConnectProvider, e);
}
return true;
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
@@ -363,8 +343,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
Logger.LogWarning("Error quitting IRC: {0}", e);
}
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
Dispose();
await listenTask.ConfigureAwait(false);
await HardDisconnect().ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -376,6 +355,16 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
}
async Task HardDisconnect()
{
if (!Connected)
return;
disconnecting = true;
client.Disconnect();
await listenTask.ConfigureAwait(false);
}
/// <inheritdoc />
public override Task<IReadOnlyCollection<ChannelRepresentation>> MapChannels(
IEnumerable<ChatChannel> channels,
@@ -1,8 +1,10 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components.Chat.Providers
@@ -10,10 +12,20 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
abstract class Provider : IProvider
{
/// <summary>
/// The <see cref="ChatBot"/> the <see cref="Provider"/> is for.
/// </summary>
protected ChatBot ChatBot { get; }
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="Provider"/>.
/// </summary>
protected ILogger Logger { get; }
protected ILogger<Provider> Logger { get; }
/// <summary>
/// The <see cref="IJobManager"/> for the <see cref="Provider"/>.
/// </summary>
readonly IJobManager jobManager;
/// <summary>
/// <see cref="Queue{T}"/> of received <see cref="Message"/>s
@@ -43,18 +55,20 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <summary>
/// Construct a <see cref="Provider"/>
/// </summary>
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
/// <param name="logger">The value of <see cref="Logger"/>.</param>
/// <param name="reconnectInterval">The initial reconnection interval.</param>
protected Provider(ILogger logger, uint reconnectInterval)
/// <param name="chatBot">The value of <paramref name="chatBot"/>.</param>
protected Provider(IJobManager jobManager, ILogger<Provider> logger, ChatBot chatBot)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
ChatBot = chatBot ?? throw new ArgumentNullException(nameof(chatBot));
messageQueue = new Queue<Message>();
nextMessage = new TaskCompletionSource<object>();
reconnectTaskLock = new object();
SetReconnectInterval(reconnectInterval).GetAwaiter().GetResult();
logger.LogTrace("Created.");
}
@@ -64,6 +78,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
public abstract string BotMention { get; }
/// <inheritdoc />
public bool Disposed { get; private set; }
/// <summary>
/// Queues a <paramref name="message"/> for <see cref="NextMessage(CancellationToken)"/>
/// </summary>
@@ -78,14 +95,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
/// <inheritdoc />
public virtual void Dispose()
public virtual async ValueTask DisposeAsync()
{
StopReconnectionTimer().GetAwaiter().GetResult();
Disposed = true;
await StopReconnectionTimer().ConfigureAwait(false);
Logger.LogTrace("Disposed");
}
/// <inheritdoc />
public abstract Task<bool> Connect(CancellationToken cancellationToken);
/// <summary>
/// Attempt to connect the <see cref="Provider"/>.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
protected abstract Task Connect(CancellationToken cancellationToken);
/// <summary>
/// Gracefully disconnects the provider.
@@ -97,8 +119,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
public async Task Disconnect(CancellationToken cancellationToken)
{
if(Connected)
await DisconnectImpl(cancellationToken).ConfigureAwait(false);
await StopReconnectionTimer().ConfigureAwait(false);
await DisconnectImpl(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -141,7 +164,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
/// <inheritdoc />
public Task SetReconnectInterval(uint reconnectInterval)
public Task SetReconnectInterval(uint reconnectInterval, bool connectNow)
{
if (reconnectInterval == 0)
throw new ArgumentOutOfRangeException(nameof(reconnectInterval), reconnectInterval, "Reconnect interval cannot be zero!");
@@ -151,7 +174,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
{
stopOldTimerTask = StopReconnectionTimer();
reconnectCts = new CancellationTokenSource();
reconnectTask = ReconnectionLoop(reconnectInterval, reconnectCts.Token);
reconnectTask = ReconnectionLoop(reconnectInterval, connectNow, reconnectCts.Token);
}
return stopOldTimerTask;
@@ -161,21 +184,42 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// Creates a <see cref="Task"/> that will attempt to reconnect the <see cref="Provider"/> every <paramref name="reconnectInterval"/> minutes.
/// </summary>
/// <param name="reconnectInterval">The amount of minutes to wait between reconnection attempts.</param>
/// <param name="connectNow">If a connection attempt should be immediately made.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
async Task ReconnectionLoop(uint reconnectInterval, CancellationToken cancellationToken)
async Task ReconnectionLoop(uint reconnectInterval, bool connectNow, CancellationToken cancellationToken)
{
do
{
try
{
await Task.Delay(TimeSpan.FromMinutes(reconnectInterval), cancellationToken).ConfigureAwait(false);
if (!connectNow)
await Task.Delay(TimeSpan.FromMinutes(reconnectInterval), cancellationToken).ConfigureAwait(false);
else
connectNow = false;
if (!Connected)
{
Logger.LogInformation("Attempting to reconnect provider...");
await Disconnect(cancellationToken).ConfigureAwait(false);
if (await Connect(cancellationToken).ConfigureAwait(false))
EnqueueMessage(null);
var job = new Job
{
Description = $"Reconnect chat bot: {ChatBot.Name}",
CancelRight = (ulong)ChatBotRights.WriteEnabled,
CancelRightsType = RightsType.ChatBots,
Instance = ChatBot.Instance
};
await jobManager.RegisterOperation(
job,
async (core, databaseContextFactory, paramJob, progressReporter, jobCancellationToken) =>
{
await DisconnectImpl(jobCancellationToken).ConfigureAwait(false);
await Connect(jobCancellationToken).ConfigureAwait(false);
EnqueueMessage(null);
},
cancellationToken)
.ConfigureAwait(false);
// DCT: Always wait for the job to complete here
await jobManager.WaitForJobCompletion(job, null, cancellationToken, default).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
@@ -1,8 +1,9 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using System;
using System.Globalization;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.Components.Chat.Providers
@@ -20,6 +21,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// </summary>
readonly IAsyncDelayer asyncDelayer;
/// <summary>
/// The <see cref="IJobManager"/> for the <see cref="ProviderFactory"/>.
/// </summary>
readonly IJobManager jobManager;
/// <summary>
/// The <see cref="ILoggerFactory"/> for the <see cref="ProviderFactory"/>
/// </summary>
@@ -28,42 +34,42 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <summary>
/// Construct a <see cref="ProviderFactory"/>
/// </summary>
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/></param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/></param>
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/></param>
public ProviderFactory(
IJobManager jobManager,
IAssemblyInformationProvider assemblyInformationProvider,
IAsyncDelayer asyncDelayer,
ILoggerFactory loggerFactory)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
}
/// <inheritdoc />
public IProvider CreateProvider(Api.Models.Internal.ChatBot settings)
public IProvider CreateProvider(Models.ChatBot settings)
{
if (settings == null)
throw new ArgumentNullException(nameof(settings));
var builder = settings.CreateConnectionStringBuilder();
if (builder == null || !builder.Valid)
throw new InvalidOperationException("Invalid ChatConnectionStringBuilder!");
switch (settings.Provider)
return settings.Provider switch
{
case ChatProvider.Irc:
var ircBuilder = (IrcConnectionStringBuilder)builder;
return new IrcProvider(assemblyInformationProvider, asyncDelayer, loggerFactory.CreateLogger<IrcProvider>(), ircBuilder.Address, ircBuilder.Port.Value, ircBuilder.Nickname, ircBuilder.Password, ircBuilder.PasswordType, settings.ReconnectionInterval.Value, ircBuilder.UseSsl.Value);
case ChatProvider.Discord:
var discordBuilder = (DiscordConnectionStringBuilder)builder;
return new DiscordProvider(
assemblyInformationProvider,
loggerFactory.CreateLogger<DiscordProvider>(),
discordBuilder.BotToken,
settings.ReconnectionInterval.Value);
default:
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider));
}
ChatProvider.Irc => new IrcProvider(
jobManager,
assemblyInformationProvider,
asyncDelayer,
loggerFactory.CreateLogger<IrcProvider>(),
settings),
ChatProvider.Discord => new DiscordProvider(
jobManager,
assemblyInformationProvider,
loggerFactory.CreateLogger<DiscordProvider>(),
settings),
_ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider)),
};
}
}
}
@@ -1,4 +1,4 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using System;
@@ -13,7 +13,6 @@ using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Components.Watchdog;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
@@ -46,11 +45,6 @@ namespace Tgstation.Server.Host.Components
/// <inheritdoc />
public IDreamMaker DreamMaker { get; }
/// <summary>
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="Instance"/>
/// </summary>
readonly IDatabaseContextFactory databaseContextFactory;
/// <summary>
/// The <see cref="IDmbFactory"/> for the <see cref="Instance"/>
/// </summary>
@@ -101,7 +95,6 @@ namespace Tgstation.Server.Host.Components
/// <param name="watchdog">The value of <see cref="Watchdog"/></param>
/// <param name="chat">The value of <see cref="Chat"/></param>
/// <param name="configuration">The value of <see cref="Configuration"/></param>
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
/// <param name="dmbFactory">The value of <see cref="dmbFactory"/></param>
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/></param>
@@ -115,7 +108,6 @@ namespace Tgstation.Server.Host.Components
IChatManager chat,
StaticFiles.IConfiguration
configuration,
IDatabaseContextFactory databaseContextFactory,
IDmbFactory dmbFactory,
IJobManager jobManager,
IEventConsumer eventConsumer,
@@ -128,7 +120,6 @@ namespace Tgstation.Server.Host.Components
Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
Chat = chat ?? throw new ArgumentNullException(nameof(chat));
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
@@ -144,7 +135,7 @@ namespace Tgstation.Server.Host.Components
{
timerCts?.Dispose();
Configuration.Dispose();
Chat.Dispose();
await Chat.DisposeAsync().ConfigureAwait(false);
await Watchdog.DisposeAsync().ConfigureAwait(false);
dmbFactory.Dispose();
RepositoryManager.Dispose();
@@ -169,15 +160,6 @@ namespace Tgstation.Server.Host.Components
await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List<string>(), cancellationToken).ConfigureAwait(false);
try
{
User systemUser = null;
await databaseContextFactory.UseContext(
async (db) => systemUser = await db
.Users
.AsQueryable()
.Where(x => x.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName))
.FirstAsync(cancellationToken)
.ConfigureAwait(false))
.ConfigureAwait(false);
var repositoryUpdateJob = new Job
{
Instance = new Models.Instance
@@ -186,8 +168,7 @@ namespace Tgstation.Server.Host.Components
},
Description = "Scheduled repository update",
CancelRightsType = RightsType.Repository,
CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
StartedBy = systemUser
CancelRight = (ulong)RepositoryRights.CancelPendingChanges
};
string deploySha = null;
@@ -355,7 +336,7 @@ namespace Tgstation.Server.Host.Components
}, cancellationToken).ConfigureAwait(false);
// DCT: First token will cancel the job, second is for cancelling the cancellation, unwanted
await jobManager.WaitForJobCompletion(repositoryUpdateJob, systemUser, cancellationToken, default).ConfigureAwait(false);
await jobManager.WaitForJobCompletion(repositoryUpdateJob, null, cancellationToken, default).ConfigureAwait(false);
if (deploySha == null)
{
@@ -372,7 +353,6 @@ namespace Tgstation.Server.Host.Components
// finally set up the job
var compileProcessJob = new Job
{
StartedBy = systemUser,
Instance = repositoryUpdateJob.Instance,
Description = "Scheduled code deployment",
CancelRightsType = RightsType.DreamMaker,
@@ -393,7 +373,7 @@ namespace Tgstation.Server.Host.Components
},
cancellationToken).ConfigureAwait(false);
await jobManager.WaitForJobCompletion(compileProcessJob, systemUser, default, cancellationToken).ConfigureAwait(false);
await jobManager.WaitForJobCompletion(compileProcessJob, null, default, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -284,7 +284,6 @@ namespace Tgstation.Server.Host.Components
watchdog,
chatManager,
configuration,
databaseContextFactory,
dmbFactory,
jobManager,
eventConsumer,
@@ -306,7 +305,7 @@ namespace Tgstation.Server.Host.Components
}
catch
{
chatManager.Dispose();
await chatManager.DisposeAsync().ConfigureAwait(false);
throw;
}
}
@@ -1,4 +1,4 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -424,6 +424,9 @@ namespace Tgstation.Server.Host.Components
})
.ToList();
await Task.WhenAll(tasks).ConfigureAwait(false);
jobManager.Activate();
logger.LogInformation("Server ready!");
readyTcs.SetResult(null);
}
@@ -1,8 +1,9 @@
using System;
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.Components.Interop.Topic;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.Components.Session
@@ -33,9 +34,9 @@ namespace Tgstation.Server.Host.Components.Session
Version DMApiVersion { get; }
/// <summary>
/// The <see cref="IDmbProvider"/> being used
/// Gets the <see cref="CompileJob"/> associated with the <see cref="ISessionController"/>.
/// </summary>
IDmbProvider Dmb { get; }
CompileJob CompileJob { get; }
/// <summary>
/// The current port DreamDaemon is listening on
@@ -108,7 +109,7 @@ namespace Tgstation.Server.Host.Components.Session
void EnableCustomChatCommands();
/// <summary>
/// Replace <see cref="Dmb"/> with a given <paramref name="newProvider"/>, disposing the old one.
/// Replace the <see cref="IDmbProvider"/> in use with a given <paramref name="newProvider"/>, disposing the old one.
/// </summary>
/// <param name="newProvider">The new <see cref="IDmbProvider"/>.</param>
void ReplaceDmbProvider(IDmbProvider newProvider);
@@ -1,4 +1,4 @@
using Byond.TopicSender;
using Byond.TopicSender;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Serilog.Context;
@@ -39,14 +39,7 @@ namespace Tgstation.Server.Host.Components.Session
}
/// <inheritdoc />
public IDmbProvider Dmb
{
get
{
CheckDisposed();
return reattachInformation.Dmb;
}
}
public Models.CompileJob CompileJob => reattachInformation.Dmb.CompileJob;
/// <inheritdoc />
public ushort? Port
@@ -346,7 +339,7 @@ namespace Tgstation.Server.Host.Components.Session
else if (reattachResponse.InteropResponse != null)
logger.LogWarning(
"DMAPI v{0} isn't returning the TGS custom commands list. Functionality added in v5.2.0.",
Dmb.CompileJob.DMApiVersion.Semver());
CompileJob.DMApiVersion.Semver());
}
}
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using System;
using System.Globalization;
using System.Threading;
@@ -9,7 +9,6 @@ using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Components.Session;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
@@ -23,9 +22,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <inheritdoc />
public sealed override bool AlphaIsActive => true;
/// <inheritdoc />
public sealed override Models.CompileJob ActiveCompileJob => Server?.Dmb.CompileJob;
/// <inheritdoc />
public sealed override RebootState? RebootState => Server?.RebootState;
@@ -46,7 +42,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="sessionControllerFactory">The <see cref="ISessionControllerFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="dmbFactory">The <see cref="IDmbFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="sessionPersistor">The <see cref="ISessionPersistor"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="databaseContextFactory">The <see cref="IDatabaseContextFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="jobManager">The <see cref="IJobManager"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="serverControl">The <see cref="IServerControl"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="asyncDelayer">The <see cref="IAsyncDelayer"/> for the <see cref="WatchdogBase"/>.</param>
@@ -61,7 +56,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
ISessionControllerFactory sessionControllerFactory,
IDmbFactory dmbFactory,
ISessionPersistor sessionPersistor,
IDatabaseContextFactory databaseContextFactory,
IJobManager jobManager,
IServerControl serverControl,
IAsyncDelayer asyncDelayer,
@@ -76,7 +70,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
sessionControllerFactory,
dmbFactory,
sessionPersistor,
databaseContextFactory,
jobManager,
serverControl,
asyncDelayer,
@@ -261,7 +254,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
protected virtual Task HandleNewDmbAvailable(CancellationToken cancellationToken)
{
gracefulRebootRequired = true;
if (Server.Dmb.CompileJob.DMApiVersion == null)
if (Server.CompileJob.DMApiVersion == null)
return Chat.SendWatchdogMessage(
"A new deployment has been made but cannot be applied automatically as the currently running server has no DMAPI. Please manually reboot the server to apply the update.",
true,
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -25,7 +25,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
bool AlphaIsActive { get; }
/// <summary>
/// The <see cref="Models.CompileJob"/> currently running on the server
/// Retrieves the <see cref="Models.CompileJob"/> currently running on the server
/// </summary>
Models.CompileJob ActiveCompileJob { get; }
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models.Internal;
@@ -7,7 +7,6 @@ using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Components.Session;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
@@ -30,7 +29,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="sessionControllerFactory">The <see cref="ISessionControllerFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="dmbFactory">The <see cref="IDmbFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="sessionPersistor">The <see cref="ISessionPersistor"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="databaseContextFactory">The <see cref="IDatabaseContextFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="jobManager">The <see cref="IJobManager"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="serverControl">The <see cref="IServerControl"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="asyncDelayer">The <see cref="IAsyncDelayer"/> for the <see cref="WatchdogBase"/>.</param>
@@ -47,7 +45,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
ISessionControllerFactory sessionControllerFactory,
IDmbFactory dmbFactory,
ISessionPersistor sessionPersistor,
IDatabaseContextFactory databaseContextFactory,
IJobManager jobManager,
IServerControl serverControl,
IAsyncDelayer asyncDelayer,
@@ -64,7 +61,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
sessionControllerFactory,
dmbFactory,
sessionPersistor,
databaseContextFactory,
jobManager,
serverControl,
asyncDelayer,
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using Tgstation.Server.Api.Models.Internal;
@@ -8,7 +8,6 @@ using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Components.Session;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
@@ -24,7 +23,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
/// <param name="serverControl">The <see cref="IServerControl"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="databaseContextFactory">The <see cref="IDatabaseContextFactory"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="jobManager">The <see cref="IJobManager"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="asyncDelayer">The <see cref="IAsyncDelayer"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="symlinkFactory">The <see cref="ISymlinkFactory"/> for the <see cref="WindowsWatchdogFactory"/>.</param>
@@ -32,7 +30,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
public PosixWatchdogFactory(
IServerControl serverControl,
ILoggerFactory loggerFactory,
IDatabaseContextFactory databaseContextFactory,
IJobManager jobManager,
IAsyncDelayer asyncDelayer,
ISymlinkFactory symlinkFactory,
@@ -40,7 +37,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
: base(
serverControl,
loggerFactory,
databaseContextFactory,
jobManager,
asyncDelayer,
symlinkFactory,
@@ -63,7 +59,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
sessionControllerFactory,
dmbFactory,
sessionPersistor,
DatabaseContextFactory,
JobManager,
ServerControl,
AsyncDelayer,
@@ -1,4 +1,3 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using System;
@@ -15,7 +14,6 @@ using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Components.Interop.Topic;
using Tgstation.Server.Host.Components.Session;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
@@ -42,15 +40,15 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <inheritdoc />
public abstract bool AlphaIsActive { get; }
/// <inheritdoc />
public abstract Models.CompileJob ActiveCompileJob { get; }
/// <inheritdoc />
public DreamDaemonLaunchParameters ActiveLaunchParameters { get; protected set; }
/// <inheritdoc />
public DreamDaemonLaunchParameters LastLaunchParameters { get; protected set; }
/// <inheritdoc />
public Models.CompileJob ActiveCompileJob => GetActiveController()?.CompileJob;
/// <inheritdoc />
public abstract RebootState? RebootState { get; }
@@ -104,11 +102,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
readonly ISessionPersistor sessionPersistor;
/// <summary>
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="WatchdogBase"/>
/// </summary>
readonly IDatabaseContextFactory databaseContextFactory;
/// <summary>
/// The <see cref="IJobManager"/> for the <see cref="WatchdogBase"/>.
/// </summary>
@@ -176,7 +169,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="sessionControllerFactory">The value of <see cref="SessionControllerFactory"/></param>
/// <param name="dmbFactory">The value of <see cref="DmbFactory"/></param>
/// <param name="sessionPersistor">The value of <see cref="sessionPersistor"/></param>
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
/// <param name="serverControl">The <see cref="IServerControl"/> to populate <see cref="restartRegistration"/> with</param>
/// <param name="asyncDelayer">The value of <see cref="AsyncDelayer"/>.</param>
@@ -191,7 +183,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
ISessionControllerFactory sessionControllerFactory,
IDmbFactory dmbFactory,
ISessionPersistor sessionPersistor,
IDatabaseContextFactory databaseContextFactory,
IJobManager jobManager,
IServerControl serverControl,
IAsyncDelayer asyncDelayer,
@@ -206,7 +197,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
SessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
DmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
this.sessionPersistor = sessionPersistor ?? throw new ArgumentNullException(nameof(sessionPersistor));
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
AsyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.diagnosticsIOManager = diagnosticsIOManager ?? throw new ArgumentNullException(nameof(diagnosticsIOManager));
@@ -892,18 +882,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
if (!autoStart && reattachInfo == null)
return;
Models.User systemUser = null;
await databaseContextFactory.UseContext(
async db => systemUser = await db
.Users
.AsQueryable()
.Where(x => x.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName))
.FirstAsync(cancellationToken)
.ConfigureAwait(false))
.ConfigureAwait(false);
var job = new Models.Job
{
StartedBy = systemUser,
Instance = new Models.Instance
{
Id = instance.Id
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using Tgstation.Server.Api.Models.Internal;
@@ -8,7 +8,6 @@ using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Components.Session;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
@@ -27,11 +26,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
protected ILoggerFactory LoggerFactory { get; }
/// <summary>
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="WatchdogFactory"/>
/// </summary>
protected IDatabaseContextFactory DatabaseContextFactory { get; }
/// <summary>
/// The <see cref="IJobManager"/> for the <see cref="WatchdogFactory"/>
/// </summary>
@@ -52,21 +46,18 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
/// <param name="serverControl">The value of <see cref="ServerControl"/></param>
/// <param name="loggerFactory">The value of <see cref="LoggerFactory"/></param>
/// <param name="databaseContextFactory">The value of <see cref="DatabaseContextFactory"/></param>
/// <param name="jobManager">The value of <see cref="JobManager"/></param>
/// <param name="asyncDelayer">The value of <see cref="AsyncDelayer"/></param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="GeneralConfiguration"/></param>
public WatchdogFactory(
IServerControl serverControl,
ILoggerFactory loggerFactory,
IDatabaseContextFactory databaseContextFactory,
IJobManager jobManager,
IAsyncDelayer asyncDelayer,
IOptions<GeneralConfiguration> generalConfigurationOptions)
{
ServerControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
LoggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
DatabaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
JobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
AsyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
GeneralConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
@@ -88,7 +79,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
sessionControllerFactory,
dmbFactory,
sessionPersistor,
DatabaseContextFactory,
JobManager,
ServerControl,
AsyncDelayer,
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -8,7 +8,6 @@ using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Components.Session;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
@@ -51,7 +50,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="sessionControllerFactory">The <see cref="ISessionControllerFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="dmbFactory">The <see cref="IDmbFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="sessionPersistor">The <see cref="ISessionPersistor"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="databaseContextFactory">The <see cref="IDatabaseContextFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="jobManager">The <see cref="IJobManager"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="serverControl">The <see cref="IServerControl"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="asyncDelayer">The <see cref="IAsyncDelayer"/> for the <see cref="WatchdogBase"/>.</param>
@@ -68,7 +66,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
ISessionControllerFactory sessionControllerFactory,
IDmbFactory dmbFactory,
ISessionPersistor sessionPersistor,
IDatabaseContextFactory databaseContextFactory,
IJobManager jobManager,
IServerControl serverControl,
IAsyncDelayer asyncDelayer,
@@ -84,7 +81,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
sessionControllerFactory,
dmbFactory,
sessionPersistor,
databaseContextFactory,
jobManager,
serverControl,
asyncDelayer,
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using Tgstation.Server.Api.Models.Internal;
@@ -8,7 +8,6 @@ using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Components.Session;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
@@ -29,7 +28,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
/// <param name="serverControl">The <see cref="IServerControl"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="databaseContextFactory">The <see cref="IDatabaseContextFactory"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="jobManager">The <see cref="IJobManager"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="asyncDelayer">The <see cref="IAsyncDelayer"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="symlinkFactory">The value of <see cref="SymlinkFactory"/>.</param>
@@ -37,7 +35,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
public WindowsWatchdogFactory(
IServerControl serverControl,
ILoggerFactory loggerFactory,
IDatabaseContextFactory databaseContextFactory,
IJobManager jobManager,
IAsyncDelayer asyncDelayer,
ISymlinkFactory symlinkFactory,
@@ -45,7 +42,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
: base(
serverControl,
loggerFactory,
databaseContextFactory,
jobManager,
asyncDelayer,
generalConfigurationOptions)
@@ -69,7 +65,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
sessionControllerFactory,
dmbFactory,
sessionPersistor,
DatabaseContextFactory,
JobManager,
ServerControl,
AsyncDelayer,
@@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Extensions
{
/// <summary>
/// Extension methods for the <see cref="IDatabaseCollection{TModel}"/> <see langword="interface"/>.
/// </summary>
static class DatabaseCollectionExtensions
{
/// <summary>
/// Gets the unattached, unpopulated <see cref="User"/> with the name <see cref="User.TgsSystemUserName"/>.
/// </summary>
/// <param name="databaseCollection">The <see cref="IDatabaseCollection{TModel}"/> of <see cref="User"/>s to operate on.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the unattached TGS <see cref="User"/> on success, <see langword="null"/> on failure.</returns>
public static Task<User> GetTgsUser(this IDatabaseCollection<User> databaseCollection, CancellationToken cancellationToken)
=> databaseCollection
.AsQueryable()
.Where(x => x.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName))
.Select(x => new User
{
Id = x.Id
})
.FirstAsync(cancellationToken);
}
}
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
@@ -20,7 +20,7 @@ namespace Tgstation.Server.Host.Jobs
/// <summary>
/// Registers a given <see cref="Job"/> and begins running it
/// </summary>
/// <param name="job">The <see cref="Job"/></param>
/// <param name="job">The <see cref="Job"/>. Should at least have <see cref="Job.Instance"/> and <see cref="Api.Models.Internal.Job.Description"/>. If <see cref="Job.StartedBy"/> is <see langword="null"/>, the TGS user will be used.</param>
/// <param name="operation">The <see cref="JobEntrypoint"/> for the <paramref name="job"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing a running operation</returns>
@@ -30,7 +30,7 @@ namespace Tgstation.Server.Host.Jobs
/// Wait for a given <paramref name="job"/> to complete
/// </summary>
/// <param name="job">The <see cref="Job"/> to wait for </param>
/// <param name="canceller">The <see cref="User"/> to cancel the <paramref name="job"/></param>
/// <param name="canceller">The <see cref="User"/> to cancel the <paramref name="job"/>. If <see langword="null"/> the TGS user will be used.</param>
/// <param name="jobCancellationToken">A <see cref="CancellationToken"/> that will cancel the <paramref name="job"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the <see cref="Job"/></returns>
@@ -42,10 +42,15 @@ namespace Tgstation.Server.Host.Jobs
/// Cancels a give <paramref name="job"/>
/// </summary>
/// <param name="job">The <see cref="Job"/> to cancel</param>
/// <param name="user">The <see cref="User"/> who cancelled the <paramref name="job"/></param>
/// <param name="user">The <see cref="User"/> who cancelled the <paramref name="job"/>. If <see langword="null"/> the TGS user will be used.</param>
/// <param name="blocking">If the operation should wait until the job exits before completing</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the updated <paramref name="job"/> if it was cancelled, <see langword="null"/> if it couldn't be found.</returns>
Task<Job> CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken);
/// <summary>
/// Activate the <see cref="IJobManager"/>.
/// </summary>
void Activate();
}
}
+45 -23
View File
@@ -1,4 +1,4 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using System;
@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Components;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Jobs
@@ -35,6 +36,11 @@ namespace Tgstation.Server.Host.Jobs
/// </summary>
readonly Dictionary<long, JobHandler> jobs;
/// <summary>
/// <see cref="TaskCompletionSource{TResult}"/> to delay starting jobs until the server is ready.
/// </summary>
readonly TaskCompletionSource<object> activationTcs;
/// <summary>
/// <see langword="lock"/> <see cref="object"/> for various operations.
/// </summary>
@@ -52,6 +58,7 @@ namespace Tgstation.Server.Host.Jobs
this.instanceCoreProvider = instanceCoreProvider ?? throw new ArgumentNullException(nameof(instanceCoreProvider));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
jobs = new Dictionary<long, JobHandler>();
activationTcs = new TaskCompletionSource<object>();
synchronizationLock = new object();
}
@@ -89,7 +96,7 @@ namespace Tgstation.Server.Host.Jobs
using (LogContext.PushProperty("Job", job.Id))
try
{
void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
void LogException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
try
{
var oldJob = job;
@@ -102,6 +109,8 @@ namespace Tgstation.Server.Host.Jobs
handler.Progress = progress;
}
await activationTcs.Task.WithToken(cancellationToken).ConfigureAwait(false);
await operation(
instanceCoreProvider.Value.GetInstance(oldJob.Instance),
databaseContextFactory,
@@ -120,20 +129,13 @@ namespace Tgstation.Server.Host.Jobs
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());
job.ExceptionDetails = String.IsNullOrWhiteSpace(e.Message) ? e.InnerException?.Message : e.Message;
LogException();
}
catch (Exception e)
{
job.ExceptionDetails = e.ToString();
LogRegularException();
LogException();
}
await databaseContextFactory.UseContext(async databaseContext =>
@@ -183,10 +185,16 @@ namespace Tgstation.Server.Host.Jobs
};
databaseContext.Instances.Attach(job.Instance);
job.StartedBy = new User
{
Id = job.StartedBy.Id
};
if (job.StartedBy == null)
job.StartedBy = await databaseContext
.Users
.GetTgsUser(cancellationToken)
.ConfigureAwait(false);
else
job.StartedBy = new User
{
Id = job.StartedBy.Id
};
databaseContext.Users.Attach(job.StartedBy);
databaseContext.Jobs.Add(job);
@@ -244,11 +252,14 @@ namespace Tgstation.Server.Host.Jobs
/// <inheritdoc />
public async Task StopAsync(CancellationToken cancellationToken)
{
var joinTasks = jobs.Select(x =>
{
x.Value.Cancel();
return x.Value.Wait(cancellationToken);
});
var joinTasks = jobs.Select(x => CancelJob(
new Job
{
Id = x.Key,
},
null,
true,
cancellationToken));
await Task.WhenAll(joinTasks).ConfigureAwait(false);
}
@@ -257,8 +268,6 @@ namespace Tgstation.Server.Host.Jobs
{
if (job == null)
throw new ArgumentNullException(nameof(job));
if (user == null)
throw new ArgumentNullException(nameof(user));
JobHandler handler;
try
{
@@ -273,6 +282,12 @@ namespace Tgstation.Server.Host.Jobs
handler.Cancel(); // this will ensure the db update is only done once
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);
var attachedUser = new User { Id = user.Id };
@@ -322,5 +337,12 @@ namespace Tgstation.Server.Host.Jobs
if (cancelTask != null)
await cancelTask.ConfigureAwait(false);
}
/// <inheritdoc />
public void Activate()
{
logger.LogTrace("Activating job manager...");
activationTcs.SetResult(null);
}
}
}
@@ -1,9 +1,12 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
@@ -11,52 +14,75 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
[TestClass]
public sealed class TestDiscordProvider
{
string testToken1;
ChatBot testToken1;
IJobManager mockJobManager;
[TestInitialize]
public void Initialize()
{
testToken1 = Environment.GetEnvironmentVariable("TGS4_TEST_DISCORD_TOKEN_1");
var actualToken = Environment.GetEnvironmentVariable("TGS4_TEST_DISCORD_TOKEN");
if(!String.IsNullOrWhiteSpace(actualToken))
testToken1 = new ChatBot
{
ConnectionString = actualToken,
ReconnectionInterval = 1
};
var mockSetup = new Mock<IJobManager>();
mockSetup
.Setup(x => x.RegisterOperation(It.IsNotNull<Job>(), It.IsNotNull<JobEntrypoint>(), It.IsAny<CancellationToken>()))
.Callback<Job, JobEntrypoint, CancellationToken>((job, entrypoint, cancellationToken) => job.StartedBy ??= new User { })
.Returns(Task.CompletedTask);
mockSetup
.Setup(x => x.WaitForJobCompletion(It.IsNotNull<Job>(), It.IsAny<User>(), It.IsAny<CancellationToken>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
mockJobManager = mockSetup.Object;
}
[TestMethod]
public void TestConstructionAndDisposal()
public async Task TestConstructionAndDisposal()
{
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(null, null, null, 1));
if (testToken1 == null)
Assert.Inconclusive("Required environment variable TGS4_TEST_DISCORD_TOKEN isn't set!");
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockJobManager, null, null, null));
var mockAss = new Mock<IAssemblyInformationProvider>();
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockAss.Object, null, null, 1));
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockJobManager, mockAss.Object, null, null));
var mockLogger = new Mock<ILogger<DiscordProvider>>();
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockAss.Object, mockLogger.Object, null, 1));
var mockToken = "asdf";
Assert.ThrowsException<ArgumentOutOfRangeException>(() => new DiscordProvider(mockAss.Object, mockLogger.Object, mockToken, 0));
new DiscordProvider(mockAss.Object, mockLogger.Object, mockToken, 1).Dispose();
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockJobManager, null, mockLogger.Object, null));
await new DiscordProvider(mockJobManager, mockAss.Object, mockLogger.Object, testToken1).DisposeAsync();
}
static Task InvokeConnect(IProvider provider, CancellationToken cancellationToken = default) => (Task)provider.GetType().GetMethod("Connect", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(provider, new object[] { cancellationToken });
[TestMethod]
public async Task TestConnectWithFakeTokenFails()
{
var mockLogger = new Mock<ILogger<DiscordProvider>>();
using var provider = new DiscordProvider(Mock.Of<IAssemblyInformationProvider>(), mockLogger.Object, "asdf", 1);
Assert.IsFalse(await provider.Connect(default).ConfigureAwait(false));
await using var provider = new DiscordProvider(mockJobManager, Mock.Of<IAssemblyInformationProvider>(), mockLogger.Object, new ChatBot
{
ReconnectionInterval = 1,
ConnectionString = "asdf"
});
await Assert.ThrowsExceptionAsync<JobException>(() => InvokeConnect(provider));
Assert.IsFalse(provider.Connected);
}
[Ignore("Broken due to dependency issues after first call to .Connect()")]
[TestMethod]
public async Task TestConnectAndDisconnect()
{
if (testToken1 == null)
Assert.Inconclusive("Required environment variable TGS4_TEST_DISCORD_TOKEN_1 isn't set!");
Assert.Inconclusive("Required environment variable TGS4_TEST_DISCORD_TOKEN isn't set!");
var mockLogger = new Mock<ILogger<DiscordProvider>>();
using var provider = new DiscordProvider(Mock.Of<IAssemblyInformationProvider>(), mockLogger.Object, testToken1, 1);
await using var provider = new DiscordProvider(mockJobManager, Mock.Of<IAssemblyInformationProvider>(), mockLogger.Object, testToken1);
Assert.IsFalse(provider.Connected);
await provider.Disconnect(default).ConfigureAwait(false);
Assert.IsFalse(provider.Connected);
Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false));
await InvokeConnect(provider).ConfigureAwait(false);
Assert.IsTrue(provider.Connected);
Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false));
await InvokeConnect(provider).ConfigureAwait(false);
Assert.IsTrue(provider.Connected);
await provider.Disconnect(default).ConfigureAwait(false);
@@ -68,9 +94,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
using var cts = new CancellationTokenSource();
cts.Cancel();
var cancellationToken = cts.Token;
await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => provider.Connect(cancellationToken)).ConfigureAwait(false);
await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => InvokeConnect(provider, cancellationToken)).ConfigureAwait(false);
Assert.IsFalse(provider.Connected);
Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false));
await InvokeConnect(provider).ConfigureAwait(false);
Assert.IsTrue(provider.Connected);
await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => provider.Disconnect(cancellationToken)).ConfigureAwait(false);
Assert.IsTrue(provider.Connected);
@@ -95,7 +95,13 @@ namespace Tgstation.Server.Tests
{
try
{
return await clientFactory.CreateFromLogin(url, User.AdminName, User.DefaultAdminPassword, attemptLoginRefresh: false).ConfigureAwait(false);
return await clientFactory.CreateFromLogin(
url,
User.AdminName,
User.DefaultAdminPassword,
attemptLoginRefresh: false,
cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
catch (HttpRequestException)
{