DreamMakerController

This commit is contained in:
Cyberboss
2018-05-04 13:32:34 -04:00
parent 7f9cf6f29a
commit b8b652c7a5
11 changed files with 188 additions and 92 deletions
@@ -8,11 +8,6 @@
/// </summary>
public User TriggeredBy { get; set; }
/// <summary>
/// The <see cref="User"/> that cancelled the job if any
/// </summary>
public User CancelledBy { get; set; }
/// <summary>
/// Git revision the compiler ran on. Not modifiable
/// </summary>
@@ -19,11 +19,7 @@ namespace Tgstation.Server.Host.Components
/// Name of the primary directory used for compilation
/// </summary>
const string ADirectoryName = "A";
/// <summary>
/// The <see cref="IRepositoryManager"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IRepositoryManager repositoryManager;
/// <summary>
/// The <see cref="IIOManager"/> for <see cref="DreamMaker"/>
/// </summary>
@@ -48,15 +44,13 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// Construct <see cref="DreamMaker"/>
/// </summary>
/// <param name="repositoryManager">The value of <see cref="repositoryManager"/></param>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
/// <param name="configuration">The value of <see cref="configuration"/></param>
/// <param name="dreamDaemonExecutor">The value of <see cref="dreamDaemonExecutor"/></param>
/// <param name="byond">The value of <see cref="byond"/></param>
/// <param name="interop">The value of <see cref="interop"/></param>
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
}
/// <inheritdoc />
public async Task<Host.Models.CompileJob> Compile(string dmeName, CancellationToken cancellationToken)
public async Task<Host.Models.CompileJob> 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;
}
}
}
@@ -14,8 +14,9 @@ namespace Tgstation.Server.Host.Components
/// Starts a compile
/// </summary>
/// <param name="dmeName">The .dme file to use 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, CancellationToken cancellationToken);
Task<CompileJob> Compile(string dmeName, IRepository repository, CancellationToken cancellationToken);
}
}
@@ -62,7 +62,7 @@ namespace Tgstation.Server.Host.Components
public void Dispose()
{
repository.Dispose();
onDispose();
onDispose.Invoke();
}
/// <summary>
/// Convert <paramref name="url"/> to an "https://<paramref name="accessString"/>@{url} equivalent
@@ -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;
});
}
/// <inheritdoc />
@@ -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
{
/// <summary>
/// Controller for managing the compiler
/// </summary>
[Route("/DreamMaker")]
public sealed class DreamMakerController : ModelController<Api.Models.CompileJob>
{
/// <summary>
/// The <see cref="IJobManager"/> for the <see cref="DreamMakerController"/>
/// </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 DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager) : base(databaseContext, authenticationContextFactory) => this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
/// <inheritdoc />
[TgsAuthorize(DreamMakerRights.Compile)]
public override async Task<IActionResult> 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);
}
/// <inheritdoc />
public override async Task<IActionResult> 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();
}
/// <summary>
/// Run the compile job and insert it into the database
/// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> for the operation</param>
/// <param name="instanceModel">The <see cref="Models.Instance"/> for the operation</param>
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> for the operation</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns></returns>
static async Task RunCompile(IServiceProvider serviceProvider, Models.Instance instanceModel, IAuthenticationContext authenticationContext, CancellationToken cancellationToken)
{
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 instance = instanceManager.GetInstance(instanceModel);
CompileJob compileJob;
Task<RevisionInformation> 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);
}
}
}
@@ -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
/// <summary>
/// Manages the runtime of <see cref="Job"/>s
/// </summary>
public interface IJobManager
{
/// <summary>
/// Registers a given <see cref="Job"/> and begins running it
@@ -14,7 +18,7 @@ namespace Tgstation.Server.Host.Core
/// <param name="operation">The operation to run</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing a running operation</returns>
Task RegisterOperation(Job job, Func<CancellationToken, Task> operation, CancellationToken cancellationToken);
Task RegisterOperation(Job job, Func<IServiceProvider, CancellationToken, Task> operation, CancellationToken cancellationToken);
/// <summary>
/// Wait for a given <paramref name="job"/> to complete
+13 -6
View File
@@ -53,16 +53,23 @@ namespace Tgstation.Server.Host.Core
/// <param name="operation">The operation for the <paramref name="job"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task RunJob(Job job, Func<CancellationToken, Task> operation, CancellationToken cancellationToken)
async Task RunJob(Job job, Func<IServiceProvider, CancellationToken, Task> operation, CancellationToken cancellationToken)
{
using (var scope = serviceProvider.CreateScope())
{
var databaseContext = scope.ServiceProvider.GetRequiredService<IDatabaseContext>();
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<IDatabaseContext>();
databaseContext.Jobs.Attach(job);
}
}
catch (OperationCanceledException)
{
@@ -78,7 +85,7 @@ namespace Tgstation.Server.Host.Core
}
/// <inheritdoc />
public async Task RegisterOperation(Job job, Func<CancellationToken, Task> operation, CancellationToken cancellationToken)
public async Task RegisterOperation(Job job, Func<IServiceProvider, CancellationToken, Task> operation, CancellationToken cancellationToken)
{
using (var scope = serviceProvider.CreateScope())
{
@@ -1,6 +1,4 @@
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Host.Models
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class CompileJob : Api.Models.Internal.CompileJob
@@ -10,11 +8,6 @@ namespace Tgstation.Server.Host.Models
/// </summary>
public User TriggeredBy { get; set; }
/// <summary>
/// See <see cref="Api.Models.CompileJob.CancelledBy"/>
/// </summary>
public User CancelledBy { get; set; }
/// <summary>
/// See <see cref="Api.Models.CompileJob.RevisionInformation"/>
/// </summary>
@@ -22,6 +22,12 @@ namespace Tgstation.Server.Host.Models
/// <inheritdoc />
public DbSet<Instance> Instances { get; set; }
/// <inheritdoc />
public DbSet<CompileJob> CompileJobs { get; set; }
/// <inheritdoc />
public DbSet<RevisionInformation> RevisionInformations { get; set; }
/// <summary>
/// The <see cref="DbSet{TEntity}"/> for <see cref="Log"/>s
/// </summary>
@@ -48,10 +54,6 @@ namespace Tgstation.Server.Host.Models
/// </summary>
public DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
/// <summary>
/// The <see cref="CompileJob"/>s in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<CompileJob> CompileJobs { get; set; }
/// <summary>
/// The <see cref="Job"/>s in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<Job> Jobs { get; set; }
@@ -60,10 +62,6 @@ namespace Tgstation.Server.Host.Models
/// </summary>
public DbSet<TestMerge> TestMerges { get; set; }
/// <summary>
/// The <see cref="RevisionInformation"/>s in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<RevisionInformation> RevisionInformations { get; set; }
/// <summary>
/// The <see cref="Models.RepositorySettings"/> in the <see cref="DatabaseContext{TParentContext}"/>
/// </summary>
public DbSet<RepositorySettings> RepositorySettings { get; set; }
@@ -24,6 +24,16 @@ namespace Tgstation.Server.Host.Models
/// </summary>
DbSet<Job> Jobs { get; }
/// <summary>
/// The <see cref="CompileJob"/>s in the <see cref="IDatabaseContext"/>
/// </summary>
DbSet<CompileJob> CompileJobs { get; }
/// <summary>
/// The <see cref="RevisionInformation"/>s in the <see cref="IDatabaseContext"/>
/// </summary>
DbSet<RevisionInformation> RevisionInformations { get; }
/// <summary>
/// Get the <see cref="ServerSettings"/> in the <see cref="IDatabaseContext"/>
/// </summary>