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)
{