tgstation-server  4.3.2
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.Diagnostics;
8 using System.Linq;
9 using System.Threading;
10 using System.Threading.Tasks;
17 using Tgstation.Server.Host.IO;
21 
22 namespace Tgstation.Server.Host.Components
23 {
26  {
28  public Task Ready => readyTcs.Task;
29 
34 
39 
44 
49 
54 
59 
64 
69 
73  readonly ILogger<InstanceManager> logger;
74 
78  readonly IDictionary<long, IInstance> instances;
79 
83  readonly IDictionary<string, IBridgeHandler> bridgeHandlers;
84 
89 
93  readonly TaskCompletionSource<object> readyTcs;
94 
99 
103  bool disposed;
104 
119  IInstanceFactory instanceFactory,
120  IIOManager ioManager,
121  IDatabaseContextFactory databaseContextFactory,
122  IAssemblyInformationProvider assemblyInformationProvider,
123  IJobManager jobManager,
124  IServerControl serverControl,
125  ISystemIdentityFactory systemIdentityFactory,
126  IAsyncDelayer asyncDelayer,
127  IOptions<GeneralConfiguration> generalConfigurationOptions,
128  ILogger<InstanceManager> logger)
129  {
130  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
131  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
132  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
133  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
134  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
135  this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
136  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
137  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
138  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
139  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
140 
141  serverControl.RegisterForRestart(this);
142 
143  instances = new Dictionary<long, IInstance>();
144  bridgeHandlers = new Dictionary<string, IBridgeHandler>();
145  readyTcs = new TaskCompletionSource<object>();
146  }
147 
149  public void Dispose()
150  {
151  lock (instances)
152  {
153  if (disposed)
154  return;
155  disposed = true;
156  }
157 
158  foreach (var I in instances)
159  I.Value.Dispose();
160  }
161 
163  public IInstance GetInstance(Models.Instance metadata)
164  {
165  if (metadata == null)
166  throw new ArgumentNullException(nameof(metadata));
167  lock (instances)
168  {
169  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
170  throw new InvalidOperationException("Instance not online!");
171  return instance;
172  }
173  }
174 
176  public async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
177  {
178  if (newPath == null)
179  throw new ArgumentNullException(nameof(newPath));
180  if (instance.Online.Value)
181  throw new InvalidOperationException("Cannot move an online instance!");
182  var oldPath = instance.Path;
183  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
184  await databaseContextFactory.UseContext(db =>
185  {
186  var targetInstance = new Models.Instance
187  {
188  Id = instance.Id
189  };
190  db.Instances.Attach(targetInstance);
191  targetInstance.Path = newPath;
192  return db.Save(cancellationToken);
193  }).ConfigureAwait(false);
194  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
195  }
196 
198  public async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
199  {
200  if (metadata == null)
201  throw new ArgumentNullException(nameof(metadata));
202  logger.LogInformation("Offlining instance ID {0}", metadata.Id);
203  IInstance instance;
204  lock (instances)
205  {
206  if (!instances.TryGetValue(metadata.Id, out instance))
207  throw new InvalidOperationException("Instance not online!");
208  instances.Remove(metadata.Id);
209  }
210 
211  try
212  {
213  // we are the one responsible for cancelling his jobs
214  var tasks = new List<Task>();
215  await databaseContextFactory.UseContext(async db =>
216  {
217  var jobs = db
218  .Jobs
219  .AsQueryable()
220  .Where(x => x.Instance.Id == metadata.Id)
221  .Select(x => new Models.Job
222  {
223  Id = x.Id
224  });
225  await jobs.ForEachAsync(job =>
226  {
227  lock (tasks)
228  tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
229  }, cancellationToken).ConfigureAwait(false);
230  }).ConfigureAwait(false);
231 
232  await Task.WhenAll(tasks).ConfigureAwait(false);
233 
234  await instance.StopAsync(cancellationToken).ConfigureAwait(false);
235  }
236  finally
237  {
238  instance.Dispose();
239  }
240  }
241 
243  public async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
244  {
245  if (metadata == null)
246  throw new ArgumentNullException(nameof(metadata));
247  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
248  var instance = instanceFactory.CreateInstance(this, metadata);
249  try
250  {
251  lock (instances)
252  {
253  if (instances.ContainsKey(metadata.Id))
254  throw new InvalidOperationException("Instance already online!");
255  instances.Add(metadata.Id, instance);
256  }
257  }
258  catch
259  {
260  instance.Dispose();
261  throw;
262  }
263 
264  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
265  }
266 
268  #pragma warning disable CA1506 // TODO: Decomplexify
269  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
270  {
271  logger.LogInformation(assemblyInformationProvider.VersionString);
272 
273  try
274  {
275  CheckSystemCompatibility();
276  var factoryStartup = instanceFactory.StartAsync(cancellationToken);
277  await databaseContext.Initialize(cancellationToken).ConfigureAwait(false);
278  await jobManager.StartAsync(cancellationToken).ConfigureAwait(false);
279  var dbInstances = databaseContext
280  .Instances
281  .AsQueryable()
282  .Where(x => x.Online.Value)
283  .Include(x => x.RepositorySettings)
284  .Include(x => x.ChatSettings)
285  .ThenInclude(x => x.Channels)
286  .Include(x => x.DreamDaemonSettings)
287  .ToAsyncEnumerable();
288  var tasks = new List<Task>();
289  await factoryStartup.ConfigureAwait(false);
290  await dbInstances.ForEachAsync(metadata => tasks.Add(metadata.Online.Value ? OnlineInstance(metadata, cancellationToken) : Task.CompletedTask), cancellationToken).ConfigureAwait(false);
291  await Task.WhenAll(tasks).ConfigureAwait(false);
292  logger.LogInformation("Server ready!");
293  readyTcs.SetResult(null);
294  }
295  catch (OperationCanceledException)
296  {
297  logger.LogInformation("Cancelled instance manager initialization!");
298  }
299  catch (Exception e)
300  {
301  logger.LogCritical("Instance manager startup error! Exception: {0}", e);
302  try
303  {
304  await serverControl.Die(e).ConfigureAwait(false);
305  return;
306  }
307  catch (Exception e2)
308  {
309  logger.LogCritical("Failed to kill server! Exception: {0}", e2);
310  }
311 
312  throw;
313  }
314  });
315  #pragma warning restore CA1506 // TODO: Decomplexify
316 
318  public async Task StopAsync(CancellationToken cancellationToken)
319  {
320  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
321  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
322  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
323 
324  // downgrade the db if necessary
325  if (downgradeVersion != null)
326  await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
327  }
328 
330  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
331  {
332  downgradeVersion = updateVersion != null && updateVersion < assemblyInformationProvider.Version ? updateVersion : null;
333  return Task.CompletedTask;
334  }
335 
340  {
341  if (generalConfiguration.UseExperimentalWatchdog && !Debugger.IsAttached)
342  throw new InvalidOperationException(
343  "The experimental watchdog is currently non-functional! Please disable the '{nameof(generalConfiguration.UseExperimentalWatchdog)}' option in your configuration!");
344 
345  using var systemIdentity = systemIdentityFactory.GetCurrent();
346  if (!systemIdentity.CanCreateSymlinks)
347  throw new InvalidOperationException("The user running tgstation-server cannot create symlinks! Please try running as an administrative user!");
348  }
349 
351  public async Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
352  {
353  if (parameters == null)
354  throw new ArgumentNullException(nameof(parameters));
355 
356  IBridgeHandler bridgeHandler = null;
357  for (var i = 0; bridgeHandler == null && i < 30; ++i)
358  {
359  // There's a miniscule time period where we could potentially receive a bridge request and not have the registration ready when we launch DD
360  // This is a stopgap
361  Task delayTask = Task.CompletedTask;
362  lock (bridgeHandlers)
363  if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler))
364  delayTask = asyncDelayer.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
365 
366  await delayTask.ConfigureAwait(false);
367  }
368 
369  if (bridgeHandler == null)
370  lock (bridgeHandlers)
371  if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler))
372  {
373  logger.LogWarning("Recieved invalid bridge request with accees identifier: {0}", parameters.AccessIdentifier);
374  return null;
375  }
376 
377  return await bridgeHandler.ProcessBridgeRequest(parameters, cancellationToken).ConfigureAwait(false);
378  }
379 
382  {
383  if (bridgeHandler == null)
384  throw new ArgumentNullException(nameof(bridgeHandler));
385 
386  var accessIdentifier = bridgeHandler.DMApiParameters.AccessIdentifier;
387  lock (bridgeHandlers)
388  {
389  bridgeHandlers.Add(accessIdentifier, bridgeHandler);
390  logger.LogTrace("Registered bridge handler: {0}", accessIdentifier);
391  }
392 
393  return new BridgeRegistration(() =>
394  {
395  lock (bridgeHandlers)
396  {
397  bridgeHandlers.Remove(accessIdentifier);
398  logger.LogTrace("Unregistered bridge handler: {0}", accessIdentifier);
399  }
400  });
401  }
402  }
403 }
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 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
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.
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.
InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IAssemblyInformationProvider assemblyInformationProvider, IJobManager jobManager, IServerControl serverControl, ISystemIdentityFactory systemIdentityFactory, IAsyncDelayer asyncDelayer, IOptions< GeneralConfiguration > generalConfigurationOptions, ILogger< InstanceManager > logger)
Construct an InstanceManager
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...