WMI. Wладение Mагической Iнформацией (ч. 1)

Автор: Виталий Белик

Чего тут писать-то? Про то, как вы думаете, что нужно хорошему системному администратору? «Монога» пива? Ну, это тоже не плохо бы. Но, что еще? Чтобы пользователи никогда не звонили? Ну, а если среди них симпатичные девчонки, к которым приятно нагрянуть для «осмотра» их компушек? :) Нет, все это отмазки. 

Хороший администратор должен иметь в своем арсенале как можно больше инструментов управления подчиненной ему сети. Конечно, такие инструменты существуют и давно, кстати, но большинство из них требуют наличие установленного клиента на целевой машине, а это не всегда кошерно. Можно, но вдруг пользователь запротестует: «…нечего мне на винду ставить хлам». К тому же, не все из этих инструментов бесплатные. Что же делать в таких случаях, когда и нужно и никак? Ответ прост – написать инструмент самому, но на своих условиях игры.

Посвящается моему другу сисадмину Вадиму Фареннику

Как бы не ругали Виндоус – это все таки могучая операционная система, и если уметь владеть ей в полной мере, она станет надежнее плохо настроенного (из-за незнания) Линукса, к примеру. Я веду к тому, что в винде уже встроены механизмы удаленного администрирования. Например, это «Удаленный рабочий стол». Не Радмин, но уже неплохо – стандартный. А мы сегодня поговорим о другом механизме. О механизме, позволяющем пройтись по операционке аки по таблицам базы данных – это WMI…

 

Мы начинаем КВН

Итак, предлагаю начать с того момента, где упоминается эта самая технология. Что это, почему это, что она может? Заглянем в Википедию [1]. Это инструментарий управления Виндоусом. Уже обнадеживает. Три эти слова подразумевают возможность контроля за происходящим в операционной системе, это как раз то, что нужно хорошему администратору.

Читаем дальше: «WMI* представляет собой набор классов, содержащих свойства, доступ к которым открывает информацию о компьютере, операционной системе, их параметрах и установках». Особенную важность уделили динамическому изменению этой информации, поэтому доступ к ней происходит посредством запросов. Получается, что Виндовс выступает в роли системы управления базой данных, откуда мы эту информацию и запрашиваем. Ну, а раз это база данных, согласитесь, было бы просто замечательно, если-бы работа с ней велась на языке, привычном для многих (если не всех) СУБД – SQL.

* Комментарий редакции.

lshw в линуксе. Также в большинстве дистрибутивов можно пользоваться данными из каталога /sys Для этого достаточно обычных sh-скриптов.

Читаем дальше. Опа! Угадали. То ли мы верно догадались, то ли Микрософт просчитал заранее удобство… В любом случае, для получения информации используется язык запросов WQL, являющийся одной из разновидностью SQL. Вот это уже радует. Зная язык SQL, трудно будет запутаться при запросах WMI. Это облегчает задачу, еще и потому что запрос сам по себе является строкой, и может быть введен самим пользователем программы непосредственно во время работы самой программы.

Отличненько. По логике в SQL описывается тот или иной доступ к таблицам и объектам базы данных, но в случае с СУБД программисту известно о том, к каким объектам в БД у него есть доступ, который ставит админ базы данных, либо же сам программист знает, какими финтами обладает его база данных. Как же быть в случае с WMI? Кто скажет: какова структура таблиц? Ой, пардон, речь-то шла о классах… какова структура классов WMI и вообще что они из себя представляют?

Вот эту информацию можно почерпнуть в MSDN [2]. Кстати, счастливые обладатели Visual Studio с MSDN-ом, могут набрать в хелпе в поиске WMI, и получить ту же информацию. Ну, предположим, у нас нет такого хелпа (хотя я лично для себя запасся им), и нам придется напрягать интернет. Ничего. Не так сложно выйдя по ссылке [2], попасть на одну из ветвей, описывающих все это благо, с названием «WMI Service Management Classes», а уж на ней можно выйти на WMI Classes, где собственно описаны все классы, которые можно получить запросом**.

** Комментарий автора.

Уточню, указатели на которых WQL. Вообще поиск на MSDN гнилой, и постоянно выводит не туда куда надо. Поначалу если набрать WMI в поиске полезет документация о MS SQL, и ничего нормального кроме нее. Никакой конкретики. Впрочем, Микрософтовцы любят нагнать пурги поверх растений, чтоб поискали, понимаешь ли, подснежники под снегом…

Ну, да ладно. Билл им судья. Я на всякий случай приведу ссылку, по которой точнее можно выйти на список классов [3]. Не знаю, что с ней случится через год-два, но на момент написания статьи она была актуальна.

Давайте посмотрим, что за информацию нам предлагает Микрософт…430 классов.… Не кисло. Такое впечатление, что о системе, о ее состоянии на момент запроса можно узнать все-все. Это хорошо. Такая информация для администратора иногда оказывается очень ценной.

Ладно. Что может эта загогулина мы теперь знаем. А как получить эти самые классы? То есть объекты, или что там у них… интерфейсы?

Вот тут немного придется мыслить неравномерно. Думаю, многим привычно, что указатели на объекты возвращают конструкторы класса. Однако, в случае с WMI это не совсем так. Здесь бал правит особый провайдер (все-таки получение информации происходит путем взаимодействия с операционкой через язык запросом, а стало быть, должен быть «черный ящичек», обрабатывающий эти запросы) – WBEM. Механизм этот представляет СОМ сервер, который можно использовать в своих программах, предварительно заполучив его инстанцию. Это делается стандартными методами загрузки СОМ сервера. Например, в Делфи это функция CreateOleObject, которая вызывает CoCreateInstance из библиотеки ole32.dll, и возвращает указатель на объект-провайдер, от которого, скормив ему WQL запрос, можно получить указатели на WMI классы.

Его механизм открывает набор-энумератор***. Таким образом, проходя в цикле по динамическому списку, получаем указатели на классы, которые и содержат информацию по запросу. Вообще, если проводить аналогию с базами данных очень легко представить класс WMI как плоскую таблицу, где имена свойств – это имена полей, а их содержимое… нет, не записи, а запись. Один экземпляр класса – одна запись. Таким образом, получается два цикла: один перечисляет список объектов, запрошенного класса, а второй перечисляет поля объекта с информацией.

*** Комментарий автора.

Кто работает в Си с интерфейсами знает, кто не знает, поясню – это класс, возвращающий указатель на очередной элемент динамического списка. После чего нужно вызвать метод перемещения на следующий элемент списка, и так до конца этого динсписка.

Что еще нужно знать о WMI? То, что эта технология позволяет не только получать информацию о системе, но и вызывать команды управления системой. То есть классы, полученные энумерацией, имеют методы, вызов которых приведет к выполнению на целевой машине неких, подлежащих ему действий. Не думайте, что я описался, назвав локальный компьютер целевым. WMI позволяет проводить все эти операции с любым хостом Windows в сети, лишь бы имелись административные права на подключение к целевой машине.

Итак, пора написать программку, которая будет нашим телескопом в локальной сети.

 

Show must go on…

Ради интереса, предлагаю небольшой холливар. Да, да… Вот такой вот я нехороший провокатор. :)

Предлагаю написать программу на двух языках – Делфи и Си. В качестве компиляторов я взял Delphi 6 (на мой взгляд, самая лучшая версия Делфи) и Visual Studio 2010. Не потому что VS2008 или VS6 хуже, а просто у меня нет их под рукой.

Для начала давайте определимся со стратегией. Чтобы получить набор данных от WMI нам нужно следующее:

  • запустить СОМ сервер, провайдер WMI. А точнее WbemLocator – это имя провайдера;
  • приконнектить его к целевой машине;
  • заставить его выполнить запрос;
  • пройтись энумератором по коллекции записей;
  • пройтись энумератором по коллекции полей каждой записи.

Мы, кстати, реализуем механизмы получения значения поля, как по его названию, так и по его номеру. Для этого предлагаю выделить два класса. Первый назовем TWMI. Он будет главным. Его задача принять запрос, подключится к компьютеру, выполнить запрос, и, перебрав его записи, создать список объектов типа TWMIRecord. Этот второй класс будет отвечать за получение данных из полей переданной ему записи (записи передаются в виде объекта) либо по имени, либо по номеру поля. Опять таки и тут без энумерации не обойтись

Ладушки. Начнем. Сначала опишем главный класс. Листинг 1 и 2 показывает, как он может выглядеть в Делфи и Си:

Листинг 1 (Делфи)

 

	TWMI=class(TComponent)
	 private
	 // Список объектов-записей
	  FRecords:TObjectList;
	    FSQL: String;
	    FRoot: String;
	    FHost: String;
	    FSQL2: String;
	    FLogin: String;
	    FPassword: String;
	    procedure SetHost(const Value: String);
	    procedure SetRoot(const Value: String);
	    function GetItem(i: Variant): TWMIRecord;
	    procedure SetSQL2(const Value: String);
	    procedure SetLogin(const Value: String);
	    procedure SetPassword(const Value: String);
	 Public
	  //**************************************
	  // Функция для циклов. Выдает номер последней записи
	     Function HighObject:Integer;
	  // Функция поиска записи по имени поля
	     Function Find(AFieldName,AValue:String):TWMIRecord;
	  //**************************************
	  // Свойство, получающее по номеру запись из набора
	  Property Item[i:Variant]:TWMIRecord read GetItem; default;
	  Constructor Create(AOwner:TComponent);
	  Destructor Free;
	 published
	  // Свойства Логина и пароля. Чтоб подключится к компьютеру и
	  // получить информацию нужны привилегии администратора
	  Property Login:String read FLogin write SetLogin;
	  Property Password:String read FPassword write SetPassword;
	  // Имя машины к которой подключаемся
	  Property Host:String read FHost write SetHost;
	  // Путь к таблице с данными
	  Property Root:String read FRoot write SetRoot;
	  // Свойство, принимающее строку запроса
	  // и инициализирующее его выполнение
	  Property SQL:String read FSQL2 write SetSQL2;
	 end;
	//******************************************

Листинг 2 (С++)

 

	class TWMI
	{
	private:
	 // Обьект-провайдер, через который можно будет приконнектится
	// к компьютеру.
	IWbemLocator *loc;
	// обьект, которому будем скармливать запрос, и получать
	// записи
	IWbemServices *serv;
	// поле, хранящее наш запрос
	string FQuery;
	// энумератор записей
	IEnumWbemClassObject* enum_Record;
	// список, который будет хранить объекты-записи
	list<TWMIRecord> RecList;
	// функция, активирующая запуск провайдера
	bool RunLocatorInstance();
	// функция, активирующая подключение к хосту
	bool ConnectToWBEM();
	// процедура, создающая новыйй обьект-запись
	// в ходе энумерации
	void CreaRecord(IWbemClassObject *O);
	public:
	TWMI(void);
	~TWMI(void);
	// Функция, открывающая набор данных
	// по переданному ей запросу
	bool SetQuery(string wql);
	// Функция, получающая запись по ее номеру
	TWMIRecord* Item(int i);
	};

 

На всякий случай хочу сказать, что для Сишного кода нужны инструкции:

Листинг 3

 

	#pragma once
	#pragma comment(lib, "wbemuuid.lib")
	#include <comdef.h>
	#include <WbemIdl.h>
	#include "TWMIRecord.h"
	#include <string>
	#include <list>
	using namespace std;

Здесь единственный хедер, который стоит описать – TWMIRecord.h. Он описывает наш класс, отвечающий за получение и хранение полей записи, переданной ему. Все остальное – стандартный набор. Я не буду их описывать, ибо их описание вполне можно найти на MSDN или в хелпе.

Теперь можно описать конструкторы и деструкторы главных классов. На Делфи:

 

	constructor TWMI.Create(AOwner: TComponent);
	begin
	 inherited;
	 FRecords:=TObjectList.Create;
	 FHost:='.';
	 FRoot:='root\cimv2';
	end;
	destructor TWMI.Free;
	begin
	  FRecords.Free;FRecords:=nil;
	end;
	На Си:
	TWMI::TWMI(void)
	{
	 loc=NULL; serv=NULL;
	 CoInitialize(0);
	}
	TWMI::~TWMI(void)
	{
	      CoUninitialize();
	 RecList.clear();	
	}

Здесь мало интересного. Инициализируются главные свойства, и в случае с Си инициализируется (в конструкторе) и сбрасывается (в деструкторе) СОМ библиотека. В случае с Делфи инициализация будет не здесь, а в коде обработки запроса.

 

Для Делфи:

На всякий случай, чтоб не было недомолвок, скажу, что обработчики свойств Login, Password, Host, Root стандартные для любого проекта, я не буду приводить их реализацию, ибо там всего лишь внутреннему полю класса, отвечающему за значение этих свойств, описывается методика присвоения нового значения. Если что станьте курсором на эти свойства и нажмите CTRL+SHIFT+C – будут созданы стандартные тела обработчиков – именно их я и имею в виду.

Самое главное для этого класса – метод обработки запроса. Именно здесь сконцентрирована вся магия работы с WMI. Давайте посмотрим, как она выглядит (на Делфи):

 

	procedure TWMI.SetSQL2(const Value: String);
	var
	// Переменная провайдер. подключается к хосту,
	// Через нее будем получать объект для обработки
	// запросов
	 objSWbemLocator,
	// Переменная объекта обработки запросов
	// Ей будем скармливать запрос и ее же
	// энумератором будем получать записи
	objWMIService,
	// Сервисные переменные. Нужны для
	// получения энумераторов
	Records,o1,o2
	:OleVariant;
	i:Cardinal;
	// Интерфейс, который будет являться посредником
	// между провайдером и объектом запросов
	id:IDispatch;
	// Энумератор для прохода по записям
	Enum:IEnumVariant;
	// Переменная класса, который будет обрабатывать
	// переданную ему запись
	r:TWMIRecord;
	begin
	FSQL2 := Value;
	// инициализируем СОМ модель
	CoInitialize(0);
	// Получаем объект провайдера WMI, Локатор, если говорить
	// по микрософтовски
	objSWbemLocator:=CreateOleObject('WbemScripting.SWbemLocator');
	// Если он успешно получен, то можно далее с ним работать
	if not VarIsClear(objSWbemLocator) then begin
	// Присоединяемся к хосту, получив интерфейс для
	// отработки запросов
	objWMIService:=objSWbemLocator.ConnectServer(Host,Root,FLogin,FPassword,'','',0,id);
	// Если присоединение прошло успешно то можно
	// скармливать запрос
	if not VarIsClear(objWMIService) then begin
	// Скормим запрос WQL нашему провайдеру
	Records:=objWMIService.ExecQuery(FSQL2,'WQL',0,id);
	// Он вернет объект записей. Ну если конечно вернет
	if not VarIsClear(Records) then begin
	//**************************************
	// В случае когда все удачно, инициализируем
	// энумератор для прохода по записям
	Enum:=IEnumVariant(IUnknown(Records._NewEnum)); //Список Записей
	// Приготовим список для наполнения объектами,
	// обрабатывающими переданные им записи
	FRecords.Clear;
	// и пройдемся энумератором, пока он
	// не достигнет конца коллекции записей
	// или точнее пока очередная запись выбрана успешно
	while (Enum.Next(1, o1, i) = S_OK) do begin
	// Создадим объект - запись
	r:=TWMIRecord.Create;
	// внеся его в список
	FRecords.Add(r);
	// И заставим его пройтись по полям,
	// переданной ему записи
	r.Enum(o1);
	end;
	//**************************************
	// после чего приберем мусор. Набор мы уже получили
	// так что можно отключаться
	Records:=Unassigned;
	end;
	objWMIService:=Unassigned;
	end;
	objSWbemLocator:=Unassigned;
	end;
	// и освободить ресурсы СОМ машины.
	CoUninitialize;
	end;

Добавлю, что у класса TWMIRecord предусмотрен метод Enum, которому передается объект записи. Он инициализирует проход по полям, получая из них значения. Теперь в С++:

Листинг 7

	bool TWMI::SetQuery(string wql){
	 // Переменная для результатов СОМ механизмов
	 HRESULT hres;
	 // результат выполнения запроса.
	 // он будет подаваться на выход метода, дабы
	 // программист знал, отработал ли метод успешно
	// или не отработал
	bool res=false;
	// Если Локатор запустился нормально
	if(RunLocatorInstance()){
	// И присоединился к хосту
	if(ConnectToWBEM()){
	// Можно скармливать провайдеру
	// запрос, не забыв преобразовать типы :)
	hres=serv->ExecQuery(bstr_t("WQL"),
	bstr_t(wql.c_str()),
	WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
	NULL,
	&enum_Record);
	// Если запрос скормлен, и провайдер отработал его правильно
	// можно идти далее. Предидущей функцией был
	// получен энумератор записей, который позволит
	// сделать проход по ним
	if(!FAILED(hres)){
	// Обьявим переменную - запись. В нее на итерации
	// будет подаваться очередная запись
	IWbemClassObject *Obj;
	// и переменную, в которую будет подаваться сколько // записей
	// выбрано. На итерациях это значение будет равно 1,
	// ведь мы будет проходитьпо каждой записи :)
	ULONG uReturn = 0;
	hres=S_OK;
	// приготовим список, хранящий объекты записей
	RecList.clear();
	// и прокатимся по энумератору
	while(enum_Record){
	// получим очередную запись
	hres=enum_Record->Next(
	//указав что мы будем ждать до последнего, пока на сервере выбирается запись
	WBEM_INFINITE,
	// проходя по одной записи за итерацию,
	1,
	// передавая ее в переменную вышеописанную нами для этого
	&Obj,
	// и получая ответ "сколько записей пройдено", короче 1
	&uReturn
	);
	// Если же всетки мы достигли конца набора, вернется 0 пройденных
	// записей. Придется прервать цикл
	if( uReturn==0) break;
	// А пока записи прибывают будем составлять на них
	// опись, протокол, сдал-принял... отпечатки пальцев 😀
	CreaRecord(Obj);
	}
	// по прошествии цикла будем считать что функция успешно отработала
	// соответственно вернем позитивный ответ
	res=true;
	}
	// и освободим нашего провайдера и его сервис
	serv->Release();
	}
	loc->Release();
	}
	return res;
	};

 

 Здесь применены методы, описанные выше:

 

	bool TWMI::RunLocatorInstance(){
	  HRESULT hres;
	  bool rt=false;
	  hres=CoCreateInstance(CLSID_WbemLocator,0,CLSCTX_INPROC_SERVER,
	  IID_IWbemLocator,(LPVOID *)&loc);
	  if(!FAILED(hres)){
	   hres = 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
	   );
	   rt=(!FAILED(hres))?true:false;
	  }
	  return rt;
	 }
	 bool TWMI::ConnectToWBEM(){
	   HRESULT hres;
	   BSTR bUser=NULL,bPassword=NULL;
	   BSTR bHost=L"ROOT\\CIMV2";
	    if(Host!=""){bHost=L"\\\\"+_bstr_t(Host.c_str())+L"\\ROOT\\CIMV2";}
	 hres=loc->ConnectServer(
	  bHost, // Хост с веткой WMI
	  // User name. NULL = current user
	   (Login!="")?_bstr_t(Login.c_str()):bUser,
	// User password. NULL = current (Password!="")?_bstr_t(Password.c_str()):bPassword,
	0, // Locale. NULL indicates current
	NULL, // Security flags.
	0, // Authority (e.g. Kerberos)
	0, // Context object
	&serv // pointer to IWbemServices proxy
	);
	return (!FAILED(hres))?true:false;
	};

 

В принципе, здесь пояснять особо нечего. Создается инстанция провайдера, говоря по-русски, запускается для нашей программы механизм WMI (аналог CreateOleObject(‘WbemScripting.SWbemLocator’); примененной мной в Делфи варианте) и происходит подключение к хосту, учитывая логин и пароль пользователя.

Если логин и пароль отсутствуют, берется аккаунт текущего сеанса (Если уж по правде то текущий контекст безопасности, т.е. с чьими правами запущена программа. Будем считать, что запуск программы будет от текущего сеанса. Мы ведь рассчитываем, что это будет запускать администратор всея сети). Если отсутствует указание хоста – подключение идет к нашей машинке.

Почему я выделил в Сишном классе эти функции подключения отдельно? Захотелось так. Мне показалось, что слишком много в Си писанины, и я не хотел загромождать метод обработки запроса лишним кодом, это бы выглядело некрасиво. Казалось бы сложным. На Делфи эти функции как-то срослись в две строчки, к сожалению, в Си мне не удалось так же аккуратно написать их, может потому, что я плохо знаю Си, или потому что в Делфи действительно удобнее описывать работу с СОМ интерфейсами… Да и работа со строками в Делфи попрозрачнее будет… Впрочем, это не важно. Ну и сюда же следует приписать реализацию процедуры, создания класса для обработки записи:

 

	void TWMI::CreaRecord(IWbemClassObject *O){
	 // создаем новый объект, скормив ему
	 // переданную строку
	 TWMIRecord *r=new TWMIRecord(O);
	 // Помещаем его в список строк
	 RecList.push_back(*r);
	};

Энумерация по полям записи будет инициирована конструктором. Опять таки, в Си я описал создание класса в отдельную процедуру не по каким-то особым причинам, а потому что просто так захотелось. Запросто эти две строчки можно поместить в энумерацию SetQuery.

Так, набор получили. Прокатились по нему. Теперь нужен механизм, проходя по списку объектов TWMIRecord. Надо же как-то получать выбранные данные. Посмотрим, как это можно сделать (на Делфи):

 

	function TWMI.GetItem;
	var ii:integer;
	begin Result:=nil;
	 if VarIsOrdinal(i) then
	  // Если переданный номер записи не
	  // вылезает за рамки списка
	  // получим ее
	if (i>=0)and(i<FRecords.Count) then
	Result:=TWMIRecord(FRecords[i]);
	end;

 

Эта функция повешена на свойство:

Property Item[i:Variant]:TWMIRecord read GetItem; default;

Хорошо зная Делфи, могу заверить, что возможность объявлять свойство для класса по умолчанию очень полезна. Достаточно в выражениях указать имя переменной объекта, если какое-то из его свойств установлено по умолчанию, оно вызовется, так как будто программист руками его вызвал в коде.

Например, многим известен Дельфийский Listbox. У него есть свойство Items[номер строки]. Вызывают его так ListBox.Items[такой-то] – Это дает строку по указанному номеру. Но далеко не все знают, что при написании такого на самом деле Делфи воспринимает это указание как ListBox.Items.Strings[такая-то], потому что Strings описана как свойство по умолчанию для поля Items, да еще и с указанием индексации. Согласитесь удобнее не писать Items.Strings раз среда позволяет это. Код становится короче и красивее. Кстати некоторые нерадивые авторы методичек на этом деле спекулируют. В одних методичках пишут длинную форму обращения, в других краткую. Учеников это часто путает, появляется вопросы «В каких случаях писать длинную форму инструкции в каких короткую». Ответ то на самом деле прост – эти две формы равнозначны. Кто как хочет, пусть так и пишет. Линейкой по рукам нужно проехаться этим борзописцам-преподавателям. Сразу видно они сами мало понимают в программировании. Это большая беда нашей системы обучения, но, увы… Законы Подлости диктуют свою игру.

Вы можете ответить или разместить запись на вашем сайте.

Ответить

Powered by Procoder