Skip to content

Commit

Permalink
add test for opening and reading from device interface (dotnet#54673)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamsitnik authored Jun 30, 2021
1 parent 5b58501 commit 38df267
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -176,4 +176,148 @@ public struct SHARE_INFO_502
[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);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
Expand All @@ -19,13 +19,14 @@
<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" />
Expand Down

0 comments on commit 38df267

Please sign in to comment.