tgstation-server
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.Runtime.InteropServices;
8 using System.Security.Cryptography;
9 using System.Text;
10 using System.Threading;
11 using System.Threading.Tasks;
14 using Tgstation.Server.Host.IO;
16 
17 namespace Tgstation.Server.Host.Components.StaticFiles
18 {
21  {
22  const string CodeModificationsSubdirectory = "CodeModifications";
23  const string EventScriptsSubdirectory = "EventScripts";
24  const string GameStaticFilesSubdirectory = "GameStaticFiles";
25 
29  const string StaticIgnoreFile = ".tgsignore";
30 
31  const string CodeModificationsHeadFile = "HeadInclude.dm";
32  const string CodeModificationsTailFile = "TailInclude.dm";
33 
34  static readonly IReadOnlyDictionary<EventType, string> EventTypeScriptFileNameMap = new Dictionary<EventType, string>
35  {
36  { EventType.CompileStart, "PreCompile" },
37  { EventType.CompileComplete, "PostCompile" },
38  { EventType.RepoPreSynchronize, "PreSynchronize" }
39  };
40 
45 
50 
55 
60 
65 
70 
74  readonly ILogger<Configuration> logger;
75 
79  readonly SemaphoreSlim semaphore;
80 
91  public Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IPlatformIdentifier platformIdentifier, ILogger<Configuration> logger)
92  {
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));
100 
101  semaphore = new SemaphoreSlim(1);
102  }
103 
105  public void Dispose() => semaphore.Dispose();
106 
111  string StaticIgnorePath() => ioManager.ConcatPath(GameStaticFilesSubdirectory, StaticIgnoreFile);
112 
118  async Task EnsureDirectories(CancellationToken cancellationToken)
119  {
120  async Task ValidateStaticFolder()
121  {
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);
126  }
127 
128  await Task.WhenAll(ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken), ioManager.CreateDirectory(EventScriptsSubdirectory, cancellationToken), ValidateStaticFolder()).ConfigureAwait(false);
129  }
130 
132  public async Task<ServerSideModifications> CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken)
133  {
134 
135  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
136  {
137  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
138 
139  //just assume no other fs race conditions here
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);
144 
145  await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask).ConfigureAwait(false);
146 
147  if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result)
148  return null;
149 
150  if (dmeExistsTask.Result)
151  return new ServerSideModifications(null, null, true);
152 
153  if (!headFileExistsTask.Result && !tailFileExistsTask.Result)
154  return null;
155 
156  string IncludeLine(string filePath) => String.Format(CultureInfo.InvariantCulture, "#include \"{0}\"", filePath);
157 
158  return new ServerSideModifications(headFileExistsTask.Result ? IncludeLine(CodeModificationsHeadFile) : null, tailFileExistsTask.Result ? IncludeLine(CodeModificationsTailFile) : null, false);
159  }
160  }
161 
162  string ValidateConfigRelativePath(string configurationRelativePath)
163  {
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) //.. fuccbois
172  throw new InvalidOperationException("Attempted to access file outside of configuration manager!");
173  return resolved;
174  }
175 
177  public async Task<IReadOnlyList<ConfigurationFile>> ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
178  {
179  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
180  var path = ValidateConfigRelativePath(configurationRelativePath);
181 
182  if (configurationRelativePath == null)
183  configurationRelativePath = "/";
184 
185  List<ConfigurationFile> result = new List<ConfigurationFile>();
186 
187  void ListImpl()
188  {
189  var enumerator = synchronousIOManager.GetDirectories(path, cancellationToken);
190  try
191  {
192  result.AddRange(enumerator.Select(x => new ConfigurationFile
193  {
194  IsDirectory = true,
195  Path = ioManager.ConcatPath(configurationRelativePath, x),
196  }));
197  }
198  catch (IOException e)
199  {
200  logger.LogDebug("IOException while writing {0}: {1}", path, e);
201  result = null;
202  return;
203  }
204  enumerator = synchronousIOManager.GetFiles(path, cancellationToken);
205  result.AddRange(enumerator.Select(x => new ConfigurationFile
206  {
207  IsDirectory = false,
208  Path = ioManager.ConcatPath(configurationRelativePath, x),
209  }));
210  }
211 
212  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
213  if (systemIdentity == null)
214  ListImpl();
215  else
216  await systemIdentity.RunImpersonated(ListImpl, cancellationToken).ConfigureAwait(false);
217 
218  return result;
219  }
220 
222  public async Task<ConfigurationFile> Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
223  {
224  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
225  var path = ValidateConfigRelativePath(configurationRelativePath);
226 
227  ConfigurationFile result = null;
228 
229  void ReadImpl()
230  {
231  lock (this)
232  try
233  {
234  var content = synchronousIOManager.ReadFile(path);
235  string sha1String;
236 #pragma warning disable CA5350 // Do not use insecure cryptographic algorithm SHA1.
237  using (var sha1 = new SHA1Managed())
238 #pragma warning restore CA5350 // Do not use insecure cryptographic algorithm SHA1.
239  sha1String = String.Join("", sha1.ComputeHash(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
240  result = new ConfigurationFile
241  {
242  Content = content,
243  IsDirectory = false,
244  LastReadHash = sha1String,
245  AccessDenied = false,
246  Path = configurationRelativePath
247  };
248  }
249  catch (UnauthorizedAccessException)
250  {
251  //this happens on windows, dunno about linux
252  bool isDirectory;
253  try
254  {
255  isDirectory = synchronousIOManager.IsDirectory(path);
256  }
257  catch
258  {
259  isDirectory = false;
260  }
261 
262  result = new ConfigurationFile
263  {
264  Path = configurationRelativePath
265  };
266  if (!isDirectory)
267  result.AccessDenied = true;
268  else
269  result.IsDirectory = true;
270  }
271  }
272 
273  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
274  if (systemIdentity == null)
275  await Task.Factory.StartNew(ReadImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
276  else
277  await systemIdentity.RunImpersonated(ReadImpl, cancellationToken).ConfigureAwait(false);
278 
279  return result;
280  }
281 
283  public async Task SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken)
284  {
285  async Task<IReadOnlyList<string>> GetIgnoreFiles()
286  {
287  var ignoreFileBytes = await ioManager.ReadAllBytes(StaticIgnorePath(), cancellationToken).ConfigureAwait(false);
288  var ignoreFileText = Encoding.UTF8.GetString(ignoreFileBytes);
289 
290  var results = new List<string> { StaticIgnoreFile };
291 
292  //we don't want to lose trailing whitespace on linux
293  using (var reader = new StringReader(ignoreFileText))
294  {
295  cancellationToken.ThrowIfCancellationRequested();
296  var line = await reader.ReadLineAsync().ConfigureAwait(false);
297  if (!String.IsNullOrEmpty(line))
298  results.Add(line);
299  }
300 
301  return results;
302  };
303 
304  IReadOnlyList<string> ignoreFiles;
305 
306  async Task SymlinkBase(bool files)
307  {
308  Task<IReadOnlyList<string>> task;
309  if (files)
310  task = ioManager.GetFiles(GameStaticFilesSubdirectory, cancellationToken);
311  else
312  task = ioManager.GetDirectories(GameStaticFilesSubdirectory, cancellationToken);
313  var entries = await task.ConfigureAwait(false);
314 
315  await Task.WhenAll(entries.Select(async x =>
316  {
317  var fileName = ioManager.GetFileName(x);
318 
319  bool ignored;
320  if (platformIdentifier.IsWindows)
321  //need to normalize
322  ignored = ignoreFiles.Any(y => fileName.ToUpperInvariant() == y.ToUpperInvariant());
323  else
324  ignored = ignoreFiles.Any(y => fileName == y);
325 
326  if (ignored)
327  {
328  logger.LogTrace("Ignoring static file {0}...", fileName);
329  return;
330  }
331 
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);
338  if (fileExists)
339  await ioManager.DeleteFile(destPath, cancellationToken).ConfigureAwait(false);
340  await symlinkFactory.CreateSymbolicLink(ioManager.ResolvePath(x), ioManager.ResolvePath(destPath), cancellationToken).ConfigureAwait(false);
341  })).ConfigureAwait(false);
342  }
343 
344  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
345  {
346  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
347  ignoreFiles = await GetIgnoreFiles().ConfigureAwait(false);
348  await Task.WhenAll(SymlinkBase(true), SymlinkBase(false)).ConfigureAwait(false);
349  }
350  }
351 
353  public async Task<ConfigurationFile> Write(string configurationRelativePath, ISystemIdentity systemIdentity, byte[] data, string previousHash, CancellationToken cancellationToken)
354  {
355  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
356  var path = ValidateConfigRelativePath(configurationRelativePath);
357 
358  ConfigurationFile result = null;
359 
360  void WriteImpl()
361  {
362  lock (this)
363  try
364  {
365  var fileHash = previousHash;
366  var success = synchronousIOManager.WriteFileChecked(path, data, ref fileHash, cancellationToken);
367  if (!success)
368  return;
369  if (data != null)
370  postWriteHandler.HandleWrite(path);
371  result = new ConfigurationFile
372  {
373  Content = data,
374  IsDirectory = false,
375  LastReadHash = fileHash,
376  AccessDenied = false,
377  Path = configurationRelativePath
378  };
379  }
380  catch (UnauthorizedAccessException)
381  {
382  //this happens on windows, dunno about linux
383  bool isDirectory;
384  try
385  {
386  isDirectory = synchronousIOManager.IsDirectory(path);
387  }
388  catch
389  {
390  isDirectory = false;
391  }
392 
393  result = new ConfigurationFile
394  {
395  Path = configurationRelativePath
396  };
397  if (!isDirectory)
398  result.AccessDenied = true;
399  else
400  result.IsDirectory = true;
401  }
402  }
403 
404  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
405  if (systemIdentity == null)
406  await Task.Factory.StartNew(WriteImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
407  else
408  await systemIdentity.RunImpersonated(WriteImpl, cancellationToken).ConfigureAwait(false);
409 
410  return result;
411  }
412 
414  public async Task<bool> CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
415  {
416  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
417  var path = ValidateConfigRelativePath(configurationRelativePath);
418 
419  bool? result = null;
420  void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken);
421 
422  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
423  if (systemIdentity == null)
424  await Task.Factory.StartNew(DoCreate, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
425  else
426  await systemIdentity.RunImpersonated(DoCreate, cancellationToken).ConfigureAwait(false);
427 
428  return result.Value;
429  }
430 
432  public Task StartAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
433 
435  public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
436 
438  public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
439  {
440  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
441 
442  if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptName))
443  return true;
444 
445  //always execute in serial
446  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
447  {
448  var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, cancellationToken).ConfigureAwait(false);
449  var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
450 
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()))
454  {
455  var exitCode = await script.Lifetime.ConfigureAwait(false);
456  cancellationToken.ThrowIfCancellationRequested();
457  if (exitCode != 0)
458  return false;
459  }
460  }
461  return true;
462  }
463 
465  public async Task<bool> DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
466  {
467  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
468  var path = ValidateConfigRelativePath(configurationRelativePath);
469 
470  var result = false;
471  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
472  {
473  void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path);
474 
475  if (systemIdentity != null)
476  await systemIdentity.RunImpersonated(CheckDeleteImpl, cancellationToken).ConfigureAwait(false);
477  else
478  CheckDeleteImpl();
479  }
480  return result;
481  }
482  }
483 }
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
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
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
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