1 using Microsoft.Extensions.Logging;
3 using System.Collections.Generic;
7 using System.Runtime.InteropServices;
8 using System.Security.Cryptography;
11 using System.Threading.Tasks;
22 const string CodeModificationsSubdirectory =
"CodeModifications";
23 const string EventScriptsSubdirectory =
"EventScripts";
24 const string GameStaticFilesSubdirectory =
"GameStaticFiles";
29 const string StaticIgnoreFile =
".tgsignore";
31 const string CodeModificationsHeadFile =
"HeadInclude.dm";
32 const string CodeModificationsTailFile =
"TailInclude.dm";
34 static readonly IReadOnlyDictionary<EventType, string> EventTypeScriptFileNameMap =
new Dictionary<EventType, string>
37 {
EventType.CompileComplete,
"PostCompile" },
38 {
EventType.RepoPreSynchronize,
"PreSynchronize" }
74 readonly ILogger<Configuration>
logger;
93 this.ioManager = ioManager ??
throw new ArgumentNullException(nameof(ioManager));
94 this.synchronousIOManager = synchronousIOManager ??
throw new ArgumentNullException(nameof(synchronousIOManager));
95 this.symlinkFactory = symlinkFactory ??
throw new ArgumentNullException(nameof(symlinkFactory));
96 this.processExecutor = processExecutor ??
throw new ArgumentNullException(nameof(processExecutor));
97 this.postWriteHandler = postWriteHandler ??
throw new ArgumentNullException(nameof(postWriteHandler));
98 this.platformIdentifier = platformIdentifier ??
throw new ArgumentNullException(nameof(platformIdentifier));
99 this.logger = logger ??
throw new ArgumentNullException(nameof(logger));
101 semaphore =
new SemaphoreSlim(1);
105 public void Dispose() => semaphore.Dispose();
111 string StaticIgnorePath() => ioManager.ConcatPath(GameStaticFilesSubdirectory, StaticIgnoreFile);
120 async Task ValidateStaticFolder()
122 await ioManager.CreateDirectory(GameStaticFilesSubdirectory, cancellationToken).ConfigureAwait(
false);
123 var staticIgnorePath = StaticIgnorePath();
124 if(!await ioManager.FileExists(staticIgnorePath, cancellationToken).ConfigureAwait(
false))
125 await ioManager.WriteAllBytes(staticIgnorePath, Array.Empty<byte>(), cancellationToken).ConfigureAwait(
false);
128 await Task.WhenAll(ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken), ioManager.CreateDirectory(EventScriptsSubdirectory, cancellationToken), ValidateStaticFolder()).ConfigureAwait(
false);
132 public async Task<ServerSideModifications>
CopyDMFilesTo(
string dmeFile,
string destination, CancellationToken cancellationToken)
137 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
140 var dmeExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, dmeFile), cancellationToken);
141 var headFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsHeadFile), cancellationToken);
142 var tailFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsTailFile), cancellationToken);
143 var copyTask = ioManager.CopyDirectory(CodeModificationsSubdirectory, destination, null, cancellationToken);
145 await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask).ConfigureAwait(
false);
147 if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result)
150 if (dmeExistsTask.Result)
153 if (!headFileExistsTask.Result && !tailFileExistsTask.Result)
156 string IncludeLine(
string filePath) => String.Format(CultureInfo.InvariantCulture,
"#include \"{0}\"", filePath);
158 return new ServerSideModifications(headFileExistsTask.Result ? IncludeLine(CodeModificationsHeadFile) : null, tailFileExistsTask.Result ? IncludeLine(CodeModificationsTailFile) : null,
false);
164 var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath);
165 if (nullOrEmptyCheck)
166 configurationRelativePath =
".";
167 if (configurationRelativePath[0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar)
168 configurationRelativePath =
'.' + configurationRelativePath;
169 var resolved = ioManager.ResolvePath(configurationRelativePath);
170 var local = !nullOrEmptyCheck ? ioManager.ResolvePath(
".") : null;
171 if (!nullOrEmptyCheck && resolved.Length < local.Length)
172 throw new InvalidOperationException(
"Attempted to access file outside of configuration manager!");
177 public async Task<IReadOnlyList<ConfigurationFile>>
ListDirectory(
string configurationRelativePath,
ISystemIdentity systemIdentity, CancellationToken cancellationToken)
179 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
180 var path = ValidateConfigRelativePath(configurationRelativePath);
182 if (configurationRelativePath == null)
183 configurationRelativePath =
"/";
185 List<ConfigurationFile> result =
new List<ConfigurationFile>();
189 var enumerator = synchronousIOManager.GetDirectories(path, cancellationToken);
195 Path = ioManager.ConcatPath(configurationRelativePath, x),
198 catch (IOException e)
200 logger.LogDebug(
"IOException while writing {0}: {1}", path, e);
204 enumerator = synchronousIOManager.GetFiles(path, cancellationToken);
208 Path = ioManager.ConcatPath(configurationRelativePath, x),
213 if (systemIdentity == null)
216 await systemIdentity.
RunImpersonated(ListImpl, cancellationToken).ConfigureAwait(
false);
222 public async Task<ConfigurationFile>
Read(
string configurationRelativePath,
ISystemIdentity systemIdentity, CancellationToken cancellationToken)
224 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
225 var path = ValidateConfigRelativePath(configurationRelativePath);
234 var content = synchronousIOManager.ReadFile(path);
236 #pragma warning disable CA5350 // Do not use insecure cryptographic algorithm SHA1. 237 using (var sha1 =
new SHA1Managed())
238 #pragma warning restore CA5350
239 sha1String = String.Join(
"", sha1.ComputeHash(content).Select(b => b.ToString(
"x2", CultureInfo.InvariantCulture)));
244 LastReadHash = sha1String,
245 AccessDenied =
false,
246 Path = configurationRelativePath
249 catch (UnauthorizedAccessException)
255 isDirectory = synchronousIOManager.
IsDirectory(path);
264 Path = configurationRelativePath
274 if (systemIdentity == null)
275 await Task.Factory.StartNew(ReadImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(
false);
277 await systemIdentity.
RunImpersonated(ReadImpl, cancellationToken).ConfigureAwait(
false);
285 async Task<IReadOnlyList<string>> GetIgnoreFiles()
287 var ignoreFileBytes = await ioManager.ReadAllBytes(StaticIgnorePath(), cancellationToken).ConfigureAwait(
false);
288 var ignoreFileText = Encoding.UTF8.GetString(ignoreFileBytes);
290 var results =
new List<string> { StaticIgnoreFile };
293 using (var reader =
new StringReader(ignoreFileText))
295 cancellationToken.ThrowIfCancellationRequested();
296 var line = await reader.ReadLineAsync().ConfigureAwait(
false);
297 if (!String.IsNullOrEmpty(line))
304 IReadOnlyList<string> ignoreFiles;
306 async Task SymlinkBase(
bool files)
308 Task<IReadOnlyList<string>> task;
310 task = ioManager.GetFiles(GameStaticFilesSubdirectory, cancellationToken);
312 task = ioManager.GetDirectories(GameStaticFilesSubdirectory, cancellationToken);
313 var entries = await task.ConfigureAwait(
false);
315 await Task.WhenAll(entries.Select(async x =>
317 var fileName = ioManager.GetFileName(x);
320 if (platformIdentifier.IsWindows)
322 ignored = ignoreFiles.Any(y => fileName.ToUpperInvariant() == y.ToUpperInvariant());
324 ignored = ignoreFiles.Any(y => fileName == y);
328 logger.LogTrace(
"Ignoring static file {0}...", fileName);
332 var destPath = ioManager.ConcatPath(destination, fileName);
333 logger.LogTrace(
"Symlinking {0} to {1}...", x, destPath);
334 var fileExistsTask = ioManager.FileExists(destPath, cancellationToken);
335 if (await ioManager.DirectoryExists(destPath, cancellationToken).ConfigureAwait(
false))
336 await ioManager.DeleteDirectory(destPath, cancellationToken).ConfigureAwait(
false);
337 var fileExists = await fileExistsTask.ConfigureAwait(
false);
339 await ioManager.DeleteFile(destPath, cancellationToken).ConfigureAwait(
false);
340 await symlinkFactory.CreateSymbolicLink(ioManager.ResolvePath(x), ioManager.ResolvePath(destPath), cancellationToken).ConfigureAwait(
false);
341 })).ConfigureAwait(
false);
346 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
347 ignoreFiles = await GetIgnoreFiles().ConfigureAwait(
false);
348 await Task.WhenAll(SymlinkBase(
true), SymlinkBase(
false)).ConfigureAwait(
false);
353 public async Task<ConfigurationFile>
Write(
string configurationRelativePath,
ISystemIdentity systemIdentity, byte[] data,
string previousHash, CancellationToken cancellationToken)
355 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
356 var path = ValidateConfigRelativePath(configurationRelativePath);
365 var fileHash = previousHash;
366 var success = synchronousIOManager.WriteFileChecked(path, data, ref fileHash, cancellationToken);
370 postWriteHandler.HandleWrite(path);
375 LastReadHash = fileHash,
376 AccessDenied =
false,
377 Path = configurationRelativePath
380 catch (UnauthorizedAccessException)
386 isDirectory = synchronousIOManager.
IsDirectory(path);
395 Path = configurationRelativePath
405 if (systemIdentity == null)
406 await Task.Factory.StartNew(WriteImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(
false);
408 await systemIdentity.
RunImpersonated(WriteImpl, cancellationToken).ConfigureAwait(
false);
416 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
417 var path = ValidateConfigRelativePath(configurationRelativePath);
420 void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken);
423 if (systemIdentity == null)
424 await Task.Factory.StartNew(DoCreate, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(
false);
426 await systemIdentity.
RunImpersonated(DoCreate, cancellationToken).ConfigureAwait(
false);
432 public Task StartAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
435 public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
438 public async Task<bool>
HandleEvent(
EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
440 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
442 if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptName))
448 var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, cancellationToken).ConfigureAwait(
false);
449 var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
451 foreach (var I
in files.Select(x => ioManager.GetFileName(x)).Where(x => x.StartsWith(scriptName, StringComparison.Ordinal)))
452 using (var script = processExecutor.LaunchProcess(ioManager.ConcatPath(resolvedScriptsDir, I), resolvedScriptsDir, String.Join(
' ', parameters), noShellExecute:
true))
453 using (cancellationToken.Register(() => script.Terminate()))
455 var exitCode = await script.Lifetime.ConfigureAwait(
false);
456 cancellationToken.ThrowIfCancellationRequested();
467 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
468 var path = ValidateConfigRelativePath(configurationRelativePath);
473 void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path);
475 if (systemIdentity != null)
476 await systemIdentity.
RunImpersonated(CheckDeleteImpl, cancellationToken).ConfigureAwait(
false);
readonly ISynchronousIOManager synchronousIOManager
The ISynchronousIOManager for Configuration
async Task< bool > DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
Attempt to delete an empty directory at configurationRelativePath
async Task< ServerSideModifications > CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken)
Copies all files in the CodeModifications directory to destination
async Task EnsureDirectories(CancellationToken cancellationToken)
Ensures standard configuration directories exist
async Task< bool > CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
Create an empty directory at configurationRelativePath
async Task< ConfigurationFile > Write(string configurationRelativePath, ISystemIdentity systemIdentity, byte[] data, string previousHash, CancellationToken cancellationToken)
Writes to a given configurationRelativePath
Represents a user on the current System.Runtime.InteropServices.OSPlatform
For launching IProcess'
Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IPlatformIdentifier platformIdentifier, ILogger< Configuration > logger)
Construct Configuration
async Task< IReadOnlyList< ConfigurationFile > > ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
Get ConfigurationFile for all items in a given configurationRelativePath
EventType
Types of events. Mirror in tgs.dm
For creating filesystem symbolic links
readonly IProcessExecutor processExecutor
The IProcessExecutor for Configuration
bool IsDirectory
If Path represents a directory
readonly ILogger< Configuration > logger
The ILogger for Configuration
async Task< bool > HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for Configuration
For accessing the disk in a synchronous manner
string ValidateConfigRelativePath(string configurationRelativePath)
readonly ISymlinkFactory symlinkFactory
The ISymlinkFactory for Configuration
Task RunImpersonated(Action action, CancellationToken cancellationToken)
Runs a given action in the context of the ISystemIdentity
readonly IIOManager ioManager
The IIOManager for Configuration
async Task< ConfigurationFile > Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
Reads a given configurationRelativePath
readonly SemaphoreSlim semaphore
The SemaphoreSlim for Configuration
Represents a game configuration file. Create and delete actions uncerimonuously overwrite/delete file...
For managing the Configuration directory
readonly IPostWriteHandler postWriteHandler
The IPostWriteHandler for Configuration
bool AccessDenied
If access to the ConfigurationFile file was denied for the operation
Represents code modifications via configuration
Interface for using filesystems
async Task SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken)
Symlinks all directories in the GameData directory to destination
static async Task< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore
Async lock context helper