tgstation-server 6.17.0
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
141
145 readonly SemaphoreSlim semaphore;
146
150 readonly CancellationTokenSource stoppingCts;
151
156
179 ILogger<Configuration> logger,
180 Models.Instance metadata,
183 {
184 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
185 this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
186 this.linkFactory = linkFactory ?? throw new ArgumentNullException(nameof(linkFactory));
187 this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
188 this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
189 this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
190 this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
191 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
192 this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
193 this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration));
194 this.sessionConfiguration = sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration));
195
196 semaphore = new SemaphoreSlim(1, 1);
197 stoppingCts = new CancellationTokenSource();
198 uploadTasks = Task.CompletedTask;
199 }
200
202 public void Dispose()
203 {
204 semaphore.Dispose();
205 stoppingCts.Dispose();
206 }
207
209 public async ValueTask<ServerSideModifications?> CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken)
210 {
211 using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken, logger))
212 {
213 var ensureDirectoriesTask = EnsureDirectories(cancellationToken);
214
215 // just assume no other fs race conditions here
216 var dmeExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, dmeFile), cancellationToken);
219
220 await ensureDirectoriesTask;
221 var copyTask = ioManager.CopyDirectory(
222 null,
223 null,
225 destination,
226 generalConfiguration.GetCopyDirectoryTaskThrottle(),
227 cancellationToken);
228
229 await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask.AsTask());
230
231 if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result)
232 return null;
233
234 if (dmeExistsTask.Result)
235 return new ServerSideModifications(null, null, true);
236
237 if (!headFileExistsTask.Result && !tailFileExistsTask.Result)
238 return null;
239
240 static string IncludeLine(string filePath) => String.Format(CultureInfo.InvariantCulture, "#include \"{0}\"", filePath);
241
242 return new ServerSideModifications(
243 headFileExistsTask.Result
244 ? IncludeLine(CodeModificationsHeadFile)
245 : null,
246 tailFileExistsTask.Result
247 ? IncludeLine(CodeModificationsTailFile)
248 : null,
249 false);
250 }
251 }
252
254 public async ValueTask<IOrderedQueryable<ConfigurationFileResponse>?> ListDirectory(string? configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
255 {
256 await EnsureDirectories(cancellationToken);
257 var path = ValidateConfigRelativePath(configurationRelativePath);
258
259 configurationRelativePath ??= "/";
260
261 var result = new List<ConfigurationFileResponse>();
262
263 void ListImpl()
264 {
265 var enumerator = synchronousIOManager.GetDirectories(path, cancellationToken);
266 result.AddRange(enumerator.Select(x => new ConfigurationFileResponse
267 {
268 IsDirectory = true,
269 Path = ioManager.ConcatPath(configurationRelativePath, x),
270 }));
271
272 enumerator = synchronousIOManager.GetFiles(path, cancellationToken);
273 result.AddRange(enumerator.Select(x => new ConfigurationFileResponse
274 {
275 IsDirectory = false,
276 Path = ioManager.ConcatPath(configurationRelativePath, x),
277 }));
278 }
279
280 using (SemaphoreSlimContext.TryLock(semaphore, logger, out var locked))
281 {
282 if (!locked)
283 {
284 logger.LogDebug("Contention when attempting to enumerate directory!");
285 return null;
286 }
287
288 if (systemIdentity == null)
289 ListImpl();
290 else
291 await systemIdentity.RunImpersonated(ListImpl, cancellationToken);
292 }
293
294 return result
295 .AsQueryable()
296 .OrderBy(configFile => !configFile.IsDirectory)
297 .ThenBy(configFile => configFile.Path);
298 }
299
301 public async ValueTask<ConfigurationFileResponse?> Read(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
302 {
303 await EnsureDirectories(cancellationToken);
304 var path = ValidateConfigRelativePath(configurationRelativePath);
305
306 ConfigurationFileResponse? result = null;
307
308 void ReadImpl()
309 {
310 try
311 {
312 string GetFileSha()
313 {
314 var content = synchronousIOManager.ReadFile(path);
315 return String.Join(String.Empty, SHA1.HashData(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
316 }
317
318 var originalSha = GetFileSha();
319
320 var disposeToken = stoppingCts.Token;
321 var fileTicket = fileTransferService.CreateDownload(
323 () =>
324 {
325 if (disposeToken.IsCancellationRequested)
326 return ErrorCode.InstanceOffline;
327
328 var newSha = GetFileSha();
329 if (newSha != originalSha)
330 return ErrorCode.ConfigurationFileUpdated;
331
332 return null;
333 },
334 async cancellationToken =>
335 {
336 FileStream? result = null;
337 void GetFileStream()
338 {
339 result = ioManager.GetFileStream(path, false);
340 }
341
342 if (systemIdentity == null)
343 await Task.Factory.StartNew(GetFileStream, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
344 else
345 await systemIdentity.RunImpersonated(GetFileStream, cancellationToken);
346
347 return result!;
348 },
349 path,
350 false));
351
352 result = new ConfigurationFileResponse
353 {
354 FileTicket = fileTicket.FileTicket,
355 IsDirectory = false,
356 LastReadHash = originalSha,
357 AccessDenied = false,
358 Path = configurationRelativePath,
359 };
360 }
361 catch (UnauthorizedAccessException)
362 {
363 // this happens on windows, dunno about linux
364 bool isDirectory;
365 try
366 {
367 isDirectory = synchronousIOManager.IsDirectory(path);
368 }
369 catch (Exception ex)
370 {
371 logger.LogDebug(ex, "IsDirectory exception!");
372 isDirectory = false;
373 }
374
375 result = new ConfigurationFileResponse
376 {
377 Path = configurationRelativePath,
378 };
379 if (!isDirectory)
380 result.AccessDenied = true;
381
382 result.IsDirectory = isDirectory;
383 }
384 }
385
386 using (SemaphoreSlimContext.TryLock(semaphore, logger, out var locked))
387 {
388 if (!locked)
389 {
390 logger.LogDebug("Contention when attempting to read file!");
391 return null;
392 }
393
394 if (systemIdentity == null)
395 await Task.Factory.StartNew(ReadImpl, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
396 else
397 await systemIdentity.RunImpersonated(ReadImpl, cancellationToken);
398 }
399
400 return result;
401 }
402
404 public async ValueTask SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken)
405 {
406 List<string> ignoreFiles;
407
408 async ValueTask SymlinkBase(bool files)
409 {
410 Task<IReadOnlyList<string>> task;
411 if (files)
412 task = ioManager.GetFiles(GameStaticFilesSubdirectory, cancellationToken);
413 else
414 task = ioManager.GetDirectories(GameStaticFilesSubdirectory, cancellationToken);
415 var entries = await task;
416
417 await ValueTaskExtensions.WhenAll(entries.Select<string, ValueTask>(async file =>
418 {
419 var fileName = ioManager.GetFileName(file);
420
421 // need to normalize
422 var fileComparison = platformIdentifier.IsWindows
423 ? StringComparison.OrdinalIgnoreCase
424 : StringComparison.Ordinal;
425 var ignored = ignoreFiles.Any(y => fileName.Equals(y, fileComparison));
426 if (ignored)
427 {
428 logger.LogTrace("Ignoring static file {fileName}...", fileName);
429 return;
430 }
431
432 var destPath = ioManager.ConcatPath(destination, fileName);
433 logger.LogTrace("Symlinking {filePath} to {destPath}...", file, destPath);
434 var fileExistsTask = ioManager.FileExists(destPath, cancellationToken);
435 if (await ioManager.DirectoryExists(destPath, cancellationToken))
436 await ioManager.DeleteDirectory(destPath, cancellationToken);
437 var fileExists = await fileExistsTask;
438 if (fileExists)
439 await ioManager.DeleteFile(destPath, cancellationToken);
440 await linkFactory.CreateSymbolicLink(ioManager.ResolvePath(file), ioManager.ResolvePath(destPath), cancellationToken);
441 }));
442 }
443
444 using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken, logger))
445 {
446 await EnsureDirectories(cancellationToken);
447 var ignoreFileBytes = await ioManager.ReadAllBytes(StaticIgnorePath(), cancellationToken);
448 var ignoreFileText = Encoding.UTF8.GetString(ignoreFileBytes);
449
450 ignoreFiles = new List<string> { StaticIgnoreFile };
451
452 // we don't want to lose trailing whitespace on linux
453 using (var reader = new StringReader(ignoreFileText))
454 {
455 cancellationToken.ThrowIfCancellationRequested();
456 var line = await reader.ReadLineAsync(cancellationToken);
457 if (!String.IsNullOrEmpty(line))
458 ignoreFiles.Add(line);
459 }
460
461 var filesSymlinkTask = SymlinkBase(true);
462 var dirsSymlinkTask = SymlinkBase(false);
463 await ValueTaskExtensions.WhenAll(filesSymlinkTask, dirsSymlinkTask);
464 }
465 }
466
468 public async ValueTask<ConfigurationFileResponse?> Write(string configurationRelativePath, ISystemIdentity? systemIdentity, string? previousHash, CancellationToken cancellationToken)
469 {
470 await EnsureDirectories(cancellationToken);
471 var path = ValidateConfigRelativePath(configurationRelativePath);
472
473 logger.LogTrace("Starting write to {path}", path);
474
475 ConfigurationFileResponse? result = null;
476
477 void WriteImpl()
478 {
479 try
480 {
481 var fileTicket = fileTransferService.CreateUpload(FileUploadStreamKind.ForSynchronousIO);
482 var uploadCancellationToken = stoppingCts.Token;
483 async Task UploadHandler()
484 {
485 await using (fileTicket)
486 {
487 var fileHash = previousHash;
488 logger.LogTrace("Write to {path} waiting for upload stream", path);
489 var uploadStream = await fileTicket.GetResult(uploadCancellationToken);
490 if (uploadStream == null)
491 {
492 logger.LogTrace("Write to {path} expired", path);
493 return; // expired
494 }
495
496 logger.LogTrace("Write to {path} received stream of length {length}...", path, uploadStream.Length);
497 bool success = false;
498 void WriteCallback()
499 {
500 logger.LogTrace("Running synchronous write...");
501 success = synchronousIOManager.WriteFileChecked(path, uploadStream, ref fileHash, uploadCancellationToken);
502 logger.LogTrace("Finished write {un}successfully!", success ? String.Empty : "un");
503 }
504
505 if (fileTicket == null)
506 {
507 logger.LogDebug("File upload ticket for {path} expired!", path);
508 return;
509 }
510
511 using (SemaphoreSlimContext.TryLock(semaphore, logger, out var locked))
512 {
513 if (!locked)
514 {
515 fileTicket.SetError(ErrorCode.ConfigurationContendedAccess, null);
516 return;
517 }
518
519 logger.LogTrace("Kicking off write callback");
520 if (systemIdentity == null)
521 await Task.Factory.StartNew(WriteCallback, uploadCancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
522 else
523 await systemIdentity.RunImpersonated(WriteCallback, uploadCancellationToken);
524 }
525
526 if (!success)
527 fileTicket.SetError(ErrorCode.ConfigurationFileUpdated, fileHash);
528 else if (uploadStream.Length > 0)
529 postWriteHandler.HandleWrite(path);
530 else
531 logger.LogTrace("Write complete");
532 }
533 }
534
535 result = new ConfigurationFileResponse
536 {
537 FileTicket = fileTicket.Ticket.FileTicket,
538 LastReadHash = previousHash,
539 IsDirectory = false,
540 AccessDenied = false,
541 Path = configurationRelativePath,
542 };
543
544 lock (stoppingCts)
545 {
546 async Task ChainUploadTasks()
547 {
548 var oldUploadTask = uploadTasks;
549 var newUploadTask = UploadHandler();
550 try
551 {
552 await oldUploadTask;
553 }
554 finally
555 {
556 await newUploadTask;
557 }
558 }
559
560 uploadTasks = ChainUploadTasks();
561 }
562 }
563 catch (UnauthorizedAccessException)
564 {
565 // this happens on windows, dunno about linux
566 bool isDirectory;
567 try
568 {
569 isDirectory = synchronousIOManager.IsDirectory(path);
570 }
571 catch (Exception ex)
572 {
573 logger.LogDebug(ex, "IsDirectory exception!");
574 isDirectory = false;
575 }
576
577 result = new ConfigurationFileResponse
578 {
579 Path = configurationRelativePath,
580 };
581 if (!isDirectory)
582 result.AccessDenied = true;
583
584 result.IsDirectory = isDirectory;
585 }
586 }
587
588 using (SemaphoreSlimContext.TryLock(semaphore, logger, out var locked))
589 {
590 if (!locked)
591 {
592 logger.LogDebug("Contention when attempting to write file!");
593 return null;
594 }
595
596 if (systemIdentity == null)
597 await Task.Factory.StartNew(WriteImpl, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
598 else
599 await systemIdentity.RunImpersonated(WriteImpl, cancellationToken);
600 }
601
602 return result;
603 }
604
606 public async ValueTask<bool?> CreateDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
607 {
608 await EnsureDirectories(cancellationToken);
609 var path = ValidateConfigRelativePath(configurationRelativePath);
610
611 bool? result = null;
612 void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken);
613
614 using (SemaphoreSlimContext.TryLock(semaphore, logger, out var locked))
615 {
616 if (!locked)
617 {
618 logger.LogDebug("Contention when attempting to create directory!");
619 return null;
620 }
621
622 if (systemIdentity == null)
623 await Task.Factory.StartNew(DoCreate, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
624 else
625 await systemIdentity.RunImpersonated(DoCreate, cancellationToken);
626 }
627
628 return result!.Value;
629 }
630
632 public Task StartAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
633
635 public async Task StopAsync(CancellationToken cancellationToken)
636 {
637 await EnsureDirectories(cancellationToken);
638
639 stoppingCts.Cancel();
640 try
641 {
642 await uploadTasks;
643 }
644 catch (OperationCanceledException ex)
645 {
646 logger.LogDebug(ex, "One or more uploads/downloads were aborted!");
647 }
648 catch (Exception ex)
649 {
650 logger.LogError(ex, "Error awaiting upload tasks!");
651 }
652 }
653
655 public ValueTask HandleEvent(EventType eventType, IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
656 {
657 ArgumentNullException.ThrowIfNull(parameters);
658
659 if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptNames))
660 {
661 logger.LogTrace("No event script for event {event}!", eventType);
662 return ValueTask.CompletedTask;
663 }
664
665 return ExecuteEventScripts(parameters, deploymentPipeline, cancellationToken, scriptNames);
666 }
667
669 public ValueTask? HandleCustomEvent(string scriptName, IEnumerable<string?> parameters, CancellationToken cancellationToken)
670 {
671 var scriptNameIsTgsEventName = EventTypeScriptFileNameMap
672 .Values
673 .SelectMany(scriptNames => scriptNames)
674 .Any(tgsScriptName => tgsScriptName.Equals(
675 scriptName,
676 platformIdentifier.IsWindows
677 ? StringComparison.OrdinalIgnoreCase
678 : StringComparison.Ordinal));
679 if (scriptNameIsTgsEventName)
680 {
681 logger.LogWarning("DMAPI attempted to execute TGS reserved event: {eventName}", scriptName);
682 return null;
683 }
684
685#pragma warning disable CA2012 // Use ValueTasks correctly
686 return ExecuteEventScripts(parameters, false, cancellationToken, scriptName);
687#pragma warning restore CA2012 // Use ValueTasks correctly
688 }
689
691 public async ValueTask<bool?> DeleteDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
692 {
693 await EnsureDirectories(cancellationToken);
694 var path = ValidateConfigRelativePath(configurationRelativePath);
695
696 var result = false;
697 using (SemaphoreSlimContext.TryLock(semaphore, logger, out var locked))
698 {
699 if (!locked)
700 {
701 logger.LogDebug("Contention when attempting to enumerate directory!");
702 return null;
703 }
704
705 void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path);
706
707 if (systemIdentity != null)
708 await systemIdentity.RunImpersonated(CheckDeleteImpl, cancellationToken);
709 else
710 CheckDeleteImpl();
711 }
712
713 return result;
714 }
715
720 string StaticIgnorePath() => ioManager.ConcatPath(GameStaticFilesSubdirectory, StaticIgnoreFile);
721
727 Task EnsureDirectories(CancellationToken cancellationToken)
728 {
729 async Task ValidateStaticFolder()
730 {
731 await ioManager.CreateDirectory(GameStaticFilesSubdirectory, cancellationToken);
732 var staticIgnorePath = StaticIgnorePath();
733 if (!await ioManager.FileExists(staticIgnorePath, cancellationToken))
734 await ioManager.WriteAllBytes(staticIgnorePath, Array.Empty<byte>(), cancellationToken);
735 }
736
737 async Task ValidateCodeModsFolder()
738 {
739 if (await ioManager.DirectoryExists(CodeModificationsSubdirectory, cancellationToken))
740 return;
741
742 await ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken);
743 var headWriteTask = ioManager.WriteAllBytes(
744 ioManager.ConcatPath(
745 CodeModificationsSubdirectory,
746 CodeModificationsHeadFile),
747 Encoding.UTF8.GetBytes(DefaultHeadInclude),
748 cancellationToken);
749 var tailWriteTask = ioManager.WriteAllBytes(
750 ioManager.ConcatPath(
751 CodeModificationsSubdirectory,
752 CodeModificationsTailFile),
753 Encoding.UTF8.GetBytes(DefaultTailInclude),
754 cancellationToken);
755 await ValueTaskExtensions.WhenAll(headWriteTask, tailWriteTask);
756 }
757
758 return Task.WhenAll(
759 ValidateCodeModsFolder(),
760 ioManager.CreateDirectory(EventScriptsSubdirectory, cancellationToken),
761 ValidateStaticFolder());
762 }
763
769 string ValidateConfigRelativePath(string? configurationRelativePath)
770 {
771 var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath);
772 if (nullOrEmptyCheck)
773 configurationRelativePath = DefaultIOManager.CurrentDirectory;
774 if (configurationRelativePath![0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar)
775 configurationRelativePath = DefaultIOManager.CurrentDirectory + configurationRelativePath;
776 var resolved = ioManager.ResolvePath(configurationRelativePath);
777 var local = !nullOrEmptyCheck ? ioManager.ResolvePath() : null;
778 if (!nullOrEmptyCheck && resolved.Length < local!.Length) // .. fuccbois
779 throw new InvalidOperationException("Attempted to access file outside of configuration manager!");
780 return resolved;
781 }
782
791 async ValueTask ExecuteEventScripts(IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken, params string[] scriptNames)
792 {
793 await EnsureDirectories(cancellationToken);
794
795 // always execute in serial
796 using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken, logger))
797 {
798 var directories = generalConfiguration.AdditionalEventScriptsDirectories?.ToList() ?? new List<string>();
799 directories.Add(EventScriptsSubdirectory);
800
801 var allScripts = new List<string>();
802 var tasks = directories.Select<string, ValueTask>(
803 async scriptDirectory =>
804 {
805 var resolvedScriptsDir = ioManager.ResolvePath(scriptDirectory);
806 logger.LogTrace("Checking for scripts in {directory}...", scriptDirectory);
807 var files = await ioManager.GetFilesWithExtension(scriptDirectory, platformIdentifier.ScriptFileExtension, false, cancellationToken);
808
809 var scriptFiles = files
810 .Select(ioManager.GetFileName)
811 .Where(x => scriptNames.Any(
812 scriptName => x.StartsWith(scriptName, StringComparison.Ordinal)))
813 .Select(x =>
814 {
815 var fullScriptPath = ioManager.ConcatPath(resolvedScriptsDir, x);
816 logger.LogTrace("Found matching script: {scriptPath}", fullScriptPath);
817 return fullScriptPath;
818 });
819
820 lock (allScripts)
821 allScripts.AddRange(scriptFiles);
822 })
823 .ToList();
824
825 await ValueTaskExtensions.WhenAll(tasks);
826 if (allScripts.Count == 0)
827 {
828 logger.LogTrace("No event scripts starting with \"{scriptName}\" detected", String.Join("\" or \"", scriptNames));
829 return;
830 }
831
832 var resolvedInstanceScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
833
834 foreach (var scriptFile in allScripts.OrderBy(ioManager.GetFileName))
835 {
836 logger.LogTrace("Running event script {scriptFile}...", scriptFile);
837 await using (var script = await processExecutor.LaunchProcess(
838 scriptFile,
839 resolvedInstanceScriptsDir,
840 String.Join(
841 ' ',
842 parameters.Select(arg =>
843 {
844 if (arg == null)
845 return "(NULL)";
846
847 if (!arg.Contains(' ', StringComparison.Ordinal))
848 return arg;
849
850 arg = arg.Replace("\"", "\\\"", StringComparison.Ordinal);
851
852 return $"\"{arg}\"";
853 })),
854 cancellationToken,
855 new Dictionary<string, string>
856 {
857 { "TGS_INSTANCE_ROOT", metadata.Path! },
858 },
859 readStandardHandles: true,
860 noShellExecute: true))
861 using (cancellationToken.Register(() => script.Terminate()))
862 {
863 if (sessionConfiguration.LowPriorityDeploymentProcesses && deploymentPipeline)
864 script.AdjustPriority(false);
865
866 var exitCode = await script.Lifetime;
867 cancellationToken.ThrowIfCancellationRequested();
868 var scriptOutput = await script.GetCombinedOutput(cancellationToken);
869 if (exitCode != 0)
870 throw new JobException($"Script {scriptFile} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}");
871 else
872 logger.LogDebug("Script output:{newLine}{scriptOutput}", Environment.NewLine, scriptOutput);
873 }
874 }
875 }
876 }
877 }
878}
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 .
Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, IFilesystemLinkFactory linkFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IPlatformIdentifier platformIdentifier, IFileTransferTicketProvider fileTransferService, ILogger< Configuration > logger, Models.Instance metadata, GeneralConfiguration generalConfiguration, SessionConfiguration sessionConfiguration)
Initializes a new instance of the Configuration class.
const string StaticIgnoreFile
Name of the ignore file in GameStaticFilesSubdirectory.
Task EnsureDirectories(CancellationToken cancellationToken)
Ensures standard configuration directories exist.
readonly Models.Instance metadata
The Api.Models.Instance the Configuration belongs to.
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...
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.
readonly CancellationTokenSource stoppingCts
The CancellationTokenSource that is triggered when StopAsync(CancellationToken) is called.
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 ...
async Task StopAsync(CancellationToken cancellationToken)
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 an Api.Models.Instance in the database.
Definition Instance.cs:11
Represents a file on disk to be downloaded.
static async ValueTask< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken, ILogger? logger=null)
Asyncronously locks a semaphore .
static ? SemaphoreSlimContext TryLock(SemaphoreSlim semaphore, ILogger? logger, 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...