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)
179 ILogger<Configuration>
logger,
191 this.logger =
logger ??
throw new ArgumentNullException(nameof(
logger));
192 this.metadata =
metadata ??
throw new ArgumentNullException(nameof(
metadata));
209 public async ValueTask<ServerSideModifications?>
CopyDMFilesTo(
string dmeFile,
string destination, CancellationToken cancellationToken)
220 await ensureDirectoriesTask;
229 await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask.AsTask());
231 if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result)
234 if (dmeExistsTask.Result)
237 if (!headFileExistsTask.Result && !tailFileExistsTask.Result)
240 static string IncludeLine(
string filePath) => String.Format(CultureInfo.InvariantCulture,
"#include \"{0}\"", filePath);
243 headFileExistsTask.Result
246 tailFileExistsTask.Result
254 public async ValueTask<IOrderedQueryable<ConfigurationFileResponse>?>
ListDirectory(
string? configurationRelativePath,
ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
259 configurationRelativePath ??=
"/";
261 var result =
new List<ConfigurationFileResponse>();
269 Path = ioManager.ConcatPath(configurationRelativePath, x),
276 Path = ioManager.ConcatPath(configurationRelativePath, x),
284 logger.LogDebug(
"Contention when attempting to enumerate directory!");
288 if (systemIdentity ==
null)
296 .OrderBy(configFile => !configFile.IsDirectory)
297 .ThenBy(configFile => configFile.Path);
301 public async ValueTask<ConfigurationFileResponse?>
Read(
string configurationRelativePath,
ISystemIdentity? systemIdentity, CancellationToken cancellationToken)
315 return String.Join(String.Empty, SHA1.HashData(content).Select(b => b.ToString(
"x2", CultureInfo.InvariantCulture)));
318 var originalSha = GetFileSha();
325 if (disposeToken.IsCancellationRequested)
328 var newSha = GetFileSha();
329 if (newSha != originalSha)
330 return ErrorCode.ConfigurationFileUpdated;
334 async cancellationToken =>
336 FileStream? result =
null;
342 if (systemIdentity ==
null)
345 await systemIdentity.
RunImpersonated(GetFileStream, cancellationToken);
356 LastReadHash = originalSha,
357 AccessDenied =
false,
358 Path = configurationRelativePath,
361 catch (UnauthorizedAccessException)
371 logger.LogDebug(ex,
"IsDirectory exception!");
377 Path = configurationRelativePath,
380 result.AccessDenied =
true;
382 result.IsDirectory = isDirectory;
390 logger.LogDebug(
"Contention when attempting to read file!");
394 if (systemIdentity ==
null)
406 List<string> ignoreFiles;
408 async ValueTask SymlinkBase(
bool files)
410 Task<IReadOnlyList<string>> task;
415 var entries = await task;
419 var fileName = ioManager.GetFileName(file);
422 var fileComparison = platformIdentifier.IsWindows
423 ? StringComparison.OrdinalIgnoreCase
424 : StringComparison.Ordinal;
425 var ignored = ignoreFiles.Any(y => fileName.Equals(y, fileComparison));
428 logger.LogTrace(
"Ignoring static file {fileName}...", fileName);
433 logger.LogTrace(
"Symlinking {filePath} to {destPath}...", file, destPath);
437 var fileExists = await fileExistsTask;
448 var ignoreFileText = Encoding.UTF8.GetString(ignoreFileBytes);
453 using (var reader =
new StringReader(ignoreFileText))
455 cancellationToken.ThrowIfCancellationRequested();
456 var line = await reader.ReadLineAsync(cancellationToken);
457 if (!String.IsNullOrEmpty(line))
458 ignoreFiles.Add(line);
461 var filesSymlinkTask = SymlinkBase(
true);
462 var dirsSymlinkTask = SymlinkBase(
false);
468 public async ValueTask<ConfigurationFileResponse?>
Write(
string configurationRelativePath,
ISystemIdentity? systemIdentity,
string? previousHash, CancellationToken cancellationToken)
470 await EnsureDirectories(cancellationToken);
471 var path = ValidateConfigRelativePath(configurationRelativePath);
473 logger.LogTrace(
"Starting write to {path}", path);
482 var uploadCancellationToken = stoppingCts.Token;
483 async Task UploadHandler()
485 await
using (fileTicket)
487 var fileHash = previousHash;
488 logger.LogTrace(
"Write to {path} waiting for upload stream", path);
489 var uploadStream = await fileTicket.GetResult(uploadCancellationToken);
490 if (uploadStream ==
null)
492 logger.LogTrace(
"Write to {path} expired", path);
496 logger.LogTrace(
"Write to {path} received stream of length {length}...", path, uploadStream.Length);
497 bool success =
false;
500 logger.LogTrace(
"Running synchronous write...");
501 success = synchronousIOManager.WriteFileChecked(path, uploadStream, ref fileHash, uploadCancellationToken);
502 logger.LogTrace(
"Finished write {un}successfully!", success ? String.Empty :
"un");
505 if (fileTicket ==
null)
507 logger.LogDebug(
"File upload ticket for {path} expired!", path);
515 fileTicket.SetError(
ErrorCode.ConfigurationContendedAccess,
null);
519 logger.LogTrace(
"Kicking off write callback");
520 if (systemIdentity ==
null)
523 await systemIdentity.
RunImpersonated(WriteCallback, uploadCancellationToken);
527 fileTicket.SetError(
ErrorCode.ConfigurationFileUpdated, fileHash);
528 else if (uploadStream.Length > 0)
529 postWriteHandler.HandleWrite(path);
531 logger.LogTrace(
"Write complete");
538 LastReadHash = previousHash,
540 AccessDenied =
false,
541 Path = configurationRelativePath,
546 async Task ChainUploadTasks()
548 var oldUploadTask = uploadTasks;
549 var newUploadTask = UploadHandler();
560 uploadTasks = ChainUploadTasks();
563 catch (UnauthorizedAccessException)
569 isDirectory = synchronousIOManager.IsDirectory(path);
573 logger.LogDebug(ex,
"IsDirectory exception!");
579 Path = configurationRelativePath,
582 result.AccessDenied =
true;
584 result.IsDirectory = isDirectory;
592 logger.LogDebug(
"Contention when attempting to write file!");
596 if (systemIdentity ==
null)
608 await EnsureDirectories(cancellationToken);
609 var path = ValidateConfigRelativePath(configurationRelativePath);
612 void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken);
618 logger.LogDebug(
"Contention when attempting to create directory!");
622 if (systemIdentity ==
null)
628 return result!.Value;
632 public Task
StartAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
635 public async Task
StopAsync(CancellationToken cancellationToken)
637 await EnsureDirectories(cancellationToken);
639 stoppingCts.Cancel();
644 catch (OperationCanceledException ex)
646 logger.LogDebug(ex,
"One or more uploads/downloads were aborted!");
650 logger.LogError(ex,
"Error awaiting upload tasks!");
655 public ValueTask
HandleEvent(
EventType eventType, IEnumerable<string?> parameters,
bool deploymentPipeline, CancellationToken cancellationToken)
657 ArgumentNullException.ThrowIfNull(parameters);
659 if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptNames))
661 logger.LogTrace(
"No event script for event {event}!", eventType);
662 return ValueTask.CompletedTask;
665 return ExecuteEventScripts(parameters, deploymentPipeline, cancellationToken, scriptNames);
669 public ValueTask?
HandleCustomEvent(
string scriptName, IEnumerable<string?> parameters, CancellationToken cancellationToken)
671 var scriptNameIsTgsEventName = EventTypeScriptFileNameMap
673 .SelectMany(scriptNames => scriptNames)
674 .Any(tgsScriptName => tgsScriptName.Equals(
676 platformIdentifier.IsWindows
677 ? StringComparison.OrdinalIgnoreCase
678 : StringComparison.Ordinal));
679 if (scriptNameIsTgsEventName)
681 logger.LogWarning(
"DMAPI attempted to execute TGS reserved event: {eventName}", scriptName);
685#pragma warning disable CA2012
686 return ExecuteEventScripts(parameters,
false, cancellationToken, scriptName);
687#pragma warning restore CA2012
693 await EnsureDirectories(cancellationToken);
694 var path = ValidateConfigRelativePath(configurationRelativePath);
701 logger.LogDebug(
"Contention when attempting to enumerate directory!");
705 void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path);
707 if (systemIdentity !=
null)
708 await systemIdentity.
RunImpersonated(CheckDeleteImpl, cancellationToken);
720 string StaticIgnorePath() => ioManager.ConcatPath(GameStaticFilesSubdirectory, StaticIgnoreFile);
729 async Task ValidateStaticFolder()
731 await ioManager.CreateDirectory(GameStaticFilesSubdirectory, cancellationToken);
732 var staticIgnorePath = StaticIgnorePath();
733 if (!await ioManager.FileExists(staticIgnorePath, cancellationToken))
734 await ioManager.WriteAllBytes(staticIgnorePath, Array.Empty<
byte>(), cancellationToken);
737 async Task ValidateCodeModsFolder()
739 if (await ioManager.DirectoryExists(CodeModificationsSubdirectory, cancellationToken))
742 await ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken);
743 var headWriteTask = ioManager.WriteAllBytes(
744 ioManager.ConcatPath(
745 CodeModificationsSubdirectory,
746 CodeModificationsHeadFile),
747 Encoding.UTF8.GetBytes(DefaultHeadInclude),
749 var tailWriteTask = ioManager.WriteAllBytes(
750 ioManager.ConcatPath(
751 CodeModificationsSubdirectory,
752 CodeModificationsTailFile),
753 Encoding.UTF8.GetBytes(DefaultTailInclude),
759 ValidateCodeModsFolder(),
760 ioManager.CreateDirectory(EventScriptsSubdirectory, cancellationToken),
761 ValidateStaticFolder());
771 var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath);
772 if (nullOrEmptyCheck)
774 if (configurationRelativePath![0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar)
775 configurationRelativePath = DefaultIOManager.CurrentDirectory + configurationRelativePath;
776 var resolved = ioManager.ResolvePath(configurationRelativePath);
777 var local = !nullOrEmptyCheck ? ioManager.ResolvePath() :
null;
778 if (!nullOrEmptyCheck && resolved.Length < local!.Length)
779 throw new InvalidOperationException(
"Attempted to access file outside of configuration manager!");
791 async ValueTask
ExecuteEventScripts(IEnumerable<string?> parameters,
bool deploymentPipeline, CancellationToken cancellationToken, params
string[] scriptNames)
793 await EnsureDirectories(cancellationToken);
798 var directories = generalConfiguration.AdditionalEventScriptsDirectories?.ToList() ??
new List<string>();
799 directories.Add(EventScriptsSubdirectory);
801 var allScripts =
new List<string>();
802 var tasks = directories.Select<string, ValueTask>(
803 async scriptDirectory =>
805 var resolvedScriptsDir = ioManager.ResolvePath(scriptDirectory);
806 logger.LogTrace(
"Checking for scripts in {directory}...", scriptDirectory);
807 var files = await ioManager.GetFilesWithExtension(scriptDirectory, platformIdentifier.ScriptFileExtension,
false, cancellationToken);
809 var scriptFiles = files
810 .Select(ioManager.GetFileName)
811 .Where(x => scriptNames.Any(
812 scriptName => x.StartsWith(scriptName, StringComparison.Ordinal)))
815 var fullScriptPath = ioManager.ConcatPath(resolvedScriptsDir, x);
816 logger.LogTrace(
"Found matching script: {scriptPath}", fullScriptPath);
817 return fullScriptPath;
821 allScripts.AddRange(scriptFiles);
826 if (allScripts.Count == 0)
828 logger.LogTrace(
"No event scripts starting with \"{scriptName}\" detected", String.Join(
"\" or \"", scriptNames));
832 var resolvedInstanceScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
834 foreach (var scriptFile
in allScripts.OrderBy(ioManager.GetFileName))
836 logger.LogTrace(
"Running event script {scriptFile}...", scriptFile);
837 await
using (var script = await processExecutor.LaunchProcess(
839 resolvedInstanceScriptsDir,
842 parameters.Select(arg =>
847 if (!arg.Contains(
' ', StringComparison.Ordinal))
850 arg = arg.Replace(
"\"",
"\\\"", StringComparison.Ordinal);
855 new Dictionary<string, string>
857 {
"TGS_INSTANCE_ROOT", metadata.Path! },
859 readStandardHandles:
true,
860 noShellExecute:
true))
861 using (cancellationToken.Register(() => script.Terminate()))
863 if (sessionConfiguration.LowPriorityDeploymentProcesses && deploymentPipeline)
864 script.AdjustPriority(
false);
866 var exitCode = await script.Lifetime;
867 cancellationToken.ThrowIfCancellationRequested();
868 var scriptOutput = await script.GetCombinedOutput(cancellationToken);
870 throw new JobException($
"Script {scriptFile} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}");
872 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 .
Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, IFilesystemLinkFactory linkFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IPlatformIdentifier platformIdentifier, IFileTransferTicketProvider fileTransferService, ILogger< Configuration > logger, Models.Instance metadata, GeneralConfiguration generalConfiguration, SessionConfiguration sessionConfiguration)
Initializes a new instance of the Configuration class.
const string StaticIgnoreFile
Name of the ignore file in GameStaticFilesSubdirectory.
Task EnsureDirectories(CancellationToken cancellationToken)
Ensures standard configuration directories exist.
readonly Models.Instance metadata
The Api.Models.Instance the Configuration belongs to.
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.
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 an Api.Models.Instance in the database.
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...