From 1693580c26da55ea938e8e85494c57496a609a13 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 3 Aug 2018 13:44:21 -0400 Subject: [PATCH 01/10] Convert all bad requests to use the ErrorMessage model --- .../Controllers/AdministrationController.cs | 4 ++-- .../Controllers/ApiController.cs | 9 +++++---- .../Controllers/ByondController.cs | 6 +++--- .../Controllers/ChatController.cs | 10 +++++----- .../Controllers/DreamDaemonController.cs | 3 +++ .../Controllers/HomeController.cs | 3 +-- .../Controllers/InstanceController.cs | 11 +++++++---- .../Controllers/InstanceUserController.cs | 2 +- .../Controllers/RepositoryController.cs | 16 ++++++++-------- .../Controllers/UserController.cs | 12 ++++++------ 10 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index 9b977a548a..56215e8a08 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -121,10 +121,10 @@ namespace Tgstation.Server.Host.Controllers throw new ArgumentNullException(nameof(model)); if (model.CurrentVersion == null) - return BadRequest(new { message = "Missing new version!" }); + return BadRequest(new ErrorMessage { Message = "Missing new version!" }); if (model.CurrentVersion.Major != application.Version.Major) - return BadRequest(new { message = "Cannot update to a different suite version!" }); + return BadRequest(new ErrorMessage { Message = "Cannot update to a different suite version!" }); IEnumerable releases; try diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index a2e06dc537..5d0b7123d0 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; @@ -48,7 +49,7 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the operation /// - protected Instance Instance { get; } + protected Models.Instance Instance { get; } /// /// If permissions are required to access the @@ -150,7 +151,7 @@ namespace Tgstation.Server.Host.Controllers { if(!ApiHeaders.InstanceId.HasValue) { - await BadRequest(new { message = "Missing InstanceId header!" }).ExecuteResultAsync(context).ConfigureAwait(false); + await BadRequest(new ErrorMessage { Message = "Missing InstanceId header!" }).ExecuteResultAsync(context).ConfigureAwait(false); return; } if (AuthenticationContext.InstanceUser == null) @@ -163,14 +164,14 @@ namespace Tgstation.Server.Host.Controllers } catch (InvalidOperationException e) { - await BadRequest(new { message = e.Message }).ExecuteResultAsync(context).ConfigureAwait(false); + await BadRequest(new ErrorMessage { Message = e.Message }).ExecuteResultAsync(context).ConfigureAwait(false); return; } if(ModelState?.IsValid == false) { var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage); - await BadRequest(new { message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false); + await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false); return; } diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index 2cb13a0652..0d89f56d12 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -65,10 +65,10 @@ namespace Tgstation.Server.Host.Controllers public override async Task Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken) { if (model == null) - return BadRequest(new { message = "Missing request model!" }); + throw new ArgumentNullException(nameof(model)); - if(model.Version == null) - return BadRequest(new { message = "Missing version!" }); + if (model.Version == null) + return BadRequest(new ErrorMessage { Message = "Missing version!" }); var byondManager = instanceManager.GetInstance(Instance).ByondManager; diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index d6f66523c8..1084b7988d 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -62,16 +62,16 @@ namespace Tgstation.Server.Host.Controllers throw new ArgumentNullException(nameof(model)); if (String.IsNullOrWhiteSpace(model.Name)) - return BadRequest(new { message = "name cannot be null or whitespace!" }); + return BadRequest(new ErrorMessage { Message = "name cannot be null or whitespace!" }); if (String.IsNullOrWhiteSpace(model.ConnectionString)) - return BadRequest(new { message = "connection_string cannot be null or whitespace!" }); + return BadRequest(new ErrorMessage { Message = "connection_string cannot be null or whitespace!" }); if (!model.Provider.HasValue) - return BadRequest(new { message = "provider cannot be null!" }); + return BadRequest(new ErrorMessage { Message = "provider cannot be null!" }); if (!model.Enabled.HasValue) - return BadRequest(new { message = "enabled cannot be null!" }); + return BadRequest(new ErrorMessage { Message = "enabled cannot be null!" }); //try to update das db first var dbModel = new Models.ChatSettings @@ -115,7 +115,7 @@ namespace Tgstation.Server.Host.Controllers } catch (InvalidOperationException e) { - return BadRequest(new { message = e.Message }); + return BadRequest(new ErrorMessage { Message = e.Message }); } return Json(dbModel.ToApi()); } diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index f91384a46f..5e9cf98168 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -134,6 +134,9 @@ namespace Tgstation.Server.Host.Controllers [TgsAuthorize(DreamDaemonRights.SetAutoStart | DreamDaemonRights.SetPorts | DreamDaemonRights.SetSecurity | DreamDaemonRights.SetWebClient | DreamDaemonRights.SoftRestart | DreamDaemonRights.SoftShutdown | DreamDaemonRights.Start | DreamDaemonRights.SetStartupTimeout)] public override async Task Update([FromBody] DreamDaemon model, CancellationToken cancellationToken) { + if (model == null) + throw new ArgumentNullException(nameof(model)); + //alias for changing DD settings var current = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => x.DreamDaemonSettings).FirstAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index 72b266e87d..bc1b3f63fa 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; using System.Linq; -using System.Net; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; @@ -77,7 +76,7 @@ namespace Tgstation.Server.Host.Controllers public async Task CreateToken(CancellationToken cancellationToken) { if (ApiHeaders.IsTokenAuthentication) - return BadRequest(new { message = "Cannot create a token using another token!" }); + return BadRequest(new Api.Models.ErrorMessage { Message = "Cannot create a token using another token!" }); ISystemIdentity identity; try diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 04530c31bc..f487909233 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -93,16 +93,19 @@ namespace Tgstation.Server.Host.Controllers [TgsAuthorize(InstanceManagerRights.Create)] public override async Task Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken) { + if (model == null) + throw new ArgumentNullException(nameof(model)); + if (String.IsNullOrWhiteSpace(model.Name)) - return BadRequest(new { message = "name must not be empty!" }); + return BadRequest(new ErrorMessage { Message = "name must not be empty!" }); if(model.Path == null) - return BadRequest(new { message = "path must not be empty!" }); + return BadRequest(new ErrorMessage { Message = "path must not be empty!" }); NormalizeModelPath(model, out var rawPath); var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken); if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false)) - return Conflict(new { message = "Path not empty!" }); + return Conflict(new ErrorMessage { Message = "Path not empty!" }); var newInstance = new Models.Instance { @@ -338,7 +341,7 @@ namespace Tgstation.Server.Host.Controllers public override Task Read(CancellationToken cancellationToken) { if (Instance == null) - return Task.FromResult(BadRequest(new { message = "No instance specified" })); + return Task.FromResult(BadRequest(new ErrorMessage { Message = "No instance specified" })); return Task.FromResult(Json(Instance.ToApi())); } } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs index 7d2deb2d37..e7aff815c6 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs @@ -41,7 +41,7 @@ namespace Tgstation.Server.Host.Controllers throw new ArgumentNullException(nameof(model)); if (!model.UserId.HasValue) - return BadRequest(new { message = "Missing UserId!" }); + return BadRequest(new ErrorMessage { Message = "Missing UserId!" }); return null; } diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index c919b417fa..5b0209649f 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -110,13 +110,13 @@ namespace Tgstation.Server.Host.Controllers public override async Task Create([FromBody] Repository model, CancellationToken cancellationToken) { if (model == null) - return BadRequest(new { message = "Missing request model!" }); + throw new ArgumentNullException(nameof(model)); if (model.Origin == null) - return BadRequest(new { message = "Missing repo origin!" }); + return BadRequest(new ErrorMessage { Message = "Missing repo origin!" }); if (model.AccessUser == null ^ model.AccessToken == null) - return BadRequest(new { message = "Either both accessToken and accessUser must be present or neither!" }); + return BadRequest(new ErrorMessage { Message = "Either both accessToken and accessUser must be present or neither!" }); var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); @@ -229,19 +229,19 @@ namespace Tgstation.Server.Host.Controllers public override async Task Update([FromBody]Repository model, CancellationToken cancellationToken) { if (model == null) - return BadRequest(new { message = "Missing request model!" }); + throw new ArgumentNullException(nameof(model)); if (model.AccessUser == null ^ model.AccessToken == null) - return BadRequest(new { message = "Either both accessToken and accessUser must be present or neither!" }); + return BadRequest(new ErrorMessage { Message = "Either both accessToken and accessUser must be present or neither!" }); if (model.CheckoutSha != null && model.Reference != null) - return BadRequest(new { message = "Only one of sha or reference may be specified!" }); + return BadRequest(new ErrorMessage { Message = "Only one of sha or reference may be specified!" }); if (model.CheckoutSha != null && model.UpdateFromOrigin == true) - return BadRequest(new { message = "Cannot update a reference when checking out a sha!" }); + return BadRequest(new ErrorMessage { Message = "Cannot update a reference when checking out a sha!" }); if (model.Origin != null) - return BadRequest(new { message = "origin cannot be modified without deleting the repository!" }); + return BadRequest(new ErrorMessage { Message = "origin cannot be modified without deleting the repository!" }); var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0; var userRights = (RepositoryRights)AuthenticationContext.GetRight(RightsType.Repository); diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 756e2e3067..3e4833ad79 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -68,14 +68,14 @@ namespace Tgstation.Server.Host.Controllers throw new ArgumentNullException(nameof(model)); if (!(model.Password == null ^ model.SystemIdentifier == null)) - return BadRequest(new { message = "User must have exactly one of either a password or system identifier!" }); + return BadRequest(new ErrorMessage { Message = "User must have exactly one of either a password or system identifier!" }); model.Name = model.Name?.Trim(); if (model.Name?.Length == 0) model.Name = null; if (!(model.Name == null ^ model.SystemIdentifier == null)) - return BadRequest(new { message = "User must have a name if and only if user has no system identifier!" }); + return BadRequest(new ErrorMessage { Message = "User must have a name if and only if user has no system identifier!" }); var dbUser = new Models.User { @@ -107,7 +107,7 @@ namespace Tgstation.Server.Host.Controllers else { if (model.Password.Length < generalConfiguration.MinimumPasswordLength) - return BadRequest(new { message = String.Format(CultureInfo.InvariantCulture, "Password must be at least {0} characters long!", generalConfiguration.MinimumPasswordLength) }); + return BadRequest(new ErrorMessage { Message = String.Format(CultureInfo.InvariantCulture, "Password must be at least {0} characters long!", generalConfiguration.MinimumPasswordLength) }); cryptographySuite.SetUserPassword(dbUser, model.Password); } @@ -154,14 +154,14 @@ namespace Tgstation.Server.Host.Controllers if (model.Password != null) { if (originalUser.PasswordHash == null) - return BadRequest(new { message = "Cannot convert a system user to a password user!" }); + return BadRequest(new ErrorMessage { Message = "Cannot convert a system user to a password user!" }); cryptographySuite.SetUserPassword(originalUser, model.Password); } else if(model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier) - return BadRequest(new { message = "Cannot change a user's system identifier!" }); + return BadRequest(new ErrorMessage { Message = "Cannot change a user's system identifier!" }); if (model.Name != null && model.Name.ToUpperInvariant() != originalUser.CanonicalName) - return BadRequest(new { message = "Can only change capitalization of a user's name!" }); + return BadRequest(new ErrorMessage { Message = "Can only change capitalization of a user's name!" }); originalUser.InstanceManagerRights = model.InstanceManagerRights ?? originalUser.InstanceManagerRights; originalUser.AdministrationRights = model.AdministrationRights ?? originalUser.AdministrationRights; From 9fcda67725a3792b0633f722c4368d1ef5023901 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 3 Aug 2018 13:54:07 -0400 Subject: [PATCH 02/10] Buff CONTRIBUTING.md --- .github/CONTRIBUTING.md | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 0ef7f5fe5e..91e79079aa 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -20,19 +20,9 @@ You can of course, as always, ask for help at [#coderbus](irc://irc.rizon.net/co ### Development Environment -We reccommend any Visual Studio version that can support the .NET framework v4.5.2. However, the project should be buildable with any C# compiler than can read .sln files. Once installed, simply double-click TGStationServer3.sln to open it. +You need the Dotnet SDK to compile the main server and command line programs. In order to build the service version and control panel you also need a .NET 4.7.1 build chain -#### Installing Dependencies - -Visual Studio comes with the nuget package manager. To install the dependencies, right-click the solution and select `Restore NuGet Packages`. If you are using some other development environment, you can download nuget [here](https://dist.nuget.org/win-x86-commandline/latest/nuget.exe) as a single CLI executable. Then simply run `nuget restore TGStationServer3.sln` from the root of the project directory. - -##### (Optional) Installing WiX Toolset Visual Studio Extension - -The [WiX Toolset](http://wixtoolset.org/) is used for creating the installer .msi (Not the .exe, which is a standard C# program wrapper to the .msi). Building and modifying this is not required for debugging and development of the service but necessary if you want to debug tweaks to the installer configuration. The WiX toolset is bundled as a nuget package with the solution, all that is required to use it is the VS extension found [here](https://marketplace.visualstudio.com/items?itemName=RobMensching.WixToolsetVisualStudio2017Extension). This will allow you to build `TGS.Installer.wixproj` just like all the other projects. - -##### Debugging - -Be careful while debugging. The service runs with root level privileges and you wouldn't want any [accidents](http://i.imgur.com/zvGEpJD.png) to happen, would you? +The recommended IDE is visual studio 2017 which has installation options for both of these. ## Meet the Team @@ -69,7 +59,7 @@ Copying code from one place to another may be suitable for small, short-time pro Instead you can use object orientation, or simply placing repeated code in a function, to obey this specification easily. ### 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 #defines 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! +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! ### Versioning @@ -113,12 +103,8 @@ This prevents nesting levels from getting deeper then they need to be. ### Other Notes * Code should be modular where possible; if you are working on a new addition, then strongly consider putting it in its own file unless it makes sense to put it with similar ones. -* Bloated code may be necessary to add a certain feature, which means there has to be a judgement over whether the feature is worth having or not. You can help make this decision easier by making sure your code is modular. - * You are expected to help maintain the code that you add, meaning that if there is a problem then you are likely to be approached in order to fix any issues, runtimes, or bugs. -* If you used regex to replace code during development of your code, post the regex in your PR for the benefit of future developers and downstream users. - ## Pull Request Process There is no strict process when it comes to merging pull requests. Pull requests will sometimes take a while before they are looked at by a maintainer; the bigger the change, the more time it will take before they are accepted into the code. Every team member is a volunteer who is giving up their own time to help maintain and contribute, so please be courteous and respectful. Here are some helpful ways to make it easier for you and for the maintainers when making a pull request. @@ -143,9 +129,3 @@ Just becuase something isn't on this list doesn't mean that it's acceptable. Use ## A word on Git Yes, we know that the files have a tonne of mixed Windows and Linux line endings. Attempts to fix this have been met with less than stellar success, and as such we have decided to give up caring until there comes a time when it matters. - -Therefore, EOF settings of main repo are forbidden territory one must avoid wandering into, at risk of losing body and/or mind to the Git gods. - -## Other Notes - -* Feel free to add your nuget account to TGServiceInterface/Packages.nuspec authors list if you modify the interface From 3a2d78a6897a8c60af95bc6efcd21470b3745662 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 3 Aug 2018 13:56:52 -0400 Subject: [PATCH 03/10] Lies and slander --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 85a09ea251..f7facb88a4 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,12 @@ [![forthebadge](http://forthebadge.com/images/badges/built-with-love.svg)](http://forthebadge.com) [![forthebadge](http://forthebadge.com/images/badges/60-percent-of-the-time-works-every-time.svg)](http://forthebadge.com) -This is a toolset to manage a production BYOND server. It includes the ability to update the server without having to stop or shutdown the server (the update will take effect on a "reboot" of the server) the ability start the server and restart it if it crashes, as well as systems for fixing errors and merging GitHub Pull Requests locally. +This is a toolset to manage production BYOND servers. It includes the ability to update the server without having to stop or shutdown the server (the update will take effect on a "reboot" of the server) the ability start the server and restart it if it crashes, as well as systems for fixing errors and merging GitHub Pull Requests locally. Generally, updates force a live tracking of the configured git repo, resetting local modifications. If you plan to make modifications, set up a new git repo to store your version of the code in, and point this script to that in the config (explained below). This can be on GitHub or a local repo using file:/// urls. ### Legacy Servers -* Versions 3 and 4 can be found in the `legacy/` directory +* Older server versions can be found in the V# branches of this repository ## CONTRIBUTING From 6da3b6d5737035dfbf320cb246a22856ad8783d1 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 3 Aug 2018 14:34:32 -0400 Subject: [PATCH 04/10] Adds ByondCommand --- .../Components/Chat/Commands/ByondCommand.cs | 40 +++++++++++++++++++ .../Chat/Commands/CommandFactory.cs | 15 +++++-- .../Components/InstanceFactory.cs | 6 +-- 3 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/Chat/Commands/ByondCommand.cs diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/ByondCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/ByondCommand.cs new file mode 100644 index 0000000000..e9cb16011b --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/ByondCommand.cs @@ -0,0 +1,40 @@ +using System; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Components.Byond; + +namespace Tgstation.Server.Host.Components.Chat.Commands +{ + /// + /// For displaying the installed Byond version + /// + sealed class ByondCommand : ICommand + { + /// + public string Name => "byond"; + + /// + public string HelpText => "Displays the installed Byond version"; + + /// + public bool AdminOnly => false; + + /// + /// the for the + /// + readonly IByondManager byondManager; + + /// + /// Construct a + /// + /// The value of + public ByondCommand(IByondManager byondManager) + { + this.byondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager)); + } + + /// + public Task Invoke(string arguments, User user, CancellationToken cancellationToken) => Task.FromResult(byondManager.ActiveVersion == null ? "None!" : String.Format(CultureInfo.InvariantCulture, "{0}.{1}", byondManager.ActiveVersion.Major, byondManager.ActiveVersion.Minor)); + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs index a9bd873534..7d2d8b0244 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Core; namespace Tgstation.Server.Host.Components.Chat.Commands @@ -12,20 +13,28 @@ namespace Tgstation.Server.Host.Components.Chat.Commands /// readonly IApplication application; + /// + /// The for the + /// + readonly IByondManager byondManager; + /// /// Construct a /// /// The value of - public CommandFactory(IApplication application) + /// The value of + public CommandFactory(IApplication application, IByondManager byondManager) { this.application = application ?? throw new ArgumentNullException(nameof(application)); + this.byondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager)); } /// public IReadOnlyList GenerateCommands() => new List { - new KekCommand(), - new VersionCommand(application) + new VersionCommand(application), + new ByondCommand(byondManager), + new KekCommand() }; } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 2994bf1ef5..d6c4ded3d1 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -140,14 +140,14 @@ namespace Tgstation.Server.Host.Components var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, metadata.CloneMetadata()); try { - var commandFactory = new CommandFactory(application); - var chatFactory = new ChatFactory(instanceIoManager, loggerFactory, commandFactory, providerFactory); - var repoManager = new RepositoryManager(metadata.RepositorySettings, repoIoManager, eventConsumer); try { var byond = new ByondManager(byondIOManager, byondInstaller, loggerFactory.CreateLogger()); + var commandFactory = new CommandFactory(application, byond); + var chatFactory = new ChatFactory(instanceIoManager, loggerFactory, commandFactory, providerFactory); + var chat = chatFactory.CreateChat(metadata.ChatSettings); try { From 90585a64050d48a84432963b4661b82bad339ff8 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 3 Aug 2018 14:34:54 -0400 Subject: [PATCH 05/10] Prevents offline instances from recieving requests --- .../Security/AuthenticationContextFactory.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs index 49cb04f4a2..258bf0dd3d 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs @@ -50,8 +50,7 @@ namespace Tgstation.Server.Host.Security var userQuery = databaseContext.Users.Where(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken); var instanceUser = instanceId.HasValue ? (await databaseContext.InstanceUsers - .Where(x => x.UserId == userId) - .Where(x => x.InstanceId == instanceId) + .Where(x => x.UserId == userId && x.InstanceId == instanceId && x.Instance.Online.Value) .Include(x => x.Instance) .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false)) : null; From e2eea59468a51f11eb7a51b1226cb2dd01e63117 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 3 Aug 2018 16:42:50 -0400 Subject: [PATCH 06/10] Various things: - Class design tips in CONTRIBUTING - Finish Instance manager and chat documentation - Fix Task.WhenAny cancellation gotcha - Add PullRequestsCommand - Fix JSON "Required" fields - Fix chat bot Hi! shennanigans --- .github/CONTRIBUTING.md | 22 +++++ docs/API.dox | 41 +++++++- .../Components/Byond/ByondManager.cs | 1 + .../Components/Chat/Chat.cs | 19 ++-- .../Components/Chat/ChatFactory.cs | 2 +- .../Chat/Commands/CommandFactory.cs | 57 +++++++++-- .../Chat/Commands/PullRequestsCommand.cs | 98 +++++++++++++++++++ .../Components/Chat/Providers/IrcProvider.cs | 18 +++- .../Components/Chat/Providers/Provider.cs | 1 + .../Components/Compiler/DreamMaker.cs | 2 +- .../Components/InstanceFactory.cs | 12 +-- .../Components/Watchdog/IWatchdog.cs | 5 + .../Components/Watchdog/Watchdog.cs | 5 + .../Controllers/ApiController.cs | 19 +++- src/Tgstation.Server.Host/Core/Application.cs | 4 +- src/Tgstation.Server.Host/Core/JobHandler.cs | 1 + 16 files changed, 273 insertions(+), 34 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs 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(); } /// From 956699151371ef6617cd248e5cc4ac4d40f4483b Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 3 Aug 2018 16:49:11 -0400 Subject: [PATCH 07/10] Omg just keep it fucking simple --- src/Tgstation.Server.Host/Components/Chat/Chat.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index 1ed91b26d1..c8e3b77ae7 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -208,7 +208,7 @@ namespace Tgstation.Server.Host.Components.Chat if (addressed) splits.RemoveAt(0); - if (splits.Count == 0 || (!addressed && splits.Count == 1)) + if (splits.Count == 0) { //just a mention await SendMessage("Hi!", new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false); From 04c1f0d7fea75c9162f3e16fd5e0b57d8fa17a8e Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 3 Aug 2018 16:51:32 -0400 Subject: [PATCH 08/10] Memes make the world go round --- .../Components/Chat/Providers/IrcProvider.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 37abf31b15..b10b82b0fc 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -19,6 +19,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers { const int TimeoutSeconds = 5; + const string QuitMeme = "Mr. Stark, I don't feel so good..."; + /// public override bool Connected => client.IsConnected; @@ -316,7 +318,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers { try { - client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); //priocritical otherwise it wont go through + client.RfcQuit(QuitMeme, Priority.Critical); //priocritical otherwise it wont go through } catch (Exception e) { @@ -348,7 +350,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers toPart.Add(I); foreach (var I in toPart) - client.RfcPart(I); + client.RfcPart(I, QuitMeme); foreach (var I in hs) client.RfcJoin(I); From e74a62b206594c801c01b1c6151ada3c0b6ce600 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 3 Aug 2018 20:42:28 -0400 Subject: [PATCH 09/10] Various cleanups --- .../Components/Watchdog/SessionController.cs | 3 ++- .../Controllers/ConfigurationController.cs | 12 ++++++------ .../Controllers/InstanceController.cs | 12 ++++++++---- .../Controllers/InstanceUserController.cs | 4 ++-- src/Tgstation.Server.Host/Models/DatabaseContext.cs | 2 ++ 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs index 757734d4f1..b2ecc29db6 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Components.Chat; namespace Tgstation.Server.Host.Components.Watchdog @@ -233,7 +234,7 @@ namespace Tgstation.Server.Host.Components.Watchdog break; default: status = HttpStatusCode.BadRequest; - content = new { message = "Requested command not supported!" }; + content = new ErrorMessage { Message = "Requested command not supported!" }; break; } return Task.FromResult(new { STATUS = status, CONTENT = content }); diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index 1173ad68e6..3920cbb321 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -59,9 +59,9 @@ namespace Tgstation.Server.Host.Controllers return Json(newFile); } - catch(NotImplementedException e) + catch(NotImplementedException) { - return StatusCode((int)HttpStatusCode.NotImplemented, new { message = e.Message }); + return StatusCode((int)HttpStatusCode.NotImplemented); } } @@ -86,9 +86,9 @@ namespace Tgstation.Server.Host.Controllers return Json(result); } - catch (NotImplementedException e) + catch (NotImplementedException) { - return StatusCode((int)HttpStatusCode.NotImplemented, new { message = e.Message }); + return StatusCode((int)HttpStatusCode.NotImplemented); } } @@ -113,9 +113,9 @@ namespace Tgstation.Server.Host.Controllers return Json(result); } - catch (NotImplementedException e) + catch (NotImplementedException) { - return StatusCode((int)HttpStatusCode.NotImplemented, new { message = e.Message }); + return StatusCode((int)HttpStatusCode.NotImplemented); } catch (UnauthorizedAccessException) { diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index f487909233..b1430a4fd1 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Linq.Expressions; using System.Net; @@ -157,13 +158,16 @@ namespace Tgstation.Server.Host.Controllers DatabaseContext.Instances.Remove(newInstance); await DatabaseContext.Save(default).ConfigureAwait(false); - throw; } } + catch(IOException e) + { + return Conflict(new ErrorMessage { Message = e.Message }); + } catch (DbUpdateConcurrencyException e) { - return Conflict(new { message = e.Message }); + return Conflict(new ErrorMessage{ Message = e.Message }); } Logger.LogInformation("{0} created instance {1}: {2}", AuthenticationContext.User.Name, newInstance.Name, newInstance.Id); @@ -243,11 +247,11 @@ namespace Tgstation.Server.Host.Controllers if (!userRights.HasFlag(InstanceManagerRights.Relocate)) return Forbid(); if (originalModel.Online.Value && model.Online != true) - return Conflict(new { message = "Cannot relocate an online instance!" }); + return Conflict(new ErrorMessage { Message = "Cannot relocate an online instance!" }); var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken); if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false)) - return Conflict(new { message = "Path not empty!" }); + return Conflict(new ErrorMessage { Message = "Path not empty!" }); originalModelPath = originalModel.Path; originalModel.Path = model.Path; diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs index e7aff815c6..39185f2bd8 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs @@ -75,7 +75,7 @@ namespace Tgstation.Server.Host.Controllers } catch (DbUpdateConcurrencyException e) { - return Conflict(new { message = e.Message }); + return Conflict(new ErrorMessage { Message = e.Message }); } return Json(dbUser.ToApi()); } @@ -104,7 +104,7 @@ namespace Tgstation.Server.Host.Controllers } catch (DbUpdateConcurrencyException e) { - return Conflict(new { message = e.Message }); + return Conflict(new ErrorMessage { Message = e.Message }); } return Json(originalUser.ToApi()); } diff --git a/src/Tgstation.Server.Host/Models/DatabaseContext.cs b/src/Tgstation.Server.Host/Models/DatabaseContext.cs index ebdb01a161..1a9e5bac6e 100644 --- a/src/Tgstation.Server.Host/Models/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Models/DatabaseContext.cs @@ -112,6 +112,8 @@ namespace Tgstation.Server.Host.Models chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique(); chatChannel.HasOne(x => x.ChatSettings).WithMany(x => x.Channels).HasForeignKey(x => x.ChatSettingsId).OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity().HasIndex(x => x.Name).IsUnique(); + var instanceModel = modelBuilder.Entity(); instanceModel.HasIndex(x => x.Path).IsUnique(); instanceModel.HasMany(x => x.ChatSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade); From dee9471e021494f1aa3d03fae2977ecf83e62c98 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 3 Aug 2018 22:56:55 -0400 Subject: [PATCH 10/10] Stuff and such --- .../Internal/DreamDaemonLaunchParameters.cs | 2 +- .../Components/Chat/Chat.cs | 14 +++++++- .../Components/Compiler/DreamMaker.cs | 7 ++-- .../Components/Compiler/IDreamMaker.cs | 4 +-- .../Components/Instance.cs | 2 +- .../Components/InstanceFactory.cs | 2 +- .../Controllers/DreamMakerController.cs | 36 +++++++++++++------ src/Tgstation.Server.Host/Core/Application.cs | 1 + src/Tgstation.Server.Host/Core/JobManager.cs | 5 +-- .../IO/DefaultIOManager.cs | 2 +- 10 files changed, 51 insertions(+), 24 deletions(-) diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs index f52315595f..093277987d 100644 --- a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs +++ b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs @@ -42,6 +42,6 @@ namespace Tgstation.Server.Api.Models.Internal /// [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetStartupTimeout)] [Required] - public int? StartupTimeout { get; set; } + public uint? StartupTimeout { get; set; } } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index c8e3b77ae7..72f8c2cd78 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -519,6 +519,18 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public Task DeleteConnection(long connectionId, CancellationToken cancellationToken) => RemoveProvider(connectionId, true, cancellationToken); + public async Task DeleteConnection(long connectionId, CancellationToken cancellationToken) + { + var provider = await RemoveProvider(connectionId, true, cancellationToken).ConfigureAwait(false); + if (provider != null) + try + { + await provider.Disconnect(cancellationToken).ConfigureAwait(false); + } + finally + { + provider.Dispose(); + } + } } } diff --git a/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs index ba9d935d71..33beec3f99 100644 --- a/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Diagnostics; @@ -105,7 +104,7 @@ namespace Tgstation.Server.Host.Components.Compiler /// The current /// The for the operation /// A resulting in if the DMAPI was successfully validated, otherwise - async Task VerifyApi(int timeout, Models.CompileJob job, IByondExecutableLock byondLock, CancellationToken cancellationToken) + async Task VerifyApi(uint timeout, Models.CompileJob job, IByondExecutableLock byondLock, CancellationToken cancellationToken) { var launchParameters = new DreamDaemonLaunchParameters { @@ -226,7 +225,7 @@ namespace Tgstation.Server.Host.Components.Compiler } /// - public async Task Compile(string projectName, int apiValidateTimeout, IRepository repository, CancellationToken cancellationToken) + public async Task Compile(string projectName, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken) { logger.LogTrace("Begin Compile"); await eventConsumer.HandleEvent(EventType.CompileStart, new List{ repository.Origin }, cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Compiler/IDreamMaker.cs b/src/Tgstation.Server.Host/Components/Compiler/IDreamMaker.cs index 900a3e648e..da6878c667 100644 --- a/src/Tgstation.Server.Host/Components/Compiler/IDreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Compiler/IDreamMaker.cs @@ -18,11 +18,11 @@ namespace Tgstation.Server.Host.Components.Compiler /// /// Starts a compile /// - /// The name of the .dme to compile without the extension + /// The optional name of the .dme to compile without the extension if not pre /// The time in seconds to wait while validating the API /// The to copy from /// The for the operation /// A resulting in the partially populated for the operation. In particular, note the field will only have it's field populated - Task Compile(string projectName, int apiValidateTimeout, IRepository repository, CancellationToken cancellationToken); + Task Compile(string projectName, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index cb16e111f4..fcd335a27a 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -124,7 +124,7 @@ namespace Tgstation.Server.Host.Components RepositorySettings repositorySettings = null; string projectName = null; - int timeout = 0; + uint timeout = 0; var dbTask = databaseContextFactory.UseContext(async (db) => { var instanceQuery = db.Instances.Where(x => x.Id == metadata.Id); diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index c200766fc8..87f26407be 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -152,7 +152,7 @@ namespace Tgstation.Server.Host.Components commandFactory.SetWatchdog(watchdog); try { - var dreamMaker = new DreamMaker(byond, ioManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, loggerFactory.CreateLogger()); + var dreamMaker = new DreamMaker(byond, gameIoManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, loggerFactory.CreateLogger()); return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory, loggerFactory.CreateLogger()); } diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index 75801aac2d..460a6c598c 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -48,11 +48,11 @@ namespace Tgstation.Server.Host.Controllers public override async Task Read(CancellationToken cancellationToken) { var instance = instanceManager.GetInstance(Instance); - var projectNameTask = DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).Select(x => x.ProjectName).FirstAsync(cancellationToken); - var job = await DatabaseContext.CompileJobs.OrderByDescending(x => x.Job.StartedAt).Include(x => x.Job).FirstAsync(cancellationToken).ConfigureAwait(false); + var projectNameTask = DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).Select(x => x.ProjectName).FirstOrDefaultAsync(cancellationToken); + var job = await DatabaseContext.CompileJobs.OrderByDescending(x => x.Job.StartedAt).Include(x => x.Job).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); return Json(new Api.Models.DreamMaker { - LastJob = job.ToApi(), + LastJob = job?.ToApi(), ProjectName = await projectNameTask.ConfigureAwait(false), Status = instance.DreamMaker.Status }); @@ -71,7 +71,7 @@ namespace Tgstation.Server.Host.Controllers Instance = Instance }; await jobManager.RegisterOperation(job, (paramJob, serviceProvider, progressReporter, ct) => RunCompile(paramJob, serviceProvider, Instance, ct), cancellationToken).ConfigureAwait(false); - return Json(job); + return Json(job.ToApi()); } /// @@ -85,7 +85,7 @@ namespace Tgstation.Server.Host.Controllers DatabaseContext.DreamMakerSettings.Attach(hostModel); hostModel.ProjectName = model.ProjectName; await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); - return Ok(); + return await Read(cancellationToken).ConfigureAwait(false); } /// @@ -96,19 +96,19 @@ namespace Tgstation.Server.Host.Controllers /// The for the operation /// The for the operation /// A representing the running operation - static async Task RunCompile(Job job, IServiceProvider serviceProvider, Models.Instance instanceModel, CancellationToken cancellationToken) + async Task RunCompile(Job job, IServiceProvider serviceProvider, Models.Instance instanceModel, CancellationToken cancellationToken) { var instanceManager = serviceProvider.GetRequiredService(); var databaseContext = serviceProvider.GetRequiredService(); - var timeoutTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == instanceModel.Id).Select(x => x.StartupTimeout).FirstAsync(cancellationToken); - var projectName = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == instanceModel.Id).Select(x => x.ProjectName).FirstAsync(cancellationToken).ConfigureAwait(false); + var timeoutTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == instanceModel.Id).Select(x => x.StartupTimeout).FirstOrDefaultAsync(cancellationToken); + var projectName = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == instanceModel.Id).Select(x => x.ProjectName).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); var timeout = await timeoutTask.ConfigureAwait(false); var instance = instanceManager.GetInstance(instanceModel); CompileJob compileJob; - Task revInfoTask; + string repoSha = null; using (var repo = await instance.RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) { if (repo == null) @@ -116,12 +116,26 @@ namespace Tgstation.Server.Host.Controllers job.ExceptionDetails = "Missing repository!"; return; } - revInfoTask = databaseContext.RevisionInformations.Where(x => x.CommitSha == repo.Head).Select(x => new RevisionInformation { Id = x.Id }).FirstAsync(); + repoSha = repo.Head; compileJob = await instance.DreamMaker.Compile(projectName, timeout.Value, repo, cancellationToken).ConfigureAwait(false); } compileJob.Job = job; - compileJob.RevisionInformation = await revInfoTask.ConfigureAwait(false); + compileJob.RevisionInformation = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha).Select(x => new RevisionInformation { Id = x.Id }).FirstOrDefaultAsync().ConfigureAwait(false); + + if (compileJob.RevisionInformation == default) + { + compileJob.RevisionInformation = new RevisionInformation + { + CommitSha = repoSha, + OriginCommitSha = repoSha, + Instance = new Models.Instance + { + Id = Instance.Id + } + }; + DatabaseContext.Instances.Attach(compileJob.RevisionInformation.Instance); + } databaseContext.CompileJobs.Add(compileJob); //default ct because we don't want to give up after getting this far diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 0b97551b2a..168e747e3f 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -150,6 +150,7 @@ namespace Tgstation.Server.Host.Core options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; options.SerializerSettings.CheckAdditionalContent = true; options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error; + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); var databaseConfiguration = databaseConfigurationSection.Get(); diff --git a/src/Tgstation.Server.Host/Core/JobManager.cs b/src/Tgstation.Server.Host/Core/JobManager.cs index c059cdb7ed..b05e90757c 100644 --- a/src/Tgstation.Server.Host/Core/JobManager.cs +++ b/src/Tgstation.Server.Host/Core/JobManager.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Collections.Generic; @@ -148,7 +149,7 @@ namespace Tgstation.Server.Host.Core var databaseContext = scope.ServiceProvider.GetRequiredService(); //mark all jobs as cancelled - var enumerator = await databaseContext.Jobs.Where(y => !y.Cancelled.Value && !y.StoppedAt.HasValue).Select(y => y.Id).ToAsyncEnumerable().ToList(cancellationToken).ConfigureAwait(false); + var enumerator = await databaseContext.Jobs.Where(y => !y.Cancelled.Value && !y.StoppedAt.HasValue).Select(y => y.Id).ToListAsync(cancellationToken).ConfigureAwait(false); foreach(var I in enumerator) { var job = new Job { Id = I }; diff --git a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs index c4b235b3d7..65ae98fd1f 100644 --- a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs +++ b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs @@ -133,7 +133,7 @@ namespace Tgstation.Server.Host.IO throw new ArgumentNullException(nameof(dest)); using (var srcStream = new FileStream(ResolvePath(src), FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete, DefaultBufferSize, true)) using (var destStream = new FileStream(ResolvePath(dest), FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, DefaultBufferSize, true)) - await srcStream.CopyToAsync(destStream, DefaultBufferSize, cancellationToken).ConfigureAwait(false); + await srcStream.CopyToAsync(destStream, 81920, cancellationToken).ConfigureAwait(false); } ///