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);
466 logger.LogTrace(
"Starting write to {path}", path);
475 var uploadCancellationToken = disposeCts.Token;
476 async Task UploadHandler()
478 await
using (fileTicket)
480 var fileHash = previousHash;
481 logger.LogTrace(
"Write to {path} waiting for upload stream", path);
482 var uploadStream = await fileTicket.GetResult(uploadCancellationToken);
483 if (uploadStream ==
null)
485 logger.LogTrace(
"Write to {path} expired", path);
489 logger.LogTrace(
"Write to {path} received stream of length {length}...", path, uploadStream.Length);
490 bool success =
false;
493 logger.LogTrace(
"Running synchronous write...");
494 success = synchronousIOManager.WriteFileChecked(path, uploadStream, ref fileHash, cancellationToken);
495 logger.LogTrace(
"Finished write {un}successfully!", success ? String.Empty :
"un");
498 if (fileTicket ==
null)
500 logger.LogDebug(
"File upload ticket for {path} expired!", path);
508 fileTicket.SetError(
ErrorCode.ConfigurationContendedAccess,
null);
512 logger.LogTrace(
"Kicking off write callback");
513 if (systemIdentity ==
null)
516 await systemIdentity.
RunImpersonated(WriteCallback, cancellationToken);
520 fileTicket.SetError(
ErrorCode.ConfigurationFileUpdated, fileHash);
521 else if (uploadStream.Length > 0)
522 postWriteHandler.HandleWrite(path);
524 logger.LogTrace(
"Write complete");
531 LastReadHash = previousHash,
533 AccessDenied =
false,
534 Path = configurationRelativePath,
538 uploadTasks = Task.WhenAll(uploadTasks, UploadHandler());
540 catch (UnauthorizedAccessException)
546 isDirectory = synchronousIOManager.IsDirectory(path);
550 logger.LogDebug(ex,
"IsDirectory exception!");
556 Path = configurationRelativePath,
559 result.AccessDenied =
true;
561 result.IsDirectory = isDirectory;
569 logger.LogDebug(
"Contention when attempting to write file!");
573 if (systemIdentity ==
null)
585 await EnsureDirectories(cancellationToken);
586 var path = ValidateConfigRelativePath(configurationRelativePath);
589 void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken);
595 logger.LogDebug(
"Contention when attempting to create directory!");
599 if (systemIdentity ==
null)
605 return result!.Value;
609 public Task
StartAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
612 public Task
StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
615 public ValueTask
HandleEvent(
EventType eventType, IEnumerable<string?> parameters,
bool deploymentPipeline, CancellationToken cancellationToken)
617 ArgumentNullException.ThrowIfNull(parameters);
619 if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptNames))
621 logger.LogTrace(
"No event script for event {event}!", eventType);
622 return ValueTask.CompletedTask;
625 return ExecuteEventScripts(parameters, deploymentPipeline, cancellationToken, scriptNames);
629 public ValueTask?
HandleCustomEvent(
string scriptName, IEnumerable<string?> parameters, CancellationToken cancellationToken)
631 var scriptNameIsTgsEventName = EventTypeScriptFileNameMap
633 .SelectMany(scriptNames => scriptNames)
634 .Any(tgsScriptName => tgsScriptName.Equals(
636 platformIdentifier.IsWindows
637 ? StringComparison.OrdinalIgnoreCase
638 : StringComparison.Ordinal));
639 if (scriptNameIsTgsEventName)
641 logger.LogWarning(
"DMAPI attempted to execute TGS reserved event: {eventName}", scriptName);
645#pragma warning disable CA2012
646 return ExecuteEventScripts(parameters,
false, cancellationToken, scriptName);
647#pragma warning restore CA2012
653 await EnsureDirectories(cancellationToken);
654 var path = ValidateConfigRelativePath(configurationRelativePath);
661 logger.LogDebug(
"Contention when attempting to enumerate directory!");
665 void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path);
667 if (systemIdentity !=
null)
668 await systemIdentity.
RunImpersonated(CheckDeleteImpl, cancellationToken);
680 string StaticIgnorePath() => ioManager.ConcatPath(GameStaticFilesSubdirectory, StaticIgnoreFile);
689 async Task ValidateStaticFolder()
691 await ioManager.CreateDirectory(GameStaticFilesSubdirectory, cancellationToken);
692 var staticIgnorePath = StaticIgnorePath();
693 if (!await ioManager.FileExists(staticIgnorePath, cancellationToken))
694 await ioManager.WriteAllBytes(staticIgnorePath, Array.Empty<
byte>(), cancellationToken);
697 async Task ValidateCodeModsFolder()
699 if (await ioManager.DirectoryExists(CodeModificationsSubdirectory, cancellationToken))
702 await ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken);
703 var headWriteTask = ioManager.WriteAllBytes(
704 ioManager.ConcatPath(
705 CodeModificationsSubdirectory,
706 CodeModificationsHeadFile),
707 Encoding.UTF8.GetBytes(DefaultHeadInclude),
709 var tailWriteTask = ioManager.WriteAllBytes(
710 ioManager.ConcatPath(
711 CodeModificationsSubdirectory,
712 CodeModificationsTailFile),
713 Encoding.UTF8.GetBytes(DefaultTailInclude),
719 ValidateCodeModsFolder(),
720 ioManager.CreateDirectory(EventScriptsSubdirectory, cancellationToken),
721 ValidateStaticFolder());
731 var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath);
732 if (nullOrEmptyCheck)
734 if (configurationRelativePath![0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar)
735 configurationRelativePath = DefaultIOManager.CurrentDirectory + configurationRelativePath;
736 var resolved = ioManager.ResolvePath(configurationRelativePath);
737 var local = !nullOrEmptyCheck ? ioManager.ResolvePath() :
null;
738 if (!nullOrEmptyCheck && resolved.Length < local!.Length)
739 throw new InvalidOperationException(
"Attempted to access file outside of configuration manager!");
751 async ValueTask
ExecuteEventScripts(IEnumerable<string?> parameters,
bool deploymentPipeline, CancellationToken cancellationToken, params
string[] scriptNames)
753 await EnsureDirectories(cancellationToken);
758 var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension,
false, cancellationToken);
759 var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
761 var scriptFiles = files
762 .Select(x => ioManager.GetFileName(x))
763 .Where(x => scriptNames.Any(
764 scriptName => x.StartsWith(scriptName, StringComparison.Ordinal)))
767 if (scriptFiles.Count == 0)
769 logger.LogTrace(
"No event scripts starting with \"{scriptName}\" detected", String.Join(
"\" or \"", scriptNames));
773 foreach (var scriptFile
in scriptFiles)
775 logger.LogTrace(
"Running event script {scriptFile}...", scriptFile);
776 await
using (var script = await processExecutor.LaunchProcess(
777 ioManager.ConcatPath(resolvedScriptsDir, scriptFile),
781 parameters.Select(arg =>
786 if (!arg.Contains(
' ', StringComparison.Ordinal))
789 arg = arg.Replace(
"\"",
"\\\"", StringComparison.Ordinal);
794 readStandardHandles:
true,
795 noShellExecute:
true))
796 using (cancellationToken.Register(() => script.Terminate()))
798 if (sessionConfiguration.LowPriorityDeploymentProcesses && deploymentPipeline)
799 script.AdjustPriority(
false);
801 var exitCode = await script.Lifetime;
802 cancellationToken.ThrowIfCancellationRequested();
803 var scriptOutput = await script.GetCombinedOutput(cancellationToken);
805 throw new JobException($
"Script {scriptFile} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}");
807 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, 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...