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