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));
202 public async ValueTask<ServerSideModifications?>
CopyDMFilesTo(
string dmeFile,
string destination, CancellationToken cancellationToken)
213 await ensureDirectoriesTask;
222 await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask.AsTask());
224 if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result)
227 if (dmeExistsTask.Result)
230 if (!headFileExistsTask.Result && !tailFileExistsTask.Result)
233 static string IncludeLine(
string filePath) => String.Format(CultureInfo.InvariantCulture,
"#include \"{0}\"", filePath);
236 headFileExistsTask.Result
239 tailFileExistsTask.Result
247 public async ValueTask<IOrderedQueryable<ConfigurationFileResponse>?>
ListDirectory(
string? configurationRelativePath,
ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
252 configurationRelativePath ??=
"/";
254 var result =
new List<ConfigurationFileResponse>();
262 Path = ioManager.ConcatPath(configurationRelativePath, x),
269 Path = ioManager.ConcatPath(configurationRelativePath, x),
277 logger.LogDebug(
"Contention when attempting to enumerate directory!");
281 if (systemIdentity ==
null)
289 .OrderBy(configFile => !configFile.IsDirectory)
290 .ThenBy(configFile => configFile.Path);
294 public async ValueTask<ConfigurationFileResponse?>
Read(
string configurationRelativePath,
ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
308 return String.Join(String.Empty, SHA1.HashData(content).Select(b => b.ToString(
"x2", CultureInfo.InvariantCulture)));
311 var originalSha = GetFileSha();
318 if (disposeToken.IsCancellationRequested)
321 var newSha = GetFileSha();
322 if (newSha != originalSha)
323 return ErrorCode.ConfigurationFileUpdated;
327 async cancellationToken =>
329 FileStream? result =
null;
335 if (systemIdentity ==
null)
338 await systemIdentity.
RunImpersonated(GetFileStream, cancellationToken);
349 LastReadHash = originalSha,
350 AccessDenied =
false,
351 Path = configurationRelativePath,
354 catch (UnauthorizedAccessException)
364 logger.LogDebug(ex,
"IsDirectory exception!");
370 Path = configurationRelativePath,
373 result.AccessDenied =
true;
375 result.IsDirectory = isDirectory;
383 logger.LogDebug(
"Contention when attempting to read file!");
387 if (systemIdentity ==
null)
399 List<string> ignoreFiles;
401 async ValueTask SymlinkBase(
bool files)
403 Task<IReadOnlyList<string>> task;
408 var entries = await task;
412 var fileName = ioManager.GetFileName(file);
415 var fileComparison = platformIdentifier.IsWindows
416 ? StringComparison.OrdinalIgnoreCase
417 : StringComparison.Ordinal;
418 var ignored = ignoreFiles.Any(y => fileName.Equals(y, fileComparison));
421 logger.LogTrace(
"Ignoring static file {fileName}...", fileName);
426 logger.LogTrace(
"Symlinking {filePath} to {destPath}...", file, destPath);
430 var fileExists = await fileExistsTask;
441 var ignoreFileText = Encoding.UTF8.GetString(ignoreFileBytes);
446 using (var reader =
new StringReader(ignoreFileText))
448 cancellationToken.ThrowIfCancellationRequested();
449 var line = await reader.ReadLineAsync(cancellationToken);
450 if (!String.IsNullOrEmpty(line))
451 ignoreFiles.Add(line);
454 var filesSymlinkTask = SymlinkBase(
true);
455 var dirsSymlinkTask = SymlinkBase(
false);
461 public async ValueTask<ConfigurationFileResponse?>
Write(
string configurationRelativePath,
ISystemIdentity? systemIdentity,
string? previousHash, CancellationToken cancellationToken)
463 await EnsureDirectories(cancellationToken);
464 var path = ValidateConfigRelativePath(configurationRelativePath);
473 var uploadCancellationToken = disposeCts.Token;
474 async Task UploadHandler()
476 await
using (fileTicket)
478 var fileHash = previousHash;
479 var uploadStream = await fileTicket.GetResult(uploadCancellationToken);
480 if (uploadStream ==
null)
483 bool success =
false;
486 success = synchronousIOManager.WriteFileChecked(path, uploadStream, ref fileHash, cancellationToken);
489 if (fileTicket ==
null)
491 logger.LogDebug(
"File upload ticket for {path} expired!", path);
499 fileTicket.SetError(
ErrorCode.ConfigurationContendedAccess,
null);
503 if (systemIdentity ==
null)
506 await systemIdentity.
RunImpersonated(WriteCallback, cancellationToken);
510 fileTicket.SetError(
ErrorCode.ConfigurationFileUpdated, fileHash);
511 else if (uploadStream.Length > 0)
512 postWriteHandler.HandleWrite(path);
519 LastReadHash = previousHash,
521 AccessDenied =
false,
522 Path = configurationRelativePath,
526 uploadTasks = Task.WhenAll(uploadTasks, UploadHandler());
528 catch (UnauthorizedAccessException)
534 isDirectory = synchronousIOManager.IsDirectory(path);
538 logger.LogDebug(ex,
"IsDirectory exception!");
544 Path = configurationRelativePath,
547 result.AccessDenied =
true;
549 result.IsDirectory = isDirectory;
557 logger.LogDebug(
"Contention when attempting to write file!");
561 if (systemIdentity ==
null)
573 await EnsureDirectories(cancellationToken);
574 var path = ValidateConfigRelativePath(configurationRelativePath);
577 void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken);
583 logger.LogDebug(
"Contention when attempting to create directory!");
587 if (systemIdentity ==
null)
593 return result!.Value;
597 public Task
StartAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
600 public Task
StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
603 public ValueTask
HandleEvent(
EventType eventType, IEnumerable<string?> parameters,
bool deploymentPipeline, CancellationToken cancellationToken)
605 ArgumentNullException.ThrowIfNull(parameters);
607 if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptNames))
609 logger.LogTrace(
"No event script for event {event}!", eventType);
610 return ValueTask.CompletedTask;
613 return ExecuteEventScripts(parameters, deploymentPipeline, cancellationToken, scriptNames);
617 public ValueTask?
HandleCustomEvent(
string scriptName, IEnumerable<string?> parameters, CancellationToken cancellationToken)
619 var scriptNameIsTgsEventName = EventTypeScriptFileNameMap
621 .SelectMany(scriptNames => scriptNames)
622 .Any(tgsScriptName => tgsScriptName.Equals(
624 platformIdentifier.IsWindows
625 ? StringComparison.OrdinalIgnoreCase
626 : StringComparison.Ordinal));
627 if (scriptNameIsTgsEventName)
629 logger.LogWarning(
"DMAPI attempted to execute TGS reserved event: {eventName}", scriptName);
633#pragma warning disable CA2012
634 return ExecuteEventScripts(parameters,
false, cancellationToken, scriptName);
635#pragma warning restore CA2012
641 await EnsureDirectories(cancellationToken);
642 var path = ValidateConfigRelativePath(configurationRelativePath);
649 logger.LogDebug(
"Contention when attempting to enumerate directory!");
653 void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path);
655 if (systemIdentity !=
null)
656 await systemIdentity.
RunImpersonated(CheckDeleteImpl, cancellationToken);
668 string StaticIgnorePath() => ioManager.ConcatPath(GameStaticFilesSubdirectory, StaticIgnoreFile);
677 async Task ValidateStaticFolder()
679 await ioManager.CreateDirectory(GameStaticFilesSubdirectory, cancellationToken);
680 var staticIgnorePath = StaticIgnorePath();
681 if (!await ioManager.FileExists(staticIgnorePath, cancellationToken))
682 await ioManager.WriteAllBytes(staticIgnorePath, Array.Empty<
byte>(), cancellationToken);
685 async Task ValidateCodeModsFolder()
687 if (await ioManager.DirectoryExists(CodeModificationsSubdirectory, cancellationToken))
690 await ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken);
691 var headWriteTask = ioManager.WriteAllBytes(
692 ioManager.ConcatPath(
693 CodeModificationsSubdirectory,
694 CodeModificationsHeadFile),
695 Encoding.UTF8.GetBytes(DefaultHeadInclude),
697 var tailWriteTask = ioManager.WriteAllBytes(
698 ioManager.ConcatPath(
699 CodeModificationsSubdirectory,
700 CodeModificationsTailFile),
701 Encoding.UTF8.GetBytes(DefaultTailInclude),
707 ValidateCodeModsFolder(),
708 ioManager.CreateDirectory(EventScriptsSubdirectory, cancellationToken),
709 ValidateStaticFolder());
719 var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath);
720 if (nullOrEmptyCheck)
722 if (configurationRelativePath![0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar)
723 configurationRelativePath = DefaultIOManager.CurrentDirectory + configurationRelativePath;
724 var resolved = ioManager.ResolvePath(configurationRelativePath);
725 var local = !nullOrEmptyCheck ? ioManager.ResolvePath() :
null;
726 if (!nullOrEmptyCheck && resolved.Length < local!.Length)
727 throw new InvalidOperationException(
"Attempted to access file outside of configuration manager!");
739 async ValueTask
ExecuteEventScripts(IEnumerable<string?> parameters,
bool deploymentPipeline, CancellationToken cancellationToken, params
string[] scriptNames)
741 await EnsureDirectories(cancellationToken);
746 var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension,
false, cancellationToken);
747 var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
749 var scriptFiles = files
750 .Select(x => ioManager.GetFileName(x))
751 .Where(x => scriptNames.Any(
752 scriptName => x.StartsWith(scriptName, StringComparison.Ordinal)))
755 if (scriptFiles.Count == 0)
757 logger.LogTrace(
"No event scripts starting with \"{scriptName}\" detected", String.Join(
"\" or \"", scriptNames));
761 foreach (var scriptFile
in scriptFiles)
763 logger.LogTrace(
"Running event script {scriptFile}...", scriptFile);
764 await
using (var script = await processExecutor.LaunchProcess(
765 ioManager.ConcatPath(resolvedScriptsDir, scriptFile),
769 parameters.Select(arg =>
774 if (!arg.Contains(
' ', StringComparison.Ordinal))
777 arg = arg.Replace(
"\"",
"\\\"", StringComparison.Ordinal);
782 readStandardHandles:
true,
783 noShellExecute:
true))
784 using (cancellationToken.Register(() => script.Terminate()))
786 if (sessionConfiguration.LowPriorityDeploymentProcesses && deploymentPipeline)
787 script.AdjustPriority(
false);
789 var exitCode = await script.Lifetime;
790 cancellationToken.ThrowIfCancellationRequested();
791 var scriptOutput = await script.GetCombinedOutput(cancellationToken);
793 throw new JobException($
"Script {scriptFile} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}");
795 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 .
Task StopAsync(CancellationToken cancellationToken)
const string StaticIgnoreFile
Name of the ignore file in GameStaticFilesSubdirectory.
Task EnsureDirectories(CancellationToken cancellationToken)
Ensures standard configuration directories exist.
readonly SessionConfiguration sessionConfiguration
The SessionConfiguration for Configuration.
string StaticIgnorePath()
Get the proper path to StaticIgnoreFile.
async ValueTask SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken)
Symlinks all directories in the GameData directory to destination .A ValueTask representing the runni...
readonly CancellationTokenSource disposeCts
The CancellationTokenSource that is triggered when IDisposable.Dispose is called.
Task uploadTasks
The culmination of all upload file transfer callbacks.
readonly IFilesystemLinkFactory linkFactory
The IFilesystemLinkFactory for Configuration.
const string CodeModificationsSubdirectory
The CodeModifications directory name.
readonly IPostWriteHandler postWriteHandler
The IPostWriteHandler for Configuration.
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for Configuration.
async ValueTask< ConfigurationFileResponse?> Write(string configurationRelativePath, ISystemIdentity? systemIdentity, string? previousHash, CancellationToken cancellationToken)
Writes to a given configurationRelativePath .A ValueTask<TResult> resulting in the updated Configurat...
const string CodeModificationsHeadFile
The HeadInclude.dm filename.
ValueTask HandleEvent(EventType eventType, IEnumerable< string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
Handle a given eventType .A ValueTask representing the running operation.
Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, IFilesystemLinkFactory linkFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IPlatformIdentifier platformIdentifier, IFileTransferTicketProvider fileTransferService, ILogger< Configuration > logger, GeneralConfiguration generalConfiguration, SessionConfiguration sessionConfiguration)
Initializes a new instance of the Configuration class.
readonly ILogger< Configuration > logger
The ILogger for Configuration.
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for Configuration.
async ValueTask< IOrderedQueryable< ConfigurationFileResponse >?> ListDirectory(string? configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
Get ConfigurationFileResponses for all items in a given configurationRelativePath ....
const string CodeModificationsTailFile
The TailInclude.dm filename.
async ValueTask< bool?> DeleteDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
Attempt to delete an empty directory at configurationRelativePath .A ValueTask<TResult> resulting in ...
ValueTask? HandleCustomEvent(string scriptName, IEnumerable< string?> parameters, CancellationToken cancellationToken)
Handles a given custom event.A ValueTask representing the running operation if the event was triggere...
async ValueTask< ServerSideModifications?> CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken)
Copies all files in the CodeModifications directory to destination .A ValueTask<TResult> resulting in...
async ValueTask< bool?> CreateDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
Create an empty directory at configurationRelativePath .A ValueTask<TResult> resulting in true if the...
static readonly string DefaultHeadInclude
Default contents of CodeModificationsHeadFile.
async ValueTask< ConfigurationFileResponse?> Read(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
Reads a given configurationRelativePath .A ValueTask<TResult> resulting in the ConfigurationFileRespo...
const string GameStaticFilesSubdirectory
The GameStaticFiles directory name.
Task StartAsync(CancellationToken cancellationToken)
const string EventScriptsSubdirectory
The EventScripts directory name.
readonly SemaphoreSlim semaphore
The SemaphoreSlim for Configuration. Also used as a lock object.
static IReadOnlyDictionary< EventType, string[]> EventTypeScriptFileNameMap
Map of EventTypes to the filename of the event scripts they trigger.
readonly IIOManager ioManager
The IIOManager for Configuration.
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)
Asyncronously locks a semaphore .
static ? SemaphoreSlimContext TryLock(SemaphoreSlim semaphore, 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...