2using System.Collections.Generic;
3using System.Globalization;
6using System.Security.Cryptography;
9using System.Threading.Tasks;
11using Microsoft.Extensions.Logging;
63 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}";
68 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}";
77 eventType =>
new KeyValuePair<EventType, string>(
80 .GetField(eventType.ToString())
81 .GetCustomAttributes(
false)
172 ILogger<Configuration>
logger,
183 this.logger =
logger ??
throw new ArgumentNullException(nameof(
logger));
201 public async Task<ServerSideModifications>
CopyDMFilesTo(
string dmeFile,
string destination, CancellationToken cancellationToken)
219 await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask);
221 if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result)
224 if (dmeExistsTask.Result)
227 if (!headFileExistsTask.Result && !tailFileExistsTask.Result)
230 static string IncludeLine(
string filePath) => String.Format(CultureInfo.InvariantCulture,
"#include \"{0}\"", filePath);
237 public async Task<IReadOnlyList<ConfigurationFileResponse>>
ListDirectory(
string configurationRelativePath,
ISystemIdentity systemIdentity, CancellationToken cancellationToken)
242 configurationRelativePath ??=
"/";
244 var result =
new List<ConfigurationFileResponse>();
252 Path = ioManager.ConcatPath(configurationRelativePath, x),
253 }).OrderBy(file => file.Path));
259 Path = ioManager.ConcatPath(configurationRelativePath, x),
260 }).OrderBy(file => file.Path));
267 logger.LogDebug(
"Contention when attempting to enumerate directory!");
271 if (systemIdentity ==
null)
281 public async Task<ConfigurationFileResponse>
Read(
string configurationRelativePath,
ISystemIdentity systemIdentity, CancellationToken cancellationToken)
295 using var sha1 = SHA1.Create();
296 return String.Join(String.Empty, sha1.ComputeHash(content).Select(b => b.ToString(
"x2", CultureInfo.InvariantCulture)));
299 var originalSha = GetFileSha();
306 if (disposeToken.IsCancellationRequested)
309 var newSha = GetFileSha();
310 if (newSha != originalSha)
311 return ErrorCode.ConfigurationFileUpdated;
315 async cancellationToken =>
317 FileStream result =
null;
328 if (systemIdentity ==
null)
331 await systemIdentity.
RunImpersonated(GetFileStream, cancellationToken);
343 LastReadHash = originalSha,
344 AccessDenied =
false,
345 Path = configurationRelativePath,
348 catch (UnauthorizedAccessException)
358 logger.LogDebug(ex,
"IsDirectory exception!");
364 Path = configurationRelativePath,
367 result.AccessDenied =
true;
369 result.IsDirectory = isDirectory;
377 logger.LogDebug(
"Contention when attempting to read file!");
381 if (systemIdentity ==
null)
393 async Task<IReadOnlyList<string>> GetIgnoreFiles()
396 var ignoreFileText = Encoding.UTF8.GetString(ignoreFileBytes);
401 using (var reader =
new StringReader(ignoreFileText))
403 cancellationToken.ThrowIfCancellationRequested();
404 var line = await reader.ReadLineAsync();
405 if (!String.IsNullOrEmpty(line))
412 IReadOnlyList<string> ignoreFiles;
414 async Task SymlinkBase(
bool files)
416 Task<IReadOnlyList<string>> task;
421 var entries = await task;
423 await Task.WhenAll(entries.Select(async file =>
425 var fileName = ioManager.GetFileName(file);
429 if (platformIdentifier.IsWindows)
430 ignored = ignoreFiles.Any(y => fileName.ToUpperInvariant() == y.ToUpperInvariant());
432 ignored = ignoreFiles.Any(y => fileName == y);
436 logger.LogTrace(
"Ignoring static file {fileName}...", fileName);
441 logger.LogTrace(
"Symlinking {filePath} to {destPath}...", file, destPath);
445 var fileExists = await fileExistsTask;
455 ignoreFiles = await GetIgnoreFiles();
456 await Task.WhenAll(SymlinkBase(
true), SymlinkBase(
false));
461 public async Task<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)
597 public Task
StartAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
600 public Task
StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
603 public async Task
HandleEvent(
EventType eventType, IEnumerable<string> parameters,
bool deploymentPipeline, CancellationToken cancellationToken)
605 ArgumentNullException.ThrowIfNull(parameters);
607 await EnsureDirectories(cancellationToken);
609 if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptName))
615 var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension,
false, cancellationToken);
616 var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
618 var scriptFiles = files
619 .Select(x => ioManager.GetFileName(x))
620 .Where(x => x.StartsWith(scriptName, StringComparison.Ordinal))
623 if (!scriptFiles.Any())
625 logger.LogTrace(
"No event scripts starting with \"{scriptName}\" detected", scriptName);
629 foreach (var scriptFile
in scriptFiles)
631 logger.LogTrace(
"Running event script {scriptFile}...", scriptFile);
632 await
using (var script = await processExecutor.LaunchProcess(
633 ioManager.ConcatPath(resolvedScriptsDir, scriptFile),
637 parameters.Select(arg =>
639 if (!arg.Contains(
' ', StringComparison.Ordinal))
642 arg = arg.Replace(
"\"",
"\\\"", StringComparison.Ordinal);
646 readStandardHandles:
true,
647 noShellExecute:
true))
648 using (cancellationToken.Register(() => script.Terminate()))
650 if (sessionConfiguration.LowPriorityDeploymentProcesses)
651 script.AdjustPriority(
false);
653 var exitCode = await script.Lifetime;
654 cancellationToken.ThrowIfCancellationRequested();
655 var scriptOutput = await script.GetCombinedOutput(cancellationToken);
657 throw new JobException($
"Script {scriptFile} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}");
659 logger.LogDebug(
"Script output:{newLine}{scriptOutput}", Environment.NewLine, scriptOutput);
668 await EnsureDirectories(cancellationToken);
669 var path = ValidateConfigRelativePath(configurationRelativePath);
676 logger.LogDebug(
"Contention when attempting to enumerate directory!");
680 void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path);
682 if (systemIdentity !=
null)
683 await systemIdentity.
RunImpersonated(CheckDeleteImpl, cancellationToken);
695 string StaticIgnorePath() => ioManager.ConcatPath(GameStaticFilesSubdirectory, StaticIgnoreFile);
704 async Task ValidateStaticFolder()
706 await ioManager.CreateDirectory(GameStaticFilesSubdirectory, cancellationToken);
707 var staticIgnorePath = StaticIgnorePath();
708 if (!await ioManager.FileExists(staticIgnorePath, cancellationToken))
709 await ioManager.WriteAllBytes(staticIgnorePath, Array.Empty<
byte>(), cancellationToken);
712 async Task ValidateCodeModsFolder()
714 if (await ioManager.DirectoryExists(CodeModificationsSubdirectory, cancellationToken))
717 await ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken);
719 ioManager.WriteAllBytes(
720 ioManager.ConcatPath(
721 CodeModificationsSubdirectory,
722 CodeModificationsHeadFile),
723 Encoding.UTF8.GetBytes(DefaultHeadInclude),
725 ioManager.WriteAllBytes(
726 ioManager.ConcatPath(
727 CodeModificationsSubdirectory,
728 CodeModificationsTailFile),
729 Encoding.UTF8.GetBytes(DefaultTailInclude),
734 ValidateCodeModsFolder(),
735 ioManager.CreateDirectory(EventScriptsSubdirectory, cancellationToken),
736 ValidateStaticFolder());
746 var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath);
747 if (nullOrEmptyCheck)
749 if (configurationRelativePath[0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar)
750 configurationRelativePath = DefaultIOManager.CurrentDirectory + configurationRelativePath;
751 var resolved = ioManager.ResolvePath(configurationRelativePath);
752 var local = !nullOrEmptyCheck ? ioManager.ResolvePath() :
null;
753 if (!nullOrEmptyCheck && resolved.Length < local.Length)
754 throw new InvalidOperationException(
"Attempted to access file outside of configuration manager!");
Response when reading configuration files.
virtual ? string FileTicket
The ticket to use to access the Routes.Transfer controller.
Attribute for indicating the script that a given EventType runs.
string ScriptName
The name of the script the event script the EventType runs.
readonly IProcessExecutor processExecutor
The IProcessExecutor for Configuration.
async Task< bool?> DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
Attempt to delete an empty directory at configurationRelativePath . true if the directory was empty a...
readonly IFileTransferTicketProvider fileTransferService
The IFileTransferTicketProvider for Configuration.
static readonly string DefaultTailInclude
Default contents of CodeModificationsHeadFile.
readonly ISynchronousIOManager synchronousIOManager
The ISynchronousIOManager for Configuration.
async Task< IReadOnlyList< ConfigurationFileResponse > > ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
Get ConfigurationFileResponses for all items in a given configurationRelativePath ....
Task StopAsync(CancellationToken cancellationToken)
string ValidateConfigRelativePath(string configurationRelativePath)
Resolve a given configurationRelativePath to it's full path or throw an InvalidOperationException if...
async Task< ServerSideModifications > CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken)
Copies all files in the CodeModifications directory to destination . A Task<TResult> resulting in the...
const string StaticIgnoreFile
Name of the ignore file in GameStaticFilesSubdirectory.
readonly SessionConfiguration sessionConfiguration
The SessionConfiguration for Configuration.
string StaticIgnorePath()
Get the proper path to StaticIgnoreFile.
Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IPlatformIdentifier platformIdentifier, IFileTransferTicketProvider fileTransferService, ILogger< Configuration > logger, GeneralConfiguration generalConfiguration, SessionConfiguration sessionConfiguration)
Initializes a new instance of the Configuration class.
async Task< ConfigurationFileResponse > Write(string configurationRelativePath, ISystemIdentity systemIdentity, string previousHash, CancellationToken cancellationToken)
Writes to a given configurationRelativePath . A Task<TResult> resulting in the updated ConfigurationF...
static readonly IReadOnlyDictionary< EventType, string > EventTypeScriptFileNameMap
Map of EventTypes to the filename of the event scripts they trigger.
async Task EnsureDirectories(CancellationToken cancellationToken)
Ensures standard configuration directories exist.
readonly ISymlinkFactory symlinkFactory
The ISymlinkFactory for Configuration.
readonly CancellationTokenSource disposeCts
The CancellationTokenSource that is triggered when IDisposable.Dispose is called.
Task uploadTasks
The culmination of all upload file transfer callbacks.
const string CodeModificationsSubdirectory
The CodeModifications directory name.
readonly IPostWriteHandler postWriteHandler
The IPostWriteHandler for Configuration.
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for Configuration.
async Task< ConfigurationFileResponse > Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
Reads a given configurationRelativePath . A Task<TResult> resulting in the ConfigurationFileResponse ...
const string CodeModificationsHeadFile
The HeadInclude.dm filename.
async Task HandleEvent(EventType eventType, IEnumerable< string > parameters, bool deploymentPipeline, CancellationToken cancellationToken)
Handle a given eventType . A Task representing the running operation.
readonly ILogger< Configuration > logger
The ILogger for Configuration.
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for Configuration.
const string CodeModificationsTailFile
The TailInclude.dm filename.
async Task< bool?> CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
Create an empty directory at configurationRelativePath . A Task<TResult> resulting in true if the dir...
static readonly string DefaultHeadInclude
Default contents of CodeModificationsHeadFile.
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.
async Task SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken)
Symlinks all directories in the GameData directory to destination . A Task representing the running o...
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.
Interface for using filesystems.
Task< IReadOnlyList< string > > GetFiles(string path, CancellationToken cancellationToken)
Returns file names in a given path .
string ResolvePath()
Retrieve the full path of the current working directory.
string ConcatPath(params string[] paths)
Combines an array of strings into a path.
Task< IReadOnlyList< string > > GetDirectories(string path, CancellationToken cancellationToken)
Returns directory names in a given path .
Task DeleteFile(string path, CancellationToken cancellationToken)
Deletes a file at path .
Task CopyDirectory(IEnumerable< string > ignore, Func< string, string, Task > postCopyCallback, string src, string dest, int? taskThrottle, CancellationToken cancellationToken)
Copies a directory from src to dest .
FileStream GetFileStream(string path, bool shareWrite)
Gets the Stream for a given file path .
Task< byte[]> ReadAllBytes(string path, CancellationToken cancellationToken)
Returns all the contents of a file at path as a byte array.
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.
Task< bool > DirectoryExists(string path, CancellationToken cancellationToken)
Check that the directory at path exists.
Handles changing file modes/permissions after writing.
For creating filesystem symbolic links.
Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken)
Create a symbolic link.
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.
FileUploadStreamKind
Determines the type of global::System.IO.Stream returned from IFileUploadTicket's created from IFileT...