Freitag, 25. März 2011

WMI in C++ - Query everyting from your OS!

Hi everyone

In this article i will give you some information how you can dig into the deep waters of WMI which is a part of the Windows API that is designed to give feedback to queries about software and hardware specifications of the windows system the calling application is executing on or even on remote machines. This goes from simple things like the version of the installed windows over installed software and running processors up to very detailed stuff like the number of cycles the fan of your cooler (if you have a fan cooling the CPU) made so far.

WMI is designed to act like a huge database system. Such a system contains databases, tables and rows. Thats also what WMI provides us. We can select objects from tables using a SQL like syntax. For example the following would be a legit query in WMI:
SELECT * FROM Win32_Session;

In WMI databases are not called databases but namespaces. On your system there may be various namespaces available. One with very interesting tables is the namespace "CIMV2". It contains tables for nearly everything that acts on your computer! As with any other database you first need to connect to it. This is done using the interface IWbemLocator. This interface (and all subsequent ones) are defined in the header file WbemCli.h. The CLSID for those interfaces are defined in wbemuuid.lib. So this results in our first code snippet:
#include <Windows.h>
#include <iostream>
#include <WbemCli.h>

#pragma comment(lib, "wbemuuid.lib")

int main()
{
 using std::cout;
 using std::cin;
 using std::endl;

 HRESULT hRes = CoInitializeEx(NULL, COINIT_MULTITHREADED);
 if(FAILED(hRes))
 {
  cout << "Unable to launch COM: 0x" << std::hex << hRes << endl;
  return 1;
 }

 IWbemLocator* pLocator = NULL;
 if(FAILED(hRes = CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pLocator))))
 {
  cout << "Unable to create a WbemLocator: " << std::hex << hRes << endl;
  return 1;
 }


 pLocator->Release();
 return 0;
}

Thats just some COM-stuff, nothing special. The IWbemLocator interface now exposes a method called ConnectServer. This function connects to a namespace. We will be using CIMV2 so thats where we will connect to. As a result of that function we get an interface called IWbemServices. Imagine that like an open connection to the database. As i stated above you can also connect to namespace on a different machine. So the ConnectServer accepts also a username, a password and an authority to connect. As we will just use the current user on the local machine we dont need to supply that and can pass NULL to all of them and we set root as the location where the namespace should be searched. Its important to specify WBEM_FLAG_CONNECT_USE_MAX_WAIT as flag to indicate that the function should abort after MAX_WAIT time has passed. So we can prevent that the function never returns if the remote host for example is not ready.

Here is some more code:
#include <Windows.h>
#include <iostream>
#include <WbemCli.h>

#pragma comment(lib, "wbemuuid.lib")

int main()
{
 using std::cout;
 using std::cin;
 using std::endl;

 HRESULT hRes = CoInitializeEx(NULL, COINIT_MULTITHREADED);
 if(FAILED(hRes))
 {
  cout << "Unable to launch COM: 0x" << std::hex << hRes << endl;
  return 1;
 }

 IWbemLocator* pLocator = NULL;
 if(FAILED(hRes = CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pLocator))))
 {
  cout << "Unable to create a WbemLocator: " << std::hex << hRes << endl;
  return 1;
 }

 IWbemServices* pService = NULL;
 if(FAILED(hRes = pLocator->ConnectServer(L"root\\CIMV2", NULL, NULL, NULL, WBEM_FLAG_CONNECT_USE_MAX_WAIT, NULL, NULL, &pService)))
 {
  pLocator->Release();
  cout << "Unable to connect to \"CIMV2\": " << std::hex << hRes << endl;
  return 1;
 }

 pService->Release();
 pLocator->Release();
 return 0;
}

Now we are ready to query information from the CIMV2 namespace! This is done using the function ExecQuery from the IWbemService interface. This function will return an enumerator which enumerates all objects that match the query. This enumerator can be speed up if it must not be a bidirectional enumerator. Therefore if you dont need it its good to specify that in ExecQuery. The first parameter of ExecQuery theoretically would allow to specify the type of language that is used in the query but at the moment the only valid value is WQL. So we get the following code.
#include <Windows.h>
#include <iostream>
#include <WbemCli.h>

#pragma comment(lib, "wbemuuid.lib")

int main()
{
 using std::cout;
 using std::cin;
 using std::endl;

 HRESULT hRes = CoInitializeEx(NULL, COINIT_MULTITHREADED);
 if(FAILED(hRes))
 {
  cout << "Unable to launch COM: 0x" << std::hex << hRes << endl;
  return 1;
 }

 IWbemLocator* pLocator = NULL;
 if(FAILED(hRes = CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pLocator))))
 {
  cout << "Unable to create a WbemLocator: " << std::hex << hRes << endl;
  return 1;
 }

 IWbemServices* pService = NULL;
 if(FAILED(hRes = pLocator->ConnectServer(L"root\\CIMV2", NULL, NULL, NULL, WBEM_FLAG_CONNECT_USE_MAX_WAIT, NULL, NULL, &pService)))
 {
  pLocator->Release();
  cout << "Unable to connect to \"CIMV2\": " << std::hex << hRes << endl;
  return 1;
 }

 IEnumWbemClassObject* pEnumerator = NULL;
 if(FAILED(hRes = pService->ExecQuery(L"WQL", L"SELECT * FROM Win32_DesktopMonitor", WBEM_FLAG_FORWARD_ONLY, NULL, &pEnumerator)))
 {
  pLocator->Release();
  pService->Release();
  cout << "Unable to retrive desktop monitors: " << std::hex << hRes << endl;
  return 1;
 }

 pEnumerator->Release();
 pService->Release();
 pLocator->Release();
 return 0;
}

If you compile that code and run it you will most likely run into problems because it will print Unalbe to retrieve desktop monitors. Thats because all those objects are encapsulated into security levels. At the moment our application has not chosen any security level and is therefore assigned to the lowest. From this layer we cannot access that object! In order to do that we call the function CoInitializeSecurity right after CoInitialiize. Please refer to MSDN for a full description of security levels (until i wrote an article about that). Just believe that for the current operation (and most common ones) the following is fairly enough:
CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_CONNECT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, 0);

Adding that after CoInitialize should remove the error encountered above! That means we are now ready to enumerate through the objects! The enumerator is pretty easy to handle. IEnumWbemClassObjects exposes a method called Next. This method request a chunk of objects from the collection and returns how many objects were extracted. Also here its important to know that these objects could be on another machine. So Next also expects a parameter indicating how long it should wait for the objects to respond. As we are on our own computer i specify INFINITE. That yields the following code:
#include <Windows.h>
#include <iostream>
#include <WbemCli.h>

#pragma comment(lib, "wbemuuid.lib")

int main()
{
 using std::cout;
 using std::cin;
 using std::endl;

 HRESULT hRes = CoInitializeEx(NULL, COINIT_MULTITHREADED);
 if(FAILED(hRes))
 {
  cout << "Unable to launch COM: 0x" << std::hex << hRes << endl;
  return 1;
 }

 if((FAILED(hRes = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_CONNECT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, 0))))
 {
  cout << "Unable to initialize security: 0x" << std::hex << hRes << endl;
  return 1;
 }

 IWbemLocator* pLocator = NULL;
 if(FAILED(hRes = CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pLocator))))
 {
  cout << "Unable to create a WbemLocator: " << std::hex << hRes << endl;
  return 1;
 }

 IWbemServices* pService = NULL;
 if(FAILED(hRes = pLocator->ConnectServer(L"root\\CIMV2", NULL, NULL, NULL, WBEM_FLAG_CONNECT_USE_MAX_WAIT, NULL, NULL, &pService)))
 {
  pLocator->Release();
  cout << "Unable to connect to \"CIMV2\": " << std::hex << hRes << endl;
  return 1;
 }

 IEnumWbemClassObject* pEnumerator = NULL;
 if(FAILED(hRes = pService->ExecQuery(L"WQL", L"SELECT * FROM Win32_DesktopMonitor", WBEM_FLAG_FORWARD_ONLY, NULL, &pEnumerator)))
 {
  pLocator->Release();
  pService->Release();
  cout << "Unable to retrive desktop monitors: " << std::hex << hRes << endl;
  return 1;
 }

 IWbemClassObject* clsObj = NULL;
 int numElems;
 while((hRes = pEnumerator->Next(WBEM_INFINITE, 1, &clsObj, (ULONG*)&numElems)) != WBEM_S_FALSE)
 {
  if(FAILED(hRes))
   break;

  clsObj->Release();
 }

 pEnumerator->Release();
 pService->Release();
 pLocator->Release();
 return 0;
}

Now finally we have a IWbemClassObject that exposes the properties of the selected object. This is done using the Get function. The usage is pretty simple! We query the Description of the monitor in this example:
#include <Windows.h>
#include <iostream>
#include <WbemCli.h>

#pragma comment(lib, "wbemuuid.lib")

int main()
{
 using std::cout;
 using std::cin;
 using std::endl;

 HRESULT hRes = CoInitializeEx(NULL, COINIT_MULTITHREADED);
 if(FAILED(hRes))
 {
  cout << "Unable to launch COM: 0x" << std::hex << hRes << endl;
  return 1;
 }

 if((FAILED(hRes = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_CONNECT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, 0))))
 {
  cout << "Unable to initialize security: 0x" << std::hex << hRes << endl;
  return 1;
 }

 IWbemLocator* pLocator = NULL;
 if(FAILED(hRes = CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pLocator))))
 {
  cout << "Unable to create a WbemLocator: " << std::hex << hRes << endl;
  return 1;
 }

 IWbemServices* pService = NULL;
 if(FAILED(hRes = pLocator->ConnectServer(L"root\\CIMV2", NULL, NULL, NULL, WBEM_FLAG_CONNECT_USE_MAX_WAIT, NULL, NULL, &pService)))
 {
  pLocator->Release();
  cout << "Unable to connect to \"CIMV2\": " << std::hex << hRes << endl;
  return 1;
 }

 IEnumWbemClassObject* pEnumerator = NULL;
 if(FAILED(hRes = pService->ExecQuery(L"WQL", L"SELECT * FROM Win32_DesktopMonitor", WBEM_FLAG_FORWARD_ONLY, NULL, &pEnumerator)))
 {
  pLocator->Release();
  pService->Release();
  cout << "Unable to retrive desktop monitors: " << std::hex << hRes << endl;
  return 1;
 }

 IWbemClassObject* clsObj = NULL;
 int numElems;
 while((hRes = pEnumerator->Next(WBEM_INFINITE, 1, &clsObj, (ULONG*)&numElems)) != WBEM_S_FALSE)
 {
  if(FAILED(hRes))
   break;

  VARIANT vRet;
  VariantInit(&vRet);
  if(SUCCEEDED(clsObj->Get(L"Description", 0, &vRet, NULL, NULL)) && vRet.vt == VT_BSTR)
  {
   std::wcout << L"Description: " << vRet.bstrVal << endl;
   VariantClear(&vRet);
  }

  clsObj->Release();
 }

 pEnumerator->Release();
 pService->Release();
 pLocator->Release();
 return 0;
}

Thats it, we now successfully accessed our first objects from WMI!

For a full list of classes available please have a look at the following link:
MSDN

In the next part we will have a look on how to call methods inside those objects!

Thanks for reading and bye bye
Yanick

Kommentare:

  1. Excellent explaination.
    I search and read a lot but this is the first time I understand wmi in C++
    the way you explain is so simple.
    Thank you so much.
    I would like to see the next part as your promise.

    reader from USA (vietnamese native)

    AntwortenLöschen
  2. You did it very well, and wonderfully explained it.
    I searched alot to get a good example and yours is the best I've seen.
    continue your good work!

    AntwortenLöschen
  3. This is good but it will not work (returns COM access denied), at least on Windows 2012, without calling this after CoInitializeEx()..

    hr = CoInitializeSecurity(
    NULL,
    -1, // COM authentication
    NULL, // Authentication services
    NULL, // Reserved
    RPC_C_AUTHN_LEVEL_DEFAULT, // Default authentication
    RPC_C_IMP_LEVEL_IMPERSONATE, // Default Impersonation
    NULL, // Authentication info
    EOAC_NONE, // Additional capabilities
    NULL // Reserved
    );

    AntwortenLöschen
  4. Why do i get ""Unable to launch COM: 0x"?!
    Im on windows 7 x64, QtCreator 5.2 Mscv2012!

    AntwortenLöschen