mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 07:04:57 +01:00
DreamMakerController
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user