diff --git a/build/Version.props b/build/Version.props index a53842da56..004258e666 100644 --- a/build/Version.props +++ b/build/Version.props @@ -6,12 +6,13 @@ 5.0.4 4.2.0 9.6.0 - 9.6.1 - 10.7.1 + 10.0.0 + 11.0.0 6.0.5 5.3.0 1.2.0 1.2.0 + 1.0.0 net6.0 diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs index 349522be7f..0ad8c7a487 100644 --- a/src/Tgstation.Server.Api/ApiHeaders.cs +++ b/src/Tgstation.Server.Api/ApiHeaders.cs @@ -50,6 +50,11 @@ namespace Tgstation.Server.Api /// public const string OAuthAuthenticationScheme = "OAuth"; + /// + /// Added to in netstandard2.1. Can't use because of Tgstation.Server.Migrator. + /// + public const string ApplicationJsonMime = "application/json"; + /// /// Get the version of the the caller is using. /// @@ -167,9 +172,9 @@ namespace Tgstation.Server.Api errorBuilder.Append(message); } - var jsonAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(MediaTypeNames.Application.Json); + var jsonAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(ApplicationJsonMime); if (!requestHeaders.Accept.Any(x => jsonAccept.IsSubsetOf(x))) - AddError(HeaderTypes.Accept, $"Client does not accept {MediaTypeNames.Application.Json}!"); + AddError(HeaderTypes.Accept, $"Client does not accept {ApplicationJsonMime}!"); if (!requestHeaders.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgentValues) || userAgentValues.Count == 0) AddError(HeaderTypes.UserAgent, $"Missing {HeaderNames.UserAgent} header!"); @@ -306,7 +311,7 @@ namespace Tgstation.Server.Api throw new InvalidOperationException("Specified different instance IDs in constructor and SetRequestHeaders!"); headers.Clear(); - headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJsonMime)); headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent)); headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString()); if (OAuthProvider.HasValue) diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj index a16a93ebed..7f275db48d 100644 --- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj +++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj @@ -2,7 +2,7 @@ - netstandard2.1 + netstandard2.0 Full $(TgsApiLibraryVersion) true @@ -16,7 +16,7 @@ https://github.com/tgstation/tgstation-server 2018-2022 json web api tgstation-server tgstation ss13 byond - See https://github.com/tgstation/tgstation-server/releases/tag/api-v$(TgsApiVersion) + Retargeted to netstandard2.0 to support migrator. true snupkg ../../build/analyzers.ruleset diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index fac48c439a..36344c265f 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -26,6 +26,12 @@ namespace Tgstation.Server.Client /// sealed class ApiClient : IApiClient { + /// + /// PATCH . + /// + /// HOW IS THIS NOT INCLUDED IN THE FRAMEWORK??!?!? + static readonly HttpMethod HttpPatch = new ("PATCH"); + /// public Uri Url { get; } @@ -77,7 +83,7 @@ namespace Tgstation.Server.Client /// Get the to use. /// /// A new instance. - static JsonSerializerSettings GetSerializerSettings() => new JsonSerializerSettings + static JsonSerializerSettings GetSerializerSettings() => new () { ContractResolver = new CamelCasePropertyNamesContractResolver(), Converters = new[] { new VersionConverter() }, @@ -171,7 +177,7 @@ namespace Tgstation.Server.Client public Task Update(string route, TBody body, CancellationToken cancellationToken) where TBody : class => RunRequest(route, body, HttpMethod.Post, null, false, cancellationToken); /// - public Task Patch(string route, CancellationToken cancellationToken) => RunRequest(route, null, HttpMethod.Patch, null, false, cancellationToken); + public Task Patch(string route, CancellationToken cancellationToken) => RunRequest(route, null, HttpPatch, null, false, cancellationToken); /// public Task Update(string route, TBody body, CancellationToken cancellationToken) where TBody : class => RunRequest(route, body, HttpMethod.Post, null, false, cancellationToken); @@ -204,7 +210,7 @@ namespace Tgstation.Server.Client public Task Create(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, new object(), HttpMethod.Put, instanceId, false, cancellationToken); /// - public Task Patch(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, new object(), HttpMethod.Patch, instanceId, false, cancellationToken); + public Task Patch(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, new object(), HttpPatch, instanceId, false, cancellationToken); /// public void AddRequestLogger(IRequestLogger requestLogger) => requestLoggers.Add(requestLogger ?? throw new ArgumentNullException(nameof(requestLogger))); @@ -314,7 +320,7 @@ namespace Tgstation.Server.Client content = new StringContent( JsonConvert.SerializeObject(body, typeof(TBody), Formatting.None, GetSerializerSettings()), Encoding.UTF8, - MediaTypeNames.Application.Json); + ApiHeaders.ApplicationJsonMime); return RunRequest( route, diff --git a/src/Tgstation.Server.Client/CachedResponseStream.cs b/src/Tgstation.Server.Client/CachedResponseStream.cs index d50d14fbc2..89effcaf56 100644 --- a/src/Tgstation.Server.Client/CachedResponseStream.cs +++ b/src/Tgstation.Server.Client/CachedResponseStream.cs @@ -30,14 +30,6 @@ namespace Tgstation.Server.Client return new CachedResponseStream(response, stream); } - /// - public override async ValueTask DisposeAsync() - { - await base.DisposeAsync().ConfigureAwait(false); - await responseStream.DisposeAsync().ConfigureAwait(false); - response.Dispose(); - } - /// public override bool CanRead => responseStream.CanRead; diff --git a/src/Tgstation.Server.Client/Components/ByondClient.cs b/src/Tgstation.Server.Client/Components/ByondClient.cs index cd4fbf76a6..673794b38b 100644 --- a/src/Tgstation.Server.Client/Components/ByondClient.cs +++ b/src/Tgstation.Server.Client/Components/ByondClient.cs @@ -38,7 +38,7 @@ namespace Tgstation.Server.Client.Components => ReadPaged(paginationSettings, Routes.ListRoute(Routes.Byond), instance.Id, cancellationToken); /// - public async Task SetActiveVersion(ByondVersionRequest installRequest, Stream zipFileStream, CancellationToken cancellationToken) + public async Task SetActiveVersion(ByondVersionRequest installRequest, Stream? zipFileStream, CancellationToken cancellationToken) { if (installRequest == null) throw new ArgumentNullException(nameof(installRequest)); diff --git a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs index d0758f41be..581c1ad791 100644 --- a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs +++ b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs @@ -88,7 +88,7 @@ namespace Tgstation.Server.Client.Components cancellationToken); if (memoryStream != null) - await uploadStream!.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); + await uploadStream!.CopyToAsync(memoryStream).ConfigureAwait(false); var configFile = await configFileTask.ConfigureAwait(false); diff --git a/src/Tgstation.Server.Client/Components/IByondClient.cs b/src/Tgstation.Server.Client/Components/IByondClient.cs index 55e8ee1561..554adceab2 100644 --- a/src/Tgstation.Server.Client/Components/IByondClient.cs +++ b/src/Tgstation.Server.Client/Components/IByondClient.cs @@ -35,6 +35,6 @@ namespace Tgstation.Server.Client.Components /// The for the .zip file if is . /// The for the operation. /// A resulting in the updated information. - Task SetActiveVersion(ByondVersionRequest installRequest, Stream zipFileStream, CancellationToken cancellationToken); + Task SetActiveVersion(ByondVersionRequest installRequest, Stream? zipFileStream, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj index b8a2277360..65f7edce9f 100644 --- a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj +++ b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj @@ -2,7 +2,7 @@ - netstandard2.1 + netstandard2.0 Full $(TgsClientVersion) true @@ -16,7 +16,7 @@ https://github.com/tgstation/tgstation-server 2018-2022 json web api tgstation-server tgstation ss13 byond client - Added OAuth login methods to IServerClientFactory. + Retargeted to netstandard2.0 to support migrator. Fixed nullablity of IByondClient.SetActiveVersion's Stream parameter. true snupkg ../../build/analyzers.ruleset diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index a781bbd73f..1dc1c14631 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -111,7 +111,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); var builder = chatBot.CreateConnectionStringBuilder(); - if (builder == null || !builder.Valid || !(builder is IrcConnectionStringBuilder ircBuilder)) + if (builder == null || !builder.Valid || builder is not IrcConnectionStringBuilder ircBuilder) throw new InvalidOperationException("Invalid ChatConnectionStringBuilder!"); address = ircBuilder.Address; @@ -248,7 +248,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception e) { - Logger.LogWarning(e, "Unable to send to channel {0}!", channelName); + Logger.LogWarning(e, "Unable to send to channel {channelName}!", channelName); } }, cancellationToken, @@ -266,7 +266,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers bool localCommitPushed, CancellationToken cancellationToken) { - var commitInsert = revisionInformation.CommitSha.Substring(0, 7); + var commitInsert = revisionInformation.CommitSha[..7]; string remoteCommitInsert; if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha) { @@ -274,7 +274,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers remoteCommitInsert = String.Empty; } else - remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7)); + remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha[..7]); var testmergeInsert = (revisionInformation.ActiveTestMerges?.Count ?? 0) == 0 ? String.Empty @@ -288,7 +288,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers .Select(x => x.TestMerge) .Select(x => { - var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.TargetCommitSha.Substring(0, 7)); + var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.TargetCommitSha[..7]); if (x.Comment != null) result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment); return result; @@ -333,7 +333,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers cancellationToken.ThrowIfCancellationRequested(); - Logger.LogTrace("Authenticating ({0})...", passwordType); + Logger.LogTrace("Authenticating ({passwordType})...", passwordType); switch (passwordType) { case IrcPasswordType.Server: diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index cac4724cda..243470001f 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -139,11 +139,6 @@ namespace Tgstation.Server.Host.Components /// readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory; - /// - /// The for the . - /// - readonly GeneralConfiguration generalConfiguration; - /// /// The for the . /// @@ -175,7 +170,6 @@ namespace Tgstation.Server.Host.Components /// The value of . /// The value of . /// The value of . - /// The containing the value of . /// The containing the value of . public InstanceFactory( IIOManager ioManager, @@ -200,7 +194,6 @@ namespace Tgstation.Server.Host.Components IFileTransferTicketProvider fileTransferService, IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, - IOptions generalConfigurationOptions, IOptions sessionConfigurationOptions) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); @@ -225,7 +218,6 @@ namespace Tgstation.Server.Host.Components this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory)); this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory)); - generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); sessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); } #pragma warning restore CA1502 diff --git a/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs b/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs index 6022ac421a..f3e9e92234 100644 --- a/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs @@ -3,7 +3,7 @@ /// /// Configuration options pertaining to elasticsearch log storage. /// - sealed class ElasticsearchConfiguration + public sealed class ElasticsearchConfiguration { /// /// The key for the the resides in. diff --git a/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs b/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs index f425e4ab36..47c47d5083 100644 --- a/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs @@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Configuration /// /// OAuth configuration options. /// - sealed class OAuthConfiguration : OAuthConfigurationBase + public sealed class OAuthConfiguration : OAuthConfigurationBase { /// /// The client redirect URL. Not used by all providers. diff --git a/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs b/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs index 9da3d398ac..a32db951d8 100644 --- a/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs +++ b/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs @@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Configuration /// /// Base OAuth options. /// - abstract class OAuthConfigurationBase + public abstract class OAuthConfigurationBase { /// /// The client ID. diff --git a/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs b/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs index d4e8e46c15..b78cf55b9a 100644 --- a/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs @@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Configuration /// /// Configuration options pertaining to user security. /// - sealed class SecurityConfiguration + public sealed class SecurityConfiguration { /// /// The key for the the resides in. diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 22ca5e00ee..f357240410 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -261,11 +261,17 @@ namespace Tgstation.Server.Host.Controllers }); } - Logger.LogInformation("{0} {1} instance {2}: {3} ({4})", AuthenticationContext.User.Name, attached ? "attached" : "created", newInstance.Name, newInstance.Id, newInstance.Path); + Logger.LogInformation( + "{userName} {attachedOrCreated} instance {instanceName}: {instanceId} ({instancePath})", + AuthenticationContext.User.Name, + attached ? "attached" : "created", + newInstance.Name, + newInstance.Id, + newInstance.Path); var api = newInstance.ToApi(); api.Accessible = true; // instances are always accessible by their creator - return attached ? (IActionResult)Json(api) : Created(api); + return attached ? Json(api) : Created(api); } /// @@ -337,9 +343,7 @@ namespace Tgstation.Server.Host.Controllers var moveJob = await InstanceQuery() .SelectMany(x => x.Jobs). -#pragma warning disable CA1310 // Specify StringComparison Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix)) -#pragma warning restore CA1310 // Specify StringComparison .Select(x => new Job { Id = x.Id, @@ -451,7 +455,7 @@ namespace Tgstation.Server.Host.Controllers } catch (Exception e) { - if (!(e is OperationCanceledException)) + if (e is not OperationCanceledException) Logger.LogError(e, "Error changing instance online state!"); originalModel.Online = originalOnline; originalModel.DreamDaemonSettings.AutoStart = oldAutoStart; @@ -497,7 +501,7 @@ namespace Tgstation.Server.Host.Controllers } await CheckAccessible(api, cancellationToken); - return moving ? (IActionResult)Accepted(api) : Json(api); + return moving ? Accepted(api) : Json(api); } #pragma warning restore CA1502 @@ -540,9 +544,7 @@ namespace Tgstation.Server.Host.Controllers var moveJobs = await GetBaseQuery() .SelectMany(x => x.Jobs) -#pragma warning disable CA1310 // Specify StringComparison .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix)) -#pragma warning restore CA1310 // Specify StringComparison .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy) .Include(x => x.Instance) .ToListAsync(cancellationToken) @@ -620,9 +622,7 @@ namespace Tgstation.Server.Host.Controllers var moveJob = await QueryForUser() .SelectMany(x => x.Jobs) -#pragma warning disable CA1310 // Specify StringComparison .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix)) -#pragma warning restore CA1310 // Specify StringComparison .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy) .FirstOrDefaultAsync(cancellationToken) ; @@ -764,11 +764,10 @@ namespace Tgstation.Server.Host.Controllers /// or a new with full rights. InstancePermissionSet InstanceAdminPermissionSet(InstancePermissionSet permissionSetToModify) { - if (permissionSetToModify == null) - permissionSetToModify = new InstancePermissionSet() - { - PermissionSetId = AuthenticationContext.PermissionSet.Id.Value, - }; + permissionSetToModify ??= new InstancePermissionSet() + { + PermissionSetId = AuthenticationContext.PermissionSet.Id.Value, + }; permissionSetToModify.ByondRights = RightsHelper.AllRights(); permissionSetToModify.ChatBotRights = RightsHelper.AllRights(); permissionSetToModify.ConfigurationRights = RightsHelper.AllRights(); diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 5e0ad1f486..b59a2e19d6 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -54,7 +54,7 @@ namespace Tgstation.Server.Host.Core /// Sets up dependency injection. /// #pragma warning disable CA1506 - sealed class Application : SetupApplication + public sealed class Application : SetupApplication { /// /// The for the . @@ -387,6 +387,11 @@ namespace Tgstation.Server.Host.Core this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory)); + if (instanceManager == null) + throw new ArgumentNullException(nameof(instanceManager)); + if (serverPortProvider == null) + throw new ArgumentNullException(nameof(serverPortProvider)); + var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions)); var generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); diff --git a/src/Tgstation.Server.Host/IServerFactory.cs b/src/Tgstation.Server.Host/IServerFactory.cs index 679ab1d421..7ec50612ed 100644 --- a/src/Tgstation.Server.Host/IServerFactory.cs +++ b/src/Tgstation.Server.Host/IServerFactory.cs @@ -8,7 +8,7 @@ namespace Tgstation.Server.Host /// /// For creating s. /// - interface IServerFactory + public interface IServerFactory { /// /// The for the . diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs index 1783a03993..3c0f504986 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs @@ -18,10 +18,10 @@ namespace Tgstation.Server.Host.Security.OAuth public override OAuthProvider Provider => OAuthProvider.TGForums; /// - protected override Uri TokenUrl => new Uri("https://tgstation13.org/phpBB/app.php/tgapi/oauth/token"); + protected override Uri TokenUrl => new ("https://tgstation13.org/phpBB/app.php/tgapi/oauth/token"); /// - protected override Uri UserInformationUrl => new Uri("https://tgstation13.org/phpBB/app.php/tgapi/user/me"); + protected override Uri UserInformationUrl => new ("https://tgstation13.org/phpBB/app.php/tgapi/user/me"); /// /// Initializes a new instance of the class. @@ -50,6 +50,6 @@ namespace Tgstation.Server.Host.Security.OAuth protected override string DecodeUserInformationPayload(dynamic responseJson) => responseJson.phpbb_username; /// - protected override OAuthTokenRequest CreateTokenRequest(string code) => new OAuthTokenRequest(OAuthConfiguration, code, "user"); + protected override OAuthTokenRequest CreateTokenRequest(string code) => new (OAuthConfiguration, code, "user"); } } diff --git a/src/Tgstation.Server.Host/Setup/IPostSetupServices.cs b/src/Tgstation.Server.Host/Setup/IPostSetupServices.cs index e531d97029..8f4e161179 100644 --- a/src/Tgstation.Server.Host/Setup/IPostSetupServices.cs +++ b/src/Tgstation.Server.Host/Setup/IPostSetupServices.cs @@ -6,7 +6,7 @@ namespace Tgstation.Server.Host.Setup /// /// Set of objects needed to configure an . /// - interface IPostSetupServices + public interface IPostSetupServices { /// /// The . diff --git a/src/Tgstation.Server.Host/Setup/SetupApplication.cs b/src/Tgstation.Server.Host/Setup/SetupApplication.cs index 98b46c4e20..290bd748d0 100644 --- a/src/Tgstation.Server.Host/Setup/SetupApplication.cs +++ b/src/Tgstation.Server.Host/Setup/SetupApplication.cs @@ -17,17 +17,17 @@ namespace Tgstation.Server.Host.Setup /// /// DI root for configuring a . /// - class SetupApplication + public class SetupApplication { /// /// The for the . /// - protected static readonly IAssemblyInformationProvider AssemblyInformationProvider = new AssemblyInformationProvider(); + public static readonly IAssemblyInformationProvider AssemblyInformationProvider = new AssemblyInformationProvider(); /// /// The for the . /// - protected static readonly IIOManager IOManager = new DefaultIOManager(AssemblyInformationProvider); + public static readonly IIOManager IOManager = new DefaultIOManager(AssemblyInformationProvider); /// /// The for the . diff --git a/tgstation-server.sln b/tgstation-server.sln index 257b86cc4d..320a2ee01e 100644 --- a/tgstation-server.sln +++ b/tgstation-server.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29728.190 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32825.248 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DA32568D-1D8D-4A4C-9943-BFD3CE796B3F}" ProjectSection(SolutionItems) = preProject @@ -73,11 +73,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tgs", "tgs", "{F7765A4B-021 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "core", "core", "{DCCBA9DA-47BA-4C70-823B-E99A3ACA0377}" ProjectSection(SolutionItems) = preProject - src\DMAPI\tgs\core\_definitions.dm = src\DMAPI\tgs\core\_definitions.dm src\DMAPI\tgs\core\core.dm = src\DMAPI\tgs\core\core.dm src\DMAPI\tgs\core\datum.dm = src\DMAPI\tgs\core\datum.dm src\DMAPI\tgs\core\README.md = src\DMAPI\tgs\core\README.md src\DMAPI\tgs\core\tgs_version.dm = src\DMAPI\tgs\core\tgs_version.dm + src\DMAPI\tgs\core\_definitions.dm = src\DMAPI\tgs\core\_definitions.dm EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "v3210", "v3210", "{1B228ACB-60C3-4DF9-B716-9F0BD31F6766}" @@ -135,12 +135,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{316141B0 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "v5", "v5", "{FAEAD3B5-2EAB-465C-A9C3-E8CB6AAA7131}" ProjectSection(SolutionItems) = preProject - src\DMAPI\tgs\v5\_defines.dm = src\DMAPI\tgs\v5\_defines.dm src\DMAPI\tgs\v5\api.dm = src\DMAPI\tgs\v5\api.dm src\DMAPI\tgs\v5\commands.dm = src\DMAPI\tgs\v5\commands.dm src\DMAPI\tgs\v5\interop_version.dm = src\DMAPI\tgs\v5\interop_version.dm src\DMAPI\tgs\v5\README.md = src\DMAPI\tgs\v5\README.md src\DMAPI\tgs\v5\undefs.dm = src\DMAPI\tgs\v5\undefs.dm + src\DMAPI\tgs\v5\_defines.dm = src\DMAPI\tgs\v5\_defines.dm EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LongRunning", "LongRunning", "{EB1DDE8C-CA6F-4BE3-947B-597CA8EABEA5}" @@ -185,6 +185,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEM EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Host.Tests.Signals", "tests\Tgstation.Server.Host.Tests.Signals\Tgstation.Server.Host.Tests.Signals.csproj", "{5813CC33-B16C-485D-A74D-20204DDF6542}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tgstation.Server.Migrator", "tools\Tgstation.Server.Migrator\Tgstation.Server.Migrator.csproj", "{CE499888-B22B-457C-891E-0EA9DC317228}" + ProjectSection(ProjectDependencies) = postProject + {07ED0FD5-E46B-4841-931D-BA2B673E16B2} = {07ED0FD5-E46B-4841-931D-BA2B673E16B2} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tgstation.Server.Migrator.Comms", "tools\Tgstation.Server.Migrator.Comms\Tgstation.Server.Migrator.Comms.csproj", "{07ED0FD5-E46B-4841-931D-BA2B673E16B2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -309,6 +316,22 @@ Global {5813CC33-B16C-485D-A74D-20204DDF6542}.Release|Any CPU.Build.0 = Release|Any CPU {5813CC33-B16C-485D-A74D-20204DDF6542}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU {5813CC33-B16C-485D-A74D-20204DDF6542}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.Release|Any CPU.Build.0 = Release|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.Release|Any CPU.Build.0 = Release|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -336,6 +359,8 @@ Global {28CDEB8F-2B2A-47A2-985B-5E2487E8D096} = {E82104F4-F5C4-4786-ACD4-B635166CDB21} {CFFD7992-E73A-4D1F-9D7A-C817C07B7BEB} = {E82104F4-F5C4-4786-ACD4-B635166CDB21} {5813CC33-B16C-485D-A74D-20204DDF6542} = {316141B0-CD21-4769-A013-D53DA9B9EC09} + {CE499888-B22B-457C-891E-0EA9DC317228} = {A55C1117-5808-4AB2-BEA6-4D4A3E66A2F2} + {07ED0FD5-E46B-4841-931D-BA2B673E16B2} = {A55C1117-5808-4AB2-BEA6-4D4A3E66A2F2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DFD36C95-3E49-41C7-ACDB-86BAF5B18A79} diff --git a/tools/Tgstation.Server.Migrator.Comms/Program.cs b/tools/Tgstation.Server.Migrator.Comms/Program.cs new file mode 100644 index 0000000000..826e07a80c --- /dev/null +++ b/tools/Tgstation.Server.Migrator.Comms/Program.cs @@ -0,0 +1,390 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http.Headers; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +using TGS.Interface; + +using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Api.Models.Request; +using Tgstation.Server.Client; + +static class Program +{ + static async Task Main(string[] args) + { + try + { + var tgs3Client = new Client(); + + switch (args[0]) + { + case "--verify-connection": + var status = tgs3Client.ConnectionStatus(out var error); + if (status != ConnectivityLevel.Administrator) + { + Console.WriteLine($"Connection Error: {error}"); + return 3; + } + return 0; + case "--migrate": + ushort apiPort = ushort.Parse(args[1]); + return await Migrate(tgs3Client, apiPort); + default: + return 2; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + return 1; + } + } + + static async Task Migrate(IClient tgs3Client, ushort apiPort) + { +#if DEBUG + Console.WriteLine("Test log line..."); + Debugger.Launch(); +#endif + Console.WriteLine("Connecting to TGS3..."); + var status = tgs3Client.ConnectionStatus(out var tgs3Error); + if (status != ConnectivityLevel.Administrator) + { + Console.WriteLine($"Connection Error: {tgs3Client}"); + return 13; + } + + Console.WriteLine("Connected!"); + + Console.WriteLine("Connecting to TGS5..."); + var assemblyName = Assembly.GetExecutingAssembly().GetName(); + var productInfoHeaderValue = + new ProductInfoHeaderValue( + assemblyName.Name!, + assemblyName.Version!.Semver().ToString()); + + var serverUrl = new Uri($"http://localhost:{apiPort}"); + var clientFactory = new ServerClientFactory(productInfoHeaderValue.Product); + var tgs5Client = await clientFactory.CreateFromLogin( + serverUrl, + DefaultCredentials.AdminUserName, + DefaultCredentials.DefaultAdminUserPassword); + + Console.WriteLine("Connected!"); + + // we do this synchronously and patiently because we aren't chumbii and this is delicate + // We need clear logs + var tgs3Instances = tgs3Client.Server.Instances.ToList(); + foreach (var tgs3Instance in tgs3Instances) + { + var instanceName = tgs3Instance.Metadata.Name; + var instancePath = tgs3Instance.Metadata.Path; + + if (!tgs3Instance.Metadata.Enabled) + { + Console.WriteLine($"Skipping instance {instanceName} at {instancePath}. Disabled."); + continue; + } + + Console.WriteLine($"Migrating instance {instanceName} at {instancePath}"); + + RepositoryUpdateRequest? repositoryUpdateRequest = null; + if (tgs3Instance.Repository.Exists()) + { + Console.WriteLine("Gathering instance repository data..."); + repositoryUpdateRequest = new RepositoryUpdateRequest + { + CommitterEmail = tgs3Instance.Repository.GetCommitterEmail(), + CommitterName = tgs3Instance.Repository.GetCommitterName(), + UpdateSubmodules = true, // default in 3 + Reference = tgs3Instance.Repository.GetBranch(out tgs3Error), + }; + + if (tgs3Error != null) + { + Console.WriteLine($"Error retrieving current branch: {tgs3Error}"); + } + } + else + Console.WriteLine("Instance has no repository, that's fine."); + + Console.WriteLine("Gather DreamDaemon and DreamMaker data..."); + var dreamDaemonRequest = new DreamDaemonRequest + { + AllowWebClient = tgs3Instance.DreamDaemon.Webclient(), + AutoStart = tgs3Instance.DreamDaemon.Autostart(), + Port = tgs3Instance.DreamDaemon.Port(), + SecurityLevel = tgs3Instance.DreamDaemon.SecurityLevel() switch + { + TGS.Interface.DreamDaemonSecurity.Safe => Tgstation.Server.Api.Models.DreamDaemonSecurity.Safe, + TGS.Interface.DreamDaemonSecurity.Ultrasafe => Tgstation.Server.Api.Models.DreamDaemonSecurity.Ultrasafe, + _ => Tgstation.Server.Api.Models.DreamDaemonSecurity.Trusted, + } + }; + + var dreamMakerRequest = new DreamMakerRequest + { + ApiValidationSecurityLevel = dreamDaemonRequest.SecurityLevel, + ApiValidationPort = (ushort)(dreamDaemonRequest.Port + 111) // Best rotation we can do... + }; + + Console.WriteLine("Gathering chat data..."); + var providerInfos = tgs3Instance.Chat.ProviderInfos(); + var chatBotCreateRequests = new List(); + foreach(var providerInfo in providerInfos) + { + if (!providerInfo.Enabled) + continue; + + var createRequest = new ChatBotCreateRequest() + { + Provider = providerInfo.Provider switch + { + TGS.Interface.ChatProvider.Discord => Tgstation.Server.Api.Models.ChatProvider.Discord, + _ => Tgstation.Server.Api.Models.ChatProvider.Irc, + }, + Enabled = true, + ReconnectionInterval = 5, + }; + + var isDiscordProvider = createRequest.Provider == Tgstation.Server.Api.Models.ChatProvider.Discord; + createRequest.Name = isDiscordProvider + ? "Discord Bot" + : "IRC Bot"; + + Console.WriteLine($"Gathering data for {createRequest.Name}..."); + + ChatConnectionStringBuilder csb; + if (createRequest.Provider == Tgstation.Server.Api.Models.ChatProvider.Discord) + { + var discordSetupInfo = new DiscordSetupInfo(providerInfo); + csb = new DiscordConnectionStringBuilder + { + BasedMeme = false, + DMOutputDisplay = DiscordDMOutputDisplayType.Always, + BotToken = discordSetupInfo.BotToken + }; + + } + else + { + var ircSetupInfo = new IRCSetupInfo(providerInfo); + csb = new IrcConnectionStringBuilder + { + Address = ircSetupInfo.URL, + Nickname = ircSetupInfo.Nickname, + Port = ircSetupInfo.Port + }; + } + createRequest.ConnectionString = csb.ToString(); + + createRequest.Channels = new List(); + + static string NormalizeChannelId(string channelId) => channelId.ToLowerInvariant().Trim(); + + var distinctChannels = providerInfo.WatchdogChannels + .Union(providerInfo.DevChannels) + .Union(providerInfo.AdminChannels) + .Union(providerInfo.GameChannels) + .Select(NormalizeChannelId) + .Distinct(); + + foreach(var channelIdentifier in distinctChannels) + { + var newChatChannel = new ChatChannel + { + IsWatchdogChannel = providerInfo.WatchdogChannels.Any(x => NormalizeChannelId(x) == channelIdentifier), + IsAdminChannel = providerInfo.AdminChannels.Any(x => NormalizeChannelId(x) == channelIdentifier), + IsUpdatesChannel = providerInfo.DevChannels.Any(x => NormalizeChannelId(x) == channelIdentifier), + }; + + if (isDiscordProvider) + newChatChannel.DiscordChannelId = ulong.Parse(channelIdentifier); + else + newChatChannel.IrcChannel = channelIdentifier; + + createRequest.Channels.Add(newChatChannel); + } + + chatBotCreateRequests.Add(createRequest); + } + + Console.WriteLine("Detaching TGS3 instance..."); + tgs3Client.Server.InstanceManager.DetachInstance(instanceName); + + Console.WriteLine("Creating TGS5 attach file..."); + File.WriteAllText(Path.Combine(instancePath, "TGS4_ALLOW_INSTANCE_ATTACH"), String.Empty); + + Console.WriteLine("Checking BYOND install..."); + var byondDirectory = Path.Combine(instancePath, "BYOND"); + var byondVersionFile = Path.Combine(byondDirectory, "byond_version.dat"); + + ByondVersionRequest? byondVersionRequest = null; + if (Directory.Exists(byondDirectory) && File.Exists(byondVersionFile)) + { + var byondVersion = Version.Parse(File.ReadAllText(byondVersionFile).Trim()); + Console.WriteLine($"Found installed BYOND version: {byondVersion.Major}.{byondVersion.Minor}"); + byondVersionRequest = new ByondVersionRequest + { + Version = byondVersion + }; + } + + var oldStaticDirectory = Path.Combine(instancePath, "Static"); + var newConfigurationDirectory = Path.Combine(instancePath, "Configuration"); + if (Directory.Exists(oldStaticDirectory)) + { + Console.WriteLine("Migrating Static to Configuration/GameStaticFiles"); + var gameStaticFilesDirectory = Path.Combine(newConfigurationDirectory, "GameStaticFiles"); + Directory.CreateDirectory(newConfigurationDirectory); + Directory.Move(oldStaticDirectory, gameStaticFilesDirectory); + + Console.WriteLine("Moving code modifications..."); + var codeModsDirectory = Path.Combine(newConfigurationDirectory, "CodeModifications"); + Directory.CreateDirectory(codeModsDirectory); + + var allDmFiles = Directory.EnumerateFiles(gameStaticFilesDirectory, "*.dm", SearchOption.TopDirectoryOnly).ToList(); + foreach (var dmFile in allDmFiles) + { + File.Move(Path.Combine(gameStaticFilesDirectory, dmFile), Path.Combine(codeModsDirectory, Path.GetFileName(dmFile))); + } + + var allDmeFiles = Directory.EnumerateFiles(gameStaticFilesDirectory, "*.dme", SearchOption.TopDirectoryOnly).ToList(); + if (allDmeFiles.Any()) + { + foreach (var dmeFile in allDmeFiles) + { + File.Move(Path.Combine(gameStaticFilesDirectory, dmeFile), Path.Combine(codeModsDirectory, Path.GetFileName(dmeFile))); + } + } + else if (allDmFiles.Any()) + { + Console.WriteLine("Generating HeadInclude.dm..."); + var headIncludeBuilder = new StringBuilder(); + foreach (var dmFile in allDmFiles.OrderBy(fileName => fileName.ToUpperInvariant())) + { + headIncludeBuilder.Append("#include \""); + headIncludeBuilder.Append(Path.GetFileName(dmFile)); + headIncludeBuilder.Append("\""); + headIncludeBuilder.Append(Environment.NewLine); + } + + File.WriteAllText(Path.Combine(codeModsDirectory, "HeadInclude.dm"), headIncludeBuilder.ToString()); + } + } + + var eventHandlersDirectory = Path.Combine(instancePath, "EventHandlers"); + if (Directory.Exists(eventHandlersDirectory)) + { + Console.WriteLine("Moving event scripts..."); + Directory.CreateDirectory(newConfigurationDirectory); + Directory.Move(eventHandlersDirectory, Path.Combine(newConfigurationDirectory, "EventScripts")); + } + + var diagnosticsDirectory = Path.Combine(instancePath, "Diagnostics"); + var minidumpsDirectory = Path.Combine(diagnosticsDirectory, "Minidumps"); + if (Directory.Exists(minidumpsDirectory)) + { + Console.WriteLine("Renaming Minidumps folder to ProcessDumps..."); + Directory.Move(minidumpsDirectory, Path.Combine(diagnosticsDirectory, "ProcessDumps")); + } + + Console.WriteLine("Deleting BYOND folder..."); + await RecursivelyDeleteDirectory(new DirectoryInfo(byondDirectory)); + Console.WriteLine("Deleting RepoKey folder..."); + await RecursivelyDeleteDirectory(new DirectoryInfo(Path.Combine(instancePath, "RepoKey"))); + Console.WriteLine("Deleting Game folder..."); + await RecursivelyDeleteDirectory(new DirectoryInfo(Path.Combine(instancePath, "Game"))); + Console.WriteLine("Deleting Instance.json..."); + File.Delete(Path.Combine(instancePath, "Instance.json")); + Console.WriteLine("Deleting prtestjob.json..."); + File.Delete(Path.Combine(instancePath, "prtestjob.json")); + Console.WriteLine("Deleting TGS3.json..."); + File.Delete(Path.Combine(instancePath, "TGS3.json")); + Console.WriteLine("Deleting TGDreamDaemonBridge.dll..."); + File.Delete(Path.Combine(instancePath, "TGDreamDaemonBridge.dll")); + + Console.WriteLine("Attaching TGS5 instance..."); + var tgs5Instance = await tgs5Client.Instances.CreateOrAttach(new InstanceCreateRequest + { + ConfigurationType = ConfigurationType.Disallowed, + Name = instanceName, + Path = instancePath, + }, default); + + Console.WriteLine($"Onlining TGS5 instance ID {tgs5Instance.Id}..."); + tgs5Instance = await tgs5Client.Instances.Update(new InstanceUpdateRequest + { + Online = true, + Id = tgs5Instance.Id + }, default); + + var v5InstanceClient = tgs5Client.Instances.CreateClient(tgs5Instance); + + if (byondVersionRequest != null) + { + Console.WriteLine("Triggering BYOND install job..."); + await v5InstanceClient.Byond.SetActiveVersion(byondVersionRequest, null, default); + } + + if (repositoryUpdateRequest != null) + { + Console.WriteLine("Updating repository settings..."); + await v5InstanceClient.Repository.Update(repositoryUpdateRequest, default); + } + + Console.WriteLine("Updating deployment settings..."); + await v5InstanceClient.DreamMaker.Update(dreamMakerRequest, default); + + Console.WriteLine("Updating DreamDaemon settings..."); + await v5InstanceClient.DreamDaemon.Update(dreamDaemonRequest, default); + + foreach(var chatBotCreateRequest in chatBotCreateRequests) + { + Console.WriteLine($"Creating chat bot {chatBotCreateRequest.Name}..."); + await v5InstanceClient.ChatBots.Create(chatBotCreateRequest, default); + } + + Console.WriteLine($"Instance {instanceName} (TGS5 ID: {tgs5Instance.Id}) successfully migrated!"); + } + + Console.WriteLine("All enabled V3 instances migrated into V5 and detached from V3!"); + + return 0; + } + + static async Task RecursivelyDeleteDirectory(DirectoryInfo dir) + { + var tasks = new List(); + + if (!dir.Exists) + return; + + // check if we are a symbolic link + if (!dir.Attributes.HasFlag(FileAttributes.Directory) || dir.Attributes.HasFlag(FileAttributes.ReparsePoint)) + { + dir.Delete(); + return; + } + + foreach (var subDir in dir.EnumerateDirectories()) + tasks.Add(RecursivelyDeleteDirectory(subDir)); + + foreach (var file in dir.EnumerateFiles()) + { + file.Attributes = FileAttributes.Normal; + file.Delete(); + } + + await Task.WhenAll(tasks); + dir.Delete(true); + } +} diff --git a/tools/Tgstation.Server.Migrator.Comms/Tgstation.Server.Migrator.Comms.csproj b/tools/Tgstation.Server.Migrator.Comms/Tgstation.Server.Migrator.Comms.csproj new file mode 100644 index 0000000000..e952cf4ed1 --- /dev/null +++ b/tools/Tgstation.Server.Migrator.Comms/Tgstation.Server.Migrator.Comms.csproj @@ -0,0 +1,24 @@ + + + + Exe + net472 + win-x86 + $(TgsMigratorVersion) + latest + enable + + + + + + + + + + + + + + + diff --git a/tools/Tgstation.Server.Migrator/Program.cs b/tools/Tgstation.Server.Migrator/Program.cs new file mode 100644 index 0000000000..9a175fb86c --- /dev/null +++ b/tools/Tgstation.Server.Migrator/Program.cs @@ -0,0 +1,524 @@ +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Configuration.Install; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Management; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; +using System.Security.Principal; +using System.ServiceProcess; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +using Octokit; + +using Tgstation.Server.Api; +using Tgstation.Server.Client; +using Tgstation.Server.Host.Setup; + +using FileMode = System.IO.FileMode; + +[DoesNotReturn] +static void ExitPause(int exitCode) +{ + Console.WriteLine("Consider saving the console text to report issues. Press any key to exit..."); + Console.ReadKey(); + Environment.Exit(exitCode); +} + +try +{ + var commandLine = Environment.GetCommandLineArgs(); + var commandLineArguments = commandLine.Skip(1); + var skipPreamble = commandLineArguments.Any(x => x.Equals("--skip-preamble", StringComparison.OrdinalIgnoreCase)); + + Console.WriteLine("This is a very straightfoward script to migrate the instances of a TGS3 install into a new TGS5 install"); + + static bool PromptYesOrNo(string question) + { + Console.Write($"{question} (y/n):"); + var character = Console.ReadKey(); + Console.WriteLine(); + return character.KeyChar.ToString().ToUpperInvariant() == "Y"; + } + + // WORKING DIRECTORY CHECK + var currentAssembly = Assembly.GetExecutingAssembly(); + if(Path.GetDirectoryName(Path.GetFullPath(currentAssembly.Location))!.Replace("\\", "/").ToUpperInvariant() + != Path.GetFullPath(Environment.CurrentDirectory).Replace("\\", "/").ToUpperInvariant()) + { + Console.WriteLine("Please keep the working directory equivalent to the program directory for this migration!"); + ExitPause(8); + } + + // PREREQUISITE CHECK + if (!skipPreamble) + { + Console.WriteLine("We need to ensure you're running this program as Administrator because there are several operations we'll do that require it."); + Console.WriteLine("If not, you will be prompted to elevate this process."); + } + + // ADMINISTRATOR CHECK + static bool IsAdministrator() + { + using var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + + if (!IsAdministrator()) + { + Console.WriteLine("Not running as admin. Elevating process..."); + var selfExecutable = commandLine.First(); + var selfArguments = String.Join(" ", commandLineArguments) + " --skip-preamble"; + using var elevatedProcess = new Process(); + elevatedProcess.StartInfo.UseShellExecute = true; + elevatedProcess.StartInfo.FileName = selfExecutable; + elevatedProcess.StartInfo.Arguments = selfArguments; + elevatedProcess.StartInfo.Verb = "runas"; + + elevatedProcess.Start(); + ExitPause(0); + } + + Console.WriteLine("Administrative privileges confirmed."); + + // TGS3 SERVICE CHECK + + const string PathToCommsBinary = +#if DEBUG + "../../../../../Tgstation.Server.Migrator.Comms/bin/Debug/net472/win-x86/" + +#endif + "Tgstation.Server.Migrator.Comms.exe"; + + Console.WriteLine($"Checking {PathToCommsBinary} exists..."); + if (!File.Exists(PathToCommsBinary)) + { + Console.WriteLine("Could not find WCF comms binary!"); + ExitPause(7); + } + + static int RunComms(string command) + { + using var commsProcess = new Process(); + commsProcess.StartInfo.FileName = PathToCommsBinary; + commsProcess.StartInfo.Arguments = command; + commsProcess.Start(); + commsProcess.WaitForExit(); + return commsProcess.ExitCode; + } + + Console.WriteLine("Checking for TGS3 service..."); + const string OldServiceName = "TG Station Server"; + const string NewServiceName = "tgstation-server"; + + static ServiceController GetTgs3Service(bool checkNewOneIsntInstalled) + { + var allServices = ServiceController.GetServices(); + var tgs3Service = allServices.FirstOrDefault(service => service.ServiceName == OldServiceName); + foreach (var service in allServices) + { + if (service == tgs3Service) + continue; + + if (checkNewOneIsntInstalled && (service.ServiceName == NewServiceName || service.ServiceName == "tgstation-server-4")) + { + Console.WriteLine("Detected existing TGS4+ install! Cannot continue. Please uninstall any versions of TGS4+ before continuing."); + ExitPause(10); + } + + service.Dispose(); + } + + if (checkNewOneIsntInstalled) + Console.WriteLine("TGS4+ service install not detected."); + + if (tgs3Service == null) + { + Console.WriteLine("TGS3 is not installed on this machine!"); + ExitPause(5); + } + + return tgs3Service; + } + + var tgs3Service = GetTgs3Service(true); + + if (tgs3Service.Status != ServiceControllerStatus.Running) + { + Console.WriteLine("TGS3 service is installed but not running! Please start the service before continuing."); + ExitPause(9); + } + + // TGS3 CONNECTION CHECK + Console.WriteLine("Checking TGS3 connection..."); + var commsExitCode = RunComms("--verify-connection"); + if(commsExitCode != 0) + { + Console.WriteLine("Could not connect to TGS3 as administrator!"); + ExitPause(6); + } + + // USER INPUT + Console.WriteLine("We've confirmed you have have both TGS3 installed and TGS4+ service UNinstalled on THIS machine."); + Console.WriteLine(); + Console.WriteLine("Please read all of the following CAREFULLY before proceeding:"); + Console.WriteLine("Confirm you want to migrate to the latest version installing the necessary prerequisite .NET version along the way."); + Console.WriteLine("Please note that this is a one way upgrade and will not keep your DreamDaemon servers running throughout it."); + Console.WriteLine("All TGS3 instances will be migrated in place. The following components will be preserved:"); + Console.WriteLine("- Repository (No test merge data or SSH key)"); + Console.WriteLine("- BYOND version (redownloaded from byond.com)"); + Console.WriteLine("- A FEW server configuration settings (Committer info, Autostart, Webclient, Game Port, Security Level)"); + Console.WriteLine("- EventHandlers"); + Console.WriteLine("- Chat Bots, if enabled"); + Console.WriteLine(" - TGS4+ doesn't support individual user/group identification. Admin channels will be used instead"); + Console.WriteLine(" - IRC authentication information cannot be copied and must be manually adjusted"); + Console.WriteLine("- Static Files"); + Console.WriteLine("- Code Modifications"); + Console.WriteLine("Remaining components such as logins, game builds, etc. can be recreated once the migration is complete."); + Console.WriteLine("IMPORTANT NOTES:"); + Console.WriteLine("- INSTANCES CANNOT HAVE GAME PORTS OFFSET 111 UNITS FROM EACH OTHER OR HIGHER THAN 65423! WE AREN'T CORRECTING FOR THIS WHILE MIGRATING!"); + Console.WriteLine("- DISABLED INSTANCES WILL NOT BE MIGRATED! PLEASE ENABLE ALL INSTANCES YOU WISH TO MIGRATE BEFORE CONTINUING!"); + Console.WriteLine("- INSTANCE AUTO UPDATE CAN INTERFERE WITH THE MIGRATION! PLEASE DISABLE IT ON ALL INSTANCES BEING MIGRATED BEFORE CONTINUING!"); + Console.WriteLine("- DO NOT ATTEMPT TO USE TGS3 VIA NORMAL METHODS WHILE THIS MIGRATION IS TAKING PLACE OR YOU COULD CORRUPT YOUR DATA!"); + Console.WriteLine("Side note: You can skip the TGS5 setup wizard step by copying your premade appsettings.Production.yml file next to this .exe NOW."); + if (!PromptYesOrNo("Proceed with upgrade?")) + { + Console.WriteLine("Prerequisite not met."); + ExitPause(0); + } + + string? tgsInstallPath = null; + do + { + Console.WriteLine("Please enter the directory where you would like the TGS binaries installed."); + Console.Write("This may be anywhere but should be empty: "); + tgsInstallPath = Console.ReadLine(); + if (!String.IsNullOrWhiteSpace(tgsInstallPath)) + { + if (!Path.IsPathRooted(tgsInstallPath)) + { + Console.WriteLine("Please do not use a relative path for this. Enter the full path including the drive letter."); + tgsInstallPath = null; + } + else if (Path.GetInvalidPathChars().Any(invalidChar => tgsInstallPath.Contains(invalidChar))) + { + Console.WriteLine("Invalid characters detected!"); + tgsInstallPath = null; + } + } + } + while (String.IsNullOrWhiteSpace(tgsInstallPath)); + + Console.WriteLine("Attempting to create TGS install directory..."); + Directory.CreateDirectory(tgsInstallPath); + + // ASP.NET 6.0 RUNTIME CHECK + Console.WriteLine("Next step, we need to ensure the .NET 4.7.2 and ASP.NET Core 6 runtimes are installed on your machine."); + Console.WriteLine("We are assuming you already have .NET 4.7.2 installed if you're running TGS3 and this program. So we're going to download .NET 6 for you."); + Console.WriteLine("Yes, this program runs .NET 6, but it contains the entire runtime embedded into it. You will need a system-wide install for TGS."); + + var runtimeInstalled = true; // assume for now + using (var dotnetRuntimeCheck = new Process()) + { + dotnetRuntimeCheck.StartInfo.FileName = "C:/Program Files/dotnet/dotnet.exe"; + dotnetRuntimeCheck.StartInfo.Arguments = "--list-runtimes"; + dotnetRuntimeCheck.StartInfo.RedirectStandardOutput = true; + + try + { + dotnetRuntimeCheck.Start(); + + dotnetRuntimeCheck.WaitForExit(); + } + catch (Win32Exception ex) when (ex.NativeErrorCode == 2) + { + Console.WriteLine("Dotnet does not appear to be installed at all."); + runtimeInstalled = false; + } + + if (runtimeInstalled) + { + var versions = await dotnetRuntimeCheck.StandardOutput.ReadToEndAsync(); + var regex = new Regex("Microsoft\\.AspNetCore\\.App 6\\.0\\.[0-9]+"); + + if (!regex.IsMatch(versions)) + runtimeInstalled = false; + } + } + + // ASP.NET 6.0 RUNTIME SETUP + var assemblyName = currentAssembly.GetName(); + var productInfoHeaderValue = + new ProductInfoHeaderValue( + assemblyName.Name!, + assemblyName.Version!.Semver().ToString()); + if (!runtimeInstalled) + { + // RUNTIME DONWLOAD + Console.WriteLine("The version we are installing is the latest circa 26-09-2022, feel free to update it later if you want but that is not necessary."); + + var x64 = Environment.Is64BitOperatingSystem; + var xSubstitution = x64 ? "64" : "86"; + Console.WriteLine($"Running on an x{xSubstitution} system."); + + var downloadUri = new Uri(x64 + ? "https://download.visualstudio.microsoft.com/download/pr/98dbe241-8b77-4be0-b130-a5fb6af8d724/27b655adce6250da42be9440abe847a2/aspnetcore-runtime-6.0.9-win-x64.exe" + : "https://download.visualstudio.microsoft.com/download/pr/8f583028-b802-4661-b8dd-47139b0561ce/3c0cd3bdc6051759ccae40f78982c86e/aspnetcore-runtime-6.0.9-win-x86.exe"); + + var dotnetDownloadFilePath = $"aspnetcore-runtime-6.0.9-win-x{xSubstitution}.exe"; + + Console.WriteLine($"Downloading {downloadUri} to {Path.GetFullPath(dotnetDownloadFilePath)}..."); + + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.UserAgent.Add(productInfoHeaderValue); + var webRequestTask = httpClient.GetAsync(downloadUri); + using var response = await webRequestTask; + response.EnsureSuccessStatusCode(); + using (var responseStream = await response.Content.ReadAsStreamAsync()) + { + using var fileStream = new FileStream( + dotnetDownloadFilePath, + FileMode.Create, + FileAccess.Write, + FileShare.ReadWrite | FileShare.Delete, + 4096, + FileOptions.Asynchronous | FileOptions.SequentialScan); + await responseStream.CopyToAsync(fileStream); + } + + // RUNTIME INSTALLATION + Console.WriteLine("Runtime downloaded. Running silent installation..."); + bool silentInstallSuccess = true; + using var silentInstallProcess = new Process(); + { + silentInstallProcess.StartInfo.UseShellExecute = false; + silentInstallProcess.StartInfo.FileName = dotnetDownloadFilePath; + silentInstallProcess.StartInfo.Arguments = "/install /quiet /norestart"; + silentInstallProcess.Start(); + silentInstallProcess.WaitForExit(); + + if (silentInstallProcess.ExitCode != 0) + { + Console.WriteLine("Silent installation failed! Please install the runtime interactively."); + Console.WriteLine("Launching install dialog"); + silentInstallSuccess = false; + } + } + + if (!silentInstallSuccess) + { + using var installProcess = new Process(); + installProcess.StartInfo.FileName = dotnetDownloadFilePath; + installProcess.Start(); + installProcess.WaitForExit(); + + if (!PromptYesOrNo("Was the installation successful?")) + { + Console.WriteLine("Cannot continue without ASP.NET 6.0 runtime installed."); + ExitPause(2); + } + } + } + else + { + Console.WriteLine("Runtime detected successfully. Continuing..."); + } + + + // TGS5 ONLINE LOCATING + Console.WriteLine("Now we're going to locate the latest version of the TGS service."); + Console.WriteLine("(This migrator does not support the console runner, but you may switch the installation to it after completion)"); + + Console.WriteLine("Determining latest version of TGS 5.X.X..."); + + var gitHubClient = new GitHubClient(new Octokit.ProductHeaderValue(productInfoHeaderValue.Product!.Name, productInfoHeaderValue.Product.Version)); + + string? gitHubPat = Environment.GetEnvironmentVariable("TGS_MIGRATOR_GITHUB_PAT"); + if (gitHubPat != null) + gitHubClient.Credentials = new Credentials(gitHubPat); + + const int TgstationServerRepoId = 92952846; + + var allReleases = await gitHubClient.Repository.Release.GetAll(TgstationServerRepoId); + + const string VersionFiveTagPrefix = "tgstation-server-v5."; + var allVersionFiveReleases = allReleases + .Where(release => release.TagName.StartsWith(VersionFiveTagPrefix)); + var latestVersionFiveRelease = allVersionFiveReleases + .OrderByDescending(release => Version.Parse(release.TagName[(VersionFiveTagPrefix.Length - 2)..])) + .FirstOrDefault(); + + if (latestVersionFiveRelease == null) + { + Console.WriteLine("Unable to determine latest version 5 release!"); + ExitPause(3); + } + + Console.WriteLine($"Latest V5 version: {latestVersionFiveRelease.TagName}"); + + var serverServiceAsset = latestVersionFiveRelease.Assets.FirstOrDefault(asset => asset.Name == "ServerService.zip"); + if (serverServiceAsset == null) + { + Console.WriteLine("Unable to determine ServerService.zip release asset!"); + ExitPause(4); + } + + // TGS5 SETUP WIZARD + Console.WriteLine("We are now going to run the TGS setup wizard to generate your new server configuration file."); + + var serverFactory = Tgstation.Server.Host.Core.Application.CreateDefaultServerFactory(); + _ = await serverFactory.CreateServer(new[] { $"General:SetupWizardMode={SetupWizardMode.Only}" }, null, default); // This is where the wizard actually runs + + // TGS5 DOWNLOAD AND UNZIP + Console.WriteLine("Downloading TGS5..."); + using (var tgsFiveZipMemoryStream = await SetupApplication.IOManager.DownloadFile(new Uri(serverServiceAsset.BrowserDownloadUrl), default)) + { + Console.WriteLine("Unzipping TGS5..."); + await SetupApplication.IOManager.ZipToDirectory(tgsInstallPath, tgsFiveZipMemoryStream, default); + } + + // TGS5 CONFIG SETUP + const string ConfigurationFileName = "appsettings.Production.yml"; + Console.WriteLine("Extracting API port from configuration..."); + ushort configuredApiPort; + { + var configFileContents = await File.ReadAllTextAsync(ConfigurationFileName); + var match = Regex.Match(configFileContents, "ApiPort: ([0-9]+)"); + if (!match.Success) + { + Console.WriteLine("Unable to extract ApiPort setting!"); + ExitPause(12); + } + + configuredApiPort = ushort.Parse(match.Groups[1].Value); + } + + Console.WriteLine("Moving configuration file from setup wizard to installation folder..."); + File.Copy(ConfigurationFileName, Path.Combine(tgsInstallPath, ConfigurationFileName)); + + // TGS5 SERVICE SETUP + Console.WriteLine("Installing TGS5 service..."); + using (var processInstaller = new ServiceProcessInstaller()) + using (var installer = new ServiceInstaller()) + { + processInstaller.Account = ServiceAccount.LocalSystem; + + installer.Context = new InstallContext( + "tgs-migrate-install.log", + new string[] + { + $"assemblypath={Path.Combine(tgsInstallPath, "Tgstation.Server.Host.Service.exe")}" + }); + installer.Description = "/tg/station 13 server running as a windows service"; + installer.DisplayName = "/tg/station server"; + installer.DelayedAutoStart = true; + installer.StartType = ServiceStartMode.Automatic; + installer.ServicesDependedOn = new string[] { "Tcpip", "Dhcp", "Dnscache" }; + installer.ServiceName = "tgstation-server"; + installer.Parent = processInstaller; + + var state = new ListDictionary(); + installer.Install(state); + } + + Console.WriteLine("Starting TGS5 service..."); + var allServices = ServiceController.GetServices(); + using (var tgs5Service = allServices.FirstOrDefault(service => service.ServiceName == NewServiceName)) + { + if (tgs5Service == null) + { + Console.WriteLine("Unable to locate newly installed TGS5 service!"); + ExitPause(11); + } + + foreach (var service in allServices) + { + if (service == tgs5Service) + continue; + + service.Dispose(); + } + + tgs5Service.Start(); + tgs5Service.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromMinutes(2)); + } + + // TGS5 CLIENT CONNECTION + const int MaxWaitMinutes = 5; + Console.WriteLine($"Connecting to TGS5 (Max {MaxWaitMinutes} minute wait)..."); + var giveUpAt = DateTimeOffset.UtcNow.AddMinutes(MaxWaitMinutes); + + var serverUrl = new Uri($"http://localhost:{configuredApiPort}"); + var clientFactory = new ServerClientFactory(productInfoHeaderValue.Product); + IServerClient tgs5Client; + for (var I = 1; ; ++I) + { + try + { + Console.WriteLine($"Attempt {I}..."); + tgs5Client = await clientFactory.CreateFromLogin( + serverUrl, + DefaultCredentials.AdminUserName, + DefaultCredentials.DefaultAdminUserPassword); + break; + } + catch (HttpRequestException) + { + //migrating, to be expected + if (DateTimeOffset.UtcNow > giveUpAt) + throw; + await Task.Delay(TimeSpan.FromSeconds(1)); + } + catch (ServiceUnavailableException) + { + // migrating, to be expected + if (DateTimeOffset.UtcNow > giveUpAt) + throw; + await Task.Delay(TimeSpan.FromSeconds(1)); + } + } + + Console.WriteLine("Successfully connected to TGS5!"); + + // COMMS MIGRATION + Console.WriteLine("Deferring to Comms binary to migrate instances..."); + + commsExitCode = RunComms($"--migrate {configuredApiPort}"); + if (commsExitCode != 0) + { + Console.WriteLine("Could not connect to TGS3 as administrator!"); + ExitPause(commsExitCode); + } + + // TGS3 SHUTDOWN + Console.WriteLine("Shutting down TGS3 service..."); + tgs3Service.Stop(); + tgs3Service.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromMinutes(2)); + tgs3Service.Dispose(); + + Console.WriteLine("Disabling TGS3 service..."); + using (var managementObject = new ManagementObject(string.Format("Win32_Service.Name=\"{0}\"", OldServiceName))) + { + managementObject.InvokeMethod("ChangeStartMode", new object[] { "Disabled" }); + } + + tgs3Service = GetTgs3Service(false); + if(tgs3Service.StartType != ServiceStartMode.Disabled) + Console.WriteLine("Failed to disable TGS3 service! This isn't critical, however."); + + Console.WriteLine("Migration complete! Please continue uninstall TGS3 using Add/Remove Programs."); + Console.WriteLine("Then configure TGS5 using an interactive client to build and start your server."); + ExitPause(0); +} +catch (Exception ex) +{ + Console.WriteLine("An error occurred in the migration!"); + Console.WriteLine(ex); + ExitPause(1); +} diff --git a/tools/Tgstation.Server.Migrator/Tgstation.Server.Migrator.csproj b/tools/Tgstation.Server.Migrator/Tgstation.Server.Migrator.csproj new file mode 100644 index 0000000000..a421faa696 --- /dev/null +++ b/tools/Tgstation.Server.Migrator/Tgstation.Server.Migrator.csproj @@ -0,0 +1,23 @@ + + + + + Exe + net6.0 + win-x86 + $(TgsMigratorVersion) + latest + enable + CA1416 + false + + + + + + + + + + +