Хроники разработки сценария для определения установленных версий. NET Framework

Каждый месяц Microsoft выпускает новые исправления для системы безопасности для продуктов и компонентов операционной системы, а наша группа сопровождения каждый месяц занимается тем, что автоматизирует их установку на все компьютеры, требующие обновления. Как правило, эта работа не вызывает значительных затруднений – все сводится к тому, чтобы запустить исполняемый файл программы обновления с соответствующими ключами для удаленной установки без вмешательства пользователя. У нас это делается с помощью инструментария HP ProLiant Essentials Rapid Deployment Package (RDP), который был в свое время принят нами в качестве стандарта для установки программ на серверы Windows 2000. Но иногда возникают ситуации, когда обновление не может быть установлено с помощью стандартной программы установки. Так, например, было с печально известным обновлением безопасности для уязвимости GDI, описанной в статье Microsoft Security Bulletin MS04-028 «Buffer Overrun in JPEG Processing (GDI+) Could Allow Code Execution" (http://www.microsoft.com/technet/security/bulletin/ms04-028.mspx). Это обновление затрагивает слишком много разных версий и редакций Windows и компонентов Windows, в том числе Windows. NET Framework 1.1 и Framework 1.0 Service Pack 2 (SP2).

Проблема проявляется в режиме совместной установки, когда Framework 1.1 и 1.0 сосуществуют на одном компьютере параллельно (это позволяет разработчикам тестировать свои приложения для работы как с одной из версий, так и для работы с обеими версиями одновременно). Конечно, для большинства пользователей достаточно сразу обновить. NET Framework до версии 1.1 и не связываться с параллельной установкой обеих систем на одном компьютере, поскольку приложения, скомпилированные для версии 1.0, совершенно нормально функционируют и с версией 1.1. Если вас интересуют вопросы совместимости версий. NET Framework, вы можете обратиться к статье базы знаний Microsoft "Versioning, Compatibility, and Side-by-Side Execution in the. NET Framework", расположенной по адресу http://msdn.microsoft.com/netframework/technologyinfo/ versioncomparison/default.aspx.

Поскольку обновление GDI должно затрагивать все установленные на компьютере версии Framework, (если на компьютере разработчика установлены обе версии для параллельного использования) для нас критически важно уметь обнаруживать наличие всех установленных на компьютере версий. NET Framework и определять установленные пакеты обновлений. Только таким образом можно убедиться, что все необходимые обновления действительно установлены на данном компьютере. При этом, поскольку Framework не является обязательным компонентом наших стандартных конфигураций, нам следовало убедиться, что компьютеры без Framework не получают ненужные обновления. Исходя из этих соображений, мы решили разработать сценарий для определения всех установленных на компьютере версий Framework с пакетами обновлений. И хотя нам пришлось несколько раз переделывать наш сценарий, результат того стоил.

Первая попытка

Существует множество способов получить подтверждение, что данный продукт установлен на компьютере. Например, можно считать значения параметров в реестре, прочитать файлы. ini, проверить наличие и версии определенных файлов. Поскольку для установки обеих версий Framework используется Windows Installer (установочный комплект представляет собой файл. msi), мы решили, что лучший способ определить наличие на компьютере. NET Framework – выполнить запрос к классу WMI (Windows Management Instrumentatiuon) Win32_Product – который возвращает коллекцию всех установленных на компьютере программ, – а затем сопоставить имена программ с известными именами версий для Framework 1.1 и Framework 1.0.

Листинг 1 представляет первый вариант сценария – он начинается с определения имени компьютера, на котором будет выполняться сценарий. Имя компьютера сохраняется в переменной strComputer. Поскольку сценарий будет исполняться на локальном компьютере, переменной strComputer присваивается значение ".". Далее, выполняется обращение GetObject для подключения к пространству имен WM root\cimv2, которое содержит в себе нужный нам класс Win32_Product.

Фрагмент А листинга 1 представляет собой главную часть сценария. Сначала в переменной wqlQuery формируется запрос на языке Windows Query Language (WQL), далее этот запрос исполняется обращением к методу ExecQuery. Результатом выполнения запроса является коллекция объектов, которая присваивается переменной colProducts. С помощью управляющей конструкции For Each. .. Next выполняется перебор всех элементов коллекции. Внутри цикла с помощью операции Select Case выполняется сопоставление имени текущего продукта (элемента коллекции) определяемого свойством Name, с именами Microsoft. NET Framework 1.1 и Microsoft. NET Framework (English) чтобы определить, установлен ли хотя бы один из искомых продуктов на сервере.

Второй вариант

Запрос WQL, используемый во фрагменте А листинга 1 представляет собой обобщенный запрос, результатом выполнения которого является коллекция всех установленных на компьютере пакетов. msi. Поскольку нас в данной задаче интересуют только установленные версии Framework, мы решили оптимизировать запрос, введя в запрос предложение Where для поиска только продуктов с именами, содержащими ключевое слово Framework. Этот запрос представлен на листинге 2.

Для ускорения обработки запроса ExecQuery мы также добавили ключи wbemFlagReturnImmediately и wbemFlagForwardOnly. Первый из них указывает, что при выполнении запроса возврат результата может начаться до того, как запрос полностью отработает. Второй ключ определяет перебор только в направлении возрастания, что увеличивает скорость исполнения запроса, по сравнению с двунаправленным перебором. Более подробную информацию об использовании метода ExecQuery можно найти на странице http://msdn.microsoft.com/library/en-us/wmisdk/wmi/ swbemservices_execquery.asp?frame=true.

Третье чтение

Как мы убедились при анализе листинга 2, простой путь не всегда является самым правильным. Хотя самым простым способом было написать сценарий с использованием класса WMI Win32_Product, выполнение запроса к данному классу занимает почти 20 секунд, даже в случае оптимизированного запроса. Поскольку нам было необходимо запустить сценарий более чем на 200 серверах, такое длительное ожидание было просто неприемлемым. В качестве альтернативы мы решили применить анализ реестра на удаленных компьютерах, чтобы выяснить, какая версия Framework установлена на каждом компьютере. Результат переработки сценария представлен в листинге 3.

Как и первые двух вариантах сценария, листинг 3 начинается с присвоения переменной strComputerName имени локального компьютера, но на этом сходство заканчивается. Различия начинаются уже с моникера WMI в вызове GetObject, это видно из фрагмента А листинга 3. В первых вариантах сценария использовался моникер пространства имен root\cimv2, а для обращения к реестру используется провайдер WMI StdRegProv. Все версии WMI содержат класс StdRegProv, поэтому в данном случае мы подключаемся к пространству имен root\default.

Класс StdRegProv предоставляет метод GetStringValue, позволяющий считывать из системного реестра данные, имеющие тип REG_SZ. При вызове метода GetStringValue необходимо указать следующие четыре параметра:

Key tree root. Параметр key tree root задает ветвь реестра, из которой будут считываться данные. Ключом по умолчанию является HKEY_LOCAL_MACHINE, которому соответствует значение &H80000002.

Subkey. Параметр subkey задает путь к разделу в дереве реестра (без ветви), в котором содержится запрашиваемое значение.

Entry (элемент). Параметр entry задает имя элемента реестра, значение которого вы собираетесь получить.

Out variable (результирующая переменная). Этот параметр указывает имя переменной, в которой в результате работы метода GetStringValue будет сохранено считанное из реестра значение.

Все приложения, для которых возможно удаление, создают свои записи в разделе реестра HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall. Именно этим разделом реестра мы и решили воспользоваться для поиска одного из трех параметров: "Microsoft. NET Framework (English)", "Microsoft. NET Framework Full v1.0.3705 (1033)", или загадочное {CB2F7EDD-9D1F-43C1-90FC-4F52EAE172A1}. Два первые значения соответствуют Framework 1.0. Когда я впервые загрузил и установил Framework 1.0 (это было довольно давно), создаваемый раздел реестра назывался "Microsoft. NET Framework (English)". Загружаемая в настоящее время с сайта Microsoft версия Framework 1.0 создает "Microsoft. NET Framework Full v1.0.3705 (1033)" (Microsoft иногда меняет имена продуктов, чтобы сделать их более понятными). Кодовое название раздела реестра {CB2F7EDD-9D1F-43C1-90FC-4F52EAE172A1} представляет Framework 1.1.

Из фрагмента кода B листинга 3 видно, что для получения данных из реестра используются константа и две переменные. Сначала выполняется присваивание переменной HKEY_LOCAL_MACHINE значения &H80000002. Эта переменная используется в качестве параметра key tree root. Поскольку сценарий проверяет три подраздела в разделе Uninstall, то для указания параметра запуска subkey используются две переменные – strKey и arrKeysToCheck. Переменная strKey содержит строку SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall. Переменная arrKeysToCheck представляет собой массив из трех строковых значений, "Microsoft. NET Framework (English)", "Microsoft. NET Framework Full v1.0.3705 (1033)", и "{CB2F7EDD-9D1F-43C1-90FC-4F52EAE172A1}".

В качестве параметра entry при вызове GetStringValue указывается параметр DisplayName. Не следует забывать, что свойство Name класса Win32_Product содержит имена продуктов, установленных с помощью Microsoft Installer. Элемент реестра DisplayNamr содержит значение свойства Name. Последним аргументом метода GetStringValue является переменная strDisplayName.

Далее в процессе работы сценарий перебирает все значения массива arrKeysToCheck и вызывает для каждого из них метод GetStringValue (см. фрагмент C листинга 3). Вызываемый метод GetStringValue возвращает значение элемента DisplayName в переменной strDisplayName, которое выводится сценарием.

При завершении работы метод GetStringValue возвращает 0 при успешном выполнении и отличное от 0 значение при возникновении ошибки. Поскольку установка. NET Framework является необязательным шагом, на некоторых из наших компьютеров реестр не содержит ни одного из перечисленных подразделов. В этих случаях вызов GetStringValue может вызвать ошибку и аварийное завершение работы сценария. Чтобы избежать этого, перед началом цикла For Each. .. Next используется инструкция On Error Resume Next, обеспечивающее продолжение выполнения сценария при возникновении ошибки.

Окончательный вариант

Представленный в листинге 3 сценарий позволил нашей инженерной команде быстро определять, какие из версий Framework установлены на каждом компьютере. Но нам еще необходимо определить наличие и версии пакетов обновлений Framework. Поэтому мы решили добавить в сценарий дополнительную проверку наличия установленных пакетов обновлений.

Проведенное нами исследование показало, что Microsoft не предоставил прямых способов определения наличия обновлений Framework, поэтому пришлось пользоваться косвенными признаками. Так, для Framework 1.1 наличие обновлений можно определить по присутствию в реестре раздела HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v1.1.4322. Если он принимает значение 1, это означает, что установлен пакет обновлений SP1.

Обновление для Framework 1.0 следов в реестре не оставляет. Microsoft предлагает проверить версию файла mscorcfg.dll.

Располагая этой информацией об установленных пакетах обновлений, мы взялись за разработку сценария DetectFramework.vbs, который представлен в листинге 4. Первая часть этого сценария похожа на уже разобранные примеры. С помощью вызова метода GetStringValue класса StdRegProv значение элемента DisplayName присваивается переменной strDisplayName. Но далее, вместо того, чтобы просто выдать считанное имя с помощью Wscript.Echo, начинается серия вложенных проверок If. .. Then. .. Else и Select Case. При нулевом возвращаемом значении в результате вызова метода GetStringValue (процедура метода была выполнено успешно, следовательно, в системе установлен Framework), в предложении Select Case выполняется проверка на равенство значения переменной strDisplayName строке "Microsoft. NET Framework 1.1" или "Microsoft. NET Framework (English)".

Microsoft. NET Framework 1.1. Если переменная strDisplayName содержит строку Microsoft. NET Framework 1.1, сценарий проверяет в реестре значение элемента SP. Для этого используется метод GetDWORDValue класса StdRegProv, считывающий элементы реестра типа REG_DWORD. Как и метод GetStringValue, метод GetDWORDValue требует четыре параметра, ветвь, имя подраздела, имя элемента и имя переменной, в которой будет сохранено считанное из реестра значение параметра.

Считав с помощью GetDWORDValue значение параметра SP, с помощью предложения Select Case (см. фрагмент А листинга 4), полученное значение сравнивается с единицей. В случае равенства, сценарий выдает сообщение Microsoft. NET Framework 1.1 SP1 found, в противном случае выдается Microsoft. NET Framework 1.1 found. Другими словами, установлен Framework 1.1 без пакета обновлений SP1.

Microsoft. NET Framework (English). Если strDisplayName содержит строку Microsoft. NET Framework (English), сценарий проверяет версию файла mscorcfg.dll. Объект FileSystemObject библиотеки времени исполнения Microsoft Scripting Runtime Library содержит метод GetFileVersion, который возвращает версию указанного файла. GetFileVersion требует только один параметр – полное имя файла, версию которого следует определить.

Впрочем, до вызова GetFileVersion, сценарий определяет, существует ли искомый файл. Хотя mscorcfg.dll должен существовать, если установлен Framework 1.0, хорошим тоном при написании сценария является проверка существования файла до того, как пытаться определить его версию – это позволяет исключить возможность аварийного завершения сценария с ошибкой "File Not Found — Файл не найден».

Для проверки существования файла mscorcfg.dll сценарий использует метод FileExists объекта FileSystemObject – см. фрагмент B листинга 4. Как и GetFileVersion, метод FileExists требует единственный параметр – имя целевого файла. Только в случае положительного результата проверки существования файла сценарий вызывает метод GetFileVerison.

Предложение Select Case фрагмента C листинга 4 выполняет сравнение значения возвращенной GetFileVersion версии файла с каждым из четырех вариантов версии, перечисленных в таблице 2. При совпадении с одним из вариантов сценарий выдает соответствующее сообщение о найденной версии пакета исправлений.

Мы проверили корректность работы сценария DetectFramework.vbs с различными комбинациями Framework 1.0, Framework 1.1 и пакетов обновлений, а так же на компьютерах без. NET Framework. Во всех случаях выданные сценарием результаты оказались верными.

Результат стоил того

Хотя разработка сценария DetectFramework.vbs методом проб и ошибок потребовала большого труда, результат стоил затраченного труда. И вообще, ребята в нашем инженерном подразделении считают, что если бы все было просто, было бы слишком скучно жить.


Листинг 1. Сценарий — первый вариант
strComputer = "." 
Set objWMIService = GetObject("winmgmts:\\" 
& strComputer & "\root\cimv2") 


 
' НАЧАЛО ФРАГМЕНТА A 
wqlQuery = "select * from win32_product" 
Set colProducts = objWMIService.ExecQuery(wqlQuery) 
For Each Product in colProducts 
Select Case Product.name 
Case "Microsoft .NET Framework (English)" 
Msgbox "Microsoft .NET Framework (English) 
is installed on the system" 
Case "Microsoft .NET Framework 1.1" 
Msgbox "Microsoft .NET Framework 1.1 
is installed on the system" 
End Select 
Next 
' КОНЕЦ ФРАГМЕНТА A 


 
Set objWMIService = nothing

Листинг 2. Сценарий — вторая попытка
Const wbemFlagReturnImmediately = &h10 
Const wbemFlagForwardOnly = &h20 
strComputer = "." 


 
Set objWMIService = GetObject("winmgmts:\\" 
& strComputer & "\root\cimv2") 


 
wqlQuery = "Select name From Win32_Product 
Where name = " & _ 
"'Microsoft .NET Framework 1.1' or name 
= 'Microsoft .NET Framework (English)'" 
' НАЧАЛО ФРАГМЕНТА A 
Set colProducts = objWMIService.ExecQuery _ 
(wqlQuery,"WQL",wbemFlagReturnImmediately 
+ wbemFlagForwardOnly) 
' КОНЕЦ ФРАГМЕНТА A 
For Each Product in colProducts 
Select Case Product.name 
Case "Microsoft .NET Framework (English)" 
MsgBox "Microsoft .NET Framework (English) 
is installed on the system" 
Case "Microsoft .NET Framework 1.1" 
Msgbox "Microsoft .NET Framework 1.1 
is installed on the system" 
End Select 
Next

Листинг 3. Сценарий — третий вариант
strComputer = "." 


 
' НАЧАЛО ФРАГМЕНТА A 
Set objRegistry = GetObject("winmgmts:{impersonationLevel 
=Impersonate}!\\" & _ 
strComputer & "\root\default:StdRegProv") 
' КОНЕЦ ФРАГМЕНТА A 


 
' НАЧАЛО ФРАГМЕНТА B 
Const HKEY_LOCAL_MACHINE = &H80000002 
strKey = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" 
arrKeysToCheck = array("Microsoft .NET Framework (English)", _ 
"Microsoft .NET Framework Full v1.0.3705 (1033)", _ 
"{CB2F7EDD-9D1F-43C1-90FC-4F52EAE172A1}" ) 
' КОНЕЦ ФРАГМЕНТА B 


 
On Error Resume Next 
' НАЧАЛО ФРАГМЕНТА C 
For Each Key in arrKeysToCheck 
objRegistry.GetStringValue HKEY_LOCAL_MACHINE, strKey 
& "\" & Key, _ 
"DisplayName", strDisplayName WScript.Echo strDisplayName 
Next 
' КОНЕЦ ФРАГМЕНТА C 


 
Set objRegistry = Nothing

Листинг 4. Сценарий DetectFramework.vbs
Const WindowsFolder = 0 
Const HKEY_LOCAL_MACHINE = &H80000002 
strComputer = "." 
strKey = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" 
arrKeysToCheck = array("Microsoft .NET Framework (English)", _ 
"Microsoft .NET Framework Full v1.0.3705 (1033)", _ 
"{CB2F7EDD-9D1F-43C1-90FC-4F52EAE172A1}" ) 
strFramework11Key = "Software\Microsoft\NET Framework Setup\NDP\v1.1.4322" 
strFramework11ValueName = "SP" 
strFramework10File = ofs.GetSpecialFolder(WindowsFolder) & _ 
"\Microsoft.Net\Framework\v1.0.3705\Mscorcfg.dll" 


 
Set objRegistry = GetObject("winmgmts:{impersonationLevel=Impersonate}!\\" & _ 
strComputer & "\root\default:StdRegProv") 


 
On Error Resume Next 
For Each Key in arrKeysToCheck 
Result = objRegistry.GetStringValue HKEY_LOCAL_MACHINE, strKey & "\" & Key, _ 
"DisplayName", strDisplayName 
If Err.Number = 0 and Result = 0 Then 
Found = true 
Select Case strDisplayName 
Case "Microsoft .NET Framework 1.1" 
objRegistry.GetDWORDValue ,strFramework11Key, _ 
strFramework11ValueName,ValueData 
' НАЧАЛО ФРАГМЕНТА A 
Select Case cInt(ValueData) 
Case 1 : WScript.Echo "Microsoft .NET Framework 1.1 SP 1 found" 
Case Else : WScript.Echo "Microsoft .NET Framework 1.1 found" 
End Select 
' КОНЕЦ ФРАГМЕНТА A 
Case "Microsoft .NET Framework (English)", _ 
"Microsoft .NET Framework (English) v1.0.3705" 
' НАЧАЛО ФРАГМЕНТА B 
If ofs.FileExists(strFramework10File) Then 
FileVersion = ofs.GetFileVersion(strFramework10File) 
' КОНЕЦ ФРАГМЕНТА B 
' НАЧАЛО ФРАГМЕНТА C 
Select Case FileVersion 
Case "1.0.3705.000" 
WScript.Echo "Microsoft .NET Framework 1.0 found" 
Case "1.0.3705.209" 
WScript.Echo "Microsoft .NET Framework 1.0 SP 1 found" 
Case "1.0.3705.288" 
WScript.Echo "Microsoft .NET Framework 1.0 SP 2 found" 
Case "1.0.3705.6018" 
WScript.Echo "Microsoft .NET Framework 1.0 SP 3 found" 
End Select 
' КОНЕЦ ФРАГМЕНТА C 
End If 
End Select 
End If 
Next 


 
If Not Found Then WScript.Echo ".Net Framework is not installed on this system" 


 
Set ofs = nothing 
Set objRegistry = nothing