2using System.Collections.Generic;
3using System.Globalization;
6using System.Security.Cryptography;
9using System.Threading.Tasks;
11using Microsoft.Extensions.Logging;
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}";
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}";
78 eventType =>
new KeyValuePair<
EventType,
string[]>(
81 .GetField(eventType.ToString())!
82 .GetCustomAttributes(
false)
173 ILogger<Configuration>
logger,
184 this.logger =
logger ??
throw new ArgumentNullException(nameof(
logger));
201 public async ValueTask<ServerSideModifications?>
CopyDMFilesTo(
string dmeFile,
string destination, CancellationToken cancellationToken)
212 await ensureDirectoriesTask;
221 await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask.AsTask());
223 if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result)
226 if (dmeExistsTask.Result)
229 if (!headFileExistsTask.Result && !tailFileExistsTask.Result)
232 static string IncludeLine(
string filePath) => String.Format(CultureInfo.InvariantCulture,
"#include \"{0}\"", filePath);
235 headFileExistsTask.Result
238 tailFileExistsTask.Result
246 public async ValueTask<IOrderedQueryable<ConfigurationFileResponse>?>
ListDirectory(
string? configurationRelativePath,
ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
251 configurationRelativePath ??=
"/";
253 var result =
new List<ConfigurationFileResponse>();
261 Path = ioManager.ConcatPath(configurationRelativePath, x),
268 Path = ioManager.ConcatPath(configurationRelativePath, x),
276 logger.LogDebug(
"Contention when attempting to enumerate directory!");
280 if (systemIdentity ==
null)
288 .OrderBy(configFile => !configFile.IsDirectory)
289 .ThenBy(configFile => configFile.Path);
293 public async ValueTask<ConfigurationFileResponse?>
Read(
string configurationRelativePath,
ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
307 return String.Join(String.Empty, SHA1.HashData(content).Select(b => b.ToString(
"x2", CultureInfo.InvariantCulture)));
310 var originalSha = GetFileSha();
317 if (disposeToken.IsCancellationRequested)
320 var newSha = GetFileSha();
321 if (newSha != originalSha)
322 return ErrorCode.ConfigurationFileUpdated;
326 async cancellationToken =>
328 FileStream? result =
null;
334 if (systemIdentity ==
null)
337 await systemIdentity.
RunImpersonated(GetFileStream, cancellationToken);
348 LastReadHash = originalSha,
349 AccessDenied =
false,
350 Path = configurationRelativePath,
353 catch (UnauthorizedAccessException)
363 logger.LogDebug(ex,
"IsDirectory exception!");
369 Path = configurationRelativePath,
372 result.AccessDenied =
true;
374 result.IsDirectory = isDirectory;
382 logger.LogDebug(
"Contention when attempting to read file!");
386 if (systemIdentity ==
null)
398 List<string> ignoreFiles;
400 async ValueTask SymlinkBase(
bool files)
402 Task<IReadOnlyList<string>> task;
407 var entries = await task;
411 var fileName = ioManager.GetFileName(file);
414 var fileComparison = platformIdentifier.IsWindows
415 ? StringComparison.OrdinalIgnoreCase
416 : StringComparison.Ordinal;
417 var ignored = ignoreFiles.Any(y => fileName.Equals(y, fileComparison));
420 logger.LogTrace(
"Ignoring static file {fileName}...", fileName);
425 logger.LogTrace(
"Symlinking {filePath} to {destPath}...", file, destPath);
429 var fileExists = await fileExistsTask;
440 var ignoreFileText = Encoding.UTF8.GetString(ignoreFileBytes);
445 using (var reader =
new StringReader(ignoreFileText))
447 cancellationToken.ThrowIfCancellationRequested();
448 var line = await reader.ReadLineAsync(cancellationToken);
449 if (!String.IsNullOrEmpty(line))
450 ignoreFiles.Add(line);
453 var filesSymlinkTask = SymlinkBase(
true);
454 var dirsSymlinkTask = SymlinkBase(
false);
460 public async ValueTask<ConfigurationFileResponse?>
Write(
string configurationRelativePath,
ISystemIdentity? systemIdentity,
string? previousHash, CancellationToken cancellationToken)
462 await EnsureDirectories(cancellationToken);
463 var path = ValidateConfigRelativePath(configurationRelativePath);
465 logger.LogTrace(
"Starting write to {path}", path);
474 var uploadCancellationToken = stoppingCts.Token;
475 async Task UploadHandler()
477 await
using (fileTicket)
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)
484 logger.LogTrace(
"Write to {path} expired", path);
488 logger.LogTrace(
"Write to {path} received stream of length {length}...", path, uploadStream.Length);
489 bool success =
false;
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");
497 if (fileTicket ==
null)
499 logger.LogDebug(
"File upload ticket for {path} expired!", path);
507 fileTicket.SetError(
ErrorCode.ConfigurationContendedAccess,
null);
511 logger.LogTrace(
"Kicking off write callback");
512 if (systemIdentity ==
null)
515 await systemIdentity.
RunImpersonated(WriteCallback, uploadCancellationToken);
519 fileTicket.SetError(
ErrorCode.ConfigurationFileUpdated, fileHash);
520 else if (uploadStream.Length > 0)
521 postWriteHandler.HandleWrite(path);
523 logger.LogTrace(
"Write complete");
530 LastReadHash = previousHash,
532 AccessDenied =
false,
533 Path = configurationRelativePath,
538 async Task ChainUploadTasks()
540 var oldUploadTask = uploadTasks;
541 var newUploadTask = UploadHandler();
552 uploadTasks = ChainUploadTasks();
555 catch (UnauthorizedAccessException)
561 isDirectory = synchronousIOManager.IsDirectory(path);
565 logger.LogDebug(ex,
"IsDirectory exception!");
571 Path = configurationRelativePath,
574 result.AccessDenied =
true;
576 result.IsDirectory = isDirectory;
584 logger.LogDebug(
"Contention when attempting to write file!");
588 if (systemIdentity ==
null)
600 await EnsureDirectories(cancellationToken);
601 var path = ValidateConfigRelativePath(configurationRelativePath);
604 void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken);
610 logger.LogDebug(
"Contention when attempting to create directory!");
614 if (systemIdentity ==
null)
620 return result!.Value;
624 public Task
StartAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
627 public async Task
StopAsync(CancellationToken cancellationToken)
629 await EnsureDirectories(cancellationToken);
631 stoppingCts.Cancel();
636 catch (OperationCanceledException ex)
638 logger.LogDebug(ex,
"One or more uploads/downloads were aborted!");
642 logger.LogError(ex,
"Error awaiting upload tasks!");
647 public ValueTask
HandleEvent(
EventType eventType, IEnumerable<string?> parameters,
bool deploymentPipeline, CancellationToken cancellationToken)
649 ArgumentNullException.ThrowIfNull(parameters);
651 if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptNames))
653 logger.LogTrace(
"No event script for event {event}!", eventType);
654 return ValueTask.CompletedTask;
657 return ExecuteEventScripts(parameters, deploymentPipeline, cancellationToken, scriptNames);
661 public ValueTask?
HandleCustomEvent(
string scriptName, IEnumerable<string?> parameters, CancellationToken cancellationToken)
663 var scriptNameIsTgsEventName = EventTypeScriptFileNameMap
665 .SelectMany(scriptNames => scriptNames)
666 .Any(tgsScriptName => tgsScriptName.Equals(
668 platformIdentifier.IsWindows
669 ? StringComparison.OrdinalIgnoreCase
670 : StringComparison.Ordinal));
671 if (scriptNameIsTgsEventName)
673 logger.LogWarning(
"DMAPI attempted to execute TGS reserved event: {eventName}", scriptName);
677#pragma warning disable CA2012
678 return ExecuteEventScripts(parameters,
false, cancellationToken, scriptName);
679#pragma warning restore CA2012
685 await EnsureDirectories(cancellationToken);
686 var path = ValidateConfigRelativePath(configurationRelativePath);
693 logger.LogDebug(
"Contention when attempting to enumerate directory!");
697 void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path);
699 if (systemIdentity !=
null)
700 await systemIdentity.
RunImpersonated(CheckDeleteImpl, cancellationToken);
712 string StaticIgnorePath() => ioManager.ConcatPath(GameStaticFilesSubdirectory, StaticIgnoreFile);
721 async Task ValidateStaticFolder()
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);
729 async Task ValidateCodeModsFolder()
731 if (await ioManager.DirectoryExists(CodeModificationsSubdirectory, cancellationToken))
734 await ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken);
735 var headWriteTask = ioManager.WriteAllBytes(
736 ioManager.ConcatPath(
737 CodeModificationsSubdirectory,
738 CodeModificationsHeadFile),
739 Encoding.UTF8.GetBytes(DefaultHeadInclude),
741 var tailWriteTask = ioManager.WriteAllBytes(
742 ioManager.ConcatPath(
743 CodeModificationsSubdirectory,
744 CodeModificationsTailFile),
745 Encoding.UTF8.GetBytes(DefaultTailInclude),
751 ValidateCodeModsFolder(),
752 ioManager.CreateDirectory(EventScriptsSubdirectory, cancellationToken),
753 ValidateStaticFolder());
763 var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath);
764 if (nullOrEmptyCheck)
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)
771 throw new InvalidOperationException(
"Attempted to access file outside of configuration manager!");
783 async ValueTask
ExecuteEventScripts(IEnumerable<string?> parameters,
bool deploymentPipeline, CancellationToken cancellationToken, params
string[] scriptNames)
785 await EnsureDirectories(cancellationToken);
790 var directories = generalConfiguration.AdditionalEventScriptsDirectories?.ToList() ??
new List<string>();
791 directories.Add(EventScriptsSubdirectory);
793 var allScripts =
new List<string>();
794 var tasks = directories.Select<string, ValueTask>(
795 async scriptDirectory =>
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);
801 var scriptFiles = files
802 .Select(ioManager.GetFileName)
803 .Where(x => scriptNames.Any(
804 scriptName => x.StartsWith(scriptName, StringComparison.Ordinal)))
807 var fullScriptPath = ioManager.ConcatPath(resolvedScriptsDir, x);
808 logger.LogTrace(
"Found matching script: {scriptPath}", fullScriptPath);
809 return fullScriptPath;
813 allScripts.AddRange(scriptFiles);
818 if (allScripts.Count == 0)
820 logger.LogTrace(
"No event scripts starting with \"{scriptName}\" detected", String.Join(
"\" or \"", scriptNames));
824 var resolvedInstanceScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
826 foreach (var scriptFile
in allScripts.OrderBy(ioManager.GetFileName))
828 logger.LogTrace(
"Running event script {scriptFile}...", scriptFile);
829 await
using (var script = await processExecutor.LaunchProcess(
831 resolvedInstanceScriptsDir,
834 parameters.Select(arg =>
839 if (!arg.Contains(
' ', StringComparison.Ordinal))
842 arg = arg.Replace(
"\"",
"\\\"", StringComparison.Ordinal);
847 readStandardHandles:
true,
848 noShellExecute:
true))
849 using (cancellationToken.Register(() => script.Terminate()))
851 if (sessionConfiguration.LowPriorityDeploymentProcesses && deploymentPipeline)
852 script.AdjustPriority(
false);
854 var exitCode = await script.Lifetime;
855 cancellationToken.ThrowIfCancellationRequested();
856 var scriptOutput = await script.GetCombinedOutput(cancellationToken);
858 throw new JobException($
"Script {scriptFile} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}");
860 logger.LogDebug(
"Script output:{newLine}{scriptOutput}", Environment.NewLine, scriptOutput);
Response when reading configuration files.
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.
Represents code modifications via configuration.
General configuration options.
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.
Async lock context helper.
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 .
For managing the Configuration directory.
For creating filesystem symbolic links.
Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken)
Create a symbolic link.
Interface for using filesystems.
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.
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.
EventType
Types of events. Mirror in tgs.dm. Prefer last listed name for script.
FileUploadStreamKind
Determines the type of global::System.IO.Stream returned from IFileUploadTicket's created from IFileT...