1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
6 using System.Collections.Generic;
10 using System.Linq.Expressions;
14 using System.Threading.Tasks;
33 #pragma warning disable CA1506 // TODO: Decomplexify 39 const string InstanceAttachFileName =
"TGS4_ALLOW_INSTANCE_ATTACH";
41 const string MoveInstanceJobPrefix =
"Move instance ID ";
93 IOptions<GeneralConfiguration> generalConfigurationOptions,
94 ILogger<InstanceController> logger)
95 : base(databaseContext, authenticationContextFactory, logger, false, true)
97 this.jobManager = jobManager ??
throw new ArgumentNullException(nameof(jobManager));
98 this.instanceManager = instanceManager ??
throw new ArgumentNullException(nameof(instanceManager));
99 this.ioManager = ioManager ??
throw new ArgumentNullException(nameof(ioManager));
100 this.assemblyInformationProvider = assemblyInformationProvider ??
throw new ArgumentNullException(nameof(assemblyInformationProvider));
101 this.platformIdentifier = platformIdentifier ??
throw new ArgumentNullException(nameof(platformIdentifier));
102 generalConfiguration = generalConfigurationOptions?.Value ??
throw new ArgumentNullException(nameof(generalConfigurationOptions));
110 path = ioManager.ResolvePath(path);
111 if (platformIdentifier.IsWindows)
112 path = path.ToUpperInvariant().Replace(
'\\',
'/');
117 Models.InstanceUser InstanceAdminUser() =>
new Models.InstanceUser
139 [ProducesResponseType(typeof(Api.Models.Instance), 200)]
140 [ProducesResponseType(typeof(Api.Models.Instance), 201)]
141 public async Task<IActionResult>
Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
144 throw new ArgumentNullException(nameof(model));
146 if (String.IsNullOrWhiteSpace(model.Name))
149 var targetInstancePath = NormalizePath(model.Path);
150 model.Path = targetInstancePath;
154 bool InstanceIsChildOf(
string otherPath)
156 if (!targetInstancePath.StartsWith(otherPath, StringComparison.Ordinal))
159 bool sameLength = targetInstancePath.Length == otherPath.Length;
160 char dirSeparatorChar = targetInstancePath.ToCharArray()[Math.Min(otherPath.Length, targetInstancePath.Length - 1)];
162 || dirSeparatorChar == Path.DirectorySeparatorChar
163 || dirSeparatorChar == Path.AltDirectorySeparatorChar;
166 if (InstanceIsChildOf(installationDirectoryPath))
170 IActionResult earlyOut = null;
171 ulong countOfOtherInstances = 0;
172 using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
174 var newCancellationToken = cts.Token;
180 .Select(x =>
new Models.Instance
187 if (++countOfOtherInstances >= generalConfiguration.InstanceLimit)
189 else if (InstanceIsChildOf(otherInstance.Path))
192 if (earlyOut != null && !newCancellationToken.IsCancellationRequested)
195 newCancellationToken)
196 .ConfigureAwait(
false);
198 catch (OperationCanceledException)
200 cancellationToken.ThrowIfCancellationRequested();
204 if (earlyOut != null)
208 if (!(generalConfiguration.ValidInstancePaths?
209 .Select(path => NormalizePath(path))
210 .Any(path => InstanceIsChildOf(path)) ??
true))
213 async Task<bool> DirExistsAndIsNotEmpty()
215 if (!await ioManager.DirectoryExists(model.Path, cancellationToken).ConfigureAwait(
false))
218 var filesTask = ioManager.GetFiles(model.Path, cancellationToken);
219 var dirsTask = ioManager.GetDirectories(model.Path, cancellationToken);
221 var files = await filesTask.ConfigureAwait(
false);
222 var dirs = await dirsTask.ConfigureAwait(
false);
224 return files.Concat(dirs).Any();
227 var dirExistsTask = DirExistsAndIsNotEmpty();
228 bool attached =
false;
229 if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(
false) || await dirExistsTask.ConfigureAwait(
false))
230 if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(
false))
235 var newInstance =
new Models.Instance
240 AllowWebClient =
false,
243 SecondaryPort = 1338,
246 HeartbeatSeconds = 60
250 ApiValidationPort = 1339,
256 AutoUpdateInterval = model.AutoUpdateInterval ?? 0,
260 CommitterEmail =
"tgstation-server@users.noreply.github.com",
261 CommitterName = assemblyInformationProvider.VersionPrefix,
262 PushTestMergeCommits =
false,
263 ShowTestMergeCommitters =
false,
264 AutoUpdatesKeepTestMerges =
false,
265 AutoUpdatesSynchronize =
false,
266 PostTestMergeComment =
false 268 InstanceUsers =
new List<Models.InstanceUser>
282 await ioManager.CreateDirectory(targetInstancePath, cancellationToken).ConfigureAwait(
false);
283 await ioManager.DeleteFile(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken).ConfigureAwait(
false);
294 catch (IOException e)
298 AdditionalData = e.Message
302 Logger.LogInformation(
"{0} {1} instance {2}: {3} ({4})",
AuthenticationContext.
User.
Name, attached ?
"attached" :
"created", newInstance.Name, newInstance.Id, newInstance.Path);
304 var api = newInstance.ToApi();
305 return attached ? (IActionResult)Json(api) : StatusCode((
int)HttpStatusCode.Created, api);
318 [ProducesResponseType(204)]
319 [ProducesResponseType(410)]
320 public async Task<IActionResult>
Delete(
long id, CancellationToken cancellationToken)
325 .Where(x => x.Id ==
id)
326 .Include(x => x.WatchdogReattachInformation)
327 .Include(x => x.WatchdogReattachInformation.Alpha)
328 .Include(x => x.WatchdogReattachInformation.Bravo)
329 .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(
false);
330 if (originalModel ==
default)
331 return StatusCode((
int)HttpStatusCode.Gone);
332 if (originalModel.Online.Value)
335 if (originalModel.WatchdogReattachInformation != null)
338 if (originalModel.WatchdogReattachInformation.Alpha != null)
340 if (originalModel.WatchdogReattachInformation.Bravo != null)
346 var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName);
347 await ioManager.WriteAllBytes(attachFileName, Array.Empty<byte>(),
default).ConfigureAwait(
false);
362 [ProducesResponseType(typeof(Api.Models.Instance), 200)]
363 [ProducesResponseType(typeof(Api.Models.Instance), 202)]
364 [ProducesResponseType(410)]
365 #pragma warning disable CA1502 // TODO: Decomplexify 366 public async Task<IActionResult>
Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
369 throw new ArgumentNullException(nameof(model));
374 .Where(x => x.Id == model.Id);
376 var moveJob = await InstanceQuery()
377 .SelectMany(x => x.Jobs).
378 #pragma warning disable CA1307
379 Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
380 #pragma warning restore CA1307
381 .Select(x =>
new Models.Job
384 }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(
false);
386 if (moveJob !=
default)
389 var originalModel = await InstanceQuery()
390 .Include(x => x.RepositorySettings)
391 .Include(x => x.ChatSettings)
392 .ThenInclude(x => x.Channels)
393 .Include(x => x.DreamDaemonSettings)
394 .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(
false);
395 if (originalModel ==
default(Models.Instance))
396 return StatusCode((
int)HttpStatusCode.Gone);
399 bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression,
InstanceManagerRights requiredRight)
401 var memberSelectorExpression = (MemberExpression)expression.Body;
402 var property = (PropertyInfo)memberSelectorExpression.Member;
404 var newVal =
property.GetValue(model);
407 if (!userRights.HasFlag(requiredRight) &&
property.GetValue(originalModel) != newVal)
410 property.SetValue(originalModel, newVal);
414 string originalModelPath = null;
415 string rawPath = null;
416 if (model.Path != null)
418 rawPath = NormalizePath(model.Path);
420 if (model.Path != originalModel.Path)
424 if (originalModel.Online.Value && model.Online !=
true)
427 var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
428 if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(
false) || await dirExistsTask.ConfigureAwait(
false))
431 originalModelPath = originalModel.Path;
432 originalModel.Path = model.Path;
436 var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value;
437 var originalOnline = originalModel.Online.Value;
438 var renamed = model.Name != null && originalModel.Name != model.Name;
447 if (model.ChatBotLimit.HasValue)
452 .Where(x => x.InstanceId == originalModel.Id)
453 .CountAsync(cancellationToken)
454 .ConfigureAwait(
false);
456 if (countOfExistingChatBots > model.ChatBotLimit.Value)
461 var usersInstanceUser = await InstanceQuery()
462 .SelectMany(x => x.InstanceUsers)
464 .FirstOrDefaultAsync(cancellationToken)
465 .ConfigureAwait(
false);
466 if (usersInstanceUser ==
default)
468 var instanceAdminUser = InstanceAdminUser();
469 instanceAdminUser.InstanceId = originalModel.Id;
478 await instanceManager.GetInstance(originalModel).InstanceRenamed(originalModel.Name, cancellationToken).ConfigureAwait(
false);
480 var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
483 if (originalOnline && model.Online ==
false)
484 await instanceManager.OfflineInstance(originalModel,
AuthenticationContext.
User, cancellationToken).ConfigureAwait(
false);
485 else if (!originalOnline && model.Online ==
true)
489 originalModel.DreamDaemonSettings.AutoStart =
false;
490 await instanceManager.OnlineInstance(originalModel, cancellationToken).ConfigureAwait(
false);
495 if(!(e is OperationCanceledException))
496 Logger.LogError(
"Error changing instance online state! Exception: {0}", e);
497 originalModel.Online = originalOnline;
498 originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
499 if (originalModelPath != null)
500 originalModel.Path = originalModelPath;
507 Id = originalModel.Id
510 var moving = originalModelPath != null;
513 var job =
new Models.Job
515 Description = String.Format(CultureInfo.InvariantCulture, MoveInstanceJobPrefix +
"{0} from {1} to {2}", originalModel.Id, originalModel.Path, rawPath),
517 CancelRightsType =
RightsType.InstanceManager,
522 await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(
false);
523 api.MoveJob = job.ToApi();
526 if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
527 await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(
false);
529 return moving ? (IActionResult)Accepted(api) : Json(api);
531 #pragma warning restore CA1502 541 [ProducesResponseType(typeof(IEnumerable<Api.Models.Instance>), 200)]
542 public async Task<IActionResult>
List(CancellationToken cancellationToken)
550 .Where(x => x.InstanceUsers.Any(instanceUser =>
559 return query.Select(x => x);
562 var moveJobs = await GetBaseQuery()
563 .SelectMany(x => x.Jobs)
564 #pragma warning disable CA1307 // Specify StringComparison 565 .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
566 #pragma warning restore CA1307
567 .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
568 .Include(x => x.Instance)
569 .ToListAsync(cancellationToken)
570 .ConfigureAwait(
false);
572 var instances = await GetBaseQuery()
573 .ToListAsync(cancellationToken)
574 .ConfigureAwait(
false);
576 var apis = instances.Select(x => x.ToApi());
577 foreach(var I
in moveJobs)
578 apis.Where(x => x.Id == I.Instance.Id).First().MoveJob = I.ToApi();
592 [ProducesResponseType(typeof(Api.Models.Instance), 200)]
593 [ProducesResponseType(410)]
594 public async Task<IActionResult>
GetId(
long id, CancellationToken cancellationToken)
602 .Where(x => x.Id ==
id);
605 query = query.Include(x => x.InstanceUsers);
609 var instance = await QueryForUser().FirstOrDefaultAsync(cancellationToken).ConfigureAwait(
false);
611 if (instance == null)
612 return StatusCode((
int)HttpStatusCode.Gone);
623 var api = instance.ToApi();
625 var moveJob = await QueryForUser()
626 .SelectMany(x => x.Jobs)
627 #pragma warning disable CA1307 // Specify StringComparison 628 .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
629 #pragma warning restore CA1307
630 .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
631 .FirstOrDefaultAsync(cancellationToken)
632 .ConfigureAwait(
false);
633 api.MoveJob = moveJob?.ToApi();
string Name
The name of the User
DbSet< ChatBot > ChatBots
The ChatBots in the DatabaseContext.
For creating and accessing authentication contexts
ConfigurationRights
Rights for Models.ConfigurationFile
DbSet< DualReattachInformation > WatchdogReattachInformations
The DualReattachInformations in the DatabaseContext.
Manages Api.Models.Users for a scope
async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Get a specific Api.Models.Instance.
ErrorCode
Types of ErrorMessages that the API may return.
ByondRights
Rights for Models.Byond
RightsType
The type of rights a model uses
InstanceUserRights
Rights for an Models.Instance
Use server authentication
InstanceManagerRights InstanceManagerRights
The Rights.InstanceManagerRights for the User
readonly IInstanceManager instanceManager
The IInstanceManager for the InstanceController
async Task< IActionResult > List(CancellationToken cancellationToken)
List Api.Models.Instances.
ConfigurationType
The type of configuration allowed on an Instance
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the InstanceController.
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
ApiController for managing Components.Instances
readonly IIOManager ioManager
The IIOManager for the InstanceController
Backend abstract implementation of IDatabaseContext
readonly IJobManager jobManager
The IJobManager for the InstanceController
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the InstanceController
async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Detach an Api.Models.Instance with the given id .
Routes to a server actions
DreamDaemonRights
Rights for Models.DreamDaemon
const string CurrentDirectory
Path to the current working directory for the IIOManager.
InstanceManagerRights
Rights for managing Models.Instances
DbSet< Instance > Instances
The Instances in the DatabaseContext.
async Task< IActionResult > Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
Modify an Api.Models.Instance's settings.
A Controller for API functions
DbSet< ReattachInformation > ReattachInformations
The ReattachInformations in the DatabaseContext.
DreamMakerRights
Rights for Models.DreamMaker
DbSet< InstanceUser > InstanceUsers
The InstanceUsers in the DatabaseContext.
RepositoryRights
Rights for a Models.Repository
InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, IAssemblyInformationProvider assemblyInformationProvider, IPlatformIdentifier platformIdentifier, IOptions< GeneralConfiguration > generalConfigurationOptions, ILogger< InstanceController > logger)
Construct a InstanceController
const string InstanceManager
The Models.Instance controller
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the InstanceController
string NormalizePath(string path)
long Id
The ID of the User
Metadata about a server instance
General configuration options
Manages the runtime of Jobs
Instance Instance
The parent Models.Instance
IIOManager that resolves paths to Environment.CurrentDirectory
DreamDaemonSecurity
DreamDaemon's security level
const string List
The postfix for list operations
Interface for using filesystems
async Task< IActionResult > Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
Create or attach an Api.Models.Instance.
ChatBotRights
Rights for Models.ChatBot
Represents an error message returned by the server
const ushort DefaultChatBotLimit
Default for Api.Models.Instance.ChatBotLimit.