DevCity.NET - http://devcity.net
WMI connections made easy in VB.NET
http://devcity.net/Articles/144/1/article.aspx
Martin de Klerk

Born in 1958 and bitten by the computer-bug in 1976, I am an autodidact to the letter.

Through the years I have:
  - accumulated programming experience ranging from writing device-drivers to developing advanced MIS programs.
  - mastered languages like Assembler, C/C++/C#, (several flavours of) Basic, Cobol, Forth, Eiffel and Clipper
  - filled nearly every job/position the IT world has to offer as employee or contractor.
  - worked with customers ranging from your local grocery-shop, farmers, hospitals, offshore companies, governemental departments to multinationals like Shell, Afga-Gevaert, ABN/AMRO and Phillips N.V.

Currently I am:
  - running my own consultancy business since 1993.
  - developing 80% of my solutions in VB.NET
  - dividing my sparse moments of free time between my family, music (especially enjoy jamming with my son : on the guitar he's the gifted one, I just manage a bit of semi-decent guitar/harmonica play) and VBCity.
  - eagerly awaiting the official release of .NET Framework 2.0

 
by Martin de Klerk
Published on 4/24/2005
 

This document is not intended as a WMI tutorial, and as such does not cover WMI itself. This document is about using WMI in VB.NET and is intended as a rough and practical (but by no means complete!)  guideline to get you up and running with WMI as a VB.NET programmer.


Introduction

This document is not intended as a WMI tutorial, and as such does not cover WMI itself. This document is about using WMI in VB.NET and is intended as a rough and practical (but by no means complete!) guide to get you up and running with WMI as a VB.NET programmer.

Without running into brick walls, that is. At least a lot less than I did. Windows Management Instrumentation can be a big PITA for the inexperienced. It's intricacies and (for instance) OS version dependencies are not for the fainthearted, but once you've got it going it offers tremendous possibilities.

I am a seasoned programmer and have programmed for various operating systems, but as a result of recently being exposed to the realm of WMI the number of gray hairs on my head has doubled. And to prevent all VB.NET programmers all over the world being recognizable by their excessive grey hairs or even their baldness, I offer you the benefits of my mistakes and experiences on this topic so that your VB.NET/WMI endeavours may be enjoyable.

Deciding I would stick to the (VB).NET paradigm I rejected scripting or WBEM as a means of accessing WMI, and concentrate on the System.Management namespace.

(Remote) connection to WMI Overview

Establishing a remote WMI connection involves several issues, varying from communication ports to authorization. If I was given a dollar for every time someone was confronted with the error message "Remote RPC-server not available" due to a port blocked by a firewall or an invalid DCOM/COM+ authentication, I'd probably end up in the Forbes Top 100.

As WMI uses several protocols and each protocol using its own pre-defined communication ports, firewall settings should allow access through the following ports for basic WMI operations:

      RPC   TCP 135,139,445,593
      SNMP   UDP 161,162
      Optional:
      WINS   TCP 42 UDP 42, 137
      PrintSpooler  TCP 139, 445
      TCP/IP PrintServer TCP 515

Note: there are more port settings involved for advanced features, but this will suffice for basic functionality. Also, make sure the DCOM/COM+ services are running all computers involved. Security in WMI is related to connecting to a namespace. WMI uses DCOM to handle remote calls. The most common reason for failure to connect to a remote computer is due to DCOM failure (error "DCOM Access Denied" decimal -2147024891 or hex 0x80070005). You can configure DCOM settings for WMI using DCOM Config in the Control Panel Administrative Tools. Here you can set the security to launch, access, and configure the WMI service.

The second pitfall is authorization: you must have admin rights on the (remote) computer.

The third pitfall is O.S. dependency. For instance, the DCOM/COM+ interface used by WMI requires different AuthenticationLevel settings depending on the OS: when connecting to systems running a MS Windows version prior to XP the AuthenticationLevel.Connect setting is mandatory to get access to the objects and classes interfaced through DCOM/COM+, but XP requires this setting to be AuthenticationLevel.Packet. Later on this OS dependency will also be an issue on the availability and/or functionality of several WMI classes and objects.

When the above conditions are met, you are connected to the (remote) WMI server. The next step is to connect to a WMI namespace. A WMI namespace contains WMI classes and objects, just like a .NET namespace, but it is also your 'workspace'. Best way to think of a WMI namespace is like a folder and its subfolder to which you have to log in.

Just as the .NET Framework, WMI provides several of those namespaces according to functionality. Currently the default WMI namespace = "\root\cimv2". By supplying the full path to the namespace you can connect to the local WMI namespace ("\.\root\cimv2") or to a remote namespace ("\pc_admin\root\cimv2")

Once connected to a the namespace, you have access to the classes and objects residing in that namespace.

What you need to code a WMI connection in VB.NET.

Enter System.Management.ConnectionOptions and System.Management.ManagementScope. Together they form the gateway to the WMI realm. You need the ConnectionsOptions object for authentication settings, and the ManagementScope class for the actual connection. All communications regarding WMI classes and objects will involve the latter class.

    Dim myConnectionOptions As New System.Management.ConnectionOptions
    With myConnectionOptions
        .Impersonation = System.Management.ImpersonationLevel.Impersonate
        '* Use next line for XP
        .Authentication = System.Management.AuthenticationLevel.Packet
        '* Use next line for Win prior XP
        '*.Authentication = System.Management.AuthenticationLevel.Connect
    End With

The above code will set the default authorization needed for a WMI connection. This object will, together with a fully qualified WMI namespace, form the means to establish a connection:

    Dim myManagementScope As System.Management.ManagementScope
    '* Replace the "." with an actual servername for remote connection
    Dim myServerName As String = "."
    myManagementScope = New System.Management.ManagementScope("\" & _
      myServerName & "\root\cimv2", myConnectionOptions)
    '* connect to WMI namespace
    myManagementScope.Connect()
    If myManagementScope.IsConnected = False Then
        ConsoleWriteLine("Could not connect to WMI namespace")
    End If

Beware that if the conditions do not conform as fore-mentioned in the previous paragraph '(Remote) connection to WMI Overview', your application will throw an exception on the statement myManagementScope.Connect().

 

Okay, I'm In. Now what?

Let's say you want to know what software is installed on a remote PC. In essence you send a query to the (remote) WMI server. This WMI server returns the result of that query (in this case as an ManagementObjectCollection, which is an array of ManagementObjects each representing a particular piece of software installed by the MS-Installer). The query will be coordinated by the ManagementObjectSearcher class:

    Dim myObjectSearcher as System.Management.ManagementObjectSearcher
    Dim myCollection As System.Management.ManagementObjectCollection
    Dim myObject As System.Management.ManagementObject
    myObjectSearcher = New System.Management.ManagementObjectSearcher( _
      myManagementScope.Path.ToString, "Select * From Win32_Product")
    '* execute query
    myCollection = myObjectSearcher.Get()
    '* list packages installed
    For Each myObject In myObjectCollection
        Console.WriteLine( myObject.GetPropertyValue("Caption"))
    Next

The above routine will work for any of WMI's Win32_XXXX classes, as all of these classes possess the property "Caption". For a complete list of available WMI classes, objects and namespaces, check out MSDN or see the links at the end of this article.

A note on retrieving WMI values: as these values are not type-safe ( the VB.NET compiler has no way of knowing in advance what type of data will be returned), you will have to convert certain data-types to the .NET equivalents. For instance: the property Win32_Printer.Priority is an unsigned 32 bits integer (uint32). The .NET Framework does not allow unsigned integers, so to use this value you will have to convert the value to a .NET data type:

    myPriority = Convert.ToInt32( myPrinterObject.GetPropertyValue("Priority"))

This is fine if you only make basic use of WMI, but as your use of WMI extends you'll find these coded conversions a big trouble-source and waste of typing effort. The solution comes in the form of Management Strongly Typed Class Generator (Mgmtclassgen.exe). This utility, contained in the .NET Framework Toolkit, generates an early-bound (strong typed) wrapper for the selected WMI Win32_XXXX management class to include in your project. Using this wrapper, you do not have to worry about standard conversions.

 

...and The Title Reads : "WMI Access in VB.NET Made Easy?"

You are totally right, and I will live up to the title. Just bear with me, it will be worth it.

In November 2004 I got involved the first time with WMI by means of a question from a VBCitizen. He wanted to monitor printers on his network through WMI and was stuck in some code-logic. The code he showed on how he retrieved the remote printer information kind of sucked my eyes into the my monitor. The statement that got me going was:

    moSearch = New Management.ManagementObjectSearcher("Select * from Win32_Printer")

Surely it could not be as simple as issuing one statement ? After a quick copy 'n' paste I ran his code on my machine, and lo and behold: all my logical installed printers got listed, fax and (installed shares to) remote printers included. I have to admit I felt euphoric as the realization of the new possibilities rushed through my veins. Anyway, his posting marked the start of my journey through the WMI. I started out to assist the VBCitizen, and in that process I've learned a lot about WMI in conjunction with VB.NET.

Are you still with me ? Good, then here is your reward for your perseverance:

The Connection Tester

One of the results of this journey is my VB.NET class 'ConnectionTester'. As the name implies, it was originally conceived as a quick means to see if a remote computer was on-line, as the WMI connect() statement can take up considerable time to decide that a remote is off-line or the remote RPC server is unavailable. I found a faster method in a DNS-query, which tells me if a computer is on-line in a matter of seconds.

The next step was to build in a check for WMI availability on the remote computer by initiating a WMI connection to the remote namespace and handle all possible exceptions thrown. Having finished testing the WMI check I realized that if remote WMI was available I had an open WMI connection to my disposal. Acknowledging the fact that closing an re-opening a WMI connection is a time-consuming and processor-intensive activity ( as with several other WMI functionalities, so be prepared to dig into threading to speed up or un-freeze your application..) I decided to extend my class to a light-weight WMI connection wrapper.

This wrapper-class consists of only two methods ( .Poll and .ExecWmiQuery), and properties that are divided into two types: those that are to be set before invoking .Poll, and those that get populated as a result of invoking .Poll.

Of the properties that are to be set, providing either the .ServerName or the .IPAddress is mandatory. The other properties (.WmiCheck, .WmiNameSpace, .UserName, .Password) are optional to provide some degree of flexibility

The results of a call to .Poll gets stored in the properties .IsOnline, .WmiEnabled, .HasErrors, .ErrorMessage, .PollInProgress, .OperatingSystem and .WmiScope

...So , Where Does the EASY Part Come In ?

Right here. Establishing a WMI connection to a remote computer now takes just three statements:

    '* Create an instance of the ConnectionTester class
    Dim myConn as New ConnectionTester
    '* point to the computer to connect
    MyConn.ServerName = "PC_admin"
    '* Initiate connection
    MyCon.Poll()

That's it. Gone are the ManagementScope, ConnectionOptions, ManagementObjectSearcher classes and low-level exception handling.

After a successful call to .Poll ( you can check that through the .IsOnline, .WmiEnabled and .HasErrors properties), you can take over the connection ( the .WmiScope property) to do your own stuff, or you could use the build-in query mechanism.

The next code block will list all processes running on the selected computer:

    '* Create storage for the query result
    Dim WmiQueryResult As System.Management.ManagementObjectCollection
    '* Retrieve WMI-objects of processes running on target
    WmiQueryResult = myConn.ExecWmiQuery("Select * From Win32_Process")
    '* List returned process-names
    Dim WmiObject as System.Management.ManagementObject
    For Each WmiObject In WmiQueryResult
        Console.WriteLine(WmiObject.GetPropertyValue("Caption"))
    Next
 

WMI Queries

A note on WMI queries: the WMI Query Language (WQL) is modelled after SQL, and as such supports extending clauses like WHERE, WITHIN, HAVING etc. For instance, to retrieve the WMI representation of a selected printer on a remote computer, you could issue a query like "Select * From Win32_Printer Where Name=""hpdeskjet""". Executing this query should result in an ManagementObjectCollection containing a single ManagementObject representing the selected printer.

Enter O.S. dependency. When running Windows 9.x or ME, the extended WQL clauses like WHERE are not supported. In this case the only option is to retrieve all printer objects and select the correct WMI object by code:

    WmiQueryResult = myConn.ExecWmiQuery("Select * From Win32_Printer")
    For Each WmiObject In WmiQueryResult
        If WmiObject.GetPropertyValue("Name") = "hpdeskjet" Then
            '
            '
        End If
    Next

Yet More Pitfalls

I had to work my way through the WMI information-jungle with a machete. For every working VBScript/WBEM/C++ solution found on the internet, my determination to solve the problem on hand in VB.NET grew. During this ordeal I found many promising (WMI) pathways towards my goals, but in many cases I had to abandon that trail half-way due to O.S. dependencies having only a XP Pro client at my disposal.

Another point is managed vs. unmanaged code. In above sample the WmiObject is of type Win32_Printer. This WMI object gets it's properties derived from various WMI subsystems, like it shows in MSDN's definition of the Win32_Printer.Caption property:

Caption
    Data type: string
    Access type: Read-only
    Qualifiers: MaxLen(64)

Short description of an object, a one-line string. This property is inherited from CIM_ManagedSystemElement.

I discovered that many WMI properties were not available, as those were unmanaged code/data . Although disappointing, in .NET that makes sense. But it also restricted me by default to the use only those properties that were either WMI intrinsic, generic to the handled WMI object (no inheritance) or properties that were inherited from the CIM_ManagedSystemElement class.

For accessing unmanaged WMI code or data, check out the links provided at the end of this article.

An Added Bonus (or Two)

So why call the method .Poll() and not .Connect() ?
There are several reasons for this.

The first one being that the necessity for this class originated out of creating a network printer monitor program. The program had to be able to detect when a remote computer came on-line, and if so, automatically create the WMI connection to start querying the remote printers.

The second reason was that the ConnectionTester class was original intended to speed up the connection process. If a remote computer was not on-line, there was no reason to initiate a WMI connection which would take a considerable amount of time before throwing an exception. For the same reason the WMI availability check was to be performed only on the first time the remote was detected on-line.

The third reason is OS dependency. As certain WMI features and classes are only available under specific Windows versions, I needed an easy way to identify the version of Windows running on the remote computer. This is done by the Sub GetRemoteOsInfo() which is ( oh sweet irony!) totally OS dependent. Just check out the source code and you'll see what I mean.

The way the code is set up, you can use the ConnectionTester class for a more general use:

Bonus #1: You can switch off the WMI functionality of the ConnectionTester class, so it will perform only a quick online test. This, together with the addition of a Timer, enables you to build a simple connection monitor program. Just set .WmiCheck to False, load .ServerName with "www.vbcity.com", and in the Timer.Tick event ( or Timer.Elapsed event, depending on which Timer class you're using) simply place the following statements:

    WmiConn.Poll()
    If Not WmiConn.IsOnline Then
        Console.WriteLine("VBCity unavailable. " _
          & "Please switch to PANIC MODE and dial 911.")
    End If

Now, of course the VBCity server is not WMI enabled. But if it were the case, you should have no problem with retrieving WMI information over the Internet using the ConnectionTester class. That makes Bonus #2: Whether you need to manage WMI objects locally, on a computer in a LAN or a computer somewhere in this world connected to the Internet, it is completely transparent using the ConnectionTester class.

 

Walking Through The Sample

The next part shows the sample code that is included in the ConnectionTester source. Just remove the module 'Sample' to incorporate the ConnectionTester class in your own project. As it is heavily commented, I won't insult the readers intelligence by explaining what's being explained:

   Dim WmiConn As New ConnectionTester
      '* point the ConnectionTester to the target computer.
      '* That can be done either by supplying the NetBios-name or
      '* Domain-name of the (remote) computer:
      WmiConn.ServerName = "localhost"
      '* OR by supplying the IP address of the (remote) computer
      '* computer as a string like:
      '*    WmiConn.IPAddress = "127.0.0.1"
      '* OR by providing the HTTP address of the (remote) computer:
      '*    WmiConn.ServerName = "www.vbcity.com"
      '* The ConnectionTester's WMI capabilities are enabled per default.
      '* To disable them and only perform a quick online check, set the
      '* next value to False
      WmiConn.WmiCheck = True
      '* Test the connection
      Console.WriteLine("Connecting to {0}", wmiConn.ServerName)
      WmiConn.Poll()
      '* Check if target is online
      If wmiConn.IsOnLine = False Then
         Console.WriteLine("{0} is off-line.", WmiConn.ServerName)
         Exit Sub
      Else
         '* Display on-line state
         Console.WriteLine("{0} is online with IP address: {1}.", _
           WmiConn.ServerName, WmiConn.IPAddress)
      End If
      '* Check if errors occured while connecting
      If WmiConn.HasErrors = True Then
         Console.WriteLine("Error while connecting to {0}: {1}", _
           WmiConn.ServerName, WmiConn.ErrorMessage)
         Exit Sub
      End If
      '* Check if WMI connection to remote is active
      If WmiConn.WmiEnabled = False Then
         Console.WriteLine("Could not connect WMI with \{0}{1} ", _
           WmiConn.ServerName, WmiConn.WmiNamespace)
      Else
         '* Display the connection to WMI namespace
         Console.WriteLine("WMI connection with {0} established: {1}", _
           WmiConn.ServerName, WmiConn.wmiNameSpace)
         '* show operating system version running the (remote) WMI server
         Console.WriteLine("O.S. : {0} ", wmiconn.OperatingSystem)
         '* get all MSI installed programs (can take a while)
         Console.WriteLine("Retrieving information. Please wait......")
         '* Create storage for the query results
         Dim moc As System.Management.ManagementObjectCollection
         '* Instruct the (remote) WMI server to execute query
         moc = WmiConn.ExecWmiQuery("Select * From Win32_Product")
         '* when found, display program-names
         If Not moc Is Nothing Then
            Console.WriteLine(" - Programs installed on {0} -", _
              wmiConn.ServerName)
            Dim mo As System.Management.ManagementObject
            For Each mo In moc
               Console.WriteLine( mo.GetPropertyValue("Caption"))
            Next
            Console.WriteLine(" - End of list -")
         End If
      End If
You can download the demonstration copy of the Connection Tester solution from the next page.
 

In Conclusion...

When you're new to WMI, I suggest you start with ( aside from a lot of reading up) downloading CIM-Studio and ScriptoMatic 2.0. These two utilities will provide you with an easy way to explore the WMI namespaces, classes and objects. Although script-driven, links are provided at the end of this article. Also download the WMI SDK for the documentation.

As I told you before, this was my first involvement in WMI. As such, I can only conclude that my way of coding works (or not) but I can not vouch for doing it the proper WMI way. WMI delivers a very powerful functionality , but ONLY when all (sub)systems involved (like DCOM, RPC, SMNP, Firewall, Group Policies, etc) are working in perfect harmony. If not working in perfect harmony, you easily find yourself looking for the needle in the proverbial haystack.

I will give you an example: during the development of the printer monitor program, I was forced to poll the printerservers while WMI offers a lot of goodies I'd like to use instead, like the ManagementEventWatcher class. This class triggers an event when a selected event (like the creation of a print job) took place. My intention was to create a service for printerservers which would inform my monitor program on printer-states and printing progress. As WMI contains the possibility to remotely create and start services and processes (so manual installation of my service was not required: no-touch deployment!) this approach would also reduce the network traffic to the minimum and free up resources on the computer running the monitor program. Programmers heaven, one should think.

Alas, after two weeks of struggling with the ManagementEventWatcher class I still got a 'method not supported' exception when I tried to invoke the ManagementEventWatcher.Start() method with VB.NET code. After having scavenged the WMI logfiles (stored in \windows\system32\wbem) , MSDN, forums, newsgroups and other Internet resources on this topic I gave up and concluded that I stumbled upon yet another OS dependency. So I had to revert to the polling approach to retain compatibility with other versions of Windows. Maybe an actual Windows server is needed to make use of this class in VB.NET, I do not know. Now when I'm wrong or overlooking the obvious, I'll happily stand corrected: - emdek@vbcity.com .

  Now, before you start hitting the e-mail button, let me show you the signature of one of VBCity's oldest (membership-wise that is) and esteemed members: CanOz. He selected a quote from George Bernard Shaw, which I find totally spot on:

    I am not a teacher
    only a fellow traveller of whom you asked the way.
    I pointed ahead
    ahead of myself as well as of you.

Say no more....

List of Helpful Links:

Using WMI with the .NET Framework

Accessing Management Information with System.Management

Operating System Availability of WMI Components

WMI Win32 Classes

WMI SDK

WMI Administrative Tools (including CIM studio)

ScriptoMatic 2.0