From 98bbce919f1edfb739e4523d9662e4200f939485 Mon Sep 17 00:00:00 2001 From: Bill Menees Date: Sat, 3 Sep 2022 10:56:23 -0500 Subject: [PATCH] Support ServerHost.Exiting event --- src/Directory.Build.props | 2 +- src/Menees.Remoting/IServerHost.cs | 6 ++- src/Menees.Remoting/ServerHost.cs | 50 +++++++++++++++---- tests/Menees.Remoting.Tests/BaseTests.cs | 3 ++ tests/Menees.Remoting.Tests/RmiServerTests.cs | 18 ++++++- 5 files changed, 64 insertions(+), 15 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 7e3a042..32d2e74 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -29,7 +29,7 @@ - 0.4.0-alpha + 0.5.0-beta diff --git a/src/Menees.Remoting/IServerHost.cs b/src/Menees.Remoting/IServerHost.cs index 1236eba..7f7d740 100644 --- a/src/Menees.Remoting/IServerHost.cs +++ b/src/Menees.Remoting/IServerHost.cs @@ -16,8 +16,10 @@ public interface IServerHost bool IsReady { get; } /// - /// Tells the host to begin the shutdown process and to signal any associated instances to stop. + /// Asks the host to begin the shutdown process and to signal any associated instances to stop. /// /// An optional exit code to pass to the host. - void Exit(int? exitCode = null); + /// True if the shutdown process was begun. False if it was cancelled by a server-side event handler + /// (e.g., by ). + bool Exit(int? exitCode = null); } diff --git a/src/Menees.Remoting/ServerHost.cs b/src/Menees.Remoting/ServerHost.cs index 6aa5276..73b2f18 100644 --- a/src/Menees.Remoting/ServerHost.cs +++ b/src/Menees.Remoting/ServerHost.cs @@ -2,6 +2,7 @@ #region Using Directives +using System.ComponentModel; using System.Runtime.CompilerServices; #endregion @@ -20,6 +21,15 @@ public sealed class ServerHost : IServerHost, IDisposable #endregion + #region Public Events + + /// + /// Raised when is called to allow logging and/or cancellation. + /// + public event CancelEventHandler? Exiting; + + #endregion + #region Public Properties bool IServerHost.IsReady => !this.resetEvent.IsSet; @@ -35,16 +45,36 @@ public sealed class ServerHost : IServerHost, IDisposable /// /// If has been called already. - /// If has been called already. - void IServerHost.Exit(int? exitCode) + /// If has started already. + bool IServerHost.Exit(int? exitCode) { this.EnsureReady(); - this.ExitCode = exitCode; - // Give the caller a little time to receive our response and disconnect. - // Otherwise, this process could end too soon, and the client would get an ArgumentException - // like "Unable to read 4 byte message length from stream. Only 0 bytes were available.". - this.StartExiting(); + CancelEventArgs should = new(); + int? previousExitCode = this.ExitCode; + try + { + this.ExitCode = exitCode; + this.Exiting?.Invoke(this, should); + } + finally + { + if (should.Cancel) + { + this.ExitCode = previousExitCode; + } + } + + bool exit = !should.Cancel; + if (exit) + { + // Give the caller a little time to receive our response and disconnect. + // Otherwise, this process could end too soon, and the client would get an ArgumentException + // like "Unable to read 4 byte message length from stream. Only 0 bytes were available.". + this.StartExiting(); + } + + return exit; } /// @@ -61,7 +91,7 @@ public void Dispose() /// A server instance. /// is null. /// If has been called already. - /// If has been called already. + /// If has started already. public void Add(IServer server) { if (server == null) @@ -87,7 +117,7 @@ public void Add(IServer server) } /// - /// Waits for all added instances to stop after is called. + /// Waits for all added instances to stop after is started. /// public void WaitForExit() => this.resetEvent.Wait(); @@ -186,7 +216,7 @@ private void EnsureReady([CallerMemberName] string? callerMemberName = null) if (this.isExiting) { - throw new InvalidOperationException($"{callerMemberName} can't be called after {nameof(IServerHost.Exit)}."); + throw new InvalidOperationException($"{callerMemberName} can't be called after {nameof(IServerHost.Exit)} starts."); } } diff --git a/tests/Menees.Remoting.Tests/BaseTests.cs b/tests/Menees.Remoting.Tests/BaseTests.cs index 86bd0f3..3377d33 100644 --- a/tests/Menees.Remoting.Tests/BaseTests.cs +++ b/tests/Menees.Remoting.Tests/BaseTests.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -20,6 +21,8 @@ public class BaseTests #region Public Properties + public static bool IsDotNetFramework { get; } = RuntimeInformation.FrameworkDescription.Contains("Framework"); + public ILoggerFactory LoggerFactory => this.logManager?.LoggerFactory ?? NullLoggerFactory.Instance; #endregion diff --git a/tests/Menees.Remoting.Tests/RmiServerTests.cs b/tests/Menees.Remoting.Tests/RmiServerTests.cs index 9483383..23ce6f7 100644 --- a/tests/Menees.Remoting.Tests/RmiServerTests.cs +++ b/tests/Menees.Remoting.Tests/RmiServerTests.cs @@ -74,13 +74,27 @@ public void InProcessServer() server.ReportUnhandledException = WriteUnhandledServerException; host.Add(server); - using RmiClient client = new(serverPath, connectTimeout: TimeSpan.FromSeconds(2), loggerFactory: this.LoggerFactory); + // When this test runs by itself a short timeout is ok. When run with other tests that use a lot of + // client connections (e.g., MessageNodeTests.StringToCodeNameInProcessAsync), then this client's + // proxies on .NET Framework can get a semaphore timeout inside NamedPipeClientStream.Connect. + // That doesn't happen with .NET Core. Since we intentionally test for a TimeoutException at the + // end, we want this timeout to be as short as we can get away with. + TimeSpan connectTimeout = TimeSpan.FromSeconds(IsDotNetFramework ? 10 : 2); + using RmiClient client = new(serverPath, connectTimeout, loggerFactory: this.LoggerFactory); IServerHost proxy = client.CreateProxy(); IServerHost direct = host; proxy.IsReady.ShouldBeTrue(); direct.IsReady.ShouldBeTrue(); + + // Test IServerHost.Exit method and ServerHost.Exiting event. + host.ExitCode.ShouldBeNull(); + bool allowExit = false; + host.Exiting += (s, e) => e.Cancel = !allowExit; + proxy.Exit(0).ShouldBeFalse(); host.ExitCode.ShouldBeNull(); - proxy.Exit(0); + direct.IsReady.ShouldBeTrue(); + allowExit = true; + proxy.Exit(0).ShouldBeTrue(); host.ExitCode.ShouldBe(0); host.WaitForExit();