tgstation-server 5.12.7
The /tg/station 13 server suite
Loading...
Searching...
No Matches
InstanceManager.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Threading;
5using System.Threading.Tasks;
6
7using Microsoft.EntityFrameworkCore;
8using Microsoft.Extensions.Hosting;
9using Microsoft.Extensions.Logging;
10using Microsoft.Extensions.Options;
11
26
28{
30 sealed class InstanceManager :
36 {
38 public Task Ready => readyTcs.Task;
39
44
49
54
59
64
69
74
79
84
89
93 readonly ILogger<InstanceManager> logger;
94
98 readonly Dictionary<long, ReferenceCountingContainer<IInstance, InstanceWrapper>> instances;
99
103 readonly Dictionary<string, IBridgeHandler> bridgeHandlers;
104
108 readonly SemaphoreSlim instanceStateChangeSemaphore;
109
114
119
123 readonly TaskCompletionSource readyTcs;
124
128 readonly CancellationTokenSource startupCancellationTokenSource;
129
133 readonly CancellationTokenSource shutdownCancellationTokenSource;
134
139
144
172 IOptions<GeneralConfiguration> generalConfigurationOptions,
173 IOptions<SwarmConfiguration> swarmConfigurationOptions,
174 ILogger<InstanceManager> logger)
175 {
176 this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
177 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
178 this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
179 this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
180 this.jobService = jobService ?? throw new ArgumentNullException(nameof(jobService));
181 this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
182 this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
183 this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
184 this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider));
185 this.swarmServiceController = swarmServiceController ?? throw new ArgumentNullException(nameof(swarmServiceController));
186 generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
187 swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
188 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
189
190 instances = new Dictionary<long, ReferenceCountingContainer<IInstance, InstanceWrapper>>();
191 bridgeHandlers = new Dictionary<string, IBridgeHandler>();
192 readyTcs = new TaskCompletionSource();
193 instanceStateChangeSemaphore = new SemaphoreSlim(1);
194 startupCancellationTokenSource = new CancellationTokenSource();
195 shutdownCancellationTokenSource = new CancellationTokenSource();
196 }
197
199 public async ValueTask DisposeAsync()
200 {
201 lock (instances)
202 {
203 if (disposed)
204 return;
205 disposed = true;
206 }
207
208 foreach (var instanceKvp in instances)
209 await instanceKvp.Value.Instance.DisposeAsync();
210
214
215 logger.LogInformation("Server shutdown");
216 }
217
219 public IInstanceReference GetInstanceReference(Api.Models.Instance metadata)
220 {
221 ArgumentNullException.ThrowIfNull(metadata);
222
223 lock (instances)
224 {
225 if (!instances.TryGetValue(metadata.Id.Value, out var instance))
226 return null;
227
228 return instance.AddReference();
229 }
230 }
231
233 public async Task MoveInstance(Models.Instance instance, string oldPath, CancellationToken cancellationToken)
234 {
235 ArgumentNullException.ThrowIfNull(oldPath);
236
237 using var lockContext = await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken);
238 using var instanceReferenceCheck = GetInstanceReference(instance);
239 if (instanceReferenceCheck != null)
240 throw new InvalidOperationException("Cannot move an online instance!");
241 var newPath = instance.Path;
242 try
243 {
244 await ioManager.MoveDirectory(oldPath, newPath, cancellationToken);
245
246 // Delete the Game directory to clear out broken symlinks
247 var instanceGameIOManager = instanceFactory.CreateGameIOManager(instance);
248 await instanceGameIOManager.DeleteDirectory(DefaultIOManager.CurrentDirectory, cancellationToken);
249 }
250 catch (Exception ex)
251 {
252 logger.LogError(
253 ex,
254 "Error moving instance {instanceId}!",
255 instance.Id);
256 try
257 {
258 logger.LogDebug("Reverting instance {instanceId}'s path to {oldPath} in the DB...", instance.Id, oldPath);
259
260 // DCT: Operation must always run
262 {
263 var targetInstance = new Models.Instance
264 {
265 Id = instance.Id,
266 };
267 db.Instances.Attach(targetInstance);
268 targetInstance.Path = oldPath;
269 return db.Save(CancellationToken.None);
270 });
271 }
272 catch (Exception innerEx)
273 {
274 logger.LogCritical(
275 innerEx,
276 "Error reverting database after failing to move instance {instanceId}! Attempting to detach...",
277 instance.Id);
278
279 try
280 {
281 // DCT: Operation must always run
284 Array.Empty<byte>(),
285 CancellationToken.None);
286 }
287 catch (Exception tripleEx)
288 {
289 logger.LogCritical(
290 tripleEx,
291 "Okay, what gamma radiation are you under? Failed to write instance attach file!");
292
293 throw new AggregateException(tripleEx, innerEx, ex);
294 }
295
296 throw new AggregateException(ex, innerEx);
297 }
298
299 throw;
300 }
301 }
302
304 public async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
305 {
306 ArgumentNullException.ThrowIfNull(metadata);
307
308 using (await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken))
309 {
311 lock (instances)
312 {
313 if (!instances.TryGetValue(metadata.Id.Value, out container))
314 {
315 logger.LogDebug("Not offlining removed instance {instanceId}", metadata.Id);
316 return;
317 }
318
319 instances.Remove(metadata.Id.Value);
320 }
321
322 logger.LogInformation("Offlining instance ID {instanceId}", metadata.Id);
323
324 try
325 {
326 await container.OnZeroReferences.WithToken(cancellationToken);
327
328 // we are the one responsible for cancelling his jobs
329 var tasks = new List<Task>();
331 async db =>
332 {
333 var jobs = await db
334 .Jobs
335 .AsQueryable()
336 .Where(x => x.Instance.Id == metadata.Id && !x.StoppedAt.HasValue)
337 .Select(x => new Models.Job
338 {
339 Id = x.Id,
340 })
341 .ToListAsync(cancellationToken);
342 foreach (var job in jobs)
343 tasks.Add(jobService.CancelJob(job, user, true, cancellationToken));
344 });
345
346 await Task.WhenAll(tasks);
347 }
348 catch
349 {
350 // not too late to change your mind
351 lock (instances)
352 instances.Add(metadata.Id.Value, container);
353
354 throw;
355 }
356
357 try
358 {
359 // at this point we can't really stop offlining the instance just because the request was cancelled
360 await container.Instance.StopAsync(shutdownCancellationTokenSource.Token);
361 }
362 finally
363 {
364 await container.Instance.DisposeAsync();
365 }
366 }
367 }
368
370 public async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
371 {
372 ArgumentNullException.ThrowIfNull(metadata);
373
374 using var lockContext = await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken);
375 lock (instances)
376 if (instances.ContainsKey(metadata.Id.Value))
377 {
378 logger.LogDebug("Aborting instance creation due to it seemingly already being online");
379 return;
380 }
381
382 logger.LogInformation("Onlining instance ID {instanceId} ({instanceName}) at {instancePath}", metadata.Id, metadata.Name, metadata.Path);
383 var instance = await instanceFactory.CreateInstance(this, metadata);
384 try
385 {
386 await instance.StartAsync(cancellationToken);
387
388 try
389 {
390 lock (instances)
391 instances.Add(
392 metadata.Id.Value,
394 }
395 catch (Exception ex)
396 {
397 logger.LogError("Unable to commit onlined instance {instanceId} into service, offlining!", metadata.Id);
398 try
399 {
400 // DCT: Must always run
401 await instance.StopAsync(CancellationToken.None);
402 }
403 catch (Exception innerEx)
404 {
405 throw new AggregateException(innerEx, ex);
406 }
407
408 throw;
409 }
410 }
411 catch
412 {
413 await instance.DisposeAsync();
414 throw;
415 }
416 }
417
419 public Task StartAsync(CancellationToken cancellationToken)
420 {
421 cancellationToken.Register(startupCancellationTokenSource.Cancel);
423 return Task.CompletedTask;
424 }
425
427 public async Task StopAsync(CancellationToken cancellationToken)
428 {
429 using (cancellationToken.Register(shutdownCancellationTokenSource.Cancel))
430 try
431 {
432 logger.LogDebug("Stopping instance manager...");
433
434 if (!startupTask.IsCompleted)
435 {
436 logger.LogTrace("Interrupting startup task...");
438 await startupTask;
439 }
440
441 var instanceFactoryStopTask = instanceFactory.StopAsync(cancellationToken);
442 await jobService.StopAsync(cancellationToken);
443
444 async Task OfflineInstanceImmediate(IInstance instance, CancellationToken cancellationToken)
445 {
446 try
447 {
448 await instance.StopAsync(cancellationToken);
449 }
450 catch (Exception ex)
451 {
452 logger.LogError(ex, "Instance shutdown exception!");
453 }
454 }
455
456 await Task.WhenAll(instances.Select(x => OfflineInstanceImmediate(x.Value.Instance, cancellationToken)));
457 await instanceFactoryStopTask;
458
459 await swarmServiceController.Shutdown(cancellationToken);
460 }
461 catch (Exception ex)
462 {
463 logger.LogCritical(ex, "Instance manager stop exception!");
464 }
465 }
466
468 public async Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
469 {
470 ArgumentNullException.ThrowIfNull(parameters);
471
472 IBridgeHandler bridgeHandler = null;
473 for (var i = 0; bridgeHandler == null && i < 30; ++i)
474 {
475 // There's a miniscule time period where we could potentially receive a bridge request and not have the registration ready when we launch DD
476 // This is a stopgap
477 Task delayTask = Task.CompletedTask;
478 lock (bridgeHandlers)
479 if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler))
480 delayTask = asyncDelayer.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
481
482 await delayTask;
483 }
484
485 if (bridgeHandler == null)
486 lock (bridgeHandlers)
487 if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler))
488 {
489 logger.LogWarning("Recieved invalid bridge request with access identifier: {accessIdentifier}", parameters.AccessIdentifier);
490 return null;
491 }
492
493 return await bridgeHandler.ProcessBridgeRequest(parameters, cancellationToken);
494 }
495
498 {
499 ArgumentNullException.ThrowIfNull(bridgeHandler);
500
501 var accessIdentifier = bridgeHandler.DMApiParameters.AccessIdentifier;
502 lock (bridgeHandlers)
503 {
504 bridgeHandlers.Add(accessIdentifier, bridgeHandler);
505 logger.LogTrace("Registered bridge handler: {accessIdentifier}", accessIdentifier);
506 }
507
508 return new BridgeRegistration(() =>
509 {
510 lock (bridgeHandlers)
511 {
512 bridgeHandlers.Remove(accessIdentifier);
513 logger.LogTrace("Unregistered bridge handler: {accessIdentifier}", accessIdentifier);
514 }
515 });
516 }
517
519 public IInstanceCore GetInstance(Models.Instance metadata)
520 {
521 lock (instances)
522 {
523 instances.TryGetValue(metadata.Id.Value, out var container);
524 return container?.Instance;
525 }
526 }
527
533 async Task Initialize(CancellationToken cancellationToken)
534 {
535 try
536 {
537 logger.LogInformation("{versionString}", assemblyInformationProvider.VersionString);
538
540
541 // To let the web server startup immediately before we do any intense work
542 await Task.Yield();
543
544 await InitializeSwarm(cancellationToken);
545
546 List<Models.Instance> dbInstances = null;
547 var instanceEnumeration = databaseContextFactory.UseContext(
548 async databaseContext => dbInstances = await databaseContext
549 .Instances
550 .AsQueryable()
551 .Where(x => x.Online.Value && x.SwarmIdentifer == swarmConfiguration.Identifier)
552 .Include(x => x.RepositorySettings)
553 .Include(x => x.ChatSettings)
554 .ThenInclude(x => x.Channels)
555 .Include(x => x.DreamDaemonSettings)
556 .ToListAsync(cancellationToken));
557
558 var factoryStartup = instanceFactory.StartAsync(cancellationToken);
559 var jobManagerStartup = jobService.StartAsync(cancellationToken);
560
561 await Task.WhenAll(instanceEnumeration, factoryStartup, jobManagerStartup);
562
563 var instanceOnliningTasks = dbInstances.Select(
564 async metadata =>
565 {
566 try
567 {
568 await OnlineInstance(metadata, cancellationToken);
569 }
570 catch (Exception ex)
571 {
572 logger.LogError(ex, "Failed to online instance {instanceId}!", metadata.Id);
573 }
574 });
575
576 await Task.WhenAll(instanceOnliningTasks);
577
578 jobService.Activate(this);
579
580 logger.LogInformation("Server ready!");
581 readyTcs.SetResult();
582 }
583 catch (OperationCanceledException ex)
584 {
585 logger.LogInformation(ex, "Cancelled instance manager initialization!");
586 }
587 catch (Exception e)
588 {
589 logger.LogCritical(e, "Instance manager startup error!");
590 try
591 {
592 await serverControl.Die(e);
593 return;
594 }
595 catch (Exception e2)
596 {
597 logger.LogCritical(e2, "Failed to kill server!");
598 }
599 }
600 }
601
606 {
608
609 using (var systemIdentity = systemIdentityFactory.GetCurrent())
610 {
611 if (!systemIdentity.CanCreateSymlinks)
612 throw new InvalidOperationException("The user running tgstation-server cannot create symlinks! Please try running as an administrative user!");
613 }
614
615 // This runs before the real socket is opened, ensures we don't perform reattaches unless we're fairly certain the bind won't fail
616 // If it does fail, DD will be killed.
618 }
619
625 async Task InitializeSwarm(CancellationToken cancellationToken)
626 {
627 SwarmRegistrationResult registrationResult;
628 do
629 {
630 registrationResult = await swarmServiceController.Initialize(cancellationToken);
631
632 if (registrationResult == SwarmRegistrationResult.Unauthorized)
633 throw new InvalidOperationException("Swarm private key does not match the swarm controller's!");
634
635 if (registrationResult == SwarmRegistrationResult.VersionMismatch)
636 throw new InvalidOperationException("Swarm controller's TGS version does not match our own!");
637
638 if (registrationResult != SwarmRegistrationResult.Success)
639 await asyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken);
640 }
641 while (registrationResult != SwarmRegistrationResult.Success && !cancellationToken.IsCancellationRequested);
642 }
643 }
644}
virtual ? long Id
The ID of the entity.
Definition: EntityId.cs:13
string? Identifier
The server's identifier.
Definition: SwarmServer.cs:21
async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
Online an IInstance. A Task representing the running operation.
Task Ready
Task that completes when the IInstanceManager finishes initializing.
async Task InitializeSwarm(CancellationToken cancellationToken)
Initializes the connection to the TGS swarm.
readonly TaskCompletionSource readyTcs
The TaskCompletionSource for Ready.
async Task MoveInstance(Models.Instance instance, string oldPath, CancellationToken cancellationToken)
Move an IInstance. A Task representing the running operation.
readonly Dictionary< long, ReferenceCountingContainer< IInstance, InstanceWrapper > > instances
Map of instance EntityId.Ids to the respective ReferenceCountingContainer<TWrapped,...
InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IAssemblyInformationProvider assemblyInformationProvider, IJobService jobService, IServerControl serverControl, ISystemIdentityFactory systemIdentityFactory, IAsyncDelayer asyncDelayer, IServerPortProvider serverPortProvider, ISwarmServiceController swarmServiceController, IOptions< GeneralConfiguration > generalConfigurationOptions, IOptions< SwarmConfiguration > swarmConfigurationOptions, ILogger< InstanceManager > logger)
Initializes a new instance of the InstanceManager class.
IInstanceCore GetInstance(Models.Instance metadata)
Get the IInstanceCore for a given instance if it's online. The IInstanceCore if it is online,...
void CheckSystemCompatibility()
Check we have a valid system and configuration.
readonly IJobService jobService
The IJobService for the InstanceManager.
IInstanceReference GetInstanceReference(Api.Models.Instance metadata)
Get the IInstanceReference associated with given metadata . The IInstance associated with the given m...
readonly IIOManager ioManager
The IIOManager for the InstanceManager.
bool disposed
If the InstanceManager has been DisposeAsync'd.
readonly ISwarmServiceController swarmServiceController
The ISwarmServiceController for the InstanceManager.
readonly CancellationTokenSource startupCancellationTokenSource
The CancellationTokenSource for Initialize(CancellationToken).
async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler)
Register a given bridgeHandler . A representative IBridgeRegistration.
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the InstanceManager.
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager.
readonly SwarmConfiguration swarmConfiguration
The SwarmConfiguration for the InstanceManager.
Task StartAsync(CancellationToken cancellationToken)
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the InstanceManager.
async Task Initialize(CancellationToken cancellationToken)
Initializes the InstanceManager.
async Task StopAsync(CancellationToken cancellationToken)
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the InstanceManager.
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager.
readonly SemaphoreSlim instanceStateChangeSemaphore
SemaphoreSlim used to guard calls to OnlineInstance(Models.Instance, CancellationToken) and OfflineIn...
readonly IServerControl serverControl
The IServerControl for the InstanceManager.
async Task< BridgeResponse > ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters . A Task<TResult> resulting in the BridgeResponse for the request o...
readonly IServerPortProvider serverPortProvider
The IServerPortProvider for the InstanceManager.
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the InstanceManager.
readonly IInstanceFactory instanceFactory
The IInstanceFactory for the InstanceManager.
readonly CancellationTokenSource shutdownCancellationTokenSource
The CancellationTokenSource linked with the token given to StopAsync(CancellationToken).
readonly Dictionary< string, IBridgeHandler > bridgeHandlers
Map of DMApiParameters.AccessIdentifiers to their respective IBridgeHandlers.
Task startupTask
The Task returned by Initialize(CancellationToken).
string AccessIdentifier
Used to identify and authenticate the DreamDaemon instance.
void CheckCompatibility(ILogger logger)
Validates the current ConfigVersion's compatibility and provides migration instructions.
Configuration for the server swarm system.
ApiController for managing Components.Instances.
const string InstanceAttachFileName
File name to allow attaching instances.
Extension methods for the Socket class.
static void BindTest(ushort port, bool includeIPv6)
Attempt to exclusively bind to a given port .
IIOManager that resolves paths to Environment.CurrentDirectory.
const string CurrentDirectory
Path to the current working directory for the IIOManager.
Represents an Api.Models.Instance in the database.
Definition: Instance.cs:11
Task OnZeroReferences
A Task that completes when there are no TReference s active for the Instance.
static async ValueTask< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore .
For interacting with the instance services.
Task< IInstance > CreateInstance(IBridgeRegistrar bridgeRegistrar, Models.Instance metadata)
Create an IInstance.
IIOManager CreateGameIOManager(Models.Instance metadata)
Create an IIOManager that resolves to the "Game" directory of the Models.Instance defined by metadata...
Component version of IInstanceCore.
Definition: IInstance.cs:9
Task< BridgeResponse > ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters .
DMApiParameters DMApiParameters
The DMApiParameters for the IBridgeHandler.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
Task Die(Exception exception)
Kill the server with a fatal exception.
Provides access to the server's HttpApiPort.
ushort HttpApiPort
The port the server listens on.
Factory for scoping usage of IDatabaseContexts. Meant for use by Components.
Task UseContext(Func< IDatabaseContext, Task > operation)
Run an operation in the scope of an IDatabaseContext.
Interface for using filesystems.
Definition: IIOManager.cs:13
string ConcatPath(params string[] paths)
Combines an array of strings into a path.
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory, removes and does not enter any symlinks encounterd.
Task MoveDirectory(string source, string destination, CancellationToken cancellationToken)
Moves a directory at source to destination .
Task WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content.
Task< Job > CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
Cancels a give job .
The service that manages everything to do with jobs.
Definition: IJobService.cs:9
void Activate(IInstanceCoreProvider instanceCoreProvider)
Activate the IJobManager.
ISystemIdentity GetCurrent()
Retrieves a ISystemIdentity representing the user executing tgstation-server.
Start and stop controllers for a swarm service.
Task< SwarmRegistrationResult > Initialize(CancellationToken cancellationToken)
Attempt to register with the swarm controller if not one, sets up the database otherwise.
Task Shutdown(CancellationToken cancellationToken)
Deregister with the swarm controller or put clients into querying state.
Task Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
Create a Task that completes after a given timeSpan .
SwarmRegistrationResult
Result of attempting to register with a swarm controller.