2using System.Collections.Generic;
3using System.Diagnostics.CodeAnalysis;
6using System.Linq.Expressions;
7using System.Reflection;
9using System.Threading.Tasks;
11using Microsoft.AspNetCore.Mvc;
12using Microsoft.EntityFrameworkCore;
13using Microsoft.Extensions.Logging;
14using Microsoft.Extensions.Options;
39#pragma warning disable CA1506
100 ILogger<InstanceController> logger,
107 IOptions<GeneralConfiguration> generalConfigurationOptions,
108 IOptions<SwarmConfiguration> swarmConfigurationOptions,
112 authenticationContext,
124 generalConfiguration = generalConfigurationOptions?.Value ??
throw new ArgumentNullException(nameof(generalConfigurationOptions));
125 swarmConfiguration = swarmConfigurationOptions?.Value ??
throw new ArgumentNullException(nameof(swarmConfigurationOptions));
142 ArgumentNullException.ThrowIfNull(model);
144 if (String.IsNullOrWhiteSpace(model.Name) || String.IsNullOrWhiteSpace(model.Path))
147 var unNormalizedPath = model.Path;
149 model.Path = targetInstancePath;
153 bool InstanceIsChildOf(
string otherPath)
155 if (!targetInstancePath.StartsWith(otherPath, StringComparison.Ordinal))
158 bool sameLength = targetInstancePath.Length == otherPath.Length;
159 char dirSeparatorChar = targetInstancePath.ToCharArray()[Math.Min(otherPath.Length, targetInstancePath.Length - 1)];
161 || dirSeparatorChar == Path.DirectorySeparatorChar
162 || dirSeparatorChar == Path.AltDirectorySeparatorChar;
165 if (InstanceIsChildOf(installationDirectoryPath))
169 IActionResult? earlyOut =
null;
170 ulong countOfOtherInstances = 0;
171 using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
173 var newCancellationToken = cts.Token;
180 .Select(x =>
new Models.Instance
189 else if (InstanceIsChildOf(otherInstance.Path!))
192 if (earlyOut !=
null && !newCancellationToken.IsCancellationRequested)
195 newCancellationToken);
197 catch (OperationCanceledException)
199 cancellationToken.ThrowIfCancellationRequested();
203 if (earlyOut !=
null)
209 .Any(path => InstanceIsChildOf(path)) ??
true))
212 async ValueTask<bool> DirExistsAndIsNotEmpty()
220 var files = await filesTask;
221 var dirs = await dirsTask;
223 return files.Concat(dirs).Any();
226 var dirExistsTask = DirExistsAndIsNotEmpty();
227 bool attached =
false;
235 if (newInstance ==
null)
259 catch (IOException e)
263 AdditionalData = e.Message,
268 "{userName} {attachedOrCreated} instance {instanceName}: {instanceId} ({instancePath})",
270 attached ?
"attached" :
"created",
276 newInstance.InstancePermissionSets.First(),
279 var api = newInstance.ToApi();
280 api.Accessible =
true;
281 return attached ? Json(api) :
Created(api);
294 [ProducesResponseType(204)]
296 public async ValueTask<IActionResult>
Delete(
long id, CancellationToken cancellationToken)
301 .Where(x => x.Id ==
id && x.SwarmIdentifer == swarmConfiguration.Identifier)
302 .FirstOrDefaultAsync(cancellationToken);
303 if (originalModel ==
default)
305 if (originalModel.Online!.Value)
310 var originalPath = originalModel.Path!;
311 var attachFileName = ioManager.ConcatPath(originalPath, InstanceAttachFileName);
314 if (await ioManager.DirectoryExists(originalPath, cancellationToken))
315 await ioManager.WriteAllBytes(attachFileName, Array.Empty<
byte>(), cancellationToken);
317 catch (OperationCanceledException)
320 await ioManager.DeleteFile(attachFileName, CancellationToken.None);
342#pragma warning disable CA1502
345 ArgumentNullException.ThrowIfNull(model);
350 .Where(x => x.Id == model.Id && x.SwarmIdentifer == swarmConfiguration.Identifier);
352 var moveJob = await InstanceQuery()
353 .SelectMany(x => x.Jobs)
354 .Where(x => !x.StoppedAt.HasValue && x.JobCode ==
JobCode.Move)
355 .Select(x =>
new Job(x.Id!.Value))
356 .FirstOrDefaultAsync(cancellationToken);
366 var originalModel = await InstanceQuery()
367 .Include(x => x.RepositorySettings)
368 .Include(x => x.ChatSettings)
369 .ThenInclude(x => x.Channels)
370 .Include(x => x.DreamDaemonSettings)
371 .FirstOrDefaultAsync(cancellationToken);
372 if (originalModel ==
default(Models.Instance))
375 if (ValidateInstanceOnlineStatus(originalModel))
379 bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression,
InstanceManagerRights requiredRight)
381 var memberSelectorExpression = (MemberExpression)expression.Body;
382 var property = (PropertyInfo)memberSelectorExpression.Member;
384 var newVal =
property.GetValue(model);
387 if (!userRights.HasFlag(requiredRight) &&
property.GetValue(originalModel) != newVal)
390 property.SetValue(originalModel, newVal);
394 string? originalModelPath =
null;
395 string? rawPath =
null;
396 var originalOnline = originalModel.Online!.Value;
397 if (model.Path !=
null)
399 rawPath = NormalizePath(model.Path);
401 if (rawPath != originalModel.Path)
405 if (originalOnline && model.Online !=
true)
408 var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
409 if (await ioManager.FileExists(model.Path, cancellationToken) || await dirExistsTask)
412 originalModelPath = originalModel.Path;
413 originalModel.Path = rawPath;
417 var oldAutoUpdateInterval = originalModel.AutoUpdateInterval!.Value;
418 var renamed = model.Name !=
null && originalModel.Name != model.Name;
427 if (model.ChatBotLimit.HasValue)
432 .Where(x => x.InstanceId == originalModel.Id)
433 .CountAsync(cancellationToken);
435 if (countOfExistingChatBots > model.ChatBotLimit.Value)
444 await WithComponentInstanceNullable(
445 async componentInstance =>
447 await componentInstance.InstanceRenamed(originalModel.Name!, cancellationToken);
453 var oldAutoStart = originalModel.DreamDaemonSettings!.AutoStart;
456 if (originalOnline && model.Online ==
false)
458 else if (!originalOnline && model.Online ==
true)
462 originalModel.DreamDaemonSettings.AutoStart =
false;
463 await InstanceOperations.OnlineInstance(originalModel, cancellationToken);
468 if (e is not OperationCanceledException)
469 Logger.LogError(e,
"Error changing instance online state!");
470 originalModel.Online = originalOnline;
471 originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
472 if (originalModelPath !=
null)
473 originalModel.Path = originalModelPath;
482 Id = originalModel.
Id,
485 var moving = originalModelPath !=
null;
488 var description = $
"Move instance ID {originalModel.Id} from {originalModelPath} to {rawPath}";
490 job.Description = description;
492 await jobManager.RegisterOperation(
494 (core, databaseContextFactory, paramJob, progressHandler, ct)
495 => InstanceOperations.MoveInstance(originalModel, originalModelPath!, ct),
497 api.MoveJob = job.
ToApi();
500 if (model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
503 await WithComponentInstanceNullable(
504 async componentInstance =>
506 await componentInstance.SetAutoUpdateInterval(model.AutoUpdateInterval.Value);
512 await CheckAccessible(api, cancellationToken);
513 return moving ? Accepted(api) : Json(api);
515#pragma warning restore CA1502
528 public async ValueTask<IActionResult>
List(
529 [FromQuery]
int? page,
530 [FromQuery]
int? pageSize,
531 CancellationToken cancellationToken)
538 .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier);
542 .Where(x => x.InstancePermissionSets.Any(instanceUser =>
551 return query.Select(x => x);
554 var moveJobs = await GetBaseQuery()
555 .SelectMany(x => x.Jobs)
556 .Where(x => !x.StoppedAt.HasValue && x.JobCode ==
JobCode.Move)
557 .Include(x => x.StartedBy!)
558 .ThenInclude(x => x.CreatedBy)
559 .Include(x => x.Instance)
560 .ToListAsync(cancellationToken);
562 var needsUpdate =
false;
564 () => ValueTask.FromResult(
567 .OrderBy(x => x.Id))),
570 needsUpdate |= ValidateInstanceOnlineStatus(instance);
571 instance.MoveJob = moveJobs.FirstOrDefault(x => x.Instance!.Id == instance.Id)?.ToApi();
572 await CheckAccessible(instance, cancellationToken);
596 public async ValueTask<IActionResult>
GetId(
long id, CancellationToken cancellationToken)
604 .Where(x => x.Id ==
id && x.SwarmIdentifer == swarmConfiguration.Identifier);
607 query = query.Include(x => x.InstancePermissionSets);
611 var instance = await QueryForUser().FirstOrDefaultAsync(cancellationToken);
613 if (instance ==
null)
616 if (ValidateInstanceOnlineStatus(instance))
629 var api = instance.ToApi();
631 var moveJob = await QueryForUser()
632 .SelectMany(x => x.Jobs)
633 .Where(x => !x.StoppedAt.HasValue && x.JobCode ==
JobCode.Move)
634 .Include(x => x.StartedBy!)
635 .ThenInclude(x => x.CreatedBy)
636 .Include(x => x.Instance)
637 .FirstOrDefaultAsync(cancellationToken);
638 api.MoveJob = moveJob?.ToApi();
639 await CheckAccessible(api, cancellationToken);
652 [ProducesResponseType(204)]
654 public async ValueTask<IActionResult>
GrantPermissions(
long id, CancellationToken cancellationToken)
659 .Where(x => x.Id ==
id && x.SwarmIdentifer == swarmConfiguration.Identifier);
662 var usersInstancePermissionSet = await BaseQuery()
663 .SelectMany(x => x.InstancePermissionSets)
665 .FirstOrDefaultAsync(cancellationToken);
666 if (usersInstancePermissionSet ==
default)
669 var instanceExists = await BaseQuery()
670 .AnyAsync(cancellationToken);
675 var instanceAdminUser = InstanceAdminPermissionSet(
null);
676 instanceAdminUser.InstanceId = id;
680 InstanceAdminPermissionSet(usersInstancePermissionSet);
695 var ddPort = await portAllocator.GetAvailablePort(1024,
false, cancellationToken);
696 if (!ddPort.HasValue)
700 const ushort DefaultDreamDaemonPort = 1337;
701 if (ddPort.Value < DefaultDreamDaemonPort)
702 ddPort = await portAllocator.GetAvailablePort(DefaultDreamDaemonPort,
false, cancellationToken) ?? ddPort;
704 const ushort DefaultApiValidationPort = 1339;
705 var dmPort = await portAllocator
707 Math.Min((ushort)(ddPort.Value + 1), DefaultApiValidationPort),
710 if (!dmPort.HasValue)
714 if (dmPort < DefaultApiValidationPort)
715 dmPort = await portAllocator.GetAvailablePort(DefaultApiValidationPort,
false, cancellationToken) ?? dmPort;
722 AllowWebClient =
false,
728 HealthCheckSeconds = 60,
729 DumpOnHealthCheckRestart =
false,
730 TopicRequestTimeout = generalConfiguration.ByondTopicTimeout,
731 AdditionalParameters = String.Empty,
732 StartProfiler =
false,
738 ApiValidationPort = dmPort,
740 RequireDMApiValidation =
true,
741 Timeout = TimeSpan.FromHours(1),
743 Name = initialSettings.
Name,
745 Path = initialSettings.
Path,
746 AutoUpdateInterval = initialSettings.AutoUpdateInterval ?? 0,
747 ChatBotLimit = initialSettings.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit,
750 CommitterEmail = Components.Repository.Repository.DefaultCommitterEmail,
751 CommitterName = Components.Repository.Repository.DefaultCommitterName,
752 PushTestMergeCommits =
false,
753 ShowTestMergeCommitters =
false,
754 AutoUpdatesKeepTestMerges =
false,
755 AutoUpdatesSynchronize =
false,
756 PostTestMergeComment =
false,
757 CreateGitHubDeployments =
false,
758 UpdateSubmodules =
true,
760 InstancePermissionSets =
new List<InstancePermissionSet>
762 InstanceAdminPermissionSet(
null),
764 SwarmIdentifer = swarmConfiguration.Identifier,
787 return permissionSetToModify;
795 [
return: NotNullIfNotNull(nameof(path))]
801 path = ioManager.ResolvePath(path);
802 if (platformIdentifier.IsWindows)
803 path = path.ToUpperInvariant().Replace(
'\\',
'/');
820 .AnyAsync(cancellationToken);
virtual ? long Id
The ID of the entity.
string? Path
The path to where the Instance is located. Can only be changed while the Instance is offline....
string? Identifier
The server's identifier.
virtual ? string Name
The name of the entity represented by the NamedEntity.
Represents a set of server permissions.
InstanceManagerRights? InstanceManagerRights
The Rights.InstanceManagerRights for the user.
A request to create an Instance.
A request to update an Instance.
Represents an error message returned by the server.
Server response for Instances.
Represents a paginated set of models.
Routes to a server actions.
const string InstanceManager
The Models.Instance controller.
const string List
The postfix for list operations.
General configuration options.
Configuration for the server swarm system.
ObjectResult Created(object payload)
Generic 201 response with a given payload .
ILogger< ApiController > Logger
The ILogger for the ApiController.
ApiController for operations on IInstanceCores.
readonly IInstanceManager instanceManager
The IInstanceManager for the ComponentInterfacingController.
ApiController for managing Components.Instances.
async ValueTask< Models.Instance?> CreateDefaultInstance(InstanceCreateRequest initialSettings, CancellationToken cancellationToken)
Creates a default Models.Instance from initialSettings .
async ValueTask< IActionResult > GetId(long id, CancellationToken cancellationToken)
Get a specific Api.Models.Instance.
readonly IJobManager jobManager
The IJobManager for the InstanceController.
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the InstanceController.
string? NormalizePath(string? path)
Normalize a given path for an instance.
readonly IPortAllocator portAllocator
The IPortAllocator for the InstanceController.
async ValueTask< IActionResult > Update([FromBody] InstanceUpdateRequest model, CancellationToken cancellationToken)
Modify an Api.Models.Instance's settings.
InstancePermissionSet InstanceAdminPermissionSet(InstancePermissionSet? permissionSetToModify)
Generate an InstancePermissionSet with full rights.
readonly SwarmConfiguration swarmConfiguration
The SwarmConfiguration for the InstanceController.
async ValueTask< IActionResult > List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
List Api.Models.Instances.
const string InstanceAttachFileName
File name to allow attaching instances.
async ValueTask< IActionResult > Create([FromBody] InstanceCreateRequest model, CancellationToken cancellationToken)
Create or attach an Api.Models.Instance.
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the InstanceController.
readonly IPermissionsUpdateNotifyee permissionsUpdateNotifyee
The IPermissionsUpdateNotifyee for the InstanceController.
async ValueTask< IActionResult > Delete(long id, CancellationToken cancellationToken)
Detach an Api.Models.Instance with the given id .
async ValueTask CheckAccessible(InstanceResponse instanceResponse, CancellationToken cancellationToken)
Populate the InstanceResponse.Accessible property of a given instanceResponse .
InstanceController(IDatabaseContext databaseContext, IAuthenticationContext authenticationContext, ILogger< InstanceController > logger, IInstanceManager instanceManager, IJobManager jobManager, IIOManager ioManager, IPortAllocator portAllocator, IPlatformIdentifier platformIdentifier, IPermissionsUpdateNotifyee permissionsUpdateNotifyee, IOptions< GeneralConfiguration > generalConfigurationOptions, IOptions< SwarmConfiguration > swarmConfigurationOptions, IApiHeadersProvider apiHeaders)
Initializes a new instance of the InstanceController class.
readonly IIOManager ioManager
The IIOManager for the InstanceController.
async ValueTask< IActionResult > GrantPermissions(long id, CancellationToken cancellationToken)
Gives the current user full permissions on a given instance id .
Helper for returning paginated models.
Backend abstract implementation of IDatabaseContext.
DbSet< Instance > Instances
The Instances in the DatabaseContext.
DbSet< InstancePermissionSet > InstancePermissionSets
The InstancePermissionSets in the DatabaseContext.
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext. A Task representing the running operation.
DbSet< ChatBot > ChatBots
The ChatBots in the DatabaseContext.
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.
static Job Create(JobCode code, User? startedBy, Api.Models.Instance instance)
Creates a new job for registering in the Jobs.IJobService.
User User
The authenticated user.
PermissionSet PermissionSet
The User's effective PermissionSet.
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType . The value of rightsType . Note that if InstancePermissionSet is...
Interface for using filesystems.
Task< IReadOnlyList< string > > GetFiles(string path, CancellationToken cancellationToken)
Returns full file names in a given path .
string ConcatPath(params string[] paths)
Combines an array of strings into a path.
Task< IReadOnlyList< string > > GetDirectories(string path, CancellationToken cancellationToken)
Returns full directory names in a given path .
Task CreateDirectory(string path, CancellationToken cancellationToken)
Create a directory at path .
Task DeleteFile(string path, CancellationToken cancellationToken)
Deletes a file at path .
Task< bool > FileExists(string path, CancellationToken cancellationToken)
Check that the file at path exists.
Task< bool > DirectoryExists(string path, CancellationToken cancellationToken)
Check that the directory at path exists.
Manages the runtime of Jobs.
Represents the currently authenticated Models.User.
Receives notifications about permissions updates.
ValueTask InstancePermissionSetCreated(InstancePermissionSet instancePermissionSet, CancellationToken cancellationToken)
Called when a given instancePermissionSet is successfully created.
Gets unassigned ports for use by TGS.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
DreamDaemonVisibility
The visibility setting for DreamDaemon.
JobCode
The different types of Response.JobResponse.
ConfigurationType
The type of configuration allowed on an Instance.
DreamDaemonSecurity
DreamDaemon's security level.
ChatBotRights
Rights for chat bots.
ConfigurationRights
Rights for Models.IConfigurationFiles.
DreamMakerRights
Rights for deployment.
RightsType
The type of rights a model uses.
EngineRights
Rights for engine version management.
RepositoryRights
Rights for the git repository.
InstancePermissionSetRights
Rights for an Models.Instance.
DreamDaemonRights
Rights for managing DreamDaemon.
InstanceManagerRights
Rights for managing Models.Instances.