EternalWindows
WMI / プロバイダの実装

前節で述べたように、WMIプロバイダの正体はCOMサーバーです。 つまり、インプロセスサーバー(DLL)かアウトプロセスサーバーかのどちらかの形態をとり、 内部でCOMオブジェクトを作成することになります。 このオブジェクトの型となるクラスは、次のように定義しています。

class CWbemProvider : public IWbemProviderInit, public IWbemServices
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();

	STDMETHODIMP Initialize(LPWSTR wszUser, LONG lFlags, LPWSTR wszNamespace, LPWSTR wszLocale, IWbemServices *pNamespace, IWbemContext *pCtx, IWbemProviderInitSink *pInitSink);

	STDMETHODIMP OpenNamespace(const BSTR strNamespace, long lFlags, IWbemContext *pCtx, IWbemServices **ppWorkingNamespace, IWbemCallResult **ppResult);
	・
	・
	・
	STDMETHODIMP ExecMethodAsync(const BSTR strObjectPath, const BSTR strMethodName, long lFlags, IWbemContext *pCtx, IWbemClassObject *pInParams, IWbemObjectSink *pResponseHandler);	
	
	CWbemProvider();
	~CWbemProvider();
	void SampleMethod(IWbemClassObject *pInParams, IWbemClassObject *pOutParams);
	
private:
	LONG          m_cRef;
	IWbemServices *m_pNamespace;
};

InitializeがIWbemProviderInitのメソッドであり、ShutdownがIWbemShutdownのメソッド、 OpenNamespaceからExecMethodAsyncまでがIWbemServicesのメソッドになります。 IWbemServicesはクライアントも使用するインターフェースですが、 そこで呼び出したメソッドが、そのままプロバイダのメソッドにリンクするとは限らないことに注意してください。 これは、クライアントとプロバイダの間にCIMマネージャが存在するからであり、 たとえばクライアントがIWbemServices::ExecMethodを呼び出しても、 プロバイダのIWbemServices::ExecMethodが呼ばれるようにはなっていません。 実際に呼ばれるのはExecMethodAsyncになっています。

IWbemProviderInit::Initializeの実装は次のようになっています。

STDMETHODIMP CWbemProvider::Initialize(LPWSTR wszUser, LONG lFlags, LPWSTR wszNamespace, LPWSTR wszLocale, IWbemServices *pNamespace, IWbemContext *pCtx, IWbemProviderInitSink *pInitSink)
{
	if (pNamespace != NULL) {
		m_pNamespace = pNamespace;
		m_pNamespace->AddRef();
		pInitSink->SetStatus(WBEM_S_INITIALIZED, 0);
		
	}
	else
		pInitSink->SetStatus(WBEM_E_FAILED, 0);

	return WBEM_S_NO_ERROR;
}

Initializeで渡されるpNamespaceは、別のメソッドで必要になるためメンバとして保存します。 適切に初期化されている場合はSetStatusにWBEM_S_INITIALIZEDを指定しますが、 NULLである場合はWBEM_E_FAILEDを指定します。

プロバイダを使用したクライアントが終了しても、参照カウントが0になるのはReleaseが呼ばれるのは一定時間経ってからです。 プロバイダがdllの場合はwmiprvse.exeにロードされているはずですが、このexeが終了するのはReleaseが呼ばれてまたしばらく経ってからです。 プロバイダがWindows XPから登場したIWbemShutdownを実装している場合は、 最後のReleaseの前にIWbemShutdown::Shutdownが呼ばれます。

今回のプロバイダはInstance providerであり、SupportsEnumerationにはTRUEを指定していました。 これはつまり、クライアントがプロバイダのクラスの名前を指定して、IWbemServices::CreateInstanceEnumやIWbemServices::ExecQueryを呼び出した場合、 プロバイダのIWbemServices::CreateInstanceEnumAsyncが呼ばれることを意味します。

STDMETHODIMP CWbemProvider::CreateInstanceEnumAsync(const BSTR strFilter, long lFlags, IWbemContext *pCtx, IWbemObjectSink *pResponseHandler)
{
	int              i;
	IWbemClassObject *pClass = NULL;
	IWbemClassObject *pNewInst[100];
	VARIANT          var;

	for (i = 0; i < g_nCount; i++) {
		m_pNamespace->GetObject(strFilter, 0, pCtx, &pClass, NULL);
		pClass->SpawnInstance(0, &pNewInst[i]);
		pClass->Release();
		
		var.vt = VT_I4;
		var.lVal = g_data[i].nId;
		pNewInst[i]->Put(L"id", 0, &var, 0);

		var.vt = VT_BSTR;
		var.bstrVal = SysAllocString(g_data[i].szData);
		pNewInst[i]->Put(L"data", 0, &var, 0);
		VariantClear(&var);
	}	

	pResponseHandler->Indicate(g_nCount, pNewInst);

	pResponseHandler->SetStatus(0, WBEM_S_NO_ERROR, NULL, NULL);

	return WBEM_S_NO_ERROR;
}

DATA構造体として定義されているg_dataが、プロバイダの中に存在するインスタンスです。 つまり、CreateInstanceEnumAsyncで行うべきことは、g_data[0]やg_data[1]をWMIが定める方法で返すことです。 strFilterにはクラスの名前(今回の場合)が格納されているため、 まずこれを表すインターフェースをGetObjectで取得します。 次に、SpawnInstanceを呼び出してクラスのインスタンスを作成し、 このインスタンスに対してg_data[i]の中身をコピーします。 MOFファイル上ではクラスはidとdataというプロパティが存在していましたから、 これらに対してIWbemClassObject::Putを実行します。 idは型がuint32であったためVARIANT構造体はVT_I4で初期化し、 dataは型がstringであるためVT_BSTRで初期化します。 pResponseHandlerにはクライアントに対してレスポンスを返すために使用します。 Indicateの第1引数はクライアントに返したいインスタンスを指定でき、 この数だけ第2引数の配列の要素がクライアントに返されます。 SetStatusの呼び出しによって、実際にクライアントが制御を返します。

今回のプロバイダはInstance providerであると同時に、Method providerでもありました。 このため、クライアントがIWbemServices::ExecMethodを呼び出した場合は、 プロバイダのIWbemServices::ExecMethodAsyncが呼ばれます。

STDMETHODIMP CWbemProvider::ExecMethodAsync(const BSTR strObjectPath, const BSTR strMethodName, long lFlags, IWbemContext *pCtx, IWbemClassObject *pInParams, IWbemObjectSink *pResponseHandler)
{
	HRESULT          hr;
	IWbemClassObject *pClass = NULL;
	IWbemClassObject *pOutClass = NULL;
	IWbemClassObject *pOutParams = NULL;
	BSTR             bstrClassName;
	
	bstrClassName = SysAllocString(g_szClassName);
	m_pNamespace->GetObjectW(bstrClassName, 0, pCtx, &pClass, NULL);
	if (pClass == NULL) {
		SysFreeString(bstrClassName);
		pResponseHandler->SetStatus(0, WBEM_E_NOT_SUPPORTED, NULL, NULL);
		return WBEM_E_NOT_SUPPORTED;
	}

	pClass->GetMethod(strMethodName, 0, NULL, &pOutClass);
	pOutClass->SpawnInstance(0, &pOutParams);

	if (lstrcmpW(L"SampleMethod", strMethodName) == 0) {	
		SampleMethod(pInParams, pOutParams);
		pResponseHandler->Indicate(1, &pOutParams);
		hr = WBEM_S_NO_ERROR;
	}
	else
		hr = WBEM_E_NOT_SUPPORTED;

	pResponseHandler->SetStatus(0, hr, NULL, NULL);

	pOutParams->Release();
	pOutClass->Release();
	pClass->Release();
	SysFreeString(bstrClassName);

	return hr;
}

このメソッドで最終的にすべきことは、IWbemObjectSink::Indicateを呼び出してクライアントに返したい引数を指定することです。 SampleMethodではクライアントに返す引数が1つでしたから、 Indicateの第1引数には1を指定し、第2引数には1つのpOutParamsを指定しています。 このpOutParamsは、まずIWbemServices::Namespace->GetObjectWでクラスを取得し、 次にGetMethodでメソッドを取得、そしてSpawnInstanceを呼び出すことで作成できます。 strMethodNameが"SampleMethod"と一致した場合は、クライアントがSampleMethodを呼び出そうとしていることを意味しているため、 この場合にSampleMethodを呼び出すようにします。 サポートするメソッドが増える場合は、if文の数も増えることになるでしょう。 サポートしないメソッドが指定された場合はWBEM_E_NOT_SUPPORTEDを指定していますが、 定義されていないメソッドを呼び出す時点で、CIMマネージャからWBEM_E_NOT_FOUNDが返るため、 else文が実行されることはおそらくないでしょう。 IWbemObjectSink::SetStatusを呼び出すことによって、クライアントのIWbemServices::ExecMethodが制御を返すようになります。

SampleMethodの実装は次のようになっています。

void CWbemProvider::SampleMethod(IWbemClassObject *pInParams, IWbemClassObject *pOutParams)
{
	BSTR    bstrInputArgName = SysAllocString(L"strIn");
	BSTR    bstrOutputArgName = SysAllocString(L"strOut");
	BSTR    bstrRetValName = SysAllocString(L"ReturnValue");
	VARIANT varIn, varOut, varRet;
	BSTR    bstr;
	LONG    lRetValue;

	VariantInit(&varIn);
	pInParams->Get(bstrInputArgName, 0, &varIn, NULL, NULL);
	if (varIn.vt == VT_BSTR) {
		bstr = varIn.bstrVal;
		CharUpperBuffW(bstr, lstrlenW(bstr));
		
		varOut.vt = VT_BSTR;
		varOut.bstrVal = bstr;
		pOutParams->Put(bstrOutputArgName, 0, &varOut, 0);

		lRetValue = lstrlenW(bstr);

		VariantClear(&varIn);
		VariantClear(&varOut);
	}
	else
		lRetValue = -1;

	varRet.vt = VT_I4;
	varRet.lVal = lRetValue;
	pOutParams->Put(bstrRetValName, 0, &varRet, 0);
	
	SysFreeString(bstrInputArgName);
	SysFreeString(bstrOutputArgName);
	SysFreeString(bstrRetValName);
}

pInParamsがクライアントから渡されたデータであり、pOutParamsがクライアントに返すデータです。 渡されたデータから実際に値を取得するために、まずpInParams->Getを呼び出します。 このとき、第1引数には取得したい引数の名前を指定します。 SampleMethodにはstrInという引数がありましたからこれを指定します。 strInの方はBSTRであったため、取得した引数の型がVT_BSTRである場合のみ具体的な処理に入ります。 まず、SampleMethodの本来の役割を行う必要がありますが、 これは渡された文字列を全て大文字にするというものでした。 よって、CharUpperBuffを呼び出せば容易に実現できます。 次に、この変換した文字列をクライアントに返すために、pOutParams->Putを呼び出します。 クライアントに文字列を返す引数の名前はstrOutであったため、これを第1引数に指定します。 strOutの型はstringでしたから、第3引数に指定する値はVT_BSTRでなければなりません。 最後に、メソッドの戻り値を決定するために再びpOutParams->Putを呼び出します。 このとき、第1引数にはReturnValueを指定します。

実用的なメソッドを実装しようとした場合、プロバイダはファイルなどのリソースにアクセスする必要があるかもしれません。 こうした場合に注意しなければならないのは、リソースへのアクセスはクライアントのアカウントで行うべきという点です。 たとえば、プロバイダがSYSTEMアカウントで動作している場合は、あらゆるリソースのアクセスに成功することになりますから、 クライアントのアカウントならば本来失敗する動作も成功してしまい、危険といえます。 これを防ぐためには、プロバイダが偽装という仕組みを通じて、クライアントのアカウントで動作する必要があります。

void Method()
{
	CoImpersonateClient();
	if (GetCurrentImpersonationLevel() < RPC_C_IMP_LEVEL_IMPERSONATE) {
		CoRevertToSelf();
		return;
	}

	// メソッドの処理を実行

	CoRevertToSelf();
}

DWORD GetCurrentImpersonationLevel()
{
	HANDLE                       hToken;
	SECURITY_IMPERSONATION_LEVEL level;
	DWORD                        dwLevel, dwLength;
	
	OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &hToken);

	GetTokenInformation(hToken, TokenImpersonationLevel, &level, sizeof(SECURITY_IMPERSONATION_LEVEL), &dwLength);
	if (level == SecurityAnonymous)
		dwLevel = RPC_C_IMP_LEVEL_ANONYMOUS;
	else if (level == SecurityIdentification)
		dwLevel = RPC_C_IMP_LEVEL_IDENTIFY;
	else if (level == SecurityImpersonation)
		dwLevel = RPC_C_IMP_LEVEL_IMPERSONATE;
	else if (level == SecurityDelegation)
		dwLevel = RPC_C_IMP_LEVEL_DELEGATE;
	else
		dwLevel = RPC_C_IMP_LEVEL_ANONYMOUS;

	CloseHandle(hToken);

	return dwLevel;
}

クライアントを偽装する関数にはいくつかの種類がありますが、 COMによる通信を行っている場合はCoImpersonateClientを呼び出します。 偽装はCoRevertToSelfを呼び出すまで続くため、この間にリソースへアクセスする処理を行います。 GetCurrentImpersonationLevelという自作関数を呼び出しているのは、 クライアントがCoInitializeSecurityの第6引数で指定した偽装レベルを取得するためです。 このレベルがRPC_C_IMP_LEVEL_IMPERSONATEより低い場合は、リソースにアクセスしようとしてもERROR_BAD_IMPERSONATION_LEVELが返って失敗してしまうため、 この時点で処理を続行しないようにしています。

プロバイダの処理がどこまで進んだかを確認したい場合があるかもしれません。 MessageBoxを呼び出せば簡単なようにも思えますが、プロバイダは既定で非対話のウインドウステーションで実行されるため、 これは現在のデスクトップに表示されることはありません。 UIを表示したい場合は、WTSSendMessageを呼び出すようにします。

void ShowMessage(LPTSTR lpszMessage, LPTSTR lpszTitle)
{
	DWORD dwSessionId, dwMessageSize, dwTitleSize, dwResult;

	dwMessageSize = (lstrlen(lpszMessage) + 1) * sizeof(TCHAR);
	dwTitleSize = (lstrlen(lpszTitle) + 1) * sizeof(TCHAR);
	dwSessionId = WTSGetActiveConsoleSessionId();
	WTSSendMessage(WTS_CURRENT_SERVER_HANDLE, dwSessionId, lpszTitle, dwTitleSize, lpszMessage, dwMessageSize, MB_OK, 0, &dwResult, FALSE);
}

独自のプロバイダを開発しても、それを使用するのが独自のクライアントに限られないことを意識すべきです。 たとえば、システムが起動されてログオン画面が表示される際には、システム内部でインスタンスの列挙が行われるようです。 これはつまり、プロバイダが実行されるということであり、そのような場面でUIが表示されたら驚きますから、 UIの表示はあくまでテスト段階に留めるべきでしょう。 WTSSendMessageを呼び出す前に、CoImpersonateClientを呼び出すことは避けてください。


戻る