Merge pull request #560 from Cyberboss/StuffAndStuff

Shit I haven't PR'd yet
This commit is contained in:
Jordan Brown
2018-08-05 00:06:34 -04:00
committed by GitHub
37 changed files with 443 additions and 134 deletions
+25 -23
View File
@@ -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,29 @@ 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!
### 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
@@ -113,12 +125,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 +151,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
+2 -2
View File
@@ -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
+40 -1
View File
@@ -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:
`"<Server URL or IP address>;<Server Port>;<Bot nickname>;<1 to use SSL, 0 otherwise>[;<`The @ref Tgstation.Server.Api.Models.IrcPasswordType`;<The password>]"`
For Discord chat bots it should be the <a href="https://discordapp.com/developers/docs/topics/oauth2#bots">bot's Token</a>
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
*/
@@ -42,6 +42,6 @@ namespace Tgstation.Server.Api.Models.Internal
/// </summary>
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetStartupTimeout)]
[Required]
public int? StartupTimeout { get; set; }
public uint? StartupTimeout { get; set; }
}
}
@@ -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
@@ -28,6 +28,11 @@ namespace Tgstation.Server.Host.Components.Chat
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="ICommandFactory"/> for the <see cref="Chat"/>
/// </summary>
readonly ICommandFactory commandFactory;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="Chat"/>
/// </summary>
@@ -94,19 +99,17 @@ namespace Tgstation.Server.Host.Components.Chat
/// <param name="providerFactory">The value of <see cref="providerFactory"/></param>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
/// <param name="commandFactory">The <see cref="ICommandFactory"/> used to populate <see cref="builtinCommands"/></param>
/// <param name="commandFactory">The value of <see cref="commandFactory"/></param>
/// <param name="initialChatSettings">The <see cref="IEnumerable{T}"/> used to populate <see cref="initialChatSettings"/></param>
public Chat(IProviderFactory providerFactory, IIOManager ioManager, ILogger<Chat> logger, ICommandFactory commandFactory, IEnumerable<Models.ChatSettings> initialChatSettings)
public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, ILogger<Chat> logger, IEnumerable<Models.ChatSettings> 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<string, ICommand>();
foreach (var I in commandFactory?.GenerateCommands() ?? throw new ArgumentNullException(nameof(commandFactory)))
builtinCommands.Add(I.Name.ToUpperInvariant(), I);
providers = new Dictionary<long, IProvider>();
mappedChannels = new Dictionary<ulong, ChannelMapping>();
trackingContexts = new List<IJsonTrackingContext>();
@@ -189,7 +192,7 @@ namespace Tgstation.Server.Host.Components.Chat
message.User.Channel.RealId = enumerable.First().Key;
}
var splits = new List<string>(message.Content.TrimEnd().Split(' '));
var splits = new List<string>(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)
{
//just a mention
await SendMessage("Hi!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
@@ -461,6 +464,8 @@ namespace Tgstation.Server.Host.Components.Chat
/// <inheritdoc />
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);
@@ -514,6 +519,18 @@ namespace Tgstation.Server.Host.Components.Chat
}
/// <inheritdoc />
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();
}
}
}
}
@@ -45,6 +45,6 @@ namespace Tgstation.Server.Host.Components.Chat
}
/// <inheritdoc />
public IChat CreateChat(IEnumerable<Models.ChatSettings> initialChatSettings) => new Chat(providerFactory, ioManager, loggerFactory.CreateLogger<Chat>(), commandFactory, initialChatSettings);
public IChat CreateChat(IEnumerable<Models.ChatSettings> initialChatSettings) => new Chat(providerFactory, ioManager, commandFactory, loggerFactory.CreateLogger<Chat>(), initialChatSettings);
}
}
@@ -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
{
/// <summary>
/// For displaying the installed Byond version
/// </summary>
sealed class ByondCommand : ICommand
{
/// <inheritdoc />
public string Name => "byond";
/// <inheritdoc />
public string HelpText => "Displays the installed Byond version";
/// <inheritdoc />
public bool AdminOnly => false;
/// <summary>
/// the <see cref="IByondManager"/> for the <see cref="ByondCommand"/>
/// </summary>
readonly IByondManager byondManager;
/// <summary>
/// Construct a <see cref="ByondCommand"/>
/// </summary>
/// <param name="byondManager">The value of <see cref="byondManager"/></param>
public ByondCommand(IByondManager byondManager)
{
this.byondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager));
}
/// <inheritdoc />
public Task<string> 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));
}
}
@@ -1,5 +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
@@ -12,20 +15,71 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
/// </summary>
readonly IApplication application;
/// <summary>
/// The <see cref="IByondManager"/> for the <see cref="CommandFactory"/>
/// </summary>
readonly IByondManager byondManager;
/// <summary>
/// The <see cref="IRepositoryManager"/> for the <see cref="CommandFactory"/>
/// </summary>
readonly IRepositoryManager repositoryManager;
/// <summary>
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="CommandFactory"/>
/// </summary>
readonly IDatabaseContextFactory databaseContextFactory;
/// <summary>
/// The <see cref="Models.Instance"/> for the <see cref="CommandFactory"/>
/// </summary>
readonly Models.Instance instance;
/// <summary>
/// The <see cref="IWatchdog"/> for the <see cref="CommandFactory"/>
/// </summary>
IWatchdog watchdog;
/// <summary>
/// Construct a <see cref="CommandFactory"/>
/// </summary>
/// <param name="application">The value of <see cref="application"/></param>
public CommandFactory(IApplication application)
/// <param name="byondManager">The value of <see cref="byondManager"/></param>
/// <param name="repositoryManager">The value of <see cref="repositoryManager"/></param>
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
/// <param name="instance">The value of <see cref="instance"/></param>
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));
}
/// <summary>
/// Set a <paramref name="watchdog"/> for the <see cref="CommandFactory"/>
/// </summary>
/// <param name="watchdog">The <see cref="IWatchdog"/> to set</param>
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));
}
/// <inheritdoc />
public IReadOnlyList<ICommand> GenerateCommands() => new List<ICommand>
public IReadOnlyList<ICommand> GenerateCommands()
{
new KekCommand(),
new VersionCommand(application)
};
if (watchdog == null)
throw new InvalidOperationException("SetWatchdog has not been called!");
return new List<ICommand>
{
new VersionCommand(application),
new ByondCommand(byondManager),
new PullRequestsCommand(watchdog, repositoryManager, databaseContextFactory, instance),
new KekCommand()
};
}
}
}
@@ -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
{
/// <summary>
/// Command for reading the active <see cref="Api.Models.TestMerge"/>s
/// </summary>
sealed class PullRequestsCommand : ICommand
{
/// <inheritdoc />
public string Name => "prs";
/// <inheritdoc />
public string HelpText => "Display live test merge pull request numbers. Add --repo to view repository test merges";
/// <inheritdoc />
public bool AdminOnly => false;
/// <summary>
/// The <see cref="IWatchdog"/> for the <see cref="PullRequestsCommand"/>
/// </summary>
readonly IWatchdog watchdog;
/// <summary>
/// The <see cref="IRepositoryManager"/> for the <see cref="PullRequestsCommand"/>
/// </summary>
readonly IRepositoryManager repositoryManager;
/// <summary>
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="PullRequestsCommand"/>
/// </summary>
readonly IDatabaseContextFactory databaseContextFactory;
/// <summary>
/// The <see cref="Models.Instance"/> for the <see cref="PullRequestsCommand"/>
/// </summary>
readonly Models.Instance instance;
/// <summary>
/// Construct a <see cref="PullRequestsCommand"/>
/// </summary>
/// <param name="watchdog">The value of <see cref="watchdog"/></param>
/// <param name="repositoryManager">The value of <see cref="repositoryManager"/></param>
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
/// <param name="instance">The value of <see cref="instance"/></param>
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));
}
/// <inheritdoc />
public async Task<string> Invoke(string arguments, User user, CancellationToken cancellationToken)
{
IEnumerable<Models.TestMerge> 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<Models.TestMerge>();
}
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))));
}
}
}
@@ -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...";
/// <inheritdoc />
public override bool Connected => client.IsConnected;
@@ -76,6 +78,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// </summary>
Task listenTask;
/// <summary>
/// If we are disconnecting
/// </summary>
bool disconnecting;
/// <summary>
/// Construct an <see cref="IrcProvider"/>
/// </summary>
@@ -130,13 +137,17 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
channelIdMap = new Dictionary<ulong, string>();
queryChannelIdMap = new Dictionary<ulong, string>();
channelIdCounter = 1;
disconnecting = false;
}
/// <inheritdoc />
public override void Dispose()
{
if(Connected)
if (Connected)
{
disconnecting = true;
client.Disconnect(); //just closes the socket
}
}
/// <summary>
@@ -209,6 +220,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
public override Task<bool> Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
disconnecting = false;
lock (this)
try
{
@@ -276,9 +288,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 +318,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(QuitMeme, Priority.Critical); //priocritical otherwise it wont go through
}
catch (Exception e)
{
@@ -336,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);
@@ -61,6 +61,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
var cancelTcs = new TaskCompletionSource<object>();
using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
await Task.WhenAny(nextMessage.Task, cancelTcs.Task).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
lock (messageQueue)
{
var result = messageQueue.Dequeue();
@@ -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
/// <param name="byondLock">The current <see cref="IByondExecutableLock"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the DMAPI was successfully validated, <see langword="false"/> otherwise</returns>
async Task<bool> VerifyApi(int timeout, Models.CompileJob job, IByondExecutableLock byondLock, CancellationToken cancellationToken)
async Task<bool> VerifyApi(uint timeout, Models.CompileJob job, IByondExecutableLock byondLock, CancellationToken cancellationToken)
{
var launchParameters = new DreamDaemonLaunchParameters
{
@@ -124,7 +123,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;
@@ -226,7 +225,7 @@ namespace Tgstation.Server.Host.Components.Compiler
}
/// <inheritdoc />
public async Task<Models.CompileJob> Compile(string projectName, int apiValidateTimeout, IRepository repository, CancellationToken cancellationToken)
public async Task<Models.CompileJob> Compile(string projectName, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken)
{
logger.LogTrace("Begin Compile");
await eventConsumer.HandleEvent(EventType.CompileStart, new List<string>{ repository.Origin }, cancellationToken).ConfigureAwait(false);
@@ -18,11 +18,11 @@ namespace Tgstation.Server.Host.Components.Compiler
/// <summary>
/// Starts a compile
/// </summary>
/// <param name="projectName">The name of the .dme to compile without the extension</param>
/// <param name="projectName">The optional name of the .dme to compile without the extension if not pre</param>
/// <param name="apiValidateTimeout">The time in seconds to wait while validating the API</param>
/// <param name="repository">The <see cref="IRepository"/> to copy from</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the partially populated <see cref="CompileJob"/> for the operation. In particular, note the <see cref="CompileJob.RevisionInformation"/> field will only have it's <see cref="Api.Models.Internal.RevisionInformation.CommitSha"/> field populated</returns>
Task<CompileJob> Compile(string projectName, int apiValidateTimeout, IRepository repository, CancellationToken cancellationToken);
Task<CompileJob> Compile(string projectName, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken);
}
}
@@ -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);
@@ -57,11 +57,6 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IExecutor executor;
/// <summary>
/// The <see cref="ICommandFactory"/> for the <see cref="InstanceFactory"/>
/// </summary>
readonly ICommandFactory commandFactory;
/// <summary>
/// The <see cref="ISynchronousIOManager"/> for the <see cref="InstanceFactory"/>
/// </summary>
@@ -98,13 +93,12 @@ namespace Tgstation.Server.Host.Components
/// <param name="serverUpdater">The value of <see cref="serverUpdater"/></param>
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/></param>
/// <param name="executor">The value of <see cref="executor"/></param>
/// <param name="commandFactory">The value of <see cref="commandFactory"/></param>
/// <param name="synchronousIOManager">The value of <see cref="synchronousIOManager"/></param>
/// <param name="symlinkFactory">The value of <see cref="symlinkFactory"/></param>
/// <param name="byondInstaller">The value of <see cref="byondInstaller"/></param>
/// <param name="providerFactory">The value of <see cref="providerFactory"/></param>
/// <param name="scriptExecutor">The value of <see cref="scriptExecutor"/></param>
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));
@@ -140,14 +133,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<ByondManager>());
var commandFactory = new CommandFactory(application, byond, repoManager, databaseContextFactory, metadata);
var chatFactory = new ChatFactory(instanceIoManager, loggerFactory, commandFactory, providerFactory);
var chat = chatFactory.CreateChat(metadata.ChatSettings);
try
{
@@ -156,9 +149,10 @@ 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<DreamMaker>());
var dreamMaker = new DreamMaker(byond, gameIoManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, loggerFactory.CreateLogger<DreamMaker>());
return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory, loggerFactory.CreateLogger<Instance>());
}
@@ -21,6 +21,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
bool AlphaIsActive { get; }
/// <summary>
/// The <see cref="CompileJob"/> currently running on the server
/// </summary>
Models.CompileJob ActiveCompileJob { get; }
/// <summary>
/// The latest <see cref="LaunchResult"/> of the twin servers
/// </summary>
@@ -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<object>(new { STATUS = status, CONTENT = content });
@@ -29,6 +29,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <inheritdoc />
public bool AlphaIsActive { get; private set; }
/// <inheritdoc />
public Models.CompileJob ActiveCompileJob => (AlphaIsActive ? alphaServer : bravoServer)?.Dmb.CompileJob;
/// <inheritdoc />
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<object>();
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 =>
@@ -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<Release> releases;
try
@@ -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
/// <summary>
/// The <see cref="Instance"/> for the operation
/// </summary>
protected Instance Instance { get; }
protected Models.Instance Instance { get; }
/// <summary>
/// If <see cref="IAuthenticationContext.InstanceUser"/> permissions are required to access the <see cref="ApiController"/>
@@ -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,15 +164,28 @@ 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);
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);
@@ -65,10 +65,10 @@ namespace Tgstation.Server.Host.Controllers
public override async Task<IActionResult> 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;
@@ -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());
}
@@ -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)
{
@@ -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<IActionResult> 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);
@@ -48,11 +48,11 @@ namespace Tgstation.Server.Host.Controllers
public override async Task<IActionResult> 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());
}
/// <inheritdoc />
@@ -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);
}
/// <summary>
@@ -96,19 +96,19 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="instanceModel">The <see cref="Models.Instance"/> for the operation</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
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<IInstanceManager>();
var databaseContext = serviceProvider.GetRequiredService<IDatabaseContext>();
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<RevisionInformation> 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
@@ -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<IActionResult> 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
@@ -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;
@@ -93,16 +94,19 @@ namespace Tgstation.Server.Host.Controllers
[TgsAuthorize(InstanceManagerRights.Create)]
public override async Task<IActionResult> 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
{
@@ -154,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);
@@ -240,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;
@@ -338,7 +345,7 @@ namespace Tgstation.Server.Host.Controllers
public override Task<IActionResult> Read(CancellationToken cancellationToken)
{
if (Instance == null)
return Task.FromResult<IActionResult>(BadRequest(new { message = "No instance specified" }));
return Task.FromResult<IActionResult>(BadRequest(new ErrorMessage { Message = "No instance specified" }));
return Task.FromResult<IActionResult>(Json(Instance.ToApi()));
}
}
@@ -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;
}
@@ -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());
}
@@ -110,13 +110,13 @@ namespace Tgstation.Server.Host.Controllers
public override async Task<IActionResult> 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<IActionResult> 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);
@@ -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;
@@ -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,8 @@ namespace Tgstation.Server.Host.Core
options.AllowInputFormatterExceptionMessages = true;
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
options.SerializerSettings.CheckAdditionalContent = true;
options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
var databaseConfiguration = databaseConfigurationSection.Get<DatabaseConfiguration>();
@@ -205,7 +205,6 @@ namespace Tgstation.Server.Host.Core
}
services.AddSingleton<IExecutor, Executor>();
services.AddSingleton<ICommandFactory, CommandFactory>();
services.AddSingleton<IScriptExecutor, ScriptExecutor>();
services.AddSingleton<IProviderFactory, ProviderFactory>();
services.AddSingleton<IByondTopicSender>(new ByondTopicSender
@@ -47,6 +47,7 @@ namespace Tgstation.Server.Host.Core
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
using (cancellationToken.Register(() => tcs.SetCanceled()))
await Task.WhenAny(tcs.Task, task).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
}
/// <summary>
+3 -2
View File
@@ -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<IDatabaseContext>();
//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 };
@@ -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);
}
/// <inheritdoc />
@@ -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<ChatSettings>().HasIndex(x => x.Name).IsUnique();
var instanceModel = modelBuilder.Entity<Instance>();
instanceModel.HasIndex(x => x.Path).IsUnique();
instanceModel.HasMany(x => x.ChatSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
@@ -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;