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