diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm index 6187a67825a..6d35cf6593e 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -1,6 +1,6 @@ // tgstation-server DMAPI -#define TGS_DMAPI_VERSION "6.5.3" +#define TGS_DMAPI_VERSION "6.6.0" // All functions and datums outside this document are subject to change with any version and should not be relied on. @@ -73,11 +73,11 @@ #define TGS_EVENT_REPO_MERGE_PULL_REQUEST 3 /// Before the repository makes a sychronize operation. Parameters: Absolute repostiory path. #define TGS_EVENT_REPO_PRE_SYNCHRONIZE 4 -/// Before a BYOND install operation begins. Parameters: [/datum/tgs_version] of the installing BYOND. +/// Before a BYOND install operation begins. Parameters: [/datum/tgs_version] of the installing BYOND, engine type of the installing BYOND. #define TGS_EVENT_BYOND_INSTALL_START 5 /// When a BYOND install operation fails. Parameters: Error message #define TGS_EVENT_BYOND_INSTALL_FAIL 6 -/// When the active BYOND version changes. Parameters: (Nullable) [/datum/tgs_version] of the current BYOND, [/datum/tgs_version] of the new BYOND. +/// When the active BYOND version changes. Parameters: (Nullable) [/datum/tgs_version] of the current BYOND, [/datum/tgs_version] of the new BYOND, engine type of the current BYOND, engine type of the new BYOND. #define TGS_EVENT_BYOND_ACTIVE_VERSION_CHANGE 7 /// When the compiler starts running. Parameters: Game directory path, origin commit SHA. #define TGS_EVENT_COMPILE_START 8 @@ -129,6 +129,11 @@ /// DreamDaemon Ultrasafe security level. #define TGS_SECURITY_ULTRASAFE 2 +/// The Build Your Own Net Dream engine. +#define TGS_ENGINE_TYPE_BYOND 0 +/// The OpenDream engine. +#define TGS_ENGINE_TYPE_OPENDREAM 1 + //REQUIRED HOOKS /** diff --git a/src/Tgstation.Server.Api/Models/EngineType.cs b/src/Tgstation.Server.Api/Models/EngineType.cs new file mode 100644 index 00000000000..67410c1d470 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/EngineType.cs @@ -0,0 +1,18 @@ +namespace Tgstation.Server.Api.Models +{ + /// + /// The type of engine the codebase is using. + /// + public enum EngineType + { + /// + /// Build your own net dream, + /// + Byond, + + /// + /// The OpenDream BYOND reimplementation. + /// + OpenDream, + } +} diff --git a/src/Tgstation.Server.Api/Models/Internal/ByondVersion.cs b/src/Tgstation.Server.Api/Models/Internal/ByondVersion.cs new file mode 100644 index 00000000000..bed173ed8fe --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Internal/ByondVersion.cs @@ -0,0 +1,108 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Tgstation.Server.Api.Models.Internal +{ + /// + /// Information about a Byond installation. + /// + public class ByondVersion : IEquatable + { + /// + /// The . + /// + [RequestOptions(FieldPresence.Required)] + public EngineType? Engine { get; set; } + + /// + /// The of the engine. + /// + [RequestOptions(FieldPresence.Required)] + public Version? Version { get; set; } + + /// + /// The git committish of the . On response, this will always be a commit SHA. + /// + [ResponseOptions] + [StringLength(Limits.MaximumCommitShaLength)] + public string? SourceCommittish { get; set; } + + /// + /// Parses a stringified . + /// + /// The input . + /// The output . + /// if parsing was successful, otherwise. + public static bool TryParse(string input, out ByondVersion byondVersion) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + var splits = input.Split(new char[] { '-' }, StringSplitOptions.RemoveEmptyEntries); + byondVersion = new ByondVersion(); + + if (splits.Length > 2) + return false; + + EngineType engine; + if (splits.Length > 1) + { + if (!Enum.TryParse(splits[0], out engine)) + return false; + } + else + engine = EngineType.Byond; + + byondVersion.Engine = engine; + + if (!Version.TryParse(splits.Last(), out var version)) + return false; + + byondVersion.Version = version; + + return true; + } + + /// + /// Initializes a new instance of the class. + /// + public ByondVersion() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The to copy. + public ByondVersion(ByondVersion other) + { + if (other == null) + throw new ArgumentNullException(nameof(other)); + + Version = other.Version; + Engine = other.Engine; + SourceCommittish = other.SourceCommittish; + } + + /// + public bool Equals(ByondVersion other) + { + // https://github.com/dotnet/roslyn-analyzers/issues/2875 +#pragma warning disable CA1062 // Validate arguments of public methods + return other!.Version == Version + && other.Engine == Engine; +#pragma warning restore CA1062 // Validate arguments of public methods + } + + /// + public override bool Equals(object obj) + => obj is ByondVersion other && Equals(other); + + /// + public override string ToString() => $"{(Engine != EngineType.Byond ? $"{Engine}-" : String.Empty)}{Version}"; // BYOND isn't display for backwards compatibility. SourceCommittish is not included + + /// + public override int GetHashCode() => ToString().GetHashCode(); + } +} diff --git a/src/Tgstation.Server.Api/Models/Request/ByondVersionDeleteRequest.cs b/src/Tgstation.Server.Api/Models/Request/ByondVersionDeleteRequest.cs index aaf8585c9be..ad5eac45ccd 100644 --- a/src/Tgstation.Server.Api/Models/Request/ByondVersionDeleteRequest.cs +++ b/src/Tgstation.Server.Api/Models/Request/ByondVersionDeleteRequest.cs @@ -1,16 +1,13 @@ using System; +using Tgstation.Server.Api.Models.Internal; + namespace Tgstation.Server.Api.Models.Request { /// /// A request to delete a specific . /// - public class ByondVersionDeleteRequest + public class ByondVersionDeleteRequest : ByondVersion { - /// - /// The BYOND version to install. - /// - [RequestOptions(FieldPresence.Required)] - public Version? Version { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Request/ByondVersionRequest.cs b/src/Tgstation.Server.Api/Models/Request/ByondVersionRequest.cs index b86eea074a9..a04dfd0d7e1 100644 --- a/src/Tgstation.Server.Api/Models/Request/ByondVersionRequest.cs +++ b/src/Tgstation.Server.Api/Models/Request/ByondVersionRequest.cs @@ -1,13 +1,22 @@ -namespace Tgstation.Server.Api.Models.Request +using System; + +using Tgstation.Server.Api.Models.Internal; + +namespace Tgstation.Server.Api.Models.Request { /// - /// A request to install a BYOND . + /// A request to install a . /// - public sealed class ByondVersionRequest : ByondVersionDeleteRequest + public sealed class ByondVersionRequest : ByondVersion { /// /// If a custom BYOND version is to be uploaded. /// public bool? UploadCustomZip { get; set; } + + /// + /// The remote repository for non- s. By default, this is the original git repository of the target . + /// + public Uri? SourceRepository { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Response/ByondResponse.cs b/src/Tgstation.Server.Api/Models/Response/ByondResponse.cs index 64b01090d53..92d524728da 100644 --- a/src/Tgstation.Server.Api/Models/Response/ByondResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/ByondResponse.cs @@ -1,16 +1,19 @@ -using System; +using Tgstation.Server.Api.Models.Internal; namespace Tgstation.Server.Api.Models.Response { /// - /// Represents an installed BYOND . + /// Represents an installed . /// - public sealed class ByondResponse + public sealed class ByondResponse : ByondVersion { /// - /// The installed BYOND . BYOND itself only considers the and numbers. TGS uses the number to represent installed custom versions. + /// Initializes a new instance of the class. /// - [ResponseOptions] - public Version? Version { get; set; } + /// The to copy. + public ByondResponse(ByondVersion byondVersion) + : base(byondVersion) + { + } } } diff --git a/src/Tgstation.Server.Api/Models/Response/CompileJobResponse.cs b/src/Tgstation.Server.Api/Models/Response/CompileJobResponse.cs index 60444d9c3c9..e7fc06c3b21 100644 --- a/src/Tgstation.Server.Api/Models/Response/CompileJobResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/CompileJobResponse.cs @@ -16,10 +16,15 @@ public sealed class CompileJobResponse : Internal.CompileJob public RevisionInformation? RevisionInformation { get; set; } /// - /// The the was made with. + /// The the was made with. /// public Version? ByondVersion { get; set; } + /// + /// The the was made with. + /// + public EngineType? Engine { get; set; } + /// /// The origin of the repository the compile job was built from. /// diff --git a/src/Tgstation.Server.Api/Rights/ByondRights.cs b/src/Tgstation.Server.Api/Rights/ByondRights.cs index 288d8ca3b8d..6e20a62b3b5 100644 --- a/src/Tgstation.Server.Api/Rights/ByondRights.cs +++ b/src/Tgstation.Server.Api/Rights/ByondRights.cs @@ -14,33 +14,43 @@ public enum ByondRights : ulong None = 0, /// - /// User may view the active installed BYOND version. + /// User may view the active installed engine versions. /// ReadActive = 1 << 0, /// - /// User may list all installed BYOND versions. + /// User may list all installed engine versions. /// ListInstalled = 1 << 1, /// - /// User may install official BYOND versions or change the active BYOND version. + /// User may install official versions or change the active version. /// - InstallOfficialOrChangeActiveVersion = 1 << 2, + InstallOfficialOrChangeActiveByondVersion = 1 << 2, /// - /// User may cancel BYOND installation job. + /// User may cancel an engine installation job. /// CancelInstall = 1 << 3, /// - /// User may upload and activate custom BYOND builds. + /// User may upload and activate custom builds. /// - InstallCustomVersion = 1 << 4, + InstallCustomByondVersion = 1 << 4, /// - /// User may delete non-active BYOND builds. + /// User may delete non-active engine builds. /// DeleteInstall = 1 << 5, + + /// + /// User may install official versions or change the active version. + /// + InstallOfficialOrChangeActiveOpenDreamVersion = 1 << 6, + + /// + /// User may activate custom builds via zip upload or custom git committish. + /// + InstallCustomOpenDreamVersion = 1 << 7, } } diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondExecutableLock.cs b/src/Tgstation.Server.Host/Components/Byond/ByondExecutableLock.cs index 334b3469ea1..71391f1d7f8 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondExecutableLock.cs +++ b/src/Tgstation.Server.Host/Components/Byond/ByondExecutableLock.cs @@ -1,5 +1,9 @@ using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Byond @@ -8,21 +12,36 @@ namespace Tgstation.Server.Host.Components.Byond sealed class ByondExecutableLock : ReferenceCounter, IByondExecutableLock { /// - public Version Version => Instance.Version; + public ByondVersion Version => Instance.Version; /// - public string DreamDaemonPath => Instance.DreamDaemonPath; + public string ServerExePath => Instance.ServerExePath; /// - public string DreamMakerPath => Instance.DreamMakerPath; + public string CompilerExePath => Instance.CompilerExePath; /// - public bool SupportsCli => Instance.SupportsCli; + public bool HasStandardOutput => Instance.HasStandardOutput; /// - public bool SupportsMapThreads => Instance.SupportsMapThreads; + public bool PromptsForNetworkAccess => Instance.PromptsForNetworkAccess; + + /// + public Task InstallationTask => Instance.InstallationTask; /// public void DoNotDeleteThisSession() => DangerousDropReference(); + + /// + public string FormatServerArguments( + IDmbProvider dmbProvider, + IReadOnlyDictionary parameters, + DreamDaemonLaunchParameters launchParameters, + string logFilePath) + => Instance.FormatServerArguments( + dmbProvider, + parameters, + launchParameters, + logFilePath); } } diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondInstallation.cs b/src/Tgstation.Server.Host/Components/Byond/ByondInstallation.cs index 8554190d0f6..74b537c0fa3 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondInstallation.cs +++ b/src/Tgstation.Server.Host/Components/Byond/ByondInstallation.cs @@ -1,5 +1,13 @@ using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; using System.Threading.Tasks; +using System.Web; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Components.Deployment; namespace Tgstation.Server.Host.Components.Byond { @@ -7,48 +15,125 @@ namespace Tgstation.Server.Host.Components.Byond sealed class ByondInstallation : IByondInstallation { /// - public Version Version { get; } + public ByondVersion Version { get; } /// - public string DreamDaemonPath { get; } + public string ServerExePath { get; } /// - public string DreamMakerPath { get; } + public string CompilerExePath { get; } /// - public bool SupportsCli { get; } + public bool PromptsForNetworkAccess { get; } /// - public bool SupportsMapThreads { get; } + public bool HasStandardOutput { get; } + + /// + public Task InstallationTask { get; } /// - /// The that completes when the BYOND version finished installing. + /// Change a given into the appropriate DreamDaemon command line word. /// - public Task InstallationTask { get; } + /// The level to change. + /// A representation of the command line parameter. + static string SecurityWord(DreamDaemonSecurity securityLevel) + { + return securityLevel switch + { + DreamDaemonSecurity.Safe => "safe", + DreamDaemonSecurity.Trusted => "trusted", + DreamDaemonSecurity.Ultrasafe => "ultrasafe", + _ => throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon security level: {0}", securityLevel)), + }; + } + + /// + /// Change a given into the appropriate DreamDaemon command line word. + /// + /// The level to change. + /// A representation of the command line parameter. + static string VisibilityWord(DreamDaemonVisibility visibility) + { + return visibility switch + { + DreamDaemonVisibility.Public => "public", + DreamDaemonVisibility.Private => "private", + DreamDaemonVisibility.Invisible => "invisible", + _ => throw new ArgumentOutOfRangeException(nameof(visibility), visibility, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon visibility level: {0}", visibility)), + }; + } /// /// Initializes a new instance of the class. /// /// The value of . /// The value of . - /// The value of . - /// The value of . - /// The value of . - /// The value of . + /// The value of . + /// The value of . + /// If a CLI application is being used. public ByondInstallation( Task installationTask, - Version version, + ByondVersion version, string dreamDaemonPath, string dreamMakerPath, - bool supportsCli, - bool supportsMapThreads) + bool supportsCli) { InstallationTask = installationTask ?? throw new ArgumentNullException(nameof(installationTask)); Version = version ?? throw new ArgumentNullException(nameof(version)); - DreamDaemonPath = dreamDaemonPath ?? throw new ArgumentNullException(nameof(dreamDaemonPath)); - DreamMakerPath = dreamMakerPath ?? throw new ArgumentNullException(nameof(dreamMakerPath)); - SupportsCli = supportsCli; - SupportsMapThreads = supportsMapThreads; + ServerExePath = dreamDaemonPath ?? throw new ArgumentNullException(nameof(dreamDaemonPath)); + CompilerExePath = dreamMakerPath ?? throw new ArgumentNullException(nameof(dreamMakerPath)); + HasStandardOutput = supportsCli; + PromptsForNetworkAccess = !supportsCli; + } + + /// + public string FormatServerArguments( + IDmbProvider dmbProvider, + IReadOnlyDictionary parameters, + DreamDaemonLaunchParameters launchParameters, + string logFilePath) + { + ArgumentNullException.ThrowIfNull(dmbProvider); + ArgumentNullException.ThrowIfNull(parameters); + ArgumentNullException.ThrowIfNull(launchParameters); + + switch (Version.Engine.Value) + { + case EngineType.Byond: + var parametersString = String.Join('&', parameters.Select(kvp => $"{HttpUtility.UrlEncode(kvp.Key)}={HttpUtility.UrlEncode(kvp.Value)}")); + + if (!String.IsNullOrEmpty(launchParameters.AdditionalParameters)) + parametersString = $"{parametersString}&{launchParameters.AdditionalParameters}"; + + var supportsMapThreads = Version.Version >= ByondInstallerBase.MapThreadsVersion; + var arguments = String.Format( + CultureInfo.InvariantCulture, + "{0} -port {1} -ports 1-65535 {2}-close -verbose -{3} -{4}{5}{6}{7} -params \"{8}\"", + dmbProvider.DmbName, + launchParameters.Port.Value, + launchParameters.AllowWebClient.Value + ? "-webclient " + : String.Empty, + SecurityWord(launchParameters.SecurityLevel.Value), + VisibilityWord(launchParameters.Visibility.Value), + logFilePath != null + ? $" -logself -log {logFilePath}" + : String.Empty, // DD doesn't output anything if -logself is set??? + launchParameters.StartProfiler.Value + ? " -profile" + : String.Empty, + supportsMapThreads && launchParameters.MapThreads.Value != 0 + ? $" -map-threads {launchParameters.MapThreads.Value}" + : String.Empty, + parametersString); + return arguments; + case EngineType.OpenDream: + throw new NotImplementedException(); + default: + throw new InvalidOperationException($"Invalid EngineType: {Version.Engine.Value}"); + } + } } } diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs b/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs index 2478acdc1f7..8260eafffaa 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs +++ b/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; +using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host.Components.Byond @@ -24,10 +25,10 @@ abstract class ByondInstallerBase : IByondInstaller public static Version MapThreadsVersion => new (515, 1609); /// - public abstract string DreamMakerName { get; } + public abstract string CompilerName { get; } /// - public abstract string PathToUserByondFolder { get; } + public abstract string PathToUserFolder { get; } /// /// Gets the URL formatter string for downloading a byond version of {0:Major} {1:Minor}. @@ -63,7 +64,7 @@ protected ByondInstallerBase(IIOManager ioManager, IFileDownloader fileDownloade } /// - public abstract string GetDreamDaemonName(Version version, out bool supportsCli, out bool supportsMapThreads); + public abstract string GetDreamDaemonName(ByondVersion version, out bool supportsCli, out bool supportsMapThreads); /// public async Task CleanCache(CancellationToken cancellationToken) @@ -73,7 +74,7 @@ public async Task CleanCache(CancellationToken cancellationToken) Logger.LogDebug("Cleaning BYOND cache..."); await IOManager.DeleteDirectory( IOManager.ConcatPath( - PathToUserByondFolder, + PathToUserFolder, CacheDirectoryName), cancellationToken); } @@ -84,18 +85,18 @@ await IOManager.DeleteDirectory( } /// - public abstract ValueTask InstallByond(Version version, string path, CancellationToken cancellationToken); + public abstract ValueTask InstallByond(ByondVersion version, string path, CancellationToken cancellationToken); /// - public abstract ValueTask UpgradeInstallation(Version version, string path, CancellationToken cancellationToken); + public abstract ValueTask UpgradeInstallation(ByondVersion version, string path, CancellationToken cancellationToken); /// - public async ValueTask DownloadVersion(Version version, CancellationToken cancellationToken) + public async ValueTask DownloadVersion(ByondVersion version, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(version); - Logger.LogTrace("Downloading BYOND version {major}.{minor}...", version.Major, version.Minor); - var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsUrlTemplate, version.Major, version.Minor); + Logger.LogTrace("Downloading BYOND version {major}.{minor}...", version.Version.Major, version.Version.Minor); + var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsUrlTemplate, version.Version.Major, version.Version.Minor); await using var download = fileDownloader.DownloadFile(new Uri(url), null); await using var buffer = new BufferedFileStreamProvider( diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs index 73fadec9874..24eb916b265 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.IO; @@ -47,10 +48,10 @@ sealed class ByondManager : IByondManager const string ActiveVersionFileName = "ActiveVersion.txt"; /// - public Version ActiveVersion { get; private set; } + public ByondVersion ActiveVersion { get; private set; } /// - public IReadOnlyList InstalledVersions + public IReadOnlyList InstalledVersions { get { @@ -87,7 +88,7 @@ public IReadOnlyList InstalledVersions /// /// Map of byond s to s that complete when they are installed. /// - readonly Dictionary> installedVersions; + readonly Dictionary> installedVersions; /// /// The for changing or deleting the active BYOND version. @@ -103,14 +104,29 @@ public IReadOnlyList InstalledVersions /// Validates a given parameter. /// /// The to validate. - static void CheckVersionParameter(Version version) + static void CheckVersionParameter(ByondVersion version) { ArgumentNullException.ThrowIfNull(version); - if (version.Build == 0) + if (!version.Engine.HasValue) + throw new InvalidOperationException("version.Engine cannot be null!"); + + if (version.Version.Build == 0) throw new InvalidOperationException("version.Build cannot be 0!"); } + /// + /// Parses a string + /// + /// + /// + /// + /// + static bool TryParseByondVersion(string input, out ByondVersion byondVersion) + { + throw new NotImplementedException(); + } + /// /// Initializes a new instance of the class. /// @@ -125,7 +141,7 @@ public ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, IEvent this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - installedVersions = new Dictionary>(); + installedVersions = new Dictionary>(); changeDeleteSemaphore = new SemaphoreSlim(1); activeVersionChanged = new TaskCompletionSource(); } @@ -136,7 +152,7 @@ public ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, IEvent /// public async ValueTask ChangeVersion( JobProgressReporter progressReporter, - Version version, + ByondVersion version, Stream customVersionStream, bool allowInstallation, CancellationToken cancellationToken) @@ -154,7 +170,10 @@ public async ValueTask ChangeVersion( cancellationToken); // We reparse the version because it could be changed after a custom install. - version = installLock.Version; + version = new ByondVersion(version) + { + Version = installLock.Version, + }; var stringVersion = version.ToString(); await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(stringVersion), cancellationToken); @@ -177,7 +196,7 @@ await eventConsumer.HandleEvent( } /// - public async ValueTask UseExecutables(Version requiredVersion, string trustDmbFullPath, CancellationToken cancellationToken) + public async ValueTask UseExecutables(ByondVersion requiredVersion, string trustDmbFullPath, CancellationToken cancellationToken) { logger.LogTrace( "Acquiring lock on BYOND version {version}...", @@ -205,7 +224,7 @@ public async ValueTask UseExecutables(Version requiredVers } /// - public async ValueTask DeleteVersion(JobProgressReporter progressReporter, Version version, CancellationToken cancellationToken) + public async ValueTask DeleteVersion(JobProgressReporter progressReporter, ByondVersion version, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(progressReporter); @@ -213,15 +232,18 @@ public async ValueTask DeleteVersion(JobProgressReporter progressReporter, Versi logger.LogTrace("DeleteVersion {version}", version); - if (version == ActiveVersion) + if (version.Equals(ActiveVersion)) throw new JobException(ErrorCode.ByondCannotDeleteActiveVersion); - ReferenceCountingContainer container; + ReferenceCountingContainer container; lock (installedVersions) if (!installedVersions.TryGetValue(version, out container)) - return; // already "deleted" + { + logger.LogTrace("Version {version} already deleted.", version); + return; + } - logger.LogInformation("Deleting BYOND version {version}...", version); + logger.LogInformation("Deleting version {version}...", version); progressReporter.StageName = "Waiting for version to not be in use..."; while (true) { @@ -238,7 +260,7 @@ await Task.WhenAny( .WaitAsync(cancellationToken); if (containerTask.IsCompleted) - logger.LogTrace("All BYOND locks for {version} are gone", version); + logger.LogTrace("All locks for version {version} are gone", version); using (await SemaphoreSlimContext.Lock(changeDeleteSemaphore, cancellationToken)) { @@ -252,7 +274,7 @@ await Task.WhenAny( proceed = container.OnZeroReferences.IsCompleted; if (proceed) if (!installedVersions.TryGetValue(version, out var newerContainer)) - logger.LogWarning("Unable to remove BYOND installation {version} from list! Is there a duplicate job running?", version); + logger.LogWarning("Unable to remove engine installation {version} from list! Is there a duplicate job running?", version); else { if (container != newerContainer) @@ -302,7 +324,7 @@ async ValueTask GetActiveVersion() var activeVersionBytesTask = GetActiveVersion(); - var byondDir = byondInstaller.PathToUserByondFolder; + var byondDir = byondInstaller.PathToUserFolder; if (byondDir != null) using (await SemaphoreSlimContext.Lock(UserFilesSemaphore, cancellationToken)) { @@ -328,7 +350,7 @@ await ioManager.DeleteFile( await ioManager.CreateDirectory(DefaultIOManager.CurrentDirectory, cancellationToken); var directories = await ioManager.GetDirectories(DefaultIOManager.CurrentDirectory, cancellationToken); - var installedVersionPaths = new Dictionary(); + var installedVersionPaths = new Dictionary(); async ValueTask ReadVersion(string path) { @@ -342,7 +364,7 @@ async ValueTask ReadVersion(string path) var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken); var text = Encoding.UTF8.GetString(bytes); - if (!Version.TryParse(text, out var version)) + if (!TryParseByondVersion(text, out var version)) { logger.LogWarning("Cleaning path with unparsable version file: {versionPath}", ioManager.ResolvePath(path)); await ioManager.DeleteDirectory(path, cancellationToken); // cleanup @@ -383,10 +405,10 @@ await ValueTaskExtensions.WhenAll( { var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes); - Version activeVersion; + ByondVersion activeVersion; bool hasRequestedActiveVersion; lock (installedVersions) - hasRequestedActiveVersion = Version.TryParse(activeVersionString, out activeVersion) + hasRequestedActiveVersion = TryParseByondVersion(activeVersionString, out activeVersion) && installedVersions.ContainsKey(activeVersion); if (hasRequestedActiveVersion) @@ -403,46 +425,47 @@ await ValueTaskExtensions.WhenAll( public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; /// - /// Ensures a BYOND is installed if it isn't already. + /// Ensures a BYOND is installed if it isn't already. /// /// The optional for the operation. - /// The BYOND to install. + /// The to install. /// Custom zip file to use. Will cause a number to be added. /// If this BYOND version is required as part of a locking operation. - /// If an installation should be performed if the is not installed. If and an installation is required an will be thrown. + /// If an installation should be performed if the is not installed. If and an installation is required an will be thrown. /// The for the operation. /// A resulting in the . async ValueTask AssertAndLockVersion( JobProgressReporter progressReporter, - Version version, + ByondVersion byondVersion, Stream customVersionStream, bool neededForLock, bool allowInstallation, CancellationToken cancellationToken) { var ourTcs = new TaskCompletionSource(); - ByondInstallation installation; + IByondInstallation installation; ByondExecutableLock installLock; bool installedOrInstalling; + var byondEngine = byondVersion.Engine.Value == EngineType.Byond; lock (installedVersions) { - if (customVersionStream != null) + if (customVersionStream != null && byondEngine) { var customInstallationNumber = 1; do { - version = new Version(version.Major, version.Minor, customInstallationNumber++); + byondVersion.Version = new Version(byondVersion.Version.Major, byondVersion.Version.Minor, customInstallationNumber++); } - while (installedVersions.ContainsKey(version)); + while (installedVersions.ContainsKey(byondVersion)); } - installedOrInstalling = installedVersions.TryGetValue(version, out var installationContainer); + installedOrInstalling = installedVersions.TryGetValue(byondVersion, out var installationContainer); if (!installedOrInstalling) { if (!allowInstallation) - throw new InvalidOperationException($"BYOND version {version} not installed!"); + throw new InvalidOperationException($"BYOND version {byondVersion} not installed!"); - installationContainer = AddInstallationContainer(version, ourTcs.Task); + installationContainer = AddInstallationContainer(byondVersion, ourTcs.Task); } installation = installationContainer.Instance; @@ -457,7 +480,7 @@ async ValueTask AssertAndLockVersion( progressReporter.StageName = "Waiting for existing installation job..."; if (neededForLock && !installation.InstallationTask.IsCompleted) - logger.LogWarning("The required BYOND version ({version}) is not readily available! We will have to wait for it to install.", version); + logger.LogWarning("The required BYOND version ({version}) is not readily available! We will have to wait for it to install.", byondVersion); await installation.InstallationTask.WaitAsync(cancellationToken); return installLock; @@ -467,24 +490,24 @@ async ValueTask AssertAndLockVersion( try { if (customVersionStream != null) - logger.LogInformation("Installing custom BYOND version as {version}...", version); + logger.LogInformation("Installing custom BYOND version as {version}...", byondVersion); else if (neededForLock) { - if (version.Build > 0) + if (byondEngine && byondVersion.Version.Build > 0) throw new JobException(ErrorCode.ByondNonExistentCustomVersion); - logger.LogWarning("The required BYOND version ({version}) is not readily available! We will have to install it.", version); + logger.LogWarning("The required BYOND version ({version}) is not readily available! We will have to install it.", byondVersion); } else - logger.LogDebug("Requested BYOND version {version} not currently installed. Doing so now...", version); + logger.LogDebug("Requested BYOND version {version} not currently installed. Doing so now...", byondVersion); if (progressReporter != null) progressReporter.StageName = "Running event"; - var versionString = version.ToString(); + var versionString = byondVersion.ToString(); await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List { versionString }, false, cancellationToken); - await InstallVersionFiles(progressReporter, version, customVersionStream, cancellationToken); + await InstallVersionFiles(progressReporter, byondVersion, customVersionStream, cancellationToken); ourTcs.SetResult(); } @@ -494,7 +517,7 @@ async ValueTask AssertAndLockVersion( await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List { ex.Message }, false, cancellationToken); lock (installedVersions) - installedVersions.Remove(version); + installedVersions.Remove(byondVersion); ourTcs.SetException(ex); throw; @@ -513,11 +536,11 @@ async ValueTask AssertAndLockVersion( /// Installs the files for a given BYOND . /// /// The optional for the operation. - /// The BYOND being installed with the number set if appropriate. + /// The being installed with the number set if appropriate. /// Custom zip file to use. Will cause a number to be added. /// The for the operation. /// A representing the running operation. - async ValueTask InstallVersionFiles(JobProgressReporter progressReporter, Version version, Stream customVersionStream, CancellationToken cancellationToken) + async ValueTask InstallVersionFiles(JobProgressReporter progressReporter, ByondVersion version, Stream customVersionStream, CancellationToken cancellationToken) { var installFullPath = ioManager.ResolvePath(version.ToString()); async ValueTask DirectoryCleanup() @@ -590,27 +613,36 @@ await ioManager.WriteAllBytes( /// The being added. /// The representing the installation process. /// The new containing the new . - ReferenceCountingContainer AddInstallationContainer(Version version, Task installationTask) + ReferenceCountingContainer AddInstallationContainer(ByondVersion version, Task installationTask) { var binPathForVersion = ioManager.ConcatPath(version.ToString(), BinPath); - var installation = new ByondInstallation( - installationTask, - version, - ioManager.ResolvePath( - ioManager.ConcatPath( - binPathForVersion, - byondInstaller.GetDreamDaemonName( - version, - out var supportsCli, - out var supportsMapThreads))), - ioManager.ResolvePath( - ioManager.ConcatPath( - binPathForVersion, - byondInstaller.DreamMakerName)), - supportsCli, - supportsMapThreads); - - var installationContainer = new ReferenceCountingContainer(installation); + IByondInstallation installation; + + switch (version.Engine.Value) + { + case EngineType.Byond: + installation = new ByondInstallation( + installationTask, + version.Version, + ioManager.ResolvePath( + ioManager.ConcatPath( + binPathForVersion, + byondInstaller.GetDreamDaemonName( + version.Version, + out var supportsCli))), + ioManager.ResolvePath( + ioManager.ConcatPath( + binPathForVersion, + byondInstaller.CompilerName)), + supportsCli); + break; + case EngineType.OpenDream: + throw new NotImplementedException(); + default: + throw new InvalidOperationException($"Invalid EngineType: {version.Engine.Value}"); + } + + var installationContainer = new ReferenceCountingContainer(installation); lock (installedVersions) installedVersions.Add(version, installationContainer); @@ -626,7 +658,7 @@ ReferenceCountingContainer AddInstallati /// A representing the running operation. async ValueTask TrustDmbPath(string fullDmbPath, CancellationToken cancellationToken) { - var byondDir = byondInstaller.PathToUserByondFolder; + var byondDir = byondInstaller.PathToUserFolder; if (String.IsNullOrWhiteSpace(byondDir)) { logger.LogTrace("No relevant user BYOND directory to install a \"{fileName}\" in", TrustedDmbFileName); diff --git a/src/Tgstation.Server.Host/Components/Byond/IByondInstallation.cs b/src/Tgstation.Server.Host/Components/Byond/IByondInstallation.cs index 52a4e694b71..0a08217ba51 100644 --- a/src/Tgstation.Server.Host/Components/Byond/IByondInstallation.cs +++ b/src/Tgstation.Server.Host/Components/Byond/IByondInstallation.cs @@ -1,4 +1,8 @@ -using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Components.Deployment; namespace Tgstation.Server.Host.Components.Byond { @@ -8,28 +12,47 @@ namespace Tgstation.Server.Host.Components.Byond public interface IByondInstallation { /// - /// The of the . + /// The of the . /// - Version Version { get; } + ByondVersion Version { get; } /// - /// The full path to the DreamDaemon executable. + /// The full path to the game server executable. /// - string DreamDaemonPath { get; } + string ServerExePath { get; } /// /// The full path to the dm/DreamMaker executable. /// - string DreamMakerPath { get; } + string CompilerExePath { get; } + + /// + /// If supports being run as a command-line application and outputs log information to be captured. + /// + bool HasStandardOutput { get; } + + /// + /// If may create network prompts. + /// + bool PromptsForNetworkAccess { get; } /// - /// If supports being run as a command-line application. + /// The that completes when the BYOND version finished installing. /// - bool SupportsCli { get; } + Task InstallationTask { get; } /// - /// If supports the -map-threads parameter. + /// Return the command line arguments for launching with given . /// - bool SupportsMapThreads { get; } + /// The . + /// The map of parameter s as a . Should NOT include the of . + /// The . + /// The path to the log file, if any. + /// The formatted arguments . + string FormatServerArguments( + IDmbProvider dmbProvider, + IReadOnlyDictionary parameters, + DreamDaemonLaunchParameters launchParameters, + string logFilePath); } } diff --git a/src/Tgstation.Server.Host/Components/Byond/IByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/IByondInstaller.cs index 9c353378303..b977c31353c 100644 --- a/src/Tgstation.Server.Host/Components/Byond/IByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Byond/IByondInstaller.cs @@ -1,8 +1,9 @@ -using System; -using System.IO; +using System.IO; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models.Internal; + namespace Tgstation.Server.Host.Components.Byond { /// @@ -11,52 +12,61 @@ namespace Tgstation.Server.Host.Components.Byond interface IByondInstaller { /// - /// Get the file name of the DreamMaker executable. + /// Get the file name of the compiler executable. /// - string DreamMakerName { get; } + string CompilerName { get; } /// - /// The path to the BYOND folder for the user. + /// The path to the folder for the user's data. /// - string PathToUserByondFolder { get; } + string PathToUserFolder { get; } /// /// Get the file name of the DreamDaemon executable. /// - /// The of BYOND to select the executable name for. + /// The of BYOND to select the executable name for. /// Whether or not the returned path supports being run as a command-line application. /// Whether or not the returned path supports the '-map-threads' parameter. /// The file name of the DreamDaemon executable. - string GetDreamDaemonName(Version version, out bool supportsCli, out bool supportsMapThreads); + string GetDreamDaemonName(ByondVersion version, out bool supportsCli, out bool supportsMapThreads); /// /// Download a given BYOND . /// - /// The of BYOND to download. + /// The of BYOND to download. /// The for the operation. /// A resulting in a of the zipfile. - ValueTask DownloadVersion(Version version, CancellationToken cancellationToken); + ValueTask DownloadVersion(ByondVersion version, CancellationToken cancellationToken); /// /// Does actions necessary to get an extracted BYOND installation working. /// - /// The of BYOND being installed. + /// The being installed. + /// The path to the BYOND installation. + /// The for the operation. + /// A representing the running operation. + ValueTask InstallByond(ByondVersion version, string path, CancellationToken cancellationToken); + + /// + /// Does actions necessary to remove a given + /// + /// The being installed. /// The path to the BYOND installation. /// The for the operation. /// A representing the running operation. - ValueTask InstallByond(Version version, string path, CancellationToken cancellationToken); + ValueTask DeleteVersion(ByondVersion version, string path, CancellationToken cancellationToken); /// /// Does actions necessary to get upgrade a BYOND version installed by a previous version of TGS. /// - /// The of BYOND being installed. + /// The being installed. /// The path to the BYOND installation. /// The for the operation. /// A representing the running operation. - ValueTask UpgradeInstallation(Version version, string path, CancellationToken cancellationToken); + ValueTask UpgradeInstallation(ByondVersion version, string path, CancellationToken cancellationToken); /// - /// Attempts to cleans the BYOND cache folder for the system. + /// Attempts to cleans the engine's cache folder for the system. /// /// The for the operation. /// A representing the running operation. diff --git a/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs index 092034b1c0b..a8a0c8d0d9b 100644 --- a/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs @@ -4,6 +4,8 @@ using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Jobs; namespace Tgstation.Server.Host.Components.Byond @@ -11,48 +13,62 @@ namespace Tgstation.Server.Host.Components.Byond /// /// For managing the BYOND installation. /// - /// When passing in s, ensure they are BYOND format versions unless referring to a custom version. This means should NEVER be 0. + /// When passing in s, ensure they are BYOND format versions unless referring to a custom or version. This means should NEVER be 0. public interface IByondManager : IComponentService, IDisposable { /// - /// The currently active BYOND version. + /// The currently active . /// - Version ActiveVersion { get; } + ByondVersion ActiveVersion { get; } /// - /// The installed BYOND versions. + /// The installed s. /// - IReadOnlyList InstalledVersions { get; } + IReadOnlyList InstalledVersions { get; } /// - /// Change the active BYOND version. + /// Ensure that the given is registered for the given . + /// + /// The source of the . + /// The . + /// The for the operation. + /// A representing the running operation. + ValueTask EnsureEngineSource(Uri source, EngineType engine, CancellationToken cancellationToken); + + /// + /// Change the active . /// /// The optional for the operation. - /// The new . + /// The new . /// Optional of a custom BYOND version zip file. /// If an installation should be performed if the is not installed. If and an installation is required an will be thrown. /// The for the operation. /// A representing the running operation. - ValueTask ChangeVersion(JobProgressReporter progressReporter, Version version, Stream customVersionStream, bool allowInstallation, CancellationToken cancellationToken); + ValueTask ChangeVersion( + JobProgressReporter progressReporter, + ByondVersion version, + Stream customVersionStream, + bool allowInstallation, + CancellationToken cancellationToken); /// - /// Deletes a given BYOND version from the disk. + /// Deletes a given from the disk. /// /// The for the operation. - /// The to delete. + /// The to delete. /// The for the operation. - /// A representing the running operation. - ValueTask DeleteVersion(JobProgressReporter progressReporter, Version version, CancellationToken cancellationToken); + /// A representing the running operation. + ValueTask DeleteVersion(JobProgressReporter progressReporter, ByondVersion version, CancellationToken cancellationToken); /// /// Lock the current installation's location and return a . /// - /// The BYOND required. + /// The required. /// The optional full path to .dmb to trust while using the executables. /// The for the operation. /// A resulting in the requested . ValueTask UseExecutables( - Version requiredVersion, + ByondVersion requiredVersion, string trustDmbFullPath, CancellationToken cancellationToken); } diff --git a/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs index 3657e015d47..ee71983054d 100644 --- a/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs @@ -32,10 +32,10 @@ sealed class PosixByondInstaller : ByondInstallerBase const string ShellScriptExtension = ".sh"; /// - public override string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension; + public override string CompilerName => DreamMakerExecutableName + ShellScriptExtension; /// - public override string PathToUserByondFolder { get; } + public override string PathToUserFolder { get; } /// protected override string ByondRevisionsUrlTemplate => "https://www.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip"; @@ -61,7 +61,7 @@ public PosixByondInstaller( { this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); - PathToUserByondFolder = IOManager.ResolvePath( + PathToUserFolder = IOManager.ResolvePath( IOManager.ConcatPath( Environment.GetFolderPath( Environment.SpecialFolder.UserProfile), @@ -69,12 +69,11 @@ public PosixByondInstaller( } /// - public override string GetDreamDaemonName(Version version, out bool supportsCli, out bool supportsMapThreads) + public override string GetDreamDaemonName(Version version, out bool supportsCli) { ArgumentNullException.ThrowIfNull(version); supportsCli = true; - supportsMapThreads = version >= MapThreadsVersion; return DreamDaemonExecutableName + ShellScriptExtension; } @@ -105,7 +104,7 @@ async ValueTask WriteAndMakeExecutable(string pathToScript, string script) dreamDaemonScript); var dmTask = WriteAndMakeExecutable( - IOManager.ConcatPath(basePath, DreamMakerName), + IOManager.ConcatPath(basePath, CompilerName), dreamMakerScript); var task = ValueTaskExtensions.WhenAll( diff --git a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs index 8080abc12ff..15a22e84eea 100644 --- a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs @@ -53,10 +53,10 @@ sealed class WindowsByondInstaller : ByondInstallerBase, IDisposable public static Version DDExeVersion => new (515, 1598); /// - public override string DreamMakerName => "dm.exe"; + public override string CompilerName => "dm.exe"; /// - public override string PathToUserByondFolder { get; } + public override string PathToUserFolder { get; } /// protected override string ByondRevisionsUrlTemplate => "https://www.byond.com/download/build/{0}/{0}.{1}_byond.zip"; @@ -102,9 +102,9 @@ public WindowsByondInstaller( var documentsDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); if (String.IsNullOrWhiteSpace(documentsDirectory)) - PathToUserByondFolder = null; // happens with the service account + PathToUserFolder = null; // happens with the service account else - PathToUserByondFolder = IOManager.ResolvePath(IOManager.ConcatPath(documentsDirectory, "BYOND")); + PathToUserFolder = IOManager.ResolvePath(IOManager.ConcatPath(documentsDirectory, "BYOND")); semaphore = new SemaphoreSlim(1); installedDirectX = false; @@ -114,12 +114,11 @@ public WindowsByondInstaller( public void Dispose() => semaphore.Dispose(); /// - public override string GetDreamDaemonName(Version version, out bool supportsCli, out bool supportsMapThreads) + public override string GetDreamDaemonName(Version version, out bool supportsCli) { ArgumentNullException.ThrowIfNull(version); supportsCli = version >= DDExeVersion; - supportsMapThreads = version >= MapThreadsVersion; return supportsCli ? "dd.exe" : "dreamdaemon.exe"; } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 30f6a555df5..1571978b299 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -637,7 +637,7 @@ await eventConsumer.HandleEvent( // run compiler progressReporter.StageName = "Running DreamMaker"; - var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken); + var exitCode = await RunDreamMaker(byondLock.CompilerExePath, job, cancellationToken); // Session takes ownership of the lock and Disposes it so save this for later var byondVersion = byondLock.Version; diff --git a/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs index efc773a9193..8f81cb7fbc1 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs @@ -1,6 +1,6 @@ using System; -using Tgstation.Server.Host.Models; +using Tgstation.Server.Api.Models.Internal; namespace Tgstation.Server.Host.Components.Deployment { @@ -22,7 +22,12 @@ public interface IDmbProvider : IDisposable /// /// The of the .dmb. /// - CompileJob CompileJob { get; } + Models.CompileJob CompileJob { get; } + + /// + /// The used to build the .dmb. + /// + ByondVersion ByondVersion { get; } /// /// Disposing the won't cause a cleanup of the working directory. diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index 3bc79939c7d..92954b7b2e0 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -128,38 +128,6 @@ sealed class SessionControllerFactory : ISessionControllerFactory /// readonly Api.Models.Instance instance; - /// - /// Change a given into the appropriate DreamDaemon command line word. - /// - /// The level to change. - /// A representation of the command line parameter. - static string SecurityWord(DreamDaemonSecurity securityLevel) - { - return securityLevel switch - { - DreamDaemonSecurity.Safe => "safe", - DreamDaemonSecurity.Trusted => "trusted", - DreamDaemonSecurity.Ultrasafe => "ultrasafe", - _ => throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon security level: {0}", securityLevel)), - }; - } - - /// - /// Change a given into the appropriate DreamDaemon command line word. - /// - /// The level to change. - /// A representation of the command line parameter. - static string VisibilityWord(DreamDaemonVisibility visibility) - { - return visibility switch - { - DreamDaemonVisibility.Public => "public", - DreamDaemonVisibility.Private => "private", - DreamDaemonVisibility.Invisible => "invisible", - _ => throw new ArgumentOutOfRangeException(nameof(visibility), visibility, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon visibility level: {0}", visibility)), - }; - } - /// /// Check if a given can be bound to. /// @@ -274,7 +242,7 @@ public async ValueTask LaunchNew( // get the byond lock var byondLock = currentByondLock ?? await byond.UseExecutables( - Version.Parse(dmbProvider.CompileJob.ByondVersion), + dmbProvider.ByondVersion, gameIOManager.ConcatPath(dmbProvider.Directory, dmbProvider.DmbName), cancellationToken); try @@ -289,7 +257,7 @@ public async ValueTask LaunchNew( string outputFilePath = null; var preserveLogFile = true; - var cliSupported = byondLock.SupportsCli; + var hasStandardOutput = byondLock.HasStandardOutput; if (launchParameters.LogOutput.Value) { var now = DateTimeOffset.UtcNow; @@ -302,7 +270,7 @@ public async ValueTask LaunchNew( logger.LogInformation("Logging DreamDaemon output to {path}...", outputFilePath); } - else if (!cliSupported) + else if (!hasStandardOutput) { outputFilePath = gameIOManager.ConcatPath(dmbProvider.Directory, $"{Guid.NewGuid()}.dd.log"); preserveLogFile = false; @@ -310,17 +278,12 @@ public async ValueTask LaunchNew( var accessIdentifier = cryptographySuite.GetSecureString(); - var byondTopicSender = topicClientFactory.CreateTopicClient( - TimeSpan.FromMilliseconds( - launchParameters.TopicRequestTimeout.Value)); - if (!apiValidate && dmbProvider.CompileJob.DMApiVersion == null) logger.LogDebug("Session will have no DMAPI support!"); // launch dd - var process = await CreateDreamDaemonProcess( + var process = await CreateGameServerProcess( dmbProvider, - byondTopicSender, byondLock, launchParameters, accessIdentifier, @@ -347,6 +310,10 @@ public async ValueTask LaunchNew( accessIdentifier, launchParameters.Port.Value); + var byondTopicSender = topicClientFactory.CreateTopicClient( + TimeSpan.FromMilliseconds( + launchParameters.TopicRequestTimeout.Value)); + var sessionController = new SessionController( reattachInformation, instance, @@ -362,7 +329,7 @@ public async ValueTask LaunchNew( () => LogDDOutput( process, outputFilePath, - cliSupported, + hasStandardOutput, preserveLogFile, CancellationToken.None), // DCT: None available launchParameters.StartupTimeout, @@ -406,7 +373,7 @@ public async ValueTask Reattach( logger.LogTrace("Begin session reattach..."); var byondTopicSender = topicClientFactory.CreateTopicClient(reattachInformation.TopicRequestTimeout); var byondLock = await byond.UseExecutables( - Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion), + reattachInformation.Dmb.ByondVersion, null, // Doesn't matter if it's trusted or not on reattach cancellationToken); @@ -423,7 +390,7 @@ public async ValueTask Reattach( try { - if (!byondLock.SupportsCli) + if (byondLock.PromptsForNetworkAccess) networkPromptReaper.RegisterProcess(process); var chatTrackingContext = chat.CreateTrackingContext(); @@ -479,10 +446,9 @@ public async ValueTask Reattach( } /// - /// Creates the DreamDaemon . + /// Creates the game server . /// /// The . - /// The to use for sanitization. /// The . /// The . /// The secure string to use for the session. @@ -490,9 +456,8 @@ public async ValueTask Reattach( /// If we are only validating the DMAPI then exiting. /// The for the operation. /// A resulting in the DreamDaemon . - async ValueTask CreateDreamDaemonProcess( + async ValueTask CreateGameServerProcess( IDmbProvider dmbProvider, - ITopicClient byondTopicSender, IByondExecutableLock byondLock, DreamDaemonLaunchParameters launchParameters, string accessIdentifier, @@ -500,39 +465,26 @@ async ValueTask CreateDreamDaemonProcess( bool apiValidate, CancellationToken cancellationToken) { - // set command line options - // more sanitization here cause it uses the same scheme - var parameters = $"{DMApiConstants.ParamApiVersion}={byondTopicSender.SanitizeString(DMApiConstants.InteropVersion.Semver().ToString())}&{byondTopicSender.SanitizeString(DMApiConstants.ParamServerPort)}={serverPortProvider.HttpApiPort}&{byondTopicSender.SanitizeString(DMApiConstants.ParamAccessIdentifier)}={byondTopicSender.SanitizeString(accessIdentifier)}"; - - if (!String.IsNullOrEmpty(launchParameters.AdditionalParameters)) - parameters = $"{parameters}&{launchParameters.AdditionalParameters}"; - // important to run on all ports to allow port changing - var arguments = String.Format( - CultureInfo.InvariantCulture, - "{0} -port {1} -ports 1-65535 {2}-close -verbose -{3} -{4}{5}{6}{7} -params \"{8}\"", - dmbProvider.DmbName, - launchParameters.Port.Value, - launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty, - SecurityWord(launchParameters.SecurityLevel.Value), - VisibilityWord(launchParameters.Visibility.Value), - !byondLock.SupportsCli - ? $" -logself -log {logFilePath}" - : String.Empty, // DD doesn't output anything if -logself is set??? - launchParameters.StartProfiler.Value - ? " -profile" - : String.Empty, - byondLock.SupportsMapThreads && launchParameters.MapThreads.Value != 0 - ? $" -map-threads {launchParameters.MapThreads.Value}" - : String.Empty, - parameters); + var arguments = byondLock.FormatServerArguments( + dmbProvider, + new Dictionary + { + { DMApiConstants.ParamApiVersion, DMApiConstants.InteropVersion.Semver().ToString() }, + { DMApiConstants.ParamServerPort, serverPortProvider.HttpApiPort.ToString(CultureInfo.InvariantCulture) }, + { DMApiConstants.ParamAccessIdentifier, accessIdentifier }, + }, + launchParameters, + !byondLock.HasStandardOutput + ? logFilePath + : null); var process = processExecutor.LaunchProcess( - byondLock.DreamDaemonPath, + byondLock.ServerExePath, dmbProvider.Directory, arguments, logFilePath, - byondLock.SupportsCli, + byondLock.HasStandardOutput, true); try @@ -545,7 +497,7 @@ async ValueTask CreateDreamDaemonProcess( else if (sessionConfiguration.LowPriorityDeploymentProcesses) process.AdjustPriority(false); - if (!byondLock.SupportsCli) + if (!byondLock.HasStandardOutput) networkPromptReaper.RegisterProcess(process); // If this isnt a staging DD (From a Deployment), fire off an event diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index 156ff784e93..082ab8b573b 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -43,7 +43,7 @@ public sealed class ByondController : InstanceRequiredController /// /// The to normalize. /// The normalized . May be a reference to . - static Version NormalizeVersion(Version version) => version.Build == 0 ? new Version(version.Major, version.Minor) : version; + static Version NormalizeByondVersion(Version version) => version.Build == 0 ? new Version(version.Major, version.Minor) : version; /// /// Initializes a new instance of the class. @@ -72,7 +72,7 @@ public ByondController( } /// - /// Gets the active . + /// Gets the active . /// /// A resulting in the for the operation. /// Retrieved version information successfully. @@ -82,13 +82,12 @@ public ByondController( public ValueTask Read() => WithComponentInstance(instance => ValueTask.FromResult( - Json(new ByondResponse - { - Version = instance.ByondManager.ActiveVersion, - }))); + Json( + new ByondResponse( + instance.ByondManager.ActiveVersion)))); /// - /// Lists installed s. + /// Lists installed s. /// /// The current page. /// The page size. @@ -106,10 +105,7 @@ public ValueTask List([FromQuery] int? page, [FromQuery] int? pag instance .ByondManager .InstalledVersions - .Select(x => new ByondResponse - { - Version = x, - }) + .Select(x => new ByondResponse(x)) .AsQueryable() .OrderBy(x => x.Version))), null, @@ -126,7 +122,11 @@ public ValueTask List([FromQuery] int? page, [FromQuery] int? pag /// Switched active version successfully. /// Created to install and switch active version successfully. [HttpPost] - [TgsAuthorize(ByondRights.InstallOfficialOrChangeActiveVersion | ByondRights.InstallCustomVersion)] + [TgsAuthorize( + ByondRights.InstallOfficialOrChangeActiveByondVersion + | ByondRights.InstallCustomByondVersion + | ByondRights.InstallOfficialOrChangeActiveOpenDreamVersion + | ByondRights.InstallCustomOpenDreamVersion)] [ProducesResponseType(typeof(ByondInstallResponse), 200)] [ProducesResponseType(typeof(ByondInstallResponse), 202)] #pragma warning disable CA1506 // TODO: Decomplexify @@ -136,17 +136,18 @@ public async ValueTask Update([FromBody] ByondVersionRequest mode ArgumentNullException.ThrowIfNull(model); var uploadingZip = model.UploadCustomZip == true; + var isByondEngine = model.Engine.Value == EngineType.Byond; - if (model.Version == null - || model.Version.Revision != -1 - || (uploadingZip && model.Version.Build > 0)) + if (model.Version.Revision != -1 + || (isByondEngine && ((uploadingZip && model.Version.Build > 0) || model.SourceCommittish != null || model.SourceRepository != null))) return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)); - var version = NormalizeVersion(model.Version); + if (model.Engine == EngineType.Byond) + model.Version = NormalizeByondVersion(model.Version); var userByondRights = AuthenticationContext.InstancePermissionSet.ByondRights.Value; - if ((!userByondRights.HasFlag(ByondRights.InstallOfficialOrChangeActiveVersion) && !uploadingZip) - || (!userByondRights.HasFlag(ByondRights.InstallCustomVersion) && uploadingZip)) + if ((!userByondRights.HasFlag(ByondRights.InstallOfficialOrChangeActiveByondVersion) && !uploadingZip) + || (!userByondRights.HasFlag(ByondRights.InstallCustomByondVersion) && uploadingZip)) return Forbid(); // remove cruff fields @@ -155,44 +156,50 @@ public async ValueTask Update([FromBody] ByondVersionRequest mode async instance => { var byondManager = instance.ByondManager; - var versionAlreadyInstalled = !uploadingZip && byondManager.InstalledVersions.Any(x => x == version); + var versionAlreadyInstalled = !uploadingZip && byondManager.InstalledVersions.Any(x => x.Equals(model)); if (versionAlreadyInstalled) { Logger.LogInformation( - "User ID {userId} changing instance ID {instanceId} BYOND version to {newByondVersion}", + "User ID {userId} changing instance ID {instanceId} {engineType} version to {newByondVersion}", AuthenticationContext.User.Id, Instance.Id, - version); + model.Engine, + model.Version); try { - await byondManager.ChangeVersion(null, version, null, false, cancellationToken); + await byondManager.ChangeVersion(null, model, null, false, cancellationToken); } catch (InvalidOperationException ex) { Logger.LogDebug( ex, - "Race condition: BYOND version {version} uninstalled before we could switch to it. Creating install job instead...", - version); + "Race condition: {engineType} version {version} uninstalled before we could switch to it. Creating install job instead...", + model.Engine.Value, + model.Version); versionAlreadyInstalled = false; } } if (!versionAlreadyInstalled) { - if (version.Build > 0) + if (model.Version.Build > 0) return BadRequest(new ErrorMessageResponse(ErrorCode.ByondNonExistentCustomVersion)); Logger.LogInformation( - "User ID {userId} installing BYOND version to {newByondVersion} on instance ID {instanceId}", + "User ID {userId} installing {engineType} version {newByondVersion}{sourceCommittish} on instance ID {instanceId}", AuthenticationContext.User.Id, - version, + model.Engine.Value, + model.Version, + model.SourceCommittish != null + ? $" ({model.SourceCommittish})" + : String.Empty, Instance.Id); // run the install through the job manager var job = new Job { - Description = $"Install {(!uploadingZip ? String.Empty : "custom ")}BYOND version {version}", + Description = $"Install {(!uploadingZip ? String.Empty : "custom ")}{model.Engine.Value} version {model.Version}", StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.Byond, CancelRight = (ulong)ByondRights.CancelInstall, @@ -209,7 +216,13 @@ await jobManager.RegisterOperation( job, async (core, databaseContextFactory, paramJob, progressHandler, jobCancellationToken) => { - Stream zipFileStream = null; + if (model.SourceRepository != null) + await core.ByondManager.EnsureEngineSource( + model.SourceRepository, + model.Engine.Value, + jobCancellationToken); + + MemoryStream zipFileStream = null; if (fileUploadTicket != null) await using (fileUploadTicket) { @@ -229,7 +242,7 @@ await jobManager.RegisterOperation( await using (zipFileStream) await core.ByondManager.ChangeVersion( progressHandler, - version, + model, zipFileStream, true, jobCancellationToken); @@ -260,7 +273,7 @@ await core.ByondManager.ChangeVersion( /// A resulting in the for the operation. /// Created to delete target version successfully. /// Attempted to delete the active BYOND . - /// The specified was not installed. + /// The specified was not installed. [HttpDelete] [TgsAuthorize(ByondRights.DeleteInstall)] [ProducesResponseType(typeof(JobResponse), 202)] @@ -270,22 +283,22 @@ public async ValueTask Delete([FromBody] ByondVersionDeleteReques { ArgumentNullException.ThrowIfNull(model); - if (model.Version == null - || model.Version.Revision != -1) + if (model.Version.Revision != -1) return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)); - var version = NormalizeVersion(model.Version); + if (model.Engine == EngineType.Byond) + model.Version = NormalizeByondVersion(model.Version); var notInstalledResponse = await WithComponentInstance( instance => { var byondManager = instance.ByondManager; - if (version == byondManager.ActiveVersion) + if (model.Equals(byondManager.ActiveVersion)) return ValueTask.FromResult( Conflict(new ErrorMessageResponse(ErrorCode.ByondCannotDeleteActiveVersion))); - var versionNotInstalled = !byondManager.InstalledVersions.Any(x => x == version); + var versionNotInstalled = !byondManager.InstalledVersions.Any(x => x.Equals(model)); return ValueTask.FromResult( versionNotInstalled @@ -296,22 +309,27 @@ public async ValueTask Delete([FromBody] ByondVersionDeleteReques if (notInstalledResponse != null) return notInstalledResponse; - var isCustomVersion = version.Build != -1; + var isByondVersion = model.Engine.Value == EngineType.Byond; // run the install through the job manager var job = new Job { - Description = $"Delete installed BYOND version {version}", + Description = $"Delete installed {model.Engine.Value} version {model.Version}", StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.Byond, - CancelRight = (ulong)(isCustomVersion ? ByondRights.InstallOfficialOrChangeActiveVersion : ByondRights.InstallCustomVersion), + CancelRight = (ulong)( + isByondVersion + ? model.Version.Build != -1 + ? ByondRights.InstallOfficialOrChangeActiveByondVersion + : ByondRights.InstallCustomByondVersion + : ByondRights.InstallCustomOpenDreamVersion | ByondRights.InstallOfficialOrChangeActiveOpenDreamVersion), Instance = Instance, }; await jobManager.RegisterOperation( job, (instanceCore, databaseContextFactory, job, progressReporter, jobCancellationToken) - => instanceCore.ByondManager.DeleteVersion(progressReporter, version, jobCancellationToken), + => instanceCore.ByondManager.DeleteVersion(progressReporter, model, jobCancellationToken), cancellationToken); var apiResponse = job.ToApi(); diff --git a/src/Tgstation.Server.Host/Models/CompileJob.cs b/src/Tgstation.Server.Host/Models/CompileJob.cs index 9ddde1344c1..92f45ab843e 100644 --- a/src/Tgstation.Server.Host/Models/CompileJob.cs +++ b/src/Tgstation.Server.Host/Models/CompileJob.cs @@ -32,6 +32,12 @@ public sealed class CompileJob : Api.Models.Internal.CompileJob, IApiTransformab [Required] public string ByondVersion { get; set; } + /// + /// The of . + /// + [Required] + public EngineType Engine { get; set; } + /// /// Backing field for of . /// @@ -82,7 +88,7 @@ public override Version DMApiVersion } /// - public CompileJobResponse ToApi() => new CompileJobResponse + public CompileJobResponse ToApi() => new () { DirectoryName = DirectoryName, DmeName = DmeName, diff --git a/tests/Tgstation.Server.Api.Tests/Rights/TestRights.cs b/tests/Tgstation.Server.Api.Tests/Rights/TestRights.cs index 62e41b7d982..e773f4da941 100644 --- a/tests/Tgstation.Server.Api.Tests/Rights/TestRights.cs +++ b/tests/Tgstation.Server.Api.Tests/Rights/TestRights.cs @@ -40,7 +40,7 @@ public void TestAllPowerOfTwo() [TestMethod] public void TestAllRightsWorks() { - var allByondRights = ByondRights.CancelInstall | ByondRights.InstallOfficialOrChangeActiveVersion | ByondRights.ListInstalled | ByondRights.ReadActive | ByondRights.InstallCustomVersion | ByondRights.DeleteInstall; + var allByondRights = ByondRights.CancelInstall | ByondRights.InstallOfficialOrChangeActiveByondVersion | ByondRights.ListInstalled | ByondRights.ReadActive | ByondRights.InstallCustomByondVersion | ByondRights.DeleteInstall; var automaticByondRights = RightsHelper.AllRights(); Assert.AreEqual(allByondRights, automaticByondRights); diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs index eedac0cd0e5..0d318b15b42 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs @@ -58,7 +58,7 @@ public static async Task GetEdgeVersion(IFileDownloader fileDownloader, await using var provider = fileDownloader.DownloadFile(new Uri("https://www.byond.com/download/version.txt"), null); var stream = await provider.GetResult(cancellationToken); using var reader = new StreamReader(stream, Encoding.UTF8, false, -1, true); - var text = await reader.ReadToEndAsync(); + var text = await reader.ReadToEndAsync(cancellationToken); var splits = text.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); var targetVersion = splits.Last(); @@ -83,9 +83,21 @@ async Task RunPartOne(CancellationToken cancellationToken) { testVersion = await GetEdgeVersion(fileDownloader, cancellationToken); await TestNoVersion(cancellationToken); + await TestInstallNullVersion(cancellationToken); await TestInstallStable(cancellationToken); } + Task TestInstallNullVersion(CancellationToken cancellationToken) + => ApiAssert.ThrowsException( + () => byondClient.SetActiveVersion( + new ByondVersionRequest + { + Engine = EngineType.Byond, + }, + null, + cancellationToken), + ErrorCode.ModelValidationFailure); + async Task RunContinued(Task firstInstall, CancellationToken cancellationToken) { await firstInstall; diff --git a/tests/Tgstation.Server.Tests/TestDatabase.cs b/tests/Tgstation.Server.Tests/TestDatabase.cs index df8ffd5e4cf..8d629bcfb67 100644 --- a/tests/Tgstation.Server.Tests/TestDatabase.cs +++ b/tests/Tgstation.Server.Tests/TestDatabase.cs @@ -134,7 +134,7 @@ DatabaseContext CreateContext() { new Host.Models.InstancePermissionSet { - ByondRights = ByondRights.InstallCustomVersion, + ByondRights = ByondRights.InstallCustomByondVersion, ChatBotRights = ChatBotRights.None, ConfigurationRights = ConfigurationRights.Read, DreamDaemonRights = DreamDaemonRights.ReadRevision,