WIP OpenDream Support

This commit is contained in:
Jordan Dominion
2023-10-09 15:42:29 -04:00
parent 24527efe68
commit 6e015f86fa
51 changed files with 1150 additions and 532 deletions
+8 -3
View File
@@ -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
/**
@@ -0,0 +1,18 @@
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// The type of engine the codebase is using.
/// </summary>
public enum EngineType
{
/// <summary>
/// Build your own net dream,
/// </summary>
Byond,
/// <summary>
/// The OpenDream BYOND reimplementation.
/// </summary>
OpenDream,
}
}
@@ -0,0 +1,109 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace Tgstation.Server.Api.Models.Internal
{
/// <summary>
/// Information about a Byond installation.
/// </summary>
public class ByondVersion : IEquatable<ByondVersion>
{
/// <summary>
/// The <see cref="EngineType"/>.
/// </summary>
[RequestOptions(FieldPresence.Required)]
public EngineType? Engine { get; set; }
/// <summary>
/// The <see cref="System.Version"/> of the engine.
/// </summary>
[ResponseOptions]
public Version? Version { get; set; }
/// <summary>
/// The git committish of the <see cref="Version"/>. On response, this will always be a commit SHA.
/// </summary>
[ResponseOptions]
[StringLength(Limits.MaximumCommitShaLength)]
public string? SourceCommittish { get; set; }
/// <summary>
/// Parses a stringified <see cref="ByondVersion"/>.
/// </summary>
/// <param name="input">The input <see cref="string"/>.</param>
/// <param name="byondVersion">The output <see cref="ByondVersion"/>.</param>
/// <returns><see langword="true"/> if parsing was successful, <see langword="false"/> otherwise.</returns>
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 = null;
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;
if (!Version.TryParse(splits.Last(), out var version))
return false;
byondVersion = new ByondVersion
{
Engine = engine,
Version = version,
};
return true;
}
/// <summary>
/// Initializes a new instance of the <see cref="ByondVersion"/> class.
/// </summary>
public ByondVersion()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ByondVersion"/> class.
/// </summary>
/// <param name="other">The <see cref="ByondVersion"/> to copy.</param>
public ByondVersion(ByondVersion other)
{
if (other == null)
throw new ArgumentNullException(nameof(other));
Version = other.Version;
Engine = other.Engine;
SourceCommittish = other.SourceCommittish;
}
/// <inheritdoc />
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
}
/// <inheritdoc />
public override bool Equals(object obj)
=> obj is ByondVersion other && Equals(other);
/// <inheritdoc />
public override string ToString() => $"{(Engine != EngineType.Byond ? $"{Engine}-" : String.Empty)}{Version}"; // BYOND isn't display for backwards compatibility. SourceCommittish is not included
/// <inheritdoc />
public override int GetHashCode() => ToString().GetHashCode();
}
}
@@ -1,16 +1,13 @@
using System;
using Tgstation.Server.Api.Models.Internal;
namespace Tgstation.Server.Api.Models.Request
{
/// <summary>
/// A request to delete a specific <see cref="Version"/>.
/// </summary>
public class ByondVersionDeleteRequest
public class ByondVersionDeleteRequest : ByondVersion
{
/// <summary>
/// The BYOND version to install.
/// </summary>
[RequestOptions(FieldPresence.Required)]
public Version? Version { get; set; }
}
}
@@ -1,13 +1,22 @@
namespace Tgstation.Server.Api.Models.Request
using System;
using Tgstation.Server.Api.Models.Internal;
namespace Tgstation.Server.Api.Models.Request
{
/// <summary>
/// A request to install a BYOND <see cref="ByondVersionDeleteRequest.Version"/>.
/// A request to install a <see cref="ByondVersion"/>.
/// </summary>
public sealed class ByondVersionRequest : ByondVersionDeleteRequest
public sealed class ByondVersionRequest : ByondVersion
{
/// <summary>
/// If a custom BYOND version is to be uploaded.
/// </summary>
public bool? UploadCustomZip { get; set; }
/// <summary>
/// The remote repository for non-<see cref="EngineType.Byond"/> <see cref="EngineType"/>s. By default, this is the original git repository of the target <see cref="EngineType"/>.
/// </summary>
public Uri? SourceRepository { get; set; }
}
}
@@ -1,16 +1,19 @@
using System;
using Tgstation.Server.Api.Models.Internal;
namespace Tgstation.Server.Api.Models.Response
{
/// <summary>
/// Represents an installed BYOND <see cref="Version"/>.
/// Represents an installed <see cref="ByondVersion"/>.
/// </summary>
public sealed class ByondResponse
public sealed class ByondResponse : ByondVersion
{
/// <summary>
/// The installed BYOND <see cref="System.Version"/>. BYOND itself only considers the <see cref="Version.Major"/> and <see cref="Version.Minor"/> numbers. TGS uses the <see cref="Version.Build"/> number to represent installed custom versions.
/// Initializes a new instance of the <see cref="ByondResponse"/> class.
/// </summary>
[ResponseOptions]
public Version? Version { get; set; }
/// <param name="byondVersion">The <see cref="ByondVersion"/> to copy.</param>
public ByondResponse(ByondVersion byondVersion)
: base(byondVersion)
{
}
}
}
@@ -1,9 +1,11 @@
using System;
using Tgstation.Server.Api.Models.Internal;
namespace Tgstation.Server.Api.Models.Response
{
/// <inheritdoc />
public sealed class CompileJobResponse : Internal.CompileJob
public sealed class CompileJobResponse : CompileJob
{
/// <summary>
/// The <see cref="Job"/> relating to this job.
@@ -16,9 +18,14 @@ namespace Tgstation.Server.Api.Models.Response
public RevisionInformation? RevisionInformation { get; set; }
/// <summary>
/// The <see cref="ByondResponse.Version"/> the <see cref="CompileJobResponse"/> was made with.
/// The <see cref="ByondVersion.Version"/> the <see cref="CompileJobResponse"/> was made with.
/// </summary>
public Version? ByondVersion { get; set; }
public ByondVersion? ByondVersion { get; set; }
/// <summary>
/// The <see cref="ByondVersion.Engine"/> the <see cref="CompileJobResponse"/> was made with.
/// </summary>
public EngineType? Engine { get; set; }
/// <summary>
/// The origin <see cref="Uri"/> of the repository the compile job was built from.
+18 -8
View File
@@ -14,33 +14,43 @@ namespace Tgstation.Server.Api.Rights
None = 0,
/// <summary>
/// User may view the active installed BYOND version.
/// User may view the active installed engine versions.
/// </summary>
ReadActive = 1 << 0,
/// <summary>
/// User may list all installed BYOND versions.
/// User may list all installed engine versions.
/// </summary>
ListInstalled = 1 << 1,
/// <summary>
/// User may install official BYOND versions or change the active BYOND version.
/// User may install official <see cref="Models.EngineType.Byond"/> versions or change the active <see cref="Models.EngineType.Byond"/> version.
/// </summary>
InstallOfficialOrChangeActiveVersion = 1 << 2,
InstallOfficialOrChangeActiveByondVersion = 1 << 2,
/// <summary>
/// User may cancel BYOND installation job.
/// User may cancel an engine installation job.
/// </summary>
CancelInstall = 1 << 3,
/// <summary>
/// User may upload and activate custom BYOND builds.
/// User may upload and activate custom <see cref="Models.EngineType.Byond"/> builds.
/// </summary>
InstallCustomVersion = 1 << 4,
InstallCustomByondVersion = 1 << 4,
/// <summary>
/// User may delete non-active BYOND builds.
/// User may delete non-active engine builds.
/// </summary>
DeleteInstall = 1 << 5,
/// <summary>
/// User may install official <see cref="Models.EngineType.OpenDream"/> versions or change the active <see cref="Models.EngineType.OpenDream"/> version.
/// </summary>
InstallOfficialOrChangeActiveOpenDreamVersion = 1 << 6,
/// <summary>
/// User may activate custom <see cref="Models.EngineType.OpenDream"/> builds via zip upload or custom git committish.
/// </summary>
InstallCustomOpenDreamVersion = 1 << 7,
}
}
@@ -1,28 +0,0 @@
using System;
using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Byond
{
/// <inheritdoc cref="IByondExecutableLock" />
sealed class ByondExecutableLock : ReferenceCounter<ByondInstallation>, IByondExecutableLock
{
/// <inheritdoc />
public Version Version => Instance.Version;
/// <inheritdoc />
public string DreamDaemonPath => Instance.DreamDaemonPath;
/// <inheritdoc />
public string DreamMakerPath => Instance.DreamMakerPath;
/// <inheritdoc />
public bool SupportsCli => Instance.SupportsCli;
/// <inheritdoc />
public bool SupportsMapThreads => Instance.SupportsMapThreads;
/// <inheritdoc />
public void DoNotDeleteThisSession() => DangerousDropReference();
}
}
@@ -1,54 +1,148 @@
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
{
/// <inheritdoc />
sealed class ByondInstallation : IByondInstallation
/// <summary>
/// Implementation of <see cref="IEngineInstallation"/> for <see cref="EngineType.Byond"/>.
/// </summary>
sealed class ByondInstallation : IEngineInstallation
{
/// <inheritdoc />
public Version Version { get; }
public ByondVersion Version { get; }
/// <inheritdoc />
public string DreamDaemonPath { get; }
public string ServerExePath { get; }
/// <inheritdoc />
public string DreamMakerPath { get; }
public string CompilerExePath { get; }
/// <inheritdoc />
public bool SupportsCli { get; }
public bool PromptsForNetworkAccess { get; }
/// <inheritdoc />
public bool SupportsMapThreads { get; }
public bool HasStandardOutput { get; }
/// <inheritdoc />
public Task InstallationTask { get; }
/// <summary>
/// The <see cref="Task"/> that completes when the BYOND version finished installing.
/// If map threads are supported by the <see cref="Version"/>.
/// </summary>
public Task InstallationTask { get; }
readonly bool supportsMapThreads;
/// <summary>
/// Change a given <paramref name="securityLevel"/> into the appropriate DreamDaemon command line word.
/// </summary>
/// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level to change.</param>
/// <returns>A <see cref="string"/> representation of the command line parameter.</returns>
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)),
};
}
/// <summary>
/// Change a given <paramref name="visibility"/> into the appropriate DreamDaemon command line word.
/// </summary>
/// <param name="visibility">The <see cref="DreamDaemonVisibility"/> level to change.</param>
/// <returns>A <see cref="string"/> representation of the command line parameter.</returns>
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)),
};
}
/// <summary>
/// Initializes a new instance of the <see cref="ByondInstallation"/> class.
/// </summary>
/// <param name="installationTask">The value of <see cref="InstallationTask"/>.</param>
/// <param name="version">The value of <see cref="Version"/>.</param>
/// <param name="dreamDaemonPath">The value of <see cref="DreamDaemonPath"/>.</param>
/// <param name="dreamMakerPath">The value of <see cref="DreamMakerPath"/>.</param>
/// <param name="supportsCli">The value of <see cref="SupportsCli"/>.</param>
/// <param name="supportsMapThreads">The value of <see cref="SupportsMapThreads"/>.</param>
/// <param name="dreamDaemonPath">The value of <see cref="ServerExePath"/>.</param>
/// <param name="dreamMakerPath">The value of <see cref="CompilerExePath"/>.</param>
/// <param name="supportsCli">If a CLI application is being used.</param>
/// <param name="supportsMapThreads">The value of <see cref="supportsMapThreads"/>.</param>
public ByondInstallation(
Task installationTask,
Version version,
ByondVersion version,
string dreamDaemonPath,
string dreamMakerPath,
bool supportsCli,
bool supportsMapThreads)
{
InstallationTask = installationTask ?? throw new ArgumentNullException(nameof(installationTask));
ArgumentNullException.ThrowIfNull(version);
if (version.Engine.Value != EngineType.Byond)
throw new ArgumentException($"Invalid EngineType: {version.Engine.Value}", nameof(version));
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;
this.supportsMapThreads = supportsMapThreads;
}
/// <inheritdoc />
public string FormatServerArguments(
IDmbProvider dmbProvider,
IReadOnlyDictionary<string, string> parameters,
DreamDaemonLaunchParameters launchParameters,
string logFilePath)
{
ArgumentNullException.ThrowIfNull(dmbProvider);
ArgumentNullException.ThrowIfNull(parameters);
ArgumentNullException.ThrowIfNull(launchParameters);
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 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;
}
/// <inheritdoc />
public string FormatCompilerArguments(string dmePath)
=> $"-clean \"{dmePath ?? throw new ArgumentNullException(nameof(dmePath))}\"";
}
}
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
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 @@ namespace Tgstation.Server.Host.Components.Byond
public static Version MapThreadsVersion => new (515, 1609);
/// <inheritdoc />
public abstract string DreamMakerName { get; }
public abstract string CompilerName { get; }
/// <inheritdoc />
public abstract string PathToUserByondFolder { get; }
public abstract string PathToUserFolder { get; }
/// <summary>
/// Gets the URL formatter string for downloading a byond version of {0:Major} {1:Minor}.
@@ -63,7 +64,7 @@ namespace Tgstation.Server.Host.Components.Byond
}
/// <inheritdoc />
public abstract string GetDreamDaemonName(Version version, out bool supportsCli, out bool supportsMapThreads);
public abstract string GetDreamDaemonName(ByondVersion version, out bool supportsCli, out bool supportsMapThreads);
/// <inheritdoc />
public async Task CleanCache(CancellationToken cancellationToken)
@@ -73,7 +74,7 @@ namespace Tgstation.Server.Host.Components.Byond
Logger.LogDebug("Cleaning BYOND cache...");
await IOManager.DeleteDirectory(
IOManager.ConcatPath(
PathToUserByondFolder,
PathToUserFolder,
CacheDirectoryName),
cancellationToken);
}
@@ -84,18 +85,18 @@ namespace Tgstation.Server.Host.Components.Byond
}
/// <inheritdoc />
public abstract ValueTask InstallByond(Version version, string path, CancellationToken cancellationToken);
public abstract ValueTask InstallByond(ByondVersion version, string path, CancellationToken cancellationToken);
/// <inheritdoc />
public abstract ValueTask UpgradeInstallation(Version version, string path, CancellationToken cancellationToken);
public abstract ValueTask UpgradeInstallation(ByondVersion version, string path, CancellationToken cancellationToken);
/// <inheritdoc />
public async ValueTask<MemoryStream> DownloadVersion(Version version, CancellationToken cancellationToken)
public async ValueTask<MemoryStream> 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(
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
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 @@ namespace Tgstation.Server.Host.Components.Byond
const string ActiveVersionFileName = "ActiveVersion.txt";
/// <inheritdoc />
public Version ActiveVersion { get; private set; }
public ByondVersion ActiveVersion { get; private set; }
/// <inheritdoc />
public IReadOnlyList<Version> InstalledVersions
public IReadOnlyList<ByondVersion> InstalledVersions
{
get
{
@@ -87,7 +88,7 @@ namespace Tgstation.Server.Host.Components.Byond
/// <summary>
/// Map of byond <see cref="Version"/>s to <see cref="Task"/>s that complete when they are installed.
/// </summary>
readonly Dictionary<Version, ReferenceCountingContainer<ByondInstallation, ByondExecutableLock>> installedVersions;
readonly Dictionary<ByondVersion, ReferenceCountingContainer<IEngineInstallation, EngineExecutableLock>> installedVersions;
/// <summary>
/// The <see cref="SemaphoreSlim"/> for changing or deleting the active BYOND version.
@@ -103,11 +104,14 @@ namespace Tgstation.Server.Host.Components.Byond
/// Validates a given <paramref name="version"/> parameter.
/// </summary>
/// <param name="version">The <see cref="Version"/> to validate.</param>
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!");
}
@@ -125,7 +129,7 @@ namespace Tgstation.Server.Host.Components.Byond
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
installedVersions = new Dictionary<Version, ReferenceCountingContainer<ByondInstallation, ByondExecutableLock>>();
installedVersions = new Dictionary<ByondVersion, ReferenceCountingContainer<IEngineInstallation, EngineExecutableLock>>();
changeDeleteSemaphore = new SemaphoreSlim(1);
activeVersionChanged = new TaskCompletionSource();
}
@@ -136,7 +140,7 @@ namespace Tgstation.Server.Host.Components.Byond
/// <inheritdoc />
public async ValueTask ChangeVersion(
JobProgressReporter progressReporter,
Version version,
ByondVersion version,
Stream customVersionStream,
bool allowInstallation,
CancellationToken cancellationToken)
@@ -154,7 +158,10 @@ namespace Tgstation.Server.Host.Components.Byond
cancellationToken);
// We reparse the version because it could be changed after a custom install.
version = installLock.Version;
version = new ByondVersion(version)
{
Version = installLock.Version.Version,
};
var stringVersion = version.ToString();
await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(stringVersion), cancellationToken);
@@ -177,7 +184,7 @@ namespace Tgstation.Server.Host.Components.Byond
}
/// <inheritdoc />
public async ValueTask<IByondExecutableLock> UseExecutables(Version requiredVersion, string trustDmbFullPath, CancellationToken cancellationToken)
public async ValueTask<IEngineExecutableLock> UseExecutables(ByondVersion requiredVersion, string trustDmbFullPath, CancellationToken cancellationToken)
{
logger.LogTrace(
"Acquiring lock on BYOND version {version}...",
@@ -205,7 +212,7 @@ namespace Tgstation.Server.Host.Components.Byond
}
/// <inheritdoc />
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 +220,18 @@ namespace Tgstation.Server.Host.Components.Byond
logger.LogTrace("DeleteVersion {version}", version);
if (version == ActiveVersion)
if (version.Equals(ActiveVersion))
throw new JobException(ErrorCode.ByondCannotDeleteActiveVersion);
ReferenceCountingContainer<ByondInstallation, ByondExecutableLock> container;
ReferenceCountingContainer<IEngineInstallation, EngineExecutableLock> 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 +248,7 @@ namespace Tgstation.Server.Host.Components.Byond
.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 +262,7 @@ namespace Tgstation.Server.Host.Components.Byond
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)
@@ -291,6 +301,13 @@ namespace Tgstation.Server.Host.Components.Byond
}
}
/// <inheritdoc />
public async ValueTask EnsureEngineSource(Uri source, EngineType engine, CancellationToken cancellationToken)
{
await Task.Yield();
throw new NotImplementedException();
}
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
{
@@ -302,7 +319,7 @@ namespace Tgstation.Server.Host.Components.Byond
var activeVersionBytesTask = GetActiveVersion();
var byondDir = byondInstaller.PathToUserByondFolder;
var byondDir = byondInstaller.PathToUserFolder;
if (byondDir != null)
using (await SemaphoreSlimContext.Lock(UserFilesSemaphore, cancellationToken))
{
@@ -328,7 +345,7 @@ namespace Tgstation.Server.Host.Components.Byond
await ioManager.CreateDirectory(DefaultIOManager.CurrentDirectory, cancellationToken);
var directories = await ioManager.GetDirectories(DefaultIOManager.CurrentDirectory, cancellationToken);
var installedVersionPaths = new Dictionary<string, Version>();
var installedVersionPaths = new Dictionary<string, ByondVersion>();
async ValueTask ReadVersion(string path)
{
@@ -342,7 +359,7 @@ namespace Tgstation.Server.Host.Components.Byond
var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken);
var text = Encoding.UTF8.GetString(bytes);
if (!Version.TryParse(text, out var version))
if (!ByondVersion.TryParse(text, out var version))
{
logger.LogWarning("Cleaning path with unparsable version file: {versionPath}", ioManager.ResolvePath(path));
await ioManager.DeleteDirectory(path, cancellationToken); // cleanup
@@ -383,10 +400,10 @@ namespace Tgstation.Server.Host.Components.Byond
{
var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
Version activeVersion;
ByondVersion activeVersion;
bool hasRequestedActiveVersion;
lock (installedVersions)
hasRequestedActiveVersion = Version.TryParse(activeVersionString, out activeVersion)
hasRequestedActiveVersion = ByondVersion.TryParse(activeVersionString, out activeVersion)
&& installedVersions.ContainsKey(activeVersion);
if (hasRequestedActiveVersion)
@@ -403,46 +420,47 @@ namespace Tgstation.Server.Host.Components.Byond
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <summary>
/// Ensures a BYOND <paramref name="version"/> is installed if it isn't already.
/// Ensures a BYOND <paramref name="byondVersion"/> is installed if it isn't already.
/// </summary>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="version">The BYOND <see cref="Version"/> to install.</param>
/// <param name="byondVersion">The <see cref="ByondVersion"/> to install.</param>
/// <param name="customVersionStream">Custom zip file <see cref="Stream"/> to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
/// <param name="neededForLock">If this BYOND version is required as part of a locking operation.</param>
/// <param name="allowInstallation">If an installation should be performed if the <paramref name="version"/> is not installed. If <see langword="false"/> and an installation is required an <see cref="InvalidOperationException"/> will be thrown.</param>
/// <param name="allowInstallation">If an installation should be performed if the <paramref name="byondVersion"/> is not installed. If <see langword="false"/> and an installation is required an <see cref="InvalidOperationException"/> will be thrown.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="ByondExecutableLock"/>.</returns>
async ValueTask<ByondExecutableLock> AssertAndLockVersion(
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="EngineExecutableLock"/>.</returns>
async ValueTask<EngineExecutableLock> AssertAndLockVersion(
JobProgressReporter progressReporter,
Version version,
ByondVersion byondVersion,
Stream customVersionStream,
bool neededForLock,
bool allowInstallation,
CancellationToken cancellationToken)
{
var ourTcs = new TaskCompletionSource();
ByondInstallation installation;
ByondExecutableLock installLock;
IEngineInstallation installation;
EngineExecutableLock 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 +475,7 @@ namespace Tgstation.Server.Host.Components.Byond
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 +485,24 @@ namespace Tgstation.Server.Host.Components.Byond
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<string> { versionString }, false, cancellationToken);
await InstallVersionFiles(progressReporter, version, customVersionStream, cancellationToken);
await InstallVersionFiles(progressReporter, byondVersion, customVersionStream, cancellationToken);
ourTcs.SetResult();
}
@@ -494,7 +512,7 @@ namespace Tgstation.Server.Host.Components.Byond
await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List<string> { ex.Message }, false, cancellationToken);
lock (installedVersions)
installedVersions.Remove(version);
installedVersions.Remove(byondVersion);
ourTcs.SetException(ex);
throw;
@@ -513,11 +531,11 @@ namespace Tgstation.Server.Host.Components.Byond
/// Installs the files for a given BYOND <paramref name="version"/>.
/// </summary>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="version">The BYOND <see cref="Version"/> being installed with the <see cref="Version.Build"/> number set if appropriate.</param>
/// <param name="version">The <see cref="ByondVersion"/> being installed with the <see cref="Version.Build"/> number set if appropriate.</param>
/// <param name="customVersionStream">Custom zip file <see cref="Stream"/> to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
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()
@@ -585,32 +603,46 @@ namespace Tgstation.Server.Host.Components.Byond
}
/// <summary>
/// Create and add a new <see cref="ByondInstallation"/> to <see cref="installedVersions"/>.
/// Create and add a new <see cref="IEngineInstallation"/> to <see cref="installedVersions"/>.
/// </summary>
/// <param name="version">The <see cref="Version"/> being added.</param>
/// <param name="installationTask">The <see cref="ValueTask"/> representing the installation process.</param>
/// <returns>The new <see cref="ReferenceCountingContainer{TWrapped, TReference}"/> containing the new <see cref="ByondInstallation"/>.</returns>
ReferenceCountingContainer<ByondInstallation, ByondExecutableLock> AddInstallationContainer(Version version, Task installationTask)
/// <returns>The new <see cref="ReferenceCountingContainer{TWrapped, TReference}"/> containing the new <see cref="IEngineInstallation"/>.</returns>
ReferenceCountingContainer<IEngineInstallation, EngineExecutableLock> 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);
IEngineInstallation installation;
var installationContainer = new ReferenceCountingContainer<ByondInstallation, ByondExecutableLock>(installation);
switch (version.Engine.Value)
{
case EngineType.Byond:
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.CompilerName)),
supportsCli,
supportsMapThreads);
break;
case EngineType.OpenDream:
installation = new OpenDreamInstallation(
installationTask,
version);
break;
default:
throw new InvalidOperationException($"Invalid EngineType: {version.Engine.Value}");
}
var installationContainer = new ReferenceCountingContainer<IEngineInstallation, EngineExecutableLock>(installation);
lock (installedVersions)
installedVersions.Add(version, installationContainer);
@@ -626,7 +658,7 @@ namespace Tgstation.Server.Host.Components.Byond
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
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);
@@ -0,0 +1,49 @@
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
{
/// <inheritdoc cref="IEngineExecutableLock" />
sealed class EngineExecutableLock : ReferenceCounter<IEngineInstallation>, IEngineExecutableLock
{
/// <inheritdoc />
public ByondVersion Version => Instance.Version;
/// <inheritdoc />
public string ServerExePath => Instance.ServerExePath;
/// <inheritdoc />
public string CompilerExePath => Instance.CompilerExePath;
/// <inheritdoc />
public bool HasStandardOutput => Instance.HasStandardOutput;
/// <inheritdoc />
public bool PromptsForNetworkAccess => Instance.PromptsForNetworkAccess;
/// <inheritdoc />
public Task InstallationTask => Instance.InstallationTask;
/// <inheritdoc />
public void DoNotDeleteThisSession() => DangerousDropReference();
/// <inheritdoc />
public string FormatServerArguments(
IDmbProvider dmbProvider,
IReadOnlyDictionary<string, string> parameters,
DreamDaemonLaunchParameters launchParameters,
string logFilePath)
=> Instance.FormatServerArguments(
dmbProvider,
parameters,
launchParameters,
logFilePath);
/// <inheritdoc />
public string FormatCompilerArguments(string dmePath) => Instance.FormatCompilerArguments(dmePath);
}
}
@@ -1,35 +0,0 @@
using System;
namespace Tgstation.Server.Host.Components.Byond
{
/// <summary>
/// Represents a BYOND installation.
/// </summary>
public interface IByondInstallation
{
/// <summary>
/// The <see cref="global::System.Version"/> of the <see cref="IByondInstallation"/>.
/// </summary>
Version Version { get; }
/// <summary>
/// The full path to the DreamDaemon executable.
/// </summary>
string DreamDaemonPath { get; }
/// <summary>
/// The full path to the dm/DreamMaker executable.
/// </summary>
string DreamMakerPath { get; }
/// <summary>
/// If <see cref="DreamDaemonPath"/> supports being run as a command-line application.
/// </summary>
bool SupportsCli { get; }
/// <summary>
/// If <see cref="DreamDaemonPath"/> supports the -map-threads parameter.
/// </summary>
bool SupportsMapThreads { get; }
}
}
@@ -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
{
/// <summary>
@@ -11,52 +12,52 @@ namespace Tgstation.Server.Host.Components.Byond
interface IByondInstaller
{
/// <summary>
/// Get the file name of the DreamMaker executable.
/// Get the file name of the compiler executable.
/// </summary>
string DreamMakerName { get; }
string CompilerName { get; }
/// <summary>
/// The path to the BYOND folder for the user.
/// The path to the folder for the user's data.
/// </summary>
string PathToUserByondFolder { get; }
string PathToUserFolder { get; }
/// <summary>
/// Get the file name of the DreamDaemon executable.
/// </summary>
/// <param name="version">The <see cref="Version"/> of BYOND to select the executable name for.</param>
/// <param name="version">The <see cref="ByondVersion"/> of BYOND to select the executable name for.</param>
/// <param name="supportsCli">Whether or not the returned path supports being run as a command-line application.</param>
/// <param name="supportsMapThreads">Whether or not the returned path supports the '-map-threads' parameter.</param>
/// <returns>The file name of the DreamDaemon executable.</returns>
string GetDreamDaemonName(Version version, out bool supportsCli, out bool supportsMapThreads);
string GetDreamDaemonName(ByondVersion version, out bool supportsCli, out bool supportsMapThreads);
/// <summary>
/// Download a given BYOND <paramref name="version"/>.
/// </summary>
/// <param name="version">The <see cref="Version"/> of BYOND to download.</param>
/// <param name="version">The <see cref="ByondVersion"/> of BYOND to download.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="MemoryStream"/> of the zipfile.</returns>
ValueTask<MemoryStream> DownloadVersion(Version version, CancellationToken cancellationToken);
ValueTask<MemoryStream> DownloadVersion(ByondVersion version, CancellationToken cancellationToken);
/// <summary>
/// Does actions necessary to get an extracted BYOND installation working.
/// </summary>
/// <param name="version">The <see cref="Version"/> of BYOND being installed.</param>
/// <param name="version">The <see cref="ByondVersion"/> being installed.</param>
/// <param name="path">The path to the BYOND installation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask InstallByond(Version version, string path, CancellationToken cancellationToken);
ValueTask InstallByond(ByondVersion version, string path, CancellationToken cancellationToken);
/// <summary>
/// Does actions necessary to get upgrade a BYOND version installed by a previous version of TGS.
/// </summary>
/// <param name="version">The <see cref="Version"/> of BYOND being installed.</param>
/// <param name="version">The <see cref="ByondVersion"/> being installed.</param>
/// <param name="path">The path to the BYOND installation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask UpgradeInstallation(Version version, string path, CancellationToken cancellationToken);
ValueTask UpgradeInstallation(ByondVersion version, string path, CancellationToken cancellationToken);
/// <summary>
/// Attempts to cleans the BYOND cache folder for the system.
/// Attempts to cleans the engine's cache folder for the system.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
@@ -4,6 +4,8 @@ using System.IO;
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
/// <summary>
/// For managing the BYOND installation.
/// </summary>
/// <remarks>When passing in <see cref="Version"/>s, ensure they are BYOND format versions unless referring to a custom version. This means <see cref="Version.Build"/> should NEVER be 0.</remarks>
/// <remarks>When passing in <see cref="ByondVersion.Version"/>s for <see cref="EngineType.Byond"/>, ensure they are BYOND format versions unless referring to a custom version. This means <see cref="Version.Build"/> should NEVER be 0.</remarks>
public interface IByondManager : IComponentService, IDisposable
{
/// <summary>
/// The currently active BYOND version.
/// The currently active <see cref="ByondVersion"/>.
/// </summary>
Version ActiveVersion { get; }
ByondVersion ActiveVersion { get; }
/// <summary>
/// The installed BYOND versions.
/// The installed <see cref="ByondVersion"/>s.
/// </summary>
IReadOnlyList<Version> InstalledVersions { get; }
IReadOnlyList<ByondVersion> InstalledVersions { get; }
/// <summary>
/// Change the active BYOND version.
/// Ensure that the given <paramref name="source"/> is registered for the given <paramref name="engine"/>.
/// </summary>
/// <param name="source">The <see cref="Uri"/> source of the <paramref name="engine"/>.</param>
/// <param name="engine">The <see cref="EngineType"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask EnsureEngineSource(Uri source, EngineType engine, CancellationToken cancellationToken);
/// <summary>
/// Change the active <see cref="ByondVersion"/>.
/// </summary>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="version">The new <see cref="Version"/>.</param>
/// <param name="version">The new <see cref="ByondVersion"/>.</param>
/// <param name="customVersionStream">Optional <see cref="Stream"/> of a custom BYOND version zip file.</param>
/// <param name="allowInstallation">If an installation should be performed if the <paramref name="version"/> is not installed. If <see langword="false"/> and an installation is required an <see cref="InvalidOperationException"/> will be thrown.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask ChangeVersion(JobProgressReporter progressReporter, Version version, Stream customVersionStream, bool allowInstallation, CancellationToken cancellationToken);
ValueTask ChangeVersion(
JobProgressReporter progressReporter,
ByondVersion version,
Stream customVersionStream,
bool allowInstallation,
CancellationToken cancellationToken);
/// <summary>
/// Deletes a given BYOND version from the disk.
/// Deletes a given <paramref name="version"/> from the disk.
/// </summary>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="version">The <see cref="Version"/> to delete.</param>
/// <param name="version">The <see cref="ByondVersion"/> to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask DeleteVersion(JobProgressReporter progressReporter, Version version, CancellationToken cancellationToken);
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
ValueTask DeleteVersion(JobProgressReporter progressReporter, ByondVersion version, CancellationToken cancellationToken);
/// <summary>
/// Lock the current installation's location and return a <see cref="IByondExecutableLock"/>.
/// Lock the current installation's location and return a <see cref="IEngineExecutableLock"/>.
/// </summary>
/// <param name="requiredVersion">The BYOND <see cref="Version"/> required.</param>
/// <param name="requiredVersion">The <see cref="ByondVersion"/> required.</param>
/// <param name="trustDmbFullPath">The optional full path to .dmb to trust while using the executables.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the requested <see cref="IByondExecutableLock"/>.</returns>
ValueTask<IByondExecutableLock> UseExecutables(
Version requiredVersion,
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the requested <see cref="IEngineExecutableLock"/>.</returns>
ValueTask<IEngineExecutableLock> UseExecutables(
ByondVersion requiredVersion,
string trustDmbFullPath,
CancellationToken cancellationToken);
}
@@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Components.Byond
/// <summary>
/// Represents usage of the two primary BYOND server executables.
/// </summary>
public interface IByondExecutableLock : IByondInstallation, IDisposable
public interface IEngineExecutableLock : IEngineInstallation, IDisposable
{
/// <summary>
/// Call if, during a detach, this version should not be deleted.
@@ -0,0 +1,65 @@
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
{
/// <summary>
/// Represents a BYOND installation.
/// </summary>
public interface IEngineInstallation
{
/// <summary>
/// The <see cref="ByondVersion"/> of the <see cref="IEngineInstallation"/>.
/// </summary>
ByondVersion Version { get; }
/// <summary>
/// The full path to the game server executable.
/// </summary>
string ServerExePath { get; }
/// <summary>
/// The full path to the dm/DreamMaker executable.
/// </summary>
string CompilerExePath { get; }
/// <summary>
/// If <see cref="ServerExePath"/> supports being run as a command-line application and outputs log information to be captured.
/// </summary>
bool HasStandardOutput { get; }
/// <summary>
/// If <see cref="ServerExePath"/> may create network prompts.
/// </summary>
bool PromptsForNetworkAccess { get; }
/// <summary>
/// The <see cref="Task"/> that completes when the BYOND version finished installing.
/// </summary>
Task InstallationTask { get; }
/// <summary>
/// Return the command line arguments for launching with given <paramref name="launchParameters"/>.
/// </summary>
/// <param name="dmbProvider">The <see cref="IDmbProvider"/>.</param>
/// <param name="parameters">The map of parameter <see cref="string"/>s as a <see cref="IReadOnlyDictionary{TKey, TValue}"/>. Should NOT include the <see cref="DreamDaemonLaunchParameters.AdditionalParameters"/> of <paramref name="launchParameters"/>.</param>
/// <param name="launchParameters">The <see cref="DreamDaemonLaunchParameters"/>.</param>
/// <param name="logFilePath">The path to the log file, if any.</param>
/// <returns>The formatted arguments <see cref="string"/>.</returns>
string FormatServerArguments(
IDmbProvider dmbProvider,
IReadOnlyDictionary<string, string> parameters,
DreamDaemonLaunchParameters launchParameters,
string logFilePath);
/// <summary>
/// Return the command line arguments for compiling a given <paramref name="dmePath"/> if compilation is necessary.
/// </summary>
/// <param name="dmePath">The full path to the .dme to compile.</param>
/// <returns>An arguments <see cref="string"/> if compilation is required, <see langword="null"/> otherwise.</returns>
string FormatCompilerArguments(string dmePath);
}
}
@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Deployment;
namespace Tgstation.Server.Host.Components.Byond
{
/// <summary>
/// Implementation of <see cref="IEngineInstallation"/> for <see cref="EngineType.OpenDream"/>.
/// </summary>
sealed class OpenDreamInstallation : IEngineInstallation
{
/// <inheritdoc />
public ByondVersion Version { get; }
/// <inheritdoc />
public string ServerExePath { get; }
/// <inheritdoc />
public string CompilerExePath { get; }
/// <inheritdoc />
public bool PromptsForNetworkAccess => false;
/// <inheritdoc />
public bool HasStandardOutput => true;
/// <inheritdoc />
public Task InstallationTask { get; }
/// <summary>
/// Initializes a new instance of the <see cref="OpenDreamInstallation"/> class.
/// </summary>
/// <param name="installationTask">The value of <see cref="InstallationTask"/>.</param>
/// <param name="version">The value of <see cref="Version"/>.</param>
public OpenDreamInstallation(
Task installationTask,
ByondVersion version)
{
InstallationTask = installationTask ?? throw new ArgumentNullException(nameof(installationTask));
ArgumentNullException.ThrowIfNull(version);
if (version.Engine.Value != EngineType.OpenDream)
throw new ArgumentException($"Invalid EngineType: {version.Engine.Value}", nameof(version));
Version = version ?? throw new ArgumentNullException(nameof(version));
throw new NotImplementedException();
}
/// <inheritdoc />
public string FormatServerArguments(IDmbProvider dmbProvider, IReadOnlyDictionary<string, string> parameters, DreamDaemonLaunchParameters launchParameters, string logFilePath)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public string FormatCompilerArguments(string dmePath)
{
ArgumentNullException.ThrowIfNull(dmePath);
return null;
}
}
}
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Host.IO;
@@ -32,10 +33,10 @@ namespace Tgstation.Server.Host.Components.Byond
const string ShellScriptExtension = ".sh";
/// <inheritdoc />
public override string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension;
public override string CompilerName => DreamMakerExecutableName + ShellScriptExtension;
/// <inheritdoc />
public override string PathToUserByondFolder { get; }
public override string PathToUserFolder { get; }
/// <inheritdoc />
protected override string ByondRevisionsUrlTemplate => "https://www.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip";
@@ -61,7 +62,7 @@ namespace Tgstation.Server.Host.Components.Byond
{
this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
PathToUserByondFolder = IOManager.ResolvePath(
PathToUserFolder = IOManager.ResolvePath(
IOManager.ConcatPath(
Environment.GetFolderPath(
Environment.SpecialFolder.UserProfile),
@@ -69,17 +70,17 @@ namespace Tgstation.Server.Host.Components.Byond
}
/// <inheritdoc />
public override string GetDreamDaemonName(Version version, out bool supportsCli, out bool supportsMapThreads)
public override string GetDreamDaemonName(ByondVersion version, out bool supportsCli, out bool supportsMapThreads)
{
ArgumentNullException.ThrowIfNull(version);
supportsCli = true;
supportsMapThreads = version >= MapThreadsVersion;
supportsMapThreads = version.Version >= MapThreadsVersion;
return DreamDaemonExecutableName + ShellScriptExtension;
}
/// <inheritdoc />
public override ValueTask InstallByond(Version version, string path, CancellationToken cancellationToken)
public override ValueTask InstallByond(ByondVersion version, string path, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(version);
ArgumentNullException.ThrowIfNull(path);
@@ -105,7 +106,7 @@ namespace Tgstation.Server.Host.Components.Byond
dreamDaemonScript);
var dmTask = WriteAndMakeExecutable(
IOManager.ConcatPath(basePath, DreamMakerName),
IOManager.ConcatPath(basePath, CompilerName),
dreamMakerScript);
var task = ValueTaskExtensions.WhenAll(
@@ -119,7 +120,7 @@ namespace Tgstation.Server.Host.Components.Byond
}
/// <inheritdoc />
public override ValueTask UpgradeInstallation(Version version, string path, CancellationToken cancellationToken)
public override ValueTask UpgradeInstallation(ByondVersion version, string path, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(version);
ArgumentNullException.ThrowIfNull(path);
@@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.IO;
@@ -53,10 +54,10 @@ namespace Tgstation.Server.Host.Components.Byond
public static Version DDExeVersion => new (515, 1598);
/// <inheritdoc />
public override string DreamMakerName => "dm.exe";
public override string CompilerName => "dm.exe";
/// <inheritdoc />
public override string PathToUserByondFolder { get; }
public override string PathToUserFolder { get; }
/// <inheritdoc />
protected override string ByondRevisionsUrlTemplate => "https://www.byond.com/download/build/{0}/{0}.{1}_byond.zip";
@@ -102,9 +103,9 @@ namespace Tgstation.Server.Host.Components.Byond
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,17 +115,17 @@ namespace Tgstation.Server.Host.Components.Byond
public void Dispose() => semaphore.Dispose();
/// <inheritdoc />
public override string GetDreamDaemonName(Version version, out bool supportsCli, out bool supportsMapThreads)
public override string GetDreamDaemonName(ByondVersion version, out bool supportsCli, out bool supportsMapThreads)
{
ArgumentNullException.ThrowIfNull(version);
supportsCli = version >= DDExeVersion;
supportsMapThreads = version >= MapThreadsVersion;
supportsCli = version.Version >= DDExeVersion;
supportsMapThreads = version.Version >= MapThreadsVersion;
return supportsCli ? "dd.exe" : "dreamdaemon.exe";
}
/// <inheritdoc />
public override ValueTask InstallByond(Version version, string path, CancellationToken cancellationToken)
public override ValueTask InstallByond(ByondVersion version, string path, CancellationToken cancellationToken)
{
var tasks = new List<ValueTask>(3)
{
@@ -139,7 +140,7 @@ namespace Tgstation.Server.Host.Components.Byond
}
/// <inheritdoc />
public override async ValueTask UpgradeInstallation(Version version, string path, CancellationToken cancellationToken)
public override async ValueTask UpgradeInstallation(ByondVersion version, string path, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(version);
ArgumentNullException.ThrowIfNull(path);
@@ -147,7 +148,7 @@ namespace Tgstation.Server.Host.Components.Byond
if (generalConfiguration.SkipAddingByondFirewallException)
return;
if (version < DDExeVersion)
if (version.Version < DDExeVersion)
return;
if (await IOManager.FileExists(IOManager.ConcatPath(path, TgsFirewalledDDFile), cancellationToken))
@@ -227,7 +228,7 @@ namespace Tgstation.Server.Host.Components.Byond
/// <param name="path">The path to the BYOND installation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask AddDreamDaemonToFirewall(Version version, string path, CancellationToken cancellationToken)
async ValueTask AddDreamDaemonToFirewall(ByondVersion version, string path, CancellationToken cancellationToken)
{
var dreamDaemonName = GetDreamDaemonName(version, out var usesDDExe, out var _);
@@ -359,7 +359,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// <inheritdoc />
public Func<string, string, Action<bool>> QueueDeploymentMessage(
Models.RevisionInformation revisionInformation,
Version byondVersion,
ByondVersion byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
@@ -1,5 +1,4 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -49,20 +48,45 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
/// <inheritdoc />
public ValueTask<MessageContent> Invoke(string arguments, ChatUser user, CancellationToken cancellationToken)
{
if (arguments.Split(' ').Any(x => x.ToUpperInvariant() == "--ACTIVE"))
return ValueTask.FromResult(new MessageContent
{
Text = byondManager.ActiveVersion == null ? "None!" : String.Format(CultureInfo.InvariantCulture, "{0}.{1}", byondManager.ActiveVersion.Major, byondManager.ActiveVersion.Minor),
});
if (watchdog.Status == WatchdogStatus.Offline)
return ValueTask.FromResult(new MessageContent
{
Text = "Server offline!",
});
return ValueTask.FromResult(new MessageContent
if (arguments.Split(' ').Any(x => x.Equals("--active", StringComparison.OrdinalIgnoreCase)))
{
Text = watchdog.ActiveCompileJob?.ByondVersion ?? "None!",
});
string text;
if (byondManager.ActiveVersion == null)
text = "None!";
else
switch (byondManager.ActiveVersion.Engine.Value)
{
case EngineType.OpenDream:
text = $"OpenDream: {byondManager.ActiveVersion.SourceCommittish}";
break;
case EngineType.Byond:
text = $"BYOND {byondManager.ActiveVersion.Version.Major}.{byondManager.ActiveVersion.Version.Minor}";
if (byondManager.ActiveVersion.Version.Build != -1)
text += " (Custom Build)";
break;
default:
throw new InvalidOperationException($"Invalid EngineType: {byondManager.ActiveVersion.Engine.Value}");
}
return ValueTask.FromResult(
new MessageContent
{
Text = text,
});
}
if (watchdog.Status == WatchdogStatus.Offline)
return ValueTask.FromResult(
new MessageContent
{
Text = "Server offline!",
});
return ValueTask.FromResult(
new MessageContent
{
Text = watchdog.ActiveCompileJob?.ByondVersion ?? "None!",
});
}
}
}
@@ -61,7 +61,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// Send the message for a deployment to configured deployment channels.
/// </summary>
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> of the deployment.</param>
/// <param name="byondVersion">The BYOND <see cref="Version"/> of the deployment.</param>
/// <param name="byondVersion">The <see cref="ByondVersion"/> of the deployment.</param>
/// <param name="estimatedCompletionTime">The optional <see cref="DateTimeOffset"/> the deployment is expected to be completed at.</param>
/// <param name="gitHubOwner">The repository GitHub owner, if any.</param>
/// <param name="gitHubRepo">The repository GitHub name, if any.</param>
@@ -69,7 +69,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// <returns>A <see cref="Func{T1, T2, TResult}"/> to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. Returns an <see cref="Action"/> to call to mark the deployment as active/inactive. Parameter: If the deployment is being activated or inactivated.</returns>
Func<string, string, Action<bool>> QueueDeploymentMessage(
Models.RevisionInformation revisionInformation,
Version byondVersion,
ByondVersion byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
@@ -22,6 +22,7 @@ using Remora.Rest.Results;
using Remora.Results;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Extensions;
@@ -129,25 +130,35 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// Create a <see cref="List{T}"/> of <see cref="IEmbedField"/>s for a discord update embed.
/// </summary>
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> of the deployment.</param>
/// <param name="byondVersion">The BYOND <see cref="Version"/> of the deployment.</param>
/// <param name="byondVersion">The <see cref="ByondVersion"/> of the deployment.</param>
/// <param name="gitHubOwner">The repository GitHub owner, if any.</param>
/// <param name="gitHubRepo">The repository GitHub name, if any.</param>
/// <param name="localCommitPushed"><see langword="true"/> if the local deployment commit was pushed to the remote repository.</param>
/// <returns>A new <see cref="List{T}"/> of <see cref="IEmbedField"/>s to use.</returns>
static List<IEmbedField> BuildUpdateEmbedFields(
Models.RevisionInformation revisionInformation,
Version byondVersion,
ByondVersion byondVersion,
string gitHubOwner,
string gitHubRepo,
bool localCommitPushed)
{
bool gitHub = gitHubOwner != null && gitHubRepo != null;
var engineField = byondVersion.Engine.Value switch
{
EngineType.Byond => new EmbedField(
"BYOND Version",
$"{byondVersion.Version.Major}.{byondVersion.Version.Minor}{(byondVersion.Version.Build > 0 ? $".{byondVersion.Version.Build}" : String.Empty)}",
true),
EngineType.OpenDream => new EmbedField(
"OpenDream Version",
$"[{byondVersion.SourceCommittish[..7]}](https://github.com/OpenDreamProject/OpenDream/commit/{revisionInformation.CommitSha})",
true),
_ => throw new InvalidOperationException($"Invaild EngineType: {byondVersion.Engine.Value}"),
};
var fields = new List<IEmbedField>
{
new EmbedField(
"BYOND Version",
$"{byondVersion.Major}.{byondVersion.Minor}{(byondVersion.Build > 0 ? $".{byondVersion.Build}" : String.Empty)}",
true),
engineField,
new EmbedField(
"Local Commit",
localCommitPushed && gitHub
@@ -323,7 +334,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
public override async ValueTask<Func<string, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
Models.RevisionInformation revisionInformation,
Version byondVersion,
ByondVersion byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Models;
@@ -83,7 +84,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// Send the message for a deployment.
/// </summary>
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> of the deployment.</param>
/// <param name="byondVersion">The BYOND <see cref="Version"/> of the deployment.</param>
/// <param name="byondVersion">The <see cref="ByondVersion"/> of the deployment.</param>
/// <param name="estimatedCompletionTime">The optional <see cref="DateTimeOffset"/> the deployment is expected to be completed at.</param>
/// <param name="gitHubOwner">The repository GitHub owner, if any.</param>
/// <param name="gitHubRepo">The repository GitHub name, if any.</param>
@@ -92,8 +93,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="Func{T1, T2, TResult}"/> to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. Returns another callback which should be called to mark the deployment as active.</returns>
ValueTask<Func<string, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
RevisionInformation revisionInformation,
Version byondVersion,
Models.RevisionInformation revisionInformation,
ByondVersion byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
@@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.IO;
@@ -220,7 +221,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
public override async ValueTask<Func<string, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
Models.RevisionInformation revisionInformation,
Version byondVersion,
ByondVersion byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
@@ -271,9 +272,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
commitInsert,
testmergeInsert,
remoteCommitInsert,
byondVersion.Build > 0
? byondVersion.ToString()
: $"{byondVersion.Major}.{byondVersion.Minor}",
byondVersion.ToString(),
estimatedCompletionTime.HasValue
? $" ETA: {estimatedCompletionTime - DateTimeOffset.UtcNow}"
: String.Empty),
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Jobs;
@@ -182,8 +183,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
public abstract ValueTask<Func<string, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
RevisionInformation revisionInformation,
Version byondVersion,
Models.RevisionInformation revisionInformation,
ByondVersion byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
@@ -273,7 +274,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
connectNow = false;
if (!Connected)
{
var job = new Job
var job = new Models.Job
{
Description = $"Reconnect chat bot: {ChatBot.Name}",
CancelRight = (ulong)ChatBotRights.WriteEnabled,
@@ -245,6 +245,12 @@ namespace Tgstation.Server.Host.Components.Deployment
.ThenInclude(x => x.MergedBy)
.FirstAsync(cancellationToken)); // can't wait to see that query
if (!Api.Models.Internal.ByondVersion.TryParse(compileJob.ByondVersion, out var byondVersion))
{
logger.LogWarning("Error loading compile job, bad BYOND version: {0}", compileJob.ByondVersion);
return null; // omae wa mou shinderu
}
if (!compileJob.Job.StoppedAt.HasValue)
{
// This happens when we're told to load the compile job that is currently finished up
@@ -262,7 +268,7 @@ namespace Tgstation.Server.Host.Components.Deployment
CleanRegisteredCompileJob(compileJob);
}
var newProvider = new DmbProvider(compileJob, ioManager, CleanupAction);
var newProvider = new DmbProvider(compileJob, byondVersion, ioManager, CleanupAction);
try
{
const string LegacyADirectoryName = "A";
@@ -299,7 +305,7 @@ namespace Tgstation.Server.Host.Components.Deployment
// rebuild the provider because it's using the legacy style directories
// Don't dispose it
logger.LogDebug("Creating legacy two folder .dmb provider targeting {aDirName} directory...", LegacyADirectoryName);
newProvider = new DmbProvider(compileJob, ioManager, CleanupAction, Path.DirectorySeparatorChar + LegacyADirectoryName);
newProvider = new DmbProvider(compileJob, byondVersion, ioManager, CleanupAction, Path.DirectorySeparatorChar + LegacyADirectoryName);
}
lock (jobLockCounts)
@@ -1,7 +1,7 @@
using System;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components.Deployment
{
@@ -14,10 +14,11 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <inheritdoc />
public string Directory => ioManager.ResolvePath(CompileJob.DirectoryName.ToString() + directoryAppend);
/// <summary>
/// The <see cref="CompileJob"/> for the <see cref="DmbProvider"/>.
/// </summary>
public CompileJob CompileJob { get; }
/// <inheritdoc />
public Models.CompileJob CompileJob { get; }
/// <inheritdoc />
public ByondVersion ByondVersion { get; }
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="DmbProvider"/>.
@@ -25,7 +26,7 @@ namespace Tgstation.Server.Host.Components.Deployment
readonly IIOManager ioManager;
/// <summary>
/// Extra path to add to the end of <see cref="Api.Models.Internal.CompileJob.DirectoryName"/>.
/// Extra path to add to the end of <see cref="CompileJob.DirectoryName"/>.
/// </summary>
readonly string directoryAppend;
@@ -38,12 +39,14 @@ namespace Tgstation.Server.Host.Components.Deployment
/// Initializes a new instance of the <see cref="DmbProvider"/> class.
/// </summary>
/// <param name="compileJob">The value of <see cref="CompileJob"/>.</param>
/// <param name="byondVersion">The value of <see cref="ByondVersion"/>.</param>
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
/// <param name="onDispose">The value of <see cref="onDispose"/>.</param>
/// <param name="directoryAppend">The optional value of <see cref="directoryAppend"/>.</param>
public DmbProvider(CompileJob compileJob, IIOManager ioManager, Action onDispose, string directoryAppend = null)
public DmbProvider(Models.CompileJob compileJob, ByondVersion byondVersion, IIOManager ioManager, Action onDispose, string directoryAppend = null)
{
CompileJob = compileJob ?? throw new ArgumentNullException(nameof(compileJob));
ByondVersion = byondVersion ?? throw new ArgumentNullException(nameof(byondVersion));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
this.directoryAppend = directoryAppend ?? String.Empty;
@@ -553,7 +553,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <param name="job">The <see cref="CompileJob"/> to run and populate.</param>
/// <param name="dreamMakerSettings">The <see cref="Api.Models.Internal.DreamMakerSettings"/> to use.</param>
/// <param name="launchParameters">The <see cref="DreamDaemonLaunchParameters"/> to use.</param>
/// <param name="byondLock">The <see cref="IByondExecutableLock"/> to use.</param>
/// <param name="byondLock">The <see cref="IEngineExecutableLock"/> to use.</param>
/// <param name="repository">The <see cref="IRepository"/> to use.</param>
/// <param name="remoteDeploymentManager">The <see cref="IRemoteDeploymentManager"/> to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
@@ -563,7 +563,7 @@ namespace Tgstation.Server.Host.Components.Deployment
Models.CompileJob job,
Api.Models.Internal.DreamMakerSettings dreamMakerSettings,
DreamDaemonLaunchParameters launchParameters,
IByondExecutableLock byondLock,
IEngineExecutableLock byondLock,
IRepository repository,
IRemoteDeploymentManager remoteDeploymentManager,
CancellationToken cancellationToken)
@@ -591,7 +591,7 @@ namespace Tgstation.Server.Host.Components.Deployment
{
resolvedOutputDirectory,
repoOrigin.ToString(),
$"{byondLock.Version.Major}.{byondLock.Version.Minor}",
byondLock.Version.ToString(),
},
true,
cancellationToken);
@@ -630,14 +630,14 @@ namespace Tgstation.Server.Host.Components.Deployment
{
resolvedOutputDirectory,
repoOrigin.ToString(),
$"{byondLock.Version.Major}.{byondLock.Version.Minor}",
byondLock.Version.ToString(),
},
true,
cancellationToken);
// run compiler
progressReporter.StageName = "Running DreamMaker";
var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken);
var compileSuceeded = await RunDreamMaker(byondLock, job, cancellationToken);
// Session takes ownership of the lock and Disposes it so save this for later
var byondVersion = byondLock.Version;
@@ -645,10 +645,10 @@ namespace Tgstation.Server.Host.Components.Deployment
// verify api
try
{
if (exitCode != 0)
if (!compileSuceeded)
throw new JobException(
ErrorCode.DreamMakerExitCode,
new JobException($"Exit code: {exitCode}{Environment.NewLine}{Environment.NewLine}{job.Output}"));
new JobException($"Compilation failed:{Environment.NewLine}{Environment.NewLine}{job.Output}"));
progressReporter.StageName = "Validating DMAPI";
await VerifyApi(
@@ -670,7 +670,7 @@ namespace Tgstation.Server.Host.Components.Deployment
new List<string>
{
resolvedOutputDirectory,
exitCode == 0 ? "1" : "0",
compileSuceeded ? "1" : "0",
byondVersion.ToString(),
},
true,
@@ -769,7 +769,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <param name="timeout">The timeout in seconds for validation.</param>
/// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level to use to validate the API.</param>
/// <param name="job">The <see cref="CompileJob"/> for the operation.</param>
/// <param name="byondLock">The current <see cref="IByondExecutableLock"/>.</param>
/// <param name="byondLock">The current <see cref="IEngineExecutableLock"/>.</param>
/// <param name="portToUse">The port to use for API validation.</param>
/// <param name="requireValidate">If the API validation is required to complete the deployment.</param>
/// <param name="logOutput">If output should be logged to the DreamDaemon Diagnostics folder.</param>
@@ -779,7 +779,7 @@ namespace Tgstation.Server.Host.Components.Deployment
uint timeout,
DreamDaemonSecurity securityLevel,
Models.CompileJob job,
IByondExecutableLock byondLock,
IEngineExecutableLock byondLock,
ushort portToUse,
bool requireValidate,
bool logOutput,
@@ -803,7 +803,11 @@ namespace Tgstation.Server.Host.Components.Deployment
job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider
ApiValidationStatus validationStatus;
using (var provider = new TemporaryDmbProvider(ioManager.ResolvePath(job.DirectoryName.ToString()), String.Concat(job.DmeName, DmbExtension), job))
using (var provider = new TemporaryDmbProvider(
ioManager.ResolvePath(job.DirectoryName.ToString()),
String.Concat(job.DmeName, DmbExtension),
job,
byondLock.Version))
await using (var controller = await sessionControllerFactory.LaunchNew(provider, byondLock, launchParameters, true, cancellationToken))
{
var launchResult = await controller.LaunchResult.WaitAsync(cancellationToken);
@@ -853,33 +857,46 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Compiles a .dme with DreamMaker.
/// </summary>
/// <param name="dreamMakerPath">The path to the DreamMaker executable.</param>
/// <param name="engineLock">The <see cref="IEngineExecutableLock"/> to use.</param>
/// <param name="job">The <see cref="CompileJob"/> for the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask<int> RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="true"/> if compilation succeeded, <see langword="false"/> otherwise.</returns>
async ValueTask<bool> RunDreamMaker(IEngineExecutableLock engineLock, Models.CompileJob job, CancellationToken cancellationToken)
{
await using var dm = processExecutor.LaunchProcess(
dreamMakerPath,
ioManager.ResolvePath(
job.DirectoryName.ToString()),
$"-clean {job.DmeName}.{DmeExtension}",
readStandardHandles: true,
noShellExecute: true);
var arguments = engineLock.FormatCompilerArguments($"{job.DmeName}.{DmeExtension}");
bool result;
if (arguments == null)
{
logger.LogTrace("Engine lock says compilation isn't necessary.");
job.Output = $"{engineLock.Version.Engine} does not require compilation.";
result = true;
}
else
{
await using var dm = processExecutor.LaunchProcess(
engineLock.CompilerExePath,
ioManager.ResolvePath(
job.DirectoryName.ToString()),
arguments,
readStandardHandles: true,
noShellExecute: true);
if (sessionConfiguration.LowPriorityDeploymentProcesses)
dm.AdjustPriority(false);
if (sessionConfiguration.LowPriorityDeploymentProcesses)
dm.AdjustPriority(false);
int exitCode;
using (cancellationToken.Register(() => dm.Terminate()))
exitCode = (await dm.Lifetime).Value;
cancellationToken.ThrowIfCancellationRequested();
int exitCode;
using (cancellationToken.Register(() => dm.Terminate()))
exitCode = (await dm.Lifetime).Value;
cancellationToken.ThrowIfCancellationRequested();
logger.LogDebug("DreamMaker exit code: {exitCode}", exitCode);
job.Output = $"{await dm.GetCombinedOutput(cancellationToken)}{Environment.NewLine}{Environment.NewLine}Exit Code: {exitCode}";
logger.LogDebug("DreamMaker output: {newLine}{output}", Environment.NewLine, job.Output);
result = exitCode == 0;
}
logger.LogDebug("DreamMaker exit code: {exitCode}", exitCode);
job.Output = await dm.GetCombinedOutput(cancellationToken);
currentDreamMakerOutput = job.Output;
logger.LogDebug("DreamMaker output: {newLine}{output}", Environment.NewLine, job.Output);
return exitCode;
return result;
}
/// <summary>
@@ -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 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// The <see cref="CompileJob"/> of the .dmb.
/// </summary>
CompileJob CompileJob { get; }
Models.CompileJob CompileJob { get; }
/// <summary>
/// The <see cref="Api.Models.Internal.ByondVersion"/> used to build the .dmb.
/// </summary>
ByondVersion ByondVersion { get; }
/// <summary>
/// Disposing the <see cref="IDmbProvider"/> won't cause a cleanup of the working directory.
@@ -2,8 +2,8 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components.Deployment
{
@@ -24,7 +24,10 @@ namespace Tgstation.Server.Host.Components.Deployment
public string Directory => ioManager.ResolvePath(LiveGameDirectory);
/// <inheritdoc />
public CompileJob CompileJob => baseProvider.CompileJob;
public Models.CompileJob CompileJob => baseProvider.CompileJob;
/// <inheritdoc />
public ByondVersion ByondVersion => baseProvider.ByondVersion;
/// <summary>
/// If <see cref="MakeActive(CancellationToken)"/> has been run.
@@ -1,6 +1,6 @@
using System;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Api.Models.Internal;
namespace Tgstation.Server.Host.Components.Deployment
{
@@ -16,7 +16,10 @@ namespace Tgstation.Server.Host.Components.Deployment
public string Directory { get; }
/// <inheritdoc />
public CompileJob CompileJob { get; }
public Models.CompileJob CompileJob { get; }
/// <inheritdoc />
public ByondVersion ByondVersion { get; }
/// <summary>
/// Initializes a new instance of the <see cref="TemporaryDmbProvider"/> class.
@@ -24,11 +27,13 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <param name="directory">The value of <see cref="Directory"/>.</param>
/// <param name="dmb">The value of <see cref="DmbName"/>.</param>
/// <param name="compileJob">The value of <see cref="CompileJob"/>.</param>
public TemporaryDmbProvider(string directory, string dmb, CompileJob compileJob)
/// <param name="byondVersion">The value of <see cref="ByondVersion"/>.</param>
public TemporaryDmbProvider(string directory, string dmb, Models.CompileJob compileJob, ByondVersion byondVersion)
{
DmbName = dmb ?? throw new ArgumentNullException(nameof(dmb));
Directory = directory ?? throw new ArgumentNullException(nameof(directory));
CompileJob = compileJob ?? throw new ArgumentNullException(nameof(compileJob));
ByondVersion = byondVersion ?? throw new ArgumentNullException(nameof(byondVersion));
}
/// <inheritdoc />
@@ -16,14 +16,14 @@ namespace Tgstation.Server.Host.Components.Session
/// Create a <see cref="ISessionController"/> from a freshly launch DreamDaemon instance.
/// </summary>
/// <param name="dmbProvider">The <see cref="IDmbProvider"/> to use.</param>
/// <param name="currentByondLock">The current <see cref="IByondExecutableLock"/> if any.</param>
/// <param name="currentByondLock">The current <see cref="IEngineExecutableLock"/> if any.</param>
/// <param name="launchParameters">The <see cref="DreamDaemonLaunchParameters"/> to use. <see cref="DreamDaemonLaunchParameters.SecurityLevel"/> will be updated with the minumum required security level for the launch.</param>
/// <param name="apiValidate">If the <see cref="ISessionController"/> should only validate the DMAPI then exit.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="ISessionController"/>.</returns>
ValueTask<ISessionController> LaunchNew(
IDmbProvider dmbProvider,
IByondExecutableLock currentByondLock,
IEngineExecutableLock currentByondLock,
DreamDaemonLaunchParameters launchParameters,
bool apiValidate,
CancellationToken cancellationToken);
@@ -123,9 +123,9 @@ namespace Tgstation.Server.Host.Components.Session
readonly IProcess process;
/// <summary>
/// The <see cref="IByondExecutableLock"/> for the <see cref="SessionController"/>.
/// The <see cref="IEngineExecutableLock"/> for the <see cref="SessionController"/>.
/// </summary>
readonly IByondExecutableLock byondLock;
readonly IEngineExecutableLock byondLock;
/// <summary>
/// The <see cref="IChatTrackingContext"/> for the <see cref="SessionController"/>.
@@ -244,7 +244,7 @@ namespace Tgstation.Server.Host.Components.Session
ReattachInformation reattachInformation,
Api.Models.Instance metadata,
IProcess process,
IByondExecutableLock byondLock,
IEngineExecutableLock byondLock,
global::Byond.TopicSender.ITopicClient byondTopicSender,
IChatTrackingContext chatTrackingContext,
IBridgeRegistrar bridgeRegistrar,
@@ -6,8 +6,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Byond.TopicSender;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Models;
@@ -128,38 +126,6 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
readonly Api.Models.Instance instance;
/// <summary>
/// Change a given <paramref name="securityLevel"/> into the appropriate DreamDaemon command line word.
/// </summary>
/// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level to change.</param>
/// <returns>A <see cref="string"/> representation of the command line parameter.</returns>
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)),
};
}
/// <summary>
/// Change a given <paramref name="visibility"/> into the appropriate DreamDaemon command line word.
/// </summary>
/// <param name="visibility">The <see cref="DreamDaemonVisibility"/> level to change.</param>
/// <returns>A <see cref="string"/> representation of the command line parameter.</returns>
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)),
};
}
/// <summary>
/// Check if a given <paramref name="port"/> can be bound to.
/// </summary>
@@ -242,7 +208,7 @@ namespace Tgstation.Server.Host.Components.Session
#pragma warning disable CA1506 // TODO: Decomplexify
public async ValueTask<ISessionController> LaunchNew(
IDmbProvider dmbProvider,
IByondExecutableLock currentByondLock,
IEngineExecutableLock currentByondLock,
DreamDaemonLaunchParameters launchParameters,
bool apiValidate,
CancellationToken cancellationToken)
@@ -274,7 +240,7 @@ namespace Tgstation.Server.Host.Components.Session
// 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 +255,7 @@ namespace Tgstation.Server.Host.Components.Session
string outputFilePath = null;
var preserveLogFile = true;
var cliSupported = byondLock.SupportsCli;
var hasStandardOutput = byondLock.HasStandardOutput;
if (launchParameters.LogOutput.Value)
{
var now = DateTimeOffset.UtcNow;
@@ -302,7 +268,7 @@ namespace Tgstation.Server.Host.Components.Session
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 +276,12 @@ namespace Tgstation.Server.Host.Components.Session
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 +308,10 @@ namespace Tgstation.Server.Host.Components.Session
accessIdentifier,
launchParameters.Port.Value);
var byondTopicSender = topicClientFactory.CreateTopicClient(
TimeSpan.FromMilliseconds(
launchParameters.TopicRequestTimeout.Value));
var sessionController = new SessionController(
reattachInformation,
instance,
@@ -362,7 +327,7 @@ namespace Tgstation.Server.Host.Components.Session
() => LogDDOutput(
process,
outputFilePath,
cliSupported,
hasStandardOutput,
preserveLogFile,
CancellationToken.None), // DCT: None available
launchParameters.StartupTimeout,
@@ -406,7 +371,7 @@ namespace Tgstation.Server.Host.Components.Session
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 +388,7 @@ namespace Tgstation.Server.Host.Components.Session
try
{
if (!byondLock.SupportsCli)
if (byondLock.PromptsForNetworkAccess)
networkPromptReaper.RegisterProcess(process);
var chatTrackingContext = chat.CreateTrackingContext();
@@ -479,60 +444,45 @@ namespace Tgstation.Server.Host.Components.Session
}
/// <summary>
/// Creates the DreamDaemon <see cref="IProcess"/>.
/// Creates the game server <see cref="IProcess"/>.
/// </summary>
/// <param name="dmbProvider">The <see cref="IDmbProvider"/>.</param>
/// <param name="byondTopicSender">The <see cref="ITopicClient"/> to use for sanitization.</param>
/// <param name="byondLock">The <see cref="IByondExecutableLock"/>.</param>
/// <param name="byondLock">The <see cref="IEngineExecutableLock"/>.</param>
/// <param name="launchParameters">The <see cref="DreamDaemonLaunchParameters"/>.</param>
/// <param name="accessIdentifier">The secure string to use for the session.</param>
/// <param name="logFilePath">The path to log DreamDaemon output to.</param>
/// <param name="apiValidate">If we are only validating the DMAPI then exiting.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the DreamDaemon <see cref="IProcess"/>.</returns>
async ValueTask<IProcess> CreateDreamDaemonProcess(
async ValueTask<IProcess> CreateGameServerProcess(
IDmbProvider dmbProvider,
ITopicClient byondTopicSender,
IByondExecutableLock byondLock,
IEngineExecutableLock byondLock,
DreamDaemonLaunchParameters launchParameters,
string accessIdentifier,
string logFilePath,
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<string, string>
{
{ 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 +495,7 @@ namespace Tgstation.Server.Host.Components.Session
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
@@ -43,7 +43,7 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
/// <param name="version">The <see cref="Version"/> to normalize.</param>
/// <returns>The normalized <see cref="Version"/>. May be a reference to <paramref name="version"/>.</returns>
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;
/// <summary>
/// Initializes a new instance of the <see cref="ByondController"/> class.
@@ -72,23 +72,26 @@ namespace Tgstation.Server.Host.Controllers
}
/// <summary>
/// Gets the active <see cref="ByondResponse.Version"/>.
/// Gets the active <see cref="Api.Models.Internal.ByondVersion"/>.
/// </summary>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
/// <response code="200">Retrieved version information successfully.</response>
/// <response code="409">No BYOND versions installed.</response>
[HttpGet]
[TgsAuthorize(ByondRights.ReadActive)]
[ProducesResponseType(typeof(ByondResponse), 200)]
[ProducesResponseType(typeof(ErrorMessageResponse), 409)]
public ValueTask<IActionResult> Read()
=> WithComponentInstance(instance =>
ValueTask.FromResult<IActionResult>(
Json(new ByondResponse
{
Version = instance.ByondManager.ActiveVersion,
})));
instance.ByondManager.ActiveVersion != null
? Json(
new ByondResponse(
instance.ByondManager.ActiveVersion))
: Conflict(new ErrorMessageResponse(ErrorCode.ResourceNotPresent))));
/// <summary>
/// Lists installed <see cref="ByondResponse.Version"/>s.
/// Lists installed <see cref="Api.Models.Internal.ByondVersion"/>s.
/// </summary>
/// <param name="page">The current page.</param>
/// <param name="pageSize">The page size.</param>
@@ -106,10 +109,7 @@ namespace Tgstation.Server.Host.Controllers
instance
.ByondManager
.InstalledVersions
.Select(x => new ByondResponse
{
Version = x,
})
.Select(x => new ByondResponse(x))
.AsQueryable()
.OrderBy(x => x.Version))),
null,
@@ -126,27 +126,42 @@ namespace Tgstation.Server.Host.Controllers
/// <response code="200">Switched active version successfully.</response>
/// <response code="202">Created <see cref="Job"/> to install and switch active version successfully.</response>
[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
#pragma warning disable CA1502 // TODO: Decomplexify
#pragma warning disable CA1506
public async ValueTask<IActionResult> Update([FromBody] ByondVersionRequest model, CancellationToken cancellationToken)
#pragma warning restore CA1506
#pragma warning restore CA1502
{
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 ((isByondEngine && (model.Version.Revision != -1 || (uploadingZip && model.Version.Build > 0) || model.SourceCommittish != null || model.SourceRepository != null))
|| (!isByondEngine && (model.Version != null || String.IsNullOrWhiteSpace(model.SourceCommittish))))
return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure));
var version = NormalizeVersion(model.Version);
Uri sourceRepo;
if (isByondEngine)
{
model.Version = NormalizeByondVersion(model.Version);
sourceRepo = null;
}
else
{
sourceRepo = model.SourceRepository ?? new Uri("https://github.com/OpenDreamProject/OpenDream");
}
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 +170,50 @@ namespace Tgstation.Server.Host.Controllers
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 +230,13 @@ namespace Tgstation.Server.Host.Controllers
job,
async (core, databaseContextFactory, paramJob, progressHandler, jobCancellationToken) =>
{
Stream zipFileStream = null;
if (sourceRepo != null)
await core.ByondManager.EnsureEngineSource(
sourceRepo,
model.Engine.Value,
jobCancellationToken);
MemoryStream zipFileStream = null;
if (fileUploadTicket != null)
await using (fileUploadTicket)
{
@@ -229,7 +256,7 @@ namespace Tgstation.Server.Host.Controllers
await using (zipFileStream)
await core.ByondManager.ChangeVersion(
progressHandler,
version,
model,
zipFileStream,
true,
jobCancellationToken);
@@ -260,7 +287,7 @@ namespace Tgstation.Server.Host.Controllers
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
/// <response code="202">Created <see cref="Job"/> to delete target version successfully.</response>
/// <response code="409">Attempted to delete the active BYOND <see cref="Version"/>.</response>
/// <response code="410">The <see cref="ByondVersionDeleteRequest.Version"/> specified was not installed.</response>
/// <response code="410">The <see cref="Api.Models.Internal.ByondVersion"/> specified was not installed.</response>
[HttpDelete]
[TgsAuthorize(ByondRights.DeleteInstall)]
[ProducesResponseType(typeof(JobResponse), 202)]
@@ -270,22 +297,22 @@ namespace Tgstation.Server.Host.Controllers
{
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<IActionResult>(
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<IActionResult>(
versionNotInstalled
@@ -296,22 +323,27 @@ namespace Tgstation.Server.Host.Controllers
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();
@@ -82,7 +82,7 @@ namespace Tgstation.Server.Host.Models
}
/// <inheritdoc />
public CompileJobResponse ToApi() => new CompileJobResponse
public CompileJobResponse ToApi() => new ()
{
DirectoryName = DirectoryName,
DmeName = DmeName,
@@ -90,7 +90,9 @@ namespace Tgstation.Server.Host.Models
Job = Job.ToApi(),
Output = Output,
RevisionInformation = RevisionInformation.ToApi(),
ByondVersion = Version.Parse(ByondVersion),
ByondVersion = Api.Models.Internal.ByondVersion.TryParse(ByondVersion, out var version)
? version
: throw new InvalidOperationException($"Failed to parse BYOND version: {ByondVersion}"),
MinimumSecurityLevel = MinimumSecurityLevel,
DMApiVersion = DMApiVersion,
RepositoryOrigin = RepositoryOrigin != null ? new Uri(RepositoryOrigin) : null,
@@ -40,7 +40,7 @@ namespace Tgstation.Server.Api.Rights.Tests
[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<ByondRights>();
Assert.AreEqual(allByondRights, automaticByondRights);
@@ -11,6 +11,8 @@ using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Common.Http;
@@ -22,10 +24,12 @@ namespace Tgstation.Server.Client.Tests
[TestMethod]
public async Task TestDeserializingByondModelsWork()
{
var sample = new ByondResponse
{
Version = new Version(511, 1385, 0)
};
var sample = new ByondResponse(
new ByondVersion
{
Engine = EngineType.Byond,
Version = new Version(511, 1385, 0)
});
var sampleJson = JsonConvert.SerializeObject(sample, new JsonSerializerSettings
{
@@ -51,10 +55,12 @@ namespace Tgstation.Server.Client.Tests
[TestMethod]
public async Task TestUnrecognizedResponse()
{
var sample = new ByondResponse
{
Version = new Version(511, 1385)
};
var sample = new ByondResponse(
new ByondVersion
{
Engine = EngineType.Byond,
Version = new Version(511, 1385)
});
var fakeJson = "asdfasd <>F#(*)U*#JLI";
@@ -6,6 +6,8 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Components.Byond.Tests
@@ -62,7 +64,11 @@ namespace Tgstation.Server.Host.Components.Byond.Tests
new MemoryStream(ourArray)))
.Verifiable();
var result = await installer.DownloadVersion(new Version(511, 1385), default);
var result = await installer.DownloadVersion(new ByondVersion
{
Engine = EngineType.Byond,
Version = new Version(123, 252345),
}, default);
Assert.IsTrue(ourArray.SequenceEqual(result.ToArray()));
mockIOManager.Verify();
@@ -79,9 +85,17 @@ namespace Tgstation.Server.Host.Components.Byond.Tests
const string FakePath = "fake";
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.InstallByond(null, null, default).AsTask());
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.InstallByond(new Version(123,252345), null, default).AsTask());
await installer.InstallByond(new Version(511, 1385), FakePath, default);
var byondVersion = new ByondVersion
{
Engine = EngineType.Byond,
Version = new Version(123, 252345),
};
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.InstallByond(byondVersion, null, default).AsTask());
byondVersion.Version = new Version(511, 1385);
await installer.InstallByond(byondVersion, FakePath, default);
mockPostWriteHandler.Verify(x => x.HandleWrite(It.IsAny<string>()), Times.Exactly(4));
}
@@ -1,4 +1,4 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
@@ -50,7 +50,7 @@ namespace Tgstation.Server.Host.Security.Tests
var authContext = new AuthenticationContext(null, user, instanceUser);
user.PermissionSet.AdministrationRights = AdministrationRights.WriteUsers;
instanceUser.ByondRights = ByondRights.InstallOfficialOrChangeActiveVersion | ByondRights.ReadActive;
instanceUser.ByondRights = ByondRights.InstallOfficialOrChangeActiveByondVersion | ByondRights.ReadActive;
Assert.AreEqual((ulong)user.PermissionSet.AdministrationRights, authContext.GetRight(RightsType.Administration));
Assert.AreEqual((ulong)instanceUser.ByondRights, authContext.GetRight(RightsType.Byond));
}
@@ -9,6 +9,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.IO;
@@ -42,9 +43,9 @@ namespace Tgstation.Server.Tests
var logger = loggerFactory.CreateLogger("CachingFileDownloader");
var cfd = new CachingFileDownloader(loggerFactory.CreateLogger<CachingFileDownloader>());
var edgeVersion = await ByondTest.GetEdgeVersion(cfd, cancellationToken);
var edgeVersion = await ByondTest.GetEdgeVersion(Api.Models.EngineType.Byond, cfd, cancellationToken);
await InitializeByondVersion(logger, edgeVersion, new PlatformIdentifier().IsWindows, cancellationToken);
await InitializeByondVersion(logger, edgeVersion.Version, new PlatformIdentifier().IsWindows, cancellationToken);
// predownload the target github release update asset
var gitHubToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN");
@@ -76,10 +77,16 @@ namespace Tgstation.Server.Tests
ServiceCollectionExtensions.UseFileDownloader<CachingFileDownloader>();
}
public static async ValueTask InitializeByondVersion(ILogger logger, Version version, bool windows, CancellationToken cancellationToken)
public static async ValueTask InitializeByondVersion(ILogger logger, Version byondVersion, bool windows, CancellationToken cancellationToken)
{
var version = new ByondVersion
{
Engine = Api.Models.EngineType.Byond,
Version = byondVersion,
};
var url = new Uri(
$"https://www.byond.com/download/build/{version.Major}/{version.Major}.{version.Minor}_byond{(!windows ? "_linux" : string.Empty)}.zip");
$"https://www.byond.com/download/build/{version.Version.Major}/{version.Version.Major}.{version.Version.Minor}_byond{(!windows ? "_linux" : string.Empty)}.zip");
string path = null;
if (TestingUtils.RunningInGitHubActions)
{
@@ -91,7 +98,7 @@ namespace Tgstation.Server.Tests
windows ? "windows" : "linux");
path = Path.Combine(
dir,
$"{version.Major}.{version.Minor}.zip");
$"{version.Version.Major}.{version.Version.Minor}.zip");
}
await (await CacheFile(logger, url, null, path, cancellationToken)).DisposeAsync();
@@ -12,6 +12,7 @@ using Moq;
using Newtonsoft.Json;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Chat.Commands;
using Tgstation.Server.Host.Components.Chat.Providers;
@@ -100,7 +101,15 @@ namespace Tgstation.Server.Tests.Live
return ValueTask.CompletedTask;
}
public override ValueTask<Func<string, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, string gitHubOwner, string gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken)
public override ValueTask<Func<string, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
Host.Models.RevisionInformation revisionInformation,
ByondVersion byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
ulong channelId,
bool localCommitPushed,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(revisionInformation);
ArgumentNullException.ThrowIfNull(byondVersion);
@@ -6,13 +6,17 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Elasticsearch.Net.Specification.IndexLifecycleManagementApi;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Api.Models.Request;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Client;
@@ -32,16 +36,22 @@ namespace Tgstation.Server.Tests.Live.Instance
readonly Api.Models.Instance metadata;
static Version edgeVersion;
static Dictionary<EngineType, ByondVersion> edgeVersions = new Dictionary<EngineType, ByondVersion>
{
{ EngineType.Byond, null },
{ EngineType.OpenDream, null }
};
Version testVersion;
ByondVersion testVersion;
EngineType testEngine;
public ByondTest(IByondClient byondClient, IJobsClient jobsClient, IFileDownloader fileDownloader, Api.Models.Instance metadata)
public ByondTest(IByondClient byondClient, IJobsClient jobsClient, IFileDownloader fileDownloader, Api.Models.Instance metadata, EngineType engineType)
: base(jobsClient)
{
this.byondClient = byondClient ?? throw new ArgumentNullException(nameof(byondClient));
this.fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader));
this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
this.testEngine = engineType;
}
public Task Run(CancellationToken cancellationToken, out Task firstInstall)
@@ -50,42 +60,67 @@ namespace Tgstation.Server.Tests.Live.Instance
return RunContinued(firstInstall, cancellationToken);
}
public static async Task<Version> GetEdgeVersion(IFileDownloader fileDownloader, CancellationToken cancellationToken)
public static async ValueTask<ByondVersion> GetEdgeVersion(EngineType engineType, IFileDownloader fileDownloader, CancellationToken cancellationToken)
{
var edgeVersion = edgeVersions[engineType];
if (edgeVersion != null)
return edgeVersion;
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 splits = text.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
ByondVersion byondVersion;
if (engineType == EngineType.Byond)
{
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(cancellationToken);
var splits = text.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
var targetVersion = splits.Last();
var targetVersion = splits.Last();
var missingVersionMap = new PlatformIdentifier().IsWindows
? new Dictionary<string, string>()
{
}
// linux map also needs updating in CI
: new Dictionary<string, string>()
{
var missingVersionMap = new PlatformIdentifier().IsWindows
? new Dictionary<string, string>()
{
}
// linux map also needs updating in CI
: new Dictionary<string, string>()
{
{ "515.1612", "515.1611" }
};
};
if (missingVersionMap.TryGetValue(targetVersion, out var remappedVersion))
targetVersion = remappedVersion;
if (missingVersionMap.TryGetValue(targetVersion, out var remappedVersion))
targetVersion = remappedVersion;
return edgeVersion = Version.Parse(targetVersion);
Assert.IsTrue(ByondVersion.TryParse(targetVersion, out byondVersion), $"Bad version: {targetVersion}");
}
else
{
Assert.Fail($"Edge version retrieval for {engineType} not implemented!");
return null;
}
return edgeVersions[engineType] = byondVersion;
}
async Task RunPartOne(CancellationToken cancellationToken)
{
testVersion = await GetEdgeVersion(fileDownloader, cancellationToken);
testVersion = await GetEdgeVersion(EngineType.Byond, fileDownloader, cancellationToken);
await TestNoVersion(cancellationToken);
await TestInstallNullVersion(cancellationToken);
await TestInstallStable(cancellationToken);
}
ValueTask TestInstallNullVersion(CancellationToken cancellationToken)
=> ApiAssert.ThrowsException<ApiConflictException, ByondInstallResponse>(
() => byondClient.SetActiveVersion(
new ByondVersionRequest
{
Engine = testEngine,
},
null,
cancellationToken),
ErrorCode.ModelValidationFailure);
async Task RunContinued(Task firstInstall, CancellationToken cancellationToken)
{
await firstInstall;
@@ -98,7 +133,8 @@ namespace Tgstation.Server.Tests.Live.Instance
{
var deleteThisOneBecauseItWasntPartOfTheOriginalTest = await byondClient.DeleteVersion(new ByondVersionDeleteRequest
{
Version = new(testVersion.Major, testVersion.Minor, 2)
Engine = testEngine,
Version = new(testVersion.Version.Major, testVersion.Version.Minor, 2)
}, cancellationToken);
await WaitForJob(deleteThisOneBecauseItWasntPartOfTheOriginalTest, 30, false, null, cancellationToken);
@@ -112,14 +148,18 @@ namespace Tgstation.Server.Tests.Live.Instance
var uninstallResponseTask = byondClient.DeleteVersion(
new ByondVersionDeleteRequest
{
Version = testVersion
Version = testVersion.Version,
Engine = testVersion.Engine,
SourceCommittish = testVersion.SourceCommittish,
},
cancellationToken);
var badBecauseActiveResponseTask = ApiAssert.ThrowsException<ConflictException, JobResponse>(() => byondClient.DeleteVersion(
new ByondVersionDeleteRequest
{
Version = new(testVersion.Major, testVersion.Minor, 1)
Version = new(testVersion.Version.Major, testVersion.Version.Minor, 1),
Engine = testVersion.Engine,
SourceCommittish = testVersion.SourceCommittish,
},
cancellationToken), ErrorCode.ByondCannotDeleteActiveVersion);
@@ -140,7 +180,7 @@ namespace Tgstation.Server.Tests.Live.Instance
var newVersions = await byondClient.InstalledVersions(null, cancellationToken);
Assert.IsNotNull(newVersions);
Assert.AreEqual(1, newVersions.Count);
Assert.AreEqual(new Version(testVersion.Major, testVersion.Minor, 1), newVersions[0].Version);
Assert.AreEqual(new Version(testVersion.Version.Major, testVersion.Version.Minor, 1), newVersions[0].Version);
}
async Task TestInstallFakeVersion(CancellationToken cancellationToken)
@@ -158,7 +198,9 @@ namespace Tgstation.Server.Tests.Live.Instance
{
var newModel = new ByondVersionRequest
{
Version = testVersion
Version = testVersion.Version,
Engine = testVersion.Engine,
SourceCommittish = testVersion.SourceCommittish,
};
var test = await byondClient.SetActiveVersion(newModel, null, cancellationToken);
Assert.IsNotNull(test.InstallJob);
@@ -220,7 +262,9 @@ namespace Tgstation.Server.Tests.Live.Instance
var test = await byondClient.SetActiveVersion(
new ByondVersionRequest
{
Version = testVersion,
Engine = testVersion.Engine,
Version = testVersion.Version,
SourceCommittish = testVersion.SourceCommittish,
UploadCustomZip = true
},
stableBytesMs,
@@ -234,7 +278,9 @@ namespace Tgstation.Server.Tests.Live.Instance
var test2 = await byondClient.SetActiveVersion(
new ByondVersionRequest
{
Version = testVersion,
Version = testVersion.Version,
SourceCommittish = testVersion.SourceCommittish,
Engine = testVersion.Engine,
UploadCustomZip = true
},
stableBytesMs,
@@ -244,22 +290,24 @@ namespace Tgstation.Server.Tests.Live.Instance
await WaitForJob(test2.InstallJob, 30, false, null, cancellationToken);
var newSettings = await byondClient.ActiveVersion(cancellationToken);
Assert.AreEqual(new Version(testVersion.Major, testVersion.Minor, 2), newSettings.Version);
Assert.AreEqual(new Version(testVersion.Version.Major, testVersion.Version.Minor, 2), newSettings.Version);
// test a few switches
var installResponse = await byondClient.SetActiveVersion(new ByondVersionRequest
{
Version = testVersion
Version = testVersion.Version,
SourceCommittish = testVersion.SourceCommittish,
Engine = testVersion.Engine,
}, null, cancellationToken);
Assert.IsNull(installResponse.InstallJob);
await ApiAssert.ThrowsException<ApiConflictException, ByondInstallResponse>(() => byondClient.SetActiveVersion(new ByondVersionRequest
{
Version = new Version(testVersion.Major, testVersion.Minor, 3)
Version = new Version(testVersion.Version.Major, testVersion.Version.Minor, 3)
}, null, cancellationToken), ErrorCode.ByondNonExistentCustomVersion);
installResponse = await byondClient.SetActiveVersion(new ByondVersionRequest
{
Version = new Version(testVersion.Major, testVersion.Minor, 1)
Version = new Version(testVersion.Version.Major, testVersion.Version.Minor, 1)
}, null, cancellationToken);
Assert.IsNull(installResponse.InstallJob);
}
@@ -10,6 +10,7 @@ using Microsoft.Extensions.Options;
using Moq;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Api.Models.Request;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Client;
@@ -43,9 +44,10 @@ namespace Tgstation.Server.Tests.Live.Instance
ushort ddPort,
bool highPrioDD,
bool lowPrioDeployment,
EngineType engineType,
CancellationToken cancellationToken)
{
var byondTest = new ByondTest(instanceClient.Byond, instanceClient.Jobs, fileDownloader, instanceClient.Metadata);
var byondTest = new ByondTest(instanceClient.Byond, instanceClient.Jobs, fileDownloader, instanceClient.Metadata, engineType);
var chatTest = new ChatTest(instanceClient.ChatBots, instanceManagerClient, instanceClient.Jobs, instanceClient.Metadata);
var configTest = new ConfigurationTest(instanceClient.Configuration, instanceClient.Metadata);
var repoTest = new RepositoryTest(instanceClient.Repository, instanceClient.Jobs);
@@ -66,17 +68,23 @@ namespace Tgstation.Server.Tests.Live.Instance
await byondTask;
await new WatchdogTest(
await ByondTest.GetEdgeVersion(fileDownloader, cancellationToken), instanceClient, instanceManager, serverPort, highPrioDD, ddPort).Run(cancellationToken);
await ByondTest.GetEdgeVersion(engineType, fileDownloader, cancellationToken), instanceClient, instanceManager, serverPort, highPrioDD, ddPort).Run(cancellationToken);
}
public async Task RunCompatTests(
Version compatVersion,
Version compatByondVersion,
IInstanceClient instanceClient,
ushort dmPort,
ushort ddPort,
bool highPrioDD,
CancellationToken cancellationToken)
{
var compatVersion = new ByondVersion
{
Engine = EngineType.Byond,
Version = compatByondVersion,
};
System.Console.WriteLine($"COMPAT TEST START: {compatVersion}");
const string Origin = "https://github.com/Cyberboss/common_core";
var cloneRequest = instanceClient.Repository.Clone(new RepositoryCreateRequest
@@ -155,7 +163,9 @@ namespace Tgstation.Server.Tests.Live.Instance
installJob2 = await instanceClient.Byond.SetActiveVersion(new ByondVersionRequest
{
UploadCustomZip = true,
Version = compatVersion,
Version = compatVersion.Version,
Engine = compatVersion.Engine,
SourceCommittish = compatVersion.SourceCommittish
}, stableBytesMs, cancellationToken);
}
@@ -18,6 +18,7 @@ using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Api.Models.Request;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Client;
@@ -57,11 +58,11 @@ namespace Tgstation.Server.Tests.Live.Instance
readonly ushort ddPort;
readonly bool highPrioDD;
readonly TopicClient topicClient;
readonly Version testVersion;
readonly ByondVersion testVersion;
bool ranTimeoutTest = false;
public WatchdogTest(Version testVersion, IInstanceClient instanceClient, InstanceManager instanceManager, ushort serverPort, bool highPrioDD, ushort ddPort)
public WatchdogTest(ByondVersion testVersion, IInstanceClient instanceClient, InstanceManager instanceManager, ushort serverPort, bool highPrioDD, ushort ddPort)
: base(instanceClient.Jobs)
{
this.instanceClient = instanceClient ?? throw new ArgumentNullException(nameof(instanceClient));
@@ -94,8 +95,9 @@ namespace Tgstation.Server.Tests.Live.Instance
var byondVersion = list[0];
Assert.AreEqual(1, byondVersion.Version.Build);
Assert.AreEqual(testVersion.Major, byondVersion.Version.Major);
Assert.AreEqual(testVersion.Minor, byondVersion.Version.Minor);
Assert.AreEqual(testVersion.Version.Major, byondVersion.Version.Major);
Assert.AreEqual(testVersion.Version.Minor, byondVersion.Version.Minor);
Assert.AreEqual(testVersion.Engine, byondVersion.Engine);
}
await Task.WhenAll(
@@ -230,10 +232,10 @@ namespace Tgstation.Server.Tests.Live.Instance
async Task<JobResponse> TestDeleteByondInstallErrorCasesAndQueing(CancellationToken cancellationToken)
{
var testCustomVersion = new Version(testVersion.Major, testVersion.Minor, 1);
var testCustomVersion = new Version(testVersion.Version.Major, testVersion.Version.Minor, 1);
var currentByond = await instanceClient.Byond.ActiveVersion(cancellationToken);
Assert.IsNotNull(currentByond);
Assert.AreEqual(testVersion.Semver(), currentByond.Version);
Assert.AreEqual(testVersion.Version.Semver(), currentByond.Version);
// Change the active version and check we get delayed while deleting the old one because the watchdog is using it
var setActiveResponse = await instanceClient.Byond.SetActiveVersion(
@@ -250,7 +252,8 @@ namespace Tgstation.Server.Tests.Live.Instance
var deleteJob = await instanceClient.Byond.DeleteVersion(
new ByondVersionDeleteRequest
{
Version = testVersion,
Version = testVersion.Version,
SourceCommittish = testVersion.SourceCommittish,
},
cancellationToken);
@@ -265,7 +268,9 @@ namespace Tgstation.Server.Tests.Live.Instance
setActiveResponse = await instanceClient.Byond.SetActiveVersion(
new ByondVersionRequest
{
Version = testVersion,
Version = testVersion.Version,
Engine = testVersion.Engine,
SourceCommittish = testVersion.SourceCommittish
},
null,
cancellationToken);
@@ -291,7 +296,9 @@ namespace Tgstation.Server.Tests.Live.Instance
deleteJob = await instanceClient.Byond.DeleteVersion(
new ByondVersionDeleteRequest
{
Version = testVersion,
Version = testVersion.Version,
Engine = testVersion.Engine,
SourceCommittish = testVersion.SourceCommittish,
},
cancellationToken);
@@ -954,9 +961,8 @@ namespace Tgstation.Server.Tests.Live.Instance
System.Console.WriteLine("TEST: WATCHDOG BYOND VERSION UPDATE TEST");
var versionToInstall = testVersion;
versionToInstall = versionToInstall.Semver();
var currentByondVersion = await instanceClient.Byond.ActiveVersion(cancellationToken);
Assert.AreNotEqual(versionToInstall, currentByondVersion.Version);
Assert.AreNotEqual(versionToInstall, currentByondVersion);
var initialStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
@@ -969,7 +975,9 @@ namespace Tgstation.Server.Tests.Live.Instance
var byondInstallJobTask = instanceClient.Byond.SetActiveVersion(
new ByondVersionRequest
{
Version = versionToInstall
Version = versionToInstall.Version,
Engine = versionToInstall.Engine,
SourceCommittish = versionToInstall.SourceCommittish,
},
null,
cancellationToken);
@@ -28,6 +28,7 @@ using Npgsql;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Api.Models.Request;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Api.Rights;
@@ -955,7 +956,12 @@ namespace Tgstation.Server.Tests.Live
}
[TestMethod]
public async Task TestStandardTgsOperation()
public Task TestStandardTgsOperation() => TestStandardTgsOperation(EngineType.Byond);
[TestMethod]
public Task TestOpenDreamTgsOperation() => TestStandardTgsOperation(EngineType.OpenDream);
async Task TestStandardTgsOperation(EngineType engineType)
{
using(var currentProcess = System.Diagnostics.Process.GetCurrentProcess())
{
@@ -980,7 +986,7 @@ namespace Tgstation.Server.Tests.Live
ServiceCollectionExtensions.UseAdditionalLoggerProvider<HardFailLoggerProvider>();
var failureTask = HardFailLoggerProvider.FailureSource;
var internalTask = TestTgsInternal(hardCancellationToken);
var internalTask = TestTgsInternal(engineType, hardCancellationToken);
await Task.WhenAny(
internalTask,
failureTask);
@@ -1012,7 +1018,7 @@ namespace Tgstation.Server.Tests.Live
await internalTask;
}
async Task TestTgsInternal(CancellationToken hardCancellationToken)
async Task TestTgsInternal(EngineType engineType, CancellationToken hardCancellationToken)
{
var discordConnectionString = Environment.GetEnvironmentVariable("TGS_TEST_DISCORD_TOKEN");
var ircConnectionString = Environment.GetEnvironmentVariable("TGS_TEST_IRC_CONNECTION_STRING");
@@ -1118,7 +1124,7 @@ namespace Tgstation.Server.Tests.Live
async Task RunInstanceTests()
{
// Some earlier linux BYOND versions have a critical bug where replacing the directory in non-basic watchdogs causes the DreamDaemon cwd to change
var canRunCompatTests = new PlatformIdentifier().IsWindows;
var canRunCompatTests = engineType == EngineType.Byond && new PlatformIdentifier().IsWindows;
var compatTests = canRunCompatTests
? FailFast(
instanceTest
@@ -1142,6 +1148,7 @@ namespace Tgstation.Server.Tests.Live
mainDDPort,
server.HighPriorityDreamDaemon,
server.LowPriorityDeployments,
engineType,
cancellationToken));
await compatTests;
@@ -1308,7 +1315,7 @@ namespace Tgstation.Server.Tests.Live
preStartupTime = DateTimeOffset.UtcNow;
serverTask = server.Run(cancellationToken).AsTask();
long expectedCompileJobId, expectedStaged;
var edgeByond = await ByondTest.GetEdgeVersion(fileDownloader, cancellationToken);
var edgeVersion = await ByondTest.GetEdgeVersion(engineType, fileDownloader, cancellationToken);
using (var adminClient = await CreateAdminClient(server.Url, cancellationToken))
{
var instanceClient = adminClient.Instances.CreateClient(instance);
@@ -1319,7 +1326,7 @@ namespace Tgstation.Server.Tests.Live
Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value);
var compileJob = await instanceClient.DreamMaker.Compile(cancellationToken);
var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort);
var wdt = new WatchdogTest(edgeVersion, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort);
await wdt.WaitForJob(compileJob, 30, false, null, cancellationToken);
dd = await instanceClient.DreamDaemon.Read(cancellationToken);
@@ -1366,7 +1373,7 @@ namespace Tgstation.Server.Tests.Live
Assert.AreEqual(WatchdogStatus.Online, currentDD.Status);
Assert.AreEqual(expectedStaged, currentDD.StagedCompileJob.Job.Id.Value);
var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort);
var wdt = new WatchdogTest(edgeVersion, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort);
currentDD = await wdt.TellWorldToReboot(cancellationToken);
Assert.AreEqual(expectedStaged, currentDD.ActiveCompileJob.Job.Id.Value);
Assert.IsNull(currentDD.StagedCompileJob);
+1 -1
View File
@@ -134,7 +134,7 @@ namespace Tgstation.Server.Tests
{
new Host.Models.InstancePermissionSet
{
ByondRights = ByondRights.InstallCustomVersion,
ByondRights = ByondRights.InstallCustomByondVersion,
ChatBotRights = ChatBotRights.None,
ConfigurationRights = ConfigurationRights.Read,
DreamDaemonRights = DreamDaemonRights.ReadRevision,
+36 -8
View File
@@ -28,6 +28,8 @@ using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.System;
using System.Net;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Tests
{
@@ -125,12 +127,18 @@ namespace Tgstation.Server.Tests
const string ArchiveEntryPath = "byond/bin/dd.exe";
var hasEntry = ArchiveHasFileEntry(
await byondInstaller.DownloadVersion(WindowsByondInstaller.DDExeVersion, default),
await byondInstaller.DownloadVersion(
new ByondVersion
{
Engine = EngineType.Byond,
Version = WindowsByondInstaller.DDExeVersion
},
default),
ArchiveEntryPath);
Assert.IsTrue(hasEntry);
var (byondBytes, version) = await GetByondVersionPriorTo(byondInstaller, WindowsByondInstaller.DDExeVersion);
var (byondBytes, _) = await GetByondVersionPriorTo(byondInstaller, WindowsByondInstaller.DDExeVersion);
hasEntry = ArchiveHasFileEntry(
byondBytes,
ArchiveEntryPath);
@@ -203,8 +211,18 @@ namespace Tgstation.Server.Tests
try
{
await TestMapThreadsVersion(
ByondInstallerBase.MapThreadsVersion,
await byondInstaller.DownloadVersion(ByondInstallerBase.MapThreadsVersion, default),
new ByondVersion
{
Engine = EngineType.Byond,
Version = ByondInstallerBase.MapThreadsVersion,
},
await byondInstaller.DownloadVersion(
new ByondVersion
{
Engine = EngineType.Byond,
Version = ByondInstallerBase.MapThreadsVersion
},
default),
byondInstaller,
ioManager,
processExecutor,
@@ -380,22 +398,32 @@ namespace Tgstation.Server.Tests
Assert.AreEqual(latestMigrationSL, DatabaseContext.SLLatestMigration);
}
static async Task<Tuple<MemoryStream, Version>> GetByondVersionPriorTo(IByondInstaller byondInstaller, Version version)
static async Task<Tuple<MemoryStream, ByondVersion>> GetByondVersionPriorTo(IByondInstaller byondInstaller, Version version)
{
var minusOneMinor = new Version(version.Major, version.Minor - 1);
var byondVersion = new ByondVersion
{
Engine = EngineType.Byond,
Version = minusOneMinor
};
try
{
return Tuple.Create(await byondInstaller.DownloadVersion(minusOneMinor, default), minusOneMinor);
return Tuple.Create(await byondInstaller.DownloadVersion(
byondVersion,
CancellationToken.None), byondVersion);
}
catch (HttpRequestException)
{
var minusOneMajor = new Version(minusOneMinor.Major - 1, minusOneMinor.Minor);
return Tuple.Create(await byondInstaller.DownloadVersion(minusOneMajor, default), minusOneMajor);
byondVersion.Version = minusOneMajor;
return Tuple.Create(await byondInstaller.DownloadVersion(
byondVersion,
CancellationToken.None), byondVersion);
}
}
static async Task TestMapThreadsVersion(
Version byondVersion,
ByondVersion byondVersion,
Stream byondBytes,
IByondInstaller byondInstaller,
IIOManager ioManager,