1 using Microsoft.Extensions.Logging;
3 using System.Collections.Generic;
7 using System.Security.Cryptography;
10 using System.Threading.Tasks;
24 const string CodeModificationsSubdirectory =
"CodeModifications";
25 const string EventScriptsSubdirectory =
"EventScripts";
26 const string GameStaticFilesSubdirectory =
"GameStaticFiles";
31 const string StaticIgnoreFile =
".tgsignore";
33 const string CodeModificationsHeadFile =
"HeadInclude.dm";
34 const string CodeModificationsTailFile =
"TailInclude.dm";
36 static readonly IReadOnlyDictionary<EventType, string> EventTypeScriptFileNameMap =
new Dictionary<EventType, string>(
40 eventType =>
new KeyValuePair<EventType, string>(
43 .GetField(eventType.ToString())
44 .GetCustomAttributes(
false)
82 readonly ILogger<Configuration>
logger;
106 ILogger<Configuration> logger)
108 this.ioManager = ioManager ??
throw new ArgumentNullException(nameof(ioManager));
109 this.synchronousIOManager = synchronousIOManager ??
throw new ArgumentNullException(nameof(synchronousIOManager));
110 this.symlinkFactory = symlinkFactory ??
throw new ArgumentNullException(nameof(symlinkFactory));
111 this.processExecutor = processExecutor ??
throw new ArgumentNullException(nameof(processExecutor));
112 this.postWriteHandler = postWriteHandler ??
throw new ArgumentNullException(nameof(postWriteHandler));
113 this.platformIdentifier = platformIdentifier ??
throw new ArgumentNullException(nameof(platformIdentifier));
114 this.logger = logger ??
throw new ArgumentNullException(nameof(logger));
116 semaphore =
new SemaphoreSlim(1);
120 public void Dispose() => semaphore.Dispose();
126 string StaticIgnorePath() => ioManager.ConcatPath(GameStaticFilesSubdirectory, StaticIgnoreFile);
135 async Task ValidateStaticFolder()
137 await ioManager.CreateDirectory(GameStaticFilesSubdirectory, cancellationToken).ConfigureAwait(
false);
138 var staticIgnorePath = StaticIgnorePath();
139 if(!await ioManager.FileExists(staticIgnorePath, cancellationToken).ConfigureAwait(
false))
140 await ioManager.WriteAllBytes(staticIgnorePath, Array.Empty<byte>(), cancellationToken).ConfigureAwait(
false);
143 await Task.WhenAll(ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken), ioManager.CreateDirectory(EventScriptsSubdirectory, cancellationToken), ValidateStaticFolder()).ConfigureAwait(
false);
147 public async Task<ServerSideModifications>
CopyDMFilesTo(
string dmeFile,
string destination, CancellationToken cancellationToken)
151 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
154 var dmeExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, dmeFile), cancellationToken);
155 var headFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsHeadFile), cancellationToken);
156 var tailFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsTailFile), cancellationToken);
157 var copyTask = ioManager.CopyDirectory(CodeModificationsSubdirectory, destination, null, cancellationToken);
159 await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask).ConfigureAwait(
false);
161 if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result)
164 if (dmeExistsTask.Result)
167 if (!headFileExistsTask.Result && !tailFileExistsTask.Result)
170 static string IncludeLine(
string filePath) => String.Format(CultureInfo.InvariantCulture,
"#include \"{0}\"", filePath);
172 return new ServerSideModifications(headFileExistsTask.Result ? IncludeLine(CodeModificationsHeadFile) : null, tailFileExistsTask.Result ? IncludeLine(CodeModificationsTailFile) : null,
false);
178 var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath);
179 if (nullOrEmptyCheck)
181 if (configurationRelativePath[0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar)
183 var resolved = ioManager.ResolvePath(configurationRelativePath);
184 var local = !nullOrEmptyCheck ? ioManager.ResolvePath() : null;
185 if (!nullOrEmptyCheck && resolved.Length < local.Length)
186 throw new InvalidOperationException(
"Attempted to access file outside of configuration manager!");
191 public async Task<IReadOnlyList<ConfigurationFile>>
ListDirectory(
string configurationRelativePath,
ISystemIdentity systemIdentity, CancellationToken cancellationToken)
193 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
194 var path = ValidateConfigRelativePath(configurationRelativePath);
196 if (configurationRelativePath == null)
197 configurationRelativePath =
"/";
199 List<ConfigurationFile> result =
new List<ConfigurationFile>();
203 var enumerator = synchronousIOManager.GetDirectories(path, cancellationToken);
209 Path = ioManager.ConcatPath(configurationRelativePath, x),
210 }).OrderBy(file => file.Path));
212 catch (IOException e)
214 logger.LogDebug(
"IOException while writing {0}: {1}", path, e);
219 enumerator = synchronousIOManager.GetFiles(path, cancellationToken);
223 Path = ioManager.ConcatPath(configurationRelativePath, x),
224 }).OrderBy(file => file.Path));
228 if (systemIdentity == null)
231 await systemIdentity.
RunImpersonated(ListImpl, cancellationToken).ConfigureAwait(
false);
237 public async Task<ConfigurationFile>
Read(
string configurationRelativePath,
ISystemIdentity systemIdentity, CancellationToken cancellationToken)
239 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
240 var path = ValidateConfigRelativePath(configurationRelativePath);
249 var content = synchronousIOManager.ReadFile(path);
251 #pragma warning disable CA5350 // Do not use insecure cryptographic algorithm SHA1. 252 using (var sha1 =
new SHA1Managed())
253 #pragma warning restore CA5350
254 sha1String = String.Join(String.Empty, sha1.ComputeHash(content).Select(b => b.ToString(
"x2", CultureInfo.InvariantCulture)));
259 LastReadHash = sha1String,
260 AccessDenied =
false,
261 Path = configurationRelativePath
264 catch (UnauthorizedAccessException)
270 isDirectory = synchronousIOManager.
IsDirectory(path);
279 Path = configurationRelativePath
289 if (systemIdentity == null)
290 await Task.Factory.StartNew(ReadImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(
false);
292 await systemIdentity.
RunImpersonated(ReadImpl, cancellationToken).ConfigureAwait(
false);
300 async Task<IReadOnlyList<string>> GetIgnoreFiles()
302 var ignoreFileBytes = await ioManager.ReadAllBytes(StaticIgnorePath(), cancellationToken).ConfigureAwait(
false);
303 var ignoreFileText = Encoding.UTF8.GetString(ignoreFileBytes);
305 var results =
new List<string> { StaticIgnoreFile };
308 using (var reader =
new StringReader(ignoreFileText))
310 cancellationToken.ThrowIfCancellationRequested();
311 var line = await reader.ReadLineAsync().ConfigureAwait(
false);
312 if (!String.IsNullOrEmpty(line))
319 IReadOnlyList<string> ignoreFiles;
321 async Task SymlinkBase(
bool files)
323 Task<IReadOnlyList<string>> task;
325 task = ioManager.GetFiles(GameStaticFilesSubdirectory, cancellationToken);
327 task = ioManager.GetDirectories(GameStaticFilesSubdirectory, cancellationToken);
328 var entries = await task.ConfigureAwait(
false);
330 await Task.WhenAll(entries.Select(async x =>
332 var fileName = ioManager.GetFileName(x);
336 if (platformIdentifier.IsWindows)
337 ignored = ignoreFiles.Any(y => fileName.ToUpperInvariant() == y.ToUpperInvariant());
339 ignored = ignoreFiles.Any(y => fileName == y);
343 logger.LogTrace(
"Ignoring static file {0}...", fileName);
347 var destPath = ioManager.ConcatPath(destination, fileName);
348 logger.LogTrace(
"Symlinking {0} to {1}...", x, destPath);
349 var fileExistsTask = ioManager.FileExists(destPath, cancellationToken);
350 if (await ioManager.DirectoryExists(destPath, cancellationToken).ConfigureAwait(
false))
351 await ioManager.DeleteDirectory(destPath, cancellationToken).ConfigureAwait(
false);
352 var fileExists = await fileExistsTask.ConfigureAwait(
false);
354 await ioManager.DeleteFile(destPath, cancellationToken).ConfigureAwait(
false);
355 await symlinkFactory.CreateSymbolicLink(ioManager.ResolvePath(x), ioManager.ResolvePath(destPath), cancellationToken).ConfigureAwait(
false);
356 })).ConfigureAwait(
false);
361 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
362 ignoreFiles = await GetIgnoreFiles().ConfigureAwait(
false);
363 await Task.WhenAll(SymlinkBase(
true), SymlinkBase(
false)).ConfigureAwait(
false);
368 public async Task<ConfigurationFile>
Write(
string configurationRelativePath,
ISystemIdentity systemIdentity, byte[] data,
string previousHash, CancellationToken cancellationToken)
370 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
371 var path = ValidateConfigRelativePath(configurationRelativePath);
380 var fileHash = previousHash;
381 var success = synchronousIOManager.WriteFileChecked(path, data, ref fileHash, cancellationToken);
385 postWriteHandler.HandleWrite(path);
390 LastReadHash = fileHash,
391 AccessDenied =
false,
392 Path = configurationRelativePath
395 catch (UnauthorizedAccessException)
401 isDirectory = synchronousIOManager.
IsDirectory(path);
410 Path = configurationRelativePath
420 if (systemIdentity == null)
421 await Task.Factory.StartNew(WriteImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(
false);
423 await systemIdentity.
RunImpersonated(WriteImpl, cancellationToken).ConfigureAwait(
false);
431 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
432 var path = ValidateConfigRelativePath(configurationRelativePath);
435 void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken);
438 if (systemIdentity == null)
439 await Task.Factory.StartNew(DoCreate, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(
false);
441 await systemIdentity.
RunImpersonated(DoCreate, cancellationToken).ConfigureAwait(
false);
447 public Task StartAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
450 public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
453 public async Task
HandleEvent(
EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
455 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
457 if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptName))
463 var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension,
false, cancellationToken).ConfigureAwait(
false);
464 var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
466 foreach (var I
in files.Select(x => ioManager.GetFileName(x)).Where(x => x.StartsWith(scriptName, StringComparison.Ordinal)))
467 using (var script = processExecutor.LaunchProcess(
468 ioManager.ConcatPath(resolvedScriptsDir, I),
470 String.Join(
' ', parameters),
474 using (cancellationToken.Register(() => script.Terminate()))
476 var exitCode = await script.Lifetime.ConfigureAwait(
false);
477 cancellationToken.ThrowIfCancellationRequested();
478 var scriptOutput = script.GetCombinedOutput();
480 throw new JobException($
"Script {I} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}");
482 logger.LogDebug(
"Script output:{0}{1}", Environment.NewLine, scriptOutput);
490 await EnsureDirectories(cancellationToken).ConfigureAwait(
false);
491 var path = ValidateConfigRelativePath(configurationRelativePath);
496 void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path);
498 if (systemIdentity != null)
499 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
Handles changing file modes/permissions after writing
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 global::System.Runtime.InteropServices.OSPlatform ...
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
For creating filesystem symbolic links
EventType
Types of events. Mirror in tgs.dm
readonly IProcessExecutor processExecutor
The IProcessExecutor for Configuration
bool IsDirectory
If Path represents a directory
readonly ILogger< Configuration > logger
The ILogger for Configuration
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for Configuration
const string CurrentDirectory
Path to the current working directory for the IIOManager.
For accessing the disk in a synchronous manner
string ValidateConfigRelativePath(string configurationRelativePath)
readonly ISymlinkFactory symlinkFactory
The ISymlinkFactory for Configuration
Operation exceptions thrown from the context of a Models.Job
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
Attribute for indicating the script that a given EventType runs.
readonly SemaphoreSlim semaphore
The SemaphoreSlim for Configuration. Also used as a object.
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
async Task HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
bool AccessDenied
If access to the ConfigurationFile file was denied for the operation
IIOManager that resolves paths to Environment.CurrentDirectory
Represents code modifications via configuration
string ScriptName
The name of the script the event script the EventType runs.
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
For launching IProcess'
Async lock context helper