tgstation-server
The /tg/station 13 server suite
InstanceManager.cs
Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
10 using Tgstation.Server.Host.IO;
11 
12 namespace Tgstation.Server.Host.Components
13 {
16  {
21 
26 
31 
36 
41 
46 
50  readonly ILogger<InstanceManager> logger;
51 
55  readonly Dictionary<long, IInstance> instances;
56 
61 
65  bool disposed;
66 
77  public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger<InstanceManager> logger)
78  {
79  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
80  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
81  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
82  this.application = application ?? throw new ArgumentNullException(nameof(application));
83  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
84  this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
85  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
86 
87  serverControl.RegisterForRestart(this);
88 
89  instances = new Dictionary<long, IInstance>();
90  }
91 
93  public void Dispose()
94  {
95  lock (this)
96  {
97  if (disposed)
98  return;
99  disposed = true;
100  }
101  foreach (var I in instances)
102  I.Value.Dispose();
103  }
104 
106  public IInstance GetInstance(Models.Instance metadata)
107  {
108  if (metadata == null)
109  throw new ArgumentNullException(nameof(metadata));
110  lock (this)
111  {
112  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
113  throw new InvalidOperationException("Instance not online!");
114  return instance;
115  }
116  }
117 
119  public async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
120  {
121  if (newPath == null)
122  throw new ArgumentNullException(nameof(newPath));
123  if (instance.Online.Value)
124  throw new InvalidOperationException("Cannot move an online instance!");
125  var oldPath = instance.Path;
126  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
127  await databaseContextFactory.UseContext(db =>
128  {
129  var targetInstance = new Models.Instance
130  {
131  Id = instance.Id
132  };
133  db.Instances.Attach(targetInstance);
134  targetInstance.Path = newPath;
135  return db.Save(cancellationToken);
136  }).ConfigureAwait(false);
137  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
138  }
139 
141  public async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
142  {
143  if (metadata == null)
144  throw new ArgumentNullException(nameof(metadata));
145  logger.LogInformation("Offlining instance ID {0}", metadata.Id);
146  IInstance instance;
147  lock (this)
148  {
149  if (!instances.TryGetValue(metadata.Id, out instance))
150  throw new InvalidOperationException("Instance not online!");
151  instances.Remove(metadata.Id);
152  }
153  try
154  {
155  //we are the one responsible for cancelling his jobs
156  var tasks = new List<Task>();
157  await databaseContextFactory.UseContext(async db =>
158  {
159  var jobs = db.Jobs.Where(x => x.Instance.Id == metadata.Id).Select(x => new Models.Job
160  {
161  Id = x.Id
162  }).ToAsyncEnumerable();
163  await jobs.ForEachAsync(job =>
164  {
165  lock (tasks)
166  tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
167  }, cancellationToken).ConfigureAwait(false);
168  }).ConfigureAwait(false);
169 
170  await Task.WhenAll(tasks).ConfigureAwait(false);
171 
172  await instance.StopAsync(cancellationToken).ConfigureAwait(false);
173  }
174  finally
175  {
176  instance.Dispose();
177  }
178  }
179 
181  public async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
182  {
183  if (metadata == null)
184  throw new ArgumentNullException(nameof(metadata));
185  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
186  var instance = instanceFactory.CreateInstance(metadata);
187  try
188  {
189  lock (this)
190  {
191  if (instances.ContainsKey(metadata.Id))
192  throw new InvalidOperationException("Instance already online!");
193  instances.Add(metadata.Id, instance);
194  }
195  }
196  catch
197  {
198  instance.Dispose();
199  throw;
200  }
201  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
202  }
203 
205  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
206  {
207  try
208  {
209  var factoryStartup = instanceFactory.StartAsync(cancellationToken);
210  await databaseContext.Initialize(cancellationToken).ConfigureAwait(false);
211  await jobManager.StartAsync(cancellationToken).ConfigureAwait(false);
212  var dbInstances = databaseContext.Instances.Where(x => x.Online.Value)
213  .Include(x => x.RepositorySettings)
214  .Include(x => x.ChatSettings)
215  .ThenInclude(x => x.Channels)
216  .Include(x => x.DreamDaemonSettings)
217  .ToAsyncEnumerable();
218  var tasks = new List<Task>();
219  await factoryStartup.ConfigureAwait(false);
220  await dbInstances.ForEachAsync(metadata => tasks.Add(metadata.Online.Value ? OnlineInstance(metadata, cancellationToken) : Task.CompletedTask), cancellationToken).ConfigureAwait(false);
221  await Task.WhenAll(tasks).ConfigureAwait(false);
222  logger.LogInformation("Server ready!");
223  application.Ready(null);
224  }
225  catch (OperationCanceledException)
226  {
227  logger.LogInformation("Cancelled instance manager initialization!");
228  }
229  catch (Exception e)
230  {
231  logger.LogCritical("Instance manager startup error! Exception: {0}", e);
232  application.Ready(e);
233  try
234  {
235  await serverControl.Die(e).ConfigureAwait(false);
236  }
237  catch (Exception e2)
238  {
239  logger.LogCritical("Failed to kill server! Exception: {0}", e2);
240  }
241  }
242  });
243 
245  public async Task StopAsync(CancellationToken cancellationToken)
246  {
247  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
248  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
249  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
250 
251  //downgrade the db if necessary
252  if (downgradeVersion != null)
253  await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
254  }
255 
257  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
258  {
259  downgradeVersion = updateVersion != null && updateVersion < application.Version ? updateVersion : null;
260  return Task.CompletedTask;
261  }
262  }
263 }
Manages the runtime of Jobs
Definition: IJobManager.cs:12
readonly IServerControl serverControl
The IServerControl for the InstanceManager
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
Online an IInstance
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
bool disposed
If the InstanceManager has been Disposed
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager
readonly IJobManager jobManager
The IJobManager for the InstanceManager
Version downgradeVersion
Used in StopAsync(CancellationToken) to determine if database downgrades must be made ...
For interacting with the instance services
Definition: IInstance.cs:17
async Task StopAsync(CancellationToken cancellationToken)
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
readonly IInstanceFactory instanceFactory
The IInstanceFactory for the InstanceManager
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
readonly IApplication application
The IApplication for the InstanceManager
readonly IIOManager ioManager
The IIOManager for the InstanceManager
Interface for using filesystems
Definition: IIOManager.cs:11
async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
Move an IInstance
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger< InstanceManager > logger)
Construct an InstanceManager
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...