From 523e19fbf72f474d6d02887bec3dcb82f83e2bba Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 4 May 2018 15:50:04 -0400 Subject: [PATCH] DmbFactory and Provider --- .../Components/DmbFactory.cs | 134 ++++++++++++++++++ .../Components/DmbProvider.cs | 58 ++++++++ .../Components/DreamMaker.cs | 19 ++- .../Components/ICompileJobConsumer.cs | 11 ++ .../Controllers/DreamMakerController.cs | 5 +- .../Core/DefaultIOManager.cs | 13 ++ src/Tgstation.Server.Host/Core/IIOManager.cs | 8 ++ 7 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/DmbFactory.cs create mode 100644 src/Tgstation.Server.Host/Components/DmbProvider.cs create mode 100644 src/Tgstation.Server.Host/Components/ICompileJobConsumer.cs diff --git a/src/Tgstation.Server.Host/Components/DmbFactory.cs b/src/Tgstation.Server.Host/Components/DmbFactory.cs new file mode 100644 index 0000000000..2fd12b08a2 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/DmbFactory.cs @@ -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 +{ + /// + /// Standard + /// + sealed class DmbFactory : IDmbFactory, ICompileJobConsumer, IHostedService, IDisposable + { + /// + /// The for the + /// + readonly IDatabaseContext databaseContext; + /// + /// The for the + /// + readonly IIOManager ioManager; + /// + /// The for + /// + readonly CancellationTokenSource cleanupCts; + + /// + /// representing calls to + /// + Task cleanupTask; + /// + /// resulting in the latest yet to exist + /// + TaskCompletionSource newerDmbTcs; + /// + /// The latest + /// + DmbProvider nextDmbProvider; + + /// + /// Construct a + /// + /// The value of + /// The value of + 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(); + } + + /// + public void Dispose() => cleanupCts.Dispose(); + + /// + /// Delete the of + /// + /// The to clean + 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(); + } + + /// + 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(); + } + } + + /// + public async Task LockNextDmb(CancellationToken cancellationToken) + { + Task task; + lock (this) + if (nextDmbProvider != null) + return nextDmbProvider; + else + task = newerDmbTcs.Task; + + return await task.ConfigureAwait(false); + } + + /// + public Task OnNewerDmb() + { + lock (this) + return newerDmbTcs.Task; + } + + /// + 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); + } + + /// + public async Task StopAsync(CancellationToken cancellationToken) + { + using (cancellationToken.Register(() => cleanupCts.Cancel())) + await cleanupTask.ConfigureAwait(false); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/DmbProvider.cs b/src/Tgstation.Server.Host/Components/DmbProvider.cs new file mode 100644 index 0000000000..0cd3f758f1 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/DmbProvider.cs @@ -0,0 +1,58 @@ +using System; +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Components +{ + /// + sealed class DmbProvider : IDmbProvider + { + /// + public string DmbName => String.Concat(CompileJob.DmeName, DreamMaker.DmbExtension); + + /// + public string PrimaryDirectory => ioManager.ResolvePath(ioManager.ConcatPath(CompileJob.DirectoryName.ToString(), DreamMaker.ADirectoryName)); + + /// + public string SecondaryDirectory => ioManager.ResolvePath(ioManager.ConcatPath(CompileJob.DirectoryName.ToString(), DreamMaker.BDirectoryName)); + + /// + public RevisionInformation RevisionInformation => CompileJob.RevisionInformation; + + /// + /// The for the + /// + public CompileJob CompileJob { get; } + + /// + /// The for the + /// + readonly IIOManager ioManager; + /// + /// The to run when is called + /// + readonly Action onDispose; + + /// + /// Construct a + /// + /// The value of + /// The value of + /// The value of + 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(); + + /// + public void Dispose() + { + onDispose(); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/DreamMaker.cs b/src/Tgstation.Server.Host/Components/DreamMaker.cs index f43598ccdb..8469838b66 100644 --- a/src/Tgstation.Server.Host/Components/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/DreamMaker.cs @@ -18,8 +18,15 @@ namespace Tgstation.Server.Host.Components /// /// Name of the primary directory used for compilation /// - const string ADirectoryName = "A"; - + public const string ADirectoryName = "A"; + /// + /// Name of the secondary directory used for compilation + /// + public const string BDirectoryName = "B"; + public const string DmbExtension = ".dmb"; + + const string DmeExtension = ".dme"; + /// /// The for /// @@ -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() { diff --git a/src/Tgstation.Server.Host/Components/ICompileJobConsumer.cs b/src/Tgstation.Server.Host/Components/ICompileJobConsumer.cs new file mode 100644 index 0000000000..0877e9516c --- /dev/null +++ b/src/Tgstation.Server.Host/Components/ICompileJobConsumer.cs @@ -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); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index 6b849d8636..56586f7bfd 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -31,7 +31,10 @@ namespace Tgstation.Server.Host.Controllers /// The for the /// The for the /// The value of - 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)); + } /// [TgsAuthorize(DreamMakerRights.Compile)] diff --git a/src/Tgstation.Server.Host/Core/DefaultIOManager.cs b/src/Tgstation.Server.Host/Core/DefaultIOManager.cs index f0644d6f33..f1cc847bc4 100644 --- a/src/Tgstation.Server.Host/Core/DefaultIOManager.cs +++ b/src/Tgstation.Server.Host/Core/DefaultIOManager.cs @@ -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); } + + /// + public Task> GetDirectories(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => + { + path = ResolvePath(path); + var results = new List(); + foreach(var I in Directory.EnumerateDirectories(path)) + { + cancellationToken.ThrowIfCancellationRequested(); + results.Add(I); + } + return (IReadOnlyList)results; + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); } } diff --git a/src/Tgstation.Server.Host/Core/IIOManager.cs b/src/Tgstation.Server.Host/Core/IIOManager.cs index 2083ad84aa..38df374e63 100644 --- a/src/Tgstation.Server.Host/Core/IIOManager.cs +++ b/src/Tgstation.Server.Host/Core/IIOManager.cs @@ -56,6 +56,14 @@ namespace Tgstation.Server.Host.Core /// A that results in the contents of a file at Task ReadAllBytes(string path, CancellationToken cancellationToken); + /// + /// Returns directory names in a given + /// + /// The path to search for directories + /// The for the operation + /// A resulting in the directories in + Task> GetDirectories(string path, CancellationToken cancellationToken); + /// /// Writes some to a file at overwriting previous content ///