DmbFactory and Provider

This commit is contained in:
Cyberboss
2018-05-04 15:50:04 -04:00
parent 40ffe4bf2f
commit 523e19fbf7
7 changed files with 241 additions and 7 deletions
@@ -0,0 +1,134 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components
{
/// <summary>
/// Standard <see cref="IDmbFactory"/>
/// </summary>
sealed class DmbFactory : IDmbFactory, ICompileJobConsumer, IHostedService, IDisposable
{
/// <summary>
/// The <see cref="IDatabaseContext"/> for the <see cref="DmbFactory"/>
/// </summary>
readonly IDatabaseContext databaseContext;
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="DmbFactory"/>
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="CancellationTokenSource"/> for <see cref="cleanupTask"/>
/// </summary>
readonly CancellationTokenSource cleanupCts;
/// <summary>
/// <see cref="Task"/> representing calls to <see cref="CleanJob(CompileJob)"/>
/// </summary>
Task cleanupTask;
/// <summary>
/// <see cref="TaskCompletionSource{TResult}"/> resulting in the latest <see cref="DmbProvider"/> yet to exist
/// </summary>
TaskCompletionSource<DmbProvider> newerDmbTcs;
/// <summary>
/// The latest <see cref="DmbProvider"/>
/// </summary>
DmbProvider nextDmbProvider;
/// <summary>
/// Construct a <see cref="DmbFactory"/>
/// </summary>
/// <param name="databaseContext">The value of <see cref="databaseContext"/></param>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
public DmbFactory(IDatabaseContext databaseContext, IIOManager ioManager)
{
this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
cleanupCts = new CancellationTokenSource();
}
/// <inheritdoc />
public void Dispose() => cleanupCts.Dispose();
/// <summary>
/// Delete the <see cref="Api.Models.Internal.CompileJob.DirectoryName"/> of <paramref name="job"/>
/// </summary>
/// <param name="job">The <see cref="CompileJob"/> to clean</param>
void CleanJob(CompileJob job)
{
async Task HandleCleanup()
{
var deleteJob = ioManager.DeleteDirectory(job.DirectoryName.ToString(), cleanupCts.Token);
Task otherTask;
lock (this)
otherTask = cleanupTask;
await Task.WhenAll(otherTask, deleteJob).ConfigureAwait(false);
}
lock (this)
cleanupTask = HandleCleanup();
}
/// <inheritdoc />
public void LoadCompileJob(CompileJob job)
{
if (job == null)
throw new ArgumentNullException(nameof(job));
if (!job.DMApiValidated || job.Job.Cancelled || job.Job.ExceptionDetails != null || job.Job.StoppedAt == null)
throw new InvalidOperationException("Cannot load incomplete compile job!");
lock (this)
{
var oldDmbProvider = nextDmbProvider;
if (oldDmbProvider != null && oldDmbProvider.CompileJob.Job.StoppedAt < oldDmbProvider.CompileJob.Job.StoppedAt)
throw new InvalidOperationException("Loaded compile job older than current job!");
nextDmbProvider = new DmbProvider(job, ioManager, () => CleanJob(job));
newerDmbTcs.SetResult(nextDmbProvider);
newerDmbTcs = new TaskCompletionSource<DmbProvider>();
}
}
/// <inheritdoc />
public async Task<IDmbProvider> LockNextDmb(CancellationToken cancellationToken)
{
Task<DmbProvider> task;
lock (this)
if (nextDmbProvider != null)
return nextDmbProvider;
else
task = newerDmbTcs.Task;
return await task.ConfigureAwait(false);
}
/// <inheritdoc />
public Task OnNewerDmb()
{
lock (this)
return newerDmbTcs.Task;
}
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
{
var cj = await databaseContext.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)
{
using (cancellationToken.Register(() => cleanupCts.Cancel()))
await cleanupTask.ConfigureAwait(false);
}
}
}
@@ -0,0 +1,58 @@
using System;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components
{
/// <inheritdoc />
sealed class DmbProvider : IDmbProvider
{
/// <inheritdoc />
public string DmbName => String.Concat(CompileJob.DmeName, DreamMaker.DmbExtension);
/// <inheritdoc />
public string PrimaryDirectory => ioManager.ResolvePath(ioManager.ConcatPath(CompileJob.DirectoryName.ToString(), DreamMaker.ADirectoryName));
/// <inheritdoc />
public string SecondaryDirectory => ioManager.ResolvePath(ioManager.ConcatPath(CompileJob.DirectoryName.ToString(), DreamMaker.BDirectoryName));
/// <inheritdoc />
public RevisionInformation RevisionInformation => CompileJob.RevisionInformation;
/// <summary>
/// The <see cref="CompileJob"/> for the <see cref="DmbProvider"/>
/// </summary>
public CompileJob CompileJob { get; }
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="DmbProvider"/>
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="Action"/> to run when <see cref="Dispose"/> is called
/// </summary>
readonly Action onDispose;
/// <summary>
/// Construct a <see cref="DmbProvider"/>
/// </summary>
/// <param name="compileJob">The value of <see cref="CompileJob"/></param>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
/// <param name="onDispose">The value of <see cref="onDispose"/></param>
public DmbProvider(CompileJob compileJob, IIOManager ioManager, Action onDispose)
{
CompileJob = compileJob ?? throw new ArgumentNullException(nameof(compileJob));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
}
~DmbProvider() => Dispose();
/// <inheritdoc />
public void Dispose()
{
onDispose();
GC.SuppressFinalize(this);
}
}
}
@@ -18,8 +18,15 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// Name of the primary directory used for compilation
/// </summary>
const string ADirectoryName = "A";
public const string ADirectoryName = "A";
/// <summary>
/// Name of the secondary directory used for compilation
/// </summary>
public const string BDirectoryName = "B";
public const string DmbExtension = ".dmb";
const string DmeExtension = ".dme";
/// <summary>
/// The <see cref="IIOManager"/> for <see cref="DreamMaker"/>
/// </summary>
@@ -90,7 +97,7 @@ namespace Tgstation.Server.Host.Components
ddTcs.SetResult(null);
};
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
var ddTestTask = dreamDaemonExecutor.RunDreamDaemon(launchParameters, null, dreamDaemonPath, new TemporaryDmbProvider(ioManager.ResolvePath(ioManager.GetDirectoryName(dirA)), ioManager.ResolvePath(ioManager.ConcatPath(dirA, String.Concat(job.DmeName, ".dmb")))), interopInfo, true, cts.Token);
var ddTestTask = dreamDaemonExecutor.RunDreamDaemon(launchParameters, null, dreamDaemonPath, new TemporaryDmbProvider(ioManager.ResolvePath(ioManager.GetDirectoryName(dirA)), ioManager.ResolvePath(ioManager.ConcatPath(dirA, String.Concat(job.DmeName, DmbExtension)))), interopInfo, true, cts.Token);
await Task.WhenAny(ddTcs.Task, ddTestTask).ConfigureAwait(false);
@@ -113,7 +120,7 @@ namespace Tgstation.Server.Host.Components
using (var dm = new Process())
{
dm.StartInfo.FileName = dreamMakerPath;
dm.StartInfo.Arguments = String.Format(CultureInfo.InvariantCulture, "-clean {0}.dme", job.DmeName);
dm.StartInfo.Arguments = String.Format(CultureInfo.InvariantCulture, "-clean {0}{1}", job.DmeName, DmeExtension);
dm.StartInfo.WorkingDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName));
dm.StartInfo.RedirectStandardOutput = true;
dm.StartInfo.RedirectStandardError = true;
@@ -155,7 +162,7 @@ namespace Tgstation.Server.Host.Components
async Task ModifyDme(Host.Models.CompileJob job, CancellationToken cancellationToken)
{
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
var dmePath = ioManager.ConcatPath(dirA, String.Concat(job.DmeName, ".dme"));
var dmePath = ioManager.ConcatPath(dirA, String.Concat(job.DmeName, DmeExtension));
var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken);
var dmeModificationsTask = configuration.CopyDMFilesTo(ioManager.ResolvePath(dirA), cancellationToken);
@@ -198,7 +205,7 @@ namespace Tgstation.Server.Host.Components
};
await ioManager.CreateDirectory(job.DirectoryName.ToString(), cancellationToken).ConfigureAwait(false);
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), "B");
var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), BDirectoryName);
async Task CleanupFailedCompile()
{
@@ -0,0 +1,11 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components
{
interface ICompileJobConsumer
{
void LoadCompileJob(CompileJob job);
}
}
@@ -31,7 +31,10 @@ 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) => this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager) : base(databaseContext, authenticationContextFactory)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
}
/// <inheritdoc />
[TgsAuthorize(DreamMakerRights.Compile)]
@@ -203,5 +203,18 @@ namespace Tgstation.Server.Host.Core
using (var file = File.Open(path, FileMode.Create, FileAccess.Write))
await file.WriteAsync(contents, 0, contents.Length, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public Task<IReadOnlyList<string>> GetDirectories(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
path = ResolvePath(path);
var results = new List<string>();
foreach(var I in Directory.EnumerateDirectories(path))
{
cancellationToken.ThrowIfCancellationRequested();
results.Add(I);
}
return (IReadOnlyList<string>)results;
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}
}
@@ -56,6 +56,14 @@ namespace Tgstation.Server.Host.Core
/// <returns>A <see cref="Task"/> that results in the contents of a file at <paramref name="path"/></returns>
Task<byte[]> ReadAllBytes(string path, CancellationToken cancellationToken);
/// <summary>
/// Returns directory names in a given <paramref name="path"/>
/// </summary>
/// <param name="path">The path to search for directories</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the directories in <paramref name="path"/></returns>
Task<IReadOnlyList<string>> GetDirectories(string path, CancellationToken cancellationToken);
/// <summary>
/// Writes some <paramref name="contents"/> to a file at <paramref name="path"/> overwriting previous content
/// </summary>