Дата публикации статьи: 09.07.2006 15:15

Использовать 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. Он выбрал цитату Джорджа Бернарда Шоу, которую я нахожу совершенно точной:

	Я не учитель,
	А просто парень-путешественник, 
	у которого ты спросил дорогу.
	Я показал вперед,
	Вперед для меня и для тебя.

Мне больше нечего сказать.