using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Microsoft.Win32.SafeHandles;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
///
/// Windows port of the soak's descriptor observer (lmxopcua#799 acceptance). Windows has no fd table: the process
/// handle table is the equivalent kernel-resource table, so Total is every handle this process owns (the
/// whole-process figure, like Linux /proc/self/fd) and Pki is the open disk-file/directory handles whose
/// final path lies under a PKI root. Paths come from GetFinalPathNameByHandle (symlinks, junctions, 8.3 names
/// and casing resolved, the Windows analog of the macOS /var→/private/var canonicalisation), and the roots are
/// canonicalised the same way, compared case-insensitively as NTFS does. The probe is in-process (no child process
/// like macOS lsof) and runs on a dedicated thread under the same 5-second bound as the lsof probe: a name query that
/// blocks is a timeout, never a favourable zero.
///
public sealed partial class Sdk2LifecycleSoakTests
{
[SupportedOSPlatform("windows")]
private static Task<(int Total, int Pki)> WindowsDescriptorCountsAsync(CancellationToken ct, IReadOnlyList 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 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();
int total = 0, pki = 0;
var path = new char[32768];
for (long i = 0; i < count; i++)
{
var entry = Marshal.PtrToStructure(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 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 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);
}