tgstation-server
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.Globalization;
5 using System.Linq;
6 using System.Net;
7 using System.Text;
8 using System.Threading;
9 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host.Components.Byond
14 {
16  sealed class ByondManager : IByondManager
17  {
21  public const string BinPath = "byond/bin";
22 
26  const string VersionFileName = "Version.txt";
30  const string ActiveVersionFileName = "ActiveVersion.txt";
31 
33  public Version ActiveVersion { get; private set; }
34 
36  public IReadOnlyList<Version> InstalledVersions
37  {
38  get
39  {
40  lock (installedVersions)
41  return installedVersions.Select(x => Version.Parse(x.Key)).ToList();
42  }
43  }
44 
49 
54 
59 
63  readonly ILogger<ByondManager> logger;
64 
68  readonly Dictionary<string, Task> installedVersions;
69 
73  readonly SemaphoreSlim semaphore;
74 
80  static string VersionKey(Version version) => new Version(version.Major, version.Minor).ToString();
81 
89  public ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, IEventConsumer eventConsumer, ILogger<ByondManager> logger)
90  {
91  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
92  this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
93  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
94  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
95 
96  installedVersions = new Dictionary<string, Task>();
97  semaphore = new SemaphoreSlim(1);
98  }
99 
101  public void Dispose() => semaphore.Dispose();
102 
109  async Task InstallVersion(Version version, CancellationToken cancellationToken)
110  {
111  var ourTcs = new TaskCompletionSource<object>();
112  Task inProgressTask;
113 
114  var versionKey = VersionKey(version);
115  bool installed;
116  lock (installedVersions)
117  {
118  installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
119  if (!installed)
120  installedVersions.Add(versionKey, ourTcs.Task);
121  }
122  if (installed)
123  using (cancellationToken.Register(() => ourTcs.SetCanceled()))
124  {
125  await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false);
126  cancellationToken.ThrowIfCancellationRequested();
127  return;
128  }
129  //okay up to us to install it then
130  try
131  {
132  await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List<string> { versionKey }, cancellationToken).ConfigureAwait(false);
133  var downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);
134 
135  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
136  await ioManager.CreateDirectory(versionKey, cancellationToken).ConfigureAwait(false);
137 
138  try
139  {
140  var download = await downloadTask.ConfigureAwait(false);
141  await ioManager.ZipToDirectory(versionKey, download, cancellationToken).ConfigureAwait(false);
142  await byondInstaller.InstallByond(ioManager.ResolvePath(versionKey), version, cancellationToken).ConfigureAwait(false);
143 
144  //make sure to do this last because this is what tells us we have a valid version in the future
145  await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
146  }
147  catch (WebException e)
148  {
149  //since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
150  throw new JobException(String.Format(CultureInfo.InvariantCulture, "Error downloading BYOND version: {0}", e.Message));
151  }
152  catch (OperationCanceledException)
153  {
154  throw;
155  }
156  catch
157  {
158  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
159  throw;
160  }
161 
162  ourTcs.SetResult(null);
163  }
164  catch (Exception e)
165  {
166  if (!(e is OperationCanceledException))
167  await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List<string> { e.Message }, cancellationToken).ConfigureAwait(false);
168  lock (installedVersions)
169  installedVersions.Remove(versionKey);
170  ourTcs.SetException(e);
171  throw;
172  }
173  }
174 
176  public async Task ChangeVersion(Version version, CancellationToken cancellationToken)
177  {
178  if (version == null)
179  throw new ArgumentNullException(nameof(version));
180  var versionKey = VersionKey(version);
181  await InstallVersion(version, cancellationToken).ConfigureAwait(false);
182  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
183  {
184  await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(versionKey), cancellationToken).ConfigureAwait(false);
185  await eventConsumer.HandleEvent(EventType.ByondActiveVersionChange, new List<string> { ActiveVersion != null ? VersionKey(ActiveVersion) : null, versionKey }, cancellationToken).ConfigureAwait(false);
186  ActiveVersion = version;
187  }
188  }
189 
191  public async Task<IByondExecutableLock> UseExecutables(Version requiredVersion, CancellationToken cancellationToken)
192  {
193  var versionToUse = requiredVersion ?? ActiveVersion;
194  if (versionToUse == null)
195  throw new JobException("No BYOND versions installed!");
196  await InstallVersion(versionToUse, cancellationToken).ConfigureAwait(false);
197 
198  var versionKey = VersionKey(versionToUse);
199 
200  return new ByondExecutableLock(versionToUse, ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamDaemonName)), ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamMakerName)));
201  }
202 
204  public async Task StartAsync(CancellationToken cancellationToken)
205  {
206  async Task<byte[]> GetActiveVersion()
207  {
208  if (!await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false))
209  return null;
210  return await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
211  }
212 
213  var activeVersionBytesTask = GetActiveVersion();
214 
215  await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);
216  var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
217 
218  async Task ReadVersion(string path)
219  {
220  var versionFile = ioManager.ConcatPath(path, VersionFileName);
221  if (!await ioManager.FileExists(versionFile, cancellationToken).ConfigureAwait(false))
222  {
223  logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path));
224  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); //cleanup
225  return;
226  }
227  var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken).ConfigureAwait(false);
228  var text = Encoding.UTF8.GetString(bytes);
229  if (Version.TryParse(text, out var version))
230  {
231  var key = VersionKey(version);
232  lock (installedVersions)
233  if (!installedVersions.ContainsKey(key))
234  {
235  installedVersions.Add(key, Task.CompletedTask);
236  return;
237  }
238  }
239  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false);
240  };
241 
242  await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false);
243 
244  var activeVersionBytes = await activeVersionBytesTask.ConfigureAwait(false);
245  if (activeVersionBytes != null)
246  {
247  var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
248  if (Version.TryParse(activeVersionString, out var activeVersion))
249  ActiveVersion = activeVersion;
250  else
251  await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
252  }
253  }
254 
256  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
257  }
258 }
readonly IIOManager ioManager
The IIOManager for the ByondManager
Definition: ByondManager.cs:48
Consumes EventTypes and takes the appropriate actions
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
async Task StartAsync(CancellationToken cancellationToken)
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
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:68
ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, IEventConsumer eventConsumer, ILogger< ByondManager > logger)
Construct a ByondManager
Definition: ByondManager.cs:89
readonly ILogger< ByondManager > logger
The ILogger for the ByondManager
Definition: ByondManager.cs:63
async Task ChangeVersion(Version version, CancellationToken cancellationToken)
Change the active BYOND version
readonly IByondInstaller byondInstaller
The IByondInstaller for the ByondManager
Definition: ByondManager.cs:53
Interface for using filesystems
Definition: IIOManager.cs:11
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the ByondManager
Definition: ByondManager.cs:73
static async Task< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:8
readonly IEventConsumer eventConsumer
The IEventConsumer for the ByondManager
Definition: ByondManager.cs:58