diff --git a/src/Tgstation.Server.Host/Components/IInstance.cs b/src/Tgstation.Server.Host/Components/IInstance.cs new file mode 100644 index 0000000000..bdd3f392e5 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/IInstance.cs @@ -0,0 +1,8 @@ +using Microsoft.Extensions.Hosting; + +namespace Tgstation.Server.Host.Components +{ + interface IInstance : IHostedService + { + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/IInstanceFactory.cs b/src/Tgstation.Server.Host/Components/IInstanceFactory.cs new file mode 100644 index 0000000000..c76afabf4f --- /dev/null +++ b/src/Tgstation.Server.Host/Components/IInstanceFactory.cs @@ -0,0 +1,9 @@ +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Components +{ + interface IInstanceFactory + { + IInstance CreateInstance(Instance metadata); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/IInstanceManager.cs b/src/Tgstation.Server.Host/Components/IInstanceManager.cs new file mode 100644 index 0000000000..d63384a3fd --- /dev/null +++ b/src/Tgstation.Server.Host/Components/IInstanceManager.cs @@ -0,0 +1,43 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components +{ + /// + /// For managing s + /// + interface IInstanceManager + { + /// + /// Get the associated with given + /// + /// The of the desired + /// The associated with the given + IInstance GetInstance(Models.Instance metadata); + + /// + /// Online an + /// + /// The of the desired + /// The for the operation + /// A representing the running operation + Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken); + + /// + /// Offline an + /// + /// The of the desired + /// The for the operation + /// A representing the running operation + Task OfflineInstance(Models.Instance metadata, CancellationToken cancellationToken); + + /// + /// Move an + /// + /// The of the desired + /// The new path of the . will have this set on if the operation completes successfully + /// The for the operation + /// A representing the running operation + Task MoveInstance(Models.Instance metadata, string newPath, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs new file mode 100644 index 0000000000..73477bbd76 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -0,0 +1,132 @@ +using Microsoft.EntityFrameworkCore; +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.Core; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Components +{ + /// + sealed class InstanceManager : IInstanceManager, IHostedService + { + /// + /// The for the + /// + readonly IInstanceFactory instanceFactory; + /// + /// The for the + /// + readonly IServiceProvider serviceProvider; + /// + /// The for the + /// + readonly IIOManager ioManager; + /// + /// Map of s to respective s + /// + readonly Dictionary instances; + + /// + /// Construct an + /// + /// The value of + /// The value of + /// The value of + public InstanceManager(IInstanceFactory instanceFactory, IServiceProvider serviceProvider, IIOManager ioManager) + { + this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory)); + this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + instances = new Dictionary(); + } + + /// + public IInstance GetInstance(Instance metadata) + { + lock (this) + { + if (!instances.TryGetValue(metadata.Id, out IInstance instance)) + throw new InvalidOperationException("Instance not online!"); + return instance; + } + } + + /// + public async Task MoveInstance(Instance instance, string newPath, CancellationToken cancellationToken) + { + if (newPath == null) + throw new ArgumentNullException(nameof(newPath)); + if (instance.Online) + await OfflineInstance(instance, cancellationToken).ConfigureAwait(false); + Task instanceOnlineTask = null; + try + { + var oldPath = instance.Path; + await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false); + instance.Path = ioManager.ResolvePath(newPath); + instanceOnlineTask = OnlineInstance(instance, default); + await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false); + } + finally + { + if (instance.Online) + if (instanceOnlineTask == null) + await OnlineInstance(instance, default).ConfigureAwait(false); + else + await instanceOnlineTask.ConfigureAwait(false); + } + } + + /// + public async Task OfflineInstance(Instance metadata, CancellationToken cancellationToken) + { + IInstance instance; + lock (this) + { + if (!instances.TryGetValue(metadata.Id, out instance)) + throw new InvalidOperationException("Instance not online!"); + instances.Remove(metadata.Id); + } + await instance.StopAsync(cancellationToken).ConfigureAwait(false); + } + + /// + public async Task OnlineInstance(Instance metadata, CancellationToken cancellationToken) + { + var instance = instanceFactory.CreateInstance(metadata); + lock (this) + { + if (instances.ContainsKey(metadata.Id)) + throw new InvalidOperationException("Instance already online!"); + instances.Add(metadata.Id, instance); + } + await instance.StartAsync(cancellationToken).ConfigureAwait(false); + } + + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + using(var scope = serviceProvider.CreateScope()) + { + var databaseContext = scope.ServiceProvider.GetRequiredService(); + await databaseContext.Initialize(cancellationToken).ConfigureAwait(false); + var dbInstances = databaseContext.Instances.Where(x => x.Online).Include(x => x.RepositorySettings).Include(x => x.ChatSettings).Include(x => x.DreamDaemonSettings).ToAsyncEnumerable(); + var tasks = new List(); + await dbInstances.ForEachAsync(metadata => tasks.Add(OnlineInstance(metadata, cancellationToken)), cancellationToken).ConfigureAwait(false); + await Task.WhenAll(tasks).ConfigureAwait(false); + } + } + + /// + public async Task StopAsync(CancellationToken cancellationToken) + { + await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false); + instances.Clear(); + } + } +} diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 87626da1fc..ca8104ec21 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -11,6 +11,7 @@ using System; using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Reflection; +using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Models; @@ -132,6 +133,10 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(x => x.GetRequiredService()); + services.AddSingleton(x => x.GetRequiredService()); + services.AddSingleton(); services.AddSingleton(x => x.GetRequiredService()); services.AddSingleton(x => x.GetRequiredService());