I've tried a few different ways of enumerating redirected USB devices using the Remote Desktop ActiveX control in PowerShell but haven't been able to figure out a way to achieve this.
mstsc.exe
so I'm hoping to be able to programatically enumerate these devices somehow -- perhaps via WMI, COM or some Win32 API method I'm currently unaware ofmstscax.mstscax
COM object, but there aren't any properties or methods or COM interfaces which expose redirected devicesDeviceCollection
property)
[Type]::GetTypeFromCLSID()
[System.Activator]::CreateInstance()
[System.Runtime.InteropServices.Marshal]::QueryInterface()
aximp.exe
Active Control Import tool to generate the .NET assembly MSTSCLib.dll
from %windir%\system32\mstscax.dll
This seemed promising since it exposes the DeviceCollection
property, which does exactly what I need:
Retrieves the collection of Plug and Play (PnP) device objects to be redirected.
Add-Type -Path 'C:\mstscax\MSTSCLib.dll'
$rdpClient = [MSTSCLib.MsRdpClient10Class]::new()
# These return a System.__ComObject object
$rdpClient.get_DeviceCollection()
$rdpClient.DeviceCollection
$rdpClient.IMsRdpClientNonScriptable4_DeviceCollection
$rdpClient.IMsRdpClientNonScriptable5_DeviceCollection
# Casting fails:
[MSTSCLib.IMsRdpDeviceCollection]$rdpClient.get_DeviceCollection()
[MSTSCLib.IMsRdpDeviceCollection]$rdpClient.DeviceCollection
[MSTSCLib.IMsRdpDeviceCollection]$rdpClient.IMsRdpClientNonScriptable4_DeviceCollection
[MSTSCLib.IMsRdpDeviceCollection]$rdpClient.IMsRdpClientNonScriptable5_DeviceCollection
# Cannot convert the "System.__ComObject" value of type "System.__ComObject" to type "MSTSCLib.IMsRdpDeviceCollection".
InvokeMember()
method on $rdpClient
but still can't poke into the DeviceCollection
property and retrieve a list of usable objects.aximp.exe
(AxMSTSCLib.dll
) didn't appear to offer what I needed, but there's a chance I've missed/overlooked something crucial.Class
is an AliasProperty for PnPClass
) names are documented herefunction Get-RedirectedPnpDevice
{
$pnpDevices = Get-PnPdevice | Where-Object {$_.Class -eq "WPD" -and $_.Status -eq "OK"}
foreach ($device in $pnpDevices)
{
[pscustomobject] @{
Name = $device.Name
InstanceId = $device.DeviceID
Caption = $device.Caption
}
}
}
(FAIL) The first option I tried (as documented in the question) was to use the Aximp.exe
tool to generate a usable .NET assembly(s) from the %windir%\System32\mstscax.dll
Remote Desktop ActiveX control, then import them into the PowerShell session via Add-Type -Path <PathToAssembly>
.
AxMSTSCLib.dll
looked a bit more promising; all of the necessary COM interfaces were exposed, but iterating through a DeviceCollection
still returned System.__ComObject
, which could not be converted.(SUCCESS) What I ended up doing was writing a C# cmdlet and manually writing out the relevant COM interop interface definitions, averting the need for the Aximp.exe
tool. Rather than compile the C# cmdlet into a .DLL assembly, I chose to keep everything in PowerShell and compile the cmdlet into memory. It's not the prettiest, but it works (even with PowerShell 4.0 and Win7 -- hence the use of New-Object
rather than using a ::new()
ctor).
function Get-RedirectedUsbDevice
{
[CmdletBinding()]
param (
[Parameter(ParameterSetName = 'DeviceType', Position = 0)]
[ValidateSet('PnP', 'USB')]
[System.String]$DeviceType
)
$redirectedUsbDeviceCSCode = @'
using System;
using System.Management.Automation;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
public enum DeviceType
{
PnP,
USB
}
public class RedirectedUsbDevice
{
public string Name {get; set;}
public string Description { get; set; }
public string InstanceId { get; set; }
public DeviceType DeviceType { get; set; }
}
internal class RdpClientInterop
{
[ComImport]
[Guid("B3378D90-0728-45C7-8ED7-B6159FB92219")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMsRdpClientNonScriptable3
{
[DispId(1)]
string ClearTextPassword { set; }
[DispId(2)]
string PortablePassword { get; set; }
[DispId(3)]
string PortableSalt { get; set; }
[DispId(4)]
string BinaryPassword { get; set; }
[DispId(5)]
string BinarySalt { get; set; }
void ResetPassword();
void NotifyRedirectDeviceChange([In][ComAliasName("MSTSCLib.UINT_PTR")] uint wParam, [In][ComAliasName("MSTSCLib.LONG_PTR")] int lParam);
void SendKeys([In] int numKeys, [In] ref bool pbArrayKeyUp, [In] ref int plKeyData);
[DispId(13)]
[ComAliasName("MSTSCLib.wireHWND")]
IntPtr UIParentWindowHandle
{
[return: ComAliasName("MSTSCLib.wireHWND")]
get;
[param: ComAliasName("MSTSCLib.wireHWND")]
set;
}
[DispId(14)]
bool ShowRedirectionWarningDialog { get; set; }
[DispId(15)]
bool PromptForCredentials { get; set; }
[DispId(16)]
bool NegotiateSecurityLayer { get; set; }
[DispId(17)]
bool EnableCredSspSupport { get; set; }
[DispId(21)]
bool RedirectDynamicDrives { get; set; }
[DispId(20)]
bool RedirectDynamicDevices { get; set; }
[DispId(18)]
IMsRdpDeviceCollection DeviceCollection
{
[return: MarshalAs(UnmanagedType.Interface)]
get;
}
[DispId(23)]
bool WarnAboutSendingCredentials { get; set; }
[DispId(22)]
bool WarnAboutClipboardRedirection { get; set; }
[DispId(24)]
string ConnectionBarText { get; set; }
}
[Guid("56540617-D281-488C-8738-6A8FDF64A118")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMsRdpDeviceCollection
{
void RescanDevices(bool vboolDynRedir);
IMsRdpDevice get_DeviceByIndex(uint index);
IMsRdpDevice get_DeviceById(string devInstanceId);
[DispId(225)]
uint DeviceCount { get; }
}
[Guid("60C3B9C8-9E92-4F5E-A3E7-604A912093EA")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMsRdpDevice
{
[DispId(222)]
string DeviceInstanceId { get; }
[DispId(220)]
string FriendlyName { get; }
[DispId(221)]
string DeviceDescription { get; }
[DispId(223)]
bool RedirectionState { get; set; }
}
[ComImport]
[Guid("5fb94466-7661-42a8-98b7-01904c11668f")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMsRdpDeviceV2
{
string DeviceInstanceId();
string FriendlyName();
string DeviceDescription();
void RedirectionState([In][MarshalAs(UnmanagedType.Bool)] bool RedirState);
bool RedirectionState();
string DeviceText();
bool IsUSBDevice();
bool IsCompositeDevice();
uint DriveLetterBitmap();
}
[ComImport]
[Guid("B2A5B5CE-3461-444A-91D4-ADD26D070638")]
[TypeLibType(4160)]
internal interface IMsRdpClient7
{
}
internal static string GetDeviceName(IMsRdpDeviceV2 device)
{
try
{
string friendlyName = device.FriendlyName();
if (!string.IsNullOrEmpty(friendlyName))
{
return friendlyName;
}
}
catch { }
try
{
string deviceText = device.DeviceText();
if (!string.IsNullOrEmpty(deviceText))
{
return deviceText;
}
}
catch { }
try
{
string deviceDescription = device.DeviceDescription();
if (!string.IsNullOrEmpty(deviceDescription))
{
return deviceDescription;
}
}
catch { }
return "Unknown device name";
}
}
[Cmdlet(VerbsCommon.Get, "RedirectedUsbDevice")]
public class GetRedirectedUSBDeviceCommand : Cmdlet
{
protected override void ProcessRecord()
{
try
{
Type mstscaxType = Type.GetTypeFromProgID("mstscax.mstscax");
var rdpClient = (RdpClientInterop.IMsRdpClient7)Activator.CreateInstance(mstscaxType);
RdpClientInterop.IMsRdpClientNonScriptable3 msRdpClientNonScriptable = (RdpClientInterop.IMsRdpClientNonScriptable3)(object)rdpClient;
RdpClientInterop.IMsRdpDeviceCollection deviceCollection = msRdpClientNonScriptable.DeviceCollection;
for (int i = 0; i < deviceCollection.DeviceCount; i++)
{
RdpClientInterop.IMsRdpDevice msRdpDevice = deviceCollection.get_DeviceByIndex((uint)i);
RdpClientInterop.IMsRdpDeviceV2 msRdpDeviceV2 = (RdpClientInterop.IMsRdpDeviceV2)msRdpDevice;
RedirectedUsbDevice redirectedUsbDevice = new RedirectedUsbDevice {
Name = RdpClientInterop.GetDeviceName(msRdpDeviceV2).Trim().Replace("\0", ""),
Description = msRdpDeviceV2.DeviceDescription().Trim().Replace("\0", ""),
InstanceId = msRdpDeviceV2.DeviceInstanceId().Trim().Replace("\0", ""),
DeviceType = msRdpDeviceV2.IsUSBDevice() ? DeviceType.USB : DeviceType.PnP
};
WriteObject(redirectedUsbDevice);
}
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "USB device enumeration failed", ErrorCategory.InvalidOperation, null));
}
}
}
'@
try
{
Add-Type -TypeDefinition $redirectedUsbDeviceCSCode -Language CSharp
$getRedirectedUSBDeviceCommandObj = New-Object -TypeName GetRedirectedUSBDeviceCommand
if ($PSCmdlet.ParameterSetName -eq 'DeviceType')
{
$outputObj = $getRedirectedUSBDeviceCommandObj.Invoke() | Where-Object {$_.DeviceType -eq $DeviceType}
}
else
{
$outputObj = $getRedirectedUSBDeviceCommandObj.Invoke() | Out-Default
}
if ($outputObj.Count -lt 1) {Write-Output $null}
else {Write-Output $outputObj}
}
catch
{
Write-Error $_
}
}