2using System.Collections.Generic;
8using System.Threading.Tasks;
10using Microsoft.Extensions.Logging;
71 readonly ILogger<EngineManager>
logger;
76 readonly Dictionary<EngineVersion, ReferenceCountingContainer<IEngineInstallation, EngineExecutableLock>>
installedVersions;
94 ArgumentNullException.ThrowIfNull(version);
96 if (!version.
Engine.HasValue)
97 throw new InvalidOperationException(
"version.Engine cannot be null!");
100 throw new InvalidOperationException(
"version.CustomIteration cannot be 0!");
116 ILogger<EngineManager>
logger)
122 this.logger =
logger ??
throw new ArgumentNullException(nameof(
logger));
124 installedVersions =
new Dictionary<EngineVersion, ReferenceCountingContainer<IEngineInstallation, EngineExecutableLock>>();
136 Stream? customVersionStream,
137 bool allowInstallation,
138 CancellationToken cancellationToken)
155 var stringVersion = version.
ToString();
161 ActiveVersion?.ToString(),
170 logger.LogInformation(
"Active version changed to {version}", version);
177 public async ValueTask<IEngineExecutableLock>
UseExecutables(
EngineVersion? requiredVersion,
string? trustDmbFullPath, CancellationToken cancellationToken)
180 "Acquiring lock on BYOND version {version}...",
181 requiredVersion?.ToString() ?? $
"{ActiveVersion} (active)");
190 requiredVersion !=
null,
195 if (trustDmbFullPath !=
null)
202 installLock.Dispose();
210 ArgumentNullException.ThrowIfNull(progressReporter);
214 logger.LogTrace(
"DeleteVersion {version}", version);
217 if (activeVersion !=
null && version.
Equals(activeVersion))
221 logger.LogTrace(
"Waiting to acquire installedVersions lock...");
226 logger.LogTrace(
"Version {version} already deleted.", version);
230 container = containerNullable;
231 logger.LogTrace(
"Installation container acquired for deletion");
234 progressReporter.StageName =
"Waiting for version to not be in use...";
240 Task activeVersionUpdate;
244 logger.LogTrace(
"Waiting for container.OnZeroReferences or switch of active version...");
245 if (!containerTask.IsCompleted)
251 .WaitAsync(cancellationToken);
253 if (containerTask.IsCompleted)
254 logger.LogTrace(
"All locks for version {version} are gone", version);
256 logger.LogTrace(
"activeVersion changed, we may have to wait again. Acquiring semaphore...");
262 if (activeVersion !=
null && version.
Equals(activeVersion))
266 logger.LogTrace(
"Locking installedVersions...");
272 logger.LogWarning(
"Unable to remove engine installation {version} from list! Is there a duplicate job running?", version);
275 if (container != newerContainer)
280 logger.LogDebug(
"Extreme race condition encountered, applying concentrated copium...");
281 container = newerContainer;
287 logger.LogTrace(
"Proceeding with installation deletion...");
295 logger.LogInformation(
"Deleting version {version}...", version);
296 progressReporter.StageName =
"Deleting installation...";
299 var installPath = version.
ToString();
307 if (containerTask.IsCompleted)
309 "Another lock was acquired before we could remove version {version} from the list. We will have to wait again.",
312 logger.LogTrace(
"Not proceeding for some reason or another");
318 public async Task
StartAsync(CancellationToken cancellationToken)
320 async ValueTask<byte[]?> GetActiveVersion()
326 var activeVersionBytesTask = GetActiveVersion();
331 var installedVersionPaths =
new Dictionary<string, EngineVersion>();
333 async ValueTask ReadVersion(
string path)
344 var text = Encoding.UTF8.GetString(bytes);
353 version = versionNullable!;
359 logger.LogDebug(
"Added detected BYOND version {versionKey}...", version);
365 "It seems that there are multiple directories that say they contain BYOND version {version}. We're ignoring and cleaning the duplicate: {duplicatePath}",
372 lock (installedVersionPaths)
378 .Select(ReadVersion));
380 logger.LogTrace(
"Upgrading BYOND installations...");
382 installedVersionPaths
385 var activeVersionBytes = await activeVersionBytesTask;
386 if (activeVersionBytes !=
null)
388 var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
391 bool hasRequestedActiveVersion;
396 if (hasRequestedActiveVersion)
400 logger.LogWarning(
"Failed to load saved active version {activeVersion}!", activeVersionString);
407 public Task
StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
422 Stream? customVersionStream,
424 bool allowInstallation,
425 CancellationToken cancellationToken)
427 var ourTcs =
new TaskCompletionSource();
430 bool installedOrInstalling;
437 if (customVersionStream !=
null)
439 var customInstallationNumber = 1;
442 version.CustomIteration = customInstallationNumber++;
459 installedOrInstalling =
installedVersions.TryGetValue(version, out var installationContainerNullable);
461 if (!installedOrInstalling)
463 if (!allowInstallation)
464 throw new InvalidOperationException($
"Engine version {version} not installed!");
469 installationContainer = installationContainerNullable!;
471 installation = installationContainer.
Instance;
475 var deploymentPipelineProcesses = !neededForLock;
478 if (installedOrInstalling)
480 progressReporter.StageName =
"Waiting for existing installation job...";
483 logger.LogWarning(
"The required engine version ({version}) is not readily available! We will have to wait for it to install.", version);
490 string? installPath =
null;
493 if (customVersionStream !=
null)
494 logger.LogInformation(
"Installing custom engine version as {version}...", version);
495 else if (neededForLock)
500 logger.LogWarning(
"The required engine version ({version}) is not readily available! We will have to install it.", version);
503 logger.LogInformation(
"Requested engine version {version} not currently installed. Doing so now...", version);
505 progressReporter.StageName =
"Running event";
507 var versionString = version.
ToString();
510 installPath = await
InstallVersionFiles(progressReporter, version, customVersionStream, deploymentPipelineProcesses, cancellationToken);
517 if (installPath !=
null)
521 logger.LogDebug(
"Cleaning up failed installation at {path}...", installPath);
526 logger.LogError(ex2,
"Error cleaning up failed installation!");
529 else if (ex is not OperationCanceledException)
535 ourTcs.SetException(ex);
561 Stream? customVersionStream,
562 bool deploymentPipelineProcesses,
563 CancellationToken cancellationToken)
566 async ValueTask DirectoryCleanup()
572 var directoryCleanupTask = DirectoryCleanup();
576 var remainingProgress = 1.0;
577 if (customVersionStream ==
null)
579 using var subReporter = progressReporter.
CreateSection(
"Downloading Version", 0.5);
580 remainingProgress -= 0.5;
584#pragma warning disable CA2000
587 customVersionStream);
588#pragma warning restore CA2000
593 remainingReporter = progressReporter.
CreateSection(
null, remainingProgress);
597 await engineInstallationData.DisposeAsync();
601 using (remainingReporter)
603 await
using (engineInstallationData)
605 remainingReporter.StageName =
"Cleaning target directory";
607 await directoryCleanupTask;
609 remainingReporter.StageName =
"Extracting data";
611 logger.LogTrace(
"Extracting engine to {extractPath}...", installFullPath);
612 await engineInstallationData.
ExtractToPath(installFullPath, cancellationToken);
616 remainingReporter.StageName =
"Running installation actions";
618 var installation = await
engineInstaller.
Install(version, installFullPath, deploymentPipelineProcesses, cancellationToken);
621 var serverInstallTask =
ioManager.
FileExists(installation.ServerExePath, cancellationToken);
624 logger.LogError(
"Compiler executable does not exist after engine installation!");
628 if (!await serverInstallTask)
630 logger.LogError(
"Server executable does not exist after engine installation!");
635 remainingReporter.StageName =
"Writing version file";
640 Encoding.UTF8.GetBytes(version.
ToString()),
644 catch (HttpRequestException ex)
649 catch (OperationCanceledException)
659 return installFullPath;
674 return installationContainer;
Information about an engine installation.
override string ToString()
static bool TryParse(string input, out EngineVersion? engineVersion)
Attempts to parse a stringified EngineVersion.
bool Equals(EngineVersion other)
EngineType? Engine
The EngineType.
int? CustomIteration
The revision of the custom build.
Extension methods for the ValueTask and ValueTask<TResult> classes.
static async ValueTask WhenAll(IEnumerable< ValueTask > tasks)
Fully await a given list of tasks .
static void CheckVersionParameter(EngineVersion version)
Validates a given version parameter.
readonly Dictionary< EngineVersion, ReferenceCountingContainer< IEngineInstallation, EngineExecutableLock > > installedVersions
Map of byond EngineVersions to Tasks that complete when they are installed.
Task StopAsync(CancellationToken cancellationToken)
const string VersionFileName
The file in which we store the Version for installations.
async Task StartAsync(CancellationToken cancellationToken)
IReadOnlyList< EngineVersion > InstalledVersions
The installed EngineVersions.
async ValueTask< EngineExecutableLock > AssertAndLockVersion(JobProgressReporter progressReporter, EngineVersion version, Stream? customVersionStream, bool neededForLock, bool allowInstallation, CancellationToken cancellationToken)
Ensures a BYOND version is installed if it isn't already.
volatile TaskCompletionSource activeVersionChanged
TaskCompletionSource that notifes when the ActiveVersion changes.
const string ActiveVersionFileName
The file in which we store the ActiveVersion.
async ValueTask< IEngineExecutableLock > UseExecutables(EngineVersion? requiredVersion, string? trustDmbFullPath, CancellationToken cancellationToken)
Lock the current installation's location and return a IEngineExecutableLock.A ValueTask<TResult> resu...
async ValueTask DeleteVersion(JobProgressReporter progressReporter, EngineVersion version, CancellationToken cancellationToken)
Deletes a given version from the disk.A Task representing the running operation.
EngineVersion? ActiveVersion
The currently active EngineVersion.
readonly SemaphoreSlim changeDeleteSemaphore
The SemaphoreSlim for changing or deleting the active BYOND version.
async ValueTask ChangeVersion(JobProgressReporter progressReporter, EngineVersion version, Stream? customVersionStream, bool allowInstallation, CancellationToken cancellationToken)
Change the active EngineVersion.A ValueTask representing the running operation.
readonly IEventConsumer eventConsumer
The IEventConsumer for the EngineManager.
readonly IIOManager ioManager
The IIOManager for the EngineManager.
async ValueTask< string > InstallVersionFiles(JobProgressReporter progressReporter, EngineVersion version, Stream? customVersionStream, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
Installs the files for a given BYOND version .
ReferenceCountingContainer< IEngineInstallation, EngineExecutableLock > AddInstallationContainer(IEngineInstallation installation)
Create and add a new IEngineInstallation to installedVersions.
readonly IEngineInstaller engineInstaller
The IEngineInstaller for the EngineManager.
EngineManager(IIOManager ioManager, IEngineInstaller engineInstaller, IEventConsumer eventConsumer, IDmbFactory dmbFactory, ILogger< EngineManager > logger)
Initializes a new instance of the EngineManager class.
readonly ILogger< EngineManager > logger
The ILogger for the EngineManager.
readonly IDmbFactory dmbFactory
The IDmbFactory for the EngineManager.
Implementation of IEngineInstallationData for a zip file in a Stream.
IIOManager that resolves paths to Environment.CurrentDirectory.
const string CurrentDirectory
Path to the current working directory for the IIOManager.
Operation exceptions thrown from the context of a Models.Job.
Progress reporter for a Job.
JobProgressReporter CreateSection(string? newStageName, double percentage)
Create a subsection of the JobProgressReporter with its optional own stage name.
void ReportProgress(double? progress)
Report progress.
Wrapper for managing some TWrapped .
TReference AddReference()
Create a new TReference to the Instance.
TWrapped Instance
The TWrapped .
Task OnZeroReferences
A Task that completes when there are no TReference s active for the Instance.
Async lock context helper.
static async ValueTask< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken, ILogger? logger=null)
Asyncronously locks a semaphore .
Factory for IDmbProviders.
void LogLockStates()
Log the states of all active IDmbProviders.
Wraps data containing an engine installation.
ValueTask ExtractToPath(string path, CancellationToken cancellationToken)
Extracts the installation to a given path.
Represents a BYOND installation.
Task InstallationTask
The Task that completes when the BYOND version finished installing.
EngineVersion Version
The EngineVersion of the IEngineInstallation.
For downloading and installing game engines for a given system.
ValueTask< IEngineInstallationData > DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken)
Download a given engine version .
ValueTask< IEngineInstallation > Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
Does actions necessary to get an extracted installation working.
ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken)
Does actions necessary to get upgrade a version installed by a previous version of TGS.
ValueTask< IEngineInstallation > GetInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken)
Creates an IEngineInstallation for a given version .
ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken)
Add a given fullDmbPath to the trusted DMBs list in BYOND's config.
For managing the engine installations.
Consumes EventTypes and takes the appropriate actions.
ValueTask HandleEvent(EventType eventType, IEnumerable< string?> parameters, bool sensitiveParameters, bool deploymentPipeline, CancellationToken cancellationToken)
Handle a given eventType .
Interface for using filesystems.
string ResolvePath()
Retrieve the full path of the current working directory.
ValueTask< byte[]> ReadAllBytes(string path, CancellationToken cancellationToken)
Returns all the contents of a file at path as a byte array.
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 .
ValueTask WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content.
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory, removes and does not enter any symlinks encounterd.
Task< bool > FileExists(string path, CancellationToken cancellationToken)
Check that the file at path exists.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
EventType
Types of events. Mirror in tgs.dm. Prefer last listed name for script.