tgstation-server  4.4.0
The /tg/station 13 server suite
ByondManager.cs
Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
5 using System.Net;
6 using System.Text;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Tgstation.Server.Api;
13 using Tgstation.Server.Host.IO;
15 
16 namespace Tgstation.Server.Host.Components.Byond
17 {
19  sealed class ByondManager : IByondManager
20  {
24  public const string BinPath = "byond/bin";
25 
29  const string CfgDirectoryName = "cfg";
30 
34  const string TrustedDmbFileName = "trusted.txt";
35 
39  const string VersionFileName = "Version.txt";
40 
44  const string ActiveVersionFileName = "ActiveVersion.txt";
45 
47  public Version ActiveVersion { get; private set; }
48 
50  public IReadOnlyList<Version> InstalledVersions
51  {
52  get
53  {
54  lock (installedVersions)
55  return installedVersions.Select(x => Version.Parse(x.Key).Semver()).ToList();
56  }
57  }
58 
63 
68 
73 
77  readonly ILogger<ByondManager> logger;
78 
82  readonly Dictionary<string, Task> installedVersions;
83 
87  readonly SemaphoreSlim semaphore;
88 
95  static string VersionKey(Version version, bool allowPatch) => (allowPatch && version.Build > 0
96  ? new Version(version.Major, version.Minor, version.Build)
97  : new Version(version.Major, version.Minor)).ToString();
98 
106  public ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, IEventConsumer eventConsumer, ILogger<ByondManager> logger)
107  {
108  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
109  this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
110  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
111  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
112 
113  installedVersions = new Dictionary<string, Task>();
114  semaphore = new SemaphoreSlim(1);
115  }
116 
118  public void Dispose() => semaphore.Dispose();
119 
127  async Task<string> InstallVersion(Version version, byte[] versionZipBytes, CancellationToken cancellationToken)
128  {
129  var ourTcs = new TaskCompletionSource<object>();
130  Task inProgressTask;
131  string versionKey;
132  bool installed;
133  lock (installedVersions)
134  {
135  if (versionZipBytes != null)
136  {
137  int customInstallationNumber = 1;
138  do
139  {
140  versionKey = $"{VersionKey(version, false)}.{customInstallationNumber++}";
141  }
142  while (installedVersions.ContainsKey(versionKey));
143  }
144  else
145  versionKey = VersionKey(version, true);
146 
147  installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
148  if (!installed)
149  installedVersions.Add(versionKey, ourTcs.Task);
150  }
151 
152  if (installed)
153  using (cancellationToken.Register(() => ourTcs.SetCanceled()))
154  {
155  await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false);
156  cancellationToken.ThrowIfCancellationRequested();
157  return versionKey;
158  }
159 
160  if (versionZipBytes != null)
161  logger.LogInformation("Installing custom BYOND version as {0}...", versionKey);
162  else if (version.Build > 0)
163  throw new JobException(ErrorCode.ByondNonExistentCustomVersion);
164  else
165  logger.LogDebug("Requested BYOND version {0} not currently installed. Doing so now...");
166 
167  // okay up to us to install it then
168  try
169  {
170  await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List<string> { versionKey }, cancellationToken).ConfigureAwait(false);
171  var zipFileBytesTask = versionZipBytes == null
172  ? byondInstaller.DownloadVersion(version, cancellationToken)
173  : Task.FromResult(versionZipBytes);
174 
175  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
176 
177  try
178  {
179  versionZipBytes = await zipFileBytesTask.ConfigureAwait(false);
180  await ioManager.CreateDirectory(versionKey, cancellationToken).ConfigureAwait(false);
181 
182  var extractPath = ioManager.ResolvePath(versionKey);
183  logger.LogTrace("Extracting downloaded BYOND zip to {0}...", extractPath);
184  await ioManager.ZipToDirectory(extractPath, versionZipBytes, cancellationToken).ConfigureAwait(false);
185  versionZipBytes = null;
186 
187  await byondInstaller.InstallByond(extractPath, version, cancellationToken).ConfigureAwait(false);
188 
189  // make sure to do this last because this is what tells us we have a valid version in the future
190  await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(versionKey), cancellationToken).ConfigureAwait(false);
191  }
192  catch (WebException e)
193  {
194  // since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
195  throw new JobException(ErrorCode.ByondDownloadFail, e);
196  }
197  catch (OperationCanceledException)
198  {
199  throw;
200  }
201  catch
202  {
203  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
204  throw;
205  }
206 
207  ourTcs.SetResult(null);
208  }
209  catch (Exception e)
210  {
211  if (!(e is OperationCanceledException))
212  await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List<string> { e.Message }, cancellationToken).ConfigureAwait(false);
213  lock (installedVersions)
214  installedVersions.Remove(versionKey);
215  ourTcs.SetException(e);
216  throw;
217  }
218 
219  return versionKey;
220  }
221 
223  public async Task ChangeVersion(Version version, byte[] customVersionBytes, CancellationToken cancellationToken)
224  {
225  if (version == null)
226  throw new ArgumentNullException(nameof(version));
227 
228  var versionKey = await InstallVersion(version, customVersionBytes, cancellationToken).ConfigureAwait(false);
229  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
230  {
231  await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(versionKey), cancellationToken).ConfigureAwait(false);
232  await eventConsumer.HandleEvent(
233  EventType.ByondActiveVersionChange,
234  new List<string>
235  {
236  ActiveVersion != null
237  ? VersionKey(ActiveVersion, true)
238  : null,
239  versionKey
240  },
241  cancellationToken)
242  .ConfigureAwait(false);
243 
244  // We reparse the version key because it could be changed after a custom install.
245  ActiveVersion = Version.Parse(versionKey);
246  }
247  }
248 
250  public async Task<IByondExecutableLock> UseExecutables(Version requiredVersion, CancellationToken cancellationToken)
251  {
252  var versionToUse = requiredVersion ?? ActiveVersion;
253  if (versionToUse == null)
254  throw new JobException(ErrorCode.ByondNoVersionsInstalled);
255  await InstallVersion(versionToUse, null, cancellationToken).ConfigureAwait(false);
256 
257  var versionKey = VersionKey(versionToUse, true);
258  var binPathForVersion = ioManager.ConcatPath(versionKey, BinPath);
259 
260  logger.LogTrace("Creating ByondExecutableLock lock for version {0}", requiredVersion);
261  return new ByondExecutableLock(
262  ioManager,
263  semaphore,
264  versionToUse,
265  ioManager.ResolvePath(
266  ioManager.ConcatPath(
267  binPathForVersion,
268  byondInstaller.DreamDaemonName)),
269  ioManager.ResolvePath(
270  ioManager.ConcatPath(
271  binPathForVersion,
272  byondInstaller.DreamMakerName)),
273  ioManager.ResolvePath(
274  ioManager.ConcatPath(
275  byondInstaller.PathToUserByondFolder,
276  CfgDirectoryName,
277  TrustedDmbFileName)));
278  }
279 
281  public async Task StartAsync(CancellationToken cancellationToken)
282  {
283  async Task<byte[]> GetActiveVersion()
284  {
285  var activeVersionFileExists = await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
286  return !activeVersionFileExists ? null : await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
287  }
288 
289  var activeVersionBytesTask = GetActiveVersion();
290 
291  // Create local cfg directory in case it doesn't exist
292  var localCfgDirectory = ioManager.ConcatPath(
293  byondInstaller.PathToUserByondFolder,
294  CfgDirectoryName);
295  await ioManager.CreateDirectory(
296  localCfgDirectory,
297  cancellationToken).ConfigureAwait(false);
298 
299  // Delete trusted.txt so it doesn't grow too large
300  var trustedFilePath =
301  ioManager.ConcatPath(
302  localCfgDirectory,
303  TrustedDmbFileName);
304  logger.LogTrace("Deleting trusted .dmbs file {0}", trustedFilePath);
305  await ioManager.DeleteFile(
306  trustedFilePath,
307  cancellationToken).ConfigureAwait(false);
308 
309  var byondDirectory = ioManager.ResolvePath();
310  await ioManager.CreateDirectory(byondDirectory, cancellationToken).ConfigureAwait(false);
311  var directories = await ioManager.GetDirectories(byondDirectory, cancellationToken).ConfigureAwait(false);
312 
313  async Task ReadVersion(string path)
314  {
315  var versionFile = ioManager.ConcatPath(path, VersionFileName);
316  if (!await ioManager.FileExists(versionFile, cancellationToken).ConfigureAwait(false))
317  {
318  logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path));
319  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); // cleanup
320  return;
321  }
322 
323  var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken).ConfigureAwait(false);
324  var text = Encoding.UTF8.GetString(bytes);
325  if (Version.TryParse(text, out var version))
326  {
327  var key = VersionKey(version, true);
328  lock (installedVersions)
329  if (!installedVersions.ContainsKey(key))
330  {
331  logger.LogDebug("Adding detected BYOND version {0}...", key);
332  installedVersions.Add(key, Task.CompletedTask);
333  return;
334  }
335  }
336 
337  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false);
338  }
339 
340  await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false);
341 
342  var activeVersionBytes = await activeVersionBytesTask.ConfigureAwait(false);
343  if (activeVersionBytes != null)
344  {
345  var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
346  bool hasRequestedActiveVersion;
347  lock (installedVersions)
348  hasRequestedActiveVersion = installedVersions.ContainsKey(activeVersionString);
349  if (hasRequestedActiveVersion && Version.TryParse(activeVersionString, out var activeVersion))
350  ActiveVersion = activeVersion.Semver();
351  else
352  {
353  logger.LogWarning("Failed to load saved active version {0}!", activeVersionString);
354  await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
355  }
356  }
357  }
358 
360  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
361  }
362 }
readonly IIOManager ioManager
The IIOManager for the ByondManager
Definition: ByondManager.cs:62
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
async Task< string > InstallVersion(Version version, byte[] versionZipBytes, CancellationToken cancellationToken)
Installs a BYOND version if it isn&#39;t already
async Task StartAsync(CancellationToken cancellationToken)
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
async Task< IByondExecutableLock > UseExecutables(Version requiredVersion, CancellationToken cancellationToken)
Lock the current installation&#39;s location and return a IByondExecutableLock
For downloading and installing BYOND extractions for a given system
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:9
async Task ChangeVersion(Version version, byte[] customVersionBytes, CancellationToken cancellationToken)
Change the active BYOND version
For managing the BYOND installation
readonly Dictionary< string, Task > installedVersions
Map of byond Versions to Tasks that complete when they are installed
Definition: ByondManager.cs:82
ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, IEventConsumer eventConsumer, ILogger< ByondManager > logger)
Construct a ByondManager
Consumes EventTypes and takes the appropriate actions
readonly ILogger< ByondManager > logger
The ILogger for the ByondManager
Definition: ByondManager.cs:77
Represents a BYOND installation. RawData.Content is used to upload custom BYOND version zip files...
Definition: Byond.cs:9
readonly IByondInstaller byondInstaller
The IByondInstaller for the ByondManager
Definition: ByondManager.cs:67
Interface for using filesystems
Definition: IIOManager.cs:11
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the ByondManager
Definition: ByondManager.cs:87
static async Task< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore
readonly IEventConsumer eventConsumer
The IEventConsumer for the ByondManager
Definition: ByondManager.cs:72