mirror of
https://github.com/dorssel/usbipd-win.git
synced 2026-11-10 14:43:56 +08:00
572 lines
20 KiB
C#
572 lines
20 KiB
C#
// SPDX-FileCopyrightText: 2025 Frans van Dorsselaer
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
using System.Globalization;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using Usbipd.Automation;
|
|
using Windows.Win32;
|
|
using Windows.Win32.Devices.DeviceAndDriverInstallation;
|
|
using Windows.Win32.Devices.Properties;
|
|
using Windows.Win32.Foundation;
|
|
|
|
namespace Usbipd;
|
|
|
|
/// <summary>
|
|
/// Represents a device as seen by Windows.
|
|
/// <para>
|
|
/// Each device has a numeric ID (device node) and an instance ID (string), which have a one-to-one correspondence.
|
|
/// </para>
|
|
/// </summary>
|
|
sealed partial class WindowsDevice : IEquatable<WindowsDevice>
|
|
{
|
|
public uint Node { get; }
|
|
// Instance IDs are case-insensitive. We always use uppercase to prevent confusion and comparison issues.
|
|
public string InstanceId { get; }
|
|
|
|
WindowsDevice(uint deviceNode, string instanceId)
|
|
{
|
|
Node = deviceNode;
|
|
InstanceId = instanceId.ToUpperInvariant();
|
|
}
|
|
|
|
#region IEquatable
|
|
public override int GetHashCode()
|
|
{
|
|
return InstanceId.GetHashCode(StringComparison.Ordinal);
|
|
}
|
|
|
|
public override bool Equals(object? obj)
|
|
{
|
|
return Equals(obj as WindowsDevice);
|
|
}
|
|
|
|
public bool Equals(WindowsDevice? other)
|
|
{
|
|
return other is not null && InstanceId == other.InstanceId;
|
|
}
|
|
#endregion
|
|
|
|
/// <returns>false if the device does not exist (neither present nor phantom).</returns>
|
|
public static bool TryCreate(string instanceId, out WindowsDevice device)
|
|
{
|
|
uint deviceNode;
|
|
device = default!;
|
|
unsafe // DevSkim: ignore DS172412
|
|
{
|
|
fixed (char* pInstanceId = instanceId)
|
|
{
|
|
if (PInvoke.CM_Locate_DevNode(out deviceNode, pInstanceId, CM_LOCATE_DEVNODE_FLAGS.CM_LOCATE_DEVNODE_PHANTOM) != CONFIGRET.CR_SUCCESS)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
device = new(deviceNode, instanceId);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// true if the device is a VBoxUSBMon stub device.
|
|
/// </summary>
|
|
public bool IsStub => VidPid.TryParseId(InstanceId, out var vidPid) && (vidPid == DriverDetails.Instance.VidPid);
|
|
|
|
/// <summary>
|
|
/// true if the device is a USB hub.
|
|
/// </summary>
|
|
public bool IsHub
|
|
{
|
|
get
|
|
{
|
|
uint length;
|
|
unsafe // DevSkim: ignore DS172412
|
|
{
|
|
fixed (char* pInstanceId = InstanceId)
|
|
{
|
|
if (PInvoke.CM_Get_Device_Interface_List_Size(out length, PInvoke.GUID_DEVINTERFACE_USB_HUB, pInstanceId,
|
|
CM_GET_DEVICE_INTERFACE_LIST_FLAGS.CM_GET_DEVICE_INTERFACE_LIST_ALL_DEVICES) != CONFIGRET.CR_SUCCESS)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
// A non-empty list (i.e., for a hub) would be double-NUL terminated.
|
|
return length >= 2;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// true if the device is currently present (plugged in).
|
|
/// </summary>
|
|
public bool IsPresent => TryGetProperty(Node, PInvoke.DEVPKEY_Device_IsPresent, out bool isPresent) && isPresent;
|
|
|
|
/// <summary>
|
|
/// true if the device is currently present (plugged in), but disabled in Device Manager.
|
|
/// </summary>
|
|
public bool IsDisabled => IsPresent
|
|
&& TryGetProperty(Node, PInvoke.DEVPKEY_Device_DevNodeStatus, out CM_DEVNODE_STATUS_FLAGS status)
|
|
&& status.HasFlag(CM_DEVNODE_STATUS_FLAGS.DN_HAS_PROBLEM)
|
|
&& TryGetProperty(Node, PInvoke.DEVPKEY_Device_ProblemCode, out CM_PROB problem)
|
|
&& problem == CM_PROB.CM_PROB_DISABLED;
|
|
|
|
public const string UnknownDescription = "Unknown device";
|
|
|
|
/// <returns>An amalgamated "best" description of the device.</returns>
|
|
public string Description
|
|
{
|
|
get
|
|
{
|
|
var isCompositeDevice = TryGetProperty(Node, PInvoke.DEVPKEY_Device_CompatibleIds, out string[] compatibleIds)
|
|
&& compatibleIds.Contains(@"USB\COMPOSITE");
|
|
if (!isCompositeDevice)
|
|
{
|
|
// NAME is FriendlyName (if it exists), or else it is Description (if it exists).
|
|
if (!TryGetProperty(Node, PInvoke.DEVPKEY_NAME, out string name))
|
|
{
|
|
// This can happen for devices without a driver *and* without a USB string table.
|
|
return UnknownDescription;
|
|
}
|
|
name = name.Trim();
|
|
return string.IsNullOrEmpty(name) ? UnknownDescription : name;
|
|
}
|
|
|
|
// For USB\COMPOSITE we need to add the descriptions of the direct children.
|
|
// We remove duplicates, often something like "HID Device", but we also want to preserve the order.
|
|
var descriptionSet = new SortedSet<string>();
|
|
var descriptionList = new List<string>();
|
|
|
|
void AddDescription(string desc)
|
|
{
|
|
desc = desc.Trim();
|
|
if (!string.IsNullOrEmpty(desc) && descriptionSet.Add(desc))
|
|
{
|
|
descriptionList.Add(desc);
|
|
}
|
|
}
|
|
|
|
// The FriendlyName (if it exists) is useful for USB\COMPOSITE.
|
|
// Description definitely is not (for composite devices).
|
|
if (TryGetProperty(Node, PInvoke.DEVPKEY_Device_FriendlyName, out string friendlyName))
|
|
{
|
|
AddDescription(friendlyName);
|
|
}
|
|
|
|
foreach (var childDevice in Children)
|
|
{
|
|
// NAME is FriendlyName (if it exists), or else it is Description (if it exists).
|
|
if (TryGetProperty(childDevice.Node, PInvoke.DEVPKEY_NAME, out string childName))
|
|
{
|
|
AddDescription(childName);
|
|
}
|
|
}
|
|
|
|
if (descriptionList.Count == 0)
|
|
{
|
|
// This can happen if the USB\COMPOSITE device does not have a FriendlyName and the
|
|
// bus has not yet enumerated the children; for example, right after releasing the device
|
|
// back to Windows. Just fall back to the non-descriptive name for USB\COMPOSITE.
|
|
if (!TryGetProperty(Node, PInvoke.DEVPKEY_NAME, out string name))
|
|
{
|
|
// This can happen for devices without a driver *and* without a USB string table.
|
|
return UnknownDescription;
|
|
}
|
|
name = name.Trim();
|
|
return string.IsNullOrEmpty(name) ? UnknownDescription : name;
|
|
}
|
|
else
|
|
{
|
|
return string.Join(", ", descriptionList);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <returns>true on success.</returns>
|
|
public bool SetFriendlyName()
|
|
{
|
|
// NOTE: Must include terminating NUL character.
|
|
return PInvoke.CM_Set_DevNode_Property(Node, PInvoke.DEVPKEY_Device_FriendlyName, DEVPROPTYPE.DEVPROP_TYPE_STRING,
|
|
Encoding.Unicode.GetBytes("USBIP Shared Device\0"), 0) == CONFIGRET.CR_SUCCESS;
|
|
}
|
|
|
|
[GeneratedRegex(@"^Port_#([0-9]{4}).Hub_#([0-9]{4})$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
|
private static partial Regex LocationInfoRegex();
|
|
|
|
/// <summary>
|
|
/// The USB bus ID of the device, or <see cref="BusId.IncompatibleHub"/> for non-USB devices, hubs, phantom devices, etc.
|
|
/// <para>
|
|
/// NOTE: This can have a valid value even if the device is currently not present.
|
|
/// </para>
|
|
/// </summary>
|
|
public BusId BusId
|
|
{
|
|
get
|
|
{
|
|
if (!TryGetProperty(Node, PInvoke.DEVPKEY_Device_LocationInfo, out string locationInfo))
|
|
{
|
|
return BusId.IncompatibleHub;
|
|
}
|
|
var match = LocationInfoRegex().Match(locationInfo);
|
|
if (!match.Success)
|
|
{
|
|
// This is probably a device on an unsupported hub-type.
|
|
// See for example https://github.com/dorssel/usbipd-win/issues/809.
|
|
return BusId.IncompatibleHub;
|
|
}
|
|
var bus = ushort.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture);
|
|
var port = ushort.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
|
|
return bus == 0 || port == 0 ? BusId.IncompatibleHub : new(bus, port);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// true if the device is using the VBoxUSB driver (i.e., either a stub device, or a forced-bound device).
|
|
/// </summary>
|
|
public bool HasVBoxDriver => TryGetProperty(Node, PInvoke.DEVPKEY_Device_MatchingDeviceId, out string matchingDeviceId)
|
|
&& VidPid.TryParseId(matchingDeviceId, out var vidPid) && vidPid == DriverDetails.Instance.VidPid;
|
|
|
|
/// <summary>
|
|
/// The current driver version.
|
|
/// <para>
|
|
/// NOTE: This is only interesting if HasVBoxDriver is true.
|
|
/// </para>
|
|
/// </summary>
|
|
public Version DriverVersion => (TryGetProperty(Node, PInvoke.DEVPKEY_Device_DriverVersion, out string versionText)
|
|
&& Version.TryParse(versionText, out var version)) ? version : new();
|
|
|
|
public IEnumerable<WindowsDevice> Children
|
|
{
|
|
get
|
|
{
|
|
if (!TryGetProperty(Node, PInvoke.DEVPKEY_Device_Children, out string[] childInstanceIds))
|
|
{
|
|
yield break;
|
|
}
|
|
foreach (var childInstanceId in childInstanceIds)
|
|
{
|
|
if (TryCreate(childInstanceId, out var childDevice))
|
|
{
|
|
yield return childDevice;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static DeviceFile OpenInterface(string instanceId, Guid interfaceClassGuid)
|
|
{
|
|
uint bufferSize;
|
|
unsafe // DevSkim: ignore DS172412
|
|
{
|
|
fixed (char* pInstanceId = instanceId)
|
|
{
|
|
if (PInvoke.CM_Get_Device_Interface_List_Size(out bufferSize, interfaceClassGuid, pInstanceId,
|
|
CM_GET_DEVICE_INTERFACE_LIST_FLAGS.CM_GET_DEVICE_INTERFACE_LIST_PRESENT) != CONFIGRET.CR_SUCCESS)
|
|
{
|
|
throw new FileNotFoundException();
|
|
}
|
|
}
|
|
}
|
|
if (bufferSize <= 1)
|
|
{
|
|
throw new FileNotFoundException();
|
|
}
|
|
var buffer = new char[checked((int)bufferSize)];
|
|
unsafe // DevSkim: ignore DS172412
|
|
{
|
|
fixed (char* pInstanceId = instanceId)
|
|
fixed (char* pBuffer = buffer)
|
|
{
|
|
if (PInvoke.CM_Get_Device_Interface_List(interfaceClassGuid, pInstanceId, pBuffer, bufferSize,
|
|
CM_GET_DEVICE_INTERFACE_LIST_FLAGS.CM_GET_DEVICE_INTERFACE_LIST_PRESENT) != CONFIGRET.CR_SUCCESS)
|
|
{
|
|
throw new FileNotFoundException();
|
|
}
|
|
}
|
|
}
|
|
// The "list" contains one entry and is double-NUL terminated.
|
|
var interfacePath = new string(buffer.AsSpan()[0..^2]);
|
|
return new(interfacePath);
|
|
}
|
|
|
|
public DeviceFile OpenVBoxInterface()
|
|
{
|
|
return OpenInterface(InstanceId, Interop.VBoxUsb.GUID_CLASS_VBOXUSB);
|
|
}
|
|
|
|
public DeviceFile OpenHubInterface()
|
|
{
|
|
return TryGetProperty(Node, PInvoke.DEVPKEY_Device_Parent, out string hubInstanceId)
|
|
&& TryCreate(hubInstanceId, out var hubDevice)
|
|
? OpenInterface(hubDevice.InstanceId, PInvoke.GUID_DEVINTERFACE_USB_HUB)
|
|
: throw new FileNotFoundException();
|
|
}
|
|
|
|
static bool TryGetProperty(uint deviceNode, in DEVPROPKEY devPropKey, out byte[] value, out DEVPROPTYPE propertyType)
|
|
{
|
|
var bufferSize = 0u;
|
|
if (PInvoke.CM_Get_DevNode_Property(deviceNode, devPropKey, out _, null, ref bufferSize, 0) != CONFIGRET.CR_BUFFER_SMALL)
|
|
{
|
|
value = default!;
|
|
propertyType = DEVPROPTYPE.DEVPROP_TYPE_EMPTY;
|
|
return false;
|
|
}
|
|
var buffer = new byte[checked((int)bufferSize)];
|
|
var bufferSizeConfirm = bufferSize;
|
|
if (PInvoke.CM_Get_DevNode_Property(deviceNode, devPropKey, out propertyType, buffer, ref bufferSizeConfirm, 0) != CONFIGRET.CR_SUCCESS)
|
|
{
|
|
value = default!;
|
|
propertyType = DEVPROPTYPE.DEVPROP_TYPE_EMPTY;
|
|
return false;
|
|
}
|
|
if (bufferSizeConfirm != bufferSize)
|
|
{
|
|
value = default!;
|
|
propertyType = DEVPROPTYPE.DEVPROP_TYPE_EMPTY;
|
|
return false;
|
|
}
|
|
value = buffer;
|
|
return true;
|
|
}
|
|
|
|
static bool TryGetProperty(uint deviceNode, in DEVPROPKEY devPropKey, out string value)
|
|
{
|
|
if (!TryGetProperty(deviceNode, devPropKey, out var buffer, out var propertyType))
|
|
{
|
|
value = default!;
|
|
return false;
|
|
}
|
|
|
|
// Must be a UTF-16 string.
|
|
if ((propertyType != DEVPROPTYPE.DEVPROP_TYPE_STRING) || (buffer.Length % sizeof(char) != 0))
|
|
{
|
|
value = default!;
|
|
return false;
|
|
}
|
|
var charBuffer = MemoryMarshal.Cast<byte, char>(buffer);
|
|
|
|
// Must be NUL terminated.
|
|
if (charBuffer.IsEmpty || (charBuffer[^1] != '\0'))
|
|
{
|
|
value = default!;
|
|
return false;
|
|
}
|
|
|
|
value = new(charBuffer[..^1]);
|
|
return true;
|
|
}
|
|
|
|
static bool TryGetProperty(uint deviceNode, in DEVPROPKEY devPropKey, out string[] value)
|
|
{
|
|
if (!TryGetProperty(deviceNode, devPropKey, out var buffer, out var propertyType))
|
|
{
|
|
value = default!;
|
|
return false;
|
|
}
|
|
|
|
// Must be a UTF-16 string list.
|
|
if ((propertyType != DEVPROPTYPE.DEVPROP_TYPE_STRING_LIST) || (buffer.Length % sizeof(char) != 0))
|
|
{
|
|
value = default!;
|
|
return false;
|
|
}
|
|
var charBuffer = MemoryMarshal.Cast<byte, char>(buffer);
|
|
|
|
// Must be NUL terminated.
|
|
if (charBuffer.IsEmpty || (charBuffer[^1] != '\0'))
|
|
{
|
|
value = default!;
|
|
return false;
|
|
}
|
|
|
|
if (charBuffer.Length == 1)
|
|
{
|
|
// Empty list.
|
|
value = [];
|
|
return true;
|
|
}
|
|
|
|
// Non-empty list must be double-NUL terminated.
|
|
if (charBuffer[^2] != '\0')
|
|
{
|
|
value = default!;
|
|
return false;
|
|
}
|
|
|
|
value = new string(charBuffer[..^2]).Split('\0');
|
|
return true;
|
|
}
|
|
|
|
static bool TryGetProperty(uint deviceNode, in DEVPROPKEY devPropKey, out uint value)
|
|
{
|
|
if (!TryGetProperty(deviceNode, devPropKey, out var buffer, out var propertyType))
|
|
{
|
|
value = default!;
|
|
return false;
|
|
}
|
|
|
|
if ((propertyType != DEVPROPTYPE.DEVPROP_TYPE_UINT32) || (buffer.Length != sizeof(uint)))
|
|
{
|
|
value = default!;
|
|
return false;
|
|
}
|
|
|
|
value = MemoryMarshal.Read<uint>(buffer);
|
|
return true;
|
|
}
|
|
|
|
static bool TryGetProperty(uint deviceNode, in DEVPROPKEY devPropKey, out bool value)
|
|
{
|
|
if (!TryGetProperty(deviceNode, devPropKey, out var buffer, out var propertyType))
|
|
{
|
|
value = default!;
|
|
return false;
|
|
}
|
|
|
|
if ((propertyType != DEVPROPTYPE.DEVPROP_TYPE_BOOLEAN) || (buffer.Length != Unsafe.SizeOf<DEVPROP_BOOLEAN>()))
|
|
{
|
|
value = default!;
|
|
return false;
|
|
}
|
|
|
|
var boolean = (DEVPROP_BOOLEAN)buffer[0];
|
|
if (boolean == DEVPROP_BOOLEAN.DEVPROP_TRUE)
|
|
{
|
|
value = true;
|
|
return true;
|
|
}
|
|
else if (boolean == DEVPROP_BOOLEAN.DEVPROP_FALSE)
|
|
{
|
|
value = false;
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
value = default!;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool TryGetProperty<T>(uint deviceNode, in DEVPROPKEY devPropKey, out T value) where T : unmanaged, Enum
|
|
{
|
|
if (!TryGetProperty(deviceNode, devPropKey, out uint tmpValue))
|
|
{
|
|
value = default;
|
|
return false;
|
|
}
|
|
value = (T)Enum.ToObject(typeof(T), tmpValue);
|
|
return true;
|
|
}
|
|
|
|
/// <returns>All devices, optionally filtered on installer class GUID and/or presence.</returns>
|
|
public static IEnumerable<WindowsDevice> GetAll(Guid? classGuid, bool presentOnly)
|
|
{
|
|
var filter = classGuid?.ToString("B");
|
|
uint flags = 0;
|
|
if (classGuid is not null)
|
|
{
|
|
flags |= PInvoke.CM_GETIDLIST_FILTER_CLASS;
|
|
}
|
|
if (presentOnly)
|
|
{
|
|
flags |= PInvoke.CM_GETIDLIST_FILTER_PRESENT;
|
|
}
|
|
|
|
if (PInvoke.CM_Get_Device_ID_List_Size(out var bufferSize, filter, flags) != CONFIGRET.CR_SUCCESS)
|
|
{
|
|
yield break;
|
|
}
|
|
if (bufferSize <= 1)
|
|
{
|
|
// Empty list.
|
|
yield break;
|
|
}
|
|
var buffer = new char[checked((int)bufferSize)];
|
|
unsafe // DevSkim: ignore DS172412
|
|
{
|
|
fixed (char* pBuffer = buffer)
|
|
{
|
|
if (PInvoke.CM_Get_Device_ID_List(filter, pBuffer, bufferSize, flags) != CONFIGRET.CR_SUCCESS)
|
|
{
|
|
yield break;
|
|
}
|
|
}
|
|
}
|
|
// The list is double-NUL terminated.
|
|
var instanceIds = new string(buffer.AsSpan()[0..^2]).Split('\0');
|
|
foreach (var instanceId in instanceIds)
|
|
{
|
|
if (TryCreate(instanceId, out var device))
|
|
{
|
|
yield return device;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <returns>All present devices supporting a specific interface class.</returns>
|
|
public static IEnumerable<WindowsDevice> GetAll(Guid interfaceClassGuid)
|
|
{
|
|
string[] interfacePaths;
|
|
{
|
|
if (PInvoke.CM_Get_Device_Interface_List_Size(out var bufferSize, interfaceClassGuid, null,
|
|
CM_GET_DEVICE_INTERFACE_LIST_FLAGS.CM_GET_DEVICE_INTERFACE_LIST_PRESENT) != CONFIGRET.CR_SUCCESS)
|
|
{
|
|
yield break;
|
|
}
|
|
if (bufferSize <= 1)
|
|
{
|
|
// Empty list.
|
|
yield break;
|
|
}
|
|
var buffer = new char[checked((int)bufferSize)];
|
|
unsafe // DevSkim: ignore DS172412
|
|
{
|
|
fixed (char* pBuffer = buffer)
|
|
{
|
|
if (PInvoke.CM_Get_Device_Interface_List(interfaceClassGuid, null, pBuffer, bufferSize,
|
|
CM_GET_DEVICE_INTERFACE_LIST_FLAGS.CM_GET_DEVICE_INTERFACE_LIST_PRESENT) != CONFIGRET.CR_SUCCESS)
|
|
{
|
|
yield break;
|
|
}
|
|
}
|
|
}
|
|
// The list is double-NUL terminated.
|
|
interfacePaths = new string(buffer.AsSpan()[..^2]).Split('\0');
|
|
}
|
|
foreach (var interfacePath in interfacePaths)
|
|
{
|
|
var bufferSize = 0u;
|
|
if (PInvoke.CM_Get_Device_Interface_Property(interfacePath, PInvoke.DEVPKEY_Device_InstanceId, out var propertyType, null,
|
|
ref bufferSize, 0) != CONFIGRET.CR_BUFFER_SMALL)
|
|
{
|
|
continue;
|
|
}
|
|
// Must be a NUL-terminated UTF-16 string.
|
|
if ((propertyType != DEVPROPTYPE.DEVPROP_TYPE_STRING)
|
|
|| (bufferSize % sizeof(char) != 0) || (bufferSize / sizeof(char) < 1))
|
|
{
|
|
continue;
|
|
}
|
|
var buffer = new byte[checked((int)bufferSize)];
|
|
var bufferSizeConfirm = bufferSize;
|
|
if (PInvoke.CM_Get_Device_Interface_Property(interfacePath, PInvoke.DEVPKEY_Device_InstanceId, out propertyType, buffer,
|
|
ref bufferSizeConfirm, 0) != CONFIGRET.CR_SUCCESS)
|
|
{
|
|
continue;
|
|
}
|
|
if ((propertyType != DEVPROPTYPE.DEVPROP_TYPE_STRING) || (bufferSizeConfirm != bufferSize))
|
|
{
|
|
continue;
|
|
}
|
|
// The buffer includes the terminating NUL character.
|
|
var instanceId = new string(MemoryMarshal.Cast<byte, char>(buffer)[..^1]);
|
|
if (!TryCreate(instanceId, out var device))
|
|
{
|
|
continue;
|
|
}
|
|
yield return device;
|
|
}
|
|
}
|
|
}
|