Merge pull request #1392 from tgstation/TGS3Migrator [NugetDeploy]

Migrator application for converting TGS3 instances to TGS5 instances while simultaneously installing TGS5 and disabling TGS3
This commit is contained in:
Jordan Dominion
2022-09-27 21:06:37 -04:00
committed by GitHub
26 changed files with 1056 additions and 70 deletions
+3 -2
View File
@@ -6,12 +6,13 @@
<TgsCoreVersion>5.0.4</TgsCoreVersion>
<TgsConfigVersion>4.2.0</TgsConfigVersion>
<TgsApiVersion>9.6.0</TgsApiVersion>
<TgsApiLibraryVersion>9.6.1</TgsApiLibraryVersion>
<TgsClientVersion>10.7.1</TgsClientVersion>
<TgsApiLibraryVersion>10.0.0</TgsApiLibraryVersion>
<TgsClientVersion>11.0.0</TgsClientVersion>
<TgsDmapiVersion>6.0.5</TgsDmapiVersion>
<TgsInteropVersion>5.3.0</TgsInteropVersion>
<TgsHostWatchdogVersion>1.2.0</TgsHostWatchdogVersion>
<TgsContainerScriptVersion>1.2.0</TgsContainerScriptVersion>
<TgsMigratorVersion>1.0.0</TgsMigratorVersion>
<TgsNetVersion>net6.0</TgsNetVersion>
</PropertyGroup>
</Project>
+8 -3
View File
@@ -50,6 +50,11 @@ namespace Tgstation.Server.Api
/// </summary>
public const string OAuthAuthenticationScheme = "OAuth";
/// <summary>
/// Added to <see cref="MediaTypeNames.Application"/> in netstandard2.1. Can't use because of Tgstation.Server.Migrator.
/// </summary>
public const string ApplicationJsonMime = "application/json";
/// <summary>
/// Get the version of the <see cref="Api"/> the caller is using.
/// </summary>
@@ -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)
@@ -2,7 +2,7 @@
<Import Project="../../build/Version.props" />
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework>
<DebugType>Full</DebugType>
<Version>$(TgsApiLibraryVersion)</Version>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
@@ -16,7 +16,7 @@
<RepositoryUrl>https://github.com/tgstation/tgstation-server</RepositoryUrl>
<Copyright>2018-2022</Copyright>
<PackageTags>json web api tgstation-server tgstation ss13 byond</PackageTags>
<PackageReleaseNotes>See https://github.com/tgstation/tgstation-server/releases/tag/api-v$(TgsApiVersion)</PackageReleaseNotes>
<PackageReleaseNotes>Retargeted to netstandard2.0 to support migrator.</PackageReleaseNotes>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<CodeAnalysisRuleSet>../../build/analyzers.ruleset</CodeAnalysisRuleSet>
+10 -4
View File
@@ -26,6 +26,12 @@ namespace Tgstation.Server.Client
/// <inheritdoc />
sealed class ApiClient : IApiClient
{
/// <summary>
/// PATCH <see cref="HttpMethod"/>.
/// </summary>
/// <remarks>HOW IS THIS NOT INCLUDED IN THE FRAMEWORK??!?!?</remarks>
static readonly HttpMethod HttpPatch = new ("PATCH");
/// <inheritdoc />
public Uri Url { get; }
@@ -77,7 +83,7 @@ namespace Tgstation.Server.Client
/// Get the <see cref="JsonSerializerSettings"/> to use.
/// </summary>
/// <returns>A new <see cref="JsonSerializerSettings"/> instance.</returns>
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<TResult> Update<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) where TBody : class => RunRequest<TBody, TResult>(route, body, HttpMethod.Post, null, false, cancellationToken);
/// <inheritdoc />
public Task Patch(string route, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Patch, null, false, cancellationToken);
public Task Patch(string route, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpPatch, null, false, cancellationToken);
/// <inheritdoc />
public Task Update<TBody>(string route, TBody body, CancellationToken cancellationToken) where TBody : class => RunRequest<TBody, object>(route, body, HttpMethod.Post, null, false, cancellationToken);
@@ -204,7 +210,7 @@ namespace Tgstation.Server.Client
public Task<TResult> Create<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<object, TResult>(route, new object(), HttpMethod.Put, instanceId, false, cancellationToken);
/// <inheritdoc />
public Task<TResult> Patch<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<object, TResult>(route, new object(), HttpMethod.Patch, instanceId, false, cancellationToken);
public Task<TResult> Patch<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<object, TResult>(route, new object(), HttpPatch, instanceId, false, cancellationToken);
/// <inheritdoc />
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<TResult>(
route,
@@ -30,14 +30,6 @@ namespace Tgstation.Server.Client
return new CachedResponseStream(response, stream);
}
/// <inheritdoc />
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync().ConfigureAwait(false);
await responseStream.DisposeAsync().ConfigureAwait(false);
response.Dispose();
}
/// <inheritdoc />
public override bool CanRead => responseStream.CanRead;
@@ -38,7 +38,7 @@ namespace Tgstation.Server.Client.Components
=> ReadPaged<ByondResponse>(paginationSettings, Routes.ListRoute(Routes.Byond), instance.Id, cancellationToken);
/// <inheritdoc />
public async Task<ByondInstallResponse> SetActiveVersion(ByondVersionRequest installRequest, Stream zipFileStream, CancellationToken cancellationToken)
public async Task<ByondInstallResponse> SetActiveVersion(ByondVersionRequest installRequest, Stream? zipFileStream, CancellationToken cancellationToken)
{
if (installRequest == null)
throw new ArgumentNullException(nameof(installRequest));
@@ -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);
@@ -35,6 +35,6 @@ namespace Tgstation.Server.Client.Components
/// <param name="zipFileStream">The <see cref="Stream"/> for the .zip file if <see cref="ByondVersionRequest.UploadCustomZip"/> is <see langword="true"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the updated <see cref="ByondInstallResponse"/> information.</returns>
Task<ByondInstallResponse> SetActiveVersion(ByondVersionRequest installRequest, Stream zipFileStream, CancellationToken cancellationToken);
Task<ByondInstallResponse> SetActiveVersion(ByondVersionRequest installRequest, Stream? zipFileStream, CancellationToken cancellationToken);
}
}
@@ -2,7 +2,7 @@
<Import Project="../../build/Version.props" />
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework>
<DebugType>Full</DebugType>
<Version>$(TgsClientVersion)</Version>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
@@ -16,7 +16,7 @@
<RepositoryUrl>https://github.com/tgstation/tgstation-server</RepositoryUrl>
<Copyright>2018-2022</Copyright>
<PackageTags>json web api tgstation-server tgstation ss13 byond client</PackageTags>
<PackageReleaseNotes>Added OAuth login methods to IServerClientFactory.</PackageReleaseNotes>
<PackageReleaseNotes>Retargeted to netstandard2.0 to support migrator. Fixed nullablity of IByondClient.SetActiveVersion's Stream parameter.</PackageReleaseNotes>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<CodeAnalysisRuleSet>../../build/analyzers.ruleset</CodeAnalysisRuleSet>
@@ -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:
@@ -139,11 +139,6 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="InstanceFactory"/>.
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// The <see cref="SessionConfiguration"/> for the <see cref="InstanceFactory"/>.
/// </summary>
@@ -175,7 +170,6 @@ namespace Tgstation.Server.Host.Components
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param>
/// <param name="gitRemoteFeaturesFactory">The value of <see cref="gitRemoteFeaturesFactory"/>.</param>
/// <param name="remoteDeploymentManagerFactory">The value of <see cref="remoteDeploymentManagerFactory"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
/// <param name="sessionConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="sessionConfiguration"/>.</param>
public InstanceFactory(
IIOManager ioManager,
@@ -200,7 +194,6 @@ namespace Tgstation.Server.Host.Components
IFileTransferTicketProvider fileTransferService,
IGitRemoteFeaturesFactory gitRemoteFeaturesFactory,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IOptions<SessionConfiguration> 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
@@ -3,7 +3,7 @@
/// <summary>
/// Configuration options pertaining to elasticsearch log storage.
/// </summary>
sealed class ElasticsearchConfiguration
public sealed class ElasticsearchConfiguration
{
/// <summary>
/// The key for the <see cref="Microsoft.Extensions.Configuration.IConfigurationSection"/> the <see cref="ElasticsearchConfiguration"/> resides in.
@@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Configuration
/// <summary>
/// OAuth configuration options.
/// </summary>
sealed class OAuthConfiguration : OAuthConfigurationBase
public sealed class OAuthConfiguration : OAuthConfigurationBase
{
/// <summary>
/// The client redirect URL. Not used by all providers.
@@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Configuration
/// <summary>
/// Base OAuth options.
/// </summary>
abstract class OAuthConfigurationBase
public abstract class OAuthConfigurationBase
{
/// <summary>
/// The client ID.
@@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Configuration
/// <summary>
/// Configuration options pertaining to user security.
/// </summary>
sealed class SecurityConfiguration
public sealed class SecurityConfiguration
{
/// <summary>
/// The key for the <see cref="Microsoft.Extensions.Configuration.IConfigurationSection"/> the <see cref="SecurityConfiguration"/> resides in.
@@ -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);
}
/// <summary>
@@ -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
/// <returns><paramref name="permissionSetToModify"/> or a new <see cref="InstancePermissionSet"/> with full rights.</returns>
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<ByondRights>();
permissionSetToModify.ChatBotRights = RightsHelper.AllRights<ChatBotRights>();
permissionSetToModify.ConfigurationRights = RightsHelper.AllRights<ConfigurationRights>();
@@ -54,7 +54,7 @@ namespace Tgstation.Server.Host.Core
/// Sets up dependency injection.
/// </summary>
#pragma warning disable CA1506
sealed class Application : SetupApplication
public sealed class Application : SetupApplication
{
/// <summary>
/// The <see cref="IWebHostEnvironment"/> for the <see cref="Application"/>.
@@ -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));
+1 -1
View File
@@ -8,7 +8,7 @@ namespace Tgstation.Server.Host
/// <summary>
/// For creating <see cref="IServer"/>s.
/// </summary>
interface IServerFactory
public interface IServerFactory
{
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="IServerFactory"/>.
@@ -18,10 +18,10 @@ namespace Tgstation.Server.Host.Security.OAuth
public override OAuthProvider Provider => OAuthProvider.TGForums;
/// <inheritdoc />
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");
/// <inheritdoc />
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");
/// <summary>
/// Initializes a new instance of the <see cref="TGForumsOAuthValidator"/> class.
@@ -50,6 +50,6 @@ namespace Tgstation.Server.Host.Security.OAuth
protected override string DecodeUserInformationPayload(dynamic responseJson) => responseJson.phpbb_username;
/// <inheritdoc />
protected override OAuthTokenRequest CreateTokenRequest(string code) => new OAuthTokenRequest(OAuthConfiguration, code, "user");
protected override OAuthTokenRequest CreateTokenRequest(string code) => new (OAuthConfiguration, code, "user");
}
}
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Host.Setup
/// <summary>
/// Set of objects needed to configure an <see cref="Core.Application"/>.
/// </summary>
interface IPostSetupServices
public interface IPostSetupServices
{
/// <summary>
/// The <see cref="Configuration.GeneralConfiguration"/>.
@@ -17,17 +17,17 @@ namespace Tgstation.Server.Host.Setup
/// <summary>
/// DI root for configuring a <see cref="SetupWizard"/>.
/// </summary>
class SetupApplication
public class SetupApplication
{
/// <summary>
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="SetupApplication"/>.
/// </summary>
protected static readonly IAssemblyInformationProvider AssemblyInformationProvider = new AssemblyInformationProvider();
public static readonly IAssemblyInformationProvider AssemblyInformationProvider = new AssemblyInformationProvider();
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="SetupApplication"/>.
/// </summary>
protected static readonly IIOManager IOManager = new DefaultIOManager(AssemblyInformationProvider);
public static readonly IIOManager IOManager = new DefaultIOManager(AssemblyInformationProvider);
/// <summary>
/// The <see cref="IConfiguration"/> for the <see cref="SetupApplication"/>.
+29 -4
View File
@@ -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}
@@ -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<int> 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<int> 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<ChatBotCreateRequest>();
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<ChatChannel>();
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<Task>();
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);
}
}
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net472</TargetFramework>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<Version>$(TgsMigratorVersion)</Version>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TGServiceInterface" Version="3.2.6" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Tgstation.Server.Client\Tgstation.Server.Client.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Net.Http" />
</ItemGroup>
</Project>
+524
View File
@@ -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);
}
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../build/Version.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<Version>$(TgsMigratorVersion)</Version>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<NoWarn>CA1416</NoWarn>
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Core.System.ServiceProcess" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Tgstation.Server.Client\Tgstation.Server.Client.csproj" />
<ProjectReference Include="..\..\src\Tgstation.Server.Host\Tgstation.Server.Host.csproj" />
</ItemGroup>
</Project>