среда, 1 апреля 2015 г.

Создание инсталлятора с помощью WiX. Часть 4. Custom Actions


Привет! Сегодня в продолжение цикла статей о создании инсталляторов мы рассмотрим, как научить инсталлятор выполнять какие-то дополнительные действия, не предусмотренные WiX из коробки.


В прошлой статье мы научились выпускать новые версии инсталлятора, которые упростили процесс обновления программного обеспечения. Теперь старая версия программы будет удаляться автоматически при установке новой. Мы уже добились немалого в создании инсталлятора, но у текущей реализации есть один изъян: при установке каждой новой версии инсталлятор будет заново запрашивать настройки системы.

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

Их можно писать на C#, C++ или Visual Basic. Custom action – это специальная библиотека, которая может выполнять нестандартные действия в системе, необходимые для установки программы. Сегодня мы напишем два Custom action’а: один будет считывать порт службы из конфигураций уже установленной версии программы, а второй будет принимать у пользователя реквизиты подключения к базе данных и проверять их корректность. Итак, начнем!

Давайте добавим к нашему инсталлятору второй проект типа C# Custom Action Project (шаблон находится в категории Templates -> Visual C# -> Windows Installer XML). Назовем его InstallerCustomActions. Будет создан проект с одним C# файлом CustomAction.cs. Описание пустого шаблона выглядит так:
public class CustomActions
{
     [CustomAction]
     public static ActionResult CustomAction1(Session session)
     {
          return ActionResult.Success;
     }
}

Из шаблона видно, что Custom Action – это просто открытый статический метод, помеченный атрибутом [CustomAction], принимающий на вход сессию инсталлятора и возвращающий результат выполнения действия. ActionResult – это перечисление, которое имеет следующий значения:
  • Failure – произошла ошибка, после которой работа инсталлятора не может быть продолжена в обычном режиме;
  • NotExecuted – action не выполнен;
  • SkipRemainingActions – оставшиеся action’ы пропущены;
  • Success – действие выполнено успешно;
  • UserExit – пользователь завершил выполнение.
С помощью этих статусов можно гибко настроить поведение custom action’а.

Объект Session позволяет управлять процессом установки. Через него можно получить всю информацию об инсталляторе: получить значения переменных, вызывать другие action’ы, логировать события и прочее.

Теперь, владея обзорными знаниями, мы создадим первый Custom Action, который будет считывать настойки порта установленной службы из ее файла конфигураций.

[CustomAction]
public static ActionResult ReadServerConfig(Session session)
{
    session.Log("Begin ReadServerConfig");

    try
    {
       string installLocation = session[InstallLocation];
       session.Log("InstallLocation: [{0}]", installLocation);

       var root = ReadConfigRoot(session, installLocation);
      if (root == null)
            return ActionResult.Success;

      int port = LoadPort(root);
      session[ServerPort] = port.ToString();
     }
     catch (Exception ex)
     {
        session.Log("Error occured on ReadServerConfig: {0}", ex.Message);
                return ActionResult.Failure;
     }

     return ActionResult.Success;
}

#region Методы парсинга файла конфигурации

private static XmlElement ReadConfigRoot(Session session, string installLocation)
{
    if (!Directory.Exists(installLocation))
    {
         session.Log("InstallLocation not foud");
         return null;
    }

    string configFile = Path.Combine(installLocation, ConfigFile);
    if (!File.Exists(configFile))
    {
         session.Log("Config file [{0}] not found", configFile);
         return null;
    }

    var doc = new XmlDocument();
    using (var reader = new StreamReader(configFile))
    {
        doc.Load(reader);
    }

     return doc.DocumentElement;
}

private static int LoadPort(XmlElement root)
{
    const string path = "descendant::service[@name='ApplicationService.Common.ApplicationService']/host/baseAddresses/add[@baseAddress]/@baseAddress";
            
    const string portPattern = @"localhost:(?<port>\d*)";

    string address = LoadAttribute(root, path);
    if (string.IsNullOrEmpty(address))
        return DefaultPort;

    var regex = new Regex(portPattern);
    var matches = regex.Match(address);
    if (!matches.Success)
          return DefaultPort;

    var portString = matches.Groups["port"].Value;

    int result;
    if (!int.TryParse(portString, out result))
          return DefaultPort;

    return result;
}

private static string LoadAttribute(XmlElement root, string path)
{
       var node = root.SelectSingleNode(path);
       return node != null ? node.Value : null;
}
#endregion

private const string InstallLocation = "INSTALLLOCATION";
private const string ConfigFile =           
                     "ApplicationWindowsService.exe.config";
private const string ServerPort = "SERVER_PORT";
private const int DefaultPort = 8888;


Несмотря на приличное количество кода, этот action выполняет вполне простые действия. Сначала он получает из сессии инсталлятора место установки службы (значение переменной INSTALLLOCATION). Это делается простым обращением к сессии по индексатору. Дальше в этой папке проверяется наличие файла ApplicationWindowsService.exe.config. И если он есть, то он открывается на чтение как обычный XML документ. Это делает метод ReadConfigRoot. Метод LoadPort принимает на вход XML и при помощи довольно простого XPath запроса и регулярного выражения возвращает номер порта. Если произошла какая-то ошибка, то метод вернет порт по умолчанию. В данном случае это константа 8888.

Стоит обратить заметить, что в примере я особо не обращаю внимания на обработку ошибок при работе action’а. Если файл не был загружен, то метод возвращает Success, чтобы можно было использовать порт  по умолчанию. Если возникло какое-то исключение, то это будет перехвачено общим обработчиком и залогировано, а action вернет статус Failure. Процесс установки будет прерван.

Второй custom action будет проверять наличие подключения к базе данных. Он принимает на вход адрес сервера, наименование базы данных, логин и пароль, и возвращает строку с текстом, которая будет показана пользователю в качестве результата проверки. Предполагается, что в проекте инсталлятора описаны все необходимые переменные.

[CustomAction]
public static ActionResult CheckDataConnection(Session session)
{
   var connString = new ConnectionStringParts
   {
        Server = session[DataServer],
        Database = session[DataDb],
        User = session[DataLogin],
        Password = session[DataPassword]
    };

    session["DATACHECKED"] = CheckDbConnection(session, connString)
              ? "Проверка подключения прошла успешно."
              : "Не удалось подключиться к базе данных.";

    return ActionResult.Success;
}
private static bool CheckDbConnection(Session session, ConnectionStringParts db)
{
    var connectionString = db.FormConnectionString();
    using (var connection = new SqlConnection(connectionString))
    {
         try
         {
             connection.Open() ;
             return true;
         }
         catch (Exception ex)
         {
              session.Log("Error occurred during connection string checking: {0}", ex.Message);
              return false;
         }
     }
}
private class ConnectionStringParts
{
    public string Server { get; set; }
    public string Database { get; set; }
    public string User { get; set; }
    public string Password { get; set; }

    public string FormConnectionString()
    {
        var connString = new SqlConnectionStringBuilder
        {
             DataSource = Server,
             InitialCatalog = Database,
             UserID = User,
             Password = Password,
             ConnectTimeout = 10,
        };
        return connString.ConnectionString;
     }
}


Тут все просто. Реквизиты подключения к базе данных считываются из переменных инсталлятора, по ним собирается строка подключения, по которой мы пытаемся подключиться с помощью объекта SqlConnection. По результату подключения формируем сообщение для пользователя. Стоит отметить, что, во-первых, в данном примере нет большой необходимости в отдельном классе для хранения реквизитов подключения к базе данных. Он был взят из реального инсталлятора, в котором он использовался еще в некоторых местах. Во-вторых, представленный здесь способ возврата статуса подключения является не самым элегантным, но вполне подходит для примера.

Теперь наша библиотека custom action’ов готова. Скомпилируем ее и подключим в качестве ссылки к проекту нашего инсталлятора. Подключить нужно как ссылку на проект, а не на скомпилированную библиотеку. Откроем файл Product.wxs и опишем в нем наши action’ы. Перед секцией проверки обновлений добавим следующий код:

<Binary Id="ServerActions" SourceFile="$(var.InstallerCustomActions.TargetDir)$(var.InstallerCustomActions.TargetName).CA.dll" />
<CustomAction Id="LoadConfigs" 
              BinaryKey="ServerActions" 
              DllEntry="ReadServerConfig" 
              Execute="immediate" 
              Return="check" />
<CustomAction Id="CheckDataDb" 
              BinaryKey="ServerActions" 
              DllEntry="CheckDataConnection" 
              Execute="immediate" 
              Return="check" />


Тег Binary используется для подключения к инсталлятору библиотеки с нашими action’ами. В поле Id указывается уникальный идентификатор библиотеки в пределах инсталлятора, а в SourceFile указывается путь до сборки с action’ами. Путь указан в виде конкатенации значений переменных системы сборки: $(var.InstallerCustomActions.TargetDir) – путь до папки bin\Debug (или bin\Release, в зависимости от конфигурации) проекта с библиотекой, $(var.InstallerCustomActions.TargetName).CA.dll – наименование библиотеки после компиляции. Библиотека custom action’ов сама по себе является неуправляемой. Когда мы создали библиотеку на C#, на первом этапе она была скомпилирована в обычную .Net сборку с именем InstallerCustomActions.dll. На втором этапе эта сборка была обработана утилитой из пакета WiX и преобразована в неуправляемую сборку, которая может быть использована в инсталляторе. Она называется InstallerCustomActions.CA.dll. Именно поэтому необходим суффикс «.CA.dll» в поле SourceFile.

Тег CustomAction описывает action в пределах инсталлятора. Он имеет следующие поля:
  • Id – уникальный идентификатор action’а, который будет использоваться для вызова;
  • BinaryKey – идентификатор библиотеки, который был описан в тэге Binary;
  • DllEntry – наименование метода в библиотеке;
  • Execute указывает, когда action должен быть выполнен. В нашем примере action должен выполняться незамедлительно;
  • Return указывает, как должен быть обработан результат выполнения. Значение check говорит, что action будет выполняться синхронно, а его результат будет проверен.
Все action’ы мы описали, осталось в формах мастера описать их использование. Для чтения порта службы из файла конфигурации необходимо отредактировать файл WixUI_ServerMaster.wxs следующим образом:

<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">NOT WIXUI_DONTVALIDATEPATH</Publish>

<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="LoadConfigs" Order="3">1</Publish>

<Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="4"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>


Мы указали, что action LoadConfig будет вызван при нажатии кнопки «Далее»  в окне выбора папки установки (InstallDirDlg). Он считает порт службы из файла конфигурации и подставит его в поле ввода.

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

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



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

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