From e87f664db8775da5f7259e41b2e133f854e6f665 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 6 Apr 2018 12:38:27 -0400 Subject: [PATCH] Get the watchdog working --- build/Dockerfile | 5 +- .../IServer.cs | 2 +- .../IServerFactory.cs | 2 +- .../Tgstation.Server.Host.Startup.csproj | 17 ++++ .../IActiveAssemblyDeleter.cs | 16 ++++ .../IsolatedServerFactory.cs | 45 +++++++++++ .../NativeMethods.cs | 21 +++++ .../PosixActiveAssemblyDeleter.cs | 15 ++++ .../Tgstation.Server.Host.Watchdog.csproj | 6 ++ .../Watchdog.cs | 79 ++++++++++--------- .../WatchdogFactory.cs | 6 +- .../WindowsActiveAssemblyDeleter.cs | 32 ++++++++ src/Tgstation.Server.Host/Server.cs | 3 +- src/Tgstation.Server.Host/ServerFactory.cs | 4 +- .../Tgstation.Server.Host.csproj | 1 + tgstation-server.sln | 6 ++ 16 files changed, 213 insertions(+), 47 deletions(-) rename src/{Tgstation.Server.Host => Tgstation.Server.Host.Startup}/IServer.cs (95%) rename src/{Tgstation.Server.Host => Tgstation.Server.Host.Startup}/IServerFactory.cs (85%) create mode 100644 src/Tgstation.Server.Host.Startup/Tgstation.Server.Host.Startup.csproj create mode 100644 src/Tgstation.Server.Host.Watchdog/IActiveAssemblyDeleter.cs create mode 100644 src/Tgstation.Server.Host.Watchdog/IsolatedServerFactory.cs create mode 100644 src/Tgstation.Server.Host.Watchdog/NativeMethods.cs create mode 100644 src/Tgstation.Server.Host.Watchdog/PosixActiveAssemblyDeleter.cs create mode 100644 src/Tgstation.Server.Host.Watchdog/WindowsActiveAssemblyDeleter.cs diff --git a/build/Dockerfile b/build/Dockerfile index 5da6912dec..5332fa7e91 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -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 diff --git a/src/Tgstation.Server.Host/IServer.cs b/src/Tgstation.Server.Host.Startup/IServer.cs similarity index 95% rename from src/Tgstation.Server.Host/IServer.cs rename to src/Tgstation.Server.Host.Startup/IServer.cs index 4db6422790..8dad58bdcc 100644 --- a/src/Tgstation.Server.Host/IServer.cs +++ b/src/Tgstation.Server.Host.Startup/IServer.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Tgstation.Server.Host +namespace Tgstation.Server.Host.Startup { /// /// Represents the host diff --git a/src/Tgstation.Server.Host/IServerFactory.cs b/src/Tgstation.Server.Host.Startup/IServerFactory.cs similarity index 85% rename from src/Tgstation.Server.Host/IServerFactory.cs rename to src/Tgstation.Server.Host.Startup/IServerFactory.cs index 7a5efbc7b2..43668a1049 100644 --- a/src/Tgstation.Server.Host/IServerFactory.cs +++ b/src/Tgstation.Server.Host.Startup/IServerFactory.cs @@ -1,4 +1,4 @@ -namespace Tgstation.Server.Host +namespace Tgstation.Server.Host.Startup { /// /// For creating s diff --git a/src/Tgstation.Server.Host.Startup/Tgstation.Server.Host.Startup.csproj b/src/Tgstation.Server.Host.Startup/Tgstation.Server.Host.Startup.csproj new file mode 100644 index 0000000000..5f12e43ecd --- /dev/null +++ b/src/Tgstation.Server.Host.Startup/Tgstation.Server.Host.Startup.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + + + + latest + true + + bin\Release\netstandard2.0\Tgstation.Server.Host.Startup..xml + + + + latest + + diff --git a/src/Tgstation.Server.Host.Watchdog/IActiveAssemblyDeleter.cs b/src/Tgstation.Server.Host.Watchdog/IActiveAssemblyDeleter.cs new file mode 100644 index 0000000000..aa6d361490 --- /dev/null +++ b/src/Tgstation.Server.Host.Watchdog/IActiveAssemblyDeleter.cs @@ -0,0 +1,16 @@ +using System.Reflection; + +namespace Tgstation.Server.Host.Watchdog +{ + /// + /// For deleting s used by the program + /// + interface IActiveAssemblyDeleter + { + /// + /// Deletes an that is in use by the runtime + /// + /// The to delete + void DeleteActiveAssembly(Assembly assembly); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host.Watchdog/IsolatedServerFactory.cs b/src/Tgstation.Server.Host.Watchdog/IsolatedServerFactory.cs new file mode 100644 index 0000000000..c7fe7f49cc --- /dev/null +++ b/src/Tgstation.Server.Host.Watchdog/IsolatedServerFactory.cs @@ -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 +{ + /// + /// for loading s in a different + /// + sealed class IsolatedServerFactory : AssemblyLoadContext, IServerFactory + { + /// + /// The path of the to load + /// + readonly string assemblyPath; + + /// + /// Construct a + /// + /// The value of + public IsolatedServerFactory(string assemblyPath) => this.assemblyPath = assemblyPath ?? throw new ArgumentNullException(nameof(assemblyPath)); + + /// + /// Loads the at and creates an from it + /// + /// A new + 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 + /// + protected override Assembly Load(AssemblyName assemblyName) => null; + } +} diff --git a/src/Tgstation.Server.Host.Watchdog/NativeMethods.cs b/src/Tgstation.Server.Host.Watchdog/NativeMethods.cs new file mode 100644 index 0000000000..1ce03329f7 --- /dev/null +++ b/src/Tgstation.Server.Host.Watchdog/NativeMethods.cs @@ -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); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host.Watchdog/PosixActiveAssemblyDeleter.cs b/src/Tgstation.Server.Host.Watchdog/PosixActiveAssemblyDeleter.cs new file mode 100644 index 0000000000..d4f0fdbf17 --- /dev/null +++ b/src/Tgstation.Server.Host.Watchdog/PosixActiveAssemblyDeleter.cs @@ -0,0 +1,15 @@ +using System; +using System.IO; +using System.Reflection; + +namespace Tgstation.Server.Host.Watchdog +{ + /// + /// See for POSIX systems + /// + sealed class PosixActiveAssemblyDeleter : IActiveAssemblyDeleter + { + /// + public void DeleteActiveAssembly(Assembly assembly) => File.Delete(assembly?.Location ?? throw new ArgumentNullException(nameof(assembly))); //glory of inodes + } +} diff --git a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj index d90f320a9d..d6e2d0f05e 100644 --- a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj +++ b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj @@ -18,6 +18,12 @@ + + + + + + diff --git a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs index f655debe8b..1080bc6bc0 100644 --- a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs @@ -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 { /// - sealed class Watchdog : MarshalByRefObject, IWatchdog + sealed class Watchdog : IWatchdog { - string DomainName => GetType().Namespace.Replace(nameof(Watchdog), String.Empty); + /// + /// The initial for the + /// + readonly IServerFactory initialServerFactory; - string DllName => String.Concat(DomainName, ".dll"); + /// + /// The for the + /// + readonly IActiveAssemblyDeleter activeAssemblyDeleter; - async Task 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()); - using (server) - { - var task = (Task)serverRunFunction.Invoke(server, new object[] { args, cancellationToken }); - await task.ConfigureAwait(false); - return (string)serverUpdatePathAccessor.Invoke(server, Array.Empty()); - } - } + /// + /// Construct a + /// + /// The value of + /// The value of + public Watchdog(IServerFactory initialServerFactory, IActiveAssemblyDeleter activeAssemblyDeleter) + { + this.initialServerFactory = initialServerFactory ?? throw new ArgumentNullException(nameof(initialServerFactory)); + this.activeAssemblyDeleter = activeAssemblyDeleter ?? throw new ArgumentNullException(nameof(activeAssemblyDeleter)); + } /// 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); } } } diff --git a/src/Tgstation.Server.Host.Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host.Watchdog/WatchdogFactory.cs index d1dab42db5..c460d89448 100644 --- a/src/Tgstation.Server.Host.Watchdog/WatchdogFactory.cs +++ b/src/Tgstation.Server.Host.Watchdog/WatchdogFactory.cs @@ -1,9 +1,11 @@ -namespace Tgstation.Server.Host.Watchdog +using System.Runtime.InteropServices; + +namespace Tgstation.Server.Host.Watchdog { /// public sealed class WatchdogFactory : IWatchdogFactory { /// - public IWatchdog CreateWatchdog() => new Watchdog(); + public IWatchdog CreateWatchdog() => new Watchdog(new ServerFactory(), RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? (IActiveAssemblyDeleter)new WindowsActiveAssemblyDeleter() : new PosixActiveAssemblyDeleter()); } } diff --git a/src/Tgstation.Server.Host.Watchdog/WindowsActiveAssemblyDeleter.cs b/src/Tgstation.Server.Host.Watchdog/WindowsActiveAssemblyDeleter.cs new file mode 100644 index 0000000000..85b9315095 --- /dev/null +++ b/src/Tgstation.Server.Host.Watchdog/WindowsActiveAssemblyDeleter.cs @@ -0,0 +1,32 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Tgstation.Server.Host.Watchdog +{ + /// + /// See for Windows systems + /// + sealed class WindowsActiveAssemblyDeleter : IActiveAssemblyDeleter + { + /// + 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()); + } + } +} diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index 902f3f3802..d0df99aa60 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -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 { /// - public string UpdatePath => null; + public string UpdatePath => "Tgstation.Server.Host.New.dll"; /// public void Dispose() { } diff --git a/src/Tgstation.Server.Host/ServerFactory.cs b/src/Tgstation.Server.Host/ServerFactory.cs index d22e9938ee..e0f9291516 100644 --- a/src/Tgstation.Server.Host/ServerFactory.cs +++ b/src/Tgstation.Server.Host/ServerFactory.cs @@ -1,4 +1,6 @@ -namespace Tgstation.Server.Host +using Tgstation.Server.Host.Startup; + +namespace Tgstation.Server.Host { /// public sealed class ServerFactory : IServerFactory diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 6082d6d99c..d624c0ae7b 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -22,6 +22,7 @@ + diff --git a/tgstation-server.sln b/tgstation-server.sln index 8e80e05dbf..0aeda4e3e6 100644 --- a/tgstation-server.sln +++ b/tgstation-server.sln @@ -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