Skip to content

Commit

Permalink
Support ServerHost.Exiting event
Browse files Browse the repository at this point in the history
  • Loading branch information
menees committed Sep 3, 2022
1 parent 687ad0a commit 98bbce9
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 15 deletions.
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

<!-- Make the assembly, file, and NuGet package versions the same. -->
<!-- https://docs.microsoft.com/en-us/nuget/concepts/package-versioning#pre-release-versions -->
<Version>0.4.0-alpha</Version>
<Version>0.5.0-beta</Version>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)'=='Debug'">
Expand Down
6 changes: 4 additions & 2 deletions src/Menees.Remoting/IServerHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ public interface IServerHost
bool IsReady { get; }

/// <summary>
/// Tells the host to begin the shutdown process and to signal any associated <see cref="IServer"/> instances to stop.
/// Asks the host to begin the shutdown process and to signal any associated <see cref="IServer"/> instances to stop.
/// </summary>
/// <param name="exitCode">An optional exit code to pass to the host.</param>
void Exit(int? exitCode = null);
/// <returns>True if the shutdown process was begun. False if it was cancelled by a server-side event handler
/// (e.g., by <see cref="ServerHost.Exiting"/>).</returns>
bool Exit(int? exitCode = null);
}
50 changes: 40 additions & 10 deletions src/Menees.Remoting/ServerHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#region Using Directives

using System.ComponentModel;
using System.Runtime.CompilerServices;

#endregion
Expand All @@ -20,6 +21,15 @@ public sealed class ServerHost : IServerHost, IDisposable

#endregion

#region Public Events

/// <summary>
/// Raised when <see cref="IServerHost.Exit(int?)"/> is called to allow logging and/or cancellation.
/// </summary>
public event CancelEventHandler? Exiting;

#endregion

#region Public Properties

bool IServerHost.IsReady => !this.resetEvent.IsSet;
Expand All @@ -35,16 +45,36 @@ public sealed class ServerHost : IServerHost, IDisposable

/// <inheritdoc/>
/// <exception cref="ObjectDisposedException">If <see cref="Dispose()"/> has been called already.</exception>
/// <exception cref="InvalidOperationException">If <see cref="IServerHost.Exit"/> has been called already.</exception>
void IServerHost.Exit(int? exitCode)
/// <exception cref="InvalidOperationException">If <see cref="IServerHost.Exit"/> has started already.</exception>
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;
}

/// <inheritdoc/>
Expand All @@ -61,7 +91,7 @@ public void Dispose()
/// <param name="server">A server instance.</param>
/// <exception cref="ArgumentNullException"><paramref name="server"/> is null.</exception>
/// <exception cref="ObjectDisposedException">If <see cref="Dispose()"/> has been called already.</exception>
/// <exception cref="InvalidOperationException">If <see cref="IServerHost.Exit"/> has been called already.</exception>
/// <exception cref="InvalidOperationException">If <see cref="IServerHost.Exit"/> has started already.</exception>
public void Add(IServer server)
{
if (server == null)
Expand All @@ -87,7 +117,7 @@ public void Add(IServer server)
}

/// <summary>
/// Waits for all added <see cref="IServer"/> instances to stop after <see cref="IServerHost.Exit"/> is called.
/// Waits for all added <see cref="IServer"/> instances to stop after <see cref="IServerHost.Exit"/> is started.
/// </summary>
public void WaitForExit() => this.resetEvent.Wait();

Expand Down Expand Up @@ -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.");
}
}

Expand Down
3 changes: 3 additions & 0 deletions tests/Menees.Remoting.Tests/BaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

Expand All @@ -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
Expand Down
18 changes: 16 additions & 2 deletions tests/Menees.Remoting.Tests/RmiServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,27 @@ public void InProcessServer()
server.ReportUnhandledException = WriteUnhandledServerException;
host.Add(server);

using RmiClient<IServerHost> 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<IServerHost> 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();

Expand Down

0 comments on commit 98bbce9

Please sign in to comment.