WMI is extremely powerful and provides you with a wealth of information about computer systems and software. Unnoticed by many, the PowerShell team has introduced an exciting technology called Cmdlet Definition XML (CDXML) in PowerShell 3. It can auto-magically turn WMI classes into full-featured PowerShell cmdlets.
Quick Intro
WMI is much older than PowerShell and has always been an excellent and fairly easy-to-use source of information. When PowerShell initially surfaced, the team did two things to leverage the assets in WMI:
-
Simple WMI Access: the cmdlet
Get-WmiObject
was added to simplify querying WMI for information, and up to today this single cmdlet is still among the most popular and widely used cmdlets. -
Wrapping WMI Queries: some cmdlets were added that internally just wrapped WMI queries in an effort to make things even easier and more discoverable. For example, these two calls produce identical results:
# get hotfix information: Get-WmiObject -Class Win32_QuickFixEngineering # same: Get-Hotfix
Then, in PowerShell 3, the team did a lot of re-thinking:
- Better Standardization:
Get-WmiObject
was hard-coded to use the old DCOM network protocol. With the advent of WsMan and other remoting standards, it became important to query and access WMI in a flexible yet standardized manner. So the PowerShell team introduced CIM Cmdlets (read more). - Automatic Cmdlet Generation: Of course cmdlets like
Get-Hotfix
helped not having to remember awkward class names like Win32_QuickFixEngineering, but manually producing cmdlets for every single WMI class was no option. Instead, the PowerShell team added Cmdlet Definition XML (CDXML) to wrap WMI classes and produce cmdlets in an automated way.
CDXML enables you to auto-magically turn WMI classes and WMI methods into easy-to-use cmdlets, and you’re probably working with CDXML-generated cmdlets all the time without even knowing. CDXML is the reason why we have seen such a steep increase in cmdlets shipping with the Windows operating system.
In this article, I’d like to take you on a tour to discover CDXML. You’ll see how it can tremendously simplify your work with WMI, and you’ll find a lot of example code, including how to enable and disable your webcam (so you don’t accidentally leave your webcam on when the video conference is over).
Discovering CIM Cmdlets
A normal PowerShell module ships the code for new commands. A PowerShell module based on CDXML in contrast does not contain source code. Instead, it uses a .cdxml file to describe how cmdlets can query WMI classes and call WMI methods.
Finding CDXML-Based Modules
To find PowerShell modules based on CDXML, search for files with extension .cdxml in one of your module locations:
# find all CDXML-based PowerShell modules:
$env:PSModulePath -split ';' | Get-ChildItem -Filter *.cdxml -Recurse | Select-Object -ExpandProperty DirectoryName | Sort-Object -Unique
On a typical Windows 10 machine, the result looks similar to this and exposes 50 PowerShell modules that in reality just wrap WMI calls:
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\AppBackgroundTask
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\BranchCache
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\ConfigDefender
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\Defender
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\DFSN\MSFT_DFSNamespace
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\DFSN\MSFT_DFSNamespaceAccess
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\DFSN\MSFT_DFSNamespaceFolder
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\DFSN\MSFT_DFSNamespaceFolderTarget
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\DFSN\MSFT_DFSNamespaceRootTarget
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\DFSN\MSFT_DFSNamespaceServerConfig
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\DhcpServer
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\DirectAccessClientComponents
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\DnsClient
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\DnsServer
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\EventTracingManagement
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\FailoverClusters
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\HgsClient
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\IpamServer
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\iSCSI
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\MMAgent
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\MsDtc
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\NetAdapter
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\NetConnection
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\NetEventPacketCapture
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\NetLbfo
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\NetLldpAgent
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\NetNat
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\NetQos
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\NetSecurity
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\NetSwitchTeam
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\NetTCPIP
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\NetworkConnectivityStatus
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\NetworkController
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\NetworkTransition
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\NFS
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\PcsvDevice
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\PnpDevice
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\PrintManagement
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\PSDesiredStateConfiguration
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\RemoteAccess
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\ScheduledTasks
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\ServerManagerTasks
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\SmbShare
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\SmbWitness
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\Storage
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\StorageQoS
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\StorageReplica
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\VpnClient
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\Wdac
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\WindowsUpdateProvider
Don’t worry about the v1.0 subfolder. The PowerShell team planned on having different PowerShell versions side-by-side in version-specific subfolders and added the folder v1.0. This idea was abandoned, but the folder remained. All versions of Windows PowerShell reside in the subfolder v1.0.
Understanding .CDXML Files
At the core of each of these modules, you find one or more .cdxml files. These xml files use this structure:
<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
<Class ClassName="namespace/classname" ClassVersion="1.0.0">
<Version>1.0.0</Version>
<DefaultNoun>NounName</DefaultNoun>
<InstanceCmdlets>
<Cmdlet>
<!-- cmdlet definition(s) applying to WMI instances -->
</Cmdlet>
</InstanceCmdlets>
<StaticCmdlets>
<Cmdlet>
<!-- cmdlet definition(s) applying to WMI classes -->
</Cmdlet>
</StaticCmdlets>
</Class>
</PowerShellMetadata>
- Class: the attribute ClassName specifies the WMI namespace and WMI class name that it targets.
- DefaultNoun: defines the noun name for the cmdlets that are produced by this file
- InstanceCmdlets: defines the cmdlets that target the instances of the WMI class. If the WMI class was Win32_Share, for example, representing network shares, then the cmdlets defined in this section would apply to each individual share that exists.
- StaticCmdlets: defines the cmdlets that target the WMI class itself. Many WMI classes provide methods that are independent of actual instances.
Discovering “Managed” WMI Classes
In a next step, let’s read the .cdxml files of all PowerShell modules to discover the names of the WMI classes they manage:
$env:PSModulePath -split ';' | Get-ChildItem -Filter *.cdxml -Recurse | ForEach-Object -begin {
# create a xml object to manage the xml content of files:
$xml = [xml]::new()
} -process {
# load the .cdxml file content into xml object
$xml.Load($_.FullName)
# create custom object with the WMI class information
# if this is not a reference to a CmdletAdapter
if ($xml.PowerShellMetadata.Class.CmdletAdapter -eq $null)
{
#
[PSCustomObject]@{
Namespace = $xml.PowerShellMetadata.Class.ClassName | Split-Path
ClassName = $xml.PowerShellMetadata.Class.ClassName | Split-Path -Leaf
Module = $_.DirectoryName | Split-Path -Leaf
Path = ($_.FullName -replace 'C:\\Windows\\system32\\WindowsPowerShell\\v1.0', '$pshome')
}
}
} |
# eliminate duplicates
Sort-Object -Property Namespace, ClassName -Unique
When you run this code, it produces a long list of WMI classes. All of these classes are “managed”:
Namespace ClassName Module Path
--------- --------- ------ ----
ROOT\cimv2 Win32_PnPEntity PnpDevice $pshome\Modules\PnpDevice\PnpDevice.cdxml
ROOT\microsoft\ipam MSFT_IPAM_AccessScope IpamServer $pshome\Modules\IpamServer\IpamAccessScope.cdxml
ROOT\microsoft\ipam MSFT_IPAM_Address IpamServer $pshome\Modules\IpamServer\IpamAddress.cdxml
ROOT\microsoft\ipam MSFT_IPAM_AddressSpace IpamServer $pshome\Modules\IpamServer\IpamAddressSpace.cdxml
ROOT\microsoft\ipam MSFT_IPAM_Block IpamServer $pshome\Modules\IpamServer\IpamBlock.cdxml
...
ROOT\StandardCimv2 MSFT_PrinterNfcTagTasks PrintManagement $pshome\Modules\PrintManagement\MSFT_PrinterNfcTagTas
ks_v1.0.cdxml
ROOT\StandardCimv2 MSFT_PrinterPort PrintManagement $pshome\Modules\PrintManagement\MSFT_PrinterPort_v1.0
.cdxml
ROOT\StandardCimv2 MSFT_PrinterPortTasks PrintManagement $pshome\Modules\PrintManagement\MSFT_PrinterPortTasks
_v1.0.cdxml
ROOT\StandardCimv2 MSFT_PrinterProperty PrintManagement $pshome\Modules\PrintManagement\MSFT_PrinterProperty_
v1.0.cdxml
ROOT\StandardCimv2 MSFT_PrintJob PrintManagement $pshome\Modules\PrintManagement\MSFT_PrintJob_v1.0.cd
xml
ROOT\StandardCimv2 MSFT_TcpIpPrinterPort PrintManagement $pshome\Modules\PrintManagement\MSFT_TcpIpPrinterPort
_v1.0.cdxml
ROOT\StandardCimv2 MSFT_WsdPrinterPort PrintManagement $pshome\Modules\PrintManagement\MSFT_WsdPrinterPort_v
1.0.cdxml
Using “Managed” WMI Classes
While you can still use the raw native WMI cmdlets like Get-CimInstance
, it is much easier to use the managed cmdlets provided by the CDXML-based PowerShell modules.
Raw WMI Access…
For example, to get a list of Plug&Play devices, you may have figured out that these are represented by the WMI class Win32_PnPEntity:
Get-CimInstance -ClassName Win32_PnpEntity
This works well, and you can use WMI queries to further refine your search, and for example find all of your webcams:
Get-CimInstance -ClassName Win32_PnpEntity -Filter 'Name LIKE "%camera%"' | Out-GridView -Title 'Select Camera Device'
Obviously, this line of code yields results only if there is at least one camera device present in your system.
If you wanted to actually disable your webcam, things would become a lot more complex. You’d now need to know that the class Win32_PnpEntity has methods called Enable() and Disable(), and you’d need to know how to invoke these methods.
You’d also need to know that the methods Enable() and Disable() have been added in Windows 10 and Server 2016 and aren’t available in older versions of Windows, and that you need to have Administrator privileges to invoke them.
With all of this knowledge, you could come up with a script that can disable webcams:
# disable webcams
Get-CimInstance -ClassName Win32_PnpEntity -Filter 'Name LIKE "%camera%"' |
Out-GridView -Title 'Select Camera Device To Disable' -OutputMode Single |
Invoke-CimMethod -MethodName Disable
…Versus Managed CDXML Cmdlets
Fortunately, the WMI class Win32_PnPEntity has been turned into managed cmdlets by the PowerShell module PnPDevice as you have discovered earlier:
Get-Command -Module PnPDevice
The module comes with four auto-generated cmdlets:
CommandType Name Version Source
----------- ---- ------- ------
Function Disable-PnpDevice 1.0.0.0 PnPDevice
Function Enable-PnpDevice 1.0.0.0 PnPDevice
Function Get-PnpDevice 1.0.0.0 PnPDevice
Function Get-PnpDeviceProperty 1.0.0.0 PnPDevice
These four cmdlets represent querying Win32_PnPEntity and calling the three methods the class instances support. So now your code becomes much easier:
# disable webcams:
Get-PnpDevice -FriendlyName *Camera* |
Out-GridView -Title 'Select Camera Device To Disable' -OutputMode Single |
Disable-PnpDevice
When you run this code, you’ll discover even more goodness:
- Better Formatting: the gridview displays the Plug&Play devices in a much more readable format: you just see the four important properties Status (which shows Error when the device is not operational, i.e. because you disabled it), Class, FriendlyName, and InstanceId. When you ran
Get-CimInstance
, PowerShell showed all properties, and it was much harder and involved more scrolling to identify the right camera in the gridview. - Better Security: since disabling a device can be a harmful action, PowerShell automatically pops up a confirmation dialog when you call
Disable-PnPDevice
. - Silent Output: while calling WMI methods via
Invoke-CimMethod
always returns information,Disable-PnPDevice
by default just disables the device and does not return anything. If you want to know the results, which of course can be useful, add the parameter -PassThru.
To not show the confirmation box and return method results, run this instead:
# disable webcams: Get-PnpDevice -FriendlyName *Camera* | Out-GridView -Title 'Select Camera Device To Disable' -OutputMode Single | Disable-PnpDevice -Confirm:$false -Passthru
Identifying Managed WMI Classes
You’ve just seen how much easier it is to use the CDXML-based managed cmdlets over the raw WMI access. If you regularly work with WMI classes directly, you might want to check whether the classes you typically use are available via managed cmdlets as well.
# listing managed WMI classes and the names of CDXML-based cmdlets:
$env:PSModulePath -split ';' | Get-ChildItem -Filter *.cdxml -Recurse | ForEach-Object -begin {
# create a xml object to manage the xml content of files:
$xml = [xml]::new()
} -process {
# load the .cdxml file content into xml object
$xml.Load($_.FullName)
# create custom object with the WMI class information
# if this is not a reference to a CmdletAdapter
if ($xml.PowerShellMetadata.Class.CmdletAdapter -eq $null)
{
#
$moduleName = $_.DirectoryName | Split-Path -Leaf
$className = $xml.PowerShellMetadata.Class.ClassName | Split-Path -Leaf
$namespace = $xml.PowerShellMetadata.Class.ClassName | Split-Path
[PSCustomObject]@{
Cmdlet = ('Get-{0}' -f $xml.PowerShellMetadata.Class.DefaultNoun)
Method = ''
ClassName = $className
Module = $moduleName
Namespace = $namespace
}
# add instance method calls:
$xml.PowerShellMetadata.Class.InstanceCmdlets.Cmdlet |
Where-Object { $_ } |
ForEach-Object {
[PSCustomObject]@{
Cmdlet = ('{1}-{0}' -f $xml.PowerShellMetadata.Class.DefaultNoun, $_.CmdletMetadata.Verb)
Method = ('{0}()' -f ($_.Method.MethodName -replace '^cim:'))
ClassName = $className
Module = $moduleName
Namespace = $namespace
}
}
}
} |
# eliminate duplicates
Sort-Object -Property ClassName, Method
Now you can look up the WMI classes that provide managed cmdlets:
Cmdlet Method ClassName Module Namespace
------ ------ --------- ------ ---------
Get-ClusterHealth MSCluster_ClusterHealthService FailoverClusters root\MSCLUSTER
Get-ClusterHealth GetFault() MSCluster_ClusterHealthService FailoverClusters root\MSCLUSTER
Get-ClusterHealth GetMetric() MSCluster_ClusterHealthService FailoverClusters root\MSCLUSTER
Get-ClusterNode MSCluster_ClusterService FailoverClusters root\MSCLUSTER
Get-ClusterFaultDomain MSCluster_FaultDomain FailoverClusters root\MSCLUSTER
Remove-ClusterFaultDomain RemoveFaultDomain() MSCluster_FaultDomain FailoverClusters root\MSCLUSTER
Set-ClusterFaultDomain SetFaultDomain() MSCluster_FaultDomain FailoverClusters root\MSCLUSTER
Get-ClusterGroupSet MSCluster_GroupSet FailoverClusters root\MSCLUSTER
Add-ClusterGroupSet AddGroupToSet() MSCluster_GroupSet FailoverClusters root\MSCLUSTER
Add-ClusterGroupSet AddSetProvider() MSCluster_GroupSet FailoverClusters root\MSCLUSTER
Remove-ClusterGroupSet Remove() MSCluster_GroupSet FailoverClusters root\MSCLUSTER
Remove-ClusterGroupSet RemoveGroupFromSet() MSCluster_GroupSet FailoverClusters root\MSCLUSTER
Remove-ClusterGroupSet RemoveSetProvider() MSCluster_GroupSet FailoverClusters root\MSCLUSTER
Set-ClusterGroupSet SetSet() MSCluster_GroupSet FailoverClusters root\MSCLUSTER
Get-ClusterStorageNode MSCluster_StorageNode FailoverClusters root\MSCLUSTER
Remove-ClusterStorageNode RemoveStorageNode() MSCluster_StorageNode FailoverClusters root\MSCLUSTER
Set-ClusterStorageNode SetStorageNode() MSCluster_StorageNode FailoverClusters root\MSCLUSTER
Get-ClusterStorageSpacesDirect MSCluster_StorageSpacesDirect FailoverClusters root\MSCLUSTER
Get-Printer MSFT_3DPrinter PrintManagement ROOT\StandardCimv2
Get-AutologgerConfig MSFT_AutologgerConfig EventTracingManagement ROOT\Microsoft\Windows\Ev
entTracingManagement
Remove-AutologgerConfig DeleteInstance() MSFT_AutologgerConfig EventTracingManagement ROOT\Microsoft\Windows\Ev
entTracingManagement
Update-AutologgerConfig ModifyInstance() MSFT_AutologgerConfig EventTracingManagement ROOT\Microsoft\Windows\Ev
entTracingManagement
...
Get-PnpDevice Win32_PnPEntity PnpDevice ROOT\cimv2
Disable-PnpDevice Disable() Win32_PnPEntity PnpDevice ROOT\cimv2
Enable-PnpDevice Enable() Win32_PnPEntity PnpDevice ROOT\cimv2
Get-PnpDevice GetDeviceProperties() Win32_PnPEntity PnpDevice ROOT\cimv2
As you see, most CDXML-based modules come with their own new WMI classes. Win32_PnPEntity in fact is the only “classic” WMI class that has been turned into a managed module.
Example: Enabling And Disabling Hardware
Thanks to the simple-to-use CDXML-based cmdlets with their filter parameters and the better formatting, you can now easily create two PowerShell functions: one to enable cameras (or any other device you query), and one to disable:
#requires -Version 3.0 -Modules PnPDevice
#requires -RunAsAdministrator
function Enable-Camera
{
# find disabled cameras:
# (change the search phrase to find and manage any other hardware)
$result = Get-PnpDevice -FriendlyName *Camera* -Status ERROR -ErrorAction Ignore |
Out-GridView -Title 'Select Camera Device To Enable' -OutputMode Single |
Enable-PnpDevice -Confirm:$false -Passthru
if ($result.RebootNeeded)
{
Write-Warning 'Changes require a reboot to take effect.'
}
}
function Disable-Camera
{
# find enabled cameras:
# (change the search phrase to find and manage any other hardware)
$result = Get-PnpDevice -FriendlyName *Camera* -Status OK -ErrorAction Ignore |
Out-GridView -Title 'Select Camera Device To Disable' -OutputMode Single |
Enable-PnpDevice -Confirm:$false -Passthru
if ($result.RebootNeeded)
{
Write-Warning 'Changes require a reboot to take effect.'
}
}
Dependencies
The script requires the cmdlets Get-PnpDevice
, Enable-PnPDevice
and Disable-PnPDevice
provided by the module PnpDevice. It also requires Administrator privileges. That’s what the #requires statements are for: they ensure that these prerequisites are met. Else, PowerShell won’t run the script.
The module PnPDevice is listed as a separate dependency. Isn’t this module part of PowerShell 3? No, it isn’t, and it’s important to understand this:
PowerShell 3 (and better) ship with the Cim Cmdlets and CDXML technology. The module PnpDevice however was added to the operating system and introduced in Windows 10 and Server 2016. So when you are running an older Windows version and upgrade to PowerShell 3 or better, you are still missing the module PnpDevice. You’d have to upgrade your operating system, not PowerShell.
WMI Dependencies
In fact, this is the reason why you can’t just copy modules based on CDXML. These modules reference WMI classes and their methods, so to run them, your WMI classes must support this. As it turns out, the WMI instance methods Enable() and Disable() used by Enable-PnPDevice
and Disable-PnpDevice
where also added only in Windows 10 and Server 2016.
If you wanted to enable and disable devices in Windows versions prior to Windows 10 and Server 2016, you’d have to write code yourself to access the SetupAPI directly - or use a PowerShell module that does:
Install-Module -Name DeviceManagement -Scope CurrentUser -Force
Get-Command -Module DeviceManagement
The free module DeviceManagement comes with methods to enable and disable devices that work independent of WMI and are available in older Windows versions as well:
CommandType Name Version Source
----------- ---- ------- ------
Cmdlet Disable-Device 1.3.0 DeviceManagement
Cmdlet Enable-Device 1.3.0 DeviceManagement
Cmdlet Get-Device 1.3.0 DeviceManagement
Cmdlet Get-Driver 1.3.0 DeviceManagement
Cmdlet Get-NUMA 1.3.0 DeviceManagement
Cmdlet Install-DeviceDriver 1.3.0 DeviceManagement
Beginning with Windows 10 and Server 2016, though, this module is no longer needed as you have seen.
How Improved Formatting Works
Aside from easier discoverability, managed CDXML-based cmdlets also provide much better output. Let’s see how this works, and what your benefits are.
Raw WMI Output: Hard To Read
Open a fresh PowerShell and run this to see what the raw WMI content typically looks like:
Get-CimInstance -ClassName Win32_PnPEntity | Select-Object -First 1
This dumps the first available Plug&Play device instance, and you get back a lot of raw WMI information:
Caption : HID-compliant system controller
Description : HID-compliant system controller
InstallDate :
Name : HID-compliant system controller
Status : OK
Availability :
ConfigManagerErrorCode : 0
ConfigManagerUserConfig : False
CreationClassName : Win32_PnPEntity
DeviceID : HID\CONVERTEDDEVICE&COL03\5&7674E02&0&0002
ErrorCleared :
ErrorDescription :
LastErrorCode :
PNPDeviceID : HID\CONVERTEDDEVICE&COL03\5&7674E02&0&0002
PowerManagementCapabilities :
PowerManagementSupported :
StatusInfo :
SystemCreationClassName : Win32_ComputerSystem
SystemName : DELL7390
ClassGuid : {745a17a0-74d3-11d0-b6fe-00a0c90f57da}
CompatibleID :
HardwareID : {HID\ConvertedDevice&Col03, HID\VID_045E&UP:0001_U:0080,
HID_DEVICE_SYSTEM_CONTROL, HID_DEVICE_UP:0001_U:0080...}
Manufacturer : (Standard system devices)
PNPClass : HIDClass
Present : True
Service :
PSComputerName :
This includes properties that use numeric codes, for example ConfigManagerErrorCode. You can only guess that 0 represents “no error”, and if you find other values, it isn’t evident what they might stand for:
Get-CimInstance -ClassName Win32_PnPEntity -Filter 'ConfigManagerErrorCode > 0' | Select-Object -Property Name, ConfigManagerErrorCode
In my example, since I just disabled my webcam above, I get this:
Name ConfigManagerErrorCode
---- ----------------------
Intel(R) AVStream Camera 22
So with raw WMI content, there are two problems:
- Too much: you get back too much information and need to invest time to find the useful properties.
- Too cryptic: some of the information is coded and uses numeric values that are hard to understand
CDXML: Friendly Output
When you run this code, you get back a much easier to read representation of a Win32_PnPEntity instance:
Get-PnPDevice | Select-Object -First 1
PowerShell now only shows the four most important properties:
Status Class FriendlyName InstanceId
------ ----- ------------ ----------
OK HIDClass HID-compliant system controller HID\CONVERTEDDEVICE&COL03\5&7674E02&0&0002
You can still see all the other information by using Select-Object
, and when you do, you also see that most numeric values have been replaced by friendly text. This line dumps all Plug&Play devices in error state, and returns the error reason (instead of a code number):
If all of your Plug&Play devices work properly, you won’t get back anything. You may want to disable a device to test this.
# querying problematic hardware
Get-PnpDevice -Status ERROR | Select-Object -Property Name, ConfigManagerErrorCode
The result looks like this:
Name ConfigManagerErrorCode
---- ----------------------
Intel(R) AVStream Camera CM_PROB_DISABLED
In fact, CDXML has added a number of additional properties. To get a full status report, try this:
Get-PnPDevice | Select-Object -Property Name, Problem, ProblemDescription | Out-GridView
This produces a sophisticated report. Problem is an alias for ConfigManagerErrorCode, yet ProblemDescription is new and provides you with a human-readable description of the problem.
Only, ProblemDescription is empty in most cases due to a bug. The module reads the problem descriptions from a local resource file which resides inside the module. Since the resource file uses relative paths, you need to change the current path to the module base folder to see the problem descriptions:
# temporarily switch current path to module base Push-Location -Path (Get-Module -Name PnPDevice).ModuleBase # now problem descriptions are shown Get-PnPDevice | Select-Object -Property Name, Problem, ProblemDescription # restore path Pop-Location
Now the report looks good, and ProblemDescription has content:
Name Problem ProblemDescription ---- ------- ------------------ HID-compliant system controller CM_PROB_NONE This device is working properly. HID-compliant vendor-defined device CM_PROB_NONE This device is working properly. Jabra BIZ 2300 CM_PROB_PHANTOM Currently, this hardware device is not connected to... Intel(R) Control Logic CM_PROB_NONE This device is working properly. Samsung Flash Drive USB Device CM_PROB_NONE This device is working properly. USB Mass Storage Device CM_PROB_NONE This device is working properly. Bose QC35 II Avrcp Transport CM_PROB_NONE This device is working properly. Killer Networking Software CM_PROB_NONE This device is working properly. Bluetooth LE Generic Attribute Service CM_PROB_NONE This device is working properly. HID-compliant consumer control device CM_PROB_NONE This device is working properly.
Formatting Improvements For All Instances
Once you have loaded the module PnPDevice, this auto-magically prettifies the output of Win32_PnPEntity instances, regardless of how you produced them. So even raw WMI queries now show friendly texts.
The line below, which previously produced a raw numeric ConfigManagerErrorCode, now displays the same friendly result:
Get-CimInstance -ClassName Win32_PnPEntity -Filter 'ConfigManagerErrorCode > 0' | Select-Object -Property Name, ConfigManagerErrorCode
Name ConfigManagerErrorCode
---- ----------------------
Intel(R) AVStream Camera CM_PROB_DISABLED
That’s important to understand because this way, you can combine the best of both worlds: Get-PnPDevice
is perfect for most routine tasks but it won’t let you do sophisticated server-sided queries. For those, you can still use Get-CimInstance
. Just make sure you imported the module to get the sophisticated formatting.
Here is an example: Get-PnPDevice
supports the parameter -Status so you can actively search for a given status. You cannot, however, negate this and search for any device not in that status. Here comes Get-CimInstance
to the rescue:
# make sure you import the module to get better formatting
Import-Module -Name PnPDevice
# use direct WMI cmdlets and still get improved formatting:
Get-CimInstance -ClassName Win32_PnPEntity -Filter 'Status <> "OK"' | Select-Object -Property Name, Problem
Custom Formats And Types
Take a look into the PnPDevice module folder to understand how the module improved the output:
explorer "$PSHome\Modules\PnpDevice"
You’ll discover two files: PnPDevice.Format.ps1xml and PnPDevice.Types.ps1xml.
Formats Prettify WMI Instances
The first file defines the properties that PowerShell displays by default:
notepad "$PSHome\Modules\PnpDevice\PnPDevice.Format.ps1xml"
This format is applied to all objects of these types:
<ViewSelectedBy>
<TypeName>Microsoft.Management.Infrastructure.CimInstance#ROOT/cimv2/Win32_PnPDeviceProperty</TypeName>
<TypeName>Microsoft.Management.Infrastructure.CimInstance#Win32_PnPDeviceProperty</TypeName>
</ViewSelectedBy>
That’s why all instances of Win32_PnPDevice appeared prettified as soon as the module PnPDevice was loaded (and added these formats) - regardless of whether you used Get-PnPDevice
or Get-CimInstance -ClassName Win32_PnPDevice
to produce these instances.
Type Extensions Prettify Property Content
The second file adds new properties to the WMI instances and can turn code numbers into friendly text:
notepad "$PSHome\Modules\PnpDevice\PnPDevice.Types.ps1xml"
The type extension applies to all instances of this class which again explains why the benefits apply regardless of how you produced the instances:
<Name>Microsoft.Management.Infrastructure.CimInstance#ROOT/Cimv2/Win32_PnPEntity</Name>
New alias properties like Problem are added like this:
<AliasProperty>
<Name>Problem</Name>
<ReferencedMemberName>ConfigManagerErrorCode</ReferencedMemberName>
</AliasProperty>
Numeric Values Become Enumerations
Conversions from numerics to friendly text are implemented like this:
<ScriptProperty>
<Name>ConfigManagerErrorCode</Name>
<GetScriptBlock>
[Microsoft.PowerShell.Cmdletization.GeneratedTypes.PnpDevice.Problem]($this.PSBase.CimInstanceProperties['ConfigManagerErrorCode'].Value)
</GetScriptBlock>
</ScriptProperty>
The raw numeric value is converted into an enumeration type, in this example [Microsoft.PowerShell.Cmdletization.GeneratedTypes.PnpDevice.Problem]. For this to work, the script property needs to read the raw value directly from the CimInstanceProperties, effectively bypassing the PowerShell type system. Else, you’d produce an endless loop.
The type [Microsoft.PowerShell.Cmdletization.GeneratedTypes.PnpDevice.Problem] is defined directly in the .cdxml file:
notepad "$PSHome\Modules\PnpDevice\PnPDevice.cdxml"
At the end, there is an (optional) <Enums//> section that translates the numeric values to friendly text:
<Enums>
<Enum EnumName="PnpDevice.Problem" UnderlyingType="uint32">
<Value Name="CM_PROB_NONE" Value="0" />
<Value Name="CM_PROB_NOT_CONFIGURED" Value="1" />
<Value Name="CM_PROB_DEVLOADER_FAILED" Value="2" />
...
<Value Name="CM_PROB_USED_BY_DEBUGGER" Value="53" />
<Value Name="CM_PROB_DEVICE_RESET" Value="54" />
<Value Name="CM_PROB_CONSOLE_LOCKED" Value="55" />
<Value Name="CM_PROB_NEED_CLASS_CONFIG" Value="56" />
</Enum>
<Enum EnumName="PnpDeviceProperty.Type" UnderlyingType="uint32">
<Value Name="Empty" Value="0" />
<Value Name="Null" Value="1" />
<Value Name="SByte" Value="2" />
<Value Name="Byte" Value="3" />
...
<Value Name="ErrorArray" Value="4119" />
<Value Name="NTStatusArray" Value="4120" />
<Value Name="StringIndirectList" Value="8217" />
</Enum>
</Enums>
Buggy: Using Text Resources
Finally, let’s take a look at how the module PnPDevice implemented the new property ProblemDescription, and why this property only works when the current directory is set to the module base.
Problem Description
As you have seen, ProblemDescription is always empty unless you set your current folder path to the module base folder:
# temporarily switch current path to module base
Push-Location -Path (Get-Module -Name PnPDevice).ModuleBase
# now problem descriptions are shown
Get-PnPDevice | Select-Object -Property Name, Problem, ProblemDescription
# restore path
Pop-Location
Problem Cause
Let’s investigate the cause of this problem. Take a look at the types definition again:
notepad "$PSHome\Modules\PnpDevice\PnPDevice.Types.ps1xml"
ProblemDescription is implemented like this:
<ScriptProperty>
<Name>ProblemDescription</Name>
<GetScriptBlock>
Microsoft.PowerShell.Utility\Import-LocalizedData LocalizedData -filename PnpDevice.Resource.psd1
switch([Microsoft.PowerShell.Cmdletization.GeneratedTypes.PnpDevice.Problem]($this.PSBase.CimInstanceProperties['ConfigManagerErrorCode'].Value))
{
CM_PROB_NONE
{
$str = $LocalizedData.IDS_PROB_NOPROBLEM
}
...
In essence, the problem descriptions are read from a local resource file located in the module folder:
Microsoft.PowerShell.Utility\Import-LocalizedData LocalizedData -filename PnpDevice.Resource.psd1
Import-LocalizedData
reads the content from the .psd1 file into the variable $LocalizedData
. then the remainder of the code produces the problem descriptions.
Unfortunately, all of this code is executed in the caller context, not the module context. So Import-LocalizedData
uses the current path to load the file PnpDevice.Resource.psd1 and can only succeed if you happen to have set your current folder to the module base folder.
We have had an interesting discussion on this on Twitter about this with great suggestions from community members. In the end, we came up with a simple fix (see below). Many thanks to all who participated!
Fixing Problem
To fix the problem, you need to submit the module root folder to the parameter -BaseDirectory so PowerShell knows where to find the resource file. Fortunately, you can safely assume that the module PnPDevice is already loaded into memory when PowerShell processes the type extension.
So to fix the problem, replace this line in PnpDevice.Types.ps1xml:
Microsoft.PowerShell.Utility\Import-LocalizedData LocalizedData -filename PnpDevice.Resource.psd1
…with this line:
Microsoft.PowerShell.Utility\Import-LocalizedData LocalizedData -filename PnpDevice.Resource.psd1 -BaseDirectory (Get-Module -Name PnPDevice).ModuleBase
But wait: since all PowerShell modules shipping with Windows are installed (and protected) by the TrustedInstaller, you don’t have permission to alter the files. So for the time being, the best workaround is to temporarily change the current path as described above using Push-Location
and Pop-Location
. Hopefully, Microsoft will fix the issue eventually.
If you want to test-drive the fix, simply copy the entire module folder PnPDevice to a place where you do have write permissions, and apply the fix. Next, use
Import-Module -Name <path_to_copied_module_folder>
to manually load the fixed module into memory.Now this line works without a flaw regardless of your current location:
Get-PnpDevice | Select-Object -Property Name, Problem* | Out-GridView
I have reported this issue, and hopefully we soon see an official fix. It’s worth it: ProblemDescription provides useful extra-information for your Plug&Play devices.
If you come across PowerShell issues that repro in PowerShell 7, or if you’d like to provide feature requests or other feedback to PowerShell 7, this is the official starting page for your submissions to the PowerShell team.
Before you go ahead, make sure your submission applies to PowerShell 7. There are many issues in Windows PowerShell that have long been fixed in PowerShell 7 (if you haven’t looked at PowerShell 7 yet, you might want to now)