Understanding CDXML

CDXML turns WMI queries and method calls into simple and reusable cmdlets. Let's uncover how this PowerShell magic works!

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)