Time to go home

This commit is contained in:
Cyberboss
2018-07-24 17:12:55 -04:00
parent d18c584f63
commit 10955fc3c8
19 changed files with 137 additions and 256 deletions
@@ -80,6 +80,7 @@
</ItemGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Configuration.Install" />
<Reference Include="System.ServiceProcess" />
</ItemGroup>
<ItemGroup>
@@ -17,4 +17,8 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.1.1" />
</ItemGroup>
</Project>
@@ -1,14 +0,0 @@
namespace Tgstation.Server.Host.Watchdog
{
/// <summary>
/// For deleting <see cref="System.Reflection.Assembly"/>s used by the program
/// </summary>
interface IActiveAssemblyDeleter
{
/// <summary>
/// Deletes an <see cref="System.Reflection.Assembly"/> that is in use by the runtime
/// </summary>
/// <param name="assemblyPath">The <see cref="System.Reflection.Assembly.Location"/> of the <see cref="System.Reflection.Assembly"/> to delete</param>
void DeleteActiveAssembly(string assemblyPath);
}
}
@@ -0,0 +1,14 @@
namespace Tgstation.Server.Host.Watchdog
{
/// <summary>
/// For deleting <see cref="Host"/> libraries used by the program
/// </summary>
interface IActiveLibraryDeleter
{
/// <summary>
/// Deletes a <see cref="Host"/> library that is in use by the runtime
/// </summary>
/// <param name="assemblyPath">The path of the library to delete</param>
void DeleteActiveLibrary(string assemblyPath);
}
}
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
@@ -30,14 +32,26 @@ namespace Tgstation.Server.Host.Watchdog
/// <returns>A new <see cref="IServer"/></returns>
public IServer CreateServer(string[] args, string updatePath)
{
var assembly = LoadFromAssemblyPath(assemblyPath);
//find the IServerFactory implementation
//help here: https://stackoverflow.com/questions/40908568/assembly-loading-in-net-core
var serverFactoryInterfaceType = typeof(IServerFactory);
var serverFactoryImplementationType = assembly.GetTypes().Where(x => serverFactoryInterfaceType.IsAssignableFrom(x)).First();
var oldCd = Environment.CurrentDirectory;
Directory.SetCurrentDirectory(Path.GetDirectoryName(assemblyPath));
try
{
var assembly = LoadFromAssemblyPath(assemblyPath);
var serverFactory = (IServerFactory)Activator.CreateInstance(serverFactoryImplementationType);
return serverFactory.CreateServer(args, updatePath);
//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(args, updatePath);
}
finally
{
Directory.SetCurrentDirectory(oldCd);
}
}
//honestly have no idea what this is for, but the examples i see just return null and it seems to work just fine
@@ -1,15 +0,0 @@
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(string assemblyPath) => File.Delete(assemblyPath ?? throw new ArgumentNullException(nameof(assemblyPath))); //glory of inodes
}
}
@@ -0,0 +1,14 @@
using System;
using System.IO;
namespace Tgstation.Server.Host.Watchdog
{
/// <summary>
/// See <see cref="IActiveLibraryDeleter"/> for POSIX systems
/// </summary>
sealed class PosixActiveLibraryDeleter : IActiveLibraryDeleter
{
/// <inheritdoc />
public void DeleteActiveLibrary(string assemblyPath) => Directory.Delete(assemblyPath ?? throw new ArgumentNullException(nameof(assemblyPath)), true); //glory of inodes
}
}
@@ -23,7 +23,6 @@
<ItemGroup>
<ProjectReference Include="..\Tgstation.Server.Host.Startup\Tgstation.Server.Host.Startup.csproj" />
<ProjectReference Include="..\Tgstation.Server.Host\Tgstation.Server.Host.csproj" />
</ItemGroup>
</Project>
+36 -41
View File
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Startup;
@@ -10,15 +11,11 @@ namespace Tgstation.Server.Host.Watchdog
/// <inheritdoc />
sealed class Watchdog : IWatchdog
{
/// <summary>
/// The initial <see cref="IServerFactory"/> for the <see cref="Watchdog"/>
/// </summary>
readonly IServerFactory initialServerFactory;
/// <summary>
/// The <see cref="IActiveAssemblyDeleter"/> for the <see cref="Watchdog"/>
/// The <see cref="IActiveLibraryDeleter"/> for the <see cref="Watchdog"/>
/// </summary>
readonly IActiveAssemblyDeleter activeAssemblyDeleter;
readonly IActiveLibraryDeleter activeLibraryDeleter;
/// <summary>
/// The <see cref="IIsolatedAssemblyContextFactory"/> for the <see cref="Watchdog"/>
@@ -33,14 +30,12 @@ namespace Tgstation.Server.Host.Watchdog
/// <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>
/// <param name="activeLibraryDeleter">The value of <see cref="activeLibraryDeleter"/></param>
/// <param name="isolatedAssemblyLoader">The value of <see cref="isolatedAssemblyLoader"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
public Watchdog(IServerFactory initialServerFactory, IActiveAssemblyDeleter activeAssemblyDeleter, IIsolatedAssemblyContextFactory isolatedAssemblyLoader, ILogger<Watchdog> logger)
public Watchdog(IActiveLibraryDeleter activeLibraryDeleter, IIsolatedAssemblyContextFactory isolatedAssemblyLoader, ILogger<Watchdog> logger)
{
this.initialServerFactory = initialServerFactory ?? throw new ArgumentNullException(nameof(initialServerFactory));
this.activeAssemblyDeleter = activeAssemblyDeleter ?? throw new ArgumentNullException(nameof(activeAssemblyDeleter));
this.activeLibraryDeleter = activeLibraryDeleter ?? throw new ArgumentNullException(nameof(activeLibraryDeleter));
this.isolatedAssemblyLoader = isolatedAssemblyLoader ?? throw new ArgumentNullException(nameof(isolatedAssemblyLoader));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -48,48 +43,43 @@ namespace Tgstation.Server.Host.Watchdog
/// <inheritdoc />
public async Task RunAsync(string[] args, CancellationToken cancellationToken)
{
const string DefaultAssemblyPath = "Default";
var assemblyStoragePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "lib");
var assemblyName = String.Join(".", nameof(Tgstation), nameof(Server), nameof(Host), "dll");
logger.LogInformation("Host watchdog starting...");
var nextAssemblyPath = Path.GetFullPath(Path.Combine(assemblyStoragePath, DefaultAssemblyPath));
string lastAssemblyPath = null;
try
{
//first run the host we started with
logger.LogTrace("Running with initial server factory...");
var serverFactory = initialServerFactory;
logger.LogTrace("Determining location of host assembly...");
var assemblyPath = serverFactory.GetType().Assembly.Location;
logger.LogDebug("Path to initial host assembly: {0}", assemblyPath);
var assemblyName = Path.GetFileName(assemblyPath);
const string UpdatePath = "Updates";
var newAssemblyDirectory = Path.Combine(Path.GetDirectoryName(assemblyPath), UpdatePath);
var firstIteration = true;
do
while (!cancellationToken.IsCancellationRequested)
using (logger.BeginScope("Host invocation"))
{
logger.LogTrace("Atttempting to create new server factory...");
Guid updateGuid;
using (var server = serverFactory.CreateServer(args, newAssemblyDirectory))
{
logger.LogTrace("Running server...");
await server.RunAsync(cancellationToken).ConfigureAwait(false);
logger.LogInformation("Active host exited.");
{ //forces serverFactory out of the picture once the scope ends
var serverFactory = isolatedAssemblyLoader.CreateIsolatedServerFactory(Path.Combine(nextAssemblyPath, assemblyName));
using (var server = serverFactory.CreateServer(args, assemblyStoragePath))
{
logger.LogTrace("Running server...");
await server.RunAsync(cancellationToken).ConfigureAwait(false);
logger.LogInformation("Active host exited.");
if (!server.UpdateGuid.HasValue)
break;
updateGuid = server.UpdateGuid.Value;
if (!server.UpdateGuid.HasValue)
break;
updateGuid = server.UpdateGuid.Value;
}
}
logger.LogInformation("Update path is set to \"{0}\", attempting host assembly hotswap...", updateGuid);
GC.Collect(Int32.MaxValue, GCCollectionMode.Forced, true, true);
if (!firstIteration)
{
logger.LogTrace("Deleting old host assembly");
//TODO: make this use directories
//activeAssemblyDeleter.DeleteActiveAssembly(newAssemblyDirectory);
}
logger.LogTrace("Atttempting to create new server factory...");
serverFactory = isolatedAssemblyLoader.CreateIsolatedServerFactory(Path.Combine(newAssemblyDirectory, updateGuid.ToString(), assemblyName));
firstIteration = false;
activeLibraryDeleter.DeleteActiveLibrary(nextAssemblyPath);
nextAssemblyPath = Path.Combine(assemblyStoragePath, updateGuid.ToString());
}
while (!cancellationToken.IsCancellationRequested);
}
catch (OperationCanceledException)
{
@@ -98,6 +88,11 @@ namespace Tgstation.Server.Host.Watchdog
catch (Exception e)
{
logger.LogCritical("Error running host assembly! Exception: {0}", e);
nextAssemblyPath = lastAssemblyPath ?? DefaultAssemblyPath; //don't wanna save a critfailed assembly
}
if (nextAssemblyPath != DefaultAssemblyPath)
{
logger.LogInformation("Setting next default host assembly path to {0}...", nextAssemblyPath);
}
logger.LogInformation("Host watchdog exiting...");
}
@@ -10,6 +10,6 @@ namespace Tgstation.Server.Host.Watchdog
{
/// <inheritdoc />
[ExcludeFromCodeCoverage]
public IWatchdog CreateWatchdog(ILoggerFactory loggerFactory) => new Watchdog(new ServerFactory(), RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? (IActiveAssemblyDeleter)new WindowsActiveAssemblyDeleter() : new PosixActiveAssemblyDeleter(), new IsolatedAssemblyContextFactory(), loggerFactory?.CreateLogger<Watchdog>() ?? throw new ArgumentNullException(nameof(loggerFactory)));
public IWatchdog CreateWatchdog(ILoggerFactory loggerFactory) => new Watchdog(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? (IActiveLibraryDeleter)new WindowsActiveLibraryDeleter() : new PosixActiveLibraryDeleter(), new IsolatedAssemblyContextFactory(), loggerFactory?.CreateLogger<Watchdog>() ?? throw new ArgumentNullException(nameof(loggerFactory)));
}
}
@@ -1,37 +0,0 @@
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
namespace Tgstation.Server.Host.Watchdog
{
/// <summary>
/// See <see cref="IActiveAssemblyDeleter"/> for Windows systems
/// </summary>
sealed class WindowsActiveAssemblyDeleter : IActiveAssemblyDeleter
{
/// <summary>
/// Set a file located at <paramref name="path"/> to be deleted on reboot
/// </summary>
/// <param name="path">The file to delete on reboot</param>
[ExcludeFromCodeCoverage]
static void DeleteFileOnReboot(string path)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !NativeMethods.MoveFileEx(path, null, NativeMethods.MoveFileFlags.DelayUntilReboot))
throw new Win32Exception(Marshal.GetLastWin32Error());
}
/// <inheritdoc />
public void DeleteActiveAssembly(string assemblyPath)
{
if (assemblyPath == null)
throw new ArgumentNullException(nameof(assemblyPath));
//Can't use Path.GetTempFileName() because it may cross drives, which won't actually rename the file
var tmpLocation = String.Concat(assemblyPath, Guid.NewGuid());
File.Move(assemblyPath, tmpLocation);
DeleteFileOnReboot(tmpLocation);
}
}
}
@@ -0,0 +1,36 @@
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
namespace Tgstation.Server.Host.Watchdog
{
/// <summary>
/// See <see cref="IActiveLibraryDeleter"/> for Windows systems
/// </summary>
sealed class WindowsActiveLibraryDeleter : IActiveLibraryDeleter
{
/// <summary>
/// Set a directory located at <paramref name="path"/> to be deleted on reboot
/// </summary>
/// <param name="path">The file to delete on reboot</param>
[ExcludeFromCodeCoverage]
static void DeleteDirectoryOnReboot(string path)
{
if (!NativeMethods.MoveFileEx(path, null, NativeMethods.MoveFileFlags.DelayUntilReboot))
throw new Win32Exception(Marshal.GetLastWin32Error());
}
/// <inheritdoc />
public void DeleteActiveLibrary(string assemblyPath)
{
if (assemblyPath == null)
throw new ArgumentNullException(nameof(assemblyPath));
var tmpLocation = Path.Combine(Path.GetDirectoryName(assemblyPath), Guid.NewGuid().ToString());
Directory.Move(assemblyPath, tmpLocation);
DeleteDirectoryOnReboot(tmpLocation);
}
}
}
+1
View File
@@ -56,6 +56,7 @@ namespace Tgstation.Server.Host
[ExcludeFromCodeCoverage]
public async Task RunAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Hello world!");
using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
using (var webHost = webHostBuilder
.UseStartup<Application>()
@@ -1,18 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Tgstation.Server.Host.Watchdog.Tests
{
/// <summary>
/// Tests for <see cref="IsolatedAssemblyContextFactory"/>
/// </summary>
[TestClass]
public sealed class TestIsolatedAssemblyContextFactory
{
[TestMethod]
public void TestServerFactoryCreation()
{
var contextFactory = new IsolatedAssemblyContextFactory();
Assert.IsNotNull(contextFactory.CreateIsolatedServerFactory(typeof(ServerFactory).Assembly.Location));
}
}
}
@@ -1,26 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace Tgstation.Server.Host.Watchdog.Tests
{
/// <summary>
/// Tests for <see cref="IsolatedServerFactory"/>
/// </summary>
[TestClass]
public sealed class TestIsolatedServerFactory
{
[TestMethod]
public void TestConstruction()
{
Assert.ThrowsException<ArgumentNullException>(() => new IsolatedServerFactory(null));
var isf = new IsolatedServerFactory(typeof(ServerFactory).Assembly.Location);
}
[TestMethod]
public void TestLoading()
{
var isf = new IsolatedServerFactory(typeof(ServerFactory).Assembly.Location);
Assert.IsNotNull(isf.CreateServer(Array.Empty<string>(), String.Empty));
}
}
}
@@ -1,40 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
namespace Tgstation.Server.Host.Watchdog.Tests
{
/// <summary>
/// Tests for <see cref="PosixActiveAssemblyDeleter"/>
/// </summary>
[TestClass]
public sealed class TestPosixActiveAssemblyDeleter
{
[TestMethod]
public void TestAssemblyDeletion()
{
var ourAssembly = GetType().Assembly;
var fakeAssemblyPath = String.Concat(ourAssembly.Location, Guid.NewGuid());
File.Copy(ourAssembly.Location, fakeAssemblyPath);
try
{
var deleter = new PosixActiveAssemblyDeleter();
deleter.DeleteActiveAssembly(fakeAssemblyPath);
Assert.IsFalse(File.Exists(fakeAssemblyPath));
}
catch
{
File.Delete(fakeAssemblyPath);
throw;
}
}
[TestMethod]
public void TestNullInvoke()
{
var deleter = new PosixActiveAssemblyDeleter();
Assert.ThrowsException<ArgumentNullException>(() => deleter.DeleteActiveAssembly(null));
}
}
}
@@ -14,15 +14,13 @@ namespace Tgstation.Server.Host.Watchdog.Tests
[TestMethod]
public void TestConstruction()
{
Assert.ThrowsException<ArgumentNullException>(() => new Watchdog(null, null, null, null));
var mockServerFactory = new Mock<IServerFactory>();
Assert.ThrowsException<ArgumentNullException>(() => new Watchdog(mockServerFactory.Object, null, null, null));
var mockActiveAssemblyDeleter = new Mock<IActiveAssemblyDeleter>();
Assert.ThrowsException<ArgumentNullException>(() => new Watchdog(mockServerFactory.Object, mockActiveAssemblyDeleter.Object, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new Watchdog(null, null, null));
var mockActiveAssemblyDeleter = new Mock<IActiveLibraryDeleter>();
Assert.ThrowsException<ArgumentNullException>(() => new Watchdog(mockActiveAssemblyDeleter.Object, null, null));
var mockIsolatedServerContextFactory = new Mock<IIsolatedAssemblyContextFactory>();
Assert.ThrowsException<ArgumentNullException>(() => new Watchdog(mockServerFactory.Object, mockActiveAssemblyDeleter.Object, mockIsolatedServerContextFactory.Object, null));
Assert.ThrowsException<ArgumentNullException>(() => new Watchdog(mockActiveAssemblyDeleter.Object, mockIsolatedServerContextFactory.Object, null));
var mockLogger = new LoggerFactory().CreateLogger<Watchdog>();
var wd = new Watchdog(mockServerFactory.Object, mockActiveAssemblyDeleter.Object, mockIsolatedServerContextFactory.Object, mockLogger);
var wd = new Watchdog(mockActiveAssemblyDeleter.Object, mockIsolatedServerContextFactory.Object, mockLogger);
}
class MockServerFactory : IServerFactory
@@ -37,11 +35,11 @@ namespace Tgstation.Server.Host.Watchdog.Tests
{
var mockServer = new Mock<IServer>();
var mockServerFactory = new MockServerFactory(mockServer.Object);
var mockActiveAssemblyDeleter = new Mock<IActiveAssemblyDeleter>();
var mockActiveAssemblyDeleter = new Mock<IActiveLibraryDeleter>();
var mockIsolatedServerContextFactory = new Mock<IIsolatedAssemblyContextFactory>();
var mockLogger = new LoggerFactory().CreateLogger<Watchdog>();
var wd = new Watchdog(mockServerFactory, mockActiveAssemblyDeleter.Object, mockIsolatedServerContextFactory.Object, mockLogger);
var wd = new Watchdog(mockActiveAssemblyDeleter.Object, mockIsolatedServerContextFactory.Object, mockLogger);
using (var cts = new CancellationTokenSource())
{
@@ -1,48 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.ComponentModel;
using System.IO;
namespace Tgstation.Server.Host.Watchdog.Tests
{
/// <summary>
/// Tests for <see cref="WindowsActiveAssemblyDeleter"/>
/// </summary>
[TestClass]
public sealed class TestWindowsActiveAssemblyDeleter
{
[TestMethod]
public void TestAssemblyDeletion()
{
var ourAssembly = GetType().Assembly;
var fakeAssemblyPath = String.Concat(ourAssembly.Location, Guid.NewGuid());
File.Copy(ourAssembly.Location, fakeAssemblyPath);
try
{
var deleter = new WindowsActiveAssemblyDeleter();
try
{
deleter.DeleteActiveAssembly(fakeAssemblyPath);
}
catch (Win32Exception e)
{
Assert.AreEqual(e.NativeErrorCode, 5);
}
Assert.IsFalse(File.Exists(fakeAssemblyPath));
}
catch
{
File.Delete(fakeAssemblyPath);
throw;
}
}
[TestMethod]
public void TestNullInvoke()
{
var deleter = new WindowsActiveAssemblyDeleter();
Assert.ThrowsException<ArgumentNullException>(() => deleter.DeleteActiveAssembly(null));
}
}
}
+3
View File
@@ -52,6 +52,9 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Host.Tests", "tests\Tgstation.Server.Host.Tests\Tgstation.Server.Host.Tests.csproj", "{A3362FF6-550F-480F-859E-8EC1EB6EAB31}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Host.Watchdog", "src\Tgstation.Server.Host.Watchdog\Tgstation.Server.Host.Watchdog.csproj", "{5D2D682C-6BF0-439C-850B-6AB945BBEAEA}"
ProjectSection(ProjectDependencies) = postProject
{2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A} = {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}
EndProjectSection
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