1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
6 using System.Collections.Generic;
10 using System.Threading.Tasks;
28 public Task Ready => readyTcs.Task;
73 readonly ILogger<InstanceManager>
logger;
93 readonly TaskCompletionSource<object>
readyTcs;
127 IOptions<GeneralConfiguration> generalConfigurationOptions,
128 ILogger<InstanceManager> logger)
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));
143 instances =
new Dictionary<long, IInstance>();
144 bridgeHandlers =
new Dictionary<string, IBridgeHandler>();
145 readyTcs =
new TaskCompletionSource<object>();
158 foreach (var I
in instances)
165 if (metadata == null)
166 throw new ArgumentNullException(nameof(metadata));
169 if (!instances.TryGetValue(metadata.Id, out
IInstance instance))
170 throw new InvalidOperationException(
"Instance not online!");
176 public async Task
MoveInstance(Models.Instance instance,
string newPath, CancellationToken cancellationToken)
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 =>
186 var targetInstance =
new Models.Instance
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);
198 public async Task
OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
200 if (metadata == null)
201 throw new ArgumentNullException(nameof(metadata));
202 logger.LogInformation(
"Offlining instance ID {0}", metadata.Id);
206 if (!instances.TryGetValue(metadata.Id, out instance))
207 throw new InvalidOperationException(
"Instance not online!");
208 instances.Remove(metadata.Id);
214 var tasks =
new List<Task>();
215 await databaseContextFactory.UseContext(async db =>
220 .Where(x => x.Instance.Id == metadata.Id)
221 .Select(x =>
new Models.Job
225 await jobs.ForEachAsync(job =>
228 tasks.Add(jobManager.CancelJob(job, user,
true, cancellationToken));
229 }, cancellationToken).ConfigureAwait(
false);
230 }).ConfigureAwait(
false);
232 await Task.WhenAll(tasks).ConfigureAwait(
false);
234 await instance.StopAsync(cancellationToken).ConfigureAwait(
false);
243 public async Task
OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
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);
253 if (instances.ContainsKey(metadata.Id))
254 throw new InvalidOperationException(
"Instance already online!");
255 instances.Add(metadata.Id, instance);
264 await instance.StartAsync(cancellationToken).ConfigureAwait(
false);
268 #pragma warning disable CA1506 // TODO: Decomplexify 269 public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
271 logger.LogInformation(assemblyInformationProvider.VersionString);
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
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);
295 catch (OperationCanceledException)
297 logger.LogInformation(
"Cancelled instance manager initialization!");
301 logger.LogCritical(
"Instance manager startup error! Exception: {0}", e);
304 await serverControl.Die(e).ConfigureAwait(
false);
309 logger.LogCritical(
"Failed to kill server! Exception: {0}", e2);
315 #pragma warning restore CA1506 // TODO: Decomplexify 318 public async Task
StopAsync(CancellationToken cancellationToken)
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);
325 if (downgradeVersion != null)
326 await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(
false);
330 public Task
HandleRestart(Version updateVersion, CancellationToken cancellationToken)
332 downgradeVersion = updateVersion != null && updateVersion < assemblyInformationProvider.Version ? updateVersion : null;
333 return Task.CompletedTask;
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!");
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!");
353 if (parameters == null)
354 throw new ArgumentNullException(nameof(parameters));
357 for (var i = 0; bridgeHandler == null && i < 30; ++i)
361 Task delayTask = Task.CompletedTask;
362 lock (bridgeHandlers)
363 if (!bridgeHandlers.TryGetValue(parameters.
AccessIdentifier, out bridgeHandler))
364 delayTask = asyncDelayer.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
366 await delayTask.ConfigureAwait(
false);
369 if (bridgeHandler == null)
370 lock (bridgeHandlers)
371 if (!bridgeHandlers.TryGetValue(parameters.
AccessIdentifier, out bridgeHandler))
373 logger.LogWarning(
"Recieved invalid bridge request with accees identifier: {0}", parameters.
AccessIdentifier);
377 return await bridgeHandler.
ProcessBridgeRequest(parameters, cancellationToken).ConfigureAwait(
false);
383 if (bridgeHandler == null)
384 throw new ArgumentNullException(nameof(bridgeHandler));
387 lock (bridgeHandlers)
389 bridgeHandlers.Add(accessIdentifier, bridgeHandler);
390 logger.LogTrace(
"Registered bridge handler: {0}", accessIdentifier);
395 lock (bridgeHandlers)
397 bridgeHandlers.Remove(accessIdentifier);
398 logger.LogTrace(
"Unregistered bridge handler: {0}", accessIdentifier);
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.
Use server authentication
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
For waiting asynchronously
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.
Factory for creating IInstances
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager
readonly IJobManager jobManager
The IJobManager for the InstanceManager
Handler for server restarts
readonly IDictionary< long, IInstance > instances
Map of instance EntityId.Ids to respective IInstances. Also used as a object.
Factory for ISystemIdentitys
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
async Task StopAsync(CancellationToken cancellationToken)
string AccessIdentifier
Used to identify and authenticate the DreamDaemon instance
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
Parameters for a bridge request.
readonly IInstanceFactory instanceFactory
The IInstanceFactory for the InstanceManager
Task< BridgeResponse > ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters .
General configuration options
Manages the runtime of Jobs
readonly IIOManager ioManager
The IIOManager for the InstanceManager
Interface for using filesystems
Registers IBridgeHandlers.
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...