diff --git a/appveyor.yml b/appveyor.yml index 98c9cb64f1..f326ed3b2d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -16,12 +16,15 @@ configuration: shallow_clone: true artifacts: - path: artifacts/ServerConsole + name: Server Console + - path: artifacts/ServerService + name: Server Service cache: - ~\.nuget\packages -> **\*.csproj - C:\ProgramData\chocolatey\bin -> appveyor.yml - C:\ProgramData\chocolatey\lib -> appveyor.yml install: - - choco install fciv doxygen.portable codecov + - choco install doxygen.portable codecov - nuget restore tgstation-server.sln build: project: tgstation-server.sln @@ -53,6 +56,7 @@ after_test: - ps: build/UploadCoverage.ps1 - ps: build/BuildDox.ps1 - dotnet publish src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj -o ../../artifacts/ServerConsole -c %CONFIGURATION% + - ps: Copy-Item -path "src/Tgstation.Server.Host.Service/bin/$env:CONFIGURATION" -destination ../../artifacts/ServerService -recurse deploy: - provider: GitHub release: "tgstation-server-v$(TGSVersion)" diff --git a/src/Tgstation.Server.Api/Models/Administration.cs b/src/Tgstation.Server.Api/Models/Administration.cs index 640a5c65ac..9e51bd7d3c 100644 --- a/src/Tgstation.Server.Api/Models/Administration.cs +++ b/src/Tgstation.Server.Api/Models/Administration.cs @@ -5,14 +5,8 @@ using Tgstation.Server.Api.Rights; namespace Tgstation.Server.Api.Models { /// - public sealed class Administration : Internal.ServerSettings + public sealed class Administration { - /// - /// If the instances will not be stopped when the server exits. Resets to when the server restarts - /// - [Permissions(ReadRight = AdministrationRights.SoftStop, WriteRight = AdministrationRights.SoftStop)] - public bool SoftStop { get; set; } - /// /// The latest available version of the Tgstation.Server.Host assembly from the upstream repository. If is higher than 's the update cannot be applied due to API changes /// diff --git a/src/Tgstation.Server.Api/Models/Byond.cs b/src/Tgstation.Server.Api/Models/Byond.cs index 6654babe0a..17f5b1456c 100644 --- a/src/Tgstation.Server.Api/Models/Byond.cs +++ b/src/Tgstation.Server.Api/Models/Byond.cs @@ -9,22 +9,10 @@ namespace Tgstation.Server.Api.Models [Model(RightsType.Byond, RequiresInstance = true)] public sealed class Byond { - /// - /// The for the installation - /// - [Permissions(DenyWrite = true, ReadRight = ByondRights.ReadStatus)] - public ByondStatus ByondStatus { get; set; } - /// /// The of the installation used for new compiles. Will be if the user does not have permission to view it or there is no BYOND version installed. Only considers the and numbers /// - [Permissions(ReadRight = ByondRights.ReadInstalled, WriteRight = ByondRights.ChangeVersion)] + [Permissions(ReadRight = ByondRights.ReadActive, WriteRight = ByondRights.ChangeVersion)] public Version Version { get; set; } - - /// - /// The of the that's currently running the game servers - /// - [Permissions(DenyWrite = true, ReadRight = ByondRights.ReadPrevious)] - public Version PreviousVersion { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/ByondStatus.cs b/src/Tgstation.Server.Api/Models/ByondStatus.cs deleted file mode 100644 index 638a91e475..0000000000 --- a/src/Tgstation.Server.Api/Models/ByondStatus.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Tgstation.Server.Api.Models -{ - /// - /// The status of a update job - /// -#pragma warning disable CA1717 // Only FlagsAttribute enums should have plural names - public enum ByondStatus -#pragma warning restore CA1717 // Only FlagsAttribute enums should have plural names - { - /// - /// No update in progress - /// - Idle, - /// - /// Preparing to update - /// - Starting, - /// - /// Revision is downloading - /// - Downloading, - /// - /// Revision is deflating - /// - Staging, - /// - /// Revision is ready and waiting for DreamDaemon reboot - /// - Staged, - /// - /// Revision is being applied - /// - Updating, - /// - /// User does not have permission to view the - /// - Hidden, - } -} diff --git a/src/Tgstation.Server.Api/Models/Internal/ServerSettings.cs b/src/Tgstation.Server.Api/Models/Internal/ServerSettings.cs deleted file mode 100644 index 3ab52adef5..0000000000 --- a/src/Tgstation.Server.Api/Models/Internal/ServerSettings.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Tgstation.Server.Api.Rights; - -namespace Tgstation.Server.Api.Models.Internal -{ - /// - /// Metadata about an installation - /// - [Model(RightsType.Administration)] - public class ServerSettings - { - /// - /// Automatically send unhandled exception data to a public collection service. This will be limited to system information, path data, and game code compilation information. - /// - [Permissions(ReadRight = AdministrationRights.ChangeTelemetry, WriteRight = AdministrationRights.ChangeTelemetry)] - public bool EnableTelemetry { get; set; } - - /// - /// The git repository URL to recieve updates to Tgstation.Server.Host from, must include credentials if necessary. If set to upstream pulls will be disabled entirely. Should be https://github.com/tgstation/tgstation-server or a fork of it - /// - [Permissions(ReadRight = AdministrationRights.SetUpstreamRepository, WriteRight = AdministrationRights.SetUpstreamRepository)] - public string UpstreamRepository { get; set; } - } -} diff --git a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs index c8992b3575..58f208711c 100644 --- a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs +++ b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs @@ -13,24 +13,16 @@ namespace Tgstation.Server.Api.Rights /// None = 0, /// - /// User can change - /// - SetUpstreamRepository = 1, - /// - /// User can change - /// - ChangeTelemetry = 2, - /// /// User can edit themself and other s /// - EditUsers = 4, + EditUsers = 1, /// - /// User can change + /// User can gracefully restart the host /// - SoftStop = 8, + SoftStop = 2, /// /// User can change /// - ChangeVersion = 16 + ChangeVersion = 4 } } diff --git a/src/Tgstation.Server.Api/Rights/ByondRights.cs b/src/Tgstation.Server.Api/Rights/ByondRights.cs index 142ea098b4..ae650b69af 100644 --- a/src/Tgstation.Server.Api/Rights/ByondRights.cs +++ b/src/Tgstation.Server.Api/Rights/ByondRights.cs @@ -13,24 +13,12 @@ namespace Tgstation.Server.Api.Rights /// None = 0, /// - /// User may check the installed BYOND version + /// User may check the active installed BYOND version /// - ReadInstalled = 1, - /// - /// User may check the previous BYOND version - /// - ReadPrevious = 2, + ReadActive = 1, /// /// User may change to any BYOND version /// - ChangeVersion = 4, - /// - /// User may cancel a pending installation job - /// - Cancel = 8, - /// - /// User may read the of the installation job - /// - ReadStatus = 16, + ChangeVersion = 4 } } diff --git a/src/Tgstation.Server.Client/Components/IByondClient.cs b/src/Tgstation.Server.Client/Components/IByondClient.cs index f7e064e0b2..9f9d8421c6 100644 --- a/src/Tgstation.Server.Client/Components/IByondClient.cs +++ b/src/Tgstation.Server.Client/Components/IByondClient.cs @@ -21,10 +21,9 @@ namespace Tgstation.Server.Client.Components /// /// Updates the installed BYOND /// - /// The to update - /// Optional taking a to run when it changes + /// The to set to active /// The for the operation /// A representing the running operation - Task Update(Byond byond, Action progressCallback, CancellationToken cancellationToken); + Task SetActiveVersion(Version version, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/ByondExecutableLock.cs b/src/Tgstation.Server.Host/Components/ByondExecutableLock.cs new file mode 100644 index 0000000000..2be7ab498a --- /dev/null +++ b/src/Tgstation.Server.Host/Components/ByondExecutableLock.cs @@ -0,0 +1,26 @@ +using System; + +namespace Tgstation.Server.Host.Components +{ + /// + sealed class ByondExecutableLock : IByondExecutableLock + { + /// + public Version Version { get; set; } + + /// + public string DreamDaemonPath { get; set; } + + /// + public string DreamMakerPath { get; set; } + + //at one point in design, byond versions were to delete themselves if they werent the active version + //That changed at some point so these functions are intentioanlly left blank + + /// + public void Dispose() { } + + /// + public void DoNotDeleteThisSession() { } + } +} diff --git a/src/Tgstation.Server.Host/Components/ByondManager.cs b/src/Tgstation.Server.Host/Components/ByondManager.cs new file mode 100644 index 0000000000..e046bcf56c --- /dev/null +++ b/src/Tgstation.Server.Host/Components/ByondManager.cs @@ -0,0 +1,166 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.IO; + +using MemoryStream = System.IO.MemoryStream; + +namespace Tgstation.Server.Host.Components +{ + /// + sealed class ByondManager : IByondManager + { + const string VersionFileName = "Version.txt"; + const string ActiveVersionFileName = "ActiveVersion.txt"; + + const string BinPath = "byond/bin"; + + /// + public Version ActiveVersion { get; private set; } + + /// + /// The for the + /// + readonly IIOManager ioManager; + + /// + /// The for the + /// + readonly IByondInstaller byondInstaller; + + /// + /// The for the + /// + readonly ILogger logger; + + /// + /// Map of byond s to s that complete when they are installed + /// + readonly Dictionary installedVersions; + + /// + /// Construct a + /// + /// The value of + /// The value of + /// The value of + public ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, ILogger logger) + { + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + installedVersions = new Dictionary(); + } + + static string VersionKey(Version version) => new Version(version.Major, version.Minor).ToString(); + + async Task InstallVersion(Version version, CancellationToken cancellationToken) + { + var ourTcs = new TaskCompletionSource(); + Task inProgressTask; + + var versionKey = VersionKey(version); + bool installed; + lock (installedVersions) + { + installed = installedVersions.TryGetValue(versionKey, out inProgressTask); + if (!installed) + installedVersions.Add(versionKey, ourTcs.Task); + } + if(installed) + using (cancellationToken.Register(() => ourTcs.SetCanceled())) + { + await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false); + return; + } + + var downloadTask = byondInstaller.DownloadVersion(version, cancellationToken); + + //okay up to us to install it then + await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false); + await ioManager.CreateDirectory(versionKey, cancellationToken).ConfigureAwait(false); + + var resolvedPath = ioManager.ResolvePath(versionKey); + using (var zipBytes = new MemoryStream(await downloadTask.ConfigureAwait(false))) + using (var archive = new ZipArchive(zipBytes)) + await Task.Factory.StartNew(() => archive.ExtractToDirectory(resolvedPath), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); + + await byondInstaller.InstallByond(resolvedPath, version, cancellationToken).ConfigureAwait(false); + + //make sure to do this last because this is what tells us we have a valid version + await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false); + } + + /// + public async Task ChangeVersion(Version version, CancellationToken cancellationToken) + { + await InstallVersion(version, cancellationToken).ConfigureAwait(false); + await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false); + ActiveVersion = version; + } + + /// + public async Task UseExecutables(Version requiredVersion, CancellationToken cancellationToken) + { + var versionToUse = requiredVersion ?? ActiveVersion; + if (versionToUse == null) + throw new InvalidOperationException("No BYOND versions installed!"); + await InstallVersion(requiredVersion, cancellationToken).ConfigureAwait(false); + + var versionKey = VersionKey(versionToUse); + + return new ByondExecutableLock + { + DreamDaemonPath = ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamDaemonName)), + DreamMakerPath = ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamMakerName)), + Version = versionToUse + }; + } + + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + var cacheCleanTask = byondInstaller.CleanCache(cancellationToken); + + var activeVersionBytesTask = ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken); + + var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false); + + async Task ReadVersion(string path) + { + var bytes = await ioManager.ReadAllBytes(ioManager.ConcatPath(path, VersionFileName), cancellationToken).ConfigureAwait(false); + var text = Encoding.UTF8.GetString(bytes); + if (Version.TryParse(text, out var version)) + { + var key = VersionKey(version); + lock (installedVersions) + if (!installedVersions.ContainsKey(key)) + { + installedVersions.Add(key, Task.CompletedTask); + return; + } + } + await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); + }; + + await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false); + + var activeVersionString = Encoding.UTF8.GetString(await activeVersionBytesTask.ConfigureAwait(false)); + if (Version.TryParse(activeVersionString, out var activeVersion)) + ActiveVersion = activeVersion; + else + await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken).ConfigureAwait(false); + + await cacheCleanTask.ConfigureAwait(false); + } + + /// + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/src/Tgstation.Server.Host/Components/DreamMaker.cs b/src/Tgstation.Server.Host/Components/DreamMaker.cs index 06cd345bf7..7d1c19ba36 100644 --- a/src/Tgstation.Server.Host/Components/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/DreamMaker.cs @@ -39,9 +39,9 @@ namespace Tgstation.Server.Host.Components public CompilerStatus Status { get; private set; } /// - /// The for + /// The for /// - readonly IByond byond; + readonly IByondManager byond; /// /// The for /// @@ -82,7 +82,7 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - public DreamMaker(IByond byond, IIOManager ioManager, IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application, IEventConsumer eventConsumer, ILogger logger) + public DreamMaker(IByondManager byond, IIOManager ioManager, IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application, IEventConsumer eventConsumer, ILogger logger) { this.byond = byond; this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); @@ -274,7 +274,7 @@ namespace Tgstation.Server.Host.Components //run compiler, verify api bool ddVerified; - using (var byondLock = byond.UseExecutables(null)) + using (var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false)) { job.ByondVersion = byondLock.Version.ToString(); diff --git a/src/Tgstation.Server.Host/Components/IByondInstaller.cs b/src/Tgstation.Server.Host/Components/IByondInstaller.cs new file mode 100644 index 0000000000..9ad7e26510 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/IByondInstaller.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components +{ + /// + /// For downloading and installing BYOND extractions for a given system + /// + interface IByondInstaller + { + /// + /// Get the file name of the DreamDaemon executable + /// + string DreamDaemonName { get; } + + /// + /// Get the file name of the DreamMaker executable + /// + string DreamMakerName { get; } + + /// + /// Download a given BYOND + /// + /// The of BYOND to download + /// The for the operation + /// A resulting in the s of the zipfile + Task DownloadVersion(Version version, CancellationToken cancellationToken); + + /// + /// Does actions necessary to get an extracted BYOND installation working + /// + /// The path to the BYOND installation + /// The of BYOND being installed + /// The for the operation + /// + Task InstallByond(string path, Version version, CancellationToken cancellationToken); + + /// + /// Attempts to cleans the BYOND cache folder for the system + /// + /// The for the operation + /// A representing the running operation + Task CleanCache(CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/IByond.cs b/src/Tgstation.Server.Host/Components/IByondManager.cs similarity index 56% rename from src/Tgstation.Server.Host/Components/IByond.cs rename to src/Tgstation.Server.Host/Components/IByondManager.cs index 105d6dac74..c6ac12119e 100644 --- a/src/Tgstation.Server.Host/Components/IByond.cs +++ b/src/Tgstation.Server.Host/Components/IByondManager.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Extensions.Hosting; +using System; using System.Threading; using System.Threading.Tasks; @@ -7,33 +8,26 @@ namespace Tgstation.Server.Host.Components /// /// For managing the BYOND installation /// - public interface IByond + public interface IByondManager : IHostedService { /// - /// Change the current BYOND version + /// The currently active BYOND version + /// + Version ActiveVersion { get; } + + /// + /// Change the active BYOND version /// /// The new /// The for the operation Task ChangeVersion(Version version, CancellationToken cancellationToken); - /// - /// Get the currently installed BYOND version - /// - /// The for the operation - /// The current BYOND version - Task GetVersion(CancellationToken cancellationToken); - /// /// Lock the current installation's location and return a /// /// The BYOND required - IByondExecutableLock UseExecutables(Version requiredVersion); - - /// - /// Clears the cache folder - /// /// The for the operation - /// A representing the running operation - Task ClearCache(CancellationToken cancellationToken); + /// A resulting in the requested + Task UseExecutables(Version requiredVersion, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/IInstance.cs b/src/Tgstation.Server.Host/Components/IInstance.cs index 9868430342..ffcf471681 100644 --- a/src/Tgstation.Server.Host/Components/IInstance.cs +++ b/src/Tgstation.Server.Host/Components/IInstance.cs @@ -16,9 +16,9 @@ namespace Tgstation.Server.Host.Components IRepositoryManager RepositoryManager { get; } /// - /// The for the + /// The for the /// - IByond Byond { get; } + IByondManager ByondManager { get; } /// /// The for the diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index d607053ab2..fea1f1b57f 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -16,7 +16,7 @@ namespace Tgstation.Server.Host.Components public IRepositoryManager RepositoryManager { get; } /// - public IByond Byond { get; } + public IByondManager ByondManager { get; } /// public IDreamMaker DreamMaker { get; } @@ -59,11 +59,11 @@ namespace Tgstation.Server.Host.Components /// CancellationTokenSource timerCts; - public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByond byond, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory) + public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory) { this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); - Byond = byond ?? throw new ArgumentNullException(nameof(byond)); + ByondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager)); DreamMaker = dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker)); watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog)); Chat = chat ?? throw new ArgumentNullException(nameof(chat)); @@ -125,10 +125,10 @@ namespace Tgstation.Server.Host.Components } /// - public Task StartAsync(CancellationToken cancellationToken) => Task.WhenAll(SetAutoUpdateInterval(metadata.AutoUpdateInterval), Watchdog.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken), compileJobConsumer.StartAsync(cancellationToken), Watchdog.StartAsync(cancellationToken)); + public Task StartAsync(CancellationToken cancellationToken) => Task.WhenAll(SetAutoUpdateInterval(metadata.AutoUpdateInterval), ByondManager.StartAsync(cancellationToken), Watchdog.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken), compileJobConsumer.StartAsync(cancellationToken), Watchdog.StartAsync(cancellationToken)); /// - public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(SetAutoUpdateInterval(null), Watchdog.StopAsync(cancellationToken), Chat.StopAsync(cancellationToken), compileJobConsumer.StopAsync(cancellationToken), Watchdog.StopAsync(cancellationToken)); + public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(SetAutoUpdateInterval(null), ByondManager.StopAsync(cancellationToken), Watchdog.StopAsync(cancellationToken), Chat.StopAsync(cancellationToken), compileJobConsumer.StopAsync(cancellationToken), Watchdog.StopAsync(cancellationToken)); /// public async Task SetAutoUpdateInterval(int? newInterval) diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 391b06d5db..ec9021ff1d 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -115,7 +115,7 @@ namespace Tgstation.Server.Host.Components var repoManager = new RepositoryManager(metadata.RepositorySettings, repoIoManager); - IByond byond = null; + IByondManager byond = null; var configuration = new Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, loggerFactory.CreateLogger()); var chat = chatFactory.CreateChat(); diff --git a/src/Tgstation.Server.Host/Components/PosixByondInstaller.cs b/src/Tgstation.Server.Host/Components/PosixByondInstaller.cs new file mode 100644 index 0000000000..2a3e5cd44d --- /dev/null +++ b/src/Tgstation.Server.Host/Components/PosixByondInstaller.cs @@ -0,0 +1,93 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Globalization; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.IO; + +namespace Tgstation.Server.Host.Components +{ + /// + /// for Posix systems + /// + sealed class PosixByondInstaller : IByondInstaller + { + /// + /// The URL format string for getting BYOND linux version {0}.{1} zipfile + /// + const string ByondRevisionsURL = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip"; + /// + /// Path to the BYOND cache + /// + const string ByondCachePath = "~/.byond"; //TODO: Verify this is correct!!!!! + + /// + public string DreamDaemonName => "DreamDaemon"; + + /// + public string DreamMakerName => "DreamMaker"; + + /// + /// The for the + /// + readonly IIOManager ioManager; + + /// + /// The for the + /// + readonly ILogger logger; + + /// + /// Construct a + /// + /// The value of + /// The value of + public PosixByondInstaller(IIOManager ioManager, ILogger logger) + { + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + public async Task CleanCache(CancellationToken cancellationToken) + { + try + { + await ioManager.DeleteDirectory(ByondCachePath, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e); + } + } + + /// + public async Task DownloadVersion(Version version, CancellationToken cancellationToken) + { + var ourVersion = version; + //lummox is annoying and doesn't like to post linux versions if nothing changed in DreamDaemon/DM + //if this for's exit condition ever triggers, i get to say I told you so + Exception lastException = null; + for (var I = 0; I < 5 && ourVersion.Minor >= 1; ++I, ourVersion = new Version(ourVersion.Major, ourVersion.Minor)) + { + try + { + var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURL, ourVersion.Major, ourVersion.Minor); + + return await ioManager.DownloadFile(new Uri(url), cancellationToken).ConfigureAwait(false); + } + catch (WebException e) + { + if (!(e.Status == WebExceptionStatus.ProtocolError && e.Response is HttpWebResponse response && response.StatusCode == HttpStatusCode.NotFound)) + throw; + lastException = e; + } + } + throw lastException; + } + + /// + public Task InstallByond(string path, Version version, CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs index b84535a1fa..9b48f51f8d 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs @@ -24,9 +24,9 @@ namespace Tgstation.Server.Host.Components.Watchdog readonly IExecutor executor; /// - /// The for the + /// The for the /// - readonly IByond byond; + readonly IByondManager byond; /// /// The for the @@ -81,7 +81,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of - public SessionControllerFactory(IExecutor executor, IByond byond, IByondTopicSender byondTopicSender, IInteropRegistrar interopRegistrar, ICryptographySuite cryptographySuite, IApplication application, IIOManager ioManager, IChat chat, ILoggerFactory loggerFactory, Models.Instance instance) + public SessionControllerFactory(IExecutor executor, IByondManager byond, IByondTopicSender byondTopicSender, IInteropRegistrar interopRegistrar, ICryptographySuite cryptographySuite, IApplication application, IIOManager ioManager, IChat chat, ILoggerFactory loggerFactory, Models.Instance instance) { this.executor = executor ?? throw new ArgumentNullException(nameof(executor)); this.byond = byond ?? throw new ArgumentNullException(nameof(byond)); @@ -142,7 +142,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var chatJsonTrackingContext = await chatJsonTrackingTask.ConfigureAwait(false); try { - var byondLock = currentByondLock ?? byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion)); + var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false); try { //more sanitization here cause it uses the same scheme @@ -190,7 +190,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var chatJsonTrackingContext = await chat.TrackJsons(basePath, reattachInformation.ChatChannelsJson, reattachInformation.ChatCommandsJson, cancellationToken).ConfigureAwait(false); try { - var byondLock = byond.UseExecutables(Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion)); + var byondLock = await byond.UseExecutables(Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false); try { var session = executor.AttachToDreamDaemon(reattachInformation.ProcessId, byondLock); diff --git a/src/Tgstation.Server.Host/Components/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/WindowsByondInstaller.cs new file mode 100644 index 0000000000..6b102ace09 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/WindowsByondInstaller.cs @@ -0,0 +1,143 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics; +using System.Globalization; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.IO; + +namespace Tgstation.Server.Host.Components +{ + /// + /// for windows systems + /// + sealed class WindowsByondInstaller : IByondInstaller + { + /// + /// The URL format string for getting BYOND windows version {0}.{1} zipfile + /// + const string ByondRevisionsURL = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip"; + /// + /// BYOND's DreamDaemon config file in the cfg modification directory + /// + const string ByondDDConfig = "byond/config/daemon.txt"; + /// + /// Setting to add to to suppress an invisible user prompt for running a trusted mode .dmb + /// + const string ByondNoPromptTrustedMode = "trusted-check 0"; + /// + /// The directory that contains the BYOND directx redistributable + /// + const string ByondDXDir = "byond/directx"; + + /// + public string DreamDaemonName => "dreamdaemon.exe"; + + /// + public string DreamMakerName => "dm.exe"; + + /// + /// The for the + /// + readonly IIOManager ioManager; + + /// + /// The for the + /// + readonly ILogger logger; + + /// + /// If DirectX was installed + /// + bool installedDirectX; + + /// + /// Construct a + /// + /// The value of + /// The value of + public WindowsByondInstaller(IIOManager ioManager, ILogger logger) + { + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + installedDirectX = false; + } + + /// + public async Task CleanCache(CancellationToken cancellationToken) + { + try + { + await ioManager.DeleteDirectory(ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "byond/cache"), cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e); + } + } + + /// + public Task DownloadVersion(Version version, CancellationToken cancellationToken) + { + var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURL, version.Major, version.Minor); + + return ioManager.DownloadFile(new Uri(url), cancellationToken); + } + + /// + public async Task InstallByond(string path, Version version, CancellationToken cancellationToken) + { + var setNoPromptTrustedModeTask = ioManager.WriteAllBytes(ByondDDConfig, Encoding.UTF8.GetBytes(ByondNoPromptTrustedMode), cancellationToken); + + //after this version lummox made DD depend of directx lol + if (version.Major >= 512 && version.Minor >= 1427 && Monitor.TryEnter(this)) + try + { + if (!installedDirectX) + //always install it, it's pretty fast and will do better redundancy checking than us + using (var p = new Process()) + { + p.StartInfo.Arguments = "/silent"; + var rbdx = ioManager.ConcatPath(path, ByondDXDir); + p.StartInfo.FileName = rbdx + "/DXSETUP.exe"; + p.StartInfo.UseShellExecute = false; + p.StartInfo.WorkingDirectory = rbdx; + p.EnableRaisingEvents = true; + var tcs = new TaskCompletionSource(); + p.Exited += (a, b) => tcs.SetResult(null); + try + { + p.Start(); + using (cancellationToken.Register(() => + { + p.Kill(); + tcs.SetCanceled(); + })) + await tcs.Task.ConfigureAwait(false); + } + finally + { + try + { + p.Kill(); + p.WaitForExit(); + } + catch (InvalidOperationException) { } + } + + if (p.ExitCode != 0) + throw new Exception("Failed to install included DirectX! Exit code: " + p.ExitCode); + installedDirectX = true; + } + } + finally + { + Monitor.Exit(this); + } + + await setNoPromptTrustedModeTask.ConfigureAwait(false); + } + } +} diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index ab686c1495..aee2ab7feb 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -154,11 +154,13 @@ namespace Tgstation.Server.Host.Core { services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } else { services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } services.AddSingleton(); diff --git a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs index 5a4bb9654f..793b5881e2 100644 --- a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs +++ b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -243,5 +244,13 @@ namespace Tgstation.Server.Host.IO link = ResolvePath(link); }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + + /// + public async Task DownloadFile(Uri url, CancellationToken cancellationToken) + { + using (var wc = new WebClient()) + using (cancellationToken.Register(() => wc.CancelAsync())) + return await wc.DownloadDataTaskAsync(url).ConfigureAwait(false); + } } } diff --git a/src/Tgstation.Server.Host/IO/IIOManager.cs b/src/Tgstation.Server.Host/IO/IIOManager.cs index e5b6cdefde..ad320805d3 100644 --- a/src/Tgstation.Server.Host/IO/IIOManager.cs +++ b/src/Tgstation.Server.Host/IO/IIOManager.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -153,5 +154,13 @@ namespace Tgstation.Server.Host.IO /// A for the operation /// A representing the running operation Task MoveFile(string source, string destination, CancellationToken cancellationToken); + + /// + /// Downloads a file from + /// + /// The URL to download + /// A for the operation + /// A resulting in the s of the downloaded file + Task DownloadFile(Uri url, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs index 052eff0a34..cc00821fdd 100644 --- a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs +++ b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs @@ -58,18 +58,22 @@ namespace Tgstation.Server.Host.IO return false; } } - cancellationToken.ThrowIfCancellationRequested(); - file.Seek(0, SeekOrigin.Begin); - cancellationToken.ThrowIfCancellationRequested(); - file.SetLength(data.Length); + if (data != null) + { + file.Seek(0, SeekOrigin.Begin); - cancellationToken.ThrowIfCancellationRequested(); - file.Write(data, 0, data.Length); + cancellationToken.ThrowIfCancellationRequested(); + file.SetLength(data.Length); - return true; + cancellationToken.ThrowIfCancellationRequested(); + file.Write(data, 0, data.Length); + } } + if (data == null) + File.Delete(path); + return true; } } } diff --git a/src/Tgstation.Server.Host/Models/DatabaseContext.cs b/src/Tgstation.Server.Host/Models/DatabaseContext.cs index 44099444d8..c9bacb6086 100644 --- a/src/Tgstation.Server.Host/Models/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Models/DatabaseContext.cs @@ -14,9 +14,6 @@ namespace Tgstation.Server.Host.Models /// abstract class DatabaseContext : DbContext, IDatabaseContext where TParentContext : DbContext { - /// - public DbSet ServerSettings { get; set; } - /// public DbSet Users { get; set; } @@ -126,18 +123,6 @@ namespace Tgstation.Server.Host.Models base.OnConfiguring(optionsBuilder); } - /// - public async Task GetServerSettings(CancellationToken cancellationToken) - { - var settings = await ServerSettings.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); - if (settings == default(ServerSettings)) - { - settings = new ServerSettings(); - ServerSettings.Add(settings); - } - return settings; - } - /// public async Task Initialize(CancellationToken cancellationToken) { diff --git a/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs index 772368aed3..acaf0e1f53 100644 --- a/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs @@ -21,11 +21,6 @@ namespace Tgstation.Server.Host.Models /// const string DefaultAdminPassword = "ISolemlySwearToDeleteTheDataDirectory"; - /// - /// The default git repository to pull server updates from - /// - const string DefaultUpstreamRepository = "https://github.com/tgstation/tgstation-server"; - /// /// The for the /// @@ -60,12 +55,6 @@ namespace Tgstation.Server.Host.Models public async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken) { SeedAdminUser(databaseContext); - - var serverSettings = await databaseContext.GetServerSettings(cancellationToken).ConfigureAwait(false); - - serverSettings.EnableTelemetry = true; - serverSettings.UpstreamRepository = DefaultUpstreamRepository; - await databaseContext.Save(cancellationToken).ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Models/IDatabaseContext.cs b/src/Tgstation.Server.Host/Models/IDatabaseContext.cs index 9e8d906905..0d3814a284 100644 --- a/src/Tgstation.Server.Host/Models/IDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Models/IDatabaseContext.cs @@ -59,13 +59,6 @@ namespace Tgstation.Server.Host.Models /// DbSet RepositorySettings { get; set; } - /// - /// Get the in the - /// - /// The for the operation - /// A resulting in the in the - Task GetServerSettings(CancellationToken cancellationToken); - /// /// Saves changes made to the /// diff --git a/src/Tgstation.Server.Host/Models/ServerSettings.cs b/src/Tgstation.Server.Host/Models/ServerSettings.cs deleted file mode 100644 index 4326d9f563..0000000000 --- a/src/Tgstation.Server.Host/Models/ServerSettings.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Tgstation.Server.Host.Models -{ - /// - public sealed class ServerSettings : Api.Models.Internal.ServerSettings - { - /// - /// The row Id - /// - public long Id { get; set; } - } -} diff --git a/tests/Tgstation.Server.Host.Tests/Security/TestAuthenticationContext.cs b/tests/Tgstation.Server.Host.Tests/Security/TestAuthenticationContext.cs index aa125813de..91af8fa215 100644 --- a/tests/Tgstation.Server.Host.Tests/Security/TestAuthenticationContext.cs +++ b/tests/Tgstation.Server.Host.Tests/Security/TestAuthenticationContext.cs @@ -43,7 +43,7 @@ namespace Tgstation.Server.Host.Security.Tests var authContext = new AuthenticationContext(null, user, instanceUser); user.AdministrationRights = AdministrationRights.EditUsers; - instanceUser.ByondRights = ByondRights.ChangeVersion | ByondRights.ReadInstalled; + instanceUser.ByondRights = ByondRights.ChangeVersion | ByondRights.ReadActive; Assert.AreEqual((int)user.AdministrationRights, authContext.GetRight(RightsType.Administration)); Assert.AreEqual((int)instanceUser.ByondRights, authContext.GetRight(RightsType.Byond)); }