diff --git a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs
index e02c109e6b..527fcae985 100644
--- a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs
+++ b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs
@@ -6,8 +6,10 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models;
+using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
@@ -41,6 +43,11 @@ namespace Tgstation.Server.Host.Components.Engine
///
protected override EngineType TargetEngineType => EngineType.Byond;
+ ///
+ /// The for the .
+ ///
+ protected IOptionsMonitor GeneralConfigurationOptions { get; }
+
///
/// Path to the system user's local BYOND folder.
///
@@ -52,25 +59,85 @@ namespace Tgstation.Server.Host.Components.Engine
protected abstract string DreamMakerName { get; }
///
- /// Gets the URL formatter string for downloading a byond version of {0:Major} {1:Minor}.
+ /// Template to do ${Marker:xxx} replacements in .
///
- protected abstract string ByondRevisionsUrlTemplate { get; }
+ protected abstract string OSMarkerTemplate { get; }
///
/// The for the .
///
readonly IFileDownloader fileDownloader;
+ ///
+ /// Format a given .
+ ///
+ /// The BYOND version to download.
+ /// The template.
+ /// The .
+ /// The formatted byond download .
+ /// Exposed only for testability.
+ internal static Uri GetDownloadZipUrl(Version semver, string byondZipDownloadTemplate, string osMarkerTemplate)
+ {
+ // god forbid
+ var guardGuid = Guid.NewGuid();
+
+ var url = byondZipDownloadTemplate
+ .Replace("$$", guardGuid.ToString(), StringComparison.Ordinal)
+ .Replace("${Major}", semver.Major.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal)
+ .Replace("${Minor}", semver.Minor.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal);
+
+ var osMarkerPrefix = $"${{{osMarkerTemplate}:";
+ var osMarkerIndex = url.IndexOf(osMarkerPrefix);
+ while (osMarkerIndex != -1)
+ {
+ var start = osMarkerIndex + osMarkerPrefix.Length;
+ var end = url.IndexOf('}', start);
+ if (end == -1)
+ break;
+
+ var substitution = url.Substring(start, end - start);
+ url = url.Replace($"{osMarkerPrefix}{substitution}}}", substitution, StringComparison.Ordinal);
+
+ osMarkerIndex = url.IndexOf(osMarkerPrefix);
+ }
+
+ // at this point, any other substitution attempts should be removed
+ var otherMarkerPrefix = "${";
+ var otherMarkerIndex = url.IndexOf(otherMarkerPrefix);
+ while (otherMarkerIndex != -1)
+ {
+ var start = otherMarkerIndex + otherMarkerPrefix.Length;
+ var end = url.IndexOf('}', start);
+ if (end == -1)
+ break;
+
+ var substitution = url.Substring(start, end - start);
+ url = url.Replace($"{otherMarkerPrefix}{substitution}}}", String.Empty, StringComparison.Ordinal);
+
+ otherMarkerIndex = url.IndexOf(otherMarkerPrefix);
+ }
+
+ url = url.Replace(guardGuid.ToString(), "$", StringComparison.Ordinal);
+
+ return new Uri(url);
+ }
+
///
/// Initializes a new instance of the class.
///
/// The for the .
/// The for the .
/// The value of .
- protected ByondInstallerBase(IIOManager ioManager, ILogger logger, IFileDownloader fileDownloader)
+ /// The value of .
+ protected ByondInstallerBase(
+ IIOManager ioManager,
+ ILogger logger,
+ IFileDownloader fileDownloader,
+ IOptionsMonitor generalConfigurationOptions)
: base(ioManager, logger)
{
this.fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader));
+ GeneralConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
}
///
@@ -195,8 +262,12 @@ namespace Tgstation.Server.Host.Components.Engine
Uri GetDownloadZipUrl(EngineVersion version)
{
CheckVersionValidity(version);
- var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsUrlTemplate, version.Version!.Major, version.Version.Minor);
- return new Uri(url);
+
+ var guardGuid = Guid.NewGuid();
+
+ var semver = version.Version!;
+ var template = GeneralConfigurationOptions.CurrentValue.ByondZipDownloadTemplate;
+ return GetDownloadZipUrl(semver, template, OSMarkerTemplate);
}
}
}
diff --git a/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs
index fff62b3b3f..7e21f6a378 100644
--- a/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs
+++ b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs
@@ -5,9 +5,11 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Extensions;
+using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Components.Engine
@@ -39,7 +41,7 @@ namespace Tgstation.Server.Host.Components.Engine
protected override string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension;
///
- protected override string ByondRevisionsUrlTemplate => "https://www.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip";
+ protected override string OSMarkerTemplate => "Linux";
///
/// The for the .
@@ -52,13 +54,15 @@ namespace Tgstation.Server.Host.Components.Engine
/// The value of .
/// The for the .
/// The for the .
+ /// The .
/// The for the .
public PosixByondInstaller(
IPostWriteHandler postWriteHandler,
IIOManager ioManager,
IFileDownloader fileDownloader,
+ IOptionsMonitor generalConfigurationOptions,
ILogger logger)
- : base(ioManager, logger, fileDownloader)
+ : base(ioManager, logger, fileDownloader, generalConfigurationOptions)
{
this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
diff --git a/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs
index ce2c7704e2..786d8df147 100644
--- a/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs
+++ b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs
@@ -69,18 +69,13 @@ namespace Tgstation.Server.Host.Components.Engine
protected override string PathToUserFolder { get; }
///
- protected override string ByondRevisionsUrlTemplate => "https://www.byond.com/download/build/{0}/{0}.{1}_byond.zip";
+ protected override string OSMarkerTemplate => "Windows";
///
/// The for the .
///
readonly IProcessExecutor processExecutor;
- ///
- /// The for the .
- ///
- readonly GeneralConfiguration generalConfiguration;
-
///
/// The for the .
///
@@ -100,7 +95,7 @@ namespace Tgstation.Server.Host.Components.Engine
/// Initializes a new instance of the class.
///
/// The value of .
- /// The containing the value of .
+ /// The containing the .
/// The containing the value of .
/// The for the .
/// The for the .
@@ -109,13 +104,12 @@ namespace Tgstation.Server.Host.Components.Engine
IProcessExecutor processExecutor,
IIOManager ioManager,
IFileDownloader fileDownloader,
- IOptions generalConfigurationOptions,
+ IOptionsMonitor generalConfigurationOptions,
IOptions sessionConfigurationOptions,
ILogger logger)
- : base(ioManager, logger, fileDownloader)
+ : base(ioManager, logger, fileDownloader, generalConfigurationOptions)
{
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
- generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
sessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions));
var useServiceSpecialTactics = Environment.Is64BitProcess && Environment.UserName == $"{Environment.MachineName}$";
@@ -150,7 +144,7 @@ namespace Tgstation.Server.Host.Components.Engine
installDirectXTask,
};
- if (!generalConfiguration.SkipAddingByondFirewallException)
+ if (!GeneralConfigurationOptions.CurrentValue.SkipAddingByondFirewallException)
{
var firewallTask = AddDreamDaemonToFirewall(version, path, deploymentPipelineProcesses, cancellationToken);
tasks.Add(firewallTask);
@@ -165,7 +159,7 @@ namespace Tgstation.Server.Host.Components.Engine
CheckVersionValidity(version);
ArgumentNullException.ThrowIfNull(path);
- if (generalConfiguration.SkipAddingByondFirewallException)
+ if (GeneralConfigurationOptions.CurrentValue.SkipAddingByondFirewallException)
return;
if (version.Version < DDExeVersion)
diff --git a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs
index 3ba8b3e878..c47282ea80 100644
--- a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs
+++ b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs
@@ -148,6 +148,17 @@ namespace Tgstation.Server.Host.Configuration
[YamlMember(SerializeAs = typeof(string))]
public Uri OpenDreamGitUrl { get; set; } = new Uri(DefaultOpenDreamGitUrl);
+ ///
+ /// The formatter used to download official byond zip files for a given version
+ /// - ${Major} is substituted with the major version number
+ /// - ${Minor} is substituted with the minor version number
+ /// - ${Linux:xxx}, where xxx is any string, will be substituted with xxx if running under Linux.
+ /// - ${Windows:xxx}, where xxx is any string, will be substituted with xxx if running under Windows.
+ /// - $$ will evaluate to a literal $ and not be used for substitutions.
+ /// - Any inapplicable ${xxx} string will be removed.
+ ///
+ public string ByondZipDownloadTemplate { get; set; } = "https://www.byond.com/download/build/${Major}/${Major}.{Minor}_byond${Linux:_linux}.zip";
+
///
/// The prefix to the OpenDream semver as tags appear in the git repository.
///
diff --git a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestByondInstallerBase.cs b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestByondInstallerBase.cs
new file mode 100644
index 0000000000..a3a4605877
--- /dev/null
+++ b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestByondInstallerBase.cs
@@ -0,0 +1,23 @@
+using System;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Tgstation.Server.Host.Components.Engine.Tests
+{
+ [TestClass]
+ public sealed class TestByondInstallerBase
+ {
+ [TestMethod]
+ public void TestUrlTemplateFormatting()
+ {
+ const string OSMarker = "TempleOS";
+
+ Assert.AreEqual(
+ new Uri("https://example.com/$515.1111_Hello Worl$d.zip"),
+ ByondInstallerBase.GetDownloadZipUrl(
+ new Version(515, 1111),
+ "https://example.com/$$${Major}.${Minor}_${TempleOS:Hello Worl$$d}.zip${Linux:Not this}${Or This}",
+ OSMarker));
+ }
+ }
+}
diff --git a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs
index 60e86ad63e..c6288b6a7c 100644
--- a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs
+++ b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
@@ -9,6 +10,7 @@ using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
+using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Components.Engine.Tests
@@ -19,16 +21,18 @@ namespace Tgstation.Server.Host.Components.Engine.Tests
[TestMethod]
public void TestConstruction()
{
- Assert.ThrowsException(() => new PosixByondInstaller(null, null, null, null));
+ Assert.ThrowsException(() => new PosixByondInstaller(null, null, null, null, null));
var mockPostWriteHandler = new Mock();
- Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, null, null, null));
+ Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, null, null, null, null));
var mockIOManager = new Mock();
- Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, null, null));
+ Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, null, null, null));
var mockFileDownloader = Mock.Of();
- Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, null));
+ Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, null, null));
+ var mockOptions = Mock.Of>();
+ Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockOptions, null));
var mockLogger = new Mock>();
- _ = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockLogger.Object);
+ _ = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockOptions, mockLogger.Object);
}
[TestMethod]
@@ -38,7 +42,8 @@ namespace Tgstation.Server.Host.Components.Engine.Tests
var mockIOManager = new Mock();
var mockLogger = new Mock>();
var mockFileDownloader = Mock.Of();
- var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockLogger.Object);
+ var mockOptions = Mock.Of>();
+ var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockOptions, mockLogger.Object);
await installer.CleanCache(default);
}
@@ -50,7 +55,8 @@ namespace Tgstation.Server.Host.Components.Engine.Tests
var mockPostWriteHandler = new Mock();
var mockLogger = new Mock>();
var mockFileDownloader = new Mock();
- var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader.Object, mockLogger.Object);
+ var mockOptions = Mock.Of>();
+ var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader.Object, mockOptions, mockLogger.Object);
await Assert.ThrowsExceptionAsync(() => installer.DownloadVersion(null, null, default).AsTask());
@@ -87,7 +93,8 @@ namespace Tgstation.Server.Host.Components.Engine.Tests
var mockPostWriteHandler = new Mock();
var mockLogger = new Mock>();
var mockFileDownloader = Mock.Of();
- var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockLogger.Object);
+ var mockOptions = Mock.Of>();
+ var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockOptions, mockLogger.Object);
const string FakePath = "fake";
await Assert.ThrowsExceptionAsync(() => installer.Install(null, null, false, default).AsTask());