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();