Setup dependency injection for mapping IEngineInstallers and global OD repo manager

Various other things to get it compiling
This commit is contained in:
Jordan Dominion
2023-10-12 21:47:14 -04:00
parent 90d516cfb9
commit f00cb55fc3
16 changed files with 421 additions and 167 deletions
@@ -25,6 +25,7 @@ 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.Configuration;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
@@ -69,6 +70,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// </summary>
readonly IAssemblyInformationProvider assemblyInformationProvider;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="DiscordProvider"/>.
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// The <see cref="ServiceProvider"/> containing Discord services.
/// </summary>
@@ -126,63 +132,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <returns>The normalized mention <see cref="string"/>.</returns>
static string NormalizeMentions(string fromDiscord) => fromDiscord.Replace("<@!", "<@", StringComparison.Ordinal);
/// <summary>
/// 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 <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,
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>
{
engineField,
new EmbedField(
"Local Commit",
localCommitPushed && gitHub
? $"[{revisionInformation.CommitSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.CommitSha})"
: revisionInformation.CommitSha[..7],
true),
new EmbedField(
"Branch Commit",
gitHub
? $"[{revisionInformation.OriginCommitSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.OriginCommitSha})"
: revisionInformation.OriginCommitSha[..7],
true),
};
fields.AddRange((revisionInformation.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>())
.Select(x => x.TestMerge)
.Select(x => new EmbedField(
$"#{x.Number}",
$"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.TargetCommitSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.TargetCommitSha}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}",
false)));
return fields;
}
/// <summary>
/// Initializes a new instance of the <see cref="DiscordProvider"/> class.
/// </summary>
@@ -191,15 +140,18 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="Provider"/>.</param>
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
/// <param name="chatBot">The <see cref="ChatBot"/> for the <see cref="Provider"/>.</param>
/// <param name="generalConfiguration">The value of <see cref="generalConfiguration"/>.</param>
public DiscordProvider(
IJobManager jobManager,
IAsyncDelayer asyncDelayer,
ILogger<DiscordProvider> logger,
IAssemblyInformationProvider assemblyInformationProvider,
ChatBot chatBot)
ChatBot chatBot,
GeneralConfiguration generalConfiguration)
: base(jobManager, asyncDelayer, logger, chatBot)
{
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration));
mappedChannels = new List<ulong>();
connectDisconnectLock = new object();
@@ -903,12 +855,69 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
return allAccessibleChannels;
}
/// <summary>
/// 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 <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>
List<IEmbedField> BuildUpdateEmbedFields(
Models.RevisionInformation revisionInformation,
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]}]({generalConfiguration.OpenDreamGitUrl}/commit/{revisionInformation.CommitSha})",
true),
_ => throw new InvalidOperationException($"Invaild EngineType: {byondVersion.Engine.Value}"),
};
var fields = new List<IEmbedField>
{
engineField,
new EmbedField(
"Local Commit",
localCommitPushed && gitHub
? $"[{revisionInformation.CommitSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.CommitSha})"
: revisionInformation.CommitSha[..7],
true),
new EmbedField(
"Branch Commit",
gitHub
? $"[{revisionInformation.OriginCommitSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.OriginCommitSha})"
: revisionInformation.OriginCommitSha[..7],
true),
};
fields.AddRange((revisionInformation.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>())
.Select(x => x.TestMerge)
.Select(x => new EmbedField(
$"#{x.Number}",
$"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.TargetCommitSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.TargetCommitSha}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}",
false)));
return fields;
}
/// <summary>
/// Convert a <see cref="ChatEmbed"/> to an <see cref="IEmbed"/> parameters.
/// </summary>
/// <param name="embed">The <see cref="ChatEmbed"/> to convert.</param>
/// <returns>The parameter for sending a single <see cref="IEmbed"/>.</returns>
#pragma warning disable CA1502
#pragma warning disable CA1502
Optional<IReadOnlyList<IEmbed>> ConvertEmbed(ChatEmbed embed)
{
if (embed == null)
@@ -2,8 +2,10 @@
using System.Globalization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.System;
using Tgstation.Server.Host.Utils;
@@ -33,6 +35,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// </summary>
readonly ILoggerFactory loggerFactory;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="ProviderFactory"/>.
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// Initializes a new instance of the <see cref="ProviderFactory"/> class.
/// </summary>
@@ -40,16 +47,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
public ProviderFactory(
IJobManager jobManager,
IAssemblyInformationProvider assemblyInformationProvider,
IAsyncDelayer asyncDelayer,
ILoggerFactory loggerFactory)
ILoggerFactory loggerFactory,
IOptions<GeneralConfiguration> generalConfigurationOptions)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
}
/// <inheritdoc />
@@ -69,7 +79,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
asyncDelayer,
loggerFactory.CreateLogger<DiscordProvider>(),
assemblyInformationProvider,
settings),
settings,
generalConfiguration),
_ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider)),
};
}
@@ -864,39 +864,29 @@ namespace Tgstation.Server.Host.Components.Deployment
async ValueTask<bool> RunDreamMaker(IEngineExecutableLock engineLock, Models.CompileJob job, CancellationToken cancellationToken)
{
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);
await using var dm = processExecutor.LaunchProcess(
engineLock.CompilerExePath,
ioManager.ResolvePath(
job.DirectoryName.ToString()),
arguments,
readStandardHandles: true,
noShellExecute: true);
int exitCode;
using (cancellationToken.Register(() => dm.Terminate()))
exitCode = (await dm.Lifetime).Value;
cancellationToken.ThrowIfCancellationRequested();
if (sessionConfiguration.LowPriorityDeploymentProcesses)
dm.AdjustPriority(false);
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;
}
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);
currentDreamMakerOutput = job.Output;
return result;
return exitCode == 0;
}
/// <summary>
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Host.Jobs;
namespace Tgstation.Server.Host.Components.Engine
{
/// <summary>
/// Implementation of <see cref="IEngineInstaller"/> that forwards calls to different <see cref="IEngineInstaller"/> based on their appropriate <see cref="EngineType"/>.
/// </summary>
sealed class DelegatingEngineInstaller : IEngineInstaller
{
/// <summary>
/// The <see cref="IReadOnlyDictionary{TKey, TValue}"/> mapping <see cref="EngineType"/>s to their appropriate <see cref="IEngineInstaller"/>.
/// </summary>
readonly IReadOnlyDictionary<EngineType, IEngineInstaller> delegatedInstallers;
/// <summary>
/// Initializes a new instance of the <see cref="DelegatingEngineInstaller"/> class.
/// </summary>
/// <param name="delegatedInstallers">The value of <see cref="delegatedInstallers"/>.</param>
public DelegatingEngineInstaller(IReadOnlyDictionary<EngineType, IEngineInstaller> delegatedInstallers)
{
this.delegatedInstallers = delegatedInstallers ?? throw new ArgumentNullException(nameof(delegatedInstallers));
}
/// <inheritdoc />
public Task CleanCache(CancellationToken cancellationToken)
=> Task.WhenAll(delegatedInstallers.Values.Select(installer => installer.CleanCache(cancellationToken)));
/// <inheritdoc />
public IEngineInstallation CreateInstallation(ByondVersion version, Task installationTask)
=> DelegateCall(version, installer => installer.CreateInstallation(version, installationTask));
/// <inheritdoc />
public ValueTask<IEngineInstallationData> DownloadVersion(ByondVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken)
=> DelegateCall(version, installer => installer.DownloadVersion(version, jobProgressReporter, cancellationToken));
/// <inheritdoc />
public ValueTask Install(ByondVersion version, string path, CancellationToken cancellationToken)
=> DelegateCall(version, installer => installer.Install(version, path, cancellationToken));
/// <inheritdoc />
public ValueTask TrustDmbPath(string fullDmbPath, CancellationToken cancellationToken)
=> ValueTaskExtensions.WhenAll(delegatedInstallers.Values.Select(installer => installer.TrustDmbPath(fullDmbPath, cancellationToken)));
/// <inheritdoc />
public ValueTask UpgradeInstallation(ByondVersion version, string path, CancellationToken cancellationToken)
=> DelegateCall(version, installer => installer.UpgradeInstallation(version, path, cancellationToken));
/// <summary>
/// Delegate a given <paramref name="call"/> to its appropriate <see cref="IEngineInstaller"/>.
/// </summary>
/// <typeparam name="TReturn">The return <see cref="Type"/> of the call.</typeparam>
/// <param name="version">The <see cref="ByondVersion"/> used to perform delegate selection.</param>
/// <param name="call">The <see cref="Func{T, TResult}"/> that will be called with the correct <see cref="IEngineInstaller"/> based on <paramref name="version"/>.</param>
/// <returns>The <typeparamref name="TReturn"/> value of the delegated call.</returns>
TReturn DelegateCall<TReturn>(ByondVersion version, Func<IEngineInstaller, TReturn> call)
{
ArgumentNullException.ThrowIfNull(version);
return call(delegatedInstallers[version.Engine.Value]);
}
}
}
@@ -59,7 +59,7 @@ namespace Tgstation.Server.Host.Components.Engine
/// 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>
/// <returns>The formatted arguments <see cref="string"/>.</returns>
string FormatCompilerArguments(string dmePath);
}
}
@@ -54,6 +54,10 @@ namespace Tgstation.Server.Host.Components.Engine
/// <inheritdoc />
public string FormatServerArguments(IDmbProvider dmbProvider, IReadOnlyDictionary<string, string> parameters, DreamDaemonLaunchParameters launchParameters, string logFilePath)
{
ArgumentNullException.ThrowIfNull(dmbProvider);
ArgumentNullException.ThrowIfNull(parameters);
ArgumentNullException.ThrowIfNull(launchParameters);
throw new NotImplementedException();
}
@@ -61,7 +65,7 @@ namespace Tgstation.Server.Host.Components.Engine
public string FormatCompilerArguments(string dmePath)
{
ArgumentNullException.ThrowIfNull(dmePath);
return null;
throw new NotImplementedException();
}
}
}
@@ -8,6 +8,7 @@ using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Host.Common;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Configuration;
@@ -120,14 +121,20 @@ namespace Tgstation.Server.Host.Components.Engine
var progressSection2 = jobProgressReporter.CreateSection("Checking out OpenDream version", 0.5f);
var committish = version.SourceCommittish;
if (committish == null)
committish = $"{generalConfiguration.OpenDreamGitTagPrefix}{version.Version.Semver()}";
await repo.CheckoutObject(
version.SourceCommittish,
committish,
null,
null,
true,
progressSection2,
cancellationToken);
version.SourceCommittish = repo.Head;
return new RepositoryEngineInstallationData(IOManager, repo, InstallationRepositorySubDirectory);
}
catch
@@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Components.Events
{
/// <summary>
/// No-op implementation of <see cref="IEventConsumer"/>.
/// </summary>
sealed class NoopEventConsumer : IEventConsumer
{
/// <inheritdoc />
public ValueTask HandleEvent(EventType eventType, IEnumerable<string> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
}
}
@@ -111,14 +111,9 @@ namespace Tgstation.Server.Host.Components
readonly IPlatformIdentifier platformIdentifier;
/// <summary>
/// The <see cref="ILibGit2RepositoryFactory"/> for the <see cref="InstanceFactory"/>.
/// The <see cref="IRepositoryManagerFactory"/> for the <see cref=" InstanceFactory"/>.
/// </summary>
readonly ILibGit2RepositoryFactory repositoryFactory;
/// <summary>
/// The <see cref="ILibGit2Commands"/> for the <see cref="InstanceFactory"/>.
/// </summary>
readonly ILibGit2Commands repositoryCommands;
readonly IRepositoryManagerFactory repositoryManagerFactory;
/// <summary>
/// The <see cref="IServerPortProvider"/> for the <see cref="InstanceFactory"/>.
@@ -130,11 +125,6 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IFileTransferTicketProvider fileTransferService;
/// <summary>
/// The <see cref="IGitRemoteFeaturesFactory"/> for the <see cref="InstanceFactory"/>.
/// </summary>
readonly IGitRemoteFeaturesFactory gitRemoteFeaturesFactory;
/// <summary>
/// The <see cref="IRemoteDeploymentManagerFactory"/> for the <see cref="InstanceFactory"/>.
/// </summary>
@@ -182,11 +172,9 @@ namespace Tgstation.Server.Host.Components
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
/// <param name="networkPromptReaper">The value of <see cref="networkPromptReaper"/>.</param>
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/>.</param>
/// <param name="repositoryFactory">The value of <see cref="repositoryFactory"/>.</param>
/// <param name="repositoryCommands">The value of <see cref="repositoryCommands"/>.</param>
/// <param name="repositoryManagerFactory">The value of <see cref="repositoryManagerFactory"/>.</param>
/// <param name="serverPortProvider">The value of <see cref="serverPortProvider"/>.</param>
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param>
/// <param name="gitRemoteFeaturesFactory">The value of <see cref="gitRemoteFeaturesFactory"/>.</param>
/// <param name="remoteDeploymentManagerFactory">The value of <see cref="remoteDeploymentManagerFactory"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
@@ -208,11 +196,9 @@ namespace Tgstation.Server.Host.Components
IJobManager jobManager,
INetworkPromptReaper networkPromptReaper,
IPlatformIdentifier platformIdentifier,
ILibGit2RepositoryFactory repositoryFactory,
ILibGit2Commands repositoryCommands,
IRepositoryManagerFactory repositoryManagerFactory,
IServerPortProvider serverPortProvider,
IFileTransferTicketProvider fileTransferService,
IGitRemoteFeaturesFactory gitRemoteFeaturesFactory,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IAsyncDelayer asyncDelayer,
IOptions<GeneralConfiguration> generalConfigurationOptions,
@@ -234,11 +220,9 @@ namespace Tgstation.Server.Host.Components
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper));
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory));
this.repositoryCommands = repositoryCommands ?? throw new ArgumentNullException(nameof(repositoryCommands));
this.repositoryManagerFactory = repositoryManagerFactory ?? throw new ArgumentNullException(nameof(repositoryManagerFactory));
this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider));
this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory));
this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
@@ -284,19 +268,10 @@ namespace Tgstation.Server.Host.Components
generalConfiguration,
sessionConfiguration);
var eventConsumer = new EventConsumer(configuration);
var repoManager = new RepositoryManager(
repositoryFactory,
repositoryCommands,
repoIoManager,
eventConsumer,
postWriteHandler,
gitRemoteFeaturesFactory,
loggerFactory.CreateLogger<Repository.Repository>(),
loggerFactory.CreateLogger<RepositoryManager>(),
generalConfiguration);
var repoManager = repositoryManagerFactory.CreateRepositoryManager(repoIoManager, eventConsumer);
try
{
var byond = new EngineManager(byondIOManager, engineInstaller, eventConsumer, loggerFactory.CreateLogger<EngineManager>());
var engineManager = new EngineManager(byondIOManager, engineInstaller, eventConsumer, loggerFactory.CreateLogger<EngineManager>());
var dmbFactory = new DmbFactory(
databaseContextFactory,
@@ -307,7 +282,7 @@ namespace Tgstation.Server.Host.Components
metadata);
try
{
var commandFactory = new CommandFactory(assemblyInformationProvider, byond, repoManager, databaseContextFactory, dmbFactory, metadata);
var commandFactory = new CommandFactory(assemblyInformationProvider, engineManager, repoManager, databaseContextFactory, dmbFactory, metadata);
var chatManager = chatFactory.CreateChatManager(commandFactory, metadata.ChatSettings);
try
@@ -321,7 +296,7 @@ namespace Tgstation.Server.Host.Components
var sessionControllerFactory = new SessionControllerFactory(
processExecutor,
byond,
engineManager,
topicClientFactory,
cryptographySuite,
assemblyInformationProvider,
@@ -357,7 +332,7 @@ namespace Tgstation.Server.Host.Components
Instance instance = null;
var dreamMaker = new DreamMaker(
byond,
engineManager,
gameIoManager,
configuration,
sessionControllerFactory,
@@ -375,7 +350,7 @@ namespace Tgstation.Server.Host.Components
instance = new Instance(
metadata,
repoManager,
byond,
engineManager,
dreamMaker,
watchdog,
chatManager,
@@ -417,18 +392,12 @@ namespace Tgstation.Server.Host.Components
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken)
{
CheckSystemCompatibility();
return engineInstaller.CleanCache(cancellationToken);
}
=> Task.WhenAll(
repositoryManagerFactory.StartAsync(cancellationToken),
engineInstaller.CleanCache(cancellationToken));
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <summary>
/// Test that the <see cref="repositoryFactory"/> is functional.
/// </summary>
void CheckSystemCompatibility() => repositoryFactory.CreateInMemory();
public Task StopAsync(CancellationToken cancellationToken) => repositoryManagerFactory.StopAsync(cancellationToken);
/// <summary>
/// Create the <see cref="IIOManager"/> for a given set of instance <paramref name="metadata"/>.
@@ -0,0 +1,19 @@
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Components.Repository
{
/// <summary>
/// Factory for creating <see cref="IRepositoryManager"/>s.
/// </summary>
interface IRepositoryManagerFactory : IComponentService
{
/// <summary>
/// Create a <see cref="IRepositoryManager"/>.
/// </summary>
/// <param name="ioManager">The <see cref="IIOManager"/> to use.</param>
/// <param name="eventConsumer">The <see cref="IEventConsumer"/> to use.</param>
/// <returns>A new <see cref="IRepositoryManager"/>.</returns>
IRepositoryManager CreateRepositoryManager(IIOManager ioManager, IEventConsumer eventConsumer);
}
}
@@ -0,0 +1,100 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Components.Repository
{
/// <inheritdoc />
sealed class RepostoryManagerFactory : IRepositoryManagerFactory
{
/// <summary>
/// The <see cref="ILibGit2RepositoryFactory"/> for the <see cref="RepostoryManagerFactory"/>.
/// </summary>
readonly ILibGit2RepositoryFactory repositoryFactory;
/// <summary>
/// The <see cref="ILibGit2Commands"/> for the <see cref="RepostoryManagerFactory"/>.
/// </summary>
readonly ILibGit2Commands repositoryCommands;
/// <summary>
/// The <see cref="IPostWriteHandler"/> for the <see cref="RepostoryManagerFactory"/>.
/// </summary>
readonly IPostWriteHandler postWriteHandler;
/// <summary>
/// The <see cref="IGitRemoteFeaturesFactory"/> for the <see cref="RepostoryManagerFactory"/>.
/// </summary>
readonly IGitRemoteFeaturesFactory gitRemoteFeaturesFactory;
/// <summary>
/// The <see cref="ILoggerFactory"/> for the <see cref="RepostoryManagerFactory"/>.
/// </summary>
readonly ILoggerFactory loggerFactory;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="RepostoryManagerFactory"/>.
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// Initializes a new instance of the <see cref="RepostoryManagerFactory"/> class.
/// </summary>
/// <param name="repositoryFactory">The value of <see cref="repositoryFactory"/>.</param>
/// <param name="repositoryCommands">The value of <see cref="repositoryCommands"/>.</param>
/// <param name="postWriteHandler">The value of <see cref="postWriteHandler"/>.</param>
/// <param name="gitRemoteFeaturesFactory">The value of <see cref="gitRemoteFeaturesFactory"/>.</param>
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
public RepostoryManagerFactory(
ILibGit2RepositoryFactory repositoryFactory,
ILibGit2Commands repositoryCommands,
IPostWriteHandler postWriteHandler,
IGitRemoteFeaturesFactory gitRemoteFeaturesFactory,
ILoggerFactory loggerFactory,
IOptions<GeneralConfiguration> generalConfigurationOptions)
{
this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory));
this.repositoryCommands = repositoryCommands ?? throw new ArgumentNullException(nameof(repositoryCommands));
this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory));
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
}
/// <inheritdoc />
public IRepositoryManager CreateRepositoryManager(IIOManager ioManager, IEventConsumer eventConsumer)
=> new RepositoryManager(
repositoryFactory,
repositoryCommands,
ioManager,
eventConsumer,
postWriteHandler,
gitRemoteFeaturesFactory,
loggerFactory.CreateLogger<Repository>(),
loggerFactory.CreateLogger<RepositoryManager>(),
generalConfiguration);
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken)
{
CheckSystemCompatibility();
return Task.CompletedTask;
}
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <summary>
/// Test that the <see cref="repositoryFactory"/> is functional.
/// </summary>
void CheckSystemCompatibility() => repositoryFactory.CreateInMemory();
}
}
@@ -66,6 +66,11 @@ namespace Tgstation.Server.Host.Configuration
/// </summary>
const string DefaultOpenDreamGitUrl = "https://github.com/OpenDreamProject/OpenDream";
/// <summary>
/// The default value for <see cref="OpenDreamGitTagPrefix"/>.
/// </summary>
const string DefaultOpenDreamGitTagPrefix = "v";
/// <summary>
/// The current <see cref="ConfigVersion"/>.
/// </summary>
@@ -132,6 +137,11 @@ namespace Tgstation.Server.Host.Configuration
/// </summary>
public Uri OpenDreamGitUrl { get; set; } = new Uri(DefaultOpenDreamGitUrl);
/// <summary>
/// The prefix to the OpenDream semver as tags appear in the git repository.
/// </summary>
public string OpenDreamGitTagPrefix { get; set; } = DefaultOpenDreamGitTagPrefix;
/// <summary>
/// Initializes a new instance of the <see cref="GeneralConfiguration"/> class.
/// </summary>
@@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging;
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;
@@ -72,7 +73,7 @@ namespace Tgstation.Server.Host.Controllers
}
/// <summary>
/// Gets the active <see cref="Api.Models.Internal.ByondVersion"/>.
/// Gets the active <see cref="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>
@@ -91,7 +92,7 @@ namespace Tgstation.Server.Host.Controllers
: Conflict(new ErrorMessageResponse(ErrorCode.ResourceNotPresent))));
/// <summary>
/// Lists installed <see cref="Api.Models.Internal.ByondVersion"/>s.
/// Lists installed <see cref="ByondVersion"/>s.
/// </summary>
/// <param name="page">The current page.</param>
/// <param name="pageSize">The page size.</param>
@@ -139,15 +140,11 @@ namespace Tgstation.Server.Host.Controllers
#pragma warning restore CA1506
#pragma warning restore CA1502
{
throw new NotImplementedException("Fix OD/BYOND model validation");
ArgumentNullException.ThrowIfNull(model);
var earlyOut = ValidateByondVersion(model);
if (earlyOut != null)
return earlyOut;
var uploadingZip = model.UploadCustomZip == true;
var isByondEngine = model.Engine.Value == EngineType.Byond;
if ((isByondEngine && (model.Version.Revision != -1 || (uploadingZip && model.Version.Build > 0) || model.SourceCommittish != null))
|| (!isByondEngine && (model.Version != null || String.IsNullOrWhiteSpace(model.SourceCommittish))))
return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure));
var userByondRights = AuthenticationContext.InstancePermissionSet.ByondRights.Value;
if ((!userByondRights.HasFlag(ByondRights.InstallOfficialOrChangeActiveByondVersion) && !uploadingZip)
@@ -201,7 +198,7 @@ namespace Tgstation.Server.Host.Controllers
Instance.Id);
// run the install through the job manager
var job = new Job
var job = new Models.Job
{
Description = $"Install {(!uploadingZip ? String.Empty : "custom ")}{model.Engine.Value} version {model.Version}",
StartedBy = AuthenticationContext.User,
@@ -271,7 +268,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="Api.Models.Internal.ByondVersion"/> specified was not installed.</response>
/// <response code="410">The <see cref="ByondVersion"/> specified was not installed.</response>
[HttpDelete]
[TgsAuthorize(ByondRights.DeleteInstall)]
[ProducesResponseType(typeof(JobResponse), 202)]
@@ -279,13 +276,9 @@ namespace Tgstation.Server.Host.Controllers
[ProducesResponseType(typeof(ErrorMessageResponse), 410)]
public async ValueTask<IActionResult> Delete([FromBody] ByondVersionDeleteRequest model, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(model);
if (model.Version.Revision != -1)
return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure));
if (model.Engine == EngineType.Byond)
model.Version = NormalizeByondVersion(model.Version);
var earlyOut = ValidateByondVersion(model);
if (earlyOut != null)
return earlyOut;
var notInstalledResponse = await WithComponentInstance(
instance =>
@@ -310,7 +303,7 @@ namespace Tgstation.Server.Host.Controllers
var isByondVersion = model.Engine.Value == EngineType.Byond;
// run the install through the job manager
var job = new Job
var job = new Models.Job
{
Description = $"Delete installed {model.Engine.Value} version {model.Version}",
StartedBy = AuthenticationContext.User,
@@ -333,5 +326,30 @@ namespace Tgstation.Server.Host.Controllers
var apiResponse = job.ToApi();
return Accepted(apiResponse);
}
/// <summary>
/// Validate and normalize a given <paramref name="version"/>.
/// </summary>
/// <param name="version">The <see cref="ByondVersion"/> to validate and normalize.</param>
/// <returns>The <see cref="BadRequestObjectResult"/> to return, if any.</returns>
BadRequestObjectResult ValidateByondVersion(ByondVersion version)
{
ArgumentNullException.ThrowIfNull(version);
var isByond = version.Engine.Value == EngineType.Byond;
if ((isByond
&& (version.Version == null
|| version.Version.Revision != -1
|| version.SourceCommittish != null))
|| (version.Engine.Value == EngineType.OpenDream &&
((version.SourceCommittish == null && version.Version == null)
|| (version.Version != null && (version.Version.Revision != -1 || version.Version.Build == -1 || version.SourceCommittish == null)))))
return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure));
if (isByond)
version.Version = NormalizeByondVersion(version.Version);
return null;
}
}
}
+28 -2
View File
@@ -27,11 +27,13 @@ using Serilog.Formatting.Display;
using Serilog.Sinks.Elasticsearch;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Components;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Deployment.Remote;
using Tgstation.Server.Host.Components.Engine;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Components.Interop.Bridge;
using Tgstation.Server.Host.Components.Repository;
@@ -332,7 +334,7 @@ namespace Tgstation.Server.Host.Core
AddWatchdog<WindowsWatchdogFactory>(services, postSetupServices);
services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
services.AddSingleton<IEngineInstaller, WindowsByondInstaller>();
services.AddSingleton<ByondInstallerBase, WindowsByondInstaller>();
services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
services.AddSingleton<IProcessFeatures, WindowsProcessFeatures>();
@@ -345,7 +347,7 @@ namespace Tgstation.Server.Host.Core
AddWatchdog<PosixWatchdogFactory>(services, postSetupServices);
services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
services.AddSingleton<IEngineInstaller, PosixByondInstaller>();
services.AddSingleton<ByondInstallerBase, PosixByondInstaller>();
services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
services.AddSingleton<IProcessFeatures, PosixProcessFeatures>();
@@ -357,6 +359,29 @@ namespace Tgstation.Server.Host.Core
services.AddHostedService<PosixSignalHandler>();
}
// only global repo manager should be for the OD repo
var openDreamRepositoryDirectory = ioManager.ConcatPath(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
assemblyInformationProvider.VersionPrefix,
"OpenDreamRepository");
services.AddSingleton(
services => services
.GetRequiredService<IRepositoryManagerFactory>()
.CreateRepositoryManager(
new ResolvingIOManager(
services.GetRequiredService<IIOManager>(),
openDreamRepositoryDirectory),
new NoopEventConsumer()));
services.AddSingleton<OpenDreamInstaller>();
services.AddSingleton(
serviceProvider => new Dictionary<EngineType, IEngineInstaller>
{
{ EngineType.Byond, serviceProvider.GetRequiredService<ByondInstallerBase>() },
{ EngineType.OpenDream, serviceProvider.GetRequiredService<OpenDreamInstaller>() },
});
services.AddSingleton<IEngineInstaller, DelegatingEngineInstaller>();
if (postSetupServices.InternalConfiguration.UsingSystemD)
services.AddHostedService<SystemDManager>();
@@ -378,6 +403,7 @@ namespace Tgstation.Server.Host.Core
services.AddSingleton<IGitRemoteFeaturesFactory, GitRemoteFeaturesFactory>();
services.AddSingleton<ILibGit2RepositoryFactory, LibGit2RepositoryFactory>();
services.AddSingleton<ILibGit2Commands, LibGit2Commands>();
services.AddSingleton<IRepositoryManagerFactory, RepostoryManagerFactory>();
services.AddSingleton<IRemoteDeploymentManagerFactory, RemoteDeploymentManagerFactory>();
services.AddChatProviderFactory();
services.AddSingleton<IChatManagerFactory, ChatManagerFactory>();
@@ -16,6 +16,8 @@ General:
HostApiDocumentation: false # Make HTTP API documentation available at /swagger/v1/swagger.json
SkipAddingByondFirewallException: false # Windows Only: Prevent running netsh.exe to add a firewall exception for installed DreamDaemon binaries
DeploymentDirectoryCopyTasksPerCore: 100 # Maximum number of concurrent file copy operations PER available CPU core
OpenDreamGitUrl: https://github.com/OpenDreamProject/OpenDream # The repository to retrieve OpenDream from
OpenDreamGitTagPrefix: v # The prefix to the OpenDream semver as tags appear in the git repository
Session:
HighPriorityLiveDreamDaemon: false # If DreamDaemon instances should run as higher priority processes
LowPriorityDeploymentProcesses: true # If TGS Deployments should run as lower priority processes
@@ -8,6 +8,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.System;
@@ -52,15 +53,17 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
ReconnectionInterval = 1,
};
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(null, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockJobManager, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(null, null, null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockJobManager, null, null, null, null, null));
var mockDel = Mock.Of<IAsyncDelayer>();
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockJobManager, mockDel, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockJobManager, mockDel, null, null, null, null));
var mockLogger = Mock.Of<ILogger<DiscordProvider>>();
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, null, null, null));
var mockAss = Mock.Of<IAssemblyInformationProvider>();
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, null));
await new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, bot).DisposeAsync();
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, bot, null));
var mockGen = new GeneralConfiguration();
await new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, bot, mockGen).DisposeAsync();
}
static ValueTask InvokeConnect(IProvider provider, CancellationToken cancellationToken = default) => (ValueTask)provider.GetType().GetMethod("Connect", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(provider, new object[] { cancellationToken });
@@ -73,7 +76,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
{
ReconnectionInterval = 1,
ConnectionString = "asdf"
});
}, new GeneralConfiguration());
await Assert.ThrowsExceptionAsync<JobException>(async () => await InvokeConnect(provider));
Assert.IsFalse(provider.Connected);
}
@@ -88,7 +91,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
Assert.Fail("TGS_TEST_DISCORD_TOKEN is not a valid Discord connection string!");
var mockLogger = new Mock<ILogger<DiscordProvider>>();
await using var provider = new DiscordProvider(mockJobManager, Mock.Of<IAsyncDelayer>(), mockLogger.Object, Mock.Of<IAssemblyInformationProvider>(), testToken1);
await using var provider = new DiscordProvider(mockJobManager, Mock.Of<IAsyncDelayer>(), mockLogger.Object, Mock.Of<IAssemblyInformationProvider>(), testToken1, new GeneralConfiguration());
Assert.IsFalse(provider.Connected);
await InvokeConnect(provider);
Assert.IsTrue(provider.Connected);