using System.Globalization; using System.Collections; using System.Diagnostics; using System.Reflection; using Opc.Ua; using Opc.Ua.Server; using Shouldly; internal static class BaselineMetrics { private static FieldInfo Field(object owner, string name) { for (var type = owner.GetType(); type is not null; type = type.BaseType) if (type.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly) is { } member) return member; throw new InvalidOperationException("Missing exact-.182 diagnostic field: " + name); } private static T Required(object owner, string name) => Field(owner, name).GetValue(owner) is T value ? value : throw new InvalidOperationException("Unexpected exact-.182 diagnostic field type: " + name); internal static (int Browse, int History) Points(ISession session) { using (Required(session, "m_lock").EnterScope()) return (Count(session, "m_browseContinuationPoints"), Count(session, "m_historyContinuationPoints")); } private static int Count(object owner, string name) => Field(owner, name).GetValue(owner) switch { null => 0, ICollection values => values.Count, _ => throw new InvalidOperationException("Unexpected continuation collection type") }; internal static int CachedStoreReferences(CertificateValidator validator) { var gate = Required(validator, "m_semaphore"); if (!gate.Wait(TimeSpan.FromSeconds(5))) throw new TimeoutException("Original validator diagnostic lock did not settle"); try { return new[] { "m_trustedCertificateStore", "m_issuerCertificateStore", "m_rejectedCertificateStore" } .Select(name => Field(validator, name).GetValue(validator)) .Count(value => value switch { null => false, CertificateStoreIdentifier store => Field(store, "m_store").GetValue(store) switch { null => false, ICertificateStore => true, _ => throw new InvalidOperationException("Original cached store type changed") }, _ => throw new InvalidOperationException("Original validator store identifier type changed") }); } finally { gate.Release(); } } internal static int Counter(object manager, string name) { var property = manager.GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new InvalidOperationException("Missing original node-manager diagnostic property: " + name); return property.PropertyType == typeof(int) && property.GetValue(manager) is int value ? value : throw new InvalidOperationException("Unexpected original node-manager diagnostic property type: " + name); } internal static async Task<(int Total, int Pki)> DescriptorsAsync(CancellationToken ct, params string[] roots) { ct.ThrowIfCancellationRequested(); var canonicalRoots = roots.Select(root => CanonicalPath(root)).Distinct(StringComparer.Ordinal).ToArray(); if (OperatingSystem.IsLinux()) { var targets = Directory.EnumerateFileSystemEntries("/proc/self/fd") .Select(path => new FileInfo(path).LinkTarget).ToArray(); return (targets.Length, targets.Count(path => IsWithinRoots(path, canonicalRoots))); } if (OperatingSystem.IsMacOS()) { using var process = new Process { StartInfo = new ProcessStartInfo("/usr/sbin/lsof") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false } }; foreach (var arg in new[] { "-a", "-p", Environment.ProcessId.ToString(CultureInfo.InvariantCulture), "-Fnf" }) process.StartInfo.ArgumentList.Add(arg); var text = await ReadDescriptorProbeAsync(process, TimeSpan.FromSeconds(5), ct); process.ExitCode.ShouldBe(0); var total = 0; var pki = 0; var descriptor = false; foreach (var line in text.Split('\n')) { if (line.StartsWith('f')) { descriptor = line.Length > 1 && char.IsDigit(line[1]); if (descriptor) total++; } else if (descriptor && line.StartsWith('n') && IsWithinRoots(line[1..], canonicalRoots)) pki++; } return (total, pki); } throw new PlatformNotSupportedException("This soak requires Linux procfs or macOS lsof for actual descriptor evidence."); } private static async Task ReadDescriptorProbeAsync(Process process, TimeSpan timeout, CancellationToken ct) { using var deadline = CancellationTokenSource.CreateLinkedTokenSource(ct); deadline.CancelAfter(timeout); process.Start(); var stdout = process.StandardOutput.ReadToEndAsync(deadline.Token); var stderr = process.StandardError.ReadToEndAsync(deadline.Token); var exited = process.WaitForExitAsync(deadline.Token); try { // Waiting for exit alone does not bound reads: both redirected pipes must also finish. await Task.WhenAll(stdout, stderr, exited).WaitAsync(deadline.Token).ConfigureAwait(false); return await stdout.ConfigureAwait(false); } catch (OperationCanceledException) { try { if (!process.HasExited) process.Kill(entireProcessTree: true); } catch (InvalidOperationException) when (process.HasExited) { /* It exited between the check and kill. */ } await process.WaitForExitAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false); // Observe both cancelled drains; never leave a pipe read running after returning. try { await Task.WhenAll(stdout, stderr).WaitAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false); } catch (OperationCanceledException) { } if (ct.IsCancellationRequested) throw; throw new TimeoutException("Descriptor probe exceeded its process and redirected-stream deadline."); } } private static bool IsWithinRoots(string? path, IReadOnlyList roots) { if (path is null || !Path.IsPathFullyQualified(path)) return false; // procfs marks an unlinked but still open file; its containing physical directory still counts. const string deleted = " (deleted)"; if (path.EndsWith(deleted, StringComparison.Ordinal)) path = path[..^deleted.Length]; var canonical = CanonicalPath(path); return roots.Any(root => canonical.Equals(root, StringComparison.Ordinal) || canonical.StartsWith(Path.EndsInDirectorySeparator(root) ? root : root + Path.DirectorySeparatorChar, StringComparison.Ordinal)); } private static string CanonicalPath(string path, int depth = 0) { if (depth > 32) throw new IOException("Descriptor path link resolution exceeded its bound."); var full = Path.GetFullPath(path); var current = Path.GetPathRoot(full)!; foreach (var component in full[current.Length..].Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)) { current = Path.Combine(current, component); FileSystemInfo entry = Directory.Exists(current) ? new DirectoryInfo(current) : new FileInfo(current); if (entry.LinkTarget is not null) { var target = entry.ResolveLinkTarget(returnFinalTarget: true) ?? throw new IOException("Descriptor path link resolution returned no target."); // Resolve parent links too: an alias target can itself begin with /var or /tmp. current = CanonicalPath(target.FullName, depth + 1); } } return Path.TrimEndingDirectorySeparator(current); } }