Files
lmxopcua/scripts/validation/opcua-lifecycle-memory/LifecycleControl.cs
T

209 lines
12 KiB
C#

using System.Diagnostics;
using System.Collections;
using System.Reflection;
using System.Net;
using System.Net.Sockets;
using System.Runtime;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;
var root = Path.GetFullPath(args[0]);
Directory.CreateDirectory(root);
var seconds = int.Parse(args[1]);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(seconds + 120));
var ct = timeout.Token;
var clientPki = Path.Combine(root, "client-pki");
var telemetry = DefaultTelemetry.Create(_ => { });
var config = new ApplicationConfiguration(telemetry);
config.ApplicationName = "NativeLifecycleControl";
config.ApplicationUri = "urn:NativeLifecycleControl:" + Guid.NewGuid();
config.ApplicationType = ApplicationType.Client;
config.SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(clientPki, "own"), SubjectName = "CN=NativeLifecycleControl" },
TrustedIssuerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(clientPki, "issuer") },
TrustedPeerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(clientPki, "trusted") },
RejectedCertificateStore = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(clientPki, "rejected") },
AutoAcceptUntrustedCertificates = true
};
config.ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 15000 };
var app = new ApplicationInstance(telemetry) { ApplicationConfiguration = config };
await config.ValidateAsync(ApplicationType.Client, ct);
await app.CheckApplicationInstanceCertificatesAsync(true, null, ct);
int port;
using (var listener = new TcpListener(IPAddress.Loopback, 0))
{
listener.Start();
port = ((IPEndPoint)listener.LocalEndpoint).Port;
}
var options = new OpcUaApplicationHostOptions { ApplicationName = "NativeLifecycleServer", ApplicationUri = "urn:NativeLifecycleServer:" + Guid.NewGuid(), OpcUaPort = port, PublicHostname = "127.0.0.1", PkiStoreRoot = Path.Combine(root, "server-pki"), AllowAnonymous = false, AutoAcceptUntrustedClientCertificates = true, EnabledSecurityProfiles = [OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt] };
OpcUaApplicationHost host = null!;
OtOpcUaSdkServer server = null!;
async Task Boot()
{
server = new OtOpcUaSdkServer();
host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance, new Auth());
await host.StartAsync(server, ct);
var n = server.NodeManager!;
n.NodeWriteGateway = new Gateway();
n.EnsureFolder("Control", null, "Control", AddressSpaceRealm.Raw);
n.EnsureFolder("Equipment", null, "Equipment", AddressSpaceRealm.Uns);
n.EnsureVariable("Control/Scalar", "Control", "Scalar", "Int32", true, AddressSpaceRealm.Raw);
n.EnsureVariable("Control/Array", "Control", "Array", "Int16", true, AddressSpaceRealm.Raw, isArray: true, arrayLength: 4);
n.EnsureVariable("Equipment/Scalar", "Equipment", "Scalar", "Int32", true, AddressSpaceRealm.Uns);
for (var i = 0; i < 16; i++) n.EnsureVariable("Control/Browse" + i, "Control", "Browse" + i, "Int32", false, AddressSpaceRealm.Raw);
n.MaterialiseAlarmCondition("Control/Alarm", "Control", "Alarm", "OffNormalAlarm", 500, AddressSpaceRealm.Raw, isNative: true);
n.WriteValue("Control/Scalar", 0, 0, DateTime.UtcNow, AddressSpaceRealm.Raw);
n.WriteValue("Control/Array", new short[] { 0, 1, 2, 3 }, 0, DateTime.UtcNow, AddressSpaceRealm.Raw);
n.WriteValue("Equipment/Scalar", 0, 0, DateTime.UtcNow, AddressSpaceRealm.Uns);
}
async Task<ISession> Connect()
{
var t = DefaultTelemetry.Create(_ => { });
var endpoint = await CoreClientUtils.SelectEndpointAsync(config, $"opc.tcp://127.0.0.1:{port}/OtOpcUa", true, t, ct);
var factory = new DefaultSessionFactory(t);
var identity = new UserIdentity("control", Encoding.UTF8.GetBytes(FixtureCredential.Value));
return await factory.CreateAsync(config, new ConfiguredEndpoint(null, endpoint ?? throw new Exception("Missing endpoint"), EndpointConfiguration.Create(config)), false, false, "NativeLifecycleControl", 15000, identity, default, ct);
}
static DataValue Value(object v)
{
#if SDK2
if (!VariantHelper.TryCastFrom(v, out var variant)) throw new Exception("Unsupported control value");
return new DataValue(variant);
#else
return new DataValue(new Variant(v));
#endif
}
static object? Clr(DataValue v)
{
#if SDK2
return v.WrappedValue.AsBoxedObject(Variant.BoxingBehavior.Legacy);
#else
return v.Value;
#endif
}
object? CertificateStats()
{
#if SDK2
return new { created = Opc.Ua.Security.Certificates.Certificate.InstancesCreated, disposed = Opc.Ua.Security.Certificates.Certificate.InstancesDisposed, outstanding = Opc.Ua.Security.Certificates.Certificate.InstancesLeaked };
#else
return null;
#endif
}
void Sample(int cycle, string kind)
{
var live = GC.GetTotalMemory(true);
using var p = Process.GetCurrentProcess();
p.Refresh();
var m = GC.GetGCMemoryInfo();
Console.WriteLine(JsonSerializer.Serialize(new { kind, cycle, pid = Environment.ProcessId, live, rss = p.WorkingSet64, gcCommitted = m.TotalCommittedBytes, gcHeap = m.HeapSizeBytes, fragmented = m.FragmentedBytes, jit = JitInfo.GetCompiledMethodCount(), jitIl = JitInfo.GetCompiledILBytes(), sdk = typeof(NodeId).Assembly.GetName().Version?.ToString(), scope = "matched encrypted public lifecycle subset; no history/revocation/proxy", assemblies = AppDomain.CurrentDomain.GetAssemblies().Length, dynamicAssemblies = AppDomain.CurrentDomain.GetAssemblies().Count(a => a.IsDynamic), certificate = CertificateStats(), timers = Timer.ActiveCount, threads = p.Threads.Count, sessions = server.CurrentInstance.SessionManager.GetSessions().Count, subscriptions = server.CurrentInstance.SubscriptionManager.GetSubscriptions().Count }));
Console.Out.Flush();
}
int cycles = 0, restarts = 0;
await Boot();
var watch = Stopwatch.StartNew();
Sample(0, "start");
try
{
while (watch.Elapsed.TotalSeconds < seconds)
{
using (var owner = await Connect()) using (var other = await Connect())
{
var ns = (ushort)owner.NamespaceUris.GetIndex("https://zb.com/otopcua/raw");
var scalar = new NodeId("Control/Scalar", ns);
var array = new NodeId("Control/Array", ns);
using var subscription = new Subscription(owner.DefaultSubscription) { PublishingInterval = 100 };
owner.AddSubscription(subscription);
await subscription.CreateAsync(ct);
subscription.AddItem(new MonitoredItem(subscription.DefaultItem) { StartNodeId = scalar, AttributeId = Attributes.Value, SamplingInterval = 50 });
subscription.AddItem(new MonitoredItem(subscription.DefaultItem) { StartNodeId = array, AttributeId = Attributes.Value, SamplingInterval = 50 });
await subscription.ApplyChangesAsync(ct);
var response = await owner.WriteAsync(null, [new WriteValue { NodeId = scalar, AttributeId = Attributes.Value, Value = Value(++cycles) }, new WriteValue { NodeId = array, AttributeId = Attributes.Value, Value = Value(new short[] { 1, 2, 3, 4 }) }], ct);
if (response.Results.ToList().Any(s => StatusCode.IsBad(s))) throw new Exception("Control write denied");
if (!Equals(Clr(await owner.ReadValueAsync(scalar, ct)), cycles)) throw new Exception("Scalar changed");
if (!((short[])Clr(await owner.ReadValueAsync(array, ct))!).SequenceEqual(new short[] { 1, 2, 3, 4 })) throw new Exception("Array changed");
var browse = await owner.BrowseAsync(null, null, 1, [new BrowseDescription { NodeId = new NodeId("Control", ns), BrowseDirection = BrowseDirection.Forward, ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, IncludeSubtypes = true, ResultMask = (uint)BrowseResultMask.All }], ct);
await Task.Delay(1000, ct);
await subscription.DeleteAsync(true, ct);
await owner.CloseAsync(ct);
await other.CloseAsync(ct);
}
if (server.CurrentInstance.SessionManager.GetSessions().Count != 0 || server.CurrentInstance.SubscriptionManager.GetSubscriptions().Count != 0) throw new Exception("Control retained sessions/subscriptions");
Sample(cycles, "sample");
if (cycles % 4 == 0)
{
using var parked = await Connect();
using var sub = new Subscription(parked.DefaultSubscription) { PublishingInterval = 100 };
parked.AddSubscription(sub);
await sub.CreateAsync(ct);
sub.AddItem(new MonitoredItem(sub.DefaultItem) { StartNodeId = new NodeId("Control/Scalar", (ushort)parked.NamespaceUris.GetIndex("https://zb.com/otopcua/raw")), AttributeId = Attributes.Value, SamplingInterval = 50 });
await sub.ApplyChangesAsync(ct);
await host.DisposeAsync();
await Boot();
restarts++;
}
}
Console.WriteLine(JsonSerializer.Serialize(new { kind = "complete", cycles, restarts, seconds = watch.Elapsed.TotalSeconds, fullGate = false }));
}
finally
{
await host.DisposeAsync();
#if SDK2
await app.DisposeAsync();
#endif
}
#if SDK2
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
var certificateType = typeof(Opc.Ua.Security.Certificates.Certificate);
var report = new Dictionary<string, object>();
var trackingEnabled = Environment.GetEnvironmentVariable("OPCUA_CERTIFICATE_LEAK_TRACKING") == "1";
if (trackingEnabled)
{
foreach (var name in new[] { "EnumerateLiveCertificates", "EnumerateUnreachableUndisposedCertificates" })
{
var method = certificateType.GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic)
?? throw new InvalidOperationException("SDK2 diagnostic method missing: " + name);
var values = method.Invoke(null, null) as IEnumerable
?? throw new InvalidOperationException("SDK2 diagnostic method return type changed: " + name);
var stacks = new List<string>();
foreach (var value in values)
{
var field = value!.GetType().GetField(name.Contains("Unreachable") ? "Item2" : "Item4")
?? throw new InvalidOperationException("SDK2 diagnostic tuple field missing: " + name);
if (field.FieldType != typeof(string))
throw new InvalidOperationException("SDK2 diagnostic stack field type changed");
// Deliberately omit tuple Item1 (certificate identity).
stacks.Add((string)field.GetValue(value)!);
}
report[name] = new
{
count = stacks.Count,
allocations = stacks.GroupBy(x => x).OrderByDescending(g => g.Count()).Take(15)
.Select(g => new { count = g.Count(), stack = g.Key }).ToArray()
};
}
}
Console.WriteLine(JsonSerializer.Serialize(new { kind = "certificate-final-diagnostics", trackingEnabled, stats = CertificateStats(), report }));
#endif
class Auth : IOpcUaUserAuthenticator
{
public Task<OpcUaUserAuthResult> AuthenticateUserNameAsync(string user, string password, CancellationToken ct) => Task.FromResult(user == "control" && password == FixtureCredential.Value ? OpcUaUserAuthResult.Allow("control", [OpcUaDataPlaneRoles.WriteOperate]) : OpcUaUserAuthResult.Deny("Control credential refused"));
}
class Gateway : IOpcUaNodeWriteGateway
{
public Task<NodeWriteOutcome> WriteAsync(string n, object? v, AddressSpaceRealm r, CancellationToken ct) => Task.FromResult(new NodeWriteOutcome(true, null));
}
static class FixtureCredential
{
internal static string Value { get; } = Environment.GetEnvironmentVariable("OPCUA_MEMORY_PASSWORD") ?? throw new InvalidOperationException("Set OPCUA_MEMORY_PASSWORD for the task-owned fixture.");
}