diff --git a/docs/API.dox b/docs/API.dox
index f3dcafc086..e4320e103b 100644
--- a/docs/API.dox
+++ b/docs/API.dox
@@ -265,6 +265,8 @@ To set the active Byond version:
I POST "/Byond" @ref Tgstation.Server.Api.Models.Byond => @ref Tgstation.Server.Api.Models.Byond
+This will queue up a job to install the byond version if it doesn't exist which will be returned in the response. The @ref Tgstation.Server.Api.Models.Internal.RawData.Content field can be used to upload custom zip files.
+
To list all installed Byond versions use the following request:
I GET "/Byond/List" => Array of @ref Tgstation.Server.Api.Models.Byond
diff --git a/src/Tgstation.Server.Api/Models/Byond.cs b/src/Tgstation.Server.Api/Models/Byond.cs
index d5322db8c0..89e7db2a91 100644
--- a/src/Tgstation.Server.Api/Models/Byond.cs
+++ b/src/Tgstation.Server.Api/Models/Byond.cs
@@ -1,11 +1,12 @@
using System;
+using Tgstation.Server.Api.Models.Internal;
namespace Tgstation.Server.Api.Models
{
///
- /// Represents a BYOND installation
+ /// Represents a BYOND installation. is used to upload custom BYOND version zip files, though must still be set.
///
- public sealed class Byond
+ public sealed class Byond : RawData
{
///
/// The of the installation used for new compiles. Will be if the user does not have permission to view it or there is no BYOND version installed. Only considers the and numbers.
diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs
index ef40d7f2da..1238284bf9 100644
--- a/src/Tgstation.Server.Api/Models/ErrorCode.cs
+++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs
@@ -537,5 +537,11 @@ namespace Tgstation.Server.Api.Models
///
[Description("Test merging cannot be performed with this remote!")]
RepoTestMergeInvalidRemote,
+
+ ///
+ /// Attempted to switch to a custom BYOND version that does not exist.
+ ///
+ [Description("Cannot switch to requested custom BYOND version as it is not currently installed.")]
+ ByondNonExistentCustomVersion,
}
}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs
index d58ad67880..1e4bb2c2b6 100644
--- a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs
+++ b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs
@@ -34,12 +34,12 @@ namespace Tgstation.Server.Host.Components.Byond
const string TrustedDmbFileName = "trusted.txt";
///
- /// The file in which we store the for installations
+ /// The file in which we store the for installations
///
const string VersionFileName = "Version.txt";
///
- /// The file in which we store the for the active installation
+ /// The file in which we store the for the active installation
///
const string ActiveVersionFileName = "ActiveVersion.txt";
@@ -89,9 +89,12 @@ namespace Tgstation.Server.Host.Components.Byond
///
/// Converts a BYOND to a
///
- /// The to convert
+ /// The to convert.
+ /// If the property of should be kept.
/// The representation of
- static string VersionKey(Version version) => new Version(version.Major, version.Minor).ToString();
+ static string VersionKey(Version version, bool allowPatch) => (allowPatch && version.Build > 0
+ ? new Version(version.Major, version.Minor, version.Build)
+ : new Version(version.Major, version.Minor)).ToString();
///
/// Construct a
@@ -118,17 +121,29 @@ namespace Tgstation.Server.Host.Components.Byond
/// Installs a BYOND if it isn't already
///
/// The BYOND to install
+ /// Custom zip file bytes to use. Will cause a number to be added.
/// The for the operation
/// A representing the running operation
- async Task InstallVersion(Version version, CancellationToken cancellationToken)
+ async Task InstallVersion(Version version, byte[] versionZipBytes, CancellationToken cancellationToken)
{
var ourTcs = new TaskCompletionSource();
Task inProgressTask;
-
- var versionKey = VersionKey(version);
+ string versionKey;
bool installed;
lock (installedVersions)
{
+ if (versionZipBytes != null)
+ {
+ int customInstallationNumber = 1;
+ do
+ {
+ versionKey = $"{VersionKey(version, false)}.{customInstallationNumber++}";
+ }
+ while (installedVersions.ContainsKey(versionKey));
+ }
+ else
+ versionKey = VersionKey(version, true);
+
installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
if (!installed)
installedVersions.Add(versionKey, ourTcs.Task);
@@ -139,8 +154,13 @@ namespace Tgstation.Server.Host.Components.Byond
{
await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
- return;
+ return versionKey;
}
+
+ if (versionZipBytes != null)
+ logger.LogInformation("Installing custom BYOND version as {0}...", versionKey);
+ else if (version.Build > 0)
+ throw new JobException(ErrorCode.ByondNonExistentCustomVersion);
else
logger.LogDebug("Requested BYOND version {0} not currently installed. Doing so now...");
@@ -148,18 +168,22 @@ namespace Tgstation.Server.Host.Components.Byond
try
{
await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List { versionKey }, cancellationToken).ConfigureAwait(false);
- var downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);
+ var zipFileBytesTask = versionZipBytes == null
+ ? byondInstaller.DownloadVersion(version, cancellationToken)
+ : Task.FromResult(versionZipBytes);
await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
try
{
- var download = await downloadTask.ConfigureAwait(false);
+ versionZipBytes = await zipFileBytesTask.ConfigureAwait(false);
await ioManager.CreateDirectory(versionKey, cancellationToken).ConfigureAwait(false);
var extractPath = ioManager.ResolvePath(versionKey);
logger.LogTrace("Extracting downloaded BYOND zip to {0}...", extractPath);
- await ioManager.ZipToDirectory(extractPath, download, cancellationToken).ConfigureAwait(false);
+ await ioManager.ZipToDirectory(extractPath, versionZipBytes, cancellationToken).ConfigureAwait(false);
+ versionZipBytes = null;
+
await byondInstaller.InstallByond(extractPath, version, cancellationToken).ConfigureAwait(false);
// make sure to do this last because this is what tells us we have a valid version in the future
@@ -191,20 +215,34 @@ namespace Tgstation.Server.Host.Components.Byond
ourTcs.SetException(e);
throw;
}
+
+ return versionKey;
}
///
- public async Task ChangeVersion(Version version, CancellationToken cancellationToken)
+ public async Task ChangeVersion(Version version, byte[] customVersionBytes, CancellationToken cancellationToken)
{
if (version == null)
throw new ArgumentNullException(nameof(version));
- var versionKey = VersionKey(version);
- await InstallVersion(version, cancellationToken).ConfigureAwait(false);
+
+ var versionKey = await InstallVersion(version, customVersionBytes, cancellationToken).ConfigureAwait(false);
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
{
await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(versionKey), cancellationToken).ConfigureAwait(false);
- await eventConsumer.HandleEvent(EventType.ByondActiveVersionChange, new List { ActiveVersion != null ? VersionKey(ActiveVersion) : null, versionKey }, cancellationToken).ConfigureAwait(false);
- ActiveVersion = version;
+ await eventConsumer.HandleEvent(
+ EventType.ByondActiveVersionChange,
+ new List
+ {
+ ActiveVersion != null
+ ? VersionKey(ActiveVersion, true)
+ : null,
+ versionKey
+ },
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ // We reparse the version key because it could be changed after a custom install.
+ ActiveVersion = Version.Parse(versionKey);
}
}
@@ -214,9 +252,9 @@ namespace Tgstation.Server.Host.Components.Byond
var versionToUse = requiredVersion ?? ActiveVersion;
if (versionToUse == null)
throw new JobException(ErrorCode.ByondNoVersionsInstalled);
- await InstallVersion(versionToUse, cancellationToken).ConfigureAwait(false);
+ await InstallVersion(versionToUse, null, cancellationToken).ConfigureAwait(false);
- var versionKey = VersionKey(versionToUse);
+ var versionKey = VersionKey(versionToUse, true);
var binPathForVersion = ioManager.ConcatPath(versionKey, BinPath);
logger.LogTrace("Creating ByondExecutableLock lock for version {0}", requiredVersion);
@@ -286,7 +324,7 @@ namespace Tgstation.Server.Host.Components.Byond
var text = Encoding.UTF8.GetString(bytes);
if (Version.TryParse(text, out var version))
{
- var key = VersionKey(version);
+ var key = VersionKey(version, true);
lock (installedVersions)
if (!installedVersions.ContainsKey(key))
{
diff --git a/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs
index 7ace2266ac..650bce0209 100644
--- a/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs
+++ b/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs
@@ -25,9 +25,10 @@ namespace Tgstation.Server.Host.Components.Byond
/// Change the active BYOND version
///
/// The new
+ /// Optional s of a custom BYOND version zip file.
/// The for the operation
/// A representing the running operation
- Task ChangeVersion(Version version, CancellationToken cancellationToken);
+ Task ChangeVersion(Version version, byte[] customVersionBytes, CancellationToken cancellationToken);
///
/// Lock the current installation's location and return a
diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs
index ed4a042794..c6f9dbd0d7 100644
--- a/src/Tgstation.Server.Host/Controllers/ByondController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs
@@ -68,11 +68,16 @@ namespace Tgstation.Server.Host.Controllers
[HttpGet(Routes.List)]
[TgsAuthorize(ByondRights.ListInstalled)]
[ProducesResponseType(typeof(IEnumerable), 200)]
- public Task List() => Task.FromResult(
- Json(instanceManager.GetInstance(Instance).ByondManager.InstalledVersions.Select(x => new Api.Models.Byond
- {
- Version = x
- })));
+ public IActionResult List()
+ => Json(
+ instanceManager
+ .GetInstance(Instance)
+ .ByondManager
+ .InstalledVersions
+ .Select(x => new Api.Models.Byond
+ {
+ Version = x
+ }));
///
/// Changes the active BYOND version to the one specified in a given .
@@ -91,20 +96,33 @@ namespace Tgstation.Server.Host.Controllers
if (model == null)
throw new ArgumentNullException(nameof(model));
+ if (model.Version == null
+ || model.Version.Revision != -1
+ || (model.Content != null && model.Version.Build > 0))
+ return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure));
+
var byondManager = instanceManager.GetInstance(Instance).ByondManager;
// remove cruff fields
- var installingVersion = new Version(model.Version.Major, model.Version.Minor);
-
var result = new Api.Models.Byond();
- if (byondManager.InstalledVersions.Any(x => x == model.Version))
+ if (model.Content == null && byondManager.InstalledVersions.Any(x => x == model.Version))
{
- Logger.LogInformation("User ID {0} changing instance ID {1} BYOND version to {2}", AuthenticationContext.User.Id, Instance.Id, installingVersion);
- await byondManager.ChangeVersion(model.Version, cancellationToken).ConfigureAwait(false);
+ Logger.LogInformation(
+ "User ID {0} changing instance ID {1} BYOND version to {2}",
+ AuthenticationContext.User.Id,
+ Instance.Id,
+ model.Version);
+ await byondManager.ChangeVersion(model.Version, null, cancellationToken).ConfigureAwait(false);
}
+ else if (model.Version.Build > 0)
+ return BadRequest(new ErrorMessage(ErrorCode.ByondNonExistentCustomVersion));
else
{
+ var installingVersion = model.Version.Build <= 0
+ ? new Version(model.Version.Major, model.Version.Minor)
+ : model.Version;
+
Logger.LogInformation(
"User ID {0} installing BYOND version to {1} on instance ID {2}",
AuthenticationContext.User.Id,
@@ -114,13 +132,20 @@ namespace Tgstation.Server.Host.Controllers
// run the install through the job manager
var job = new Models.Job
{
- Description = $"Install BYOND version {installingVersion}",
+ Description = $"Install BYOND version {model.Version}",
StartedBy = AuthenticationContext.User,
CancelRightsType = RightsType.Byond,
CancelRight = (ulong)ByondRights.CancelInstall,
Instance = Instance
};
- await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false);
+ await jobManager.RegisterOperation(
+ job,
+ (paramJob, databaseContextFactory, progressHandler, jobCancellationToken) => byondManager.ChangeVersion(
+ model.Version,
+ model.Content,
+ jobCancellationToken),
+ cancellationToken)
+ .ConfigureAwait(false);
result.InstallJob = job.ToApi();
}
diff --git a/tests/Tgstation.Server.Tests/Instance/ByondTest.cs b/tests/Tgstation.Server.Tests/Instance/ByondTest.cs
index e19a840a3e..ba1bd0b8f8 100644
--- a/tests/Tgstation.Server.Tests/Instance/ByondTest.cs
+++ b/tests/Tgstation.Server.Tests/Instance/ByondTest.cs
@@ -1,16 +1,25 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Castle.Core.Logging;
+using Microsoft.Extensions.Logging;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
+using Tgstation.Server.Api.Models;
+using Tgstation.Server.Client;
using Tgstation.Server.Client.Components;
+using Tgstation.Server.Host.Components.Byond;
+using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Tests.Instance
{
sealed class ByondTest : JobsRequiredTest
{
+ public static readonly Version TestVersion = new Version(513, 1526);
+
readonly IByondClient byondClient;
readonly Api.Models.Instance metadata;
@@ -27,6 +36,7 @@ namespace Tgstation.Server.Tests.Instance
await TestNoVersion(cancellationToken).ConfigureAwait(false);
await TestInstallStable(cancellationToken).ConfigureAwait(false);
await TestInstallFakeVersion(cancellationToken).ConfigureAwait(false);
+ await TestCustomInstalls(cancellationToken);
}
async Task TestInstallFakeVersion(CancellationToken cancellationToken)
@@ -44,7 +54,7 @@ namespace Tgstation.Server.Tests.Instance
{
var newModel = new Api.Models.Byond
{
- Version = new Version(513, 1514)
+ Version = TestVersion
};
var test = await byondClient.SetActiveVersion(newModel, cancellationToken).ConfigureAwait(false);
Assert.IsNotNull(test.InstallJob);
@@ -74,5 +84,48 @@ namespace Tgstation.Server.Tests.Instance
Assert.IsNotNull(otherShit);
Assert.AreEqual(0, otherShit.Count);
}
+
+ async Task TestCustomInstalls(CancellationToken cancellationToken)
+ {
+ var byondInstaller = new PlatformIdentifier().IsWindows
+ ? (IByondInstaller)new WindowsByondInstaller(
+ Mock.Of(),
+ new DefaultIOManager(),
+ Mock.Of>())
+ : new PosixByondInstaller(
+ Mock.Of(),
+ new DefaultIOManager(),
+ Mock.Of>());
+
+ // get the bytes for stable
+ var test = await byondClient.SetActiveVersion(new Api.Models.Byond
+ {
+ Version = TestVersion,
+ Content = await byondInstaller.DownloadVersion(TestVersion, cancellationToken)
+ }, cancellationToken).ConfigureAwait(false);
+
+ Assert.IsNotNull(test.InstallJob);
+ await WaitForJob(test.InstallJob, 60, false, cancellationToken).ConfigureAwait(false);
+
+ var newSettings = await byondClient.ActiveVersion(cancellationToken);
+ Assert.AreEqual(new Version(TestVersion.Major, TestVersion.Minor, 1), newSettings.Version);
+
+ // test a few switches
+ newSettings = await byondClient.SetActiveVersion(new Api.Models.Byond
+ {
+ Version = TestVersion
+ }, cancellationToken);
+ Assert.IsNull(newSettings.InstallJob);
+ await ApiAssert.ThrowsException(() => byondClient.SetActiveVersion(new Api.Models.Byond
+ {
+ Version = new Version(TestVersion.Major, TestVersion.Minor, 2)
+ }, cancellationToken), ErrorCode.ByondNonExistentCustomVersion);
+
+ newSettings = await byondClient.SetActiveVersion(new Api.Models.Byond
+ {
+ Version = new Version(TestVersion.Major, TestVersion.Minor, 1)
+ }, cancellationToken);
+ Assert.IsNull(newSettings.InstallJob);
+ }
}
}
diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs
index 6cfd528c12..4a8c12006f 100644
--- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs
+++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs
@@ -48,6 +48,12 @@ namespace Tgstation.Server.Tests.Instance
await RunBasicTest(cancellationToken);
+ // That was using the custom BYOND version, let's switch back to a regular one now
+ await instanceClient.Byond.SetActiveVersion(new Api.Models.Byond
+ {
+ Version = ByondTest.TestVersion
+ }, cancellationToken);
+
// await RunLongRunningTestThenUpdate(cancellationToken);
// await RunLongRunningTestThenUpdateWithByondVersionSwitch(cancellationToken);
// Remove this deploy when the above tests are reenabled