2e9cccbdef
v2-ci / unit-tests (push) Has been cancelled
Copies the reviewed baseline-canonical SDK1 port and replaces its 1..4-cycle smoke entry with the late-slope
loop (same cycle body, every-fourth-cycle active restart, 30/25/120 s bounds, 7200 s phase, cycle-3 cold
baseline with unchanged +64 MiB managed / +128 MiB RSS / +32 descriptor caps, latched first rejection,
bounded shutdown checks). Run against the immutable efb9197a publication (#807 fix included), never rebuilt.
The held-reference closed-session list assertion is recorded instead (SDK1 Close keeps lists until Dispose,
per OWNERSHIP.md) and the managed-monotonic guard is latched, so the comparator yields numbers for the whole
phase. acceptance=false by construction. Windows descriptor observer shared byte-for-byte with the SDK2 test.
Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
133 lines
6.3 KiB
C#
133 lines
6.3 KiB
C#
using System.ComponentModel;
|
|
using System.Runtime.InteropServices;
|
|
using System.Runtime.Versioning;
|
|
using Microsoft.Win32.SafeHandles;
|
|
|
|
/// <summary>Byte-for-byte the Windows descriptor observer of Sdk2LifecycleWindowsDescriptors.cs (lmxopcua#799), hosted for the SDK1 comparator.</summary>
|
|
internal static class WindowsDescriptors
|
|
{
|
|
[SupportedOSPlatform("windows")]
|
|
internal static Task<(int Total, int Pki)> CountAsync(CancellationToken ct, IReadOnlyList<string> roots)
|
|
{
|
|
var result = new TaskCompletionSource<(int Total, int Pki)>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var thread = new Thread(() =>
|
|
{
|
|
try { result.TrySetResult(WindowsHandleCounts(roots)); }
|
|
catch (Exception error) { result.TrySetException(error); }
|
|
}) { IsBackground = true, Name = "soak-descriptor-probe" };
|
|
thread.Start();
|
|
return BoundAsync();
|
|
|
|
async Task<(int Total, int Pki)> BoundAsync()
|
|
{
|
|
try { return await result.Task.WaitAsync(TimeSpan.FromSeconds(5), ct).ConfigureAwait(false); }
|
|
catch (TimeoutException) { throw new TimeoutException("Descriptor probe exceeded its handle-enumeration deadline."); }
|
|
}
|
|
}
|
|
|
|
[SupportedOSPlatform("windows")]
|
|
private static (int Total, int Pki) WindowsHandleCounts(IReadOnlyList<string> roots)
|
|
{
|
|
// Canonicalise the roots first and close those directory handles before the snapshot, so they are not counted.
|
|
var canonicalRoots = roots.Select(WindowsCanonicalRoot).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
|
var self = (nuint)Environment.ProcessId;
|
|
var length = 1 << 20;
|
|
while (true)
|
|
{
|
|
var buffer = Marshal.AllocHGlobal(length);
|
|
try
|
|
{
|
|
var status = NtQuerySystemInformation(SystemExtendedHandleInformation, buffer, length, out var needed);
|
|
if (status == StatusInfoLengthMismatch)
|
|
{
|
|
length = Math.Max(length * 2, needed + (1 << 16));
|
|
continue;
|
|
}
|
|
if (status != 0) throw new Win32Exception(RtlNtStatusToDosError(status), $"NtQuerySystemInformation failed (0x{status:X8}).");
|
|
var count = (long)Marshal.ReadIntPtr(buffer);
|
|
var entries = buffer + 2 * IntPtr.Size;
|
|
var entrySize = Marshal.SizeOf<HandleEntry>();
|
|
int total = 0, pki = 0;
|
|
var path = new char[32768];
|
|
for (long i = 0; i < count; i++)
|
|
{
|
|
var entry = Marshal.PtrToStructure<HandleEntry>(entries + (nint)(i * entrySize));
|
|
if (entry.UniqueProcessId != self) continue;
|
|
total++;
|
|
var handle = (nint)entry.HandleValue;
|
|
// GetFileType is answered by the I/O manager without the file-object lock; only disk files and
|
|
// directories go on to the name query (pipes, sockets and consoles cannot hold PKI material).
|
|
if (GetFileType(handle) != FileTypeDisk) continue;
|
|
var name = FinalPath(handle, path);
|
|
if (name is not null && WithinWindowsRoots(name, canonicalRoots)) pki++;
|
|
}
|
|
return (total, pki);
|
|
}
|
|
finally { Marshal.FreeHGlobal(buffer); }
|
|
}
|
|
}
|
|
|
|
[SupportedOSPlatform("windows")]
|
|
private static string WindowsCanonicalRoot(string root)
|
|
{
|
|
var full = Path.TrimEndingDirectorySeparator(Path.GetFullPath(root));
|
|
if (!Directory.Exists(full)) return BaselineMetrics.CanonicalPath(full);
|
|
using var directory = CreateFileW(full, 0, FileShareAll, 0, OpenExisting, FileFlagBackupSemantics, 0);
|
|
if (directory.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error(), "Cannot open a PKI root for canonicalisation.");
|
|
return FinalPath(directory.DangerousGetHandle(), new char[32768])
|
|
?? throw new IOException("A PKI root has no final path.");
|
|
}
|
|
|
|
private static string? FinalPath(nint handle, char[] buffer)
|
|
{
|
|
var written = GetFinalPathNameByHandleW(handle, buffer, buffer.Length, 0);
|
|
if (written == 0 || written >= buffer.Length) return null; // Closed/reused between snapshot and query, or not a named file.
|
|
var name = new string(buffer, 0, (int)written);
|
|
if (name.StartsWith(@"\\?\UNC\", StringComparison.Ordinal)) return @"\\" + name[8..];
|
|
return name.StartsWith(@"\\?\", StringComparison.Ordinal) ? name[4..] : name;
|
|
}
|
|
|
|
private static bool WithinWindowsRoots(string path, IReadOnlyList<string> roots)
|
|
{
|
|
path = Path.TrimEndingDirectorySeparator(path);
|
|
return roots.Any(root => path.Equals(root, StringComparison.OrdinalIgnoreCase)
|
|
|| path.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
private const int SystemExtendedHandleInformation = 64;
|
|
private const int StatusInfoLengthMismatch = unchecked((int)0xC0000004);
|
|
private const uint FileTypeDisk = 1;
|
|
private const uint FileShareAll = 7;
|
|
private const uint OpenExisting = 3;
|
|
private const uint FileFlagBackupSemantics = 0x02000000;
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct HandleEntry
|
|
{
|
|
public nint Object;
|
|
public nuint UniqueProcessId;
|
|
public nuint HandleValue;
|
|
public uint GrantedAccess;
|
|
public ushort CreatorBackTraceIndex;
|
|
public ushort ObjectTypeIndex;
|
|
public uint HandleAttributes;
|
|
public uint Reserved;
|
|
}
|
|
|
|
[DllImport("ntdll.dll")]
|
|
private static extern int NtQuerySystemInformation(int informationClass, nint information, int length, out int returnLength);
|
|
|
|
[DllImport("ntdll.dll")]
|
|
private static extern int RtlNtStatusToDosError(int status);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern uint GetFileType(nint handle);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
private static extern uint GetFinalPathNameByHandleW(nint handle, [Out] char[] path, int length, uint flags);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
private static extern SafeFileHandle CreateFileW(string name, uint access, uint share, nint security, uint disposition,
|
|
uint flags, nint template);
|
|
}
|