diff --git a/build/Version.props b/build/Version.props index 21e5da987b..02bbc0e30a 100644 --- a/build/Version.props +++ b/build/Version.props @@ -12,7 +12,7 @@ 19.3.0 7.3.0 5.10.0 - 1.5.0 + 1.6.0 8.0.0 1.2.1 2.0.0 diff --git a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj index ff23533a16..9804e251e3 100644 --- a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj +++ b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj @@ -4,6 +4,7 @@ Exe $(TgsFrameworkVersion) + $(TgsCoreVersion) enable false diff --git a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj index 417e03092c..e1f04e898b 100644 --- a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj +++ b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj @@ -7,6 +7,7 @@ enable $(TgsFrameworkVersion) + $(TgsCoreVersion) false diff --git a/src/Tgstation.Server.Host.Watchdog/BootstrapSettings.cs b/src/Tgstation.Server.Host.Watchdog/BootstrapSettings.cs new file mode 100644 index 0000000000..10c9ab11b4 --- /dev/null +++ b/src/Tgstation.Server.Host.Watchdog/BootstrapSettings.cs @@ -0,0 +1,38 @@ +using System; +using System.Reflection; + +using Tgstation.Server.Common.Extensions; + +namespace Tgstation.Server.Host.Watchdog +{ + /// + /// Settings for the bootstrapper feature. + /// + sealed class BootstrapSettings + { + /// + /// The current supported major version of . + /// + public const int FileMajorVersion = 1; + + /// + /// The token used to substitute . + /// + public const string VersionSubstitutionToken = "${version}"; + + /// + /// The version of the boostrapper file. + /// + public Version FileVersion { get; set; } = new Version(FileMajorVersion, 0, 0); + + /// + /// The of TGS last launched in the lib/Default directory. + /// + public Version TgsVersion { get; set; } = Assembly.GetEntryAssembly()!.GetName().Version!.Semver(); + + /// + /// The URL to format with to get the download URL. + /// + public string ServerUpdatePackageUrlFormatter { get; set; } = $"https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v{VersionSubstitutionToken}/ServerUpdatePackage.zip"; + } +} diff --git a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs index 4813383440..c139b9ac36 100644 --- a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs @@ -3,9 +3,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; +using System.IO.Compression; using System.Linq; +using System.Net.Http; using System.Reflection; using System.Runtime.InteropServices; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -119,6 +122,49 @@ namespace Tgstation.Server.Host.Watchdog else Directory.CreateDirectory(assemblyStoragePath); + var bootstrappable = args.Contains("--bootstrap", StringComparer.OrdinalIgnoreCase); + var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var bootstrapperSettingsFile = Path.Combine(homeDirectory, ".tgstation-server-bootstrap.json"); + BootstrapSettings? bootstrapSettings = null; + if (!Directory.Exists(defaultAssemblyPath)) + { + if (!bootstrappable) + { + logger.LogCritical("Unable to locate host assembly directory!"); + return false; + } + + if (File.Exists(bootstrapperSettingsFile)) + { + logger.LogInformation("Loading bootstrap settings..."); + var bootstrapperSettingsJson = await File.ReadAllTextAsync(bootstrapperSettingsFile, cancellationToken); + bootstrapSettings = JsonSerializer.Deserialize(bootstrapperSettingsJson); + if (bootstrapSettings == null) + { + logger.LogCritical("Failed to deserialize {settingsFile}!", bootstrapperSettingsFile); + return false; + } + } + else + { + logger.LogInformation("Using default bootstrap settings..."); + bootstrapSettings = new BootstrapSettings(); // defaults + } + + if (bootstrapSettings.FileVersion.Major != BootstrapSettings.FileMajorVersion) + { + logger.LogCritical("Unable to parse bootstrapper file! Expected version: {expected}.X.X", BootstrapSettings.FileMajorVersion); + return false; + } + + string downloadUrl = bootstrapSettings.ServerUpdatePackageUrlFormatter.Replace(BootstrapSettings.VersionSubstitutionToken, bootstrapSettings.TgsVersion.ToString(), StringComparison.Ordinal); + logger.LogInformation("Bootstrapping from: {url}", downloadUrl); + using var httpClient = new HttpClient(); + await using var zipData = await httpClient.GetStreamAsync(new Uri(downloadUrl), cancellationToken); + using var archive = new ZipArchive(zipData, ZipArchiveMode.Read, true); + archive.ExtractToDirectory(defaultAssemblyPath); + } + var assemblyName = String.Join(".", nameof(Tgstation), nameof(Server), nameof(Host), "dll"); var assemblyPath = Path.Combine(defaultAssemblyPath, assemblyName); @@ -128,28 +174,59 @@ namespace Tgstation.Server.Host.Watchdog return false; } - if (!File.Exists(assemblyPath)) - { - logger.LogCritical("Unable to locate host assembly!"); - return false; - } - - var fileVersion = FileVersionInfo.GetVersionInfo(assemblyPath).FileVersion; - if (fileVersion == null) - { - logger.LogCritical("Failed to parse version info from {assemblyPath}!", assemblyPath); - return false; - } - - initialHostVersionTcs.SetResult( - Version.Parse( - fileVersion)); - var watchdogVersion = executingAssembly.GetName().Version?.Semver().ToString(); while (!cancellationToken.IsCancellationRequested) + { + if (!File.Exists(assemblyPath)) + { + logger.LogCritical("Unable to locate host assembly!"); + return false; + } + + var fileVersion = FileVersionInfo.GetVersionInfo(assemblyPath).FileVersion; + if (fileVersion == null) + { + logger.LogCritical("Failed to parse version info from {assemblyPath}!", assemblyPath); + return false; + } + + if (bootstrappable) + { + if (!Version.TryParse(fileVersion, out var bootstrappedVersion)) + { + logger.LogCritical("Failed to parse bootstrapped version prior to launch: {fileVersion}", fileVersion); + } + else + { + // save bootstrapper settings + var oldUrl = bootstrapSettings?.ServerUpdatePackageUrlFormatter; + bootstrapSettings = new BootstrapSettings + { + TgsVersion = bootstrappedVersion.Semver(), + }; + + bootstrapSettings.ServerUpdatePackageUrlFormatter = oldUrl ?? bootstrapSettings.ServerUpdatePackageUrlFormatter; + + Directory.CreateDirectory(homeDirectory); + await File.WriteAllTextAsync( + bootstrapperSettingsFile, + JsonSerializer.Serialize( + bootstrapSettings, + new JsonSerializerOptions + { + WriteIndented = true, + }), + cancellationToken); + } + } + using (logger.BeginScope("Host invocation")) { + initialHostVersionTcs.SetResult( + Version.Parse( + fileVersion)); + updateDirectory = Path.GetFullPath(Path.Combine(assemblyStoragePath, Guid.NewGuid().ToString())); logger.LogInformation("Update path set to {updateDirectory}", updateDirectory); using (var process = new Process()) @@ -358,6 +435,7 @@ namespace Tgstation.Server.Host.Watchdog } } } + } } catch (OperationCanceledException ex) {