tgstation-server 6.12.3
The /tg/station 13 server suite
Loading...
Searching...
No Matches
EngineManager.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Linq;
5using System.Net.Http;
6using System.Text;
7using System.Threading;
8using System.Threading.Tasks;
9
10using Microsoft.Extensions.Logging;
11
18
20{
23 {
27 const string VersionFileName = "Version.txt";
28
32 const string ActiveVersionFileName = "ActiveVersion.txt";
33
35 public EngineVersion? ActiveVersion { get; private set; }
36
38 public IReadOnlyList<EngineVersion> InstalledVersions
39 {
40 get
41 {
43 return installedVersions.Keys.ToList();
44 }
45 }
46
51
56
61
65 readonly ILogger<EngineManager> logger;
66
70 readonly Dictionary<EngineVersion, ReferenceCountingContainer<IEngineInstallation, EngineExecutableLock>> installedVersions;
71
75 readonly SemaphoreSlim changeDeleteSemaphore;
76
80 volatile TaskCompletionSource activeVersionChanged;
81
87 {
88 ArgumentNullException.ThrowIfNull(version);
89
90 if (!version.Engine.HasValue)
91 throw new InvalidOperationException("version.Engine cannot be null!");
92
93 if (version.CustomIteration == 0)
94 throw new InvalidOperationException("version.CustomIteration cannot be 0!");
95 }
96
105 {
106 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
107 this.engineInstaller = engineInstaller ?? throw new ArgumentNullException(nameof(engineInstaller));
108 this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
109 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
110
111 installedVersions = new Dictionary<EngineVersion, ReferenceCountingContainer<IEngineInstallation, EngineExecutableLock>>();
112 changeDeleteSemaphore = new SemaphoreSlim(1);
113 activeVersionChanged = new TaskCompletionSource();
114 }
115
117 public void Dispose() => changeDeleteSemaphore.Dispose();
118
120 public async ValueTask ChangeVersion(
121 JobProgressReporter progressReporter,
122 EngineVersion version,
123 Stream? customVersionStream,
124 bool allowInstallation,
125 CancellationToken cancellationToken)
126 {
127 CheckVersionParameter(version);
128
129 using (await SemaphoreSlimContext.Lock(changeDeleteSemaphore, cancellationToken))
130 {
131 using var installLock = await AssertAndLockVersion(
132 progressReporter,
133 version,
134 customVersionStream,
135 false,
136 allowInstallation,
137 cancellationToken);
138
139 // We reparse the version because it could be changed after a custom install.
140 version = new EngineVersion(installLock.Version);
141
142 var stringVersion = version.ToString();
143 await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(stringVersion), cancellationToken);
145 EventType.EngineActiveVersionChange,
146 new List<string?>
147 {
148 ActiveVersion?.ToString(),
149 stringVersion,
150 },
151 false,
152 cancellationToken);
153
154 ActiveVersion = version;
155
156 logger.LogInformation("Active version changed to {version}", version);
157 var oldTcs = Interlocked.Exchange(ref activeVersionChanged, new TaskCompletionSource());
158 oldTcs.SetResult();
159 }
160 }
161
163 public async ValueTask<IEngineExecutableLock> UseExecutables(EngineVersion? requiredVersion, string? trustDmbFullPath, CancellationToken cancellationToken)
164 {
165 logger.LogTrace(
166 "Acquiring lock on BYOND version {version}...",
167 requiredVersion?.ToString() ?? $"{ActiveVersion} (active)");
168 var versionToUse = requiredVersion ?? ActiveVersion ?? throw new JobException(ErrorCode.EngineNoVersionsInstalled);
169
170 using var progressReporter = new JobProgressReporter();
171
172 var installLock = await AssertAndLockVersion(
173 progressReporter,
174 versionToUse,
175 null,
176 requiredVersion != null,
177 true,
178 cancellationToken);
179 try
180 {
181 if (trustDmbFullPath != null)
182 await engineInstaller.TrustDmbPath(installLock.Version, trustDmbFullPath, cancellationToken);
183
184 return installLock;
185 }
186 catch
187 {
188 installLock.Dispose();
189 throw;
190 }
191 }
192
194 public async ValueTask DeleteVersion(JobProgressReporter progressReporter, EngineVersion version, CancellationToken cancellationToken)
195 {
196 ArgumentNullException.ThrowIfNull(progressReporter);
197
198 CheckVersionParameter(version);
199
200 logger.LogTrace("DeleteVersion {version}", version);
201
202 var activeVersion = ActiveVersion;
203 if (activeVersion != null && version.Equals(activeVersion))
204 throw new JobException(ErrorCode.EngineCannotDeleteActiveVersion);
205
207 logger.LogTrace("Waiting to acquire installedVersions lock...");
208 lock (installedVersions)
209 {
210 if (!installedVersions.TryGetValue(version, out var containerNullable))
211 {
212 logger.LogTrace("Version {version} already deleted.", version);
213 return;
214 }
215
216 container = containerNullable;
217 logger.LogTrace("Installation container acquired for deletion");
218 }
219
220 progressReporter.StageName = "Waiting for version to not be in use...";
221 while (true)
222 {
223 var containerTask = container.OnZeroReferences;
224
225 // We also want to check when the active version changes in case we need to fail the job because of that.
226 Task activeVersionUpdate;
227 using (await SemaphoreSlimContext.Lock(changeDeleteSemaphore, cancellationToken))
228 activeVersionUpdate = activeVersionChanged.Task;
229
230 logger.LogTrace("Waiting for container.OnZeroReferences or switch of active version...");
231 await Task.WhenAny(
232 containerTask,
233 activeVersionUpdate)
234 .WaitAsync(cancellationToken);
235
236 if (containerTask.IsCompleted)
237 logger.LogTrace("All locks for version {version} are gone", version);
238 else
239 logger.LogTrace("activeVersion changed, we may have to wait again. Acquiring semaphore...");
240
241 using (await SemaphoreSlimContext.Lock(changeDeleteSemaphore, cancellationToken))
242 {
243 // check again because it could have become the active version.
244 activeVersion = ActiveVersion;
245 if (activeVersion != null && version.Equals(activeVersion))
246 throw new JobException(ErrorCode.EngineCannotDeleteActiveVersion);
247
248 bool proceed;
249 logger.LogTrace("Locking installedVersions...");
250 lock (installedVersions)
251 {
252 proceed = container.OnZeroReferences.IsCompleted;
253 if (proceed)
254 if (!installedVersions.TryGetValue(version, out var newerContainer))
255 logger.LogWarning("Unable to remove engine installation {version} from list! Is there a duplicate job running?", version);
256 else
257 {
258 if (container != newerContainer)
259 {
260 // Okay let me get this straight, there was a duplicate delete job, it ran before us after we grabbed the container, AND another installation of the same version completed?
261 // I know realistically this is practically impossible, but god damn that small possiblility
262 // best thing to do is check we exclusively own the newer container
263 logger.LogDebug("Extreme race condition encountered, applying concentrated copium...");
264 container = newerContainer;
265 proceed = container.OnZeroReferences.IsCompleted;
266 }
267
268 if (proceed)
269 {
270 logger.LogTrace("Proceeding with installation deletion...");
271 installedVersions.Remove(version);
272 }
273 }
274 }
275
276 if (proceed)
277 {
278 logger.LogInformation("Deleting version {version}...", version);
279 progressReporter.StageName = "Deleting installation...";
280
281 // delete the version file first, because we will know not to re-discover the installation if it's not present and it will get cleaned on reboot
282 var installPath = version.ToString();
283 await ioManager.DeleteFile(
285 cancellationToken);
286 await ioManager.DeleteDirectory(installPath, cancellationToken);
287 return;
288 }
289
290 if (containerTask.IsCompleted)
291 logger.LogDebug(
292 "Another lock was acquired before we could remove version {version} from the list. We will have to wait again.",
293 version);
294 else
295 logger.LogTrace("Not proceeding for some reason or another");
296 }
297 }
298 }
299
301 public async Task StartAsync(CancellationToken cancellationToken)
302 {
303 async ValueTask<byte[]?> GetActiveVersion()
304 {
305 var activeVersionFileExists = await ioManager.FileExists(ActiveVersionFileName, cancellationToken);
306 return !activeVersionFileExists ? null : await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken);
307 }
308
309 var activeVersionBytesTask = GetActiveVersion();
310
312 var directories = await ioManager.GetDirectories(DefaultIOManager.CurrentDirectory, cancellationToken);
313
314 var installedVersionPaths = new Dictionary<string, EngineVersion>();
315
316 async ValueTask ReadVersion(string path)
317 {
318 var versionFile = ioManager.ConcatPath(path, VersionFileName);
319 if (!await ioManager.FileExists(versionFile, cancellationToken))
320 {
321 logger.LogWarning("Cleaning path with no version file: {versionPath}", ioManager.ResolvePath(path));
322 await ioManager.DeleteDirectory(path, cancellationToken); // cleanup
323 return;
324 }
325
326 var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken);
327 var text = Encoding.UTF8.GetString(bytes);
328 EngineVersion version;
329 if (!EngineVersion.TryParse(text, out var versionNullable))
330 {
331 logger.LogWarning("Cleaning path with unparsable version file: {versionPath}", ioManager.ResolvePath(path));
332 await ioManager.DeleteDirectory(path, cancellationToken); // cleanup
333 return;
334 }
335 else
336 version = versionNullable!;
337
338 try
339 {
340 var installation = await engineInstaller.CreateInstallation(version, path, Task.CompletedTask, cancellationToken);
341 AddInstallationContainer(installation);
342 logger.LogDebug("Added detected BYOND version {versionKey}...", version);
343 }
344 catch (Exception ex)
345 {
346 logger.LogWarning(
347 ex,
348 "It seems that there are multiple directories that say they contain BYOND version {version}. We're ignoring and cleaning the duplicate: {duplicatePath}",
349 version,
350 ioManager.ResolvePath(path));
351 await ioManager.DeleteDirectory(path, cancellationToken);
352 return;
353 }
354
355 lock (installedVersionPaths)
356 installedVersionPaths.Add(ioManager.ResolvePath(version.ToString()), version);
357 }
358
360 directories
361 .Select(ReadVersion));
362
363 logger.LogTrace("Upgrading BYOND installations...");
365 installedVersionPaths
366 .Select(kvp => engineInstaller.UpgradeInstallation(kvp.Value, kvp.Key, cancellationToken)));
367
368 var activeVersionBytes = await activeVersionBytesTask;
369 if (activeVersionBytes != null)
370 {
371 var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
372
373 EngineVersion? activeVersion;
374 bool hasRequestedActiveVersion;
375 lock (installedVersions)
376 hasRequestedActiveVersion = EngineVersion.TryParse(activeVersionString, out activeVersion)
377 && installedVersions.ContainsKey(activeVersion!);
378
379 if (hasRequestedActiveVersion)
380 ActiveVersion = activeVersion; // not setting TCS because there's no need during init
381 else
382 {
383 logger.LogWarning("Failed to load saved active version {activeVersion}!", activeVersionString);
384 await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken);
385 }
386 }
387 }
388
390 public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
391
402 async ValueTask<EngineExecutableLock> AssertAndLockVersion(
403 JobProgressReporter progressReporter,
404 EngineVersion version,
405 Stream? customVersionStream,
406 bool neededForLock,
407 bool allowInstallation,
408 CancellationToken cancellationToken)
409 {
410 var ourTcs = new TaskCompletionSource();
411 IEngineInstallation installation;
412 EngineExecutableLock installLock;
413 bool installedOrInstalling;
414
415 // loop is because of the race condition with potentialInstallation, installedVersions, and CustomIteration selection
416 while (true)
417 {
418 lock (installedVersions)
419 {
420 if (customVersionStream != null)
421 {
422 var customInstallationNumber = 1;
423 do
424 {
425 version.CustomIteration = customInstallationNumber++;
426 }
427 while (installedVersions.ContainsKey(version));
428 }
429 }
430
431 var potentialInstallation = await engineInstaller.CreateInstallation(
432 version,
433 ioManager.ResolvePath(version.ToString()),
434 ourTcs.Task,
435 cancellationToken);
436
437 lock (installedVersions)
438 {
439 if (customVersionStream != null && installedVersions.ContainsKey(version))
440 continue;
441
442 installedOrInstalling = installedVersions.TryGetValue(version, out var installationContainerNullable);
444 if (!installedOrInstalling)
445 {
446 if (!allowInstallation)
447 throw new InvalidOperationException($"Engine version {version} not installed!");
448
449 installationContainer = AddInstallationContainer(potentialInstallation);
450 }
451 else
452 installationContainer = installationContainerNullable!;
453
454 installation = installationContainer.Instance;
455 installLock = installationContainer.AddReference();
456 }
457
458 var deploymentPipelineProcesses = !neededForLock;
459 try
460 {
461 if (installedOrInstalling)
462 {
463 progressReporter.StageName = "Waiting for existing installation job...";
464
465 if (neededForLock && !installation.InstallationTask.IsCompleted)
466 logger.LogWarning("The required engine version ({version}) is not readily available! We will have to wait for it to install.", version);
467
468 await installation.InstallationTask.WaitAsync(cancellationToken);
469 return installLock;
470 }
471
472 // okay up to us to install it then
473 string? installPath = null;
474 try
475 {
476 if (customVersionStream != null)
477 logger.LogInformation("Installing custom engine version as {version}...", version);
478 else if (neededForLock)
479 {
480 if (version.CustomIteration.HasValue)
481 throw new JobException(ErrorCode.EngineNonExistentCustomVersion);
482
483 logger.LogWarning("The required engine version ({version}) is not readily available! We will have to install it.", version);
484 }
485 else
486 logger.LogInformation("Requested engine version {version} not currently installed. Doing so now...", version);
487
488 progressReporter.StageName = "Running event";
489
490 var versionString = version.ToString();
491 await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List<string> { versionString }, deploymentPipelineProcesses, cancellationToken);
492
493 installPath = await InstallVersionFiles(progressReporter, version, customVersionStream, deploymentPipelineProcesses, cancellationToken);
494 await eventConsumer.HandleEvent(EventType.EngineInstallComplete, new List<string> { versionString }, deploymentPipelineProcesses, cancellationToken);
495
496 ourTcs.SetResult();
497 }
498 catch (Exception ex)
499 {
500 if (installPath != null)
501 {
502 try
503 {
504 logger.LogDebug("Cleaning up failed installation at {path}...", installPath);
505 await ioManager.DeleteDirectory(installPath, cancellationToken);
506 }
507 catch (Exception ex2)
508 {
509 logger.LogError(ex2, "Error cleaning up failed installation!");
510 }
511 }
512 else if (ex is not OperationCanceledException)
513 await eventConsumer.HandleEvent(EventType.EngineInstallFail, new List<string> { ex.Message }, deploymentPipelineProcesses, cancellationToken);
514
515 lock (installedVersions)
516 installedVersions.Remove(version);
517
518 ourTcs.SetException(ex);
519 throw;
520 }
521
522 return installLock;
523 }
524 catch
525 {
526 installLock.Dispose();
527 throw;
528 }
529 }
530 }
531
541 async ValueTask<string> InstallVersionFiles(
542 JobProgressReporter progressReporter,
543 EngineVersion version,
544 Stream? customVersionStream,
545 bool deploymentPipelineProcesses,
546 CancellationToken cancellationToken)
547 {
548 var installFullPath = ioManager.ResolvePath(version.ToString());
549 async ValueTask DirectoryCleanup()
550 {
551 await ioManager.DeleteDirectory(installFullPath, cancellationToken);
552 await ioManager.CreateDirectory(installFullPath, cancellationToken);
553 }
554
555 var directoryCleanupTask = DirectoryCleanup();
556 try
557 {
558 IEngineInstallationData engineInstallationData;
559 var remainingProgress = 1.0;
560 if (customVersionStream == null)
561 {
562 using var subReporter = progressReporter.CreateSection("Downloading Version", 0.5);
563 remainingProgress -= 0.5;
564 engineInstallationData = await engineInstaller.DownloadVersion(version, subReporter, cancellationToken);
565 }
566 else
567#pragma warning disable CA2000 // Dispose objects before losing scope, false positive
568 engineInstallationData = new ZipStreamEngineInstallationData(
569 ioManager,
570 customVersionStream);
571#pragma warning restore CA2000 // Dispose objects before losing scope
572
573 JobProgressReporter remainingReporter;
574 try
575 {
576 remainingReporter = progressReporter.CreateSection(null, remainingProgress);
577 }
578 catch
579 {
580 await engineInstallationData.DisposeAsync();
581 throw;
582 }
583
584 using (remainingReporter)
585 {
586 await using (engineInstallationData)
587 {
588 remainingReporter.StageName = "Cleaning target directory";
589
590 await directoryCleanupTask;
591 remainingReporter.ReportProgress(0.1);
592 remainingReporter.StageName = "Extracting data";
593
594 logger.LogTrace("Extracting engine to {extractPath}...", installFullPath);
595 await engineInstallationData.ExtractToPath(installFullPath, cancellationToken);
596 remainingReporter.ReportProgress(0.3);
597 }
598
599 remainingReporter.StageName = "Running installation actions";
600
601 await engineInstaller.Install(version, installFullPath, deploymentPipelineProcesses, cancellationToken);
602
603 remainingReporter.ReportProgress(0.9);
604 remainingReporter.StageName = "Writing version file";
605
606 // make sure to do this last because this is what tells us we have a valid version in the future
608 ioManager.ConcatPath(installFullPath, VersionFileName),
609 Encoding.UTF8.GetBytes(version.ToString()),
610 cancellationToken);
611 }
612 }
613 catch (HttpRequestException ex)
614 {
615 // since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
616 throw new JobException(ErrorCode.EngineDownloadFail, ex);
617 }
618 catch (OperationCanceledException)
619 {
620 throw;
621 }
622 catch
623 {
624 await ioManager.DeleteDirectory(installFullPath, cancellationToken);
625 throw;
626 }
627
628 return installFullPath;
629 }
630
637 {
638 var installationContainer = new ReferenceCountingContainer<IEngineInstallation, EngineExecutableLock>(installation);
639
640 lock (installedVersions)
641 installedVersions.Add(installation.Version, installationContainer);
642
643 return installationContainer;
644 }
645 }
646}
Information about an engine installation.
static bool TryParse(string input, out EngineVersion? engineVersion)
Attempts to parse a stringified EngineVersion.
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 .
ReferenceCountingContainer< IEngineInstallation, EngineExecutableLock > AddInstallationContainer(IEngineInstallation installation)
Create and add a new IEngineInstallation to installedVersions.
readonly IEngineInstaller engineInstaller
The IEngineInstaller for the EngineManager.
readonly ILogger< EngineManager > logger
The ILogger 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.
JobProgressReporter CreateSection(string? newStageName, double percentage)
Create a subsection of the JobProgressReporter with its optional own stage name.
void ReportProgress(double? progress)
Report progress.
TReference AddReference()
Create a new TReference to the Instance.
Task OnZeroReferences
A Task that completes when there are no TReference s active for the Instance.
static async ValueTask< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken, ILogger? logger=null)
Asyncronously locks a semaphore .
Task ExtractToPath(string path, CancellationToken cancellationToken)
Extracts the installation to a given path.
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 UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken)
Does actions necessary to get upgrade a version installed by a previous version of TGS.
ValueTask< IEngineInstallation > CreateInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken)
Creates an IEngineInstallation for a given version .
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.
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.
Definition IIOManager.cs:13
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.
Definition ErrorCode.cs:12
EventType
Types of events. Mirror in tgs.dm. Prefer last listed name for script.
Definition EventType.cs:7