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());