EternalWindows
WMI / イベントの受信

WMIは通常のアプリケーションだけでなく、VBScriptのようなスクリプト言語にとっても非常に有用です。 スクリプト言語ではWindows APIを直接呼び出すことができないため、 Windowsの機能を使用するためにはWMIが不可欠になるのです。 むしろ、WMIはスクリプト言語で使用されることが多く、 通常のアプリケーションならばWMIではなく、専用のAPIを呼び出した方が効率的な場面も多いでしょう。 たとえば、プロセスを列挙するのであれば、EnumProcessesのようなAPIを呼び出した方が遥かに簡単です。 ただし、WMIがサポートするイベントの通知機能は、専用のAPIにおいてもサポートされていないことがあるため、 このようなときにはWMIを使用するのが便利です。 イベントの通知を非同期に受け取りたいアプリケーションは、IWbemServices::ExecNotificationQueryAsyncを呼び出します。

HRESULT IWbemServices::ExecNotificationQueryAsync(
  const BSTR strQueryLanguage,
  const BSTR strQuery,
  long lFlags,
  IWbemContext *pCtx,
  IWbemObjectSink *pResponseHandler
);

strQueryLanguageは、WMIに使用される言語を指定します。 これはWQLでなければなりません。 strQueryは、イベント関連のテキストを含む文字列を指定します。 lFlagsは、WBEM_FLAG_SEND_STATUSでよいと思われます。 pCtxは、NULLで問題ありません。 pResponseHandlerは、イベントが発生した際に通知を受け取るオブジェクトを指定します。

WMIはイベントが発生した場合に、IWbemObjectSinkのメソッドを呼び出すようになっています。 このためには、アプリケーションが用意したオブジェクトがIWbemObjectSinkで識別できなければならないため、 このインターフェースを継承したクラスを定義する必要があります。

class CObjectSink : public IWbemObjectSink
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();
	
	STDMETHODIMP Indicate(LONG lObjectCount, IWbemClassObject **ppObjArray);
	STDMETHODIMP SetStatus(long lFlags, HRESULT hResult, BSTR strParam, IWbemClassObject *pObjParam);

	CObjectSink();

private:
	LONG m_cRef;
};

上記のように、IWbemObjectSinkを継承したクラスを定義します。 イベントが発生した場合は、Indicateが呼ばれます。 エラーが発生した場合やIWbemObjectSink::CancelAsyncCallを呼び出した場合は、SetStatusが呼ばれます。

CObjectSink型のオブジェクトを作成したら、それをExecNotificationQueryAsyncに指定できるように思えますが、 実際にはそう簡単にはいきません。 イベントの通知はプロバイダによって行われるわけですが、そのプロバイダがクライアントとは別プロセスに存在するからです。 つまり、クライアント上で有効なアドレスを指定しても、サーバー上ではそれは無効になってしまうのです。 この問題を解決するため、ExecNotificationQueryAsyncに指定するアドレスは、 内部で適切なリモート処理を行うスタブでなければなりません。 これを作成するには、CoCreateInstanceでIUnsecuredApartmentを取得し、 CreateObjectStubを呼び出します。

HRESULT IUnsecuredApartment::CreateObjectStub(
  IUnknown *pObject,
  IUnknown **ppStub
);

pObjectは、IWbemObjectSinkを実装したオブジェクトを指定します。 ppStubは、スタブを受け取る変数のアドレスを指定します。 これはIUnknownで受け取ることになっているため、 実際にExecNotificationQueryAsyncに指定するためには、 QueryInterfaceでIWbemObjectSinkを照会します。

今回のプログラムは、プロセスの作成を監視します。 プロセスの作成を検出したら、リストボックスにプロセスの名前を表示します。

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

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

class CObjectSink : public IWbemObjectSink
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();
	
	STDMETHODIMP Indicate(LONG lObjectCount, IWbemClassObject **ppObjArray);
	STDMETHODIMP SetStatus(long lFlags, HRESULT hResult, BSTR strParam, IWbemClassObject *pObjParam);

	CObjectSink();

private:
	LONG m_cRef;
};

HWND g_hwndListBox = NULL;

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static IWbemServices   *pNamespace = NULL;
	static IWbemObjectSink *pStubSink = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		HRESULT             hr;
		BSTR                bstrNamespace;
		BSTR                bstrQuery;
		BSTR                bstrLanguage;
		IWbemLocator        *pLocator;
		IUnsecuredApartment *pUnsecApp;
		IUnknown            *pStubUnk;
		CObjectSink         *pObjectSink;

		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 -1;
		}
	
		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);

		hr = CoCreateInstance(CLSID_UnsecuredApartment, NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&pUnsecApp));
		pObjectSink = new CObjectSink;
		pUnsecApp->CreateObjectStub(pObjectSink, &pStubUnk);
		pStubUnk->QueryInterface(IID_IWbemObjectSink, (void **)&pStubSink);
		
		bstrQuery = SysAllocString(L"SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Process'");
		bstrLanguage = SysAllocString(L"WQL");

		pNamespace->ExecNotificationQueryAsync(bstrLanguage, bstrQuery, WBEM_FLAG_SEND_STATUS, NULL, pStubSink);

		SysFreeString(bstrLanguage);
		SysFreeString(bstrQuery);
		SysFreeString(bstrNamespace);
		pObjectSink->Release();
		pStubUnk->Release();
		pUnsecApp->Release();
		pLocator->Release();

		g_hwndListBox = CreateWindowEx(0, TEXT("LISTBOX"), NULL, WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);

		return 0;
	}

	case WM_SIZE:
		MoveWindow(g_hwndListBox, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
		return 0;

	case WM_DESTROY:
		if (pNamespace != NULL) {
			if (pStubSink != NULL) {
				pNamespace->CancelAsyncCall(pStubSink);
				pStubSink->Release();
			}
			pNamespace->Release();
		}
		CoUninitialize();
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}


// CObjectSink


CObjectSink::CObjectSink()
{
	m_cRef = 1;
}

STDMETHODIMP CObjectSink::QueryInterface(REFIID riid, void **ppvObject)
{
	*ppvObject = NULL;

	if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IWbemObjectSink))
		*ppvObject = static_cast<IWbemObjectSink *>(this);
	else
		return E_NOINTERFACE;

	AddRef();
	
	return S_OK;
}

STDMETHODIMP_(ULONG) CObjectSink::AddRef()
{
	return InterlockedIncrement(&m_cRef);
}

STDMETHODIMP_(ULONG) CObjectSink::Release()
{
	if (InterlockedDecrement(&m_cRef) == 0) {	
		delete this;
		return 0;
	}

	return m_cRef;
}

STDMETHODIMP CObjectSink::Indicate(LONG lObjectCount, IWbemClassObject **ppObjArray)
{
	HRESULT          hr;
	VARIANT          var;
	BSTR             bstr;
	IWbemClassObject *pObject;

	bstr = SysAllocString(L"TargetInstance");
	ppObjArray[0]->Get(bstr, 0, &var, 0, 0);
	var.pdispVal->QueryInterface(IID_PPV_ARGS(&pObject));
	VariantClear(&var);

	pObject->Get(L"Caption", 0, &var, 0, 0);

	SendMessage(g_hwndListBox, LB_ADDSTRING, 0, (LPARAM)var.bstrVal);

	SysFreeString(bstr);
	pObject->Release();
	VariantClear(&var);
	
	return WBEM_S_NO_ERROR;
}

STDMETHODIMP CObjectSink::SetStatus(long lFlags, HRESULT hResult, BSTR strParam, IWbemClassObject *pObjParam)
{
	return WBEM_S_NO_ERROR;
}

WM_CREATEでは、WMIに関する一連の処理が行われています。 ExecNotificationQueryAsyncに指定するオブジェクトはスタブでなければならないため、 CObjectSink型のpObjectSinkをIUnsecuredApartment::CreateObjectStubに指定することで、これを作成しています。 IUnsecuredApartmentを取得するには、CoCreateInstanceにCLSID_UnsecuredApartmentを指定しています。 ExecNotificationQueryAsyncに指定しているクエリは次のようになっています。

SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Process'

FROM 句の後には、イベントのクラスを指定できます。 __InstanceCreationEventならばインスタンスの作成を監視することになり、 __InstanceDeletionEventならばインスタンスの破棄を監視することになります。 WITHIN 1というのは、一秒間毎に監視を行うということを意味します。 WHEREの後には条件を指定でき、TargetInstance ISA 'Win32_Process'は、Win32_Processのインスタンスのみを監視対象にすることを意味します。 つまりまとめると、プロセスのインスタンスの作成を一秒間毎に監視するということになります。

イベントが発生した場合は、IWbemObjectSink::Indicateが呼ばれます。 今回の場合であれば、プロセスが作成された場合にIndicateが呼ばれます。

STDMETHODIMP CObjectSink::Indicate(LONG lObjectCount, IWbemClassObject **ppObjArray)
{
	HRESULT          hr;
	VARIANT          var;
	BSTR             bstr;
	IWbemClassObject *pObject;

	bstr = SysAllocString(L"TargetInstance");
	ppObjArray[0]->Get(bstr, 0, &var, 0, 0);
	var.pdispVal->QueryInterface(IID_PPV_ARGS(&pObject));
	VariantClear(&var);

	pObject->Get(L"Caption", 0, &var, 0, 0);

	SendMessage(g_hwndListBox, LB_ADDSTRING, 0, (LPARAM)var.bstrVal);

	SysFreeString(bstr);
	pObject->Release();
	VariantClear(&var);
	
	return WBEM_S_NO_ERROR;
}

渡されたIWbemClassObjectに対してTargetInstanceプロパティを要求します。 ここで得られたIWbemClassObjectは正にイベントの対象であるインスタンスを識別しており、 今回の場合であればWin32_Processのインスタンスを識別しています。 つまり、Getの第1引数にWin32_Processのプロパティを指定でき、 Captionであればプロセス名を取得できます。 このCaptionを使用してif文を記述すれば、 特定のプロセスの監視ということもできますが、 そうしたことをするのであれば、 ExecNotificationQueryAsyncのクエリを次のようにしたほうがよいでしょう。

SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Process' AND TargetInstance.Caption = 'notepad.exe'

この例では、ANDで使用することで条件をさらに絞るようにしています。 つまり、インスタンスがWin32_Processであると共に、そのインスタンスのCaptionがnotepad.exeである場合のみ、 イベントを通知するようにしているのです。 つまり、メモ帳が作成された場合のみ、IWbemObjectSink::Indicateが呼ばれます。

Win32_ProcessStartTraceについて

WMIの優れた要素の1つとしてイベントの発生の監視がありますが、 これがそこまでユニークな方法で実装されていないことは、WQLの内容から想像ができると思われます。 監視する間隔を指定するということはその都度、変化を明示的に確認するということであり、 これならば無理にWMIを使用しなくても監視は可能です。 たとえば、インスタンスを列挙する関数を呼び出して、それが前回の呼び出しと変化しているのならば、 インスタンスの作成や破棄が行われていることは特定できます。 ただ、プロセスの作成という動作に関しては、特別にそれ専用のクラスが用意されています。 このクラスはWin32_ProcessStartTraceという名前を持ち、Windows XPから使用できます。 ただし、管理者として動作していなければ通知が送られないことに注意してください。

bstrQuery = SysAllocString(L"SELECT * FROM Win32_ProcessStartTrace");

見て分かるように、監視する間隔は指定する必要はありません。 プロセスが作成されると、IWbemObjectSink::Indicateが呼ばれます。

STDMETHODIMP CObjectSink::Indicate(LONG lObjectCount, IWbemClassObject **ppObjArray)
{
	HRESULT hr;
	VARIANT var;

	ppObjArray[0]->Get(L"ProcessName", 0, &var, 0, 0);

	SendMessage(g_hwndListBox, LB_ADDSTRING, 0, (LPARAM)var.bstrVal);

	VariantClear(&var);
	
	return WBEM_S_NO_ERROR;
}

IWbemClassObjectはWin32_ProcessStartTraceのインスタンスを識別しており、 GetにはWin32_ProcessStartTraceクラスのプロパティを指定できます。 ProcessNameを指定すれば、プロセスの名前が分かります。



戻る