EternalWindows
WMI / インスタンスの列挙

システム管理者にとって、「イベント ビューア」や「信頼性とパフォーマンス モニタ」は非常に便利な存在です。 これらのツールは、イベントログやパフォーマンスカウンタなどからデータを取得しているため、 システムの現在の状態を把握するうえで十分な情報を与えてくれます。 開発者にとって少し複雑となるのは、イベントログやパフォーマンスカウンタがシステムの管理という同じ方向にあるものの、 使用するAPIが相互に異なるという点です。 つまり、イベントログならばEvent Logging API(ReadEventLogなど)を使用し、 パフォーマンスカウンタならばPDH API(PdhAddCounterなど)を使用するという要領であるため、 複数のAPIを理解しなければならず大変といえます。 WMIが目指すのは、こうした管理に使用するAPIを一元化することであり、 どのような情報でも統一的な方法でアクセスできる仕組みを提供しています。 つまり、アプリケーションがWMIを使用すれば、 イベントログやパフォーマンスカウンタ、及びその他の情報をどれも同じような手順で扱えます。 WMIは、DMTF(Distributed Management Task Force)という団体が策定したWBEM(Web-Based Enterprise Management)という仕組みに基づいて設計されています。

WMIを使用するアプリケーションは、WMIプロバイダに対してのクライアントという位置づけです。 WMIプロバイダは何らかの情報を公開しているCOMサーバーであり、 クライアントのためにWMIクラスを定義します。 クライアントはWMI COM APIを使用し、サーバーはWMI COM APIを実装することになりますが、 両者は直接通信しているわけではなく、常にWMIインフラストラクチャを経由しています。 WMIインフラストラクチャが存在するおかけで、クライアントはプロバイダの名前などを前もって理解する必要はなくなります。 後述するように、クライアントが本質的に必要とするのは特定のWMIクラスなわけですが、 そのクラス名をWMI COM APIに使用すれば、WMIインフラストラクチャが適切なプロバイダを選択してくれるようになっているからです。 WMIインフラストラクチャはリポジトリと呼ばれる領域からクラスを検索するため、 プロバイダを開発する際にはこのリポジトリにクラス情報を格納することになります。

WMIには名前空間と呼ばれる概念があり、プロバイダは特定の名前空間に属します。 たとえば、あるプロバイダはroot\cimv2という名前空間に存在するけれども、 別のプロバイダはroot\defaultという名前空間に存在するという要領です。 このため、WMIクライアントが最初にすべきことは使用する名前空間の決定です。 これには、CoCreateInstanceでIWbemLocatorを取得し、ConnectServerを呼び出します。

HRESULT IWbemLocator::ConnectServer(
  const BSTR strNetworkResource,
  const BSTR strUser,
  const BSTR strPassword,
  const BSTR strLocale,
  LONG lSecurityFlags,
  const BSTR strAuthority,
  IWbemContext *pCtx,
  IWbemServices **ppNamespace
);

strNetworkResourceは、接続する名前空間を表す文字列を指定します。 リモートコンピュータに接続したい場合は、\\servername\root\\cimv2のようにします。 strUserは、接続に使用するユーザー名を指定します。 strPasswordは、接続するユーザーのパスワードを指定します。 strLocaleは、ローケルを表す文字列を指定します。 lSecurityFlagsは、リモートコンピュータが応答しない場合に関するフラグを指定します。 strAuthorityは、認証に使用するプロトコルを指定します。 pCtxは、NULLで問題ありません。 ppNamespaceは、IWbemServicesを受け取る変数のアドレスを指定します。

ConnectServerが認証に関する引数を要求するのは、プロバイダとの通信がDCOMを通じて行われているからです。 プロバイダの正体はCOMサーバーであり、それはdllかexeかの形態をとります。 dllの場合はクライアントのアドレス空間にロードされて、プロバイダと直接通信できるようにも思えますが、 実際にはそのようになっていません。 プロバイダが使用されることを検出したWMIインフラストラクチャは、WMI Provider Hostと呼ばれるwmiprvse.exeを起動し、 このプロセスがプロバイダをロードすることになっているのです。 たとえば、後述するIWbemServices::CreateInstanceEnumにWin32_OperatingSystemを指定して実行すれば、 wmiprvse.exeが実行されていることを確認でき、 そのアドレス空間にはWin32_OperatingSystemを実装するcimwin32.dllがロードされているはずです。

プロバイダの役割は、クライアントに何らかの情報を返すことです。 こうした情報はプロバイダ内でクラスとして管理され、たとえばWin32_Processならば、プロセスに関する情報を定義しています。 実際にクラスの情報を持つオブジェクトはインスタンスと呼ばれ、 たとえばプロセスのインスタンスは、現在実行されているプロセスの数だけ存在します。 こうしたインスタンスを列挙したい場合は、IWbemServices::CreateInstanceEnumを呼び出します。

HRESULT IWbemServices::CreateInstanceEnum(
  const BSTR strClass,
  LONG lFlags,
  IWbemContext *pCtx,
  IEnumWbemClassObject **ppEnum
);

strClassは、クラスを表す文字列を指定します。 このクラスのインスタンスが列挙されることになります。 lFlagsは、このメソッドの動作に影響するフラグを指定します。 WBEM_FLAG_RETURN_IMMEDIATELYとWBEM_FLAG_FORWARD_ONLYを指定するのが、 パフォーマンス的に一番よいとされています。 pCtxは、NULLで問題ありません。 ppEnumは、IEnumWbemClassObjectを受け取る変数のアドレスを指定します。

IEnumWbemClassObject::Nextを呼び出せば、実際にインスタンスを取得できます。 インスタンスはIWbemClassObjectで表すことになり、 Getメソッドを呼び出せば特定のプロパティを取得できます。 適切なプロパティを指定するために、クラスの定義を予め確認しておく必要があります。

class Win32_Process : CIM_Process
{
  string   Caption;
  string   CommandLine;
  ・
  ・
  ・
  uint64   WriteOperationCount;
  uint64   WriteTransferCount;
};

MSDNでクラス名を検索すれば、上記のように定義が確認できると思われます。 Win32_Processであればstring型のCaptionというプロパティが存在するということであり、 このCaptionという文字列をプロパティ取得の際に指定することになります。

今回のプログラムは、現在起動されているプロセスの名前をMessageBoxで順に表示します。

#include <windows.h>
#include <wbemidl.h>

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT              hr;
	IWbemLocator         *pLocator;
	IWbemServices        *pNamespace;
	IEnumWbemClassObject *pEnumerator;
	IWbemClassObject     *pObject;
	VARIANT              var;
	ULONG                uReturned;
	BSTR                 bstrNamespace, bstrFilter;

	CoInitializeEx(0, COINIT_MULTITHREADED);
	CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL);

	CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pLocator));
	
	bstrNamespace = SysAllocString(L"root\\cimv2");
	hr = pLocator->ConnectServer(bstrNamespace, NULL, NULL, NULL, 0, NULL, NULL, &pNamespace);
	if (FAILED(hr)) {
		SysFreeString(bstrNamespace);
		pLocator->Release();
		return 0;
	}
	
	hr = CoSetProxyBlanket(pNamespace, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE);

	bstrFilter = SysAllocString(L"Win32_Process");
	pNamespace->CreateInstanceEnum(bstrFilter, WBEM_FLAG_RETURN_IMMEDIATELY | WBEM_FLAG_FORWARD_ONLY, NULL, &pEnumerator);
	
	for (;;) {
		hr = pEnumerator->Next(WBEM_INFINITE, 1, &pObject, &uReturned);
		if (hr != S_OK)
			break;
		pObject->Get(L"Caption", 0, &var, NULL, NULL);
		MessageBoxW(NULL, var.bstrVal, L"OK", MB_OK);
		VariantClear(&var);
		pObject->Release();
	}

	SysFreeString(bstrFilter);
	SysFreeString(bstrNamespace);
	pNamespace->Release();
	pLocator->Release();
	CoUninitialize();

	return 0;
}

CoInitializeSecurityやCoSetProxyBlanketといった関数を呼び出しているのは、WMIがDCOMを使用した通信になっているからです。 DCOMは内部でRPCを使用しており、RPCの通信にはセキュリティ情報を指定しなければならないため、CoInitializeSecurityを呼び出すようにしています。 これにより、アプリケーション内で使用されるインターフェースには、CoInitializeSecurityに指定されたセキュリティ情報が反映されますが、 特定のインターフェースだけはセキュリティ情報を変更したいということがあるかもしれません。 たとえば、このインターフェースを使用する場合に限っては、データを暗号化して送信したいといった要領です。 こうした個別の設定はCoSetProxyBlanketで行うようになっており、 MSDNのWMIのサンプルではpNamespaceにだけ個別の設定を行うようになっていたため、これを指定しています。 WMIクライアントのスレッドはMTAとして動作しなければならないため、CoInitializeExにCOINIT_MULTITHREADEDを指定する点も重要です。

WMI関連の処理については、まずIWbemLocator::ConnectServerで名前空間に接続します。 ローカルの名前空間に接続する場合は、第2引数と第3引数はNULLで構いません。 IWbemServicesを取得したら、CreateInstanceEnumを呼び出すことでクラスのインスタンスを列挙します。 今回はクラス名としてWin32_Processを指定しているため、列挙されるのはプロセスのインスタンスになります。

for (;;) {
	hr = pEnumerator->Next(WBEM_INFINITE, 1, &pObject, &uReturned);
	if (hr != S_OK)
		break;
	pObject->Get(L"Caption", 0, &var, NULL, NULL);
	MessageBoxW(NULL, var.bstrVal, L"OK", MB_OK);
	VariantClear(&var);
	pObject->Release();
}

IEnumWbemClassObject::Nextを呼び出せば、インスタンスを表すIWbemClassObjectを取得できます。 Getを呼び出せば第1引数の名前で識別できるプロパティを取得でき、 Win32_ProcessにはCaptionというプロパティが定義されていたため、これは成功するはずです。 プロパティの値はVARIANT構造体として返され、string型のプロパティはデータ型がVT_BSTRになります。 よって、bstrValメンバから文字列を参照できます。


戻る