diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 91e79079aa..87bdb60cc0 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -61,6 +61,28 @@ Instead you can use object orientation, or simply placing repeated code in a fun ### No magic numbers or strings This means stuff like having a "mode" variable for an object set to "1" or "2" with no clear indicator of what that means. Make these `const string`s with a name that more clearly states what it's for. This is clearer and enhances readability of your code! Get used to doing it! +### Class Design Guidelines + +DO: + +- Use the sealed keyword where possible +- Use the readonly keyword where possible +- Use 1 line bodies (void X() => Y();) where possible (Excluding constructors) +- Use the factory pattern where reasonable +- Use the const keyword where possible +- Use the var keyword where possible +- Use the static keyword on member functions where possible +- Use CancellationTokens where possible +- Throw appropriate ArgumentExceptions for public functions + +DON'T: + +- Use the private keyword +- Use the internal keyword +- Use the static keyword on fields where avoidable +- Use the public keyword where avoidable +- Handle Tasks in a synchronous fashion + ### Versioning The version format we use is 4.\.\.\. The first number never changes and TGS 1/2/3/4 are to be considered seperate products. The numbers that follow are the semver. The criteria for changing a version number is as follows diff --git a/docs/API.dox b/docs/API.dox index 5dd4b2ce78..f8ddafc8a4 100644 --- a/docs/API.dox +++ b/docs/API.dox @@ -178,6 +178,45 @@ Instances are DreamDaemon server configurations, they live in their own director PUT "/Instance" @ref Tgstation.Server.Api.Models.Instance => @ref Tgstation.Server.Api.Models.Instance -The path +The user that creates an instance will be given full @ref Tgstation.Server.Api.Models.InstanceUser permission. The path must not exist at the time of creation. Support for attaching instances from backups is yet to come. + +A specific Instance may be retrieved with: + +GET "/Instances/{InstanceID}" => @ref Tgstation.Server.Api.Models.Instance + +Instances start offline, regardless of what was specified during the create request. An offline instance will return 403 for all requests made to it. + +To online or change other instance variables use the following request. Note that using this request (even with an empty object) will automatically give you the @ref Tgstation.Server.Api.Rights.InstanceUserRights.WriteUsers right for that instance if you don't have it + +POST "/Instance" @ref Tgstation.Server.Api.Models.Instance => @ref Tgstation.Server.Api.Models.Instance + +Note that onlining an offline instance will never automatically start DreamDaemon. That must be done as a seperate step. + +Instances can be detached which will delete all meta knowledge of the instance (Compile metadata, job metadata, repository commit metadata, Test merge metadata, etc...) but leave the files intact. That can be done with this request: + +DELETE "/Instance/{InstanceId}" => OK + +@subsection api_chat Chat Bots + +Each chat bot is represented by a @ref Tgstation.Server.Api.Models.ChatSettings object + +Chat bots can be created/updated/deleted with the following requests respectively + +I PUT "/Chat" @ref Tgstation.Server.Api.Models.ChatSettings => Tgstation.Server.Api.Models.ChatSettings +I POST "/Chat" @ref Tgstation.Server.Api.Models.ChatSettings => Tgstation.Server.Api.Models.ChatSettings +I DELETE "/Chat/{ChatSettingsId}" => OK + +The @ref Tgstation.Server.Api.Models.Internal.ChatSettings.ConnectionString must differ based on what kind of chat bot you wish to create + +For IRC chat bots it should be in the following format: +`";;;<1 to use SSL, 0 otherwise>[;<`The @ref Tgstation.Server.Api.Models.IrcPasswordType`;]"` + +For Discord chat bots it should be the bot's Token + +A specific bot's settings may be retrieved with: + +I GET "/Chat/{ChatSettingsId}" => @ref Tgstation.Server.Api.Models.ChatSettings + +Also note that if the @ref Tgstation.Server.Api.Models.ChatSettings.Channels is present in a POST request, the list will fully replace any active channels */ diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs index 399650332c..f66005b42d 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs @@ -100,6 +100,7 @@ namespace Tgstation.Server.Host.Components.Byond using (cancellationToken.Register(() => ourTcs.SetCanceled())) { await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); return; } try diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index 6dc8f86a6f..1ed91b26d1 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -28,6 +28,11 @@ namespace Tgstation.Server.Host.Components.Chat /// readonly IIOManager ioManager; + /// + /// The for the + /// + readonly ICommandFactory commandFactory; + /// /// The for the /// @@ -94,19 +99,17 @@ namespace Tgstation.Server.Host.Components.Chat /// The value of /// The value of /// The value of - /// The used to populate + /// The value of /// The used to populate - public Chat(IProviderFactory providerFactory, IIOManager ioManager, ILogger logger, ICommandFactory commandFactory, IEnumerable initialChatSettings) + public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, ILogger logger, IEnumerable initialChatSettings) { this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.initialChatSettings = initialChatSettings?.ToList() ?? throw new ArgumentNullException(nameof(initialChatSettings)); builtinCommands = new Dictionary(); - foreach (var I in commandFactory?.GenerateCommands() ?? throw new ArgumentNullException(nameof(commandFactory))) - builtinCommands.Add(I.Name.ToUpperInvariant(), I); - providers = new Dictionary(); mappedChannels = new Dictionary(); trackingContexts = new List(); @@ -189,7 +192,7 @@ namespace Tgstation.Server.Host.Components.Chat message.User.Channel.RealId = enumerable.First().Key; } - var splits = new List(message.Content.TrimEnd().Split(' ')); + var splits = new List(message.Content.Trim().Split(' ')); var address = splits[0]; if (address.Length > 1 && (address[address.Length - 1] == ':' || address[address.Length - 1] == ',')) address = address.Substring(0, address.Length - 1); @@ -205,7 +208,7 @@ namespace Tgstation.Server.Host.Components.Chat if (addressed) splits.RemoveAt(0); - if ((splits.Count == 1 && (!message.User.Channel.IsPrivate || splits[0].Length == 0)) || splits.Count == 0) + if (splits.Count == 0 || (!addressed && splits.Count == 1)) { //just a mention await SendMessage("Hi!", new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false); @@ -461,6 +464,8 @@ namespace Tgstation.Server.Host.Components.Chat /// public async Task StartAsync(CancellationToken cancellationToken) { + foreach (var I in commandFactory.GenerateCommands()) + builtinCommands.Add(I.Name.ToUpperInvariant(), I); await Task.WhenAll(initialChatSettings.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(initialChatSettings.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs index a5624d7f20..cd23992477 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs @@ -45,6 +45,6 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public IChat CreateChat(IEnumerable initialChatSettings) => new Chat(providerFactory, ioManager, loggerFactory.CreateLogger(), commandFactory, initialChatSettings); + public IChat CreateChat(IEnumerable initialChatSettings) => new Chat(providerFactory, ioManager, commandFactory, loggerFactory.CreateLogger(), initialChatSettings); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs index 7d2d8b0244..5f8acb3c5d 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using Tgstation.Server.Host.Components.Byond; +using Tgstation.Server.Host.Components.Repository; +using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Core; namespace Tgstation.Server.Host.Components.Chat.Commands @@ -18,23 +20,66 @@ namespace Tgstation.Server.Host.Components.Chat.Commands /// readonly IByondManager byondManager; + /// + /// The for the + /// + readonly IRepositoryManager repositoryManager; + + /// + /// The for the + /// + readonly IDatabaseContextFactory databaseContextFactory; + + /// + /// The for the + /// + readonly Models.Instance instance; + + /// + /// The for the + /// + IWatchdog watchdog; + /// /// Construct a /// /// The value of /// The value of - public CommandFactory(IApplication application, IByondManager byondManager) + /// The value of + /// The value of + /// The value of + public CommandFactory(IApplication application, IByondManager byondManager, IRepositoryManager repositoryManager, IDatabaseContextFactory databaseContextFactory, Models.Instance instance) { this.application = application ?? throw new ArgumentNullException(nameof(application)); this.byondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager)); + this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); + this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); + this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); + } + + /// + /// Set a for the + /// + /// The to set + public void SetWatchdog(IWatchdog watchdog) + { + if (this.watchdog != null) + throw new InvalidOperationException("SetWatchdog has already been called!"); + this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog)); } /// - public IReadOnlyList GenerateCommands() => new List + public IReadOnlyList GenerateCommands() { - new VersionCommand(application), - new ByondCommand(byondManager), - new KekCommand() - }; + if (watchdog == null) + throw new InvalidOperationException("SetWatchdog has not been called!"); + return new List + { + new VersionCommand(application), + new ByondCommand(byondManager), + new PullRequestsCommand(watchdog, repositoryManager, databaseContextFactory, instance), + new KekCommand() + }; + } } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs new file mode 100644 index 0000000000..f3e3233c9c --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs @@ -0,0 +1,98 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Components.Repository; +using Tgstation.Server.Host.Components.Watchdog; +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Components.Chat.Commands +{ + /// + /// Command for reading the active s + /// + sealed class PullRequestsCommand : ICommand + { + /// + public string Name => "prs"; + + /// + public string HelpText => "Display live test merge pull request numbers. Add --repo to view repository test merges"; + + /// + public bool AdminOnly => false; + + /// + /// The for the + /// + readonly IWatchdog watchdog; + + /// + /// The for the + /// + readonly IRepositoryManager repositoryManager; + + /// + /// The for the + /// + readonly IDatabaseContextFactory databaseContextFactory; + + /// + /// The for the + /// + readonly Models.Instance instance; + + /// + /// Construct a + /// + /// The value of + /// The value of + /// The value of + /// The value of + public PullRequestsCommand(IWatchdog watchdog, IRepositoryManager repositoryManager, IDatabaseContextFactory databaseContextFactory, Models.Instance instance) + { + this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog)); + this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); + this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); + this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); + } + + /// + public async Task Invoke(string arguments, User user, CancellationToken cancellationToken) + { + IEnumerable results = null; + if (arguments.Split(' ').Any(x => x.ToUpperInvariant() == "--REPO")) + { + string head; + using (var repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) + { + if (repo == null) + return "Repository unavailable!"; + head = repo.Head; + } + await databaseContextFactory.UseContext(async db => results = await db.RevisionInformations.Where(x => x.Instance.Id == instance.Id && x.CommitSha == head) + .SelectMany(x => x.ActiveTestMerges) + .Select(x => x.TestMerge) + .Select(x => new Models.TestMerge + { + Number = x.Number, + PullRequestRevision = x.PullRequestRevision + }).ToListAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + } + else + { + if (!watchdog.Running) + return "Server offline!"; + results = watchdog.ActiveCompileJob?.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList() ?? new List(); + } + + if (!results.Any()) + return "None!"; + + return String.Join(", ", results.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0} as {1}", x.Number, x.PullRequestRevision.Substring(0, 7)))); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 794421827b..37abf31b15 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -76,6 +76,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// Task listenTask; + /// + /// If we are disconnecting + /// + bool disconnecting; + /// /// Construct an /// @@ -130,13 +135,17 @@ namespace Tgstation.Server.Host.Components.Chat.Providers channelIdMap = new Dictionary(); queryChannelIdMap = new Dictionary(); channelIdCounter = 1; + disconnecting = false; } /// public override void Dispose() { - if(Connected) + if (Connected) + { + disconnecting = true; client.Disconnect(); //just closes the socket + } } /// @@ -209,6 +218,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// public override Task Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() => { + disconnecting = false; lock (this) try { @@ -276,9 +286,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers listenTask = Task.Factory.StartNew(() => { - while (client.IsConnected) + while (!disconnecting && client.IsConnected) { client.ListenOnce(true); + if (disconnecting || !client.IsConnected) + break; client.Listen(false); //ensure we have the correct nick if (client.Nickname != nickname && client.GetIrcUser(nickname) == null) @@ -304,7 +316,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers { try { - client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); //priocritical otherwise Disconnect will hard block + client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); //priocritical otherwise it wont go through } catch (Exception e) { diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs index 01484c229d..0c6b7920fb 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs @@ -61,6 +61,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers var cancelTcs = new TaskCompletionSource(); using (cancellationToken.Register(() => cancelTcs.SetCanceled())) await Task.WhenAny(nextMessage.Task, cancelTcs.Task).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); lock (messageQueue) { var result = messageQueue.Dequeue(); diff --git a/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs index c6625bbaeb..ba9d935d71 100644 --- a/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs @@ -124,7 +124,7 @@ namespace Tgstation.Server.Host.Components.Compiler var timeoutTask = Task.Delay(timeoutAt - DateTimeOffset.Now, cancellationToken); await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false); - + cancellationToken.ThrowIfCancellationRequested(); if (!controller.Lifetime.IsCompleted) return false; diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index d6c4ded3d1..c200766fc8 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -57,11 +57,6 @@ namespace Tgstation.Server.Host.Components /// readonly IExecutor executor; - /// - /// The for the - /// - readonly ICommandFactory commandFactory; - /// /// The for the /// @@ -98,13 +93,12 @@ 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 /// The value of /// The value of - public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, IServerControl serverUpdater, ICryptographySuite cryptographySuite, IExecutor executor, ICommandFactory commandFactory, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IProviderFactory providerFactory, IScriptExecutor scriptExecutor) + public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, IServerControl serverUpdater, ICryptographySuite cryptographySuite, IExecutor executor, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IProviderFactory providerFactory, IScriptExecutor scriptExecutor) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); @@ -114,7 +108,6 @@ namespace Tgstation.Server.Host.Components this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater)); this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite )); this.executor = executor ?? throw new ArgumentNullException(nameof(executor)); - this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory)); this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager)); this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory)); this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller)); @@ -145,7 +138,7 @@ namespace Tgstation.Server.Host.Components { var byond = new ByondManager(byondIOManager, byondInstaller, loggerFactory.CreateLogger()); - var commandFactory = new CommandFactory(application, byond); + var commandFactory = new CommandFactory(application, byond, repoManager, databaseContextFactory, metadata); var chatFactory = new ChatFactory(instanceIoManager, loggerFactory, commandFactory, providerFactory); var chat = chatFactory.CreateChat(metadata.ChatSettings); @@ -156,6 +149,7 @@ namespace Tgstation.Server.Host.Components var watchdogFactory = new WatchdogFactory(chat, sessionControllerFactory, serverUpdater, loggerFactory, reattachInfoHandler, databaseContextFactory, byondTopicSender, eventConsumer, metadata.CloneMetadata()); var watchdog = watchdogFactory.CreateWatchdog(dmbFactory, metadata.DreamDaemonSettings); eventConsumer.SetWatchdog(watchdog); + commandFactory.SetWatchdog(watchdog); try { var dreamMaker = new DreamMaker(byond, ioManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, loggerFactory.CreateLogger()); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs index c7533c58ca..21a45d3e60 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs @@ -21,6 +21,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// bool AlphaIsActive { get; } + /// + /// The currently running on the server + /// + Models.CompileJob ActiveCompileJob { get; } + /// /// The latest of the twin servers /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index 6c0d58c864..315685ad42 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -29,6 +29,9 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public bool AlphaIsActive { get; private set; } + /// + public Models.CompileJob ActiveCompileJob => (AlphaIsActive ? alphaServer : bravoServer)?.Dmb.CompileJob; + /// public LaunchResult LastLaunchResult { get; private set; } @@ -494,6 +497,7 @@ namespace Tgstation.Server.Host.Components.Watchdog if (monitorState.RebootingInactiveServer) toWaitOn = Task.WhenAny(toWaitOn, inactiveServerStartup); await toWaitOn.ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); } var chatTask = Task.CompletedTask; @@ -680,6 +684,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var cancelTcs = new TaskCompletionSource(); using (cancellationToken.Register(() => cancelTcs.SetCanceled())) await Task.WhenAny(allTask, cancelTcs.Task).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); //update the live and staged jobs in the db await databaseContextFactory.UseContext(async db => diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index 5d0b7123d0..b97a3eb2e3 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -170,9 +170,22 @@ namespace Tgstation.Server.Host.Controllers if(ModelState?.IsValid == false) { - var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage); - await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false); - return; + var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage).ToList(); + //do some fuckery to remove RequiredAttribute errors + for (var I = 0; I < errorMessages.Count; ++I) + { + var message = errorMessages[I]; + if (message.StartsWith("The ", StringComparison.Ordinal) && message.EndsWith(" field is required.", StringComparison.Ordinal)) + { + errorMessages.RemoveAt(I); + --I; + } + } + if (errorMessages.Count > 0) + { + await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false); + return; + } } Logger.LogInformation("Request made by User ID {0}. Api version: {1}. User-Agent: {2}", AuthenticationContext?.User.Id.ToString(CultureInfo.InvariantCulture) ?? "NULL", ApiHeaders.ApiVersion, ApiHeaders.UserAgent); diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 5dc34e3462..0b97551b2a 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -12,7 +12,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; -using Octokit; using System; using System.Globalization; using System.IdentityModel.Tokens.Jwt; @@ -23,7 +22,6 @@ using System.Threading.Tasks; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; -using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.StaticFiles; using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Configuration; @@ -151,6 +149,7 @@ namespace Tgstation.Server.Host.Core options.AllowInputFormatterExceptionMessages = true; options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; options.SerializerSettings.CheckAdditionalContent = true; + options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error; }); var databaseConfiguration = databaseConfigurationSection.Get(); @@ -205,7 +204,6 @@ namespace Tgstation.Server.Host.Core } services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(new ByondTopicSender diff --git a/src/Tgstation.Server.Host/Core/JobHandler.cs b/src/Tgstation.Server.Host/Core/JobHandler.cs index d80bbd1322..72fc7faa40 100644 --- a/src/Tgstation.Server.Host/Core/JobHandler.cs +++ b/src/Tgstation.Server.Host/Core/JobHandler.cs @@ -47,6 +47,7 @@ namespace Tgstation.Server.Host.Core TaskCompletionSource tcs = new TaskCompletionSource(); using (cancellationToken.Register(() => tcs.SetCanceled())) await Task.WhenAny(tcs.Task, task).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); } ///