Merge pull request #2000 from tgstation/nixexperiment

Nix Support
This commit is contained in:
Jordan Dominion
2024-11-09 17:38:38 -05:00
committed by GitHub
20 changed files with 747 additions and 75 deletions
+149 -2
View File
@@ -47,8 +47,17 @@ concurrency:
cancel-in-progress: true
jobs:
start-gate:
name: CI Start Gate
runs-on: ubuntu-latest
if: (!contains(github.event.head_commit.message, '[TGSRelease]'))
steps:
- name: GitHub Requires at Least One Step for a Job
run: exit 0
build-releasenotes:
name: Build ReleaseNotes for Other Jobs
needs: start-gate
runs-on: ubuntu-latest
steps:
- name: Install Native Dependencies
@@ -83,6 +92,7 @@ jobs:
code-scanning:
name: Run CodeQL
needs: start-gate
runs-on: ubuntu-latest
permissions:
security-events: write
@@ -134,6 +144,7 @@ jobs:
dmapi-build:
name: Build DMAPI
needs: start-gate
strategy:
fail-fast: false
matrix:
@@ -222,6 +233,7 @@ jobs:
opendream-build:
name: Build DMAPI (OpenDream)
needs: start-gate
strategy:
fail-fast: false
matrix:
@@ -278,6 +290,7 @@ jobs:
efcore-version-match:
name: Check Nuget Versions Match Tools
runs-on: ubuntu-latest
needs: start-gate
steps:
- name: Checkout (Branch)
uses: actions/checkout@v4
@@ -412,6 +425,7 @@ jobs:
docker-build:
name: Build Docker Image
runs-on: ubuntu-latest
needs: start-gate
env:
TGS_TELEMETRY_KEY_FILE: tgs_telemetry_key.txt
steps:
@@ -438,6 +452,7 @@ jobs:
linux-unit-tests:
name: Linux Tests
needs: start-gate
strategy:
fail-fast: false
matrix:
@@ -510,6 +525,7 @@ jobs:
windows-unit-tests:
name: Windows Tests
needs: start-gate
strategy:
fail-fast: false
matrix:
@@ -1428,6 +1444,7 @@ jobs:
build-msi:
name: Build Windows Installer .exe
runs-on: windows-latest
needs: start-gate
env:
TGS_TELEMETRY_KEY_FILE: C:/tgs_telemetry_key.txt
steps:
@@ -1715,6 +1732,12 @@ jobs:
body_path: release_notes.md
commitish: ${{ github.event.head_commit.id }}
- name: Generate Artifact Attestation
uses: actions/attest-build-provenance@v1
with:
subject-path: ./swagger/tgs_api.json
github-token: ${{ steps.app-token-generation.outputs.token }}
- name: Upload OpenApi Spec
uses: actions/upload-release-asset@v1
env:
@@ -1795,6 +1818,12 @@ jobs:
commitish: ${{ github.event.head_commit.id }}
prerelease: ${{ env.TGS_GRAPHQL_PRERELEASE }}
- name: Generate Artifact Attestation
uses: actions/attest-build-provenance@v1
with:
subject-path: ./schema/tgs_api.graphql
github-token: ${{ steps.app-token-generation.outputs.token }}
- name: Upload GraphQL Schema
uses: actions/upload-release-asset@v1
env:
@@ -1867,6 +1896,12 @@ jobs:
body_path: release_notes.md
commitish: ${{ github.event.head_commit.id }}
- name: Generate Artifact Attestation
uses: actions/attest-build-provenance@v1
with:
subject-path: ./DMAPI.zip
github-token: ${{ steps.app-token-generation.outputs.token }}
- name: Upload DMAPI Artifact
uses: actions/upload-release-asset@v1
env:
@@ -2132,7 +2167,19 @@ jobs:
body_path: release_notes.md
commitish: ${{ github.event.head_commit.id }}
- name: Upload Server Console Artifact
- name: Generate Server Console Artifact Attestation
uses: actions/attest-build-provenance@v1
with:
subject-path: ./ServerConsole.zip
github-token: ${{ steps.app-token-generation.outputs.token }}
- name: Upload Server Console Zip Artifact to Action
uses: actions/upload-artifact@v4
with:
name: server-console-release
path: ./ServerConsole.zip
- name: Upload Server Console Artifact to Release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }}
@@ -2142,6 +2189,12 @@ jobs:
asset_name: ServerConsole.zip
asset_content_type: application/zip
- name: Generate Server Service Artifact Attestation
uses: actions/attest-build-provenance@v1
with:
subject-path: ./ServerService.zip
github-token: ${{ steps.app-token-generation.outputs.token }}
- name: Upload Server Service Artifact
uses: actions/upload-release-asset@v1
env:
@@ -2152,6 +2205,12 @@ jobs:
asset_name: ServerService.zip
asset_content_type: application/zip
- name: Generate DMAPI Artifact Attestation
uses: actions/attest-build-provenance@v1
with:
subject-path: ./DMAPI.zip
github-token: ${{ steps.app-token-generation.outputs.token }}
- name: Upload DMAPI Artifact
uses: actions/upload-release-asset@v1
env:
@@ -2162,6 +2221,12 @@ jobs:
asset_name: DMAPI.zip
asset_content_type: application/zip
- name: Generate REST API Artifact Attestation
uses: actions/attest-build-provenance@v1
with:
subject-path: ./swagger/tgs_api.json
github-token: ${{ steps.app-token-generation.outputs.token }}
- name: Upload REST API Artifact
uses: actions/upload-release-asset@v1
env:
@@ -2172,6 +2237,12 @@ jobs:
asset_name: swagger.json
asset_content_type: application/json
- name: Generate GraphQL API Artifact Attestation
uses: actions/attest-build-provenance@v1
with:
subject-path: ./schema/tgs-api.graphql
github-token: ${{ steps.app-token-generation.outputs.token }}
- name: Upload GraphQL API Artifact
uses: actions/upload-release-asset@v1
env:
@@ -2182,6 +2253,12 @@ jobs:
asset_name: tgs-api.graphql
asset_content_type: text/plain
- name: Generate Server Update Package Artifact Attestation
uses: actions/attest-build-provenance@v1
with:
subject-path: ./ServerUpdatePackage.zip
github-token: ${{ steps.app-token-generation.outputs.token }}
- name: Upload Server Update Package Artifact
uses: actions/upload-release-asset@v1
env:
@@ -2192,7 +2269,13 @@ jobs:
asset_name: ServerUpdatePackage.zip
asset_content_type: application/zip
- name: Upload Debian Pacakaging Artifact
- name: Generate Debian Packaging Artifact Attestation
uses: actions/attest-build-provenance@v1
with:
subject-path: ./packaging-debian/tgstation-server-v${{ env.TGS_VERSION }}.debian.packaging.tar.xz
github-token: ${{ steps.app-token-generation.outputs.token }}
- name: Upload Debian Packaging Artifact
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ steps.app-token-generation.outputs.token }}
@@ -2202,6 +2285,12 @@ jobs:
asset_name: tgstation-server-v${{ env.TGS_VERSION }}.debian.packaging.tar.xz
asset_content_type: application/x-tar
- name: Generate MariaDB .msi Attestation
uses: actions/attest-build-provenance@v1
with:
subject-path: ./build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/bin/Release/mariadb.msi
github-token: ${{ steps.app-token-generation.outputs.token }}
- name: Upload MariaDB .msi
uses: actions/upload-release-asset@v1
env:
@@ -2212,6 +2301,12 @@ jobs:
asset_name: mariadb-${{ env.MARIADB_VERSION }}-winx64.msi
asset_content_type: application/octet-stream
- name: Generate Installer .exe Attestation
uses: actions/attest-build-provenance@v1
with:
subject-path: ./build/package/winget/tgstation-server-installer.exe
github-token: ${{ steps.app-token-generation.outputs.token }}
- name: Upload Installer .exe
uses: actions/upload-release-asset@v1
env:
@@ -2222,6 +2317,58 @@ jobs:
asset_name: tgstation-server-installer.exe
asset_content_type: application/octet-stream
update-nix:
name: Update Nix SHA
needs: deploy-tgs
runs-on: ubuntu-latest
if: (!(cancelled() || failure())) && needs.deploy-tgs.result == 'success'
steps:
- name: Install Native Packages # Name checked in rerunFlakyTests.js
run: |
sudo apt-get update
sudo apt-get install -y xmlstarlet
- name: Setup Nix
uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Checkout
uses: actions/checkout@v4
- name: Parse TGS version
run: echo "TGS_VERSION=$(xmlstarlet sel -N X="http://schemas.microsoft.com/developer/msbuild/2003" --template --value-of /X:Project/X:PropertyGroup/X:TgsCoreVersion build/Version.props)" >> $GITHUB_ENV
- name: Retrieve ServerConsole.zip Artifact
uses: actions/download-artifact@v4
with:
name: server-console-release
path: server-console-release
- name: Regenerate Nix Hash
run: |
nix hash path ./server-console-release > build/package/nix/ServerConsole.sha256
cat build/package/nix/ServerConsole.sha256
- name: Commit
run: |
git config --global push.default simple
git config user.name "tgstation-server-ci[bot]"
git config user.email "161980869+tgstation-server-ci[bot]@users.noreply.github.com"
git add build/package/nix/ServerConsole.sha256
git commit -m "Update nix SHA256 for [TGSRelease] v${{ env.TGS_VERSION }}"
- name: Re-tag
run: |
git tag -d tgstation-server-v${{ env.TGS_VERSION }}
git tag tgstation-server-v${{ env.TGS_VERSION }}
- name: Push Commit
run: git push
- name: Force Push Tags
run: git push -f --tags
changelog-regen:
name: Regenerate Changelog
runs-on: ubuntu-latest
+27
View File
@@ -137,6 +137,31 @@ sudo dpkg --add-architecture i386 \
The service will execute as the newly created user: `tgstation-server`. You should, ideally, store your instances somewhere under `/home/tgstation-server`.
##### Nix Flake
TGS supports being setup on Nix starting with version 6.12.0. Add the [flake](./build/package/nix/flake.nix) to your own system by adding the following code to your flake inputs.
```nix
tgstation-server = {
url = "github:tgstation/tgstation-server/tgstation-server-v${version}?dir=build/package/nix";
};
```
Where `version` is the latest major TGS version you wish to use.
Note that changing this version does not change the core version of TGS used after the first launch. Instead, have TGS self-update via its API.
For maximum game server uptime, do NOT modify this version unless you are doing a major TGS version update in which case it is a requirement.
Configure TGS by setting up its service definition:
```nix
services.tgstation-server = {
enable = true;
production-appsettings = (builtins.readFile ./path/to/your/appsettings.Production.yml);
};
```
Refer to [tgstation-server.nix](./build/package/nix/tgstation-server.nix) for the full list of available configuration options.
##### Manual Setup
The following dependencies are required.
@@ -243,6 +268,8 @@ Create an `appsettings.Production.yml` file next to `appsettings.yml`. This will
- `General:SkipAddingByondFirewallException`: Set to `true` if you have Windows firewall disabled
- `General:AdditionalEventScriptsDirectories`: An array of directories that are considered to contain EventScripts alongside instance directories. Working directory for executed scripts will remain the instance EventScripts directory.
- `Session:HighPriorityLiveDreamDaemon`: Boolean controlling if live DreamDaemon instances get set to above normal priority processes.
- `Session:LowPriorityDeploymentProcesses `: Boolean controlling if DreamMaker and API validation DreamDaemon instances get set to below normal priority processes.
+1 -1
View File
@@ -12,7 +12,7 @@
<TgsClientVersion>19.3.0</TgsClientVersion>
<TgsDmapiVersion>7.3.0</TgsDmapiVersion>
<TgsInteropVersion>5.10.0</TgsInteropVersion>
<TgsHostWatchdogVersion>1.5.0</TgsHostWatchdogVersion>
<TgsHostWatchdogVersion>1.6.0</TgsHostWatchdogVersion>
<TgsSwarmProtocolVersion>8.0.0</TgsSwarmProtocolVersion>
<TgsContainerScriptVersion>1.2.1</TgsContainerScriptVersion>
<TgsMigratorVersion>2.0.0</TgsMigratorVersion>
+1
View File
@@ -0,0 +1 @@
sha256-mHlRHPSeZxyJPqN3KUmc0ftYNZgh81LauIu+fCSKPUI=
+13
View File
@@ -0,0 +1,13 @@
{
description = "tgstation-server";
inputs = {};
outputs = { ... }: {
nixosModules = {
default = { ... }: {
imports = [ ./tgstation-server.nix ];
};
};
};
}
+119
View File
@@ -0,0 +1,119 @@
{
pkgs,
...
}:
let
inherit (pkgs) stdenv lib;
versionParse = stdenv.mkDerivation {
pname = "tgstation-server-version-parse";
version = "1.0.0";
meta = with pkgs.lib; {
description = "Version parser for tgstation-server";
homepage = "https://github.com/tgstation/tgstation-server";
changelog = "https://github.com/tgstation/tgstation-server/blob/gh-pages/changelog.yml";
license = licenses.agpl3Plus;
platforms = platforms.x86_64;
};
nativeBuildInputs = with pkgs; [
xmlstarlet
];
src = ./../..;
installPhase = ''
mkdir -p $out
xmlstarlet sel -N X="http://schemas.microsoft.com/developer/msbuild/2003" --template --value-of /X:Project/X:PropertyGroup/X:TgsCoreVersion ./Version.props > $out/tgs_version.txt
'';
};
fixedOutput = stdenv.mkDerivation {
pname = "tgstation-server-release-server-console-zip";
version = (builtins.readFile "${versionParse}/tgs_version.txt");
meta = with pkgs.lib; {
description = "Host watchdog binaries for tgstation-server";
homepage = "https://github.com/tgstation/tgstation-server";
changelog = "https://github.com/tgstation/tgstation-server/releases/tag/tgstation-server-v${version}";
license = licenses.agpl3Plus;
platforms = platforms.x86_64;
};
nativeBuildInputs = with pkgs; [
curl
cacert
versionParse
];
src = ./.;
buildPhase = ''
curl -L https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v${version}/ServerConsole.zip -o ServerConsole.zip
'';
installPhase = ''
mkdir -p $out
mv ServerConsole.zip $out/ServerConsole.zip
'';
outputHashAlgo = "sha256";
outputHashMode = "recursive";
outputHash = (builtins.readFile ./ServerConsole.sha256);
};
rpath = lib.makeLibraryPath [ pkgs.stdenv_32bit.cc.cc.lib ];
in
stdenv.mkDerivation {
pname = "tgstation-server";
version = (builtins.readFile "${versionParse}/tgs_version.txt");
meta = with pkgs.lib; {
description = "A production scale tool for DreamMaker server management";
homepage = "https://github.com/tgstation/tgstation-server";
changelog = "https://github.com/tgstation/tgstation-server/releases/tag/tgstation-server-v${version}";
license = licenses.agpl3Plus;
platforms = platforms.x86_64;
};
buildInputs = with pkgs; [
dotnetCorePackages.aspnetcore_8_0
gdb
systemd
zlib
gcc_multi
glibc
];
nativeBuildInputs = with pkgs; [
makeWrapper
unzip
fixedOutput
versionParse
];
src = ./.;
installPhase = ''
mkdir -p $out/bin
unzip "${fixedOutput}/ServerConsole.zip" -d $out/bin
rm -rf $out/bin/lib
makeWrapper ${pkgs.dotnetCorePackages.aspnetcore_8_0}/dotnet $out/bin/tgstation-server --suffix PATH : ${
lib.makeBinPath (
with pkgs;
[
dotnetCorePackages.aspnetcore_8_0
gdb
]
)
} --suffix LD_LIBRARY_PATH : ${
lib.makeLibraryPath (
with pkgs;
[
systemd
zlib
]
)
} --add-flags "$out/bin/Tgstation.Server.Host.Console.dll --bootstrap"
'';
}
+133
View File
@@ -0,0 +1,133 @@
inputs@{
config,
lib,
nixpkgs,
pkgs,
writeShellScriptBin,
...
}:
let
pkgs-i686 = nixpkgs.legacyPackages.i686-linux;
cfg = config.services.tgstation-server;
package = import ./package.nix inputs;
stdenv = pkgs-i686.stdenv_32bit;
rpath = pkgs-i686.lib.makeLibraryPath [
stdenv.cc.cc.lib
];
byond-patcher = pkgs-i686.writeShellScriptBin "EngineInstallComplete-050-TgsPatchELFByond.sh" ''
BYOND_PATH=$(realpath ../../Byond/$1/byond/bin/)
${pkgs.patchelf}/bin/patchelf --set-interpreter "$(cat ${stdenv.cc}/nix-support/dynamic-linker)" \
--set-rpath "$BYOND_PATH:${rpath}" \
$BYOND_PATH/{DreamDaemon,DreamDownload,DreamMaker}
'';
tgs-wrapper = pkgs.writeShellScriptBin "tgs-path-wrapper" ''
export PATH=$PATH:${cfg.extra-path}
exec ${package}/bin/tgstation-server --appsettings-base-path=/etc/tgstation-server.d --General:SetupWizardMode=Never --General:AdditionalEventScriptsDirectories:0=/etc/tgstation-server.d/EventScripts --General:AdditionalEventScriptsDirectories:1=${byond-patcher}/bin
'';
in
{
##### interface. here we define the options that users of our service can specify
options = {
# the options for our service will be located under services.foo
services.tgstation-server = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable tgstation-server.
'';
};
username = lib.mkOption {
type = lib.types.str;
default = "tgstation-server";
description = ''
The name of the user used to execute tgstation-server.
'';
};
groupname = lib.mkOption {
type = lib.types.str;
default = "tgstation-server";
description = ''
The name of group the user used to execute tgstation-server will belong to.
'';
};
home-directory = lib.mkOption {
type = lib.types.str;
default = "/home/tgstation-server";
description = ''
The home directory of TGS. Should be persistent.
'';
};
production-appsettings = lib.mkOption {
type = lib.types.lines;
default = '''';
description = ''
The contents of appsettings.Production.yml in the /etc/tgstation-server.d directory.
'';
};
extra-path = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Extra PATH entries to add to the TGS process
'';
};
};
};
config = lib.mkIf cfg.enable {
users.groups."${cfg.groupname}" = { };
users.users."${cfg.username}" = {
isSystemUser = true;
createHome = true;
group = cfg.groupname;
home = cfg.home-directory;
};
environment.etc = {
"tgstation-server.d/appsettings.yml" = {
text = (builtins.readFile "${package}/bin/appsettings.yml");
group = cfg.groupname;
mode = "0644";
};
"tgstation-server.d/appsettings.Production.yml" = {
text = cfg.production-appsettings;
group = cfg.groupname;
mode = "0640";
};
};
systemd.services.tgstation-server = {
description = "tgstation-server";
serviceConfig = {
User = cfg.username;
Type = "notify-reload";
NotifyAccess = "all";
WorkingDirectory = "${package}/bin";
ExecStart = "${tgs-wrapper}/bin/tgs-path-wrapper";
Restart = "always";
KillMode = "process";
ReloadSignal = "SIGUSR2";
AmbientCapabilities = "CAP_SYS_NICE CAP_SYS_PTRACE";
WatchdogSec = "60";
WatchdogSignal = "SIGTERM";
LogsDirectory = "tgstation-server";
};
wantedBy = [ "multi-user.target" ];
};
};
}
@@ -14,10 +14,10 @@ namespace Tgstation.Server.Host.Common
public static class DotnetHelper
{
/// <summary>
/// Gets the path to the dotnet executable.
/// Gets potential paths to the dotnet executable.
/// </summary>
/// <param name="isWindows">If the current system is a Windows OS.</param>
/// <returns>The path to the dotnet executable.</returns>
/// <returns>An <see cref="IEnumerable{T}"/> of potential paths to the dotnet executable.</returns>
public static IEnumerable<string> GetPotentialDotnetPaths(bool isWindows)
{
var enviromentPath = Environment.GetEnvironmentVariable("PATH");
@@ -4,6 +4,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(TgsFrameworkVersion)</TargetFramework>
<!-- Important that this is core and not host watchdog. Bootstrapping depends on it -->
<Version>$(TgsCoreVersion)</Version>
<Nullable>enable</Nullable>
<UseAppHost>false</UseAppHost>
@@ -7,6 +7,7 @@
<Nullable>enable</Nullable>
<!-- DO NOT ADD THE -windows SUFFIX, It makes the service require the desktop runtime instead of the ASP NET Core Hosting Bundle -->
<TargetFramework>$(TgsFrameworkVersion)</TargetFramework>
<!-- Important that this is core and not host watchdog. Bootstrapping depends on it -->
<Version>$(TgsCoreVersion)</Version>
<!-- DO NOT ENABLE THIS, It makes the service require the desktop runtime instead of the ASP NET Core Hosting Bundle -->
<UseWindowsForms>false</UseWindowsForms>
@@ -0,0 +1,38 @@
using System;
using System.Reflection;
using Tgstation.Server.Common.Extensions;
namespace Tgstation.Server.Host.Watchdog
{
/// <summary>
/// Settings for the bootstrapper feature.
/// </summary>
sealed class BootstrapSettings
{
/// <summary>
/// The current supported major version of <see cref="FileVersion"/>.
/// </summary>
public const int FileMajorVersion = 1;
/// <summary>
/// The token used to substitute <see cref="ServerUpdatePackageUrlFormatter"/>.
/// </summary>
public const string VersionSubstitutionToken = "${version}";
/// <summary>
/// The version of the boostrapper file.
/// </summary>
public Version FileVersion { get; set; } = new Version(FileMajorVersion, 0, 0);
/// <summary>
/// The <see cref="Version"/> of TGS last launched in the lib/Default directory.
/// </summary>
public Version TgsVersion { get; set; } = Assembly.GetEntryAssembly()!.GetName().Version!.Semver();
/// <summary>
/// The URL to format with <see cref="TgsVersion"/> to get the download URL.
/// </summary>
public string ServerUpdatePackageUrlFormatter { get; set; } = $"https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v{VersionSubstitutionToken}/ServerUpdatePackage.zip";
}
}
+97 -18
View File
@@ -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;
@@ -89,7 +92,10 @@ namespace Tgstation.Server.Host.Watchdog
return false;
}
var assemblyStoragePath = Path.Combine(rootLocation, "lib"); // always always next to watchdog
var bootstrappable = args.Contains("--bootstrap", StringComparer.OrdinalIgnoreCase);
var homeDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".tgstation-server");
var assemblyStoragePath = Path.Combine(bootstrappable ? homeDirectory : rootLocation, "lib"); // always always next to watchdog
var defaultAssemblyPath = Path.GetFullPath(Path.Combine(assemblyStoragePath, "Default"));
@@ -119,6 +125,47 @@ namespace Tgstation.Server.Host.Watchdog
else
Directory.CreateDirectory(assemblyStoragePath);
var bootstrapperSettingsFile = Path.Combine(homeDirectory, "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<BootstrapSettings>(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 +175,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 +436,7 @@ namespace Tgstation.Server.Host.Watchdog
}
}
}
}
}
catch (OperationCanceledException ex)
{
@@ -456,6 +456,7 @@ namespace Tgstation.Server.Host.Components.Engine
}
// okay up to us to install it then
string? installPath = null;
try
{
if (customVersionStream != null)
@@ -475,15 +476,26 @@ namespace Tgstation.Server.Host.Components.Engine
var versionString = version.ToString();
await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List<string> { versionString }, deploymentPipelineProcesses, cancellationToken);
await InstallVersionFiles(progressReporter, version, customVersionStream, deploymentPipelineProcesses, cancellationToken);
installPath = await InstallVersionFiles(progressReporter, version, customVersionStream, deploymentPipelineProcesses, cancellationToken);
await eventConsumer.HandleEvent(EventType.EngineInstallComplete, new List<string> { versionString }, deploymentPipelineProcesses, cancellationToken);
ourTcs.SetResult();
await eventConsumer.HandleEvent(EventType.EngineInstallComplete, new List<string> { versionString }, deploymentPipelineProcesses, cancellationToken);
}
catch (Exception ex)
{
if (ex is not OperationCanceledException)
if (installPath != null)
{
try
{
logger.LogDebug("Cleaning up failed installation at {path}...", installPath);
await ioManager.DeleteDirectory(installPath, cancellationToken);
}
catch (Exception ex2)
{
logger.LogError(ex2, "Error cleaning up failed installation!");
}
}
else if (ex is not OperationCanceledException)
await eventConsumer.HandleEvent(EventType.EngineInstallFail, new List<string> { ex.Message }, deploymentPipelineProcesses, cancellationToken);
lock (installedVersions)
@@ -510,8 +522,8 @@ namespace Tgstation.Server.Host.Components.Engine
/// <param name="customVersionStream">Custom zip file <see cref="Stream"/> to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
/// <param name="deploymentPipelineProcesses">If processes should be launched as part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask InstallVersionFiles(
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the directory the engine was installed to.</returns>
async ValueTask<string> InstallVersionFiles(
JobProgressReporter progressReporter,
EngineVersion version,
Stream? customVersionStream,
@@ -597,6 +609,8 @@ namespace Tgstation.Server.Host.Components.Engine
await ioManager.DeleteDirectory(installFullPath, cancellationToken);
throw;
}
return installFullPath;
}
/// <summary>
@@ -603,7 +603,7 @@ namespace Tgstation.Server.Host.Components
logger.LogInformation("{versionString}", assemblyInformationProvider.VersionString);
console.SetTitle(assemblyInformationProvider.VersionString);
CheckSystemCompatibility();
PreflightChecks();
// To let the web server startup immediately before we do any intense work
await Task.Yield();
@@ -673,7 +673,7 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// Check we have a valid system and configuration.
/// </summary>
void CheckSystemCompatibility()
void PreflightChecks()
{
logger.LogDebug("Running as user: {username}", Environment.UserName);
@@ -787,27 +787,48 @@ namespace Tgstation.Server.Host.Components.StaticFiles
// always execute in serial
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken, logger))
{
var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, false, cancellationToken);
var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
var directories = generalConfiguration.AdditionalEventScriptsDirectories?.ToList() ?? new List<string>();
directories.Add(EventScriptsSubdirectory);
var scriptFiles = files
.Select(x => ioManager.GetFileName(x))
.Where(x => scriptNames.Any(
scriptName => x.StartsWith(scriptName, StringComparison.Ordinal)))
var allScripts = new List<string>();
var tasks = directories.Select<string, ValueTask>(
async scriptDirectory =>
{
var resolvedScriptsDir = ioManager.ResolvePath(scriptDirectory);
logger.LogTrace("Checking for scripts in {directory}...", scriptDirectory);
var files = await ioManager.GetFilesWithExtension(scriptDirectory, platformIdentifier.ScriptFileExtension, false, cancellationToken);
var scriptFiles = files
.Select(ioManager.GetFileName)
.Where(x => scriptNames.Any(
scriptName => x.StartsWith(scriptName, StringComparison.Ordinal)))
.Select(x =>
{
var fullScriptPath = ioManager.ConcatPath(resolvedScriptsDir, x);
logger.LogTrace("Found matching script: {scriptPath}", fullScriptPath);
return fullScriptPath;
});
lock (allScripts)
allScripts.AddRange(scriptFiles);
})
.ToList();
if (scriptFiles.Count == 0)
await ValueTaskExtensions.WhenAll(tasks);
if (allScripts.Count == 0)
{
logger.LogTrace("No event scripts starting with \"{scriptName}\" detected", String.Join("\" or \"", scriptNames));
return;
}
foreach (var scriptFile in scriptFiles)
var resolvedInstanceScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
foreach (var scriptFile in allScripts.OrderBy(ioManager.GetFileName))
{
logger.LogTrace("Running event script {scriptFile}...", scriptFile);
await using (var script = await processExecutor.LaunchProcess(
ioManager.ConcatPath(resolvedScriptsDir, scriptFile),
resolvedScriptsDir,
scriptFile,
resolvedInstanceScriptsDir,
String.Join(
' ',
parameters.Select(arg =>
@@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
@@ -150,6 +153,11 @@ namespace Tgstation.Server.Host.Configuration
/// </summary>
public bool OpenDreamSuppressInstallOutput { get; set; }
/// <summary>
/// List of directories that have their contents merged with instance EventScripts directories when executing scripts.
/// </summary>
public List<string>? AdditionalEventScriptsDirectories { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="GeneralConfiguration"/> class.
/// </summary>
@@ -190,6 +198,9 @@ namespace Tgstation.Server.Host.Configuration
if (ByondTopicTimeout <= 1000)
logger.LogWarning("The timeout for sending BYOND topics is very low ({ms}ms). Topic calls may fail to complete at all!", ByondTopicTimeout);
if (AdditionalEventScriptsDirectories?.Any(path => !Path.IsPathRooted(path)) == true)
logger.LogWarning($"Config option \"{nameof(AdditionalEventScriptsDirectories)}\" contains non-rooted paths. These will be evaluated relative to each instances \"Configuration\" directory!");
}
}
}
+17 -1
View File
@@ -208,7 +208,23 @@ namespace Tgstation.Server.Host
async Task RunUpdate()
{
if (await updateExecutor.ExecuteUpdate(updatePath, criticalCancellationToken, criticalCancellationToken))
var updateExecutedSuccessfully = false;
try
{
updateExecutedSuccessfully = await updateExecutor.ExecuteUpdate(updatePath, criticalCancellationToken, criticalCancellationToken);
}
catch (OperationCanceledException ex)
{
logger.LogDebug(ex, "Update cancelled!");
UpdateInProgress = false;
}
catch (Exception ex)
{
logger.LogError(ex, "Update errored!");
UpdateInProgress = false;
}
if (updateExecutedSuccessfully)
{
logger.LogTrace("Update complete!");
await RestartImpl(newVersion, null, true, true);
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -62,6 +64,39 @@ namespace Tgstation.Server.Host.System
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Gets potential paths to the gcore executable.
/// </summary>
/// <returns>The potential paths to the gcore executable.</returns>
static IEnumerable<string> GetPotentialGCorePaths()
{
var enviromentPath = Environment.GetEnvironmentVariable("PATH");
IEnumerable<string> enumerator;
if (enviromentPath == null)
enumerator = Enumerable.Empty<string>();
else
{
var paths = enviromentPath.Split(';');
enumerator = paths
.Select(x => x.Split(':'))
.SelectMany(x => x);
}
var exeName = "gcore";
enumerator = enumerator
.Concat(new List<string>(2)
{
"/usr/bin",
"/usr/share/bin",
"/bin",
});
enumerator = enumerator.Select(x => Path.Combine(x, exeName));
return enumerator;
}
/// <inheritdoc />
public void ResumeProcess(global::System.Diagnostics.Process process)
{
@@ -88,8 +123,15 @@ namespace Tgstation.Server.Host.System
ArgumentNullException.ThrowIfNull(process);
ArgumentNullException.ThrowIfNull(outputFile);
const string GCorePath = "/usr/bin/gcore";
if (!await ioManager.FileExists(GCorePath, cancellationToken))
string? gcorePath = null;
foreach (var path in GetPotentialGCorePaths())
if (await ioManager.FileExists(path, cancellationToken))
{
gcorePath = path;
break;
}
if (gcorePath == null)
throw new JobException(ErrorCode.MissingGCore);
int pid;
@@ -108,7 +150,7 @@ namespace Tgstation.Server.Host.System
string? output;
int exitCode;
await using (var gcoreProc = await lazyLoadedProcessExecutor.Value.LaunchProcess(
GCorePath,
gcorePath,
Environment.CurrentDirectory,
$"{(!minidump ? "-a " : String.Empty)}-o {outputFile} {process.Id}",
cancellationToken,
@@ -19,6 +19,7 @@ General:
OpenDreamGitUrl: https://github.com/OpenDreamProject/OpenDream # The repository to retrieve OpenDream from
OpenDreamGitTagPrefix: v # The prefix to the OpenDream semver as tags appear in the git repository
OpenDreamSuppressInstallOutput: false # Suppress the dotnet output of creating an OpenDream installation. Known to cause hangs in CI.
AdditionalEventScriptsDirectories: # An array of directories that are considered to contain EventScripts alongside instance directories. Working directory for exectued scripts will remain the instance EventScripts directory.
Session:
HighPriorityLiveDreamDaemon: false # If DreamDaemon instances should run as higher priority processes
LowPriorityDeploymentProcesses: true # If TGS Deployments should run as lower priority processes
+38 -30
View File
@@ -315,6 +315,39 @@ namespace Tgstation.Server.ReleaseNotes
Description = "Next patch version"
});
async ValueTask RelocateOpenIssues(Milestone originalMilestone, int moveToMilestoneNumber)
{
if (originalMilestone.OpenIssues + originalMilestone.ClosedIssues > 0)
{
var issuesInUnusedMilestone = await client.Search.SearchIssues(new SearchIssuesRequest
{
Milestone = originalMilestone.Title,
Repos = { { RepoOwner, RepoName } }
});
var issueUpdateTasks = new List<Task>();
foreach (var I in issuesInUnusedMilestone.Items)
{
if (I.State.Value != ItemState.Closed)
issueUpdateTasks.Add(client.Issue.Update(RepoOwner, RepoName, I.Number, new IssueUpdate
{
Milestone = moveToMilestoneNumber
}));
if (I.PullRequest != null && I.PullRequest.Merged)
{
Console.WriteLine($"Adding additional merged PR #{I.Number}...");
var task = GetReleaseNotesFromPR(client, I, doNotCloseMilestone, false, false);
noteTasks.Add(task);
allTasks.Add(task);
}
}
await Task.WhenAll(issueUpdateTasks).ConfigureAwait(false);
}
}
if (version.Build == 0)
{
// close the patch milestone if it exists
@@ -326,39 +359,10 @@ namespace Tgstation.Server.ReleaseNotes
async ValueTask DeleteMilestone(Milestone milestoneToDelete, int moveToMilestoneNumber)
{
Console.WriteLine($"Moving {milestoneToDelete.OpenIssues} open issues and {milestoneToDelete.ClosedIssues} closed issues from unused patch milestone {milestoneToDelete.Title} to upcoming ones and deleting...");
if (milestoneToDelete.OpenIssues + milestoneToDelete.ClosedIssues > 0)
{
var issuesInUnusedMilestone = await client.Search.SearchIssues(new SearchIssuesRequest
{
Milestone = milestoneToDelete.Title,
Repos = { { RepoOwner, RepoName } }
});
var issueUpdateTasks = new List<Task>();
foreach (var I in issuesInUnusedMilestone.Items)
{
if (I.State.Value != ItemState.Closed)
issueUpdateTasks.Add(client.Issue.Update(RepoOwner, RepoName, I.Number, new IssueUpdate
{
Milestone = moveToMilestoneNumber
}));
if (I.PullRequest != null)
{
Console.WriteLine($"Adding additional merged PR #{I.Number}...");
var task = GetReleaseNotesFromPR(client, I, doNotCloseMilestone, false, false);
noteTasks.Add(task);
allTasks.Add(task);
}
}
await Task.WhenAll(issueUpdateTasks).ConfigureAwait(false);
}
await RelocateOpenIssues(milestoneToDelete, moveToMilestoneNumber);
allTasks.Add(client.Issue.Milestone.Delete(RepoOwner, RepoName, milestoneToDelete.Number));
}
var unreleasedNextPatchMilestone = milestones.FirstOrDefault(x => x.Title.StartsWith($"v{highestReleaseVersion.Major}.{highestReleaseVersion.Minor}."));
if (unreleasedNextPatchMilestone != null)
await DeleteMilestone(unreleasedNextPatchMilestone, nextPatchMilestone.Number);
@@ -401,7 +405,11 @@ namespace Tgstation.Server.ReleaseNotes
if (unreleasedNextMinorMilestone != null)
await DeleteMilestone(unreleasedNextMinorMilestone, nextMinorMilestone.Number);
}
else
await RelocateOpenIssues(milestone, nextMinorMilestone.Number);
}
else
await RelocateOpenIssues(milestone, nextPatchMilestone.Number);
}
newNotes.Append(milestone.HtmlUrl);