diff --git a/src/Tgstation.Server.Api/Models/CompileJob.cs b/src/Tgstation.Server.Api/Models/CompileJob.cs index a3b97ed6f7..359d7a02df 100644 --- a/src/Tgstation.Server.Api/Models/CompileJob.cs +++ b/src/Tgstation.Server.Api/Models/CompileJob.cs @@ -8,11 +8,6 @@ /// public User TriggeredBy { get; set; } - /// - /// The that cancelled the job if any - /// - public User CancelledBy { get; set; } - /// /// Git revision the compiler ran on. Not modifiable /// diff --git a/src/Tgstation.Server.Host/Components/DreamMaker.cs b/src/Tgstation.Server.Host/Components/DreamMaker.cs index 87508da8ca..eecc1b23d2 100644 --- a/src/Tgstation.Server.Host/Components/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/DreamMaker.cs @@ -19,11 +19,7 @@ namespace Tgstation.Server.Host.Components /// Name of the primary directory used for compilation /// const string ADirectoryName = "A"; - - /// - /// The for - /// - readonly IRepositoryManager repositoryManager; + /// /// The for /// @@ -48,15 +44,13 @@ namespace Tgstation.Server.Host.Components /// /// Construct /// - /// The value of /// The value of /// The value of /// The value of /// The value of /// The value of - public DreamMaker(IRepositoryManager repositoryManager, IIOManager ioManager, IConfiguration configuration, IDreamDaemonExecutor dreamDaemonExecutor, IByond byond, IInterop interop) + public DreamMaker(IIOManager ioManager, IConfiguration configuration, IDreamDaemonExecutor dreamDaemonExecutor, IByond byond, IInterop interop) { - this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); this.dreamDaemonExecutor = dreamDaemonExecutor ?? throw new ArgumentNullException(nameof(dreamDaemonExecutor)); @@ -195,7 +189,7 @@ namespace Tgstation.Server.Host.Components } /// - public async Task Compile(string dmeName, CancellationToken cancellationToken) + public async Task Compile(string dmeName, IRepository repository, CancellationToken cancellationToken) { var job = new Host.Models.CompileJob { @@ -203,68 +197,64 @@ namespace Tgstation.Server.Host.Components StartedAt = DateTimeOffset.Now, 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(), "B"); - - 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 (var repository = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) + 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"); + + async Task CleanupFailedCompile() { - job.RevisionInformation = new Host.Models.RevisionInformation + try { - Commit = repository.Head - }; - await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false); - } + await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false); + } + catch { } + }; - await ModifyDme(job, cancellationToken).ConfigureAwait(false); - - //run compiler, verify api - var ddVerified = await byond.UseExecutables(async (dreamMakerPath, dreamDaemonPath) => + try { - await RunDreamMaker(dreamMakerPath, job, cancellationToken).ConfigureAwait(false); + //copy the repository + var fullDirA = ioManager.ResolvePath(dirA); + using (repository) + await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false); - return await VerifyApi(dreamDaemonPath, job, cancellationToken).ConfigureAwait(false); - }, true).ConfigureAwait(false); + await ModifyDme(job, cancellationToken).ConfigureAwait(false); - if(!ddVerified) - { - //server never validated - job.FinishedAt = DateTimeOffset.Now; - await CleanupFailedCompile().ConfigureAwait(false); + //run compiler, verify api + var ddVerified = await byond.UseExecutables(async (dreamMakerPath, dreamDaemonPath) => + { + await RunDreamMaker(dreamMakerPath, job, cancellationToken).ConfigureAwait(false); + + 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; + + //duplicate the dmb et al + await ioManager.CopyDirectory(dirA, dirB, null, 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); + } return job; } - - job.DMApiValidated = true; - - //duplicate the dmb et al - await ioManager.CopyDirectory(dirA, dirB, null, 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); - - job.FinishedAt = DateTimeOffset.Now; - return job; + catch + { + await CleanupFailedCompile().ConfigureAwait(false); + throw; + } } - catch + finally { - await CleanupFailedCompile().ConfigureAwait(false); - throw; + job.FinishedAt = DateTimeOffset.Now; } } } diff --git a/src/Tgstation.Server.Host/Components/IDreamMaker.cs b/src/Tgstation.Server.Host/Components/IDreamMaker.cs index 8e6036dc55..527add5a8f 100644 --- a/src/Tgstation.Server.Host/Components/IDreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/IDreamMaker.cs @@ -14,8 +14,9 @@ namespace Tgstation.Server.Host.Components /// Starts a compile /// /// The .dme file to use without the extension + /// The to copy from /// The for the operation /// A resulting in the partially populated for the operation. In particular, note the field will only have it's field populated - Task Compile(string dmeName, CancellationToken cancellationToken); + Task Compile(string dmeName, IRepository repository, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Repository.cs b/src/Tgstation.Server.Host/Components/Repository.cs index 20d8d569b4..50364ca9b1 100644 --- a/src/Tgstation.Server.Host/Components/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository.cs @@ -62,7 +62,7 @@ namespace Tgstation.Server.Host.Components public void Dispose() { repository.Dispose(); - onDispose(); + onDispose.Invoke(); } /// /// Convert to an "https://@{url} equivalent diff --git a/src/Tgstation.Server.Host/Components/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/RepositoryManager.cs index 627ba44cfa..5982887adb 100644 --- a/src/Tgstation.Server.Host/Components/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/RepositoryManager.cs @@ -127,7 +127,12 @@ namespace Tgstation.Server.Host.Components { repo = new LibGit2Sharp.Repository(ioManager.ResolvePath(".")); }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); - return new Repository(repo, ioManager, () => semaphore.Release()); + var localSemaphore = semaphore; + return new Repository(repo, ioManager, () => + { + localSemaphore?.Release(); + localSemaphore = null; + }); } /// diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs new file mode 100644 index 0000000000..13ce786f5b --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Components; +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Security; + +namespace Tgstation.Server.Host.Controllers +{ + /// + /// Controller for managing the compiler + /// + [Route("/DreamMaker")] + public sealed class DreamMakerController : ModelController + { + /// + /// The for the + /// + readonly IJobManager jobManager; + + /// + /// Construct a + /// + /// 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)); + + /// + [TgsAuthorize(DreamMakerRights.Compile)] + public override async Task Create([FromBody] Api.Models.CompileJob model, CancellationToken cancellationToken) + { + var job = new Job + { + Description = "Compile active repository code", + StartedBy = AuthenticationContext.User + }; + await jobManager.RegisterOperation(job, (serviceProvider, ct) => RunCompile(serviceProvider, Instance, AuthenticationContext.Clone(), ct), cancellationToken).ConfigureAwait(false); + return Json(job); + } + + /// + public override async Task Delete([FromBody] Api.Models.CompileJob 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); + if (job.StoppedAt != null) + return StatusCode(HttpStatusCode.Gone); + jobManager.CancelJob(job); + return Ok(); + } + + /// + /// Run the compile job and insert it into the database + /// + /// The for the operation + /// The for the operation + /// The for the operation + /// The for the operation + /// + static async Task RunCompile(IServiceProvider serviceProvider, Models.Instance instanceModel, IAuthenticationContext authenticationContext, CancellationToken cancellationToken) + { + var instanceManager = serviceProvider.GetRequiredService(); + var databaseContext = serviceProvider.GetRequiredService(); + + var projectName = await databaseContext.Instances.Where(x => x.Id == instanceModel.Id).Select(x => x.DreamMakerSettings.ProjectName).FirstAsync(cancellationToken).ConfigureAwait(false); + + var instance = instanceManager.GetInstance(instanceModel); + + CompileJob compileJob; + Task revInfoTask; + using (var repo = await instance.RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) + { + revInfoTask = databaseContext.RevisionInformations.Where(x => x.Commit == repo.Head).Select(x => new RevisionInformation { Id = x.Id }).FirstAsync(); + compileJob = await instance.DreamMaker.Compile(projectName, repo, cancellationToken).ConfigureAwait(false); + } + + compileJob.TriggeredBy = authenticationContext.User; + compileJob.RevisionInformation = await revInfoTask.ConfigureAwait(false); + + databaseContext.CompileJobs.Add(compileJob); + //default ct because we don't want to give up after getting this far + await databaseContext.Save(default).ConfigureAwait(false); + } + } +} diff --git a/src/Tgstation.Server.Host/Core/IJobManager.cs b/src/Tgstation.Server.Host/Core/IJobManager.cs index 13d30ad436..ce5798ac3f 100644 --- a/src/Tgstation.Server.Host/Core/IJobManager.cs +++ b/src/Tgstation.Server.Host/Core/IJobManager.cs @@ -1,11 +1,15 @@ -using System; +using Microsoft.Extensions.DependencyInjection; +using System; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Core { - interface IJobManager + /// + /// Manages the runtime of s + /// + public interface IJobManager { /// /// Registers a given and begins running it @@ -14,7 +18,7 @@ namespace Tgstation.Server.Host.Core /// The operation to run /// The for the operation /// A representing a running operation - Task RegisterOperation(Job job, Func operation, CancellationToken cancellationToken); + Task RegisterOperation(Job job, Func operation, CancellationToken cancellationToken); /// /// Wait for a given to complete diff --git a/src/Tgstation.Server.Host/Core/JobManager.cs b/src/Tgstation.Server.Host/Core/JobManager.cs index e81f8ece73..995b544a8f 100644 --- a/src/Tgstation.Server.Host/Core/JobManager.cs +++ b/src/Tgstation.Server.Host/Core/JobManager.cs @@ -53,16 +53,23 @@ namespace Tgstation.Server.Host.Core /// The operation for the /// The for the operation /// A representing the running operation - async Task RunJob(Job job, Func operation, CancellationToken cancellationToken) + async Task RunJob(Job job, Func operation, CancellationToken cancellationToken) { using (var scope = serviceProvider.CreateScope()) { - var databaseContext = scope.ServiceProvider.GetRequiredService(); - job = new Job { Id = job.Id }; - databaseContext.Jobs.Attach(job); + IDatabaseContext databaseContext = null; try { - await operation(cancellationToken).ConfigureAwait(false); + try + { + await operation(scope.ServiceProvider, cancellationToken).ConfigureAwait(false); + } + finally + { + job = new Job { Id = job.Id }; + databaseContext = scope.ServiceProvider.GetRequiredService(); + databaseContext.Jobs.Attach(job); + } } catch (OperationCanceledException) { @@ -78,7 +85,7 @@ namespace Tgstation.Server.Host.Core } /// - public async Task RegisterOperation(Job job, Func operation, CancellationToken cancellationToken) + public async Task RegisterOperation(Job job, Func operation, CancellationToken cancellationToken) { using (var scope = serviceProvider.CreateScope()) { diff --git a/src/Tgstation.Server.Host/Models/CompileJob.cs b/src/Tgstation.Server.Host/Models/CompileJob.cs index a22e7ec5c3..8a34707703 100644 --- a/src/Tgstation.Server.Host/Models/CompileJob.cs +++ b/src/Tgstation.Server.Host/Models/CompileJob.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.DataAnnotations; - -namespace Tgstation.Server.Host.Models +namespace Tgstation.Server.Host.Models { /// public sealed class CompileJob : Api.Models.Internal.CompileJob @@ -10,11 +8,6 @@ namespace Tgstation.Server.Host.Models /// public User TriggeredBy { get; set; } - /// - /// See - /// - public User CancelledBy { get; set; } - /// /// See /// diff --git a/src/Tgstation.Server.Host/Models/DatabaseContext.cs b/src/Tgstation.Server.Host/Models/DatabaseContext.cs index 5f0ffef80f..3d848add3e 100644 --- a/src/Tgstation.Server.Host/Models/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Models/DatabaseContext.cs @@ -22,6 +22,12 @@ namespace Tgstation.Server.Host.Models /// public DbSet Instances { get; set; } + /// + public DbSet CompileJobs { get; set; } + + /// + public DbSet RevisionInformations { get; set; } + /// /// The for s /// @@ -48,10 +54,6 @@ namespace Tgstation.Server.Host.Models /// public DbSet DreamMakerSettings { get; set; } /// - /// The s in the - /// - public DbSet CompileJobs { get; set; } - /// /// The s in the /// public DbSet Jobs { get; set; } @@ -60,10 +62,6 @@ namespace Tgstation.Server.Host.Models /// public DbSet TestMerges { get; set; } /// - /// The s in the - /// - public DbSet RevisionInformations { get; set; } - /// /// The in the /// public DbSet RepositorySettings { get; set; } diff --git a/src/Tgstation.Server.Host/Models/IDatabaseContext.cs b/src/Tgstation.Server.Host/Models/IDatabaseContext.cs index c9076a354b..da1bea8f12 100644 --- a/src/Tgstation.Server.Host/Models/IDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Models/IDatabaseContext.cs @@ -24,6 +24,16 @@ namespace Tgstation.Server.Host.Models /// DbSet Jobs { get; } + /// + /// The s in the + /// + DbSet CompileJobs { get; } + + /// + /// The s in the + /// + DbSet RevisionInformations { get; } + /// /// Get the in the ///