Merge pull request #514 from Cyberboss/SeeIfWeCanGetThisBitchRunning

Like building a deathstar with no exhaust port
This commit is contained in:
Jordan Brown
2018-05-07 13:27:14 -04:00
committed by GitHub
41 changed files with 692 additions and 309 deletions
@@ -12,16 +12,32 @@
/// </summary>
Idle,
/// <summary>
/// The directiory is being targeted
/// </summary>
Targeting,
/// <summary>
/// The <see cref="Repository"/> is being copied
/// </summary>
Copying,
/// <summary>
/// The .dme is having it's server side modifications applied
/// </summary>
Modifying,
/// <summary>
/// DreamMaker is running
/// </summary>
Compiling
Compiling,
/// <summary>
/// The DMAPI is being verified
/// </summary>
Verifying,
/// <summary>
/// The compile results are being duplicated
/// </summary>
Duplicating,
/// <summary>
/// The configuration is being linked to the compile results
/// </summary>
Symlinking,
/// <summary>
/// A failed compile job is being erased
/// </summary>
Cleanup
}
}
@@ -41,6 +41,12 @@ namespace Tgstation.Server.Api.Models
[Permissions(WriteRight = InstanceManagerRights.SetConfiguration)]
public bool ConfigurationAllowed { get; set; }
/// <summary>
/// The time interval in minutes the repository is automatically pulled and compiles
/// </summary>
[Permissions(WriteRight = InstanceManagerRights.SetAutoUpdate)]
public int? AutoUpdateInterval { get; set; }
/// <inheritdoc />
public Instance CloneMetadata() => new Instance
{
@@ -8,12 +8,6 @@ namespace Tgstation.Server.Api.Models.Internal
[Model(RightsType.DreamMaker, ReadRight = DreamMakerRights.Read, CanCrud = true, RequiresInstance = true)]
public class DreamMakerSettings
{
/// <summary>
/// How often the <see cref="DreamMakerSettings"/> automatically compiles in minutes
/// </summary>
[Permissions(WriteRight = DreamMakerRights.SetAutoCompile)]
public int? AutoCompileInterval { get; set; }
/// <summary>
/// The .dme file <see cref="DreamMakerSettings"/> tries to compile with without the extension
/// </summary>
@@ -46,11 +46,5 @@ namespace Tgstation.Server.Api.Models.Internal
/// </summary>
[Permissions(WriteRight = RepositoryRights.ChangeTestMergeCommits)]
public bool ShowTestMergeCommitters { get; set; }
/// <summary>
/// How often the <see cref="Repository"/> automatically updates in minutes
/// </summary>
[Permissions(WriteRight = RepositoryRights.ChangeAutoUpdate)]
public int? AutoUpdateInterval { get; set; }
}
}
-6
View File
@@ -5,12 +5,6 @@
/// </summary>
public sealed class Job : Internal.Job
{
/// <summary>
/// If the <see cref="Job"/> has incremental progress, this will range from 1 - 100. 0 otherwise
/// </summary>
[Permissions(DenyWrite = true)]
public int Progress { get; set; }
/// <summary>
/// The <see cref="User"/> that started the job
/// </summary>
@@ -25,12 +25,8 @@ namespace Tgstation.Server.Api.Rights
/// </summary>
CancelCompile = 4,
/// <summary>
/// User may modify <see cref="Models.Internal.DreamMakerSettings.AutoCompileInterval"/>
/// </summary>
SetAutoCompile = 8,
/// <summary>
/// User may modify <see cref="Models.Internal.DreamMakerSettings.ProjectName"/>
/// </summary>
SetDme = 16
SetDme = 8
}
}
@@ -43,6 +43,10 @@ namespace Tgstation.Server.Api.Rights
/// <summary>
/// User can change <see cref="Models.Instance.ConfigurationAllowed"/>
/// </summary>
SetConfiguration = 128
SetConfiguration = 128,
/// <summary>
/// User can change <see cref="Models.Instance.AutoUpdateInterval"/>
/// </summary>
SetAutoUpdate = 256
}
}
@@ -37,20 +37,16 @@ namespace Tgstation.Server.Api.Rights
/// </summary>
ChangeTestMergeCommits = 64,
/// <summary>
/// User may change <see cref="Models.Internal.RepositorySettings.AutoUpdateInterval"/>
/// </summary>
ChangeAutoUpdate = 128,
/// <summary>
/// User may read and change <see cref="Models.Internal.RepositorySettings.AccessUser"/> and <see cref="Models.Internal.RepositorySettings.AccessToken"/>
/// </summary>
ChangeCredentials = 256,
ChangeCredentials = 128,
/// <summary>
/// User may set <see cref="Models.Repository.Reference"/> to another git reference (not a SHA)
/// </summary>
SetReference = 512,
SetReference = 256,
/// <summary>
/// User may read all fields in the <see cref="Models.Repository"/> with the exception of <see cref="Models.Internal.RepositorySettings.AccessToken"/>
/// </summary>
Read = 1024,
Read = 512,
}
}
@@ -14,9 +14,9 @@ namespace Tgstation.Server.Host.Components
sealed class DmbFactory : IDmbFactory, ICompileJobConsumer
{
/// <summary>
/// The <see cref="IDatabaseContext"/> for the <see cref="DmbFactory"/>
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="DmbFactory"/>
/// </summary>
readonly IDatabaseContext databaseContext;
readonly IDatabaseContextFactory databaseContextFactory;
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="DmbFactory"/>
/// </summary>
@@ -42,11 +42,11 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// Construct a <see cref="DmbFactory"/>
/// </summary>
/// <param name="databaseContext">The value of <see cref="databaseContext"/></param>
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
public DmbFactory(IDatabaseContext databaseContext, IIOManager ioManager)
public DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager)
{
this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
cleanupCts = new CancellationTokenSource();
@@ -117,16 +117,16 @@ namespace Tgstation.Server.Host.Components
}
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) =>
{
var cj = await databaseContext.CompileJobs.OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
var cj = await db.CompileJobs.OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
if (cj == default(CompileJob))
return;
LoadCompileJob(cj);
//delete all other compile jobs
var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
await Task.WhenAll(directories.Where(x => x != cj.Job.ToString()).Select(x => ioManager.DeleteDirectory(x, cancellationToken))).ConfigureAwait(false);
}
});
/// <inheritdoc />
public async Task StopAsync(CancellationToken cancellationToken)
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -23,10 +24,18 @@ namespace Tgstation.Server.Host.Components
/// Name of the secondary directory used for compilation
/// </summary>
public const string BDirectoryName = "B";
/// <summary>
/// Extension for .dmbs
/// </summary>
public const string DmbExtension = ".dmb";
/// <summary>
/// Extension for .dmes
/// </summary>
const string DmeExtension = ".dme";
/// <inheritdoc />
public CompilerStatus Status { get; private set; }
/// <summary>
/// The <see cref="IIOManager"/> for <see cref="DreamMaker"/>
/// </summary>
@@ -61,6 +70,7 @@ namespace Tgstation.Server.Host.Components
/// <param name="byond">The value of <see cref="byond"/></param>
/// <param name="interop">The value of <see cref="interop"/></param>
/// <param name="compileJobConsumer">The value of <see cref="compileJobConsumer"/></param>
///
public DreamMaker(IIOManager ioManager, IConfiguration configuration, IDreamDaemonExecutor dreamDaemonExecutor, IByond byond, IInterop interop, ICompileJobConsumer compileJobConsumer)
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
@@ -165,6 +175,12 @@ namespace Tgstation.Server.Host.Components
}
}
/// <summary>
/// Adds server side includes to the .dme being compiled
/// </summary>
/// <param name="job">The <see cref="Host.Models.CompileJob"/> 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>
async Task ModifyDme(Host.Models.CompileJob job, CancellationToken cancellationToken)
{
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
@@ -202,79 +218,95 @@ namespace Tgstation.Server.Host.Components
}
/// <inheritdoc />
public async Task<Host.Models.CompileJob> Compile(string dmeName, IRepository repository, CancellationToken cancellationToken)
public async Task<Host.Models.CompileJob> Compile(string projectName, IRepository repository, CancellationToken cancellationToken)
{
var job = new Host.Models.CompileJob
{
DirectoryName = Guid.NewGuid(),
DmeName = dmeName
};
await ioManager.CreateDirectory(job.DirectoryName.ToString(), cancellationToken).ConfigureAwait(false);
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), BDirectoryName);
async Task CleanupFailedCompile()
{
try
{
await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false);
}
catch { }
};
try
{
//copy the repository
var fullDirA = ioManager.ResolvePath(dirA);
using (repository)
await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false);
await ModifyDme(job, cancellationToken).ConfigureAwait(false);
//run compiler, verify api
var ddVerified = await byond.UseExecutables(async (dreamMakerPath, dreamDaemonPath) =>
Status = CompilerStatus.Copying;
var job = new Host.Models.CompileJob
{
await RunDreamMaker(dreamMakerPath, job, cancellationToken).ConfigureAwait(false);
DirectoryName = Guid.NewGuid(),
DmeName = projectName
};
await ioManager.CreateDirectory(job.DirectoryName.ToString(), cancellationToken).ConfigureAwait(false);
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), BDirectoryName);
return job.ExitCode == 0 && await VerifyApi(dreamDaemonPath, job, cancellationToken).ConfigureAwait(false);
}, true).ConfigureAwait(false);
if (!ddVerified)
//server never validated or compile failed
await CleanupFailedCompile().ConfigureAwait(false);
else
async Task CleanupFailedCompile()
{
job.DMApiValidated = true;
Status = CompilerStatus.Cleanup;
try
{
await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false);
}
catch { }
};
//duplicate the dmb et al
await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
try
{
//copy the repository
var fullDirA = ioManager.ResolvePath(dirA);
using (repository)
await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false);
//symlink in the static data
var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken);
await configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken).ConfigureAwait(false);
await symATask.ConfigureAwait(false);
Status = CompilerStatus.Modifying;
if (job.DmeName == null)
{
job.DmeName = (await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false)).FirstOrDefault();
if (job.DmeName == default)
{
job.Output = "Unable to find any .dme!";
return job;
}
}
await ModifyDme(job, cancellationToken).ConfigureAwait(false);
Status = CompilerStatus.Compiling;
//run compiler, verify api
var ddVerified = await byond.UseExecutables(async (dreamMakerPath, dreamDaemonPath) =>
{
await RunDreamMaker(dreamMakerPath, job, cancellationToken).ConfigureAwait(false);
Status = CompilerStatus.Verifying;
return job.ExitCode == 0 && await VerifyApi(dreamDaemonPath, job, cancellationToken).ConfigureAwait(false);
}, true).ConfigureAwait(false);
if (!ddVerified)
//server never validated or compile failed
await CleanupFailedCompile().ConfigureAwait(false);
else
{
job.DMApiValidated = true;
Status = CompilerStatus.Duplicating;
//duplicate the dmb et al
await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
Status = CompilerStatus.Symlinking;
//symlink in the static data
var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken);
await configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken).ConfigureAwait(false);
await symATask.ConfigureAwait(false);
}
compileJobConsumer.LoadCompileJob(job);
return job;
}
catch
{
await CleanupFailedCompile().ConfigureAwait(false);
throw;
}
compileJobConsumer.LoadCompileJob(job);
return job;
}
catch
finally
{
await CleanupFailedCompile().ConfigureAwait(false);
throw;
Status = CompilerStatus.Idle;
}
}
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken)
{
//THIS IS FOR AUTO COMPILE INTERVAL STUFF, STOP TRYING TO REMOVE IT
throw new NotImplementedException();
}
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
}
@@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// For managing the BYOND installation
/// </summary>
interface IByond
public interface IByond
{
/// <summary>
/// Change the current BYOND version
@@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// For managing connected chat services
/// </summary>
interface IChat : IHostedService
public interface IChat : IHostedService
{
/// <summary>
/// If the IRC client is connected
@@ -47,6 +47,14 @@ namespace Tgstation.Server.Host.Components
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task SendMessage(string message, IEnumerable<long> channelIds, CancellationToken cancellationToken);
/// <summary>
/// Start tracking json files for commands and channels
/// </summary>
/// <param name="basePath">The base path of the .jsons</param>
/// <param name="channelsJsonName">The name of the chat channels json</param>
/// <param name="commandsJsonName">The name of the chat commands json</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IDisposable"/> tied to the lifetime of the json trackings</returns>
Task<IDisposable> TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken);
}
}
@@ -9,7 +9,7 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// For managing the Configuration directory
/// </summary>
interface IConfiguration
public interface IConfiguration
{
/// <summary>
/// Copies all files in the CodeModifications directory to <paramref name="destination"/>
@@ -9,7 +9,7 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// For managing DreamDaemon
/// </summary>
interface IDreamDaemon : IHostedService
public interface IDreamDaemon : IHostedService
{
/// <summary>
/// If DreamDaemon is running
@@ -8,15 +8,20 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// For managing the compiler
/// </summary>
interface IDreamMaker : IHostedService
public interface IDreamMaker
{
/// <summary>
/// The <see cref="Api.Models.CompilerStatus"/> of <see cref="IDreamMaker"/>
/// </summary>
Api.Models.CompilerStatus Status { get; }
/// <summary>
/// Starts a compile
/// </summary>
/// <param name="dmeName">The .dme file to use without the extension</param>
/// <param name="projectName">The name of the .dme to compile without the extension</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.Commit"/> field populated</returns>
Task<CompileJob> Compile(string dmeName, IRepository repository, CancellationToken cancellationToken);
Task<CompileJob> Compile(string projectName, IRepository repository, CancellationToken cancellationToken);
}
}
@@ -1,23 +1,60 @@
using Microsoft.Extensions.Hosting;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Components
{
interface IInstance : IHostedService
/// <summary>
/// For interacting with the instance services
/// </summary>
public interface IInstance : IHostedService
{
/// <summary>
/// The <see cref="IRepositoryManager"/> for the <see cref="IInstance"/>
/// </summary>
IRepositoryManager RepositoryManager { get; }
/// <summary>
/// The <see cref="IByond"/> for the <see cref="IInstance"/>
/// </summary>
IByond Byond { get; }
/// <summary>
/// The <see cref="IDreamMaker"/> for the <see cref="IInstance"/>
/// </summary>
IDreamMaker DreamMaker { get; }
/// <summary>
/// The <see cref="IDreamDaemon"/> for the <see cref="IInstance"/>
/// </summary>
IDreamDaemon DreamDaemon { get; }
/// <summary>
/// The <see cref="IChat"/> for the <see cref="IInstance"/>
/// </summary>
IChat Chat { get; }
/// <summary>
/// The <see cref="IConfiguration"/> for the <see cref="IInstance"/>
/// </summary>
IConfiguration Configuration { get; }
/// <summary>
/// Get the <see cref="Api.Models.Instance"/> associated with the <see cref="IInstance"/>
/// </summary>
/// <returns>The <see cref="Api.Models.Instance"/> associated with the <see cref="IInstance"/></returns>
Api.Models.Instance GetMetadata();
/// <summary>
/// Rename the <see cref="IInstance"/>
/// </summary>
/// <param name="newName">The new name for the <see cref="IInstance"/></param>
void Rename(string newName);
/// <summary>
/// Change the <see cref="Api.Models.Instance.AutoUpdateInterval"/> for the <see cref="IInstance"/>
/// </summary>
/// <param name="newInterval">The new auto update inteval</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task SetAutoUpdateInterval(int? newInterval);
}
}
@@ -1,4 +1,4 @@
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Core;
namespace Tgstation.Server.Host.Components
{
@@ -11,8 +11,7 @@ namespace Tgstation.Server.Host.Components
/// Create an <see cref="IInstance"/>
/// </summary>
/// <param name="metadata">The <see cref="Host.Models.Instance"/></param>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the operation</param>
/// <returns>A new <see cref="IInstance"/></returns>
IInstance CreateInstance(Host.Models.Instance metadata, IDatabaseContext databaseContext);
IInstance CreateInstance(Host.Models.Instance metadata);
}
}
@@ -1,13 +1,12 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components
{
/// <summary>
/// For managing <see cref="IInstance"/>s
/// </summary>
interface IInstanceManager : IInstanceShutdownHandler
public interface IInstanceManager : IInstanceShutdownHandler
{
/// <summary>
/// Get the <see cref="IInstance"/> associated with given <paramref name="metadata"/>
@@ -20,10 +19,9 @@ namespace Tgstation.Server.Host.Components
/// Online an <see cref="IInstance"/>
/// </summary>
/// <param name="metadata">The <see cref="Host.Models.Instance"/> of the desired <see cref="IInstance"/></param>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> 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>
Task OnlineInstance(Host.Models.Instance metadata, IDatabaseContext databaseContext, CancellationToken cancellationToken);
Task OnlineInstance(Host.Models.Instance metadata, CancellationToken cancellationToken);
/// <summary>
/// Offline an <see cref="IInstance"/>
@@ -37,10 +35,9 @@ namespace Tgstation.Server.Host.Components
/// Move an <see cref="IInstance"/>
/// </summary>
/// <param name="metadata">The <see cref="Host.Models.Instance"/> of the desired <see cref="IInstance"/></param>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the operation</param>
/// <param name="newPath">The new path of the <see cref="IInstance"/>. <paramref name="metadata"/> will have this set on <see cref="Api.Models.Instance.Path"/> if the operation completes successfully</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task MoveInstance(Host.Models.Instance metadata, IDatabaseContext databaseContext, string newPath, CancellationToken cancellationToken);
Task MoveInstance(Host.Models.Instance metadata, string newPath, CancellationToken cancellationToken);
}
}
@@ -3,8 +3,20 @@ using Tgstation.Server.Api.Models.Internal;
namespace Tgstation.Server.Host.Components
{
interface IInstanceShutdownHandler
/// <summary>
/// For handling <see cref="IInstance"/> shutdowns
/// </summary>
public interface IInstanceShutdownHandler
{
//TODO
/// <summary>
/// OMG
/// </summary>
/// <param name="launchParameters"></param>
/// <param name="accessToken"></param>
/// <param name="pid"></param>
/// <param name="primary"></param>
/// <returns></returns>
Task<bool> PreserveActiveExecutablesIfNecessary(DreamDaemonLaunchParameters launchParameters, string accessToken, int pid, bool primary);
}
}
@@ -8,7 +8,7 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// Represents an on-disk git repository
/// </summary>
interface IRepository : IDisposable
public interface IRepository : IDisposable
{
/// <summary>
/// If the <see cref="IRepository"/> was cloned from GitHub.com
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -7,8 +7,8 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// Factory for creating and loading <see cref="IRepository"/>s
/// </summary>
interface IRepositoryManager : IHostedService
{
public interface IRepositoryManager
{
/// <summary>
/// Attempt to load the <see cref="IRepository"/> from the default location
/// </summary>
@@ -19,17 +19,10 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// Delete the current <see cref="IRepository"/> and replaces it with a clone of the repository at <paramref name="url"/>
/// </summary>
/// <param name="url">The location of the remote repository to clone</param>
/// <param name="url">The <see cref="Uri"/> of the remote repository to clone</param>
/// <param name="accessString">The access string to clone from <paramref name="url"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>The newly cloned <see cref="IRepository"/></returns>
Task<IRepository> CloneRepository(string url, string accessString, CancellationToken cancellationToken);
/// <summary>
/// Change the interval in minutes at which the repository auto updates
/// </summary>
/// <param name="newInterval">The new interval in minutes or <see langword="null"/> to disable the auto update</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task SetAutoUpdateInterval(int? newInterval);
Task<IRepository> CloneRepository(Uri url, string accessString, CancellationToken cancellationToken);
}
}
@@ -1,28 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Core;
namespace Tgstation.Server.Host.Components
{
sealed class Instance : IInstance
/// <inheritdoc />
sealed class Instance : IInstance, IDisposable
{
/// <inheritdoc />
public IRepositoryManager RepositoryManager { get; }
/// <inheritdoc />
public IByond Byond { get; }
/// <inheritdoc />
public IDreamMaker DreamMaker { get; }
/// <inheritdoc />
public IDreamDaemon DreamDaemon { get; }
/// <inheritdoc />
public IChat Chat { get; }
/// <inheritdoc />
public IConfiguration Configuration { get; }
/// <summary>
/// The <see cref="ICompileJobConsumer"/> for the <see cref="Instance"/>
/// </summary>
readonly ICompileJobConsumer compileJobConsumer;
/// <summary>
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="Instance"/>
/// </summary>
readonly IDatabaseContextFactory databaseContextFactory;
/// <summary>
/// The <see cref="Api.Models.Instance"/> for the <see cref="Instance"/>
/// </summary>
readonly Api.Models.Instance metadata;
public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByond byond, IDreamMaker dreamMaker, IDreamDaemon dreamDaemon, IChat chat, IConfiguration configuration, ICompileJobConsumer compileJobConsumer)
/// <summary>
/// The auto update <see cref="Task"/>
/// </summary>
Task timerTask;
/// <summary>
/// <see cref="CancellationTokenSource"/> for <see cref="timerTask"/>
/// </summary>
CancellationTokenSource timerCts;
public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByond byond, IDreamMaker dreamMaker, IDreamDaemon dreamDaemon, IChat chat, IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory)
{
this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
@@ -32,10 +62,50 @@ namespace Tgstation.Server.Host.Components
Chat = chat ?? throw new ArgumentNullException(nameof(chat));
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
}
/// <inheritdoc />
public void Dispose() => timerCts?.Dispose();
/// <summary>
/// Pull the repository and compile for every set of given <paramref name="minutes"/>
/// </summary>
/// <param name="minutes">How many minutes the operation should repeat. Does not include running time</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task TimerLoop(int minutes, CancellationToken cancellationToken)
{
try
{
while (true)
{
await Task.Delay(new TimeSpan(0, minutes, 0), cancellationToken).ConfigureAwait(false);
string accessToken = null, projectName = null;
var dbTask = databaseContextFactory.UseContext(async (db) =>
{
var instanceQuery = db.Instances.Where(x => x.Id == metadata.Id);
var projectNameTask = instanceQuery.Select(x => x.DreamMakerSettings.ProjectName).FirstOrDefaultAsync(cancellationToken);
accessToken = await instanceQuery.Select(x => x.RepositorySettings.AccessToken).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
projectName = await projectNameTask.ConfigureAwait(false);
});
using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
{
await dbTask.ConfigureAwait(false);
await repo.FetchOrigin(accessToken, cancellationToken).ConfigureAwait(false);
await repo.ResetToOrigin(cancellationToken).ConfigureAwait(false);
await DreamMaker.Compile(projectName, repo, cancellationToken).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException) { }
}
/// <inheritdoc />
public Api.Models.Instance GetMetadata() => metadata.CloneMetadata();
/// <inheritdoc />
public void Rename(string newName)
{
if (String.IsNullOrWhiteSpace(newName))
@@ -43,8 +113,38 @@ namespace Tgstation.Server.Host.Components
metadata.Name = newName;
}
public Task StartAsync(CancellationToken cancellationToken) => Task.WhenAll(RepositoryManager.StartAsync(cancellationToken), DreamDaemon.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken), compileJobConsumer.StartAsync(cancellationToken), DreamMaker.StartAsync(cancellationToken));
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken) => Task.WhenAll(SetAutoUpdateInterval(metadata.AutoUpdateInterval), DreamDaemon.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken), compileJobConsumer.StartAsync(cancellationToken));
public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(RepositoryManager.StopAsync(cancellationToken), DreamDaemon.StopAsync(cancellationToken), Chat.StopAsync(cancellationToken), compileJobConsumer.StopAsync(cancellationToken), DreamMaker.StopAsync(cancellationToken));
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(SetAutoUpdateInterval(null), DreamDaemon.StopAsync(cancellationToken), Chat.StopAsync(cancellationToken), compileJobConsumer.StopAsync(cancellationToken));
/// <inheritdoc />
public async Task SetAutoUpdateInterval(int? newInterval)
{
Task toWait;
lock (this)
{
if (timerTask != null)
{
timerCts.Cancel();
toWait = timerTask;
}
else
toWait = Task.CompletedTask;
}
await toWait.ConfigureAwait(false);
if (!newInterval.HasValue)
return;
lock (this)
{
//race condition, just quit
if (timerTask != null)
return;
timerCts?.Dispose();
timerCts = new CancellationTokenSource();
timerTask = TimerLoop(newInterval.Value, timerCts.Token);
}
}
}
}
@@ -1,6 +1,5 @@
using System;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components
{
@@ -12,14 +11,24 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="InstanceFactory"/>
/// </summary>
readonly IDatabaseContextFactory databaseContextFactory;
/// <summary>
/// Construct an <see cref="InstanceFactory"/>
/// </summary>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
public InstanceFactory(IIOManager ioManager) => this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory)
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
}
/// <inheritdoc />
public IInstance CreateInstance(Host.Models.Instance metadata, IDatabaseContext databaseContext)
public IInstance CreateInstance(Host.Models.Instance metadata)
{
//Create the ioManager for the instance
@@ -32,7 +41,7 @@ namespace Tgstation.Server.Host.Components
var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration");
var codeModificationsIoMananger = new ResolvingIOManager(instanceIoManager, "CodeModifications");
var dmbFactory = new DmbFactory(databaseContext, gameIoManager);
var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager);
@@ -8,7 +8,6 @@ using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components
{
@@ -16,18 +15,18 @@ namespace Tgstation.Server.Host.Components
sealed class InstanceManager : IInstanceManager, IHostedService
{
/// <summary>
/// The <see cref="IInstanceFactory"/> for the <see cref="IInstanceManager"/>
/// The <see cref="IInstanceFactory"/> for the <see cref="InstanceManager"/>
/// </summary>
readonly IInstanceFactory instanceFactory;
/// <summary>
/// The <see cref="IServiceProvider"/> for the <see cref="IInstanceManager"/>
/// </summary>
readonly IServiceProvider serviceProvider;
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="IInstanceManager"/>
/// The <see cref="IIOManager"/> for the <see cref="InstanceManager"/>
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="InstanceManager"/>
/// </summary>
readonly IDatabaseContextFactory databaseContextFactory;
/// <summary>
/// Map of <see cref="Api.Models.Instance.Id"/>s to respective <see cref="IInstance"/>s
/// </summary>
readonly Dictionary<long, IInstance> instances;
@@ -36,13 +35,13 @@ namespace Tgstation.Server.Host.Components
/// Construct an <see cref="InstanceManager"/>
/// </summary>
/// <param name="instanceFactory">The value of <see cref="instanceFactory"/></param>
/// <param name="serviceProvider">The value of <paramref name="serviceProvider"/></param>
/// <param name="ioManager">The value of <paramref name="ioManager"/></param>
public InstanceManager(IInstanceFactory instanceFactory, IServiceProvider serviceProvider, IIOManager ioManager)
/// <param name="databaseContextFactory">The value of <paramref name="databaseContextFactory"/></param>
public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory)
{
this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
instances = new Dictionary<long, IInstance>();
}
@@ -60,7 +59,7 @@ namespace Tgstation.Server.Host.Components
}
/// <inheritdoc />
public async Task MoveInstance(Host.Models.Instance instance, IDatabaseContext databaseContext, string newPath, CancellationToken cancellationToken)
public async Task MoveInstance(Host.Models.Instance instance, string newPath, CancellationToken cancellationToken)
{
if (newPath == null)
throw new ArgumentNullException(nameof(newPath));
@@ -72,14 +71,14 @@ namespace Tgstation.Server.Host.Components
var oldPath = instance.Path;
await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
instance.Path = ioManager.ResolvePath(newPath);
instanceOnlineTask = OnlineInstance(instance, databaseContext, default);
instanceOnlineTask = OnlineInstance(instance, default);
await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
}
finally
{
if (instance.Online)
if (instanceOnlineTask == null)
await OnlineInstance(instance, databaseContext, default).ConfigureAwait(false);
await OnlineInstance(instance, default).ConfigureAwait(false);
else
await instanceOnlineTask.ConfigureAwait(false);
}
@@ -101,11 +100,11 @@ namespace Tgstation.Server.Host.Components
}
/// <inheritdoc />
public async Task OnlineInstance(Host.Models.Instance metadata, IDatabaseContext databaseContext, CancellationToken cancellationToken)
public async Task OnlineInstance(Host.Models.Instance metadata, CancellationToken cancellationToken)
{
if (metadata == null)
throw new ArgumentNullException(nameof(metadata));
var instance = instanceFactory.CreateInstance(metadata, databaseContext);
var instance = instanceFactory.CreateInstance(metadata);
lock (this)
{
if (instances.ContainsKey(metadata.Id))
@@ -116,18 +115,14 @@ namespace Tgstation.Server.Host.Components
}
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
{
using(var scope = serviceProvider.CreateScope())
{
var databaseContext = scope.ServiceProvider.GetRequiredService<IDatabaseContext>();
await databaseContext.Initialize(cancellationToken).ConfigureAwait(false);
var dbInstances = databaseContext.Instances.Where(x => x.Online).Include(x => x.RepositorySettings).Include(x => x.ChatSettings).Include(x => x.DreamDaemonSettings).ToAsyncEnumerable();
var tasks = new List<Task>();
await dbInstances.ForEachAsync(metadata => tasks.Add(OnlineInstance(metadata, databaseContext, cancellationToken)), cancellationToken).ConfigureAwait(false);
await Task.WhenAll(tasks).ConfigureAwait(false);
}
}
await databaseContext.Initialize(cancellationToken).ConfigureAwait(false);
var dbInstances = databaseContext.Instances.Where(x => x.Online).Include(x => x.RepositorySettings).Include(x => x.ChatSettings).Include(x => x.DreamDaemonSettings).ToAsyncEnumerable();
var tasks = new List<Task>();
await dbInstances.ForEachAsync(metadata => tasks.Add(OnlineInstance(metadata, cancellationToken)), cancellationToken).ConfigureAwait(false);
await Task.WhenAll(tasks).ConfigureAwait(false);
});
/// <inheritdoc />
public async Task StopAsync(CancellationToken cancellationToken)
@@ -25,16 +25,6 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly SemaphoreSlim semaphore;
/// <summary>
/// <see cref="CancellationTokenSource"/> for <see cref="currentTimerTask"/>
/// </summary>
CancellationTokenSource timerCancellationTokenSource;
/// <summary>
/// Represents the running update timer if any
/// </summary>
Task currentTimerTask;
/// <summary>
/// Construct a <see cref="RepositoryManager"/>
/// </summary>
@@ -48,52 +38,10 @@ namespace Tgstation.Server.Host.Components
}
/// <inheritdoc />
public void Dispose()
{
timerCancellationTokenSource?.Dispose();
semaphore.Dispose();
}
/// <summary>
/// Stops <see cref="currentTimerTask"/> and joins it
/// </summary>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task StopTimer()
{
if (currentTimerTask == null)
return;
timerCancellationTokenSource.Cancel();
await currentTimerTask.ConfigureAwait(false);
currentTimerTask = null;
}
/// <summary>
/// Asyncronously fetch and reset the current branch for each given amount of <paramref name="minutes"/>
/// </summary>
/// <param name="minutes">The delay of the timer</param>
/// <param name="accessString">The accessString to use for fetch operations</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task TimerLoop(int minutes, string accessString, CancellationToken cancellationToken)
{
try
{
while (true)
{
await Task.Delay(TimeSpan.FromMinutes(minutes), cancellationToken).ConfigureAwait(false);
using (var repo = await LoadRepository(cancellationToken).ConfigureAwait(false))
{
//TODO: Find the unauthorized exception, catch it, and log it
await repo.FetchOrigin(accessString, cancellationToken).ConfigureAwait(false);
await repo.ResetToOrigin(cancellationToken).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException) { }
}
public void Dispose() => semaphore.Dispose();
/// <inheritdoc />
public async Task<IRepository> CloneRepository(string url, string accessString, CancellationToken cancellationToken)
public async Task<IRepository> CloneRepository(Uri url, string accessString, CancellationToken cancellationToken)
{
await ioManager.DeleteDirectory(".", cancellationToken).ConfigureAwait(false);
@@ -102,7 +50,7 @@ namespace Tgstation.Server.Host.Components
string path = null;
try
{
path = LibGit2Sharp.Repository.Clone(Repository.GenerateAuthUrl(url, accessString), ioManager.ResolvePath("."), new CloneOptions
path = LibGit2Sharp.Repository.Clone(Repository.GenerateAuthUrl(url.ToString(), accessString), ioManager.ResolvePath("."), new CloneOptions
{
OnProgress = (a) => !cancellationToken.IsCancellationRequested,
OnTransferProgress = (a) => !cancellationToken.IsCancellationRequested,
@@ -134,33 +82,5 @@ namespace Tgstation.Server.Host.Components
localSemaphore = null;
});
}
/// <inheritdoc />
public async Task SetAutoUpdateInterval(int? newInterval)
{
await StopTimer().ConfigureAwait(false);
if (!newInterval.HasValue)
return;
string accessString = null;
if (timerCancellationTokenSource != null)
timerCancellationTokenSource.Dispose();
timerCancellationTokenSource = new CancellationTokenSource();
currentTimerTask = TimerLoop(repositorySettings.AutoUpdateInterval.Value, accessString, timerCancellationTokenSource.Token);
}
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken) => SetAutoUpdateInterval(repositorySettings.AutoUpdateInterval);
/// <inheritdoc />
public async Task StopAsync(CancellationToken cancellationToken)
{
var timerStopTask = StopTimer();
var tcs = new TaskCompletionSource<object>();
using (cancellationToken.Register(() => tcs.SetCanceled()))
await Task.WhenAny(timerStopTask, tcs.Task).ConfigureAwait(false);
}
}
}
@@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// Represents code modifications via configuration
/// </summary>
sealed class ServerSideModifications
public sealed class ServerSideModifications
{
/// <summary>
/// If the target dme was completely overwitten
@@ -18,12 +18,16 @@ namespace Tgstation.Server.Host.Controllers
/// Controller for managing the compiler
/// </summary>
[Route("/DreamMaker")]
public sealed class DreamMakerController : ModelController<Api.Models.CompileJob>
public sealed class DreamMakerController : ModelController<Api.Models.DreamMaker>
{
/// <summary>
/// The <see cref="IJobManager"/> for the <see cref="DreamMakerController"/>
/// </summary>
readonly IJobManager jobManager;
/// <summary>
/// The <see cref="IInstanceManager"/> for the <see cref="DreamMakerController"/>
/// </summary>
readonly IInstanceManager instanceManager;
/// <summary>
/// Construct a <see cref="HomeController"/>
@@ -31,14 +35,31 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager) : base(databaseContext, authenticationContextFactory)
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager) : base(databaseContext, authenticationContextFactory)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
}
/// <inheritdoc />
[TgsAuthorize(DreamMakerRights.Read)]
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);
return Json(new Api.Models.DreamMaker
{
LastJob = job.ToApi(),
ProjectName = await projectNameTask.ConfigureAwait(false),
Status = instance.DreamMaker.Status
});
}
/// <inheritdoc />
[TgsAuthorize(DreamMakerRights.Compile)]
public override async Task<IActionResult> Create([FromBody] Api.Models.CompileJob model, CancellationToken cancellationToken)
public override async Task<IActionResult> Create([FromBody] Api.Models.DreamMaker model, CancellationToken cancellationToken)
{
var job = new Job
{
@@ -53,13 +74,27 @@ namespace Tgstation.Server.Host.Controllers
/// <inheritdoc />
[TgsAuthorize(DreamMakerRights.CancelCompile)]
public override async Task<IActionResult> Delete([FromBody] Api.Models.CompileJob model, CancellationToken cancellationToken)
public override async Task<IActionResult> Delete([FromBody] Api.Models.DreamMaker model, CancellationToken cancellationToken)
{
//alias for cancelling the latest job
var job = await DatabaseContext.Jobs.OrderByDescending(x => x.StartedAt).Select(x => new Job { Id = x.Id, StoppedAt = x.StoppedAt }).FirstAsync(cancellationToken).ConfigureAwait(false);
var job = await DatabaseContext.CompileJobs.OrderByDescending(x => x.Job.StartedAt).Select(x => new Job { Id = x.Job.Id, StoppedAt = x.Job.StoppedAt }).FirstAsync(cancellationToken).ConfigureAwait(false);
if (job.StoppedAt != null)
return StatusCode(HttpStatusCode.Gone);
jobManager.CancelJob(job);
await jobManager.CancelJob(job, AuthenticationContext.User, cancellationToken).ConfigureAwait(false);
return Ok();
}
/// <inheritdoc />
[TgsAuthorize(DreamMakerRights.SetDme)]
public override async Task<IActionResult> Update([FromBody] Api.Models.DreamMaker model, CancellationToken cancellationToken)
{
var hostModel = new DreamMakerSettings
{
InstanceId = Instance.Id
};
DatabaseContext.DreamMakerSettings.Attach(hostModel);
hostModel.ProjectName = model.ProjectName;
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
return Ok();
}
@@ -76,7 +111,7 @@ namespace Tgstation.Server.Host.Controllers
var instanceManager = serviceProvider.GetRequiredService<IInstanceManager>();
var databaseContext = serviceProvider.GetRequiredService<IDatabaseContext>();
var projectName = await databaseContext.Instances.Where(x => x.Id == instanceModel.Id).Select(x => x.DreamMakerSettings.ProjectName).FirstAsync(cancellationToken).ConfigureAwait(false);
var projectName = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == instanceModel.Id).Select(x => x.ProjectName).FirstAsync(cancellationToken).ConfigureAwait(false);
var instance = instanceManager.GetInstance(instanceModel);
@@ -0,0 +1,70 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
{
/// <summary>
/// <see cref="ModelController{TModel}"/> for <see cref="Api.Models.Job"/>s
/// </summary>
[Route("/Job")]
public sealed class JobController : ModelController<Api.Models.Job>
{
/// <summary>
/// The <see cref="IJobManager"/> for the <see cref="JobController"/>
/// </summary>
readonly IJobManager jobManager;
/// <summary>
/// Construct a <see cref="HomeController"/>
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager) : base(databaseContext, authenticationContextFactory)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
}
/// <inheritdoc />
[TgsAuthorize]
public override async Task<IActionResult> List(CancellationToken cancellationToken)
{
IQueryable<Job> query = DatabaseContext.Jobs;
if (Instance != null)
{
if (!AuthenticationContext.InstanceUser.AnyRights)
return Forbid();
query = query.Where(x => x.Instance.Id == Instance.Id);
}
else
query = query.Where(x => x.Instance == null);
var jobs = await query.Where(x => x.StoppedAt == null).ToListAsync(cancellationToken).ConfigureAwait(false);
return Json(jobs.Select(x => x.ToApi()));
}
/// <inheritdoc />
[TgsAuthorize]
public override async Task<IActionResult> Delete([FromBody] Api.Models.Job model, CancellationToken cancellationToken)
{
//don't care if an instance post or not at this point
var job = await DatabaseContext.Jobs.Where(x => x.Id == model.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
if (job == default(Job))
return NotFound();
if(job.StoppedAt != null)
return StatusCode(HttpStatusCode.Gone);
await jobManager.CancelJob(job, AuthenticationContext.User, cancellationToken).ConfigureAwait(false);
return Ok();
}
}
}
@@ -0,0 +1,29 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Core
{
/// <inheritdoc />
sealed class DatabaseContextFactory : IDatabaseContextFactory
{
/// <summary>
/// The <see cref="IServiceProvider"/> for the <see cref="DatabaseContextFactory"/>
/// </summary>
readonly IServiceProvider serviceProvider;
/// <summary>
/// Construct a <see cref="DatabaseContextFactory"/>
/// </summary>
/// <param name="serviceProvider">The value of <see cref="serviceProvider"/></param>
public DatabaseContextFactory(IServiceProvider serviceProvider) => this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
/// <inheritdoc />
public async Task UseContext(Func<IDatabaseContext, Task> operation)
{
using (var scope = serviceProvider.CreateScope())
await operation(scope.ServiceProvider.GetRequiredService<IDatabaseContext>()).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,19 @@
using System;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Core
{
/// <summary>
/// Factory for scoping usage of <see cref="IDatabaseContext"/>s. Meant for use by <see cref="Components"/>
/// </summary>
interface IDatabaseContextFactory
{
/// <summary>
/// Run an <paramref name="operation"/> in the scope of an <see cref="IDatabaseContext"/>
/// </summary>
/// <param name="operation">The operation to run</param>
/// <returns>A <see cref="Task"/> representing the running <paramref name="operation"/></returns>
Task UseContext(Func<IDatabaseContext, Task> operation);
}
}
@@ -20,18 +20,13 @@ namespace Tgstation.Server.Host.Core
/// <returns>A <see cref="Task"/> representing a running operation</returns>
Task RegisterOperation(Job job, Func<Job, IServiceProvider, CancellationToken, Task> operation, CancellationToken cancellationToken);
/// <summary>
/// Wait for a given <paramref name="job"/> to complete
/// </summary>
/// <param name="job">The <see cref="Job"/> to wait for</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing a running operation</returns>
Task WaitForJob(Job job, CancellationToken cancellationToken);
/// <summary>
/// Cancels a give <paramref name="job"/>
/// </summary>
/// <param name="job">The <see cref="Job"/> to cancel</param>
void CancelJob(Job job);
/// <param name="user">The <see cref="User"/> who cancelled the <paramref name="job"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing a running operation</returns>
Task CancelJob(Job job, User user, CancellationToken cancellationToken);
}
}
+40 -26
View File
@@ -55,33 +55,45 @@ namespace Tgstation.Server.Host.Core
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task RunJob(Job job, Func<Job, IServiceProvider, CancellationToken, Task> operation, CancellationToken cancellationToken)
{
using (var scope = serviceProvider.CreateScope())
try
{
IDatabaseContext databaseContext = null;
try
using (var scope = serviceProvider.CreateScope())
{
var oldJob = job;
job = new Job { Id = oldJob.Id };
IDatabaseContext databaseContext = null;
try
{
await operation(job, scope.ServiceProvider, cancellationToken).ConfigureAwait(false);
var oldJob = job;
job = new Job { Id = oldJob.Id };
try
{
await operation(job, scope.ServiceProvider, cancellationToken).ConfigureAwait(false);
}
finally
{
databaseContext = scope.ServiceProvider.GetRequiredService<IDatabaseContext>();
databaseContext.Jobs.Attach(job);
}
}
finally
catch (OperationCanceledException)
{
databaseContext = scope.ServiceProvider.GetRequiredService<IDatabaseContext>();
databaseContext.Jobs.Attach(job);
job.Cancelled = true;
}
catch (Exception e)
{
job.ExceptionDetails = e.ToString();
}
job.StoppedAt = DateTimeOffset.Now;
await databaseContext.Save(default).ConfigureAwait(false);
}
catch (OperationCanceledException)
}
finally
{
lock (this)
{
job.Cancelled = true;
var handler = jobs[job.Id];
jobs.Remove(job.Id);
handler.Dispose();
}
catch (Exception e)
{
job.ExceptionDetails = e.ToString();
}
job.StoppedAt = DateTimeOffset.Now;
await databaseContext.Save(default).ConfigureAwait(false);
}
}
@@ -135,16 +147,18 @@ namespace Tgstation.Server.Host.Core
}
/// <inheritdoc />
public async Task WaitForJob(Job job, CancellationToken cancellationToken)
public async Task CancelJob(Job job, User user, CancellationToken cancellationToken)
{
var handler = CheckGetJob(job);
await handler.Wait(cancellationToken).ConfigureAwait(false);
lock (this)
jobs.Remove(job.Id);
handler.Dispose();
if (user != null)
using (var scope = serviceProvider.CreateScope())
{
var databaseContext = scope.ServiceProvider.GetRequiredService<IDatabaseContext>();
job = new Job { Id = job.Id };
databaseContext.Jobs.Attach(job);
job.CancelledBy = user;
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
}
CheckGetJob(job).Cancel();
}
/// <inheritdoc />
public void CancelJob(Job job) => CheckGetJob(job).Cancel();
}
}
+17 -3
View File
@@ -1,10 +1,11 @@
using System.ComponentModel.DataAnnotations;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class CompileJob : Api.Models.Internal.CompileJob
{
public sealed class CompileJob : Api.Models.Internal.CompileJob, IApiConvertable<Api.Models.CompileJob>
{
/// <summary>
/// The <see cref="Api.Models.Internal.Job.Id"/> of <see cref="Job"/>
/// </summary>
@@ -21,5 +22,18 @@ namespace Tgstation.Server.Host.Models
/// </summary>
[Required]
public RevisionInformation RevisionInformation { get; set; }
}
/// <inheritdoc />
public Api.Models.CompileJob ToApi() => new Api.Models.CompileJob
{
DirectoryName = DirectoryName,
DMApiValidated = DMApiValidated,
DmeName = DmeName,
ExitCode = ExitCode,
Id = Id,
Job = Job.ToApi(),
Output = Output,
RevisionInformation = RevisionInformation.ToApi()
};
}
}
@@ -28,6 +28,9 @@ namespace Tgstation.Server.Host.Models
/// <inheritdoc />
public DbSet<RevisionInformation> RevisionInformations { get; set; }
/// <inheritdoc />
public DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
/// <summary>
/// The <see cref="DbSet{TEntity}"/> for <see cref="Log"/>s
/// </summary>
@@ -50,10 +53,6 @@ namespace Tgstation.Server.Host.Models
/// </summary>
public DbSet<DreamDaemonSettings> DreamDaemonSettings { get; set; }
/// <summary>
/// The <see cref="Models.DreamMakerSettings"/> in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
/// <summary>
/// The <see cref="Job"/>s in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<Job> Jobs { get; set; }
@@ -0,0 +1,15 @@
namespace Tgstation.Server.Host.Models
{
/// <summary>
/// For converting models to their API form
/// </summary>
/// <typeparam name="TModel">Which of the <see cref="Api.Models"/> this model backs</typeparam>
public interface IApiConvertable<TModel> where TModel : class
{
/// <summary>
/// Convert the model to it's API form
/// </summary>
/// <returns>A new <typeparamref name="TModel"/></returns>
TModel ToApi();
}
}
@@ -34,6 +34,11 @@ namespace Tgstation.Server.Host.Models
/// </summary>
DbSet<RevisionInformation> RevisionInformations { get; }
/// <summary>
/// The <see cref="DreamMakerSettings"/> in the <see cref="IDatabaseContext"/>
/// </summary>
DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
/// <summary>
/// Get the <see cref="ServerSettings"/> in the <see cref="IDatabaseContext"/>
/// </summary>
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Host.Models
{
@@ -15,5 +16,14 @@ namespace Tgstation.Server.Host.Models
/// </summary>
[Required]
public Instance Instance { get; set; }
/// <summary>
/// If the <see cref="InstanceUser"/> has any instance rights
/// </summary>
public bool AnyRights => ByondRights != ByondRights.None ||
ChatSettingsRights != ChatSettingsRights.None ||
ConfigurationRights != ConfigurationRights.None ||
DreamDaemonRights != DreamDaemonRights.None ||
DreamMakerRights != DreamMakerRights.None;
}
}
+26 -1
View File
@@ -3,12 +3,37 @@
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class Job : Api.Models.Internal.Job
public sealed class Job : Api.Models.Internal.Job, IApiConvertable<Api.Models.Job>
{
/// <summary>
/// See <see cref="Api.Models.Job.StartedBy"/>
/// </summary>
[Required]
public User StartedBy { get; set; }
/// <summary>
/// See <see cref="Api.Models.Job.CancelledBy"/>
/// </summary>
public User CancelledBy { get; set; }
/// <summary>
/// The <see cref="Instance"/> the job belongs to if any
/// </summary>
public Instance Instance { get; set; }
/// <inheritdoc />
public Api.Models.Job ToApi() => new Api.Models.Job
{
Id = Id,
StartedAt = StartedAt,
StoppedAt = StoppedAt,
Cancelled = Cancelled,
CancelledBy = CancelledBy.ToApi(),
CancelRight = CancelRight,
CancelRightsType = CancelRightsType,
Description = Description,
ExceptionDetails = ExceptionDetails,
StartedBy = StartedBy.ToApi()
};
}
}
@@ -1,9 +1,10 @@
using System.Collections.Generic;
using System.Linq;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation
public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation, IApiConvertable<Api.Models.RevisionInformation>
{
/// <summary>
/// The row Id
@@ -14,5 +15,13 @@ namespace Tgstation.Server.Host.Models
/// See <see cref="Api.Models.RevisionInformation.TestMerges"/>
/// </summary>
public List<TestMerge> TestMerges { get; set; }
/// <inheritdoc />
public Api.Models.RevisionInformation ToApi() => new Api.Models.RevisionInformation
{
Commit = Commit,
OriginRevision = OriginRevision,
TestMerges = TestMerges.Select(x => x.ToApi()).ToList()
};
}
}
+17 -1
View File
@@ -1,9 +1,10 @@
using System.ComponentModel.DataAnnotations;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class TestMerge : Api.Models.Internal.TestMerge
public sealed class TestMerge : Api.Models.Internal.TestMerge, IApiConvertable<Api.Models.TestMerge>
{
/// <summary>
/// See <see cref="Api.Models.TestMerge.MergedBy"/>
@@ -15,5 +16,20 @@ namespace Tgstation.Server.Host.Models
/// The <see cref="Models.RevisionInformation"/> for the <see cref="TestMerge"/>
/// </summary>
public RevisionInformation RevisionInformation { get; set; }
/// <inheritdoc />
public Api.Models.TestMerge ToApi() => new Api.Models.TestMerge
{
Author = Author,
BodyAtMerge = BodyAtMerge,
MergedAt = MergedAt,
TitleAtMerge = TitleAtMerge,
Comment = Comment,
Id = Id,
MergedBy = MergedBy.ToApi(),
Number =Number,
PullRequestRevision = PullRequestRevision,
Url = Url
};
}
}
+22 -1
View File
@@ -1,9 +1,10 @@
using System.Collections.Generic;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class User : Api.Models.Internal.User
public sealed class User : Api.Models.Internal.User, IApiConvertable<Api.Models.User>
{
/// <summary>
/// The hash of the user's password
@@ -24,5 +25,25 @@ namespace Tgstation.Server.Host.Models
/// The <see cref="InstanceUser"/>s for the <see cref="User"/>
/// </summary>
public List<InstanceUser> InstanceUsers { get; set; }
/// <summary>
/// See <see cref="ToApi()"/>
/// </summary>
/// <param name="recursive">If we should recurse on <see cref="CreatedBy"/></param>
/// <returns>A new <see cref="Api.Models.User"/></returns>
Api.Models.User ToApi(bool recursive) => new Api.Models.User
{
AdministrationRights = AdministrationRights,
CreatedAt = CreatedAt,
CreatedBy = recursive ? CreatedBy?.ToApi(false) : null,
Enabled = Enabled,
Id = Id,
InstanceManagerRights = InstanceManagerRights,
Name = Name,
SystemIdentifier = SystemIdentifier
};
/// <inheritdoc />
public Api.Models.User ToApi() => ToApi(true);
}
}