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