mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 07:04:57 +01:00
Add support for installing custom BYOND versions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a BYOND installation
|
||||
/// Represents a BYOND installation. <see cref="RawData.Content"/> is used to upload custom BYOND version zip files, though <see cref="Version"/> must still be set.
|
||||
/// </summary>
|
||||
public sealed class Byond
|
||||
public sealed class Byond : RawData
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="System.Version"/> of the <see cref="Byond"/> installation used for new compiles. Will be <see langword="null"/> if the user does not have permission to view it or there is no BYOND version installed. Only considers the <see cref="Version.Major"/> and <see cref="Version.Minor"/> numbers.
|
||||
|
||||
@@ -537,5 +537,11 @@ namespace Tgstation.Server.Api.Models
|
||||
/// </summary>
|
||||
[Description("Test merging cannot be performed with this remote!")]
|
||||
RepoTestMergeInvalidRemote,
|
||||
|
||||
/// <summary>
|
||||
/// Attempted to switch to a custom BYOND version that does not exist.
|
||||
/// </summary>
|
||||
[Description("Cannot switch to requested custom BYOND version as it is not currently installed.")]
|
||||
ByondNonExistentCustomVersion,
|
||||
}
|
||||
}
|
||||
@@ -34,12 +34,12 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
const string TrustedDmbFileName = "trusted.txt";
|
||||
|
||||
/// <summary>
|
||||
/// The file in which we store the <see cref="VersionKey(Version)"/> for installations
|
||||
/// The file in which we store the <see cref="VersionKey(Version, bool)"/> for installations
|
||||
/// </summary>
|
||||
const string VersionFileName = "Version.txt";
|
||||
|
||||
/// <summary>
|
||||
/// The file in which we store the <see cref="VersionKey(Version)"/> for the active installation
|
||||
/// The file in which we store the <see cref="VersionKey(Version, bool)"/> for the active installation
|
||||
/// </summary>
|
||||
const string ActiveVersionFileName = "ActiveVersion.txt";
|
||||
|
||||
@@ -89,9 +89,12 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
/// <summary>
|
||||
/// Converts a BYOND <paramref name="version"/> to a <see cref="string"/>
|
||||
/// </summary>
|
||||
/// <param name="version">The <see cref="Version"/> to convert</param>
|
||||
/// <param name="version">The <see cref="Version"/> to convert.</param>
|
||||
/// <param name="allowPatch">If the <see cref="Version.Build"/> property of <paramref name="version"/> should be kept.</param>
|
||||
/// <returns>The <see cref="string"/> representation of <paramref name="version"/></returns>
|
||||
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();
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="ByondManager"/>
|
||||
@@ -118,17 +121,29 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
/// Installs a BYOND <paramref name="version"/> if it isn't already
|
||||
/// </summary>
|
||||
/// <param name="version">The BYOND <see cref="Version"/> to install</param>
|
||||
/// <param name="versionZipBytes">Custom zip file bytes to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
async Task InstallVersion(Version version, CancellationToken cancellationToken)
|
||||
async Task<string> InstallVersion(Version version, byte[] versionZipBytes, CancellationToken cancellationToken)
|
||||
{
|
||||
var ourTcs = new TaskCompletionSource<object>();
|
||||
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<string> { 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<string> { ActiveVersion != null ? VersionKey(ActiveVersion) : null, versionKey }, cancellationToken).ConfigureAwait(false);
|
||||
ActiveVersion = version;
|
||||
await eventConsumer.HandleEvent(
|
||||
EventType.ByondActiveVersionChange,
|
||||
new List<string>
|
||||
{
|
||||
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))
|
||||
{
|
||||
|
||||
@@ -25,9 +25,10 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
/// Change the active BYOND version
|
||||
/// </summary>
|
||||
/// <param name="version">The new <see cref="Version"/></param>
|
||||
/// <param name="customVersionBytes">Optional <see cref="byte"/>s of a custom BYOND version zip file.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task ChangeVersion(Version version, CancellationToken cancellationToken);
|
||||
Task ChangeVersion(Version version, byte[] customVersionBytes, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Lock the current installation's location and return a <see cref="IByondExecutableLock"/>
|
||||
|
||||
@@ -68,11 +68,16 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[HttpGet(Routes.List)]
|
||||
[TgsAuthorize(ByondRights.ListInstalled)]
|
||||
[ProducesResponseType(typeof(IEnumerable<Api.Models.Byond>), 200)]
|
||||
public Task<IActionResult> List() => Task.FromResult<IActionResult>(
|
||||
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
|
||||
}));
|
||||
|
||||
/// <summary>
|
||||
/// Changes the active BYOND version to the one specified in a given <paramref name="model"/>.
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IProcessExecutor>(),
|
||||
new DefaultIOManager(),
|
||||
Mock.Of<ILogger<WindowsByondInstaller>>())
|
||||
: new PosixByondInstaller(
|
||||
Mock.Of<IPostWriteHandler>(),
|
||||
new DefaultIOManager(),
|
||||
Mock.Of<ILogger<PosixByondInstaller>>());
|
||||
|
||||
// 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<ApiConflictException>(() => 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user