2using System.Collections.Generic;
8using System.Threading.Tasks;
10using Microsoft.Extensions.Logging;
65 readonly ILogger<EngineManager>
logger;
70 readonly Dictionary<EngineVersion, ReferenceCountingContainer<IEngineInstallation, EngineExecutableLock>>
installedVersions;
88 ArgumentNullException.ThrowIfNull(version);
90 if (!version.
Engine.HasValue)
91 throw new InvalidOperationException(
"version.Engine cannot be null!");
94 throw new InvalidOperationException(
"version.CustomIteration cannot be 0!");
109 this.logger =
logger ??
throw new ArgumentNullException(nameof(
logger));
111 installedVersions =
new Dictionary<EngineVersion, ReferenceCountingContainer<IEngineInstallation, EngineExecutableLock>>();
123 Stream? customVersionStream,
124 bool allowInstallation,
125 CancellationToken cancellationToken)
142 var stringVersion = version.
ToString();
148 ActiveVersion?.ToString(),
156 logger.LogInformation(
"Active version changed to {version}", version);
163 public async ValueTask<IEngineExecutableLock>
UseExecutables(
EngineVersion? requiredVersion,
string? trustDmbFullPath, CancellationToken cancellationToken)
166 "Acquiring lock on BYOND version {version}...",
167 requiredVersion?.ToString() ?? $
"{ActiveVersion} (active)");
176 requiredVersion !=
null,
181 if (trustDmbFullPath !=
null)
188 installLock.Dispose();
196 ArgumentNullException.ThrowIfNull(progressReporter);
200 logger.LogTrace(
"DeleteVersion {version}", version);
203 if (activeVersion !=
null && version.
Equals(activeVersion))
207 logger.LogTrace(
"Waiting to acquire installedVersions lock...");
212 logger.LogTrace(
"Version {version} already deleted.", version);
216 container = containerNullable;
217 logger.LogTrace(
"Installation container acquired for deletion");
220 progressReporter.StageName =
"Waiting for version to not be in use...";
226 Task activeVersionUpdate;
230 logger.LogTrace(
"Waiting for container.OnZeroReferences or switch of active version...");
234 .WaitAsync(cancellationToken);
236 if (containerTask.IsCompleted)
237 logger.LogTrace(
"All locks for version {version} are gone", version);
239 logger.LogTrace(
"activeVersion changed, we may have to wait again. Acquiring semaphore...");
245 if (activeVersion !=
null && version.
Equals(activeVersion))
249 logger.LogTrace(
"Locking installedVersions...");
255 logger.LogWarning(
"Unable to remove engine installation {version} from list! Is there a duplicate job running?", version);
258 if (container != newerContainer)
263 logger.LogDebug(
"Extreme race condition encountered, applying concentrated copium...");
264 container = newerContainer;
270 logger.LogTrace(
"Proceeding with installation deletion...");
278 logger.LogInformation(
"Deleting version {version}...", version);
279 progressReporter.StageName =
"Deleting installation...";
282 var installPath = version.
ToString();
290 if (containerTask.IsCompleted)
292 "Another lock was acquired before we could remove version {version} from the list. We will have to wait again.",
295 logger.LogTrace(
"Not proceeding for some reason or another");
301 public async Task
StartAsync(CancellationToken cancellationToken)
303 async ValueTask<byte[]?> GetActiveVersion()
309 var activeVersionBytesTask = GetActiveVersion();
314 var installedVersionPaths =
new Dictionary<string, EngineVersion>();
316 async ValueTask ReadVersion(
string path)
327 var text = Encoding.UTF8.GetString(bytes);
336 version = versionNullable!;
341 logger.LogDebug(
"Added detected BYOND version {versionKey}...", version);
347 "It seems that there are multiple directories that say they contain BYOND version {version}. We're ignoring and cleaning the duplicate: {duplicatePath}",
354 lock (installedVersionPaths)
360 .Select(ReadVersion));
362 logger.LogTrace(
"Upgrading BYOND installations...");
364 installedVersionPaths
367 var activeVersionBytes = await activeVersionBytesTask;
368 if (activeVersionBytes !=
null)
370 var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
373 bool hasRequestedActiveVersion;
378 if (hasRequestedActiveVersion)
382 logger.LogWarning(
"Failed to load saved active version {activeVersion}!", activeVersionString);
389 public Task
StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
404 Stream? customVersionStream,
406 bool allowInstallation,
407 CancellationToken cancellationToken)
409 var ourTcs =
new TaskCompletionSource();
412 bool installedOrInstalling;
415 if (customVersionStream !=
null)
417 var customInstallationNumber = 1;
420 version.CustomIteration = customInstallationNumber++;
425 installedOrInstalling =
installedVersions.TryGetValue(version, out var installationContainerNullable);
427 if (!installedOrInstalling)
429 if (!allowInstallation)
430 throw new InvalidOperationException($
"Engine version {version} not installed!");
438 installationContainer = installationContainerNullable!;
440 installation = installationContainer.
Instance;
444 var deploymentPipelineProcesses = !neededForLock;
447 if (installedOrInstalling)
449 progressReporter.StageName =
"Waiting for existing installation job...";
452 logger.LogWarning(
"The required engine version ({version}) is not readily available! We will have to wait for it to install.", version);
459 string? installPath =
null;
462 if (customVersionStream !=
null)
463 logger.LogInformation(
"Installing custom engine version as {version}...", version);
464 else if (neededForLock)
469 logger.LogWarning(
"The required engine version ({version}) is not readily available! We will have to install it.", version);
472 logger.LogInformation(
"Requested engine version {version} not currently installed. Doing so now...", version);
474 progressReporter.StageName =
"Running event";
476 var versionString = version.
ToString();
479 installPath = await
InstallVersionFiles(progressReporter, version, customVersionStream, deploymentPipelineProcesses, cancellationToken);
486 if (installPath !=
null)
490 logger.LogDebug(
"Cleaning up failed installation at {path}...", installPath);
495 logger.LogError(ex2,
"Error cleaning up failed installation!");
498 else if (ex is not OperationCanceledException)
504 ourTcs.SetException(ex);
529 Stream? customVersionStream,
530 bool deploymentPipelineProcesses,
531 CancellationToken cancellationToken)
534 async ValueTask DirectoryCleanup()
540 var directoryCleanupTask = DirectoryCleanup();
544 var remainingProgress = 1.0;
545 if (customVersionStream ==
null)
547 using var subReporter = progressReporter.
CreateSection(
"Downloading Version", 0.5);
548 remainingProgress -= 0.5;
552#pragma warning disable CA2000
555 customVersionStream);
556#pragma warning restore CA2000
561 remainingReporter = progressReporter.
CreateSection(
null, remainingProgress);
565 await engineInstallationData.DisposeAsync();
569 using (remainingReporter)
571 await
using (engineInstallationData)
573 remainingReporter.StageName =
"Cleaning target directory";
575 await directoryCleanupTask;
577 remainingReporter.StageName =
"Extracting data";
579 logger.LogTrace(
"Extracting engine to {extractPath}...", installFullPath);
580 await engineInstallationData.
ExtractToPath(installFullPath, cancellationToken);
584 remainingReporter.StageName =
"Running installation actions";
589 remainingReporter.StageName =
"Writing version file";
594 Encoding.UTF8.GetBytes(version.
ToString()),
598 catch (HttpRequestException ex)
603 catch (OperationCanceledException)
613 return installFullPath;
632 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.
EngineManager(IIOManager ioManager, IEngineInstaller engineInstaller, IEventConsumer eventConsumer, ILogger< EngineManager > logger)
Initializes a new instance of the EngineManager class.
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 .
readonly IEngineInstaller engineInstaller
The IEngineInstaller for the EngineManager.
readonly ILogger< EngineManager > logger
The ILogger for the EngineManager.
ReferenceCountingContainer< IEngineInstallation, EngineExecutableLock > AddInstallationContainer(EngineVersion version, string installPath, Task installationTask)
Create and add a new IEngineInstallation to installedVersions.
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 .
Wraps data containing an engine installation.
Task 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.
For downloading and installing game engines for a given system.
IEngineInstallation CreateInstallation(EngineVersion version, string path, Task installationTask)
Creates an IEngineInstallation for a given version .
ValueTask< IEngineInstallationData > DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken)
Download a given engine version .
ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken)
Does actions necessary to get upgrade a version installed by a previous version of TGS.
ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
Does actions necessary to get an extracted installation working.
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 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.