mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-07-19 20:13:45 +01:00
Get the watchdog working
This commit is contained in:
+3
-2
@@ -5,6 +5,7 @@ COPY tgstation-server.sln ./
|
||||
|
||||
COPY src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj src/Tgstation.Server.Host.Console/
|
||||
COPY src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj src/Tgstation.Server.Host.Watchdog/
|
||||
COPY src/Tgstation.Server.Host.Startup/Tgstation.Server.Host.Startup.csproj src/Tgstation.Server.Host.Startup/
|
||||
COPY src/Tgstation.Server.Host/Tgstation.Server.Host.csproj src/Tgstation.Server.Host/
|
||||
COPY src/Tgstation.Server.Api/Tgstation.Server.Api.csproj src/Tgstation.Server.Api/
|
||||
|
||||
@@ -17,8 +18,8 @@ RUN dotnet restore -nowarn:MSB3202,nu1503 -p:RestoreUseSkipNonexistentTargets=fa
|
||||
|
||||
COPY . .
|
||||
|
||||
WORKDIR /src/src/Tgstation.Server.Host.Console
|
||||
RUN dotnet build -c Release -o /app
|
||||
WORKDIR /src/src/Tgstation.Server.Host.Startup
|
||||
RUN dotnet build -c Release
|
||||
|
||||
WORKDIR /src/tests/Tgstation.Server.Api.Tests
|
||||
RUN dotnet test -c Release
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host
|
||||
namespace Tgstation.Server.Host.Startup
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the host
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Tgstation.Server.Host
|
||||
namespace Tgstation.Server.Host.Startup
|
||||
{
|
||||
/// <summary>
|
||||
/// For creating <see cref="IServer"/>s
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<WarningsAsErrors />
|
||||
<DocumentationFile>bin\Release\netstandard2.0\Tgstation.Server.Host.Startup..xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Tgstation.Server.Host.Watchdog
|
||||
{
|
||||
/// <summary>
|
||||
/// For deleting <see cref="Assembly"/>s used by the program
|
||||
/// </summary>
|
||||
interface IActiveAssemblyDeleter
|
||||
{
|
||||
/// <summary>
|
||||
/// Deletes an <paramref name="assembly"/> that is in use by the runtime
|
||||
/// </summary>
|
||||
/// <param name="assembly">The <see cref="Assembly"/> to delete</param>
|
||||
void DeleteActiveAssembly(Assembly assembly);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using Tgstation.Server.Host.Startup;
|
||||
|
||||
namespace Tgstation.Server.Host.Watchdog
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IServerFactory"/> for loading <see cref="IServer"/>s in a different <see cref="AssemblyLoadContext"/>
|
||||
/// </summary>
|
||||
sealed class IsolatedServerFactory : AssemblyLoadContext, IServerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// The path of the <see cref="Assembly"/> to load
|
||||
/// </summary>
|
||||
readonly string assemblyPath;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="IsolatedServerFactory"/>
|
||||
/// </summary>
|
||||
/// <param name="assemblyPath">The value of <see cref="assemblyPath"/></param>
|
||||
public IsolatedServerFactory(string assemblyPath) => this.assemblyPath = assemblyPath ?? throw new ArgumentNullException(nameof(assemblyPath));
|
||||
|
||||
/// <summary>
|
||||
/// Loads the <see cref="Assembly"/> at <see cref="assemblyPath"/> and creates an <see cref="IServer"/> from it
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="IServer"/></returns>
|
||||
public IServer CreateServer()
|
||||
{
|
||||
var assembly = LoadFromAssemblyPath(assemblyPath);
|
||||
//find the IServerFactory implementation
|
||||
|
||||
var serverFactoryInterfaceType = typeof(IServerFactory);
|
||||
var serverFactoryImplementationType = assembly.GetTypes().Where(x => serverFactoryInterfaceType.IsAssignableFrom(x)).First();
|
||||
|
||||
var serverFactory = (IServerFactory)Activator.CreateInstance(serverFactoryImplementationType);
|
||||
return serverFactory.CreateServer();
|
||||
}
|
||||
|
||||
//honestly have no idea what this is for, but the examples i see just return null and it seems to work just fine
|
||||
/// <inheritdoc />
|
||||
protected override Assembly Load(AssemblyName assemblyName) => null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Tgstation.Server.Host.Watchdog
|
||||
{
|
||||
static class NativeMethods
|
||||
{
|
||||
public enum MoveFileFlags
|
||||
{
|
||||
None = 0,
|
||||
ReplaceExisting = 1,
|
||||
CopyAllowed = 2,
|
||||
DelayUntilReboot = 4,
|
||||
WriteThrough = 8,
|
||||
CreateHardlink = 16,
|
||||
FailIfNotTrackable = 32,
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, MoveFileFlags dwFlags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Tgstation.Server.Host.Watchdog
|
||||
{
|
||||
/// <summary>
|
||||
/// See <see cref="IActiveAssemblyDeleter"/> for POSIX systems
|
||||
/// </summary>
|
||||
sealed class PosixActiveAssemblyDeleter : IActiveAssemblyDeleter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void DeleteActiveAssembly(Assembly assembly) => File.Delete(assembly?.Location ?? throw new ArgumentNullException(nameof(assembly))); //glory of inodes
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.0" />
|
||||
<PackageReference Include="Octokit" Version="0.29.0" />
|
||||
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Tgstation.Server.Host.Startup\Tgstation.Server.Host.Startup.csproj" />
|
||||
<ProjectReference Include="..\Tgstation.Server.Host\Tgstation.Server.Host.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,58 +1,59 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Startup;
|
||||
|
||||
namespace Tgstation.Server.Host.Watchdog
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class Watchdog : MarshalByRefObject, IWatchdog
|
||||
sealed class Watchdog : IWatchdog
|
||||
{
|
||||
string DomainName => GetType().Namespace.Replace(nameof(Watchdog), String.Empty);
|
||||
/// <summary>
|
||||
/// The initial <see cref="IServerFactory"/> for the <see cref="Watchdog"/>
|
||||
/// </summary>
|
||||
readonly IServerFactory initialServerFactory;
|
||||
|
||||
string DllName => String.Concat(DomainName, ".dll");
|
||||
/// <summary>
|
||||
/// The <see cref="IActiveAssemblyDeleter"/> for the <see cref="Watchdog"/>
|
||||
/// </summary>
|
||||
readonly IActiveAssemblyDeleter activeAssemblyDeleter;
|
||||
|
||||
async Task<string> RunServer(string[] args, CancellationToken cancellationToken)
|
||||
{
|
||||
var assembly = Assembly.LoadFrom(DllName);
|
||||
//the only thing we need to reflect is the IServerFactory, we can cross reference everything else from there
|
||||
var factoryInterfaceType = assembly.GetType(String.Concat(DomainName, ".IServerFactory"));
|
||||
var factoryType = assembly.GetTypes().Where(x => factoryInterfaceType.IsAssignableFrom(x)).First();
|
||||
var factory = Activator.CreateInstance(factoryType);
|
||||
var factoryFunction = factoryInterfaceType.GetMethods().First();
|
||||
var serverType = factoryFunction.ReturnType;
|
||||
var serverRunFunction = serverType.GetMethods().First();
|
||||
var serverUpdatePathAccessor = serverType.GetProperties().First().GetAccessors().First();
|
||||
|
||||
var server = (IDisposable)factoryFunction.Invoke(factory, Array.Empty<object>());
|
||||
using (server)
|
||||
{
|
||||
var task = (Task)serverRunFunction.Invoke(server, new object[] { args, cancellationToken });
|
||||
await task.ConfigureAwait(false);
|
||||
return (string)serverUpdatePathAccessor.Invoke(server, Array.Empty<object>());
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Construct a <see cref="Watchdog"/>
|
||||
/// </summary>
|
||||
/// <param name="initialServerFactory">The value of <see cref="initialServerFactory"/></param>
|
||||
/// <param name="activeAssemblyDeleter">The value of <see cref="activeAssemblyDeleter"/></param>
|
||||
public Watchdog(IServerFactory initialServerFactory, IActiveAssemblyDeleter activeAssemblyDeleter)
|
||||
{
|
||||
this.initialServerFactory = initialServerFactory ?? throw new ArgumentNullException(nameof(initialServerFactory));
|
||||
this.activeAssemblyDeleter = activeAssemblyDeleter ?? throw new ArgumentNullException(nameof(activeAssemblyDeleter));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task RunAsync(string[] args, CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
string updatePath = null;
|
||||
//first run the host we started with
|
||||
var serverFactory = initialServerFactory;
|
||||
var assembly = serverFactory.GetType().Assembly;
|
||||
do
|
||||
{
|
||||
string updatePath;
|
||||
using (var server = serverFactory.CreateServer())
|
||||
{
|
||||
serverFactory = null;
|
||||
await server.RunAsync(args, cancellationToken).ConfigureAwait(false);
|
||||
updatePath = server.UpdatePath;
|
||||
}
|
||||
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
|
||||
if (updatePath == null)
|
||||
break;
|
||||
|
||||
//Ensure the assembly is unloaded
|
||||
GC.Collect(Int32.MaxValue, GCCollectionMode.Default, true);
|
||||
|
||||
File.Delete(DllName);
|
||||
File.Move(updatePath, DllName);
|
||||
}
|
||||
if (updatePath == null)
|
||||
break;
|
||||
|
||||
activeAssemblyDeleter.DeleteActiveAssembly(assembly);
|
||||
File.Move(updatePath, assembly.Location);
|
||||
serverFactory = new IsolatedServerFactory(assembly.Location);
|
||||
}
|
||||
while (!cancellationToken.IsCancellationRequested);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
namespace Tgstation.Server.Host.Watchdog
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Tgstation.Server.Host.Watchdog
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public sealed class WatchdogFactory : IWatchdogFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IWatchdog CreateWatchdog() => new Watchdog();
|
||||
public IWatchdog CreateWatchdog() => new Watchdog(new ServerFactory(), RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? (IActiveAssemblyDeleter)new WindowsActiveAssemblyDeleter() : new PosixActiveAssemblyDeleter());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Tgstation.Server.Host.Watchdog
|
||||
{
|
||||
/// <summary>
|
||||
/// See <see cref="IActiveAssemblyDeleter"/> for Windows systems
|
||||
/// </summary>
|
||||
sealed class WindowsActiveAssemblyDeleter : IActiveAssemblyDeleter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void DeleteActiveAssembly(Assembly assembly)
|
||||
{
|
||||
if (assembly == null)
|
||||
throw new ArgumentNullException(nameof(assembly));
|
||||
|
||||
var location = assembly.Location;
|
||||
//Can't use Path.GetTempFileName() because it may cross drives, which won't actually rename the file
|
||||
//Also append the long path prefix just in case we're running on .NET framework
|
||||
var tmpLocation = String.Concat(@"\\?\", assembly.Location, Guid.NewGuid());
|
||||
File.Delete(tmpLocation);
|
||||
File.Move(location, tmpLocation);
|
||||
|
||||
//this deletes the file at reboot, sadly it also triggers a restart notification on server os's but oh well
|
||||
if (!NativeMethods.MoveFileEx(tmpLocation, null, NativeMethods.MoveFileFlags.DelayUntilReboot))
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Startup;
|
||||
|
||||
namespace Tgstation.Server.Host
|
||||
{
|
||||
@@ -7,7 +8,7 @@ namespace Tgstation.Server.Host
|
||||
sealed class Server : IServer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string UpdatePath => null;
|
||||
public string UpdatePath => "Tgstation.Server.Host.New.dll";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() { }
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Tgstation.Server.Host
|
||||
using Tgstation.Server.Host.Startup;
|
||||
|
||||
namespace Tgstation.Server.Host
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public sealed class ServerFactory : IServerFactory
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Tgstation.Server.Api\Tgstation.Server.Api.csproj" />
|
||||
<ProjectReference Include="..\Tgstation.Server.Host.Startup\Tgstation.Server.Host.Startup.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -53,6 +53,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Host.Watch
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Host.Watchdog.Tests", "tests\Tgstation.Server.Host.Watchdog.Tests\Tgstation.Server.Host.Watchdog.Tests.csproj", "{7500F776-4384-4B5F-A8D8-22461CAD108B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Host.Startup", "src\Tgstation.Server.Host.Startup\Tgstation.Server.Host.Startup.csproj", "{7D067E1B-06A1-49C8-A8E6-7ACBA28A5A41}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -123,6 +125,10 @@ Global
|
||||
{7500F776-4384-4B5F-A8D8-22461CAD108B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7500F776-4384-4B5F-A8D8-22461CAD108B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7500F776-4384-4B5F-A8D8-22461CAD108B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7D067E1B-06A1-49C8-A8E6-7ACBA28A5A41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7D067E1B-06A1-49C8-A8E6-7ACBA28A5A41}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7D067E1B-06A1-49C8-A8E6-7ACBA28A5A41}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7D067E1B-06A1-49C8-A8E6-7ACBA28A5A41}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Reference in New Issue
Block a user