tgstation-server  4.3.2
The /tg/station 13 server suite
Configuration.cs
Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Globalization;
5 using System.IO;
6 using System.Linq;
7 using System.Security.Cryptography;
8 using System.Text;
9 using System.Threading;
10 using System.Threading.Tasks;
14 using Tgstation.Server.Host.IO;
18 
19 namespace Tgstation.Server.Host.Components.StaticFiles
20 {
23  {
24  const string CodeModificationsSubdirectory = "CodeModifications";
25  const string EventScriptsSubdirectory = "EventScripts";
26  const string GameStaticFilesSubdirectory = "GameStaticFiles";
27 
31  const string StaticIgnoreFile = ".tgsignore";
32 
33  const string CodeModificationsHeadFile = "HeadInclude.dm";
34  const string CodeModificationsTailFile = "TailInclude.dm";
35 
36  static readonly IReadOnlyDictionary<EventType, string> EventTypeScriptFileNameMap = new Dictionary<EventType, string>(
37  Enum.GetValues(typeof(EventType))
38  .OfType<EventType>()
39  .Select(
40  eventType => new KeyValuePair<EventType, string>(
41  eventType,
42  typeof(EventType)
43  .GetField(eventType.ToString())
44  .GetCustomAttributes(false)
45  .OfType<EventScriptAttribute>()
46  .First()
47  .ScriptName)));
48 
53 
58 
63 
68 
73 
78 
82  readonly ILogger<Configuration> logger;
83 
87  readonly SemaphoreSlim semaphore;
88 
99  public Configuration(
100  IIOManager ioManager,
101  ISynchronousIOManager synchronousIOManager,
102  ISymlinkFactory symlinkFactory,
103  IProcessExecutor processExecutor,
104  IPostWriteHandler postWriteHandler,
105  IPlatformIdentifier platformIdentifier,
106  ILogger<Configuration> logger)
107  {
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));
115 
116  semaphore = new SemaphoreSlim(1);
117  }
118 
120  public void Dispose() => semaphore.Dispose();
121 
126  string StaticIgnorePath() => ioManager.ConcatPath(GameStaticFilesSubdirectory, StaticIgnoreFile);
127 
133  async Task EnsureDirectories(CancellationToken cancellationToken)
134  {
135  async Task ValidateStaticFolder()
136  {
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);
141  }
142 
143  await Task.WhenAll(ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken), ioManager.CreateDirectory(EventScriptsSubdirectory, cancellationToken), ValidateStaticFolder()).ConfigureAwait(false);
144  }
145 
147  public async Task<ServerSideModifications> CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken)
148  {
149  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
150  {
151  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
152 
153  // just assume no other fs race conditions here
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);
158 
159  await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask).ConfigureAwait(false);
160 
161  if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result)
162  return null;
163 
164  if (dmeExistsTask.Result)
165  return new ServerSideModifications(null, null, true);
166 
167  if (!headFileExistsTask.Result && !tailFileExistsTask.Result)
168  return null;
169 
170  static string IncludeLine(string filePath) => String.Format(CultureInfo.InvariantCulture, "#include \"{0}\"", filePath);
171 
172  return new ServerSideModifications(headFileExistsTask.Result ? IncludeLine(CodeModificationsHeadFile) : null, tailFileExistsTask.Result ? IncludeLine(CodeModificationsTailFile) : null, false);
173  }
174  }
175 
176  string ValidateConfigRelativePath(string configurationRelativePath)
177  {
178  var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath);
179  if (nullOrEmptyCheck)
180  configurationRelativePath = DefaultIOManager.CurrentDirectory;
181  if (configurationRelativePath[0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar)
182  configurationRelativePath = DefaultIOManager.CurrentDirectory + configurationRelativePath;
183  var resolved = ioManager.ResolvePath(configurationRelativePath);
184  var local = !nullOrEmptyCheck ? ioManager.ResolvePath() : null;
185  if (!nullOrEmptyCheck && resolved.Length < local.Length) // .. fuccbois
186  throw new InvalidOperationException("Attempted to access file outside of configuration manager!");
187  return resolved;
188  }
189 
191  public async Task<IReadOnlyList<ConfigurationFile>> ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
192  {
193  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
194  var path = ValidateConfigRelativePath(configurationRelativePath);
195 
196  if (configurationRelativePath == null)
197  configurationRelativePath = "/";
198 
199  List<ConfigurationFile> result = new List<ConfigurationFile>();
200 
201  void ListImpl()
202  {
203  var enumerator = synchronousIOManager.GetDirectories(path, cancellationToken);
204  try
205  {
206  result.AddRange(enumerator.Select(x => new ConfigurationFile
207  {
208  IsDirectory = true,
209  Path = ioManager.ConcatPath(configurationRelativePath, x),
210  }).OrderBy(file => file.Path));
211  }
212  catch (IOException e)
213  {
214  logger.LogDebug("IOException while writing {0}: {1}", path, e);
215  result = null;
216  return;
217  }
218 
219  enumerator = synchronousIOManager.GetFiles(path, cancellationToken);
220  result.AddRange(enumerator.Select(x => new ConfigurationFile
221  {
222  IsDirectory = false,
223  Path = ioManager.ConcatPath(configurationRelativePath, x),
224  }).OrderBy(file => file.Path));
225  }
226 
227  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
228  if (systemIdentity == null)
229  ListImpl();
230  else
231  await systemIdentity.RunImpersonated(ListImpl, cancellationToken).ConfigureAwait(false);
232 
233  return result;
234  }
235 
237  public async Task<ConfigurationFile> Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
238  {
239  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
240  var path = ValidateConfigRelativePath(configurationRelativePath);
241 
242  ConfigurationFile result = null;
243 
244  void ReadImpl()
245  {
246  lock (semaphore)
247  try
248  {
249  var content = synchronousIOManager.ReadFile(path);
250  string sha1String;
251 #pragma warning disable CA5350 // Do not use insecure cryptographic algorithm SHA1.
252  using (var sha1 = new SHA1Managed())
253 #pragma warning restore CA5350 // Do not use insecure cryptographic algorithm SHA1.
254  sha1String = String.Join(String.Empty, sha1.ComputeHash(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
255  result = new ConfigurationFile
256  {
257  Content = content,
258  IsDirectory = false,
259  LastReadHash = sha1String,
260  AccessDenied = false,
261  Path = configurationRelativePath
262  };
263  }
264  catch (UnauthorizedAccessException)
265  {
266  // this happens on windows, dunno about linux
267  bool isDirectory;
268  try
269  {
270  isDirectory = synchronousIOManager.IsDirectory(path);
271  }
272  catch
273  {
274  isDirectory = false;
275  }
276 
277  result = new ConfigurationFile
278  {
279  Path = configurationRelativePath
280  };
281  if (!isDirectory)
282  result.AccessDenied = true;
283  else
284  result.IsDirectory = true;
285  }
286  }
287 
288  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
289  if (systemIdentity == null)
290  await Task.Factory.StartNew(ReadImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
291  else
292  await systemIdentity.RunImpersonated(ReadImpl, cancellationToken).ConfigureAwait(false);
293 
294  return result;
295  }
296 
298  public async Task SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken)
299  {
300  async Task<IReadOnlyList<string>> GetIgnoreFiles()
301  {
302  var ignoreFileBytes = await ioManager.ReadAllBytes(StaticIgnorePath(), cancellationToken).ConfigureAwait(false);
303  var ignoreFileText = Encoding.UTF8.GetString(ignoreFileBytes);
304 
305  var results = new List<string> { StaticIgnoreFile };
306 
307  // we don't want to lose trailing whitespace on linux
308  using (var reader = new StringReader(ignoreFileText))
309  {
310  cancellationToken.ThrowIfCancellationRequested();
311  var line = await reader.ReadLineAsync().ConfigureAwait(false);
312  if (!String.IsNullOrEmpty(line))
313  results.Add(line);
314  }
315 
316  return results;
317  }
318 
319  IReadOnlyList<string> ignoreFiles;
320 
321  async Task SymlinkBase(bool files)
322  {
323  Task<IReadOnlyList<string>> task;
324  if (files)
325  task = ioManager.GetFiles(GameStaticFilesSubdirectory, cancellationToken);
326  else
327  task = ioManager.GetDirectories(GameStaticFilesSubdirectory, cancellationToken);
328  var entries = await task.ConfigureAwait(false);
329 
330  await Task.WhenAll(entries.Select(async x =>
331  {
332  var fileName = ioManager.GetFileName(x);
333 
334  // need to normalize
335  bool ignored;
336  if (platformIdentifier.IsWindows)
337  ignored = ignoreFiles.Any(y => fileName.ToUpperInvariant() == y.ToUpperInvariant());
338  else
339  ignored = ignoreFiles.Any(y => fileName == y);
340 
341  if (ignored)
342  {
343  logger.LogTrace("Ignoring static file {0}...", fileName);
344  return;
345  }
346 
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);
353  if (fileExists)
354  await ioManager.DeleteFile(destPath, cancellationToken).ConfigureAwait(false);
355  await symlinkFactory.CreateSymbolicLink(ioManager.ResolvePath(x), ioManager.ResolvePath(destPath), cancellationToken).ConfigureAwait(false);
356  })).ConfigureAwait(false);
357  }
358 
359  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
360  {
361  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
362  ignoreFiles = await GetIgnoreFiles().ConfigureAwait(false);
363  await Task.WhenAll(SymlinkBase(true), SymlinkBase(false)).ConfigureAwait(false);
364  }
365  }
366 
368  public async Task<ConfigurationFile> Write(string configurationRelativePath, ISystemIdentity systemIdentity, byte[] data, string previousHash, CancellationToken cancellationToken)
369  {
370  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
371  var path = ValidateConfigRelativePath(configurationRelativePath);
372 
373  ConfigurationFile result = null;
374 
375  void WriteImpl()
376  {
377  lock (semaphore)
378  try
379  {
380  var fileHash = previousHash;
381  var success = synchronousIOManager.WriteFileChecked(path, data, ref fileHash, cancellationToken);
382  if (!success)
383  return;
384  if (data != null)
385  postWriteHandler.HandleWrite(path);
386  result = new ConfigurationFile
387  {
388  Content = data,
389  IsDirectory = false,
390  LastReadHash = fileHash,
391  AccessDenied = false,
392  Path = configurationRelativePath
393  };
394  }
395  catch (UnauthorizedAccessException)
396  {
397  // this happens on windows, dunno about linux
398  bool isDirectory;
399  try
400  {
401  isDirectory = synchronousIOManager.IsDirectory(path);
402  }
403  catch
404  {
405  isDirectory = false;
406  }
407 
408  result = new ConfigurationFile
409  {
410  Path = configurationRelativePath
411  };
412  if (!isDirectory)
413  result.AccessDenied = true;
414  else
415  result.IsDirectory = true;
416  }
417  }
418 
419  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
420  if (systemIdentity == null)
421  await Task.Factory.StartNew(WriteImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
422  else
423  await systemIdentity.RunImpersonated(WriteImpl, cancellationToken).ConfigureAwait(false);
424 
425  return result;
426  }
427 
429  public async Task<bool> CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
430  {
431  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
432  var path = ValidateConfigRelativePath(configurationRelativePath);
433 
434  bool? result = null;
435  void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken);
436 
437  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
438  if (systemIdentity == null)
439  await Task.Factory.StartNew(DoCreate, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
440  else
441  await systemIdentity.RunImpersonated(DoCreate, cancellationToken).ConfigureAwait(false);
442 
443  return result.Value;
444  }
445 
447  public Task StartAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
448 
450  public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
451 
453  public async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
454  {
455  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
456 
457  if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptName))
458  return;
459 
460  // always execute in serial
461  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
462  {
463  var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, false, cancellationToken).ConfigureAwait(false);
464  var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
465 
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),
469  resolvedScriptsDir,
470  String.Join(' ', parameters),
471  true,
472  true,
473  true))
474  using (cancellationToken.Register(() => script.Terminate()))
475  {
476  var exitCode = await script.Lifetime.ConfigureAwait(false);
477  cancellationToken.ThrowIfCancellationRequested();
478  var scriptOutput = script.GetCombinedOutput();
479  if (exitCode != 0)
480  throw new JobException($"Script {I} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}");
481  else
482  logger.LogDebug("Script output:{0}{1}", Environment.NewLine, scriptOutput);
483  }
484  }
485  }
486 
488  public async Task<bool> DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
489  {
490  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
491  var path = ValidateConfigRelativePath(configurationRelativePath);
492 
493  var result = false;
494  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
495  {
496  void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path);
497 
498  if (systemIdentity != null)
499  await systemIdentity.RunImpersonated(CheckDeleteImpl, cancellationToken).ConfigureAwait(false);
500  else
501  CheckDeleteImpl();
502  }
503 
504  return result;
505  }
506  }
507 }
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
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
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
Definition: JobException.cs:9
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
string ScriptName
The name of the script the event script the EventType runs.
Interface for using filesystems
Definition: IIOManager.cs:11
async Task SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken)
Symlinks all directories in the GameData directory to destination
For identifying the current platform
static async Task< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore