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());