tgstation-server  4.3.2
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 
94  static string VersionKey(Version version) => new Version(version.Major, version.Minor).ToString();
95 
103  public ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, IEventConsumer eventConsumer, ILogger<ByondManager> logger)
104  {
105  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
106  this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
107  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
108  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
109 
110  installedVersions = new Dictionary<string, Task>();
111  semaphore = new SemaphoreSlim(1);
112  }
113 
115  public void Dispose() => semaphore.Dispose();
116 
123  async Task InstallVersion(Version version, CancellationToken cancellationToken)
124  {
125  var ourTcs = new TaskCompletionSource<object>();
126  Task inProgressTask;
127 
128  var versionKey = VersionKey(version);
129  bool installed;
130  lock (installedVersions)
131  {
132  installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
133  if (!installed)
134  installedVersions.Add(versionKey, ourTcs.Task);
135  }
136 
137  if (installed)
138  using (cancellationToken.Register(() => ourTcs.SetCanceled()))
139  {
140  await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false);
141  cancellationToken.ThrowIfCancellationRequested();
142  return;
143  }
144  else
145  logger.LogDebug("Requested BYOND version {0} not currently installed. Doing so now...");
146 
147  // okay up to us to install it then
148  try
149  {
150  await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List<string> { versionKey }, cancellationToken).ConfigureAwait(false);
151  var downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);
152 
153  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
154 
155  try
156  {
157  var download = await downloadTask.ConfigureAwait(false);
158  await ioManager.CreateDirectory(versionKey, cancellationToken).ConfigureAwait(false);
159 
160  var extractPath = ioManager.ResolvePath(versionKey);
161  logger.LogTrace("Extracting downloaded BYOND zip to {0}...", extractPath);
162  await ioManager.ZipToDirectory(extractPath, download, cancellationToken).ConfigureAwait(false);
163  await byondInstaller.InstallByond(extractPath, version, cancellationToken).ConfigureAwait(false);
164 
165  // make sure to do this last because this is what tells us we have a valid version in the future
166  await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
167  }
168  catch (WebException e)
169  {
170  // since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
171  throw new JobException(ErrorCode.ByondDownloadFail, e);
172  }
173  catch (OperationCanceledException)
174  {
175  throw;
176  }
177  catch
178  {
179  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
180  throw;
181  }
182 
183  ourTcs.SetResult(null);
184  }
185  catch (Exception e)
186  {
187  if (!(e is OperationCanceledException))
188  await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List<string> { e.Message }, cancellationToken).ConfigureAwait(false);
189  lock (installedVersions)
190  installedVersions.Remove(versionKey);
191  ourTcs.SetException(e);
192  throw;
193  }
194  }
195 
197  public async Task ChangeVersion(Version version, CancellationToken cancellationToken)
198  {
199  if (version == null)
200  throw new ArgumentNullException(nameof(version));
201  var versionKey = VersionKey(version);
202  await InstallVersion(version, cancellationToken).ConfigureAwait(false);
203  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
204  {
205  await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(versionKey), cancellationToken).ConfigureAwait(false);
206  await eventConsumer.HandleEvent(EventType.ByondActiveVersionChange, new List<string> { ActiveVersion != null ? VersionKey(ActiveVersion) : null, versionKey }, cancellationToken).ConfigureAwait(false);
207  ActiveVersion = version;
208  }
209  }
210 
212  public async Task<IByondExecutableLock> UseExecutables(Version requiredVersion, CancellationToken cancellationToken)
213  {
214  var versionToUse = requiredVersion ?? ActiveVersion;
215  if (versionToUse == null)
216  throw new JobException(ErrorCode.ByondNoVersionsInstalled);
217  await InstallVersion(versionToUse, cancellationToken).ConfigureAwait(false);
218 
219  var versionKey = VersionKey(versionToUse);
220  var binPathForVersion = ioManager.ConcatPath(versionKey, BinPath);
221 
222  logger.LogTrace("Creating ByondExecutableLock lock for version {0}", requiredVersion);
223  return new ByondExecutableLock(
224  ioManager,
225  semaphore,
226  versionToUse,
227  ioManager.ResolvePath(
228  ioManager.ConcatPath(
229  binPathForVersion,
230  byondInstaller.DreamDaemonName)),
231  ioManager.ResolvePath(
232  ioManager.ConcatPath(
233  binPathForVersion,
234  byondInstaller.DreamMakerName)),
235  ioManager.ResolvePath(
236  ioManager.ConcatPath(
237  byondInstaller.PathToUserByondFolder,
238  CfgDirectoryName,
239  TrustedDmbFileName)));
240  }
241 
243  public async Task StartAsync(CancellationToken cancellationToken)
244  {
245  async Task<byte[]> GetActiveVersion()
246  {
247  var activeVersionFileExists = await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
248  return !activeVersionFileExists ? null : await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
249  }
250 
251  var activeVersionBytesTask = GetActiveVersion();
252 
253  // Create local cfg directory in case it doesn't exist
254  var localCfgDirectory = ioManager.ConcatPath(
255  byondInstaller.PathToUserByondFolder,
256  CfgDirectoryName);
257  await ioManager.CreateDirectory(
258  localCfgDirectory,
259  cancellationToken).ConfigureAwait(false);
260 
261  // Delete trusted.txt so it doesn't grow too large
262  var trustedFilePath =
263  ioManager.ConcatPath(
264  localCfgDirectory,
265  TrustedDmbFileName);
266  logger.LogTrace("Deleting trusted .dmbs file {0}", trustedFilePath);
267  await ioManager.DeleteFile(
268  trustedFilePath,
269  cancellationToken).ConfigureAwait(false);
270 
271  var byondDirectory = ioManager.ResolvePath();
272  await ioManager.CreateDirectory(byondDirectory, cancellationToken).ConfigureAwait(false);
273  var directories = await ioManager.GetDirectories(byondDirectory, cancellationToken).ConfigureAwait(false);
274 
275  async Task ReadVersion(string path)
276  {
277  var versionFile = ioManager.ConcatPath(path, VersionFileName);
278  if (!await ioManager.FileExists(versionFile, cancellationToken).ConfigureAwait(false))
279  {
280  logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path));
281  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); // cleanup
282  return;
283  }
284 
285  var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken).ConfigureAwait(false);
286  var text = Encoding.UTF8.GetString(bytes);
287  if (Version.TryParse(text, out var version))
288  {
289  var key = VersionKey(version);
290  lock (installedVersions)
291  if (!installedVersions.ContainsKey(key))
292  {
293  logger.LogDebug("Adding detected BYOND version {0}...", key);
294  installedVersions.Add(key, Task.CompletedTask);
295  return;
296  }
297  }
298 
299  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false);
300  }
301 
302  await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false);
303 
304  var activeVersionBytes = await activeVersionBytesTask.ConfigureAwait(false);
305  if (activeVersionBytes != null)
306  {
307  var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
308  bool hasRequestedActiveVersion;
309  lock (installedVersions)
310  hasRequestedActiveVersion = installedVersions.ContainsKey(activeVersionString);
311  if (hasRequestedActiveVersion && Version.TryParse(activeVersionString, out var activeVersion))
312  ActiveVersion = activeVersion.Semver();
313  else
314  {
315  logger.LogWarning("Failed to load saved active version {0}!", activeVersionString);
316  await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
317  }
318  }
319  }
320 
322  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
323  }
324 }
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 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 InstallVersion(Version version, CancellationToken cancellationToken)
Installs a BYOND version if it isn&#39;t already
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
Definition: Byond.cs:8
async Task ChangeVersion(Version version, CancellationToken cancellationToken)
Change the active BYOND version
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