Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,11 @@ public static string Tcp_NoSecurity_XmlDuplexCallback_Address
get { return GetEndpointAddress("DuplexCallbackXmlComplexType.svc/tcp-nosecurity-callback", protocol: "net.tcp"); }
}

public static string Tcp_NoSecurity_ServerInitiatedShutdown_Address
{
get { return GetEndpointAddress("ServerInitiatedShutdown.svc/tcp-nosecurity-server-shutdown", protocol: "net.tcp"); }
}


public static string Tcp_CustomBinding_SslStreamSecurity_Address
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,24 @@ public interface IWcfDuplexService_Xml_Callback
void OnXmlPingCallback(XmlCompositeTypeDuplexCallbackOnly xmlCompositeType);
}

// Client-side mirror of WcfService.IServerInitiatedShutdownService used by the
// ServerInitiatedSessionShutdownTests scenario (dotnet/wcf#5803 regression).
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IServerInitiatedShutdownCallback))]
public interface IServerInitiatedShutdownService
{
[OperationContract]
string Echo(string text);

[OperationContract]
string RequestServerShutdown();
}

public interface IServerInitiatedShutdownCallback
{
[OperationContract(IsOneWay = true)]
void OnShutdownNotification();
}

[ServiceContract(CallbackContract = typeof(IWcfDuplexService_CallbackConcurrencyMode_Callback))]
public interface IWcfDuplexService_CallbackConcurrencyMode
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Threading;
using Infrastructure.Common;
using Xunit;

// Regression coverage for dotnet/wcf#5803.
//
// When a server closes its session while the client is idle between calls,
// the client's duplex receive pump processes the EndRecord and the
// ServiceChannel's auto-close path must transition the outer channel from
// Opened to Closed. Prior to the fix, only the inner session was half-closed
// and the outer channel remained Opened indefinitely.
public class ServerInitiatedSessionShutdownTests : ConditionalWcfTest
{
[CallbackBehavior(UseSynchronizationContext = false)]
private class NoOpCallback : IServerInitiatedShutdownCallback
{
public void OnShutdownNotification() { }
}

[WcfFact]
[Condition(nameof(Skip_CoreWCFService_FailedTest))]
[OuterLoop]
public static void ServerInitiatedShutdown_ClientChannelTransitionsToClosed()
{
DuplexChannelFactory<IServerInitiatedShutdownService> factory = null;
IServerInitiatedShutdownService proxy = null;
ICommunicationObject commObj = null;
ManualResetEventSlim closedSignal = new ManualResetEventSlim(false);
bool faulted = false;

try
{
// *** SETUP *** \\
NetTcpBinding binding = new NetTcpBinding(SecurityMode.None);
binding.CloseTimeout = TimeSpan.FromSeconds(10);
binding.OpenTimeout = TimeSpan.FromSeconds(10);
binding.SendTimeout = TimeSpan.FromSeconds(10);

InstanceContext context = new InstanceContext(new NoOpCallback());
factory = new DuplexChannelFactory<IServerInitiatedShutdownService>(
context,
binding,
new EndpointAddress(Endpoints.Tcp_NoSecurity_ServerInitiatedShutdown_Address));

proxy = factory.CreateChannel();
commObj = (ICommunicationObject)proxy;
commObj.Closed += (s, e) => closedSignal.Set();
commObj.Faulted += (s, e) => { faulted = true; closedSignal.Set(); };
commObj.Open();

// *** EXECUTE *** \\
// Warm-up call so the duplex receive pump is active.
string echoed = proxy.Echo("hello");
Assert.Equal("hello", echoed);

// Ask the service to close its session after replying.
string shutdownReply = proxy.RequestServerShutdown();
Assert.Equal("Server shutting down", shutdownReply);

// *** VALIDATE *** \\
// Wait up to 10s for the client to react to the server's EndRecord.
bool signaled = closedSignal.Wait(TimeSpan.FromSeconds(10));

Assert.True(signaled,
$"Client ServiceChannel did not transition out of {CommunicationState.Opened} after the " +
$"server closed its session. Current state: {commObj.State}. " +
"This indicates issue dotnet/wcf#5803 has regressed.");

Assert.False(faulted,
"Client ServiceChannel transitioned to Faulted instead of Closed in response to a graceful " +
"server-initiated session shutdown.");

Assert.Equal(CommunicationState.Closed, commObj.State);
}
finally
{
// *** ENSURE CLEANUP *** \\
ScenarioTestHelpers.CloseCommunicationObjects((ICommunicationObject)proxy, factory);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,25 @@ public interface IDuplexChannelCallback
void OnPingCallback(Guid guid);
}

// Contract used by ServerInitiatedSessionShutdownTests (dotnet/wcf#5803).
// The server replies to RequestServerShutdown and then closes its session channel,
// which delivers an EndRecord to the client and exercises ServiceChannel.DecrementActivity.
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IServerInitiatedShutdownCallback))]
public interface IServerInitiatedShutdownService
{
[OperationContract]
string Echo(string text);

[OperationContract]
string RequestServerShutdown();
}

public interface IServerInitiatedShutdownCallback
{
[OperationContract(IsOneWay = true)]
void OnShutdownNotification();
}

[ServiceContract(CallbackContract = typeof(IWcfDuplexTaskReturnCallback))]
public interface IWcfDuplexTaskReturnService
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,20 @@

#if NET
using CoreWCF;
using CoreWCF.Channels;
using CoreWCF.Description;
using CoreWCF.Dispatcher;
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
#else
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Threading.Tasks;
#endif

Expand Down Expand Up @@ -49,6 +59,109 @@ public void Ping_Xml(Guid guid)
}
}

// Service implementation for ServerInitiatedShutdownService (dotnet/wcf#5803 regression coverage).
// On RequestServerShutdown the service gracefully closes the *current session's* output channel
// (sends the NetFraming EndRecord) so the idle duplex client's receive pump observes end-of-stream
// and runs DecrementActivity. This reproduces the user-reported scenario where a host shuts down
// while a session-ful client is idle, without disturbing other sessions on the same host.
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, AddressFilterMode = AddressFilterMode.Any)]
public class ServerInitiatedShutdownService : IServerInitiatedShutdownService
{
public string Echo(string text)
{
return text;
}

public string RequestServerShutdown()
{
IClientChannel channel = CaptureChannelServiceBehavior.GetCurrentChannel();
Task.Run(async () =>
{
await Task.Delay(250);
ISessionChannel<IDuplexSession> duplex = channel as ISessionChannel<IDuplexSession>;
try
{
if (duplex != null)
{
#if NET
await duplex.Session.CloseOutputSessionAsync();
#else
duplex.Session.CloseOutputSession();
#endif
}
else if (channel != null)
{
// The inspector hands us a typed callback proxy that doesn't expose
// ISessionChannel<IDuplexSession>; closing the proxy tears down the
// underlying session and sends the framing EndRecord to the client.
#if NET
await ((ICommunicationObject)channel).CloseAsync();
#else
((ICommunicationObject)channel).Close();
#endif
}
}
catch
{
try { if (channel != null) channel.Abort(); } catch { }
}
});
return "Server shutting down";
}
}

// Captures the per-session IClientChannel into OperationContext.Extensions so the service
// operation can act on it (e.g., to gracefully close that session's output channel).
// No portable API on CoreWCF exposes the underlying ISessionChannel<IDuplexSession> from
// OperationContext, so an IDispatchMessageInspector is the simplest cross-framework hook.
internal sealed class CaptureChannelServiceBehavior : IServiceBehavior, IDispatchMessageInspector
{
public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase,
Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) { }

public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { }

public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
foreach (ChannelDispatcher cd in serviceHostBase.ChannelDispatchers)
{
foreach (EndpointDispatcher ed in cd.Endpoints)
{
ed.DispatchRuntime.MessageInspectors.Add(this);
}
}
}

public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
OperationContext ctx = OperationContext.Current;
if (ctx != null && ctx.Extensions.Find<ChannelHolder>() == null)
{
ctx.Extensions.Add(new ChannelHolder(channel));
}
return null;
}

public void BeforeSendReply(ref Message reply, object correlationState) { }

public static IClientChannel GetCurrentChannel()
{
OperationContext ctx = OperationContext.Current;
if (ctx == null) return null;
ChannelHolder holder = ctx.Extensions.Find<ChannelHolder>();
return holder == null ? null : holder.Channel;
}

private sealed class ChannelHolder : IExtension<OperationContext>
{
private readonly IClientChannel _channel;
public IClientChannel Channel { get { return _channel; } }
public ChannelHolder(IClientChannel channel) { _channel = channel; }
public void Attach(OperationContext owner) { }
public void Detach(OperationContext owner) { }
}
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall, AddressFilterMode = AddressFilterMode.Any)]
public class DuplexCallbackService : IDuplexChannelService
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,31 @@ public DuplexCallbackErrorHandlerServiceHost(params Uri[] baseAddresses)
{
}
}

// Host for ServerInitiatedShutdownService used by dotnet/wcf#5803 regression test.
[TestServiceDefinition(Schema = ServiceSchema.NETTCP, BasePath = "ServerInitiatedShutdown.svc")]
public class ServerInitiatedShutdownServiceHost : TestServiceHostBase<IServerInitiatedShutdownService>
{
protected override string Address { get { return "tcp-nosecurity-server-shutdown"; } }

protected override Binding GetBinding()
{
return new NetTcpBinding(SecurityMode.None);
}

public ServerInitiatedShutdownServiceHost(params Uri[] baseAddresses)
: base(typeof(ServerInitiatedShutdownService), baseAddresses)
{
}

// Register CaptureChannelServiceBehavior here rather than in the constructor.
// On the CoreWCF host shim, ServiceHost.Description returns a throwaway
// ServiceDescription until ApplyConfig sets the underlying ServiceHostBase, so a
// constructor-time Behaviors.Add silently no-ops on that path (dotnet/wcf#5803).
protected override void ApplyConfiguration()
{
base.ApplyConfiguration();
this.Description.Behaviors.Add(new CaptureChannelServiceBehavior());
}
}
}
Loading
Loading