Привет! Сегодня в продолжение цикла статей о создании инсталляторов мы рассмотрим, как научить инсталлятор выполнять какие-то дополнительные действия, не предусмотренные 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.
Комментариев нет:
Отправить комментарий