diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 2cedce3c5e..87626da1fc 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -1,11 +1,11 @@ -using Cyberboss.AspNetCore.AsyncInitializer; -using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; using System; using System.Globalization; @@ -34,16 +34,16 @@ namespace Tgstation.Server.Host.Core readonly IConfiguration configuration; /// - /// The for the + /// The for the /// - readonly IHostingEnvironment hostingEnvironment; + readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment; /// /// Construct an /// /// The value of /// The value of - public Application(IConfiguration configuration, IHostingEnvironment hostingEnvironment) + public Application(IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment) { this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); @@ -131,6 +131,10 @@ namespace Tgstation.Server.Host.Core services.AddSingleton, PasswordHasher>(); services.AddSingleton(); services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(x => x.GetRequiredService()); + services.AddSingleton(x => x.GetRequiredService()); } /// @@ -144,13 +148,7 @@ namespace Tgstation.Server.Host.Core if (hostingEnvironment.IsDevelopment()) applicationBuilder.UseDeveloperExceptionPage(); - - applicationBuilder.UseAsyncInitialization(async (cancellationToken) => - { - using (var scope = applicationBuilder.ApplicationServices.CreateScope()) - await scope.ServiceProvider.GetRequiredService().Initialize(cancellationToken).ConfigureAwait(false); - }); - + applicationBuilder.UseAuthentication(); applicationBuilder.UseMvc(); } diff --git a/src/Tgstation.Server.Host/Core/IJobManager.cs b/src/Tgstation.Server.Host/Core/IJobManager.cs new file mode 100644 index 0000000000..13d30ad436 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/IJobManager.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Core +{ + interface IJobManager + { + /// + /// Registers a given and begins running it + /// + /// The + /// The operation to run + /// The for the operation + /// A representing a running operation + Task RegisterOperation(Job job, Func operation, CancellationToken cancellationToken); + + /// + /// Wait for a given to complete + /// + /// The to wait for + /// The for the operation + /// A representing a running operation + Task WaitForJob(Job job, CancellationToken cancellationToken); + + /// + /// Cancels a give + /// + /// The to cancel + void CancelJob(Job job); + } +} diff --git a/src/Tgstation.Server.Host/Core/JobHandler.cs b/src/Tgstation.Server.Host/Core/JobHandler.cs new file mode 100644 index 0000000000..ab05719268 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/JobHandler.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Core +{ + /// + /// Class for pairing s with s + /// + sealed class JobHandler : IDisposable + { + /// + /// The being run + /// + readonly Task task; + /// + /// The for + /// + readonly CancellationTokenSource cancellationTokenSource; + + /// + /// Construct a + /// + /// The value of + /// The value of + JobHandler(Task task, CancellationTokenSource cancellationTokenSource) + { + this.task = task; + this.cancellationTokenSource = cancellationTokenSource; + } + + /// + public void Dispose() => cancellationTokenSource.Dispose(); + + /// + /// Wait for to complete + /// + /// The for the operation + /// A representing the running operation + public async Task Wait(CancellationToken cancellationToken) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + using (cancellationToken.Register(() => tcs.SetCanceled())) + await Task.WhenAny(tcs.Task, task).ConfigureAwait(false); + } + + /// + /// Cancels + /// + public void Cancel() => cancellationTokenSource.Cancel(); + + /// + /// Create a + /// + /// A taking a and returning a that the will wrap + /// A new + public static JobHandler Create(Func job) + { + var cts = new CancellationTokenSource(); + return new JobHandler(job(cts.Token), cts); + } + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Core/JobManager.cs b/src/Tgstation.Server.Host/Core/JobManager.cs new file mode 100644 index 0000000000..94fceefca0 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/JobManager.cs @@ -0,0 +1,143 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Core +{ + /// + sealed class JobManager : IHostedService, IJobManager + { + /// + /// The for the + /// + readonly IServiceProvider serviceProvider; + /// + /// of to running s + /// + readonly Dictionary jobs; + + /// + /// Construct a + /// + /// The value of + public JobManager(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + jobs = new Dictionary(); + } + + /// + /// Gets the for a given if it exists + /// + /// The to get the for + /// The + JobHandler CheckGetJob(Job job) + { + lock (this) + { + if (!jobs.TryGetValue(job.Id, out JobHandler jobHandler)) + throw new InvalidOperationException("Job not running!"); + return jobHandler; + } + } + + /// + /// Runner for s + /// + /// The being run + /// The operation for the + /// The for the operation + /// A representing the running operation + async Task RunJob(Job job, Func operation, CancellationToken cancellationToken) + { + bool cancelled; + try + { + await operation(cancellationToken).ConfigureAwait(false); + cancelled = false; + } + catch (OperationCanceledException) + { + cancelled = true; + } + + using (var scope = serviceProvider.CreateScope()) + { + var databaseContext = scope.ServiceProvider.GetRequiredService(); + job = new Job { Id = job.Id }; + databaseContext.Jobs.Attach(job); + if (cancelled) + job.Cancelled = true; + job.StoppedAt = DateTimeOffset.Now; + await databaseContext.Save(default).ConfigureAwait(false); + } + } + + /// + public async Task RegisterOperation(Job job, Func operation, CancellationToken cancellationToken) + { + using (var scope = serviceProvider.CreateScope()) + { + var databaseContext = scope.ServiceProvider.GetRequiredService(); + job.StartedAt = DateTimeOffset.Now; + databaseContext.Jobs.Add(job); + await databaseContext.Save(cancellationToken).ConfigureAwait(false); + var jobHandler = JobHandler.Create(x => RunJob(job, operation, x)); + lock (this) + jobs.Add(job.Id, jobHandler); + } + } + + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + using (var scope = serviceProvider.CreateScope()) + { + var databaseContext = scope.ServiceProvider.GetRequiredService(); + await databaseContext.Initialize(cancellationToken).ConfigureAwait(false); + + //mark all jobs as cancelled + var enumerator = await databaseContext.Jobs.Where(y => !y.Cancelled && y.StoppedAt == null).Select(y => y.Id).ToAsyncEnumerable().ToList(cancellationToken).ConfigureAwait(false); + foreach(var I in enumerator) + { + var job = new Job { Id = I }; + databaseContext.Jobs.Attach(job); + job.Cancelled = true; + } + await databaseContext.Save(cancellationToken).ConfigureAwait(false); + } + } + + /// + public async Task StopAsync(CancellationToken cancellationToken) + { + var joinTasks = jobs.Select(x => + { + x.Value.Cancel(); + return x.Value.Wait(cancellationToken); + }); + await Task.WhenAll(joinTasks).ConfigureAwait(false); + foreach (var job in jobs) + job.Value.Dispose(); + jobs.Clear(); + } + + /// + public async Task WaitForJob(Job job, CancellationToken cancellationToken) + { + var handler = CheckGetJob(job); + await handler.Wait(cancellationToken).ConfigureAwait(false); + lock (this) + jobs.Remove(job.Id); + handler.Dispose(); + } + + /// + public void CancelJob(Job job) => CheckGetJob(job).Cancel(); + } +} diff --git a/src/Tgstation.Server.Host/Models/IDatabaseContext.cs b/src/Tgstation.Server.Host/Models/IDatabaseContext.cs index 9ed9acf3a4..c9076a354b 100644 --- a/src/Tgstation.Server.Host/Models/IDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Models/IDatabaseContext.cs @@ -15,10 +15,15 @@ namespace Tgstation.Server.Host.Models DbSet Users { get; } /// - /// The s in the + /// The s in the /// DbSet Instances { get; } + /// + /// The s in the + /// + DbSet Jobs { get; } + /// /// Get the in the /// diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 2dfa73f107..8dfd920fb5 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -34,7 +34,6 @@ - diff --git a/src/Tgstation.Server.Host/appsettings.json b/src/Tgstation.Server.Host/appsettings.json index bfcbeffa32..12aa0e9cf9 100644 --- a/src/Tgstation.Server.Host/appsettings.json +++ b/src/Tgstation.Server.Host/appsettings.json @@ -18,7 +18,7 @@ } }, "Database": { - "DatabaseType": "Sqlite", - "ConnectionString": "Fake" + "DatabaseType": "SqlServer", + "ConnectionString": "Data Source=(local);Initial Catalog=TGS;Integrated Security=True" } }