Remove timer shennanigans from repository. Fix null project name handling. Add IDatabaseContextFactory

This commit is contained in:
Cyberboss
2018-05-07 10:06:01 -04:00
parent 74833eb9bc
commit 720863d55e
16 changed files with 181 additions and 153 deletions
@@ -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; }
}
}
@@ -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,8 +24,13 @@ 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";
/// <summary>
@@ -61,6 +67,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));
@@ -202,12 +209,12 @@ 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
DmeName = projectName
};
await ioManager.CreateDirectory(job.DirectoryName.ToString(), cancellationToken).ConfigureAwait(false);
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
@@ -229,6 +236,16 @@ namespace Tgstation.Server.Host.Components
using (repository)
await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false);
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);
//run compiler, verify api
@@ -263,18 +280,5 @@ namespace Tgstation.Server.Host.Components
throw;
}
}
/// <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();
}
}
}
@@ -8,15 +8,15 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// For managing the compiler
/// </summary>
interface IDreamMaker : IHostedService
interface IDreamMaker
{
/// <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,4 +1,5 @@
using Microsoft.Extensions.Hosting;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Components
{
@@ -19,5 +20,7 @@ namespace Tgstation.Server.Host.Components
Api.Models.Instance GetMetadata();
void Rename(string newName);
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,8 @@ 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>
/// <param name="databaseContextFactory">The <see cref="IDatabaseContextFactory"/> 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, IDatabaseContextFactory databaseContextFactory);
}
}
@@ -7,8 +7,8 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// Factory for creating and loading <see cref="IRepository"/>s
/// </summary>
interface IRepositoryManager : IHostedService
{
interface IRepositoryManager
{
/// <summary>
/// Attempt to load the <see cref="IRepository"/> from the default location
/// </summary>
@@ -24,12 +24,5 @@ namespace Tgstation.Server.Host.Components
/// <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);
}
}
@@ -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
{
@@ -19,7 +18,7 @@ namespace Tgstation.Server.Host.Components
public InstanceFactory(IIOManager ioManager) => this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
/// <inheritdoc />
public IInstance CreateInstance(Host.Models.Instance metadata, IDatabaseContext databaseContext)
public IInstance CreateInstance(Host.Models.Instance metadata, IDatabaseContextFactory databaseContextFactory)
{
//Create the ioManager for the instance
@@ -32,7 +31,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);
@@ -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,49 +38,7 @@ 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)
@@ -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);
}
}
}
@@ -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);
}
}