Использовать WMI в Visual Basic .NET - просто
Автор: Martin de Klerk
[Оригинал статьи] [Обсудить в форуме]
Перевод с английского: Виталий Готовцов
WWW: http://www.vitgot.narod.ru
Введение
Эта статья не является учебным пособием по WMI, а так же не описывает технологию WMI. Статья рассказывает об использовании WMI в VB.NET и является практическим (но ни в коем случае не полным!) руководством, которое подготовит вас, как VB.NET-программиста, к использованию WMI. Вам не придётся пробиваться сквозь кирпичные стены, как пришлось мне. По крайней мере, в меньшей степени, чем это довелось мне. Инструментарий управления Windows (Windows Management Instrumentation, WMI) может быть большой сложностью для неподготовленного человека. Сложность использования (и, к примеру, зависимость от версии ОС) не для малодушных разработчиков, но когда вы разберетесь с возможностями WMI, вы получите воистуну мощный инструмент для достижения большого количества задач.
Я бывалый программист и программировал для разных операционных систем, но в результате недавней необходимости в изучении и использовании WMI количество седых волос на моей голове удвоилось. И чтобы избавить всех VB.NET-программистов во всем мире от ранней седины или даже облысения, я предлагаю вам достижения моих проб и ошибок в этой области, чтобы ваши VB.NET/WMI усилия доставляли вам только удовольствие.
Приняв решение придерживаться парадигмы VB.NET я отверг использование скриптов, как средства доступа к WMI, и сосредоточился на пространстве имен System.Management.
Обзор (удаленного) подключения к WMI
Установлению удаленного подключения к WMI посвящено несколько статей, включающих описания портов подключения и авторизации. Если бы мне давали доллар каждый раз, когда я получал сообщение об ошибке «Удаленный RPC-сервер не доступен» обращаясь к порту, блокированному брандмауэром или неправильной DCOM/COM+ аутентификацией, я, вероятно, оказался бы в Forbes Top 100.
Так как WMI использует несколько протоколов, и каждый протокол использует свои собственные порты, то настройки брандмауэра должны позволить доступ через следующие порты:
Обязательно:
RPC TCP 135,139,445,593
SNMP UDP 161,162
Дополнительно:
WINS TCP 42 UDP 42, 137
PrintSpooler TCP 139, 445
TCP/IP PrintServer TCP 515
Для реализации основных возможностей достаточно, чтобы открыты были порты из раздела «Обязательно». Также необходимо быть уверенным, что на всех вовлеченных компьютерах работают DCOM/COM+ сервисы. WMI использует DCOM для обработки удаленных вызовов. Самая распространенная причина неудачного подключения к удаленному компьютеру связана с неудачей DCOM (ошибка “DCOM Access Denied” десятичный код – 2147024891, или шестнадцатиричный – 0х80070005). Вы можете сконфигурировать DCOM настройки для WMI-использования DCOM Config в Контрольной панели инструментов администрирования (Control Panel > Administrative Tools).
Вторая ловушка – это авторизация: у вас должны быть права администратора на (удаленном) компьютере.
Третья западня – зависимости от версии ОС. Например, интерфейс DCOM/COM+, используемый WMI, требует различные настройки AuthenticationLevel, зависящие от версии ОС: при подключении к системам, запускающим MS Windows версии, предшествовавшие Windows XP, настройки AuthenticationLevel.Connect необходимы для получения доступа к объектам и классам, подключаемым через DCOM/COM+, но XP требует, чтобы это был настроенный AuthenticationLevel.Packet. Позднее на этой зависимости ОС будет также основана возможность и функциональность некоторых WMI-классов и объектов.
Если будут выполнены вышеуказанные условия, вы сможете достичь подключения к (удаленному) WMI-серверу. Следующим шагом является подключение к пространству имен WMI. Пространство имен WMI содержит WMI-классы и объекты, так же, как и пространство имен .NET, но оно, также, является вашим «рабочим пространством». Правильнее всего относиться к пространству имен WMI, как к папкам, в которых вам приходится регистрироваться.
Так же, как и .NET Framework, WMI предлагает некоторые пространства имен соответствующих функций. По умолчанию пространство имен WMI = “\root\cimv2”. Применяя полный путь к пространству имен, вы можете подключиться к локальному пространству имен WMI (“\.\root\cimv2”) или удаленному пространству имен (“\pc_admin\root\cimv2”).
После подключения к пространству имен вы получаете доступ к классам и объектам, находящимся в этом пространстве имен.
Что вам нужно для кодирования WMI-подключения в VB.NET
Войдите в System.Management.ConnectionOptions и System.Management.ManagementScope. Вместе они формируют область взаимодействия с WMI. Для настройки аутентификации вам нужен объект ConnectionsOptions и класс ManagementScope для действующего подключения. Все коммуникативные требования WMI-классов и объектов будут установлены в последнем классе.
Dim myConnectionOptions As New System.Management.ConnectionOptions
With myConnectionOptions
.Impersonation = System.Management.ImpersonationLevel.Impersonate
'* Используйте следующую строку для XP
.Authentication = System.Management.AuthenticationLevel.Packet
'* Используйте следующую строку для Win, предшествовавший XP
'*.Authentication = System.Management.AuthenticationLevel.Connect
End With
Код, указанный сверху, устанавливает авторизацию по умолчанию необходимой для WMI-подключения. Этот объект, вместе с полностью определенным WMI-пространством имен, формирует средство установления подключения:
Dim myManagementScope As System.Management.ManagementScope
'* Замените "." действительным именем сервера для удаленного подключения
Dim myServerName As String = "."
myManagementScope = New System.Management.ManagementScope("\" & _
myServerName & "\root\cimv2", myConnectionOptions)
'* подключение к пространству имен WMI
myManagementScope.Connect()
If myManagementScope.IsConnected = False Then
ConsoleWriteLine("Could not connect to WMI namespace")
End If
Имейте в виду, если условия, описанные в предыдущем разделе, «Обзор (удаленного) подключения к WMI», не будут выполнены, ваше приложение вызовет исключение на операторе myManagementScope.Connect().
Итак, я вошел. Что теперь?
Допустим, вы хотите знать, какие программы установлены на удаленном ПК. По существу вы посылаете запрос к (удаленному) WMI-серверу. Этот WMI-сервер возвращает результат запроса (в данном случае ManagementObjectCollection, который является массивом ManagementObjects, каждый из которых представляет отдельную часть программ, установленных MS-инсталлятором). Запрос будет скоординирован классом ManagementObjectSearcher:
Dim myObjectSearcher as System.Management.ManagementObjectSearcher
Dim myCollection As System.Management.ManagementObjectCollection
Dim myObject As System.Management.ManagementObject
myObjectSearcher = New System.Management.ManagementObjectSearcher( _
myManagementScope.Path.ToString, "Select * From Win32_Product")
'* выполните запрос
myCollection = myObjectSearcher.Get()
'* список установленных пакетов
For Each myObject In myObjectCollection
Console.WriteLine( myObject.GetPropertyValue("Caption"))
Next
Программа, указанная выше, будет работать для любых Win32_XXXX WMI-классов, как и всех классов, владеющих свойством “Caption”. Чтобы получить полный список доступных WMI-классов, объектов и пространств имен, обратитесь к MSDN или смотрите ссылки в конце этой статьи.
Обратите внимание на возвращаемые WMI значения: так как тип этих значений не является безопасным (VB.NET-компилятор не имеет возможности узнать, какого типа данные возвращаются), вы будете вынуждены конвертировать типы данных в их .NET-эквиваленты. Например: свойство Win32_Printer.Priority является беззнаковым 32-битовым integer. .NET Framework не использует беззнаковые integer, поэтому для использования этого значения вам придется конвертировать его в тип данных .NET
myPriority = Convert.ToInt32( myPrinterObject.GetPropertyValue("Priority"))
Это хорошо, если вы пользуетесь только базовым WMI, но когда вы используете расширенный WMI, вы найдете эти кодовые соглашения источником больших проблем и напрасной тратой усилий. Решение приходит в форме Генератора Классов Управления Со Строгой Типизацией (Management Strongly Typed Class Generator) (Mgmtclassgen.exe). Эта программа, содержащаяся в .NET Framework Toolkit, генерирует (строго типизованный) упаковщик с ранним связыванием для выбранного WMI Win32_XXXX управляющего класса для внедрения в ваш проект. Используя этот упаковщик, вы не будете вынуждены заботиться о стандартных соглашениях.
…и заголовок гласит: «Легко совершаемые в VB.NET WMI-подключения»?
Вы совершенно правы, и я поддерживаю этот заголовок. Просто останьтесь со мной, оно того стоит. В ноябре 2004 года я впервые был вовлечен в WMI благодаря вопросу от
коллеги. Он хотел отслеживать принтеры в своей сети с помощью WMI и застрял на разработке логики кода. Он показал код, с помощью которого он получал информацию о роде удаленного принтера, и он притягивал мои глаза к экрану. Оператор, который привлекал меня, был:
moSearch = New Management.ManagementObjectSearcher("Select * from Win32_Printer")
Конечно, это не могло быть так просто, как один оператор. После быстрого «копипаста» я запустил его на моей машине и вот результат: все мои логические установленные принтеры были получены в виде списка, включая факс и (установленные, как разделяемые) удаленные принтеры. Должен признаться, я почувствовал эйфорию от реализации новых возможностей, переполнявшую мои вены. Таким образом, его сообщение отметило начало моего путешествия сквозь WMI.
Я начал помогать коллеге и в этом процессе я узнал много о пересечении WMI с VB.NET. Вы все еще со мной? Хорошо, тогда здесь находится ваша награда за ваше упорство:
Connection Tester
Одним из результатов этого путешествия стал VB.NET-класс ‘ConnectionTester’. Как предполагает это название, он изначально будет быстрым способом узнать, подключен ли удаленный компьютер, так как WMI-оператор connect() может потребовать значительного времени для того, чтобы решить, подключен ли удаленный компьютер, или доступен ли удаленный RPC-сервер. Я нашел более быстрый метод с помощью DNS-запроса, которому необходимо несколько секунд, чтобы узнать для меня, подключен ли компьютер.
Следующий шаг был встроен в проверку, доступно ли WMI на удаленном компьютере с помощью WMI-подключения к удаленному пространству имен и обработки возникших исключений. Закончив тестирование WMI-проверки, я решил, что если удаленное WMI доступно, то я получу в свое распоряжение открытое WMI-подключение. Допуская, что закрытие повторно открытого WMI-подключения требует времени и активного использования процессора (как и некоторые другие функции WMI, так что вам нужно быть готовым к тому, что вам придется закопаться в потоках для ускорения и размораживания вашего приложения…), я решил расширить мой класс до легковесного упаковщика WMI-подключения.
Этот класс-упаковщик состоит из двух методов (.Poll и .ExecWmiQuery) и свойств, которые разделены на два типа: те, что должны быть установлены до осуществления .Poll и те, что являются результатом .Poll.
Из свойств, которые должны быть установлены, обязательно предоставляются .ServerName и .IPAddress. Прочие свойства (.WmiCheck, .WmiNameSpace, .UserName, .Password) опциональны и служат для создания некоторой степени гибкости.
Результаты вызова .Poll помещены в свойства .IsOnline, .WmiEnabled, .HasErrors, .ErrorMessage, .PollInProgress, .OperatingSystem и .WmiScope.
… Ну, и где начинается ЛЕГКАЯ часть?
Прямо здесь. Выполнение WMI-подключения к удаленному компьютеру теперь требует три оператора:
'* Создание экземпляра класса ConnectionTester
Dim myConn as New ConnectionTester
'* точка подключения к компьютеру
MyConn.ServerName = "PC_admin"
'* Инициация подключения
MyCon.Poll()
Вот они. Мы получаем классы ManagementScope, ConnectionOptions, ManagementObjectSearcher низкоуровневую обработку исключений. После успешного вызова .Poll (что можно проверить с помощью свойств .IsOnline, .WmiEnabled и .HasErrors) вы можете воспользоваться подключением (свойство .WmiScope), чтобы выполнить свои поставленные задачи или использовать встроенный механизм запросов.
Следующий фрагмент кода создает список процессов, запущенных на выбранном компьютере:
'* Создает хранилище для результата запроса
Dim WmiQueryResult As System.Management.ManagementObjectCollection
'* Получает WMI-объекты запущенных процессов
WmiQueryResult = myConn.ExecWmiQuery("Select * From Win32_Process")
'* Список возвращенных имен процессов
Dim WmiObject as System.Management.ManagementObject
For Each WmiObject In WmiQueryResult
Console.WriteLine(WmiObject.GetPropertyValue("Caption"))
Next
WMI-запросы
Что касается WMI-запросов: язык запросов WMI (WMI Query Language, WQL) создан по образу SQL, и так же, как он поддерживает такие операторы, как WHERE, WITHIN, HAVING и т.д. Например, чтобы получить WMI-представление о выбранном принтере на удаленном компьютере, вы можете выполнить запрос типа: «Select * From Win32_Printer Where Name=""hpdeskjet""». Результат выполнения этого запроса находится в коллекции ManagementObjectColection, содержащей единственный объект ManagementObject представляющий выбранный принтер.
Войдите в ОС зависимость. Во время работы Windows 9.x или Me, расширения WQL, вроде оператора WHERE, не поддерживаются. В этом случае единственной возможностью является получение всех объектов-принтеров и выбор правильного WMI-объекта с помощью кода:
WmiQueryResult = myConn.ExecWmiQuery("Select * From Win32_Printer")
For Each WmiObject In WmiQueryResult
If WmiObject.GetPropertyValue("Name") = "hpdeskjet" Then
'
'
End If
Next
Еще несколько ловушек
Я должен был прорываться сквозь информационные джунгли WMI с помощью мачете. С каждым работающим VBScript/WBEM/C++ решением, найденным в интернете, моя решимость разобраться с проблемой с помощью VB.NET росла. Во время этого испытания я находил много перспективных (WMI) путей к моей цели, но во многих случаях я должен был признавать, что в половине трудностей с зависимостями ОС к моим услугам был только клиент XP Pro.
Следующим пунктом были «управляемый vs. неуправляемый» код. В предыдущем примере WmiObject представляет тип Win32_Printer. Этот WMI-объект получает свои свойства от разных подсистем WMI, как это показано в отчете MSDN о свойстве Win32_Printer.Caption:
Caption
Data type: string
Access type: Read-only
Qualifiers: MaxLen(64)
У этого объекта короткое описание, только одна строка. Это свойство, наследуемое от CIM_ManagedSystemElement. Я обнаружил, что многие WMI-свойства недоступны, как и те, что относятся к неуправляемым данным/коду. Хотя это разочаровывает, это имеет смысл в .NET. Но это так же по умолчанию ограничивало меня в использовании только тех свойств, которые были встроены в WMI и предназначались для работы с WMI-объектом (не унаследованными) и свойствами, которые были унаследованы от класса CIM_ManagedSystemElement.
Для получения доступа к неуправляемому коду и данным WMI, обратитесь к ссылкам, предложенным в конце статьи.
Добавленный бонус (или два)
Так почему же нужно вызывать метод .Poll, а не .Connect? Вот несколько причин для этого:
первая заключается в том, что этот класс необходим для создания программы, которая отслеживает работу сетевого принтера. Программа должна быть в состоянии определить, когда удаленный компьютер подключается к сети, и если он подключен, то автоматически создать WMI-подключение, чтобы начать опрашивать удаленные принтеры.
Вторая причина такова, что класс ConnectionTester изначально предназначался для ускорения процесса подключения. Если удаленный компьютер не был подключен, то не было и причины инициировать WMI-подключение, которое потребует значительных ресурсов и времени для обработки исключения. По той же причине проверка допустимости WMI должна быть выполнена сразу после определения, что компьютер подключен.
Третьей причиной является ОС-зависимость. Так как определенные WMI возможности и классы доступны только в определенных версиях, мне был нужен легкий способ определить версию запущенной Windows на удаленном компьютере. Это делалось с помощью процедуры GetRemoteOsInfo(), которая (о, милая ирония!) всецело зависима от ОС. Просто посмотрите на исходный код, и вы поймете, что я имею в виду.
Способом, которым установлен код, вы можете использовать класс ConnectionTester в широком назначении:
Бонус #1: Вы можете выключить функции WMI в классе ConnectionTester, чтобы он быстро выполнял он-лайн-проверку. Это, вместе с дополнительным Timer, позволит вам создать простую программу-монитор подключений. Просто установите .WmiCheck равным False, загрузите .ServerName с
www.vbstreets.ru и в событии TimerTick (или в событии Timer.Elapsed, в зависимости от того, какой класс Timer вы используете) просто поместите следующие операторы:
WmiConn.Poll()
If Not WmiConn.IsOnline Then
Console.WriteLine("VBStreets недоступен. " _
& "Переключитесь в режим ПАНИКА и наберите 01.")
End If
Теперь, конечно, сервер VBStreets не доступен для WMI. Но у вас не было бы проблем с получением WMI-информации через интернет с помощью класса ConnectionTester. Это делает
Бонус #2: Если вам нужно локально управлять WMI-объектами на компьютере в LAN или компьютере где-то в мире, подключенном через интернет, это произошло бы совершенно прозрачно с помощью класса ConnectionTester.
Пройдем по образцам
Следующая часть показывает образец кода, включенного в исходный код ConnectionTester. Просто удалите модуль ‘Sample’, чтобы внедрить класс ConnectionTester в ваш проект. Так как код хорошо комментирован, я не стал оскорблять интеллект читателя объяснениями того, что уже объяснено:
Dim WmiConn As New ConnectionTester
'* точко подключения ConnectionTester к целевому компьютеру.
'* Это может быть сделано либо с помощью NetBios-имени либо
'* Domain-имени (удаленного) компьютера:
WmiConn.ServerName = "localhost"
'* Или применяя IP адрес (удаленного) компьютера
'* в виде строки:
'* WmiConn.IPAddress = "127.0.0.1"
'* Или http адрес (удаленного) компьютера:
'* WmiConn.ServerName = "www.vbcity.com"
'* Возможности ConnectionTester's WMI доступны по умолчанию.
'* Чтобы отключить их и выполнить только он-лайн проверку, установите
'* следующее значение равным False
WmiConn.WmiCheck = True
'* Проверьте подключение
Console.WriteLine("Connecting to {0}", wmiConn.ServerName)
WmiConn.Poll()
'* ПроверьтеЮ что объект подключен
If wmiConn.IsOnLine = False Then
Console.WriteLine("{0} is off-line.", WmiConn.ServerName)
Exit Sub
Else
'* Отображает он-лайн состояние
Console.WriteLine("{0} is online with IP address: {1}.", _
WmiConn.ServerName, WmiConn.IPAddress)
End If
'* Проверьте, не было ли ошибок во время подключения
If WmiConn.HasErrors = True Then
Console.WriteLine("Error while connecting to {0}: {1}", _
WmiConn.ServerName, WmiConn.ErrorMessage)
Exit Sub
End If
'* Проверьте, активно ли WMI подключение
If WmiConn.WmiEnabled = False Then
Console.WriteLine("Could not connect WMI with \{0}{1} ", _
WmiConn.ServerName, WmiConn.WmiNamespace)
Else
'* Отображает подключение к пространству имен WMI
Console.WriteLine("WMI connection with {0} established: {1}", _
WmiConn.ServerName, WmiConn.wmiNameSpace)
'* показывает версию операционной системы запущенной на удаленном
'* WMI сервере
Console.WriteLine("O.S. : {0} ", wmiconn.OperatingSystem)
'* получает все MSI-установленные программы (может быть обобщенным)
Console.WriteLine("Retrieving information. Please wait......")
'* Создает хранилище для результатов запросов
Dim moc As System.Management.ManagementObjectCollection
'* Инструктирует удаленный WMI сервер для выполнения запроса
moc = WmiConn.ExecWmiQuery("Select * From Win32_Product")
'* если находит, отображает имена программ
If Not moc Is Nothing Then
Console.WriteLine(" - Programs installed on {0} -", _
wmiConn.ServerName)
Dim mo As System.Management.ManagementObject
For Each mo In moc
Console.WriteLine( mo.GetPropertyValue("Caption"))
Next
Console.WriteLine(" - End of list -")
End If
End If
В заключение…
Если вы новичок в WMI я посоветовал бы вам начать (помимо многого чтения) загрузить CIM_Studio и ScriptoMatic 2.0.. Эти две утилиты помогут вам с легкостью исследовать пространства имен, классы и объекты WMI. Ссылки, написанные скриптами, находятся в конце этой статьи. Также, для получения документации, загрузите WMI SDK.
Как я уже говорил ранее, это был мой первый опыт работы с WMI. Из этого я могу только заключить, что мой метод написания программ работает (или нет), но я не могу поручиться, что он работает правильным WMI-способом. WMI предоставляет очень мощные функции, но ТОЛЬКО тогда, когда все вовлеченные (под)системы (такие как DCOM, RPC, SMNP, Firewall, Group Policies и т.д.) работают в совершенной гармонии. Если они не будут работать в совершенной гармонии, вы вскоре обнаружите себя ищущим иглу в пресловутом стоге сена.
Я приведу вам пример: во время разработки программы мониторинга принтера я был вынужден ломать голову над серверами принтера в то время, как WMI предлагает множество замечательных возможностей, которые я бы предпочел использовать, например класс ManagementEventWatcher. Этот класс инициирует событие, если происходит выбранное событие (такое, как создание работы принтера). Я собирался создать службу для серверов принтера, которая информировала бы мою программу-монитор о состояниях принтера и прогрессе печати. Так как WMI содержит возможность удаленно создавать и запускать службы и процессы (поэтому ручная установка моей службы не требуется: бесконтактное развертывание!) такой подход также уменьшит сетевой трафик и освободит ресурсы компьютера, запускающего программу мониторинга. Можно подумать, что это рай для программиста.
Увы, через две недели борьбы с классом ManagementEventWatcher я все еще получал исключение ‘метод не поддерживается’ (‘method not supported’), когда пытался вызвать метод ManagementEventWatcher.Start() в коде VB.NET. После штудирования WMI-лог-файлов (помещающихся в \windows\system32\wbem), MSDN, форумов, конференций и других интернет-ресурсов по этой теме я прекратил поиски и пришел к выводу, что я споткнулся на еще одной ОС-зависимости. Поэтому мне пришлось вернуться к поиску поддержки совместимости с другими версиями Windows. Возможно, действующий сервер Windows нуждается в использовании этого класса в VB.NET, я не знаю. Теперь, если я не прав или взгляд со стороны будет очевидным, я с радостью приму исправления: -
emdek@vbcity.com (только
на английском языке, пожалуйста).
Теперь, прежде чем вы нажмете кнопку отправки e-mail, позвольте показать вам подпись одного из старейших (по продолжительности членства) и почитаемых членов: CanOz. Он выбрал цитату Джорджа Бернарда Шоу, которую я нахожу совершенно точной:
Я не учитель,
А просто парень-путешественник,
у которого ты спросил дорогу.
Я показал вперед,
Вперед для меня и для тебя.
Мне больше нечего сказать.