понедельник, 26 ноября 2012 г.

Добавим к C# немного PowerShell`а

Как я уже описывал тут и тут, мы избрали PowerShell в качестве языка для разработки сценариев управления инфраструктурой. Дело осталось за малым – научиться вызывать PowerShell скрипты из серверного приложения, написанного на C#.

Всё новое – это неизвестное старое…
На самом деле данная технология достаточно старая, но при этом она почему-то не особо распространена. Возможно, люди просто боятся её использовать. Может быть отчасти это связано с тем, что соответствующие статьи на MSDN до сих пор помечены вот таким не хитрым предупреждением:
This topic is pre-release documentation and is subject to change in future releases. Blank topics are included as placeholders.
Как следствие мало информации о самой возможности интегрирования PowerShell и .NET. Так или иначе, но мы наблюдаем это предупреждение уже около 3-ёх лет и спокойно пользуемся данными возможностями.

Итак, вы всё-таки решились, невзирая на предупреждение Microsoft, добавить в ваше приложение интеграцию c PowerShell`ом. В таком случае ищите все необходимые классы для управления окружением PowerShell 2.0 и вызовом скриптов из .NET в пространстве имён System.Management.Automation, которое располагается в одноимённой сборке. Правда сначала вам придётся найти саму сборку . Дело в том, что она отсутствует в списке .NET компонентов, доступных для подключения к вашему проекту:
Сборки нет в списке компонентов доступных для добавления к проекту

В итоге вопрос "Где найти System.Management.Automation.dll?" – самый популярный среди разработчиков, впервые решивших использовать данную технологию.

В большинстве случаев вам придётся поставить Windows SDK, тогда она появится в каталоге "C:\Program Files\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0". Но даже после этого, добавлять к проекту её придётся руками по прямой ссылке на файл.

Второй способ попадания сборки System.Management.Automation.dll в систему – это установка PowerShell 2.0. В этом случае её можно найти в GAC`е примерно здесь: C:\Windows\assembly\GAC_MSIL\System.Management.Automation

Добавим немного кода
На этом самая сложная часть осталась позади, потому что собственно код, позволяющий выполнить PowerShell скрипт из приложения на C#, прост до нельзя. Вот он:
using System;
using System.Collections.Generic;
using System.Collections;
using System.Management.Automation.Runspaces;
using System.Management.Automation;

// ...

private
IEnumerable<string> Call(string script, Hashtable args)
{
    InitialSessionState state = InitialSessionState.CreateDefault();
    state.Variables.Add(new SessionStateVariableEntry(
        "ErrorActionPreference", "Stop", null));
    state.Variables.Add(new SessionStateVariableEntry(
        "Arguments", args, null));
    using (Runspace runspace = RunspaceFactory.CreateRunspace(state))
    {
        runspace.Open();
        using (PowerShell shell = PowerShell.Create())
        {
            shell.Runspace = runspace;
            shell.AddScript("Set-PSDebug -Strict\n" + script);
            try
            {
                return new List<string>(
                    from PSObject obj in shell.Invoke()
                    where obj != null select obj.ToString());
            }
            catch (RuntimeException psError)
            {
                ErrorRecord error = psError.ErrorRecord;
                return error.InvocationInfo == null
                    ? FormatErrorSimple(error.Exception)
                    : FormatError(error.InvocationInfo, error.Exception);
            }
        }
    }
}

Давайте разберёмся, что же здесь происходит. Для начала я создаю конфигурацию сессии, используя конфигурацию по-умолчанию в качестве шаблона, и добавляю к ней две глобальный переменные: ErrorActionPreference со значением Stop и Arguments со значением типа System.Hashtable, которое моя функция получает из вне.
InitialSessionState state = InitialSessionState.CreateDefault();
state.Variables.Add(new SessionStateVariableEntry(
    "ErrorActionPreference", "Stop", null));
state.Variables.Add(new SessionStateVariableEntry(
    "Arguments", args, null));

Первая переменная меняет политику обработки ошибок в скриптах. PowerShell – это в первую очередь язык написания командных сценариев со всеми вытекающими отсюда особенностями. В частности, по-умолчанию он реагирует на ошибки так же, как командная оболочка Windows: выводит сообщение и продолжает выполнение скрипта. Изменив подобным образом значение переменной ErrorActionPreference в глобальном контексте, мы изменяем это поведение на противоположное: любая ошибка будет приводить к моментальному завершению и выбросу исключения, которое мы можем легко и не принуждённо поймать и обработать.

С помощью второй переменной, как, наверное, не сложно догадаться из названия, я передаю параметры моему скрипту. Мы экспериментировали с разными способами – этот оказался самый удобный. Обратите внимание на тип передаваемой переменной: в таком же виде она будет видна и скрипту, а именно, как переменная с именем Arguments и типом hashtable:
$DeviceMAC = $Arguments["DeviceMAC"]

Затем я создаю и открываю командную оболочку PowerShell, представленную экземпляром класса System.Management.Automation.Runspaces.Runspace. К ней я присоединяю новый конвейер команд (pipeline) PowerShell`а:
using (Runspace runspace = RunspaceFactory.CreateRunspace(state))
{
    runspace.Open();
    using (PowerShell shell = PowerShell.Create())
    {
        shell.Runspace = runspace;
//...
    }
}

Обрати особое внимание: класс System.Management.Automation.PowerShell представляет собой конвейер команд, а не само окружение PowerShell, как это могло бы показаться на первый взгляд. Это странное именование может ввести в заблуждение. Второй интересный момент: поскольку это именно "конвейер" команд, то все отдельно добавленные к нему команды или целые скрипты считаются объединёнными с помощью операции "|" (в простонародье "труба"). Именно эта особенность объясняет, почему я воспользовался банальной конкатенацией строк, чтобы добавить свою команду к скрипту, который я хочу выполнить, а не вызвал метод AddScript 2 раза:
shell.AddScript("Set-PSDebug -Strict\n" + script);

Собственно добавленная команда стоит отдельного упоминания. Эта команда – ещё один способ сгладить скриптовую природу PowerShell. Она всего лишь запрещает использование не инициализированных переменных, что сильно упрощает поиск ошибок в скриптах.

Всё что осталось – это вызвать скрипт, прочитать результат и обработать ошибки:
try
{
    return new List<string>(
        from PSObject obj in shell.Invoke()
        where obj != null select obj.ToString());
}
catch (RuntimeException psError)
{
    ErrorRecord error = psError.ErrorRecord;
    return error.InvocationInfo == null
        ? FormatErrorSimple(error.Exception)
        : FormatError(error.InvocationInfo, error.Exception);
}

Как не сложно понять из кода, результат выполнения скрипта возвращается в виде коллекции объектов типа System.Management.Automation.PSObject. А ошибки, благодаря глобальной переменной ErrorActionPreference, значение которой мы выставили в Stop, мы получаем, обработав исключение System.Management.Automation.RuntimeException.

Вместо заключения
Мы использовали этот метод для подключения PowerShell 2.0 к нашей системе управления инфраструктурой. С выходом Windows Server 2012 стал доступен PowerShell третьей версии. Честно признаюсь: пока что мы ещё не ставили экспериментов по интеграции его с приложением на C#. Сегодня я отбываю на MS TechEd 2012, где обязательно постараюсь задать вопрос Джефри Сноверу о самой возможности такой интеграции вообще.

Архив с полным исходным кодом проекта можно скачать здесь.

Комментариев нет:

Отправить комментарий