diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 9a350f2377..174de92a6f 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -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. /// [Description("The instance associated with the operation is currently offline!")] - InstanceOffline + InstanceOffline, + + /// + /// An attempt to connect a chat bot failed. + /// + [Description("Failed to connect chat bot!")] + ChatCannotConnectProvider } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index dafe18f360..05f2c19ee4 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -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 /// long messagesProcessed; - /// - /// If has been called - /// - bool started; - /// /// Construct a /// @@ -159,13 +154,13 @@ namespace Tgstation.Server.Host.Components.Chat } /// - 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); } /// @@ -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 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); + } + /// /// Processes a /// @@ -213,26 +223,20 @@ namespace Tgstation.Server.Host.Components.Chat /// The to process. If , this indicates the provider reconnected. /// The for the operation /// A representing the running operation - #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 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 } /// - 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(); - oldOne.SetResult(null); - } + // same thread shennanigans + var oldOne = connectionsUpdated; + connectionsUpdated = new TaskCompletionSource(); + 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; } /// @@ -774,7 +767,7 @@ namespace Tgstation.Server.Host.Components.Chat } finally { - provider.Dispose(); + await provider.DisposeAsync().ConfigureAwait(false); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs index 0dc785c687..0ca59ef63c 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs @@ -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 /// The value of /// The value of /// The value of - 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 } /// - public IChatManager CreateChatManager(IIOManager ioManager, ICommandFactory commandFactory, IEnumerable initialChatBots) => new ChatManager(providerFactory, ioManager, commandFactory, serverControl, asyncDelayer, loggerFactory, loggerFactory.CreateLogger(), initialChatBots); + public IChatManager CreateChatManager( + IIOManager ioManager, + ICommandFactory commandFactory, + IEnumerable initialChatBots) + => new ChatManager( + providerFactory, + ioManager, + commandFactory, + serverControl, + asyncDelayer, + loggerFactory, + loggerFactory.CreateLogger(), + initialChatBots.Where(x => x.Enabled.Value)); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs index 062e8fe1bd..d9a37cbe18 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs @@ -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 /// /// For managing connected chat services /// - public interface IChatManager : IHostedService, IDisposable + public interface IChatManager : IHostedService, IAsyncDisposable { /// /// Registers a to use @@ -21,10 +21,10 @@ namespace Tgstation.Server.Host.Components.Chat /// /// Change chat settings. If the is not currently in use, a new connection will be made instead /// - /// The new + /// The new /// The for the operation /// A representing the running operation. Will complete immediately if the property of is - Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken); + Task ChangeSettings(Models.ChatBot newSettings, CancellationToken cancellationToken); /// /// Disconnects and deletes a given connection @@ -87,4 +87,4 @@ namespace Tgstation.Server.Host.Components.Chat /// A new . IChatTrackingContext CreateTrackingContext(); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 5d98784dbb..e41de6012a 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -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 } } + /// + /// Gets the Discord bot token. + /// + string BotToken => ChatBot.ConnectionString; + /// /// The for the . /// @@ -40,11 +47,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// readonly DiscordSocketClient client; - /// - /// The token used for connecting to discord - /// - readonly string botToken; - /// /// of mapped s /// @@ -60,30 +62,28 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// Construct a /// + /// The for the . /// The value of . - /// The value of - /// The value of - /// The initial reconnect interval in minutes. + /// The for the . + /// The for the . public DiscordProvider( + IJobManager jobManager, IAssemblyInformationProvider assemblyInformationProvider, ILogger 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(); } /// - public override void Dispose() + public override async ValueTask DisposeAsync() { + await base.DisposeAsync().ConfigureAwait(false); client.Dispose(); - - base.Dispose(); } /// @@ -135,18 +135,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public override async Task 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; } /// @@ -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 /// public override async Task> SendUpdateMessage( - RevisionInformation revisionInformation, + Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, string gitHubOwner, diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs index cca006f663..95264c0f34 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs @@ -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 /// /// For interacting with a chat service /// - interface IProvider : IDisposable + interface IProvider : IAsyncDisposable { /// - /// If the + /// If the is currently connected. /// bool Connected { get; } + /// + /// If the was disposed. + /// + bool Disposed { get; } + /// /// The that indicates the was mentioned /// @@ -29,13 +34,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// Note that private messages will come in the form of s not returned in . Do not the on continuations run from the returned . Task NextMessage(CancellationToken cancellationToken); - /// - /// Attempt to connect the - /// - /// The for the operation - /// A resulting in on success, otherwise - Task Connect(CancellationToken cancellationToken); - /// /// Gracefully disconnects the provider. Permanently stops the reconnection timer. /// @@ -61,11 +59,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken); /// - /// Set the interval at which the provider tries to reconnect. + /// Set the interval at which the provider starts jobs to try to reconnect. /// /// The reconnection interval in minutes. + /// If a connection attempt should be made now. /// A representing the running operation. - Task SetReconnectInterval(uint reconnectInterval); + Task SetReconnectInterval(uint reconnectInterval, bool connectNow); /// /// Send the message for a deployment. diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IProviderFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IProviderFactory.cs index 91a83eeb96..9350efd7c6 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IProviderFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProviderFactory.cs @@ -1,4 +1,4 @@ -using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components.Chat.Providers { diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index b29eaf15af..5a5d98cde4 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -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 /// /// Construct an /// + /// The for the provider. /// The to get the from /// The value of - /// The value of logger - /// The value of - /// The value of - /// The value of - /// The value of - /// The value of - /// The initial reconnect interval in minutes. - /// If should be used + /// The for the . + /// The for the . public IrcProvider( + IJobManager jobManager, IAssemblyInformationProvider assemblyInformationProvider, IAsyncDelayer asyncDelayer, ILogger 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(); queryChannelIdMap = new Dictionary(); channelIdCounter = 1; - disconnecting = false; } /// - 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); } /// @@ -247,7 +230,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers void Client_OnChannelMessage(object sender, IrcEventArgs e) => HandleMessage(e, false); /// - public override Task 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); /// @@ -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); + } + /// public override Task> MapChannels( IEnumerable channels, diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs index 070db5a8ef..ea03fea802 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs @@ -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 /// abstract class Provider : IProvider { + /// + /// The the is for. + /// + protected ChatBot ChatBot { get; } + /// /// The for the . /// - protected ILogger Logger { get; } + protected ILogger Logger { get; } + + /// + /// The for the . + /// + readonly IJobManager jobManager; /// /// of received s @@ -43,18 +55,20 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// Construct a /// + /// The value of . /// The value of . - /// The initial reconnection interval. - protected Provider(ILogger logger, uint reconnectInterval) + /// The value of . + protected Provider(IJobManager jobManager, ILogger 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(); nextMessage = new TaskCompletionSource(); reconnectTaskLock = new object(); - SetReconnectInterval(reconnectInterval).GetAwaiter().GetResult(); logger.LogTrace("Created."); } @@ -64,6 +78,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// public abstract string BotMention { get; } + /// + public bool Disposed { get; private set; } + /// /// Queues a for /// @@ -78,14 +95,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public virtual void Dispose() + public virtual async ValueTask DisposeAsync() { - StopReconnectionTimer().GetAwaiter().GetResult(); + Disposed = true; + await StopReconnectionTimer().ConfigureAwait(false); Logger.LogTrace("Disposed"); } - /// - public abstract Task Connect(CancellationToken cancellationToken); + /// + /// Attempt to connect the . + /// + /// The for the operation. + /// A representing the running operation. + protected abstract Task Connect(CancellationToken cancellationToken); /// /// Gracefully disconnects the provider. @@ -97,8 +119,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// public async Task Disconnect(CancellationToken cancellationToken) { + if(Connected) + await DisconnectImpl(cancellationToken).ConfigureAwait(false); await StopReconnectionTimer().ConfigureAwait(false); - await DisconnectImpl(cancellationToken).ConfigureAwait(false); } /// @@ -141,7 +164,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - 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 that will attempt to reconnect the every minutes. /// /// The amount of minutes to wait between reconnection attempts. + /// If a connection attempt should be immediately made. /// The for the operation. /// A representing the running operation. - 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) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs index 3a23549cba..bdf1bbc0d0 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs @@ -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 /// readonly IAsyncDelayer asyncDelayer; + /// + /// The for the . + /// + readonly IJobManager jobManager; + /// /// The for the /// @@ -28,42 +34,42 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// Construct a /// + /// The value of . /// The value of /// The value of /// The value of 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)); } /// - 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(), 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(), - 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(), + settings), + ChatProvider.Discord => new DiscordProvider( + jobManager, + assemblyInformationProvider, + loggerFactory.CreateLogger(), + settings), + _ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider)), + }; } } } diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 8eba501262..8826d73929 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -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 /// public IDreamMaker DreamMaker { get; } - /// - /// The for the - /// - readonly IDatabaseContextFactory databaseContextFactory; - /// /// The for the /// @@ -101,7 +95,6 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - /// The value of /// The value of /// The value of /// The value of @@ -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(), 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) { diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 4866ab4987..916b6950f4 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -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; } } diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 0d003e0a91..044169edc7 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -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); } diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs index 3a42879935..c85171fb56 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs @@ -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; } /// - /// The being used + /// Gets the associated with the . /// - IDmbProvider Dmb { get; } + CompileJob CompileJob { get; } /// /// The current port DreamDaemon is listening on @@ -108,7 +109,7 @@ namespace Tgstation.Server.Host.Components.Session void EnableCustomChatCommands(); /// - /// Replace with a given , disposing the old one. + /// Replace the in use with a given , disposing the old one. /// /// The new . void ReplaceDmbProvider(IDmbProvider newProvider); diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index a5c1560ff9..e790feb310 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -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 } /// - public IDmbProvider Dmb - { - get - { - CheckDisposed(); - return reattachInformation.Dmb; - } - } + public Models.CompileJob CompileJob => reattachInformation.Dmb.CompileJob; /// 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()); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 528fbb706f..9ccd92a28c 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -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 /// public sealed override bool AlphaIsActive => true; - /// - public sealed override Models.CompileJob ActiveCompileJob => Server?.Dmb.CompileJob; - /// public sealed override RebootState? RebootState => Server?.RebootState; @@ -46,7 +42,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The for the . - /// The for the . /// The for the . /// The for the . /// The for the . @@ -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, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs index 1b822a7e65..919f3e0cf9 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs @@ -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; } /// - /// The currently running on the server + /// Retrieves the currently running on the server /// Models.CompileJob ActiveCompileJob { get; } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs index 51b6c7a7fe..cc03802ded 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs @@ -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 /// The for the . /// The for the . /// The for the . - /// The for the . /// The for the . /// The for the . /// The for the . @@ -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, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs index 30a8169e97..279fa3b1e9 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs @@ -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 /// /// The for the . /// The for the . - /// The for the . /// The for the . /// The for the . /// The for the . @@ -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, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 2fca625767..bc6eeda484 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -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 /// public abstract bool AlphaIsActive { get; } - /// - public abstract Models.CompileJob ActiveCompileJob { get; } - /// public DreamDaemonLaunchParameters ActiveLaunchParameters { get; protected set; } /// public DreamDaemonLaunchParameters LastLaunchParameters { get; protected set; } + /// + public Models.CompileJob ActiveCompileJob => GetActiveController()?.CompileJob; + /// public abstract RebootState? RebootState { get; } @@ -104,11 +102,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly ISessionPersistor sessionPersistor; - /// - /// The for the - /// - readonly IDatabaseContextFactory databaseContextFactory; - /// /// The for the . /// @@ -176,7 +169,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of - /// The value of /// The value of /// The to populate with /// The value of . @@ -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 diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs index dbf3fe82e9..423892a249 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs @@ -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 /// protected ILoggerFactory LoggerFactory { get; } - /// - /// The for the - /// - protected IDatabaseContextFactory DatabaseContextFactory { get; } - /// /// The for the /// @@ -52,21 +46,18 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// The value of /// The value of - /// The value of /// The value of /// The value of /// The containing the value of public WatchdogFactory( IServerControl serverControl, ILoggerFactory loggerFactory, - IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IAsyncDelayer asyncDelayer, IOptions 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, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index b719086eaa..2a084b150f 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -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 /// The for the . /// The for the . /// The for the . - /// The for the . /// The for the . /// The for the . /// The for the . @@ -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, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs index 1e52e7ecad..a597a80997 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs @@ -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 /// /// The for the . /// The for the . - /// The for the . /// The for the . /// The for the . /// The value of . @@ -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, diff --git a/src/Tgstation.Server.Host/Extensions/DatabaseCollectionExtensions.cs b/src/Tgstation.Server.Host/Extensions/DatabaseCollectionExtensions.cs new file mode 100644 index 0000000000..6599830847 --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/DatabaseCollectionExtensions.cs @@ -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 +{ + /// + /// Extension methods for the . + /// + static class DatabaseCollectionExtensions + { + /// + /// Gets the unattached, unpopulated with the name . + /// + /// The of s to operate on. + /// The for the operation. + /// A resulting in the unattached TGS on success, on failure. + public static Task GetTgsUser(this IDatabaseCollection databaseCollection, CancellationToken cancellationToken) + => databaseCollection + .AsQueryable() + .Where(x => x.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName)) + .Select(x => new User + { + Id = x.Id + }) + .FirstAsync(cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Jobs/IJobManager.cs b/src/Tgstation.Server.Host/Jobs/IJobManager.cs index 526ca4a437..567d622d40 100644 --- a/src/Tgstation.Server.Host/Jobs/IJobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/IJobManager.cs @@ -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 /// /// Registers a given and begins running it /// - /// The + /// The . Should at least have and . If is , the TGS user will be used. /// The for the . /// The for the operation /// A representing a running operation @@ -30,7 +30,7 @@ namespace Tgstation.Server.Host.Jobs /// Wait for a given to complete /// /// The to wait for - /// The to cancel the + /// The to cancel the . If the TGS user will be used. /// A that will cancel the /// The for the operation /// A representing the @@ -42,10 +42,15 @@ namespace Tgstation.Server.Host.Jobs /// Cancels a give /// /// The to cancel - /// The who cancelled the + /// The who cancelled the . If the TGS user will be used. /// If the operation should wait until the job exits before completing /// The for the operation /// A resulting in the updated if it was cancelled, if it couldn't be found. Task CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken); + + /// + /// Activate the . + /// + void Activate(); } } diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobManager.cs index 6ec0fdc7c3..ced1eb724b 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobManager.cs @@ -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 /// readonly Dictionary jobs; + /// + /// to delay starting jobs until the server is ready. + /// + readonly TaskCompletionSource activationTcs; + /// /// for various operations. /// @@ -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(); + activationTcs = new TaskCompletionSource(); 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 /// 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); } + + /// + public void Activate() + { + logger.LogTrace("Activating job manager..."); + activationTcs.SetResult(null); + } } } diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs index 8cf32df3e6..a035eee34c 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs @@ -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(); + mockSetup + .Setup(x => x.RegisterOperation(It.IsNotNull(), It.IsNotNull(), It.IsAny())) + .Callback((job, entrypoint, cancellationToken) => job.StartedBy ??= new User { }) + .Returns(Task.CompletedTask); + mockSetup + .Setup(x => x.WaitForJobCompletion(It.IsNotNull(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + mockJobManager = mockSetup.Object; } [TestMethod] - public void TestConstructionAndDisposal() + public async Task TestConstructionAndDisposal() { - Assert.ThrowsException(() => new DiscordProvider(null, null, null, 1)); + if (testToken1 == null) + Assert.Inconclusive("Required environment variable TGS4_TEST_DISCORD_TOKEN isn't set!"); + + Assert.ThrowsException(() => new DiscordProvider(null, null, null, null)); + Assert.ThrowsException(() => new DiscordProvider(mockJobManager, null, null, null)); var mockAss = new Mock(); - Assert.ThrowsException(() => new DiscordProvider(mockAss.Object, null, null, 1)); + Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockAss.Object, null, null)); var mockLogger = new Mock>(); - Assert.ThrowsException(() => new DiscordProvider(mockAss.Object, mockLogger.Object, null, 1)); - var mockToken = "asdf"; - Assert.ThrowsException(() => new DiscordProvider(mockAss.Object, mockLogger.Object, mockToken, 0)); - new DiscordProvider(mockAss.Object, mockLogger.Object, mockToken, 1).Dispose(); + Assert.ThrowsException(() => 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>(); - using var provider = new DiscordProvider(Mock.Of(), mockLogger.Object, "asdf", 1); - Assert.IsFalse(await provider.Connect(default).ConfigureAwait(false)); + await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, new ChatBot + { + ReconnectionInterval = 1, + ConnectionString = "asdf" + }); + await Assert.ThrowsExceptionAsync(() => 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>(); - using var provider = new DiscordProvider(Mock.Of(), mockLogger.Object, testToken1, 1); + await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), 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(() => provider.Connect(cancellationToken)).ConfigureAwait(false); + await Assert.ThrowsExceptionAsync(() => 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(() => provider.Disconnect(cancellationToken)).ConfigureAwait(false); Assert.IsTrue(provider.Connected); diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 7efaace32c..a82f8783e9 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -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) {