tgstation-server  4.4.0
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 Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 using System.Threading;
9 using System.Threading.Tasks;
16 using Tgstation.Server.Host.IO;
20 
21 namespace Tgstation.Server.Host.Components
22 {
25  {
27  public Task Ready => readyTcs.Task;
28 
33 
38 
43 
48 
53 
58 
63 
68 
73 
77  readonly ILogger<InstanceManager> logger;
78 
82  readonly IDictionary<long, IInstance> instances;
83 
87  readonly IDictionary<string, IBridgeHandler> bridgeHandlers;
88 
93 
97  readonly TaskCompletionSource<object> readyTcs;
98 
103 
107  bool disposed;
108 
124  IInstanceFactory instanceFactory,
125  IIOManager ioManager,
126  IDatabaseContextFactory databaseContextFactory,
127  IAssemblyInformationProvider assemblyInformationProvider,
128  IJobManager jobManager,
129  IServerControl serverControl,
130  ISystemIdentityFactory systemIdentityFactory,
131  IAsyncDelayer asyncDelayer,
132  IDatabaseSeeder databaseSeeder,
133  IOptions<GeneralConfiguration> generalConfigurationOptions,
134  ILogger<InstanceManager> logger)
135  {
136  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
137  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
138  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
139  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
140  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
141  this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
142  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
143  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
144  this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
145  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
146  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
147 
148  serverControl.RegisterForRestart(this);
149 
150  instances = new Dictionary<long, IInstance>();
151  bridgeHandlers = new Dictionary<string, IBridgeHandler>();
152  readyTcs = new TaskCompletionSource<object>();
153  }
154 
156  public void Dispose()
157  {
158  lock (instances)
159  {
160  if (disposed)
161  return;
162  disposed = true;
163  }
164 
165  foreach (var I in instances)
166  I.Value.Dispose();
167  }
168 
170  public IInstance GetInstance(Models.Instance metadata)
171  {
172  if (metadata == null)
173  throw new ArgumentNullException(nameof(metadata));
174  lock (instances)
175  {
176  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
177  throw new InvalidOperationException("Instance not online!");
178  return instance;
179  }
180  }
181 
183  public async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
184  {
185  if (newPath == null)
186  throw new ArgumentNullException(nameof(newPath));
187  if (instance.Online.Value)
188  throw new InvalidOperationException("Cannot move an online instance!");
189  var oldPath = instance.Path;
190  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
191  await databaseContextFactory.UseContext(db =>
192  {
193  var targetInstance = new Models.Instance
194  {
195  Id = instance.Id
196  };
197  db.Instances.Attach(targetInstance);
198  targetInstance.Path = newPath;
199  return db.Save(cancellationToken);
200  }).ConfigureAwait(false);
201  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
202  }
203 
205  public async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
206  {
207  if (metadata == null)
208  throw new ArgumentNullException(nameof(metadata));
209  logger.LogInformation("Offlining instance ID {0}", metadata.Id);
210  IInstance instance;
211  lock (instances)
212  {
213  if (!instances.TryGetValue(metadata.Id, out instance))
214  throw new InvalidOperationException("Instance not online!");
215  instances.Remove(metadata.Id);
216  }
217 
218  try
219  {
220  // we are the one responsible for cancelling his jobs
221  var tasks = new List<Task>();
222  await databaseContextFactory.UseContext(async db =>
223  {
224  var jobs = db
225  .Jobs
226  .AsQueryable()
227  .Where(x => x.Instance.Id == metadata.Id)
228  .Select(x => new Models.Job
229  {
230  Id = x.Id
231  });
232  await jobs.ForEachAsync(job =>
233  {
234  lock (tasks)
235  tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
236  }, cancellationToken).ConfigureAwait(false);
237  }).ConfigureAwait(false);
238 
239  await Task.WhenAll(tasks).ConfigureAwait(false);
240 
241  await instance.StopAsync(cancellationToken).ConfigureAwait(false);
242  }
243  finally
244  {
245  instance.Dispose();
246  }
247  }
248 
250  public async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
251  {
252  if (metadata == null)
253  throw new ArgumentNullException(nameof(metadata));
254  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
255  var instance = instanceFactory.CreateInstance(this, metadata);
256  try
257  {
258  lock (instances)
259  {
260  if (instances.ContainsKey(metadata.Id))
261  throw new InvalidOperationException("Instance already online!");
262  instances.Add(metadata.Id, instance);
263  }
264  }
265  catch
266  {
267  instance.Dispose();
268  throw;
269  }
270 
271  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
272  }
273 
275  #pragma warning disable CA1506 // TODO: Decomplexify
276  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
277  {
278  logger.LogInformation(assemblyInformationProvider.VersionString);
279 
280  try
281  {
282  generalConfiguration.CheckCompatibility(logger);
283 
284  CheckSystemCompatibility();
285  var factoryStartup = instanceFactory.StartAsync(cancellationToken);
286  await databaseSeeder.Initialize(databaseContext, cancellationToken).ConfigureAwait(false);
287  await jobManager.StartAsync(cancellationToken).ConfigureAwait(false);
288  var dbInstances = databaseContext
289  .Instances
290  .AsQueryable()
291  .Where(x => x.Online.Value)
292  .Include(x => x.RepositorySettings)
293  .Include(x => x.ChatSettings)
294  .ThenInclude(x => x.Channels)
295  .Include(x => x.DreamDaemonSettings)
296  .ToAsyncEnumerable();
297  var tasks = new List<Task>();
298  await factoryStartup.ConfigureAwait(false);
299  await dbInstances.ForEachAsync(metadata => tasks.Add(metadata.Online.Value ? OnlineInstance(metadata, cancellationToken) : Task.CompletedTask), cancellationToken).ConfigureAwait(false);
300  await Task.WhenAll(tasks).ConfigureAwait(false);
301  logger.LogInformation("Server ready!");
302  readyTcs.SetResult(null);
303  }
304  catch (OperationCanceledException)
305  {
306  logger.LogInformation("Cancelled instance manager initialization!");
307  }
308  catch (Exception e)
309  {
310  logger.LogCritical("Instance manager startup error! Exception: {0}", e);
311  try
312  {
313  await serverControl.Die(e).ConfigureAwait(false);
314  return;
315  }
316  catch (Exception e2)
317  {
318  logger.LogCritical("Failed to kill server! Exception: {0}", e2);
319  }
320 
321  throw;
322  }
323  });
324  #pragma warning restore CA1506 // TODO: Decomplexify
325 
327  public async Task StopAsync(CancellationToken cancellationToken)
328  {
329  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
330  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
331  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
332 
333  // downgrade the db if necessary
334  if (downgradeVersion != null)
335  await databaseContextFactory.UseContext(db => databaseSeeder.Downgrade(db, downgradeVersion, cancellationToken)).ConfigureAwait(false);
336  }
337 
339  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
340  {
341  downgradeVersion = updateVersion != null && updateVersion < assemblyInformationProvider.Version ? updateVersion : null;
342  return Task.CompletedTask;
343  }
344 
349  {
350  using var systemIdentity = systemIdentityFactory.GetCurrent();
351  if (!systemIdentity.CanCreateSymlinks)
352  throw new InvalidOperationException("The user running tgstation-server cannot create symlinks! Please try running as an administrative user!");
353  }
354 
356  public async Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
357  {
358  if (parameters == null)
359  throw new ArgumentNullException(nameof(parameters));
360 
361  IBridgeHandler bridgeHandler = null;
362  for (var i = 0; bridgeHandler == null && i < 30; ++i)
363  {
364  // There's a miniscule time period where we could potentially receive a bridge request and not have the registration ready when we launch DD
365  // This is a stopgap
366  Task delayTask = Task.CompletedTask;
367  lock (bridgeHandlers)
368  if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler))
369  delayTask = asyncDelayer.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
370 
371  await delayTask.ConfigureAwait(false);
372  }
373 
374  if (bridgeHandler == null)
375  lock (bridgeHandlers)
376  if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler))
377  {
378  logger.LogWarning("Recieved invalid bridge request with accees identifier: {0}", parameters.AccessIdentifier);
379  return null;
380  }
381 
382  return await bridgeHandler.ProcessBridgeRequest(parameters, cancellationToken).ConfigureAwait(false);
383  }
384 
387  {
388  if (bridgeHandler == null)
389  throw new ArgumentNullException(nameof(bridgeHandler));
390 
391  var accessIdentifier = bridgeHandler.DMApiParameters.AccessIdentifier;
392  lock (bridgeHandlers)
393  {
394  bridgeHandlers.Add(accessIdentifier, bridgeHandler);
395  logger.LogTrace("Registered bridge handler: {0}", accessIdentifier);
396  }
397 
398  return new BridgeRegistration(() =>
399  {
400  lock (bridgeHandlers)
401  {
402  bridgeHandlers.Remove(accessIdentifier);
403  logger.LogTrace("Unregistered bridge handler: {0}", accessIdentifier);
404  }
405  });
406  }
407  }
408 }
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the InstanceManager
readonly IServerControl serverControl
The IServerControl for the InstanceManager
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
readonly IDatabaseSeeder databaseSeeder
The IDatabaseSeeder for the InstanceManager
readonly IDictionary< string, IBridgeHandler > bridgeHandlers
Map of DMApiParameters.AccessIdentifiers to their respective IBridgeHandlers.
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
Online an IInstance
For initially setting up a database.
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the InstanceManager
bool disposed
If the InstanceManager has been Disposed
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the InstanceManager
async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler)
Register a given bridgeHandler .
async Task< BridgeResponse > ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters .
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the InstanceManager.
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager
readonly IJobManager jobManager
The IJobManager for the InstanceManager
readonly IDictionary< long, IInstance > instances
Map of instance EntityId.Ids to respective IInstances. Also used as a object.
InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IAssemblyInformationProvider assemblyInformationProvider, IJobManager jobManager, IServerControl serverControl, ISystemIdentityFactory systemIdentityFactory, IAsyncDelayer asyncDelayer, IDatabaseSeeder databaseSeeder, IOptions< GeneralConfiguration > generalConfigurationOptions, ILogger< InstanceManager > logger)
Construct an InstanceManager
readonly TaskCompletionSource< object > readyTcs
The TaskCompletionSource<TResult> for Ready.
Version downgradeVersion
Used in StopAsync(CancellationToken) to determine if database downgrades must be made ...
For interacting with the instance services
Definition: IInstance.cs:16
async Task StopAsync(CancellationToken cancellationToken)
string AccessIdentifier
Used to identify and authenticate the DreamDaemon instance
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
readonly IInstanceFactory instanceFactory
The IInstanceFactory for the InstanceManager
Task< BridgeResponse > ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters .
Manages the runtime of Jobs
Definition: IJobManager.cs:13
readonly IIOManager ioManager
The IIOManager for the InstanceManager
Interface for using filesystems
Definition: IIOManager.cs:11
void CheckSystemCompatibility()
Check we have a valid system identity.
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
Represents a registration of an interop session.
DMApiParameters DMApiParameters
The DMApiParameters for the IBridgeHandler.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...