add test for opening and reading from device interface (#54673)
authorAdam Sitnik <adam.sitnik@gmail.com>
Wed, 30 Jun 2021 14:57:51 +0000 (16:57 +0200)
committerGitHub <noreply@github.com>
Wed, 30 Jun 2021 14:57:51 +0000 (16:57 +0200)
src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.Windows.cs
src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj

index 1cfbd35..b6b2be0 100644 (file)
@@ -4,13 +4,13 @@
 using Microsoft.Win32.SafeHandles;
 using System.ComponentModel;
 using System.IO.Pipes;
-using System.Linq;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 using System.Text;
 using System.ServiceProcess;
 using System.Threading.Tasks;
 using Xunit;
+using System.Threading;
 
 namespace System.IO.Tests
 {
@@ -176,4 +176,148 @@ namespace System.IO.Tests
         [DllImport(Interop.Libraries.Netapi32)]
         public static extern int NetShareDel([MarshalAs(UnmanagedType.LPWStr)] string servername, [MarshalAs(UnmanagedType.LPWStr)] string netname, int reserved);
     }
+
+    [PlatformSpecific(TestPlatforms.Windows)] // the test setup is Windows-specifc
+    [OuterLoop("Has a very complex setup logic that in theory might have some side-effects")]
+    [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))]
+    [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
+    public class DeviceInterfaceTests
+    {
+        [Fact]
+        public async Task DeviceInterfaceCanBeOpenedForAsyncIO()
+        {
+            FileStream? fileStream = OpenFirstAvailableDeviceInterface();
+
+            if (fileStream is null)
+            {
+                // it's OK to not have any such devices available
+                // this test is just best effort
+                return;
+            }
+
+            using (fileStream)
+            {
+                Assert.True(fileStream.CanRead);
+                Assert.False(fileStream.CanWrite);
+                Assert.False(fileStream.CanSeek); // #54143
+
+                try
+                {
+                    CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(250));
+
+                    await fileStream.ReadAsync(new byte[4096], cts.Token);
+                }
+                catch (OperationCanceledException)
+                {
+                    // most likely there is no data available and the task is going to get cancelled
+                    // which is fine, we just want to make sure that reading from devices is supported (#54143)
+                }
+            }
+        }
+
+        private static FileStream? OpenFirstAvailableDeviceInterface()
+        {
+            const int DIGCF_PRESENT = 0x2;
+            const int DIGCF_DEVICEINTERFACE = 0x10;
+            const int ERROR_NO_MORE_ITEMS = 259;
+
+            HidD_GetHidGuid(out Guid HidGuid);
+            IntPtr deviceInfoSet = SetupDiGetClassDevs(in HidGuid, IntPtr.Zero, IntPtr.Zero, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
+
+            try
+            {
+                SP_DEVINFO_DATA deviceInfoData = new SP_DEVINFO_DATA();
+                deviceInfoData.cbSize = (uint)Marshal.SizeOf(deviceInfoData);
+
+                uint deviceIndex = 0;
+                while (SetupDiEnumDeviceInfo(deviceInfoSet, deviceIndex++, ref deviceInfoData))
+                {
+                    if (Marshal.GetLastWin32Error() == ERROR_NO_MORE_ITEMS)
+                    {
+                        break;
+                    }
+
+                    SP_DEVICE_INTERFACE_DATA deviceInterfaceData = new SP_DEVICE_INTERFACE_DATA();
+                    deviceInterfaceData.cbSize = Marshal.SizeOf(deviceInterfaceData);
+
+                    if (!SetupDiEnumDeviceInterfaces(deviceInfoSet, IntPtr.Zero, in HidGuid, deviceIndex, ref deviceInterfaceData))
+                    {
+                        continue;
+                    }
+
+                    SP_DEVICE_INTERFACE_DETAIL_DATA deviceInterfaceDetailData = new SP_DEVICE_INTERFACE_DETAIL_DATA();
+                    deviceInterfaceDetailData.cbSize = IntPtr.Size == 8 ? 8 : 6;
+
+                    uint size = (uint)Marshal.SizeOf(deviceInterfaceDetailData);
+
+                    if (!SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref deviceInterfaceData, ref deviceInterfaceDetailData, size, ref size, IntPtr.Zero))
+                    {
+                        continue;
+                    }
+
+                    string devicePath = deviceInterfaceDetailData.DevicePath;
+                    Assert.StartsWith(@"\\?\hid", devicePath);
+
+                    try
+                    {
+                        return new FileStream(devicePath, FileMode.Open, FileAccess.Read, FileShare.Read, 0, FileOptions.Asynchronous);
+                    }
+                    catch (IOException)
+                    {
+                        continue; // device has been locked by another process
+                    }
+                }
+            }
+            finally
+            {
+                SetupDiDestroyDeviceInfoList(deviceInfoSet);
+            }
+
+            return null;
+        }
+
+        [StructLayout(LayoutKind.Sequential)]
+        struct SP_DEVICE_INTERFACE_DATA
+        {
+            public int cbSize;
+            public Guid interfaceClassGuid;
+            public int flags;
+            private nuint reserved;
+        }
+
+        [StructLayout(LayoutKind.Sequential)]
+        struct SP_DEVINFO_DATA
+        {
+            public uint cbSize;
+            public Guid ClassGuid;
+            public uint DevInst;
+            public nint Reserved;
+        }
+
+        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+        struct SP_DEVICE_INTERFACE_DETAIL_DATA
+        {
+            public int cbSize;
+            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] // 256 should be always enough for device interface path
+            public string DevicePath;
+        }
+
+        [DllImport("hid.dll", SetLastError = true)]
+        static extern void HidD_GetHidGuid(out Guid HidGuid);
+
+        [DllImport("setupapi.dll", SetLastError = true)]
+        static extern IntPtr SetupDiGetClassDevs(in Guid ClassGuid, IntPtr Enumerator, IntPtr hwndParent, int Flags);
+
+        [DllImport("setupapi.dll", SetLastError = true)]
+        static extern bool SetupDiEnumDeviceInfo(IntPtr DeviceInfoSet, uint MemberIndex, ref SP_DEVINFO_DATA DeviceInfoData);
+
+        [DllImport("setupapi.dll", SetLastError = true)]
+        static extern bool SetupDiEnumDeviceInterfaces(IntPtr DeviceInfoSet, IntPtr DeviceInfoData, in Guid InterfaceClassGuid, uint MemberIndex, ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData);
+
+        [DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+        static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr DeviceInfoSet, ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData, ref SP_DEVICE_INTERFACE_DETAIL_DATA DeviceInterfaceDetailData, uint DeviceInterfaceDetailDataSize, ref uint RequiredSize, IntPtr DeviceInfoData);
+
+        [DllImport("setupapi.dll", SetLastError = true)]
+        static extern bool SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet);
+    }
 }
index 04d5762..e9ab0a7 100644 (file)
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <IncludeRemoteExecutor>true</IncludeRemoteExecutor>
   <ItemGroup Condition="'$(TargetsWindows)' == 'true'">
     <Compile Remove="..\**\*.Unix.cs" />
     <Compile Remove="..\**\*.Browser.cs" />
+    <!-- .NET 5 did not support async file IO for device interfaces -->
+    <Compile Remove="..\FileStream\FileStreamConformanceTests.Windows.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Interop.BOOL.cs" Link="Common\Interop\Windows\Interop.BOOL.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Interop.Libraries.cs" Link="Common\Interop\Windows\Interop.Libraries.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.FILE_STANDARD_INFO.cs" Link="Common\Interop\Windows\Interop.FILE_STANDARD_INFO.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetFileInformationByHandleEx.cs" Link="Common\Interop\Windows\Interop.GetFileInformationByHandleEx.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.MemOptions.cs" Link="Common\Interop\Windows\Interop.MemOptions.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.VirtualAlloc_Ptr.cs" Link="Common\Interop\Windows\Interop.VirtualAlloc_Ptr.cs" />
-    <ProjectReference Include="$(LibrariesProjectRoot)System.ServiceProcess.ServiceController\src\System.ServiceProcess.ServiceController.csproj" />
   </ItemGroup>
   <ItemGroup Condition="'$(TargetsBrowser)' == 'true'">
     <Compile Remove="..\**\*.Unix.cs" />