diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 492eb8e332..013f22fc36 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -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 /// readonly IAssemblyInformationProvider assemblyInformationProvider; + /// + /// The for the . + /// + readonly GeneralConfiguration generalConfiguration; + /// /// The containing Discord services. /// @@ -126,63 +132,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// The normalized mention . static string NormalizeMentions(string fromDiscord) => fromDiscord.Replace("<@!", "<@", StringComparison.Ordinal); - /// - /// Create a of s for a discord update embed. - /// - /// The of the deployment. - /// The of the deployment. - /// The repository GitHub owner, if any. - /// The repository GitHub name, if any. - /// if the local deployment commit was pushed to the remote repository. - /// A new of s to use. - static List 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 - { - 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()) - .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; - } - /// /// Initializes a new instance of the class. /// @@ -191,15 +140,18 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// The for the . /// The value of . /// The for the . + /// The value of . public DiscordProvider( IJobManager jobManager, IAsyncDelayer asyncDelayer, ILogger 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(); connectDisconnectLock = new object(); @@ -903,12 +855,69 @@ namespace Tgstation.Server.Host.Components.Chat.Providers return allAccessibleChannels; } + /// + /// Create a of s for a discord update embed. + /// + /// The of the deployment. + /// The of the deployment. + /// The repository GitHub owner, if any. + /// The repository GitHub name, if any. + /// if the local deployment commit was pushed to the remote repository. + /// A new of s to use. + List 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 + { + 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()) + .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; + } + /// /// Convert a to an parameters. /// /// The to convert. /// The parameter for sending a single . - #pragma warning disable CA1502 +#pragma warning disable CA1502 Optional> ConvertEmbed(ChatEmbed embed) { if (embed == null) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs index eeed8c1583..ed3608b92d 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs @@ -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 /// readonly ILoggerFactory loggerFactory; + /// + /// The for the . + /// + readonly GeneralConfiguration generalConfiguration; + /// /// Initializes a new instance of the class. /// @@ -40,16 +47,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// The value of . /// The value of . /// The value of . + /// The containing the value of . public ProviderFactory( IJobManager jobManager, IAssemblyInformationProvider assemblyInformationProvider, IAsyncDelayer asyncDelayer, - ILoggerFactory loggerFactory) + ILoggerFactory loggerFactory, + IOptions 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)); } /// @@ -69,7 +79,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers asyncDelayer, loggerFactory.CreateLogger(), assemblyInformationProvider, - settings), + settings, + generalConfiguration), _ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider)), }; } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 6a5f524225..8dd925f812 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -864,39 +864,29 @@ namespace Tgstation.Server.Host.Components.Deployment async ValueTask 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; } /// diff --git a/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs new file mode 100644 index 0000000000..19fde5df73 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs @@ -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 +{ + /// + /// Implementation of that forwards calls to different based on their appropriate . + /// + sealed class DelegatingEngineInstaller : IEngineInstaller + { + /// + /// The mapping s to their appropriate . + /// + readonly IReadOnlyDictionary delegatedInstallers; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public DelegatingEngineInstaller(IReadOnlyDictionary delegatedInstallers) + { + this.delegatedInstallers = delegatedInstallers ?? throw new ArgumentNullException(nameof(delegatedInstallers)); + } + + /// + public Task CleanCache(CancellationToken cancellationToken) + => Task.WhenAll(delegatedInstallers.Values.Select(installer => installer.CleanCache(cancellationToken))); + + /// + public IEngineInstallation CreateInstallation(ByondVersion version, Task installationTask) + => DelegateCall(version, installer => installer.CreateInstallation(version, installationTask)); + + /// + public ValueTask DownloadVersion(ByondVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken) + => DelegateCall(version, installer => installer.DownloadVersion(version, jobProgressReporter, cancellationToken)); + + /// + public ValueTask Install(ByondVersion version, string path, CancellationToken cancellationToken) + => DelegateCall(version, installer => installer.Install(version, path, cancellationToken)); + + /// + public ValueTask TrustDmbPath(string fullDmbPath, CancellationToken cancellationToken) + => ValueTaskExtensions.WhenAll(delegatedInstallers.Values.Select(installer => installer.TrustDmbPath(fullDmbPath, cancellationToken))); + + /// + public ValueTask UpgradeInstallation(ByondVersion version, string path, CancellationToken cancellationToken) + => DelegateCall(version, installer => installer.UpgradeInstallation(version, path, cancellationToken)); + + /// + /// Delegate a given to its appropriate . + /// + /// The return of the call. + /// The used to perform delegate selection. + /// The that will be called with the correct based on . + /// The value of the delegated call. + TReturn DelegateCall(ByondVersion version, Func call) + { + ArgumentNullException.ThrowIfNull(version); + return call(delegatedInstallers[version.Engine.Value]); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs index 75ebbab4c8..238b4fea11 100644 --- a/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs @@ -59,7 +59,7 @@ namespace Tgstation.Server.Host.Components.Engine /// Return the command line arguments for compiling a given if compilation is necessary. /// /// The full path to the .dme to compile. - /// An arguments if compilation is required, otherwise. + /// The formatted arguments . string FormatCompilerArguments(string dmePath); } } diff --git a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs index cdff39ed41..5a3d04d6d2 100644 --- a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs +++ b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs @@ -54,6 +54,10 @@ namespace Tgstation.Server.Host.Components.Engine /// public string FormatServerArguments(IDmbProvider dmbProvider, IReadOnlyDictionary 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(); } } } diff --git a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs index 976131743c..9eb60f0dfe 100644 --- a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs @@ -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 diff --git a/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs new file mode 100644 index 0000000000..a8b07e4d7e --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components.Events +{ + /// + /// No-op implementation of . + /// + sealed class NoopEventConsumer : IEventConsumer + { + /// + public ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) + => ValueTask.CompletedTask; + } +} diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 0199f1de08..e45052ecd1 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -111,14 +111,9 @@ namespace Tgstation.Server.Host.Components readonly IPlatformIdentifier platformIdentifier; /// - /// The for the . + /// The for the . /// - readonly ILibGit2RepositoryFactory repositoryFactory; - - /// - /// The for the . - /// - readonly ILibGit2Commands repositoryCommands; + readonly IRepositoryManagerFactory repositoryManagerFactory; /// /// The for the . @@ -130,11 +125,6 @@ namespace Tgstation.Server.Host.Components /// readonly IFileTransferTicketProvider fileTransferService; - /// - /// The for the . - /// - readonly IGitRemoteFeaturesFactory gitRemoteFeaturesFactory; - /// /// The for the . /// @@ -182,11 +172,9 @@ namespace Tgstation.Server.Host.Components /// The value of . /// The value of . /// The value of . - /// The value of . - /// The value of . + /// The value of . /// The value of . /// The value of . - /// The value of . /// The value of . /// The value of . /// The containing the value of . @@ -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 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(), - loggerFactory.CreateLogger(), - generalConfiguration); + var repoManager = repositoryManagerFactory.CreateRepositoryManager(repoIoManager, eventConsumer); try { - var byond = new EngineManager(byondIOManager, engineInstaller, eventConsumer, loggerFactory.CreateLogger()); + var engineManager = new EngineManager(byondIOManager, engineInstaller, eventConsumer, loggerFactory.CreateLogger()); 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 /// public Task StartAsync(CancellationToken cancellationToken) - { - CheckSystemCompatibility(); - return engineInstaller.CleanCache(cancellationToken); - } + => Task.WhenAll( + repositoryManagerFactory.StartAsync(cancellationToken), + engineInstaller.CleanCache(cancellationToken)); /// - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - /// - /// Test that the is functional. - /// - void CheckSystemCompatibility() => repositoryFactory.CreateInMemory(); + public Task StopAsync(CancellationToken cancellationToken) => repositoryManagerFactory.StopAsync(cancellationToken); /// /// Create the for a given set of instance . diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepositoryManagerFactory.cs b/src/Tgstation.Server.Host/Components/Repository/IRepositoryManagerFactory.cs new file mode 100644 index 0000000000..0e8c54c70e --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/IRepositoryManagerFactory.cs @@ -0,0 +1,19 @@ +using Tgstation.Server.Host.Components.Events; +using Tgstation.Server.Host.IO; + +namespace Tgstation.Server.Host.Components.Repository +{ + /// + /// Factory for creating s. + /// + interface IRepositoryManagerFactory : IComponentService + { + /// + /// Create a . + /// + /// The to use. + /// The to use. + /// A new . + IRepositoryManager CreateRepositoryManager(IIOManager ioManager, IEventConsumer eventConsumer); + } +} diff --git a/src/Tgstation.Server.Host/Components/Repository/RepostoryManagerFactory.cs b/src/Tgstation.Server.Host/Components/Repository/RepostoryManagerFactory.cs new file mode 100644 index 0000000000..cba66ff69a --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/RepostoryManagerFactory.cs @@ -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 +{ + /// + sealed class RepostoryManagerFactory : IRepositoryManagerFactory + { + /// + /// The for the . + /// + readonly ILibGit2RepositoryFactory repositoryFactory; + + /// + /// The for the . + /// + readonly ILibGit2Commands repositoryCommands; + + /// + /// The for the . + /// + readonly IPostWriteHandler postWriteHandler; + + /// + /// The for the . + /// + readonly IGitRemoteFeaturesFactory gitRemoteFeaturesFactory; + + /// + /// The for the . + /// + readonly ILoggerFactory loggerFactory; + + /// + /// The for the . + /// + readonly GeneralConfiguration generalConfiguration; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The containing the value of . + public RepostoryManagerFactory( + ILibGit2RepositoryFactory repositoryFactory, + ILibGit2Commands repositoryCommands, + IPostWriteHandler postWriteHandler, + IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, + ILoggerFactory loggerFactory, + IOptions 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)); + } + + /// + public IRepositoryManager CreateRepositoryManager(IIOManager ioManager, IEventConsumer eventConsumer) + => new RepositoryManager( + repositoryFactory, + repositoryCommands, + ioManager, + eventConsumer, + postWriteHandler, + gitRemoteFeaturesFactory, + loggerFactory.CreateLogger(), + loggerFactory.CreateLogger(), + generalConfiguration); + + /// + public Task StartAsync(CancellationToken cancellationToken) + { + CheckSystemCompatibility(); + return Task.CompletedTask; + } + + /// + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + /// Test that the is functional. + /// + void CheckSystemCompatibility() => repositoryFactory.CreateInMemory(); + } +} diff --git a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs index 668e158241..eecca9cd9f 100644 --- a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs @@ -66,6 +66,11 @@ namespace Tgstation.Server.Host.Configuration /// const string DefaultOpenDreamGitUrl = "https://github.com/OpenDreamProject/OpenDream"; + /// + /// The default value for . + /// + const string DefaultOpenDreamGitTagPrefix = "v"; + /// /// The current . /// @@ -132,6 +137,11 @@ namespace Tgstation.Server.Host.Configuration /// public Uri OpenDreamGitUrl { get; set; } = new Uri(DefaultOpenDreamGitUrl); + /// + /// The prefix to the OpenDream semver as tags appear in the git repository. + /// + public string OpenDreamGitTagPrefix { get; set; } = DefaultOpenDreamGitTagPrefix; + /// /// Initializes a new instance of the class. /// diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index 34ab468431..b57df144fc 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -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 } /// - /// Gets the active . + /// Gets the active . /// /// A resulting in the for the operation. /// Retrieved version information successfully. @@ -91,7 +92,7 @@ namespace Tgstation.Server.Host.Controllers : Conflict(new ErrorMessageResponse(ErrorCode.ResourceNotPresent)))); /// - /// Lists installed s. + /// Lists installed s. /// /// The current page. /// The page size. @@ -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 /// A resulting in the for the operation. /// Created to delete target version successfully. /// Attempted to delete the active BYOND . - /// The specified was not installed. + /// The specified was not installed. [HttpDelete] [TgsAuthorize(ByondRights.DeleteInstall)] [ProducesResponseType(typeof(JobResponse), 202)] @@ -279,13 +276,9 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(ErrorMessageResponse), 410)] public async ValueTask 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); } + + /// + /// Validate and normalize a given . + /// + /// The to validate and normalize. + /// The to return, if any. + 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; + } } } diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index eaeeb9a954..b0fbbf3169 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -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(services, postSetupServices); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -345,7 +347,7 @@ namespace Tgstation.Server.Host.Core AddWatchdog(services, postSetupServices); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -357,6 +359,29 @@ namespace Tgstation.Server.Host.Core services.AddHostedService(); } + // 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() + .CreateRepositoryManager( + new ResolvingIOManager( + services.GetRequiredService(), + openDreamRepositoryDirectory), + new NoopEventConsumer())); + + services.AddSingleton(); + services.AddSingleton( + serviceProvider => new Dictionary + { + { EngineType.Byond, serviceProvider.GetRequiredService() }, + { EngineType.OpenDream, serviceProvider.GetRequiredService() }, + }); + services.AddSingleton(); + if (postSetupServices.InternalConfiguration.UsingSystemD) services.AddHostedService(); @@ -378,6 +403,7 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddChatProviderFactory(); services.AddSingleton(); diff --git a/src/Tgstation.Server.Host/appsettings.yml b/src/Tgstation.Server.Host/appsettings.yml index bebeb79c04..3046339495 100644 --- a/src/Tgstation.Server.Host/appsettings.yml +++ b/src/Tgstation.Server.Host/appsettings.yml @@ -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 diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs index 210b31447e..b21b19f0ae 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs @@ -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(() => new DiscordProvider(null, null, null, null, null)); - Assert.ThrowsException(() => new DiscordProvider(mockJobManager, null, null, null, null)); + Assert.ThrowsException(() => new DiscordProvider(null, null, null, null, null, null)); + Assert.ThrowsException(() => new DiscordProvider(mockJobManager, null, null, null, null, null)); var mockDel = Mock.Of(); - Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, null, null, null)); + Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, null, null, null, null)); var mockLogger = Mock.Of>(); - Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, null, null)); + Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, null, null, null)); var mockAss = Mock.Of(); - Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, null)); - await new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, bot).DisposeAsync(); + Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, null, null)); + Assert.ThrowsException(() => 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(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>(); - await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, Mock.Of(), testToken1); + await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, Mock.Of(), testToken1, new GeneralConfiguration()); Assert.IsFalse(provider.Connected); await InvokeConnect(provider); Assert.IsTrue(provider.Connected);