tgstation-server 6.11.1
The /tg/station 13 server suite
Loading...
Searching...
No Matches
Configuration.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Globalization;
4using System.IO;
5using System.Linq;
6using System.Security.Cryptography;
7using System.Text;
8using System.Threading;
9using System.Threading.Tasks;
10
11using Microsoft.Extensions.Logging;
12
25
27{
30 {
34 const string CodeModificationsSubdirectory = "CodeModifications";
35
39 const string EventScriptsSubdirectory = "EventScripts";
40
44 const string GameStaticFilesSubdirectory = "GameStaticFiles";
45
49 const string StaticIgnoreFile = ".tgsignore";
50
54 const string CodeModificationsHeadFile = "HeadInclude.dm";
55
59 const string CodeModificationsTailFile = "TailInclude.dm";
60
64 static readonly string DefaultHeadInclude = @$"// TGS AUTO GENERATED HeadInclude.dm{Environment.NewLine}// This file will be included BEFORE all code in your .dme IF a replacement .dme does not exist in this directory{Environment.NewLine}// Please note that changes need to be made available if you are hosting an AGPL licensed codebase{Environment.NewLine}// The presence file in its default state does not constitute a code change that needs to be published by licensing standards{Environment.NewLine}";
65
69 static readonly string DefaultTailInclude = @$"// TGS AUTO GENERATED TailInclude.dm{Environment.NewLine}// This file will be included AFTER all code in your .dme IF a replacement .dme does not exist in this directory{Environment.NewLine}// Please note that changes need to be made available if you are hosting an AGPL licensed codebase{Environment.NewLine}// The presence file in its default state does not constitute a code change that needs to be published by licensing standards{Environment.NewLine}";
70
74 public static IReadOnlyDictionary<EventType, string[]> EventTypeScriptFileNameMap { get; } = new Dictionary<EventType, string[]>(
75 Enum.GetValues(typeof(EventType))
76 .Cast<EventType>()
77 .Select(
78 eventType => new KeyValuePair<EventType, string[]>(
79 eventType,
80 typeof(EventType)
81 .GetField(eventType.ToString())!
82 .GetCustomAttributes(false)
83 .OfType<EventScriptAttribute>()
84 .First()
85 .ScriptNames)));
86
91
96
101
106
111
116
121
125 readonly ILogger<Configuration> logger;
126
131
136
140 readonly SemaphoreSlim semaphore;
141
145 readonly CancellationTokenSource disposeCts;
146
151
173 ILogger<Configuration> logger,
176 {
177 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
178 this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
179 this.linkFactory = linkFactory ?? throw new ArgumentNullException(nameof(linkFactory));
180 this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
181 this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
182 this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
183 this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
184 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
185 this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration));
186 this.sessionConfiguration = sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration));
187
188 semaphore = new SemaphoreSlim(1);
189 disposeCts = new CancellationTokenSource();
190 uploadTasks = Task.CompletedTask;
191 }
192
194 public void Dispose()
195 {
196 semaphore.Dispose();
197 disposeCts.Cancel();
198 disposeCts.Dispose();
199 }
200
202 public async ValueTask<ServerSideModifications?> CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken)
203 {
204 using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
205 {
206 var ensureDirectoriesTask = EnsureDirectories(cancellationToken);
207
208 // just assume no other fs race conditions here
209 var dmeExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, dmeFile), cancellationToken);
212
213 await ensureDirectoriesTask;
214 var copyTask = ioManager.CopyDirectory(
215 null,
216 null,
218 destination,
219 generalConfiguration.GetCopyDirectoryTaskThrottle(),
220 cancellationToken);
221
222 await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask.AsTask());
223
224 if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result)
225 return null;
226
227 if (dmeExistsTask.Result)
228 return new ServerSideModifications(null, null, true);
229
230 if (!headFileExistsTask.Result && !tailFileExistsTask.Result)
231 return null;
232
233 static string IncludeLine(string filePath) => String.Format(CultureInfo.InvariantCulture, "#include \"{0}\"", filePath);
234
235 return new ServerSideModifications(
236 headFileExistsTask.Result
237 ? IncludeLine(CodeModificationsHeadFile)
238 : null,
239 tailFileExistsTask.Result
240 ? IncludeLine(CodeModificationsTailFile)
241 : null,
242 false);
243 }
244 }
245
247 public async ValueTask<IOrderedQueryable<ConfigurationFileResponse>?> ListDirectory(string? configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
248 {
249 await EnsureDirectories(cancellationToken);
250 var path = ValidateConfigRelativePath(configurationRelativePath);
251
252 configurationRelativePath ??= "/";
253
254 var result = new List<ConfigurationFileResponse>();
255
256 void ListImpl()
257 {
258 var enumerator = synchronousIOManager.GetDirectories(path, cancellationToken);
259 result.AddRange(enumerator.Select(x => new ConfigurationFileResponse
260 {
261 IsDirectory = true,
262 Path = ioManager.ConcatPath(configurationRelativePath, x),
263 }));
264
265 enumerator = synchronousIOManager.GetFiles(path, cancellationToken);
266 result.AddRange(enumerator.Select(x => new ConfigurationFileResponse
267 {
268 IsDirectory = false,
269 Path = ioManager.ConcatPath(configurationRelativePath, x),
270 }));
271 }
272
273 using (SemaphoreSlimContext.TryLock(semaphore, out var locked))
274 {
275 if (!locked)
276 {
277 logger.LogDebug("Contention when attempting to enumerate directory!");
278 return null;
279 }
280
281 if (systemIdentity == null)
282 ListImpl();
283 else
284 await systemIdentity.RunImpersonated(ListImpl, cancellationToken);
285 }
286
287 return result
288 .AsQueryable()
289 .OrderBy(configFile => !configFile.IsDirectory)
290 .ThenBy(configFile => configFile.Path);
291 }
292
294 public async ValueTask<ConfigurationFileResponse?> Read(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
295 {
296 await EnsureDirectories(cancellationToken);
297 var path = ValidateConfigRelativePath(configurationRelativePath);
298
299 ConfigurationFileResponse? result = null;
300
301 void ReadImpl()
302 {
303 try
304 {
305 string GetFileSha()
306 {
307 var content = synchronousIOManager.ReadFile(path);
308 return String.Join(String.Empty, SHA1.HashData(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
309 }
310
311 var originalSha = GetFileSha();
312
313 var disposeToken = disposeCts.Token;
314 var fileTicket = fileTransferService.CreateDownload(
316 () =>
317 {
318 if (disposeToken.IsCancellationRequested)
319 return ErrorCode.InstanceOffline;
320
321 var newSha = GetFileSha();
322 if (newSha != originalSha)
323 return ErrorCode.ConfigurationFileUpdated;
324
325 return null;
326 },
327 async cancellationToken =>
328 {
329 FileStream? result = null;
330 void GetFileStream()
331 {
332 result = ioManager.GetFileStream(path, false);
333 }
334
335 if (systemIdentity == null)
336 await Task.Factory.StartNew(GetFileStream, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
337 else
338 await systemIdentity.RunImpersonated(GetFileStream, cancellationToken);
339
340 return result!;
341 },
342 path,
343 false));
344
345 result = new ConfigurationFileResponse
346 {
347 FileTicket = fileTicket.FileTicket,
348 IsDirectory = false,
349 LastReadHash = originalSha,
350 AccessDenied = false,
351 Path = configurationRelativePath,
352 };
353 }
354 catch (UnauthorizedAccessException)
355 {
356 // this happens on windows, dunno about linux
357 bool isDirectory;
358 try
359 {
360 isDirectory = synchronousIOManager.IsDirectory(path);
361 }
362 catch (Exception ex)
363 {
364 logger.LogDebug(ex, "IsDirectory exception!");
365 isDirectory = false;
366 }
367
368 result = new ConfigurationFileResponse
369 {
370 Path = configurationRelativePath,
371 };
372 if (!isDirectory)
373 result.AccessDenied = true;
374
375 result.IsDirectory = isDirectory;
376 }
377 }
378
379 using (SemaphoreSlimContext.TryLock(semaphore, out var locked))
380 {
381 if (!locked)
382 {
383 logger.LogDebug("Contention when attempting to read file!");
384 return null;
385 }
386
387 if (systemIdentity == null)
388 await Task.Factory.StartNew(ReadImpl, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
389 else
390 await systemIdentity.RunImpersonated(ReadImpl, cancellationToken);
391 }
392
393 return result;
394 }
395
397 public async ValueTask SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken)
398 {
399 List<string> ignoreFiles;
400
401 async ValueTask SymlinkBase(bool files)
402 {
403 Task<IReadOnlyList<string>> task;
404 if (files)
405 task = ioManager.GetFiles(GameStaticFilesSubdirectory, cancellationToken);
406 else
407 task = ioManager.GetDirectories(GameStaticFilesSubdirectory, cancellationToken);
408 var entries = await task;
409
410 await ValueTaskExtensions.WhenAll(entries.Select<string, ValueTask>(async file =>
411 {
412 var fileName = ioManager.GetFileName(file);
413
414 // need to normalize
415 var fileComparison = platformIdentifier.IsWindows
416 ? StringComparison.OrdinalIgnoreCase
417 : StringComparison.Ordinal;
418 var ignored = ignoreFiles.Any(y => fileName.Equals(y, fileComparison));
419 if (ignored)
420 {
421 logger.LogTrace("Ignoring static file {fileName}...", fileName);
422 return;
423 }
424
425 var destPath = ioManager.ConcatPath(destination, fileName);
426 logger.LogTrace("Symlinking {filePath} to {destPath}...", file, destPath);
427 var fileExistsTask = ioManager.FileExists(destPath, cancellationToken);
428 if (await ioManager.DirectoryExists(destPath, cancellationToken))
429 await ioManager.DeleteDirectory(destPath, cancellationToken);
430 var fileExists = await fileExistsTask;
431 if (fileExists)
432 await ioManager.DeleteFile(destPath, cancellationToken);
433 await linkFactory.CreateSymbolicLink(ioManager.ResolvePath(file), ioManager.ResolvePath(destPath), cancellationToken);
434 }));
435 }
436
437 using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
438 {
439 await EnsureDirectories(cancellationToken);
440 var ignoreFileBytes = await ioManager.ReadAllBytes(StaticIgnorePath(), cancellationToken);
441 var ignoreFileText = Encoding.UTF8.GetString(ignoreFileBytes);
442
443 ignoreFiles = new List<string> { StaticIgnoreFile };
444
445 // we don't want to lose trailing whitespace on linux
446 using (var reader = new StringReader(ignoreFileText))
447 {
448 cancellationToken.ThrowIfCancellationRequested();
449 var line = await reader.ReadLineAsync(cancellationToken);
450 if (!String.IsNullOrEmpty(line))
451 ignoreFiles.Add(line);
452 }
453
454 var filesSymlinkTask = SymlinkBase(true);
455 var dirsSymlinkTask = SymlinkBase(false);
456 await ValueTaskExtensions.WhenAll(filesSymlinkTask, dirsSymlinkTask);
457 }
458 }
459
461 public async ValueTask<ConfigurationFileResponse?> Write(string configurationRelativePath, ISystemIdentity? systemIdentity, string? previousHash, CancellationToken cancellationToken)
462 {
463 await EnsureDirectories(cancellationToken);
464 var path = ValidateConfigRelativePath(configurationRelativePath);
465
466 ConfigurationFileResponse? result = null;
467
468 void WriteImpl()
469 {
470 try
471 {
472 var fileTicket = fileTransferService.CreateUpload(FileUploadStreamKind.ForSynchronousIO);
473 var uploadCancellationToken = disposeCts.Token;
474 async Task UploadHandler()
475 {
476 await using (fileTicket)
477 {
478 var fileHash = previousHash;
479 var uploadStream = await fileTicket.GetResult(uploadCancellationToken);
480 if (uploadStream == null)
481 return; // expired
482
483 bool success = false;
484 void WriteCallback()
485 {
486 success = synchronousIOManager.WriteFileChecked(path, uploadStream, ref fileHash, cancellationToken);
487 }
488
489 if (fileTicket == null)
490 {
491 logger.LogDebug("File upload ticket for {path} expired!", path);
492 return;
493 }
494
495 using (SemaphoreSlimContext.TryLock(semaphore, out var locked))
496 {
497 if (!locked)
498 {
499 fileTicket.SetError(ErrorCode.ConfigurationContendedAccess, null);
500 return;
501 }
502
503 if (systemIdentity == null)
504 await Task.Factory.StartNew(WriteCallback, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
505 else
506 await systemIdentity.RunImpersonated(WriteCallback, cancellationToken);
507 }
508
509 if (!success)
510 fileTicket.SetError(ErrorCode.ConfigurationFileUpdated, fileHash);
511 else if (uploadStream.Length > 0)
512 postWriteHandler.HandleWrite(path);
513 }
514 }
515
516 result = new ConfigurationFileResponse
517 {
518 FileTicket = fileTicket.Ticket.FileTicket,
519 LastReadHash = previousHash,
520 IsDirectory = false,
521 AccessDenied = false,
522 Path = configurationRelativePath,
523 };
524
525 lock (disposeCts)
526 uploadTasks = Task.WhenAll(uploadTasks, UploadHandler());
527 }
528 catch (UnauthorizedAccessException)
529 {
530 // this happens on windows, dunno about linux
531 bool isDirectory;
532 try
533 {
534 isDirectory = synchronousIOManager.IsDirectory(path);
535 }
536 catch (Exception ex)
537 {
538 logger.LogDebug(ex, "IsDirectory exception!");
539 isDirectory = false;
540 }
541
542 result = new ConfigurationFileResponse
543 {
544 Path = configurationRelativePath,
545 };
546 if (!isDirectory)
547 result.AccessDenied = true;
548
549 result.IsDirectory = isDirectory;
550 }
551 }
552
553 using (SemaphoreSlimContext.TryLock(semaphore, out var locked))
554 {
555 if (!locked)
556 {
557 logger.LogDebug("Contention when attempting to write file!");
558 return null;
559 }
560
561 if (systemIdentity == null)
562 await Task.Factory.StartNew(WriteImpl, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
563 else
564 await systemIdentity.RunImpersonated(WriteImpl, cancellationToken);
565 }
566
567 return result;
568 }
569
571 public async ValueTask<bool?> CreateDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
572 {
573 await EnsureDirectories(cancellationToken);
574 var path = ValidateConfigRelativePath(configurationRelativePath);
575
576 bool? result = null;
577 void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken);
578
579 using (SemaphoreSlimContext.TryLock(semaphore, out var locked))
580 {
581 if (!locked)
582 {
583 logger.LogDebug("Contention when attempting to create directory!");
584 return null;
585 }
586
587 if (systemIdentity == null)
588 await Task.Factory.StartNew(DoCreate, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
589 else
590 await systemIdentity.RunImpersonated(DoCreate, cancellationToken);
591 }
592
593 return result!.Value;
594 }
595
597 public Task StartAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
598
600 public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
601
603 public ValueTask HandleEvent(EventType eventType, IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
604 {
605 ArgumentNullException.ThrowIfNull(parameters);
606
607 if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptNames))
608 {
609 logger.LogTrace("No event script for event {event}!", eventType);
610 return ValueTask.CompletedTask;
611 }
612
613 return ExecuteEventScripts(parameters, deploymentPipeline, cancellationToken, scriptNames);
614 }
615
617 public ValueTask? HandleCustomEvent(string scriptName, IEnumerable<string?> parameters, CancellationToken cancellationToken)
618 {
619 var scriptNameIsTgsEventName = EventTypeScriptFileNameMap
620 .Values
621 .SelectMany(scriptNames => scriptNames)
622 .Any(tgsScriptName => tgsScriptName.Equals(
623 scriptName,
624 platformIdentifier.IsWindows
625 ? StringComparison.OrdinalIgnoreCase
626 : StringComparison.Ordinal));
627 if (scriptNameIsTgsEventName)
628 {
629 logger.LogWarning("DMAPI attempted to execute TGS reserved event: {eventName}", scriptName);
630 return null;
631 }
632
633#pragma warning disable CA2012 // Use ValueTasks correctly
634 return ExecuteEventScripts(parameters, false, cancellationToken, scriptName);
635#pragma warning restore CA2012 // Use ValueTasks correctly
636 }
637
639 public async ValueTask<bool?> DeleteDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
640 {
641 await EnsureDirectories(cancellationToken);
642 var path = ValidateConfigRelativePath(configurationRelativePath);
643
644 var result = false;
645 using (SemaphoreSlimContext.TryLock(semaphore, out var locked))
646 {
647 if (!locked)
648 {
649 logger.LogDebug("Contention when attempting to enumerate directory!");
650 return null;
651 }
652
653 void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path);
654
655 if (systemIdentity != null)
656 await systemIdentity.RunImpersonated(CheckDeleteImpl, cancellationToken);
657 else
658 CheckDeleteImpl();
659 }
660
661 return result;
662 }
663
668 string StaticIgnorePath() => ioManager.ConcatPath(GameStaticFilesSubdirectory, StaticIgnoreFile);
669
675 Task EnsureDirectories(CancellationToken cancellationToken)
676 {
677 async Task ValidateStaticFolder()
678 {
679 await ioManager.CreateDirectory(GameStaticFilesSubdirectory, cancellationToken);
680 var staticIgnorePath = StaticIgnorePath();
681 if (!await ioManager.FileExists(staticIgnorePath, cancellationToken))
682 await ioManager.WriteAllBytes(staticIgnorePath, Array.Empty<byte>(), cancellationToken);
683 }
684
685 async Task ValidateCodeModsFolder()
686 {
687 if (await ioManager.DirectoryExists(CodeModificationsSubdirectory, cancellationToken))
688 return;
689
690 await ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken);
691 var headWriteTask = ioManager.WriteAllBytes(
692 ioManager.ConcatPath(
693 CodeModificationsSubdirectory,
694 CodeModificationsHeadFile),
695 Encoding.UTF8.GetBytes(DefaultHeadInclude),
696 cancellationToken);
697 var tailWriteTask = ioManager.WriteAllBytes(
698 ioManager.ConcatPath(
699 CodeModificationsSubdirectory,
700 CodeModificationsTailFile),
701 Encoding.UTF8.GetBytes(DefaultTailInclude),
702 cancellationToken);
703 await ValueTaskExtensions.WhenAll(headWriteTask, tailWriteTask);
704 }
705
706 return Task.WhenAll(
707 ValidateCodeModsFolder(),
708 ioManager.CreateDirectory(EventScriptsSubdirectory, cancellationToken),
709 ValidateStaticFolder());
710 }
711
717 string ValidateConfigRelativePath(string? configurationRelativePath)
718 {
719 var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath);
720 if (nullOrEmptyCheck)
721 configurationRelativePath = DefaultIOManager.CurrentDirectory;
722 if (configurationRelativePath![0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar)
723 configurationRelativePath = DefaultIOManager.CurrentDirectory + configurationRelativePath;
724 var resolved = ioManager.ResolvePath(configurationRelativePath);
725 var local = !nullOrEmptyCheck ? ioManager.ResolvePath() : null;
726 if (!nullOrEmptyCheck && resolved.Length < local!.Length) // .. fuccbois
727 throw new InvalidOperationException("Attempted to access file outside of configuration manager!");
728 return resolved;
729 }
730
739 async ValueTask ExecuteEventScripts(IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken, params string[] scriptNames)
740 {
741 await EnsureDirectories(cancellationToken);
742
743 // always execute in serial
744 using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
745 {
746 var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, false, cancellationToken);
747 var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
748
749 var scriptFiles = files
750 .Select(x => ioManager.GetFileName(x))
751 .Where(x => scriptNames.Any(
752 scriptName => x.StartsWith(scriptName, StringComparison.Ordinal)))
753 .ToList();
754
755 if (scriptFiles.Count == 0)
756 {
757 logger.LogTrace("No event scripts starting with \"{scriptName}\" detected", String.Join("\" or \"", scriptNames));
758 return;
759 }
760
761 foreach (var scriptFile in scriptFiles)
762 {
763 logger.LogTrace("Running event script {scriptFile}...", scriptFile);
764 await using (var script = await processExecutor.LaunchProcess(
765 ioManager.ConcatPath(resolvedScriptsDir, scriptFile),
766 resolvedScriptsDir,
767 String.Join(
768 ' ',
769 parameters.Select(arg =>
770 {
771 if (arg == null)
772 return "(NULL)";
773
774 if (!arg.Contains(' ', StringComparison.Ordinal))
775 return arg;
776
777 arg = arg.Replace("\"", "\\\"", StringComparison.Ordinal);
778
779 return $"\"{arg}\"";
780 })),
781 cancellationToken,
782 readStandardHandles: true,
783 noShellExecute: true))
784 using (cancellationToken.Register(() => script.Terminate()))
785 {
786 if (sessionConfiguration.LowPriorityDeploymentProcesses && deploymentPipeline)
787 script.AdjustPriority(false);
788
789 var exitCode = await script.Lifetime;
790 cancellationToken.ThrowIfCancellationRequested();
791 var scriptOutput = await script.GetCombinedOutput(cancellationToken);
792 if (exitCode != 0)
793 throw new JobException($"Script {scriptFile} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}");
794 else
795 logger.LogDebug("Script output:{newLine}{scriptOutput}", Environment.NewLine, scriptOutput);
796 }
797 }
798 }
799 }
800 }
801}
virtual ? string FileTicket
The ticket to use to access the Routes.Transfer controller.
Extension methods for the ValueTask and ValueTask<TResult> classes.
static async ValueTask WhenAll(IEnumerable< ValueTask > tasks)
Fully await a given list of tasks .
Attribute for indicating the script that a given EventType runs.
string[] ScriptNames
The name and order of the scripts the event script the EventType runs.
readonly IProcessExecutor processExecutor
The IProcessExecutor for Configuration.
string ValidateConfigRelativePath(string? configurationRelativePath)
Resolve a given configurationRelativePath to it's full path or throw an InvalidOperationException if...
readonly IFileTransferTicketProvider fileTransferService
The IFileTransferTicketProvider for Configuration.
static readonly string DefaultTailInclude
Default contents of CodeModificationsHeadFile.
readonly ISynchronousIOManager synchronousIOManager
The ISynchronousIOManager for Configuration.
async ValueTask ExecuteEventScripts(IEnumerable< string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken, params string[] scriptNames)
Execute a set of given scriptNames .
Task StopAsync(CancellationToken cancellationToken)
const string StaticIgnoreFile
Name of the ignore file in GameStaticFilesSubdirectory.
Task EnsureDirectories(CancellationToken cancellationToken)
Ensures standard configuration directories exist.
readonly SessionConfiguration sessionConfiguration
The SessionConfiguration for Configuration.
string StaticIgnorePath()
Get the proper path to StaticIgnoreFile.
async ValueTask SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken)
Symlinks all directories in the GameData directory to destination .A ValueTask representing the runni...
readonly CancellationTokenSource disposeCts
The CancellationTokenSource that is triggered when IDisposable.Dispose is called.
Task uploadTasks
The culmination of all upload file transfer callbacks.
readonly IFilesystemLinkFactory linkFactory
The IFilesystemLinkFactory for Configuration.
const string CodeModificationsSubdirectory
The CodeModifications directory name.
readonly IPostWriteHandler postWriteHandler
The IPostWriteHandler for Configuration.
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for Configuration.
async ValueTask< ConfigurationFileResponse?> Write(string configurationRelativePath, ISystemIdentity? systemIdentity, string? previousHash, CancellationToken cancellationToken)
Writes to a given configurationRelativePath .A ValueTask<TResult> resulting in the updated Configurat...
const string CodeModificationsHeadFile
The HeadInclude.dm filename.
ValueTask HandleEvent(EventType eventType, IEnumerable< string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
Handle a given eventType .A ValueTask representing the running operation.
Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, IFilesystemLinkFactory linkFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IPlatformIdentifier platformIdentifier, IFileTransferTicketProvider fileTransferService, ILogger< Configuration > logger, GeneralConfiguration generalConfiguration, SessionConfiguration sessionConfiguration)
Initializes a new instance of the Configuration class.
readonly ILogger< Configuration > logger
The ILogger for Configuration.
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for Configuration.
async ValueTask< IOrderedQueryable< ConfigurationFileResponse >?> ListDirectory(string? configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
Get ConfigurationFileResponses for all items in a given configurationRelativePath ....
const string CodeModificationsTailFile
The TailInclude.dm filename.
async ValueTask< bool?> DeleteDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
Attempt to delete an empty directory at configurationRelativePath .A ValueTask<TResult> resulting in ...
ValueTask? HandleCustomEvent(string scriptName, IEnumerable< string?> parameters, CancellationToken cancellationToken)
Handles a given custom event.A ValueTask representing the running operation if the event was triggere...
async ValueTask< ServerSideModifications?> CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken)
Copies all files in the CodeModifications directory to destination .A ValueTask<TResult> resulting in...
async ValueTask< bool?> CreateDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
Create an empty directory at configurationRelativePath .A ValueTask<TResult> resulting in true if the...
static readonly string DefaultHeadInclude
Default contents of CodeModificationsHeadFile.
async ValueTask< ConfigurationFileResponse?> Read(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
Reads a given configurationRelativePath .A ValueTask<TResult> resulting in the ConfigurationFileRespo...
const string GameStaticFilesSubdirectory
The GameStaticFiles directory name.
Task StartAsync(CancellationToken cancellationToken)
const string EventScriptsSubdirectory
The EventScripts directory name.
readonly SemaphoreSlim semaphore
The SemaphoreSlim for Configuration. Also used as a lock object.
static IReadOnlyDictionary< EventType, string[]> EventTypeScriptFileNameMap
Map of EventTypes to the filename of the event scripts they trigger.
readonly IIOManager ioManager
The IIOManager for Configuration.
Configuration options for the game sessions.
IIOManager that resolves paths to Environment.CurrentDirectory.
const string CurrentDirectory
Path to the current working directory for the IIOManager.
const TaskCreationOptions BlockingTaskCreationOptions
The TaskCreationOptions used to spawn Tasks for potentially long running, blocking operations.
Operation exceptions thrown from the context of a Models.Job.
Represents a file on disk to be downloaded.
static async ValueTask< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore .
static ? SemaphoreSlimContext TryLock(SemaphoreSlim semaphore, out bool locked)
Asyncronously attempts to lock a semaphore .
Interface for using filesystems.
Definition IIOManager.cs:13
Task< IReadOnlyList< string > > GetFiles(string path, CancellationToken cancellationToken)
Returns full file names in a given path .
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 DeleteFile(string path, CancellationToken cancellationToken)
Deletes a file at path .
FileStream GetFileStream(string path, bool shareWrite)
Gets the Stream for a given file path .
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.
ValueTask CopyDirectory(IEnumerable< string >? ignore, Func< string, string, ValueTask >? postCopyCallback, string src, string dest, int? taskThrottle, CancellationToken cancellationToken)
Copies a directory from src to dest .
Task< bool > DirectoryExists(string path, CancellationToken cancellationToken)
Check that the directory at path exists.
Handles changing file modes/permissions after writing.
For accessing the disk in a synchronous manner.
byte[] ReadFile(string path)
Read the bytes of a file at a given path .
bool IsDirectory(string path)
Checks if a given path is a directory.
IEnumerable< string > GetFiles(string path, CancellationToken cancellationToken)
Enumerate files in a given path .
IEnumerable< string > GetDirectories(string path, CancellationToken cancellationToken)
Enumerate directories in a given path .
Represents a user on the current global::System.Runtime.InteropServices.OSPlatform.
Task RunImpersonated(Action action, CancellationToken cancellationToken)
Runs a given action in the context of the ISystemIdentity.
For identifying the current platform.
Service for temporarily storing files to be downloaded or uploaded.
FileTicketResponse CreateDownload(FileDownloadProvider fileDownloadProvider)
Create a FileTicketResponse for a download.
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
FileUploadStreamKind
Determines the type of global::System.IO.Stream returned from IFileUploadTicket's created from IFileT...