Get the watchdog working

This commit is contained in:
Cyberboss
2018-04-06 12:38:27 -04:00
parent 1ae33c059a
commit e87f664db8
16 changed files with 213 additions and 47 deletions
+3 -2
View File
@@ -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,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>
+40 -39
View File
@@ -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());
}
}
}
+2 -1
View File
@@ -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() { }
+3 -1
View File
@@ -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>
+6
View File
@@ -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