Skip to content
Merged
27 changes: 19 additions & 8 deletions Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Opc.Ua.Security.Certificates;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -99,16 +100,19 @@ public class Startup
{
private const string kHttpsContentType = "text/plain";

/// <summary>
/// Get the Https listener.
/// </summary>
public static HttpsTransportListener Listener { get; set; }

/// <summary>
/// Configure the request pipeline for the listener.
/// The <paramref name="listener"/> is resolved from the web host's DI container,
/// which is populated via <c>IWebHostBuilder.ConfigureServices</c> in
/// <see cref="HttpsTransportListener.ConfigureWebHost"/>.
/// Using method injection here (rather than constructor injection) ensures the
/// instance is resolved from the web-host app container on all target frameworks,
/// including .NET 8+ where <c>HostBuilder</c> activates <c>Startup</c> from a
/// separate host-level container that does not include web-host app services.
/// </summary>
/// <param name="appBuilder">The application builder.</param>
public void Configure(IApplicationBuilder appBuilder)
/// <param name="listener">The transport listener that handles OPC UA requests.</param>
public void Configure(IApplicationBuilder appBuilder, HttpsTransportListener listener)
{
appBuilder.Run(context =>
{
Expand All @@ -120,7 +124,7 @@ public void Configure(IApplicationBuilder appBuilder)
return context.Response.WriteAsync(string.Empty);
}

return Listener.SendAsync(context);
return listener.SendAsync(context);
});
}
}
Expand Down Expand Up @@ -272,7 +276,6 @@ public void UpdateChannelLastActiveTime(string globalChannelId)
/// </summary>
public void Start()
{
Startup.Listener = this;
#if NET8_0_OR_GREATER
m_host = new HostBuilder()
.ConfigureWebHostDefaults(ConfigureWebHost)
Expand All @@ -285,6 +288,11 @@ public void Start()
#endif
}

// CA1859: The IWebHostBuilder interface cannot be narrowed to WebHostBuilder here because
// on NET8_0_OR_GREATER this method is called from HostBuilder.ConfigureWebHostDefaults()
// which passes an IWebHostBuilder, not WebHostBuilder.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance",
Justification = "IWebHostBuilder is required for cross-framework compatibility; on NET8_0_OR_GREATER ConfigureWebHostDefaults passes IWebHostBuilder.")]
private void ConfigureWebHost(IWebHostBuilder webHostBuilder)
{
// prepare the server TLS certificate
Expand Down Expand Up @@ -338,6 +346,9 @@ private void ConfigureWebHost(IWebHostBuilder webHostBuilder)
}

webHostBuilder.UseContentRoot(Directory.GetCurrentDirectory());
// Register this listener instance in the web host DI container so it can be
// injected into Startup.Configure as a method parameter — no static state needed.
webHostBuilder.ConfigureServices(services => services.AddSingleton(this));
webHostBuilder.UseStartup<Startup>();
}

Expand Down
1 change: 1 addition & 0 deletions Tests/Opc.Ua.Client.Tests/ClientBatchTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ namespace Opc.Ua.Client.Tests
[SetCulture("en-us")]
[SetUICulture("en-us")]
[TestFixtureSource(nameof(FixtureArgs))]
[Parallelizable(ParallelScope.Fixtures)]
[MemoryDiagnoser]
[DisassemblyDiagnoser]
public class ClientBatchTest : ClientTestFramework
Expand Down
15 changes: 13 additions & 2 deletions Tests/Opc.Ua.Client.Tests/ClientFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ public async Task StartReverseConnectHostAsync()
int serverStartRetries = 25;
do
{
retryStartServer = false;
try
{
var reverseConnectUri = new Uri("opc.tcp://localhost:" + testPort);
Expand All @@ -188,11 +189,13 @@ public async Task StartReverseConnectHostAsync()
{
throw;
}

testPort = UnsecureRandom.Shared.Next(
ServerFixtureUtils.MinTestPort,
ServerFixtureUtils.MaxTestPort);
retryStartServer = true;
}

await Task.Delay(UnsecureRandom.Shared.Next(100, 1000)).ConfigureAwait(false);
} while (retryStartServer);
}
Expand All @@ -219,7 +222,7 @@ public async Task<ISession> ConnectAsync(string endpointUrl, CancellationToken c
nameof(endpointUrl));
}

const int maxAttempts = 5;
const int maxAttempts = 25;
for (int attempt = 0; ; attempt++)
{
try
Expand Down Expand Up @@ -274,7 +277,7 @@ public async Task<ISession> ConnectAsync(
getEndpointsUrl = CoreClientUtils.GetDiscoveryUrl(uri);
}

const int maxAttempts = 5;
const int maxAttempts = 25;
for (int attempt = 0; ; attempt++)
{
try
Expand All @@ -297,6 +300,14 @@ public async Task<ISession> ConnectAsync(
m_logger.LogError(e, "Failed to connect {Attempt}. Retrying in 1 second...", attempt + 1);
await Task.Delay(1000).ConfigureAwait(false);
}
catch (Exception e) when (
e is not IgnoreException &&
attempt < maxAttempts)
{
// Retry on transient errors (e.g. HttpRequestException when HTTPS server is not yet ready)
m_logger.LogError(e, "Failed to connect {Attempt}. Retrying in 1 second...", attempt + 1);
await Task.Delay(1000).ConfigureAwait(false);
}
}

throw new ServiceResultException(StatusCodes.BadNoCommunication);
Expand Down
1 change: 1 addition & 0 deletions Tests/Opc.Ua.Client.Tests/ClientTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ namespace Opc.Ua.Client.Tests
[SetCulture("en-us")]
[SetUICulture("en-us")]
[TestFixtureSource(nameof(FixtureArgs))]
[Parallelizable(ParallelScope.Fixtures)]
[MemoryDiagnoser]
[DisassemblyDiagnoser]
public class ClientTest : ClientTestFramework
Expand Down
96 changes: 96 additions & 0 deletions Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs
Original file line number Diff line number Diff line change
Expand Up @@ -627,5 +627,101 @@ protected void SessionClosing(object sender, EventArgs e)
TestContext.Out.WriteLine("Session_Closing: {0}", session.SessionId);
}
}

/// <summary>
/// Create a set of subscriptions with monitored items on the given session.
/// </summary>
protected async Task CreateSubscriptionsAsync(
ISession session,
Subscription template,
SubscriptionCollection originSubscriptions,
int[] notificationCounters,
int[] fastDataCounters,
int subscriptionCount,
uint queueSize)
{
for (int ii = 0; ii < subscriptionCount; ii++)
{
// create subscription with static monitored items
var subscription = new TestableSubscription(template)
{
PublishingEnabled = true,
Handle = ii,
FastDataChangeCallback = (s, n, _) =>
{
TestContext.Out.WriteLine(
$"FastDataChangeHandlerOrigin: {s.Id}-{n.SequenceNumber}-{n.MonitoredItems.Count}");
fastDataCounters[(int)s.Handle]++;
}
};

subscription.StateChanged += (s, e) =>
TestContext.Out
.WriteLine($"StateChanged: {s.Session.SessionId}-{s.Id}-{e.Status}");

subscription.PublishStatusChanged += (s, e) =>
TestContext.Out.WriteLine(
$"PublishStatusChanged: {s.Session.SessionId}-{s.Id}-{e.Status}");

originSubscriptions.Add(subscription);
session.AddSubscription(subscription);
await subscription.CreateAsync().ConfigureAwait(false);

// set defaults
subscription.DefaultItem.DiscardOldest = true;
subscription.DefaultItem.QueueSize = ii == 0 ? 0U : queueSize;
subscription.DefaultItem.MonitoringMode = MonitoringMode.Reporting;

// create test set using the passed session's namespace URIs
NamespaceTable namespaceUris = session.NamespaceUris;
var testSet = new List<NodeId>();
if (ii == 0)
{
testSet.AddRange(GetTestSetStatic(namespaceUris));
}
else
{
testSet.AddRange(GetTestSetSimulation(namespaceUris));
}

var list = CreateMonitoredItemTestSet(subscription, testSet).ToList();
list.ForEach(i =>
i.Notification += (item, _) =>
{
notificationCounters[(int)subscription.Handle]++;
foreach (DataValue value in item.DequeueValues())
{
TestContext.Out.WriteLine(
"Org:{0}: {1:20}, {2}, {3}, {4}",
subscription.Id,
item.DisplayName,
value.WrappedValue,
value.SourceTimestamp,
value.StatusCode);
}
});
subscription.AddItems(list);
await subscription.ApplyChangesAsync().ConfigureAwait(false);
}
}

/// <summary>
/// Create monitored items from a list of node ids on a subscription.
/// </summary>
protected static List<MonitoredItem> CreateMonitoredItemTestSet(
Subscription subscription,
IList<NodeId> nodeIds)
{
var list = new List<MonitoredItem>();
foreach (NodeId nodeId in nodeIds)
{
var item = new TestableMonitoredItem(subscription.DefaultItem)
{
StartNodeId = nodeId
};
list.Add(item);
}
return list;
}
}
}
1 change: 1 addition & 0 deletions Tests/Opc.Ua.Client.Tests/LoadTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ namespace Opc.Ua.Client.Tests
[SetCulture("en-us")]
[SetUICulture("en-us")]
[TestFixtureSource(nameof(FixtureArgs))]
[Parallelizable(ParallelScope.Fixtures)]
public class LoadTest : ClientTestFramework
{
public LoadTest(string uriScheme)
Expand Down
1 change: 1 addition & 0 deletions Tests/Opc.Ua.Client.Tests/NodeCacheAsyncTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ namespace Opc.Ua.Client.Tests
[SetCulture("en-us")]
[SetUICulture("en-us")]
[TestFixtureSource(nameof(AsyncFixtureArgs))]
[Parallelizable(ParallelScope.Fixtures)]
[MemoryDiagnoser]
[DisassemblyDiagnoser]
public class NodeCacheAsyncTest : ClientTestFramework
Expand Down
Loading
Loading