[.NET, C#, Разработка под Windows] Пишем установщик на WixSharp. Плюшки, проблемы, возможности

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
03-Мар-2021 13:33


Каждый маломальский проект сталкивается с дистрибьюцией продукта. В нашем случае это коробочный вариант и так исторически сложилось, что мы предоставляем нашим заказчикам установщик, который должен сделать уйму всего в системе, тем самым упростив заказчику этап внедрения.В первой своей реинкарнации это было решение из множества приложений, которые дергали друг друга и все это подавалось под соусом InnoSetup. Масштабировать функционал уже не представлялось возможным. И мы пришли к решению пересесть на "новые рельсы" и тут понеслось…Знакомство с Wix, а затем и WixSharpВыбор пал на Wix. Но желающих писать xml скрипты Wix в команде не оказалось. Основной приоритет отдавали C#. И, ура, был замечен фреймворк называемый Wix# (WixSharp). По Wix# написано немало статей в сети и есть интересный перевод статьи на Хабре. В каждой статье авторы пытаются донести свой уникальный опыт и помочь читателям с пользой воспользоваться материалом. Поэтому и мы решили поделиться своим опытом с вами.Wix# позволяет реализовать большинство сценариев установки и обновления msi. Также, есть возможность дополнить функционал путем подключения wix расширений и описания новых сущностей Wix. Приятно было обнаружить возможность прикрутить WPF. Однако на начальном этапе мы приняли решение написать формы на WinForm. И в процессе мы выявили ряд важных моментов, про которые расскажем ниже.Особенности работы с WinFormПри проработке форм установщика мы поняли, что сделать простые, не перегруженные формы для наших потребностей невозможно. Поэтому каждая форма требовала лаконичного размещения контролов с учетом ограничений в размерах форм в msi.
Пример формыВ итоге по формам мы смогли более менее раскидать необходимый функционал. И его можно расширить, добавив еще пару-тройку новых форм. Но...Первое, что стало бросаться в глаза при добавлении новых форм, это то, что отрисовка была с "запозданием". Наблюдалось мерцание форм при переходе от одной к другой. Происходило это при подгонке размеров контролов во вновь инициализированной форме под разрешение текущего экрана. В этом оказалось особенность работы с WinForm msi.На данный момент мы остановились на этой реализации. А в ближайшее время запланировали переписать UI на WPF.Основной модульВ основном модуле мы описываем все необходимые опции проекта. В примерах разработчика Wix# обычно это один модуль, в котором перечислена реализация всех опций. Выглядит это так:
var binaries = new Feature("Binaries", "Product binaries", true, false);
var docs = new Feature("Documentation", "Product documentation (manuals and user guides)", true);
var tuts = new Feature("Tutorials", "Product tutorials", false);
docs.Children.Add(tuts);
var project =
  new ManagedProject("ManagedSetup",
    new Dir(@"%ProgramFiles%\My Company\My Product",
      new File(binaries, @"Files\bin\MyApp.exe"),
      new Dir("Docs",
        new File(docs, "readme.txt"),
        new File(tuts, "setup.cs"))));
project.Binaries = new[]
{
  new Binary(new Id("EchoBin"), @"Files\Echo.exe")
};
project.Actions = new WixSharp.Action[]
{
  new InstalledFileAction("registrator_exe", "/u", Return.check, When.Before, Step.InstallFinalize, Condition.Installed),
  new InstalledFileAction("registrator_exe", "", Return.check, When.After, Step.InstallFinalize, Condition.NOT_Installed),
  new PathFileAction(@"%WindowsFolder%\notepad.exe", @"C:\boot.ini", "INSTALLDIR", Return.asyncNoWait, When.After, Step.PreviousAction, Condition.NOT_Installed),
  new ScriptAction(@"MsgBox ""Executing VBScript code...""", Return.ignore, When.After, Step.PreviousAction, Condition.NOT_Installed),
  new ScriptFileAction(@"Files\Sample.vbs", "Execute" , Return.ignore, When.After, Step.PreviousAction, "NOT Installed"),
  new BinaryFileAction("EchoBin", "Executing Binary file...", Return.check, When.After, Step.InstallFiles, Condition.NOT_Installed)
  {
    Execute = Execute.deferred
  }
};
project.Properties = new[]
{
    new Property("Gritting", "Hello World!"),
    new Property("Title", "Properties Test"),
    new PublicProperty("NOTEPAD_FILE", @"C:\boot.ini")
}
project.GUID = new Guid("6f330b47-2577-43ad-9095-1861ba25889b");
project.ManagedUI = ManagedUI.Default;
project.UIInitialized += Project_UIInitialized;
project.Load += msi_Load;
project.AfterInstall += msi_AfterInstall;
В своем проекте у нас получилось гораздо больше различных опций и их реализаций. Поэтому мы разнесли все опции по модулям и получили лаконичный вид:
var project = new ManagedProject(ProjectConstants.PROJECT_NAME)
{
  GUID = new Guid(ProjectConstants.PROJECT_GUID),
  Platform = Platform.x64,
  UpgradeCode = new Guid(ProjectConstants.PROJECT_GUID),
  InstallScope = InstallScope.perMachine,
  Description = ProjectConstants.COMPANY_NAME,
  Language = "ru-RU",
  LocalizationFile = @"WixUI_ru-ru.wxl",
  ControlPanelInfo = productInfo,
  MajorUpgradeStrategy = upgradeStrategy,
  MajorUpgrade = majorUpgrade,
  DefaultRefAssemblies = RefAssembliesGenerator.InitializeRefAssemblies(),
  GenericItems = GenericEntitiesGenerator.InitializeGenericEntities(),
  Properties = PropertiesGenerator.InitializeProperties(),
  Dirs = DirsGenerator.InitializeDirs(),
  Binaries = BinariesGenerator.InitializeBinaries(),
  Actions = ActionsGenerator.InitializeActions(),
  ManagedUI = new ManagedUI(),
  ReinstallMode = "amus"
};
Глобальные переменные msiВ нашем проекте мы столкнулись с необходимостью объявить не один десяток свойств (Property), которые, подобно глобальным переменным, могут использоваться практически во всех местах установки, как при работе с формами, так и при обработке Custom Action. Обращение к этим переменным происходит по их имени в текстовом виде. Например, объявив свойство new Property("Gritting", "Hello World!") в конструкторе проекта, далее, чтобы получить к этому свойству доступ, например, из диалога, нужно обратиться к Runtime.Session["Gritting "]Такое обращение к переменным требовало от разработчика помнить, как называется нужное ему свойство и в случае некорректного значения, ошибка была бы обнаружена только в runtime и при отработке именно того куска кода, где была допущена опечатка.В итоге мы решили переместить все свойства и их значения в enum и упростить работу с чтением и записью этих свойств. Объявление свойств стало выглядеть следующим образом:
public enum eProperties
{
  [Value("Hello World!"))]
  GRITTING,
  [Value("Properties Test "))]
  TITLE,
  // перечисление других свойств
}
А сама генерация свойств на основе enum так:
public static class PropertiesGenerator
{
  private static Property InizializeProperty(string propertyName, string propertyValue)
  {
    return new Property(new Id(propertyName), propertyName, propertyValue) { IsDeferred = true };
  }
  private static IList<T> ToTypedList<T>(Type entityType, Func<Enum, T> createFunc)
  {
    if (createFunc != null
      && entityType.IsEnum)
    {
      return Enum.GetValues(entityType)
        .Cast<Enum>()
        .Select(createFunc)
        .ToList();
    }
    return null;
  }
  public static Property[] InitializeProperties()
  {
    return ToTypedList(typeof(eProperties),
      e => InizializeProperty(e.ToString(), e.GetPropertyValue()))
      .ToArray();
  }
}
Обращение к свойствам из диалога тоже изменилось. На чтение стало this.GetData(GRITTING), а на запись this.SetData(GRITTING, “New value”), где GetData() и SetData() методы расширения для класса ManagedForm.Для обращения из Custom Action стало session.Data(GRITTING)MSI нужны зависимые библиотекиВ ходе работы над установщиком у нас появилась необходимость подключать дополнительные библиотеки (например для работы с СУБД Postgre). Сначала мы подключали все ручками, как было описано в документации Wix#:
project.DefaultRefAssemblies.Add("FontAwesome.Sharp.dll");
project.DefaultRefAssemblies.Add("Newtonsoft.Json.dll");
project.DefaultRefAssemblies.Add("ManagedOpenSsl.dll");
Однако из-за того, что стало возрастать количество зависимостей в проекте, мы решили не делать точечное добавление библиотек, а написали метод, который считывает список всевозможных dll из указанного ресурса:
private static List<string> GetResourceList(string resourcesDirPath) =>
  Directory.GetFiles($@"{resourcesDirPath}")
    .Where(file => file.EndsWith("dll"))
    .ToList();
public static List<string> InitializeRefAssemblies() =>
  GetResourceList(Application.StartupPath)
    .Concat(GetResourceList("Resources"))
    .ToList();
Инициализация каталоговC добавлением каталогов все оказалось, более или менее, очевидно и понятно. Указываем иерархию каталогов с добавлением в них необходимых артефактов и, по необходимости, фильтруем файлы по названиям и расширениям:
private static bool ServicePredicate(string file) => !file.EndsWith(".pdb");
private static IEnumerable<WixEntity> InitializeDirWixEntities(object dirName)
{
  var items = new List<WixEntity>();
  items.AddRange(new List<WixEntity>
  {
    new Dir("logs"),
    new DirFiles($@"Sources\{dirName}\*.*", ServicePredicate)
  });
  return new[] { new Dir(dirName.ToString(), items.ToArray()) };
}
private static WixEntity[] InilizeDirItems() =>
  new List<WixEntity>()
    .Concat(InitializeDirWixEntities(FirstService))
    .Concat(InitializeDirWixEntities(SecondService))
    .Concat(InitializeDirWixEntities(ThirdService))
    .Concat(InitializeDirWixEntities(FourthService))
    .ToArray();
public static Dir[] InitializeDirs() =>
  new[]
  {
    new Dir(@"%ProgramFiles%\CompanyName",
      new Dir("distr",
        new Dir(FluentMigrator, GetMigratorFileList("FluentMigrator")),
        ),
      new Dir("app", InilizeDirItems())
    )
  };

Развертывание сайтов на IISОдна из задач нашего установщика - это развернуть определенное количество сайтов/сервисов на IIS, при этом должна учитываться возможность включения https с указанием сертификата ssl. Из коробки Wix# такого не умел (до выпуска версии 1.14.3). Поэтому была описана кастомная сущность Wix, которая использовала расширение WixExtension.Iis.Базовый класс, описывающий Wix сущность для создания сайта на IIS:
public abstract class IISWebSite: WixEntity, IGenericEntity
{
  [Xml]
  public string Condition;
  [Xml]
  public string Description;
  [Xml]
  public string IpAddress;
  [Xml]
  public string Port;
  protected string Prefix { get; }
  protected XElement Component { get; private set; }
  protected string DirId { get; private set; }
  protected string DirName { get; private set; }
  protected string WebAppPoolId { get; private set; }
  protected IISWebSite(string prefix)
  {
    Prefix = prefix;
  }
  public virtual void Process(ProcessingContext context)
  {
    context.Project.Include(WixExtension.IIs);
    DirId = context.XParent.Attribute("Id").Value;
    DirName = context.XParent.Attribute("Name").Value;
    var componentId = $"{DirName}.{Prefix}.Component.Id";
    Component = new XElement(XName.Get("Component"),
      new XAttribute("Id", componentId),
      new XAttribute("Guid", WixGuid.NewGuid(componentId)),
      new XAttribute("KeyPath", "yes"));
    context.XParent.Add(Component);
    Component.Add(new XElement("Condition", new XCData(Condition)));
    WebAppPoolId = $"{DirName}.{Prefix}.WebAppPool.Id";
    Component.Add(new XElement(WixExtension.IIs.ToXName("WebAppPool"),
      new XAttribute("Id", WebAppPoolId),
      new XAttribute("Name", $"AppPool{Description}")
    ));
  }
}
Далее класс-наследник, для cоздания сайта с подключением по http:
public sealed class IISWebSiteHttp : IISWebSite
{
  public IISWebSiteHttp() : base("Http")
  {
  }
  public override void Process(ProcessingContext context)
  {
    base.Process(context);
    Component.Add(new XElement(WixExtension.IIs.ToXName("WebSite"),
      new XAttribute("Id", $"{DirName}.{Prefix}.WebSite.Id"),
      new XAttribute("Description", Description),
      new XAttribute("Directory", DirId),
      new XElement(WixExtension.IIs.ToXName("WebAddress"),
        new XAttribute("Id", $"{DirName}.{Prefix}.WebAddress.Id"),
        new XAttribute("IP", IpAddress),
        new XAttribute("Port", Port),
        new XAttribute("Secure", "no")
        ),
      new XElement(WixExtension.IIs.ToXName("WebApplication"),
        new XAttribute("Id", $"{DirName}.{Prefix}.WebSiteApplication.Id"),
        new XAttribute("WebAppPool", WebAppPoolId),
        new XAttribute("Name", $"AppPool{Description}"))
      ));
  }
}
И класс-наследник для создания сайта с подключением по https и с возможностью привязки сертификата ssl:
public sealed class IISWebSiteHttps : IISWebSite
{
  private readonly bool _haveCertRef;
  public IISWebSiteHttps(bool haveCertRef) : base(haveCertRef ? "HttpsCertRef" : "Https")
  {
    _haveCertRef = haveCertRef;
  }
  public override void Process(ProcessingContext context)
  {
    base.Process(context);
    var siteConfig = new XElement(WixExtension.IIs.ToXName("WebSite"),
      new XAttribute("Id", $"{DirName}.{Prefix}.WebSite.Id"),
      new XAttribute("Description", Description),
      new XAttribute("Directory", DirId),
      new XElement(WixExtension.IIs.ToXName("WebAddress"),
        new XAttribute("Id", $"{DirName}.{Prefix}.WebAddress.Id"),
        new XAttribute("IP", IpAddress),
        new XAttribute("Port", Port),
        new XAttribute("Secure", "yes")
      ),
      new XElement(WixExtension.IIs.ToXName("WebApplication"),
        new XAttribute("Id", $"{DirName}.{Prefix}.WebSiteApplication.Id"),
        new XAttribute("WebAppPool", WebAppPoolId),
        new XAttribute("Name", $"AppPool{Description}")));
    if (_haveCertRef)
    {
      siteConfig.Add(new XElement(WixExtension.IIs.ToXName("CertificateRef"),
        new XAttribute("Id", IISConstants.CERTIFICATE_ID)));
    }
    Component.Add(siteConfig);
  }
}
Все параметры сайта указываются на формах и передаются через глобальные переменные в новый экземпляр объекта.Через некоторое время в релиз Wix# добавили схожее расширение. Но, в отличии от реализации во фреймворке, наше расширение позволяет менять протокол у сайтов и делать привязку сертификата ssl.Инициализация наших объектов получилась следующая:
new List<WixEntity>
  {
    new IISWebSiteHttp
    {
      Condition = HttpSiteCondition, Description = siteName,
      IpAddress = ipAddress,
      Port = port
    },
    new IISWebSiteHttps(false)
    {
      Condition = HttpsSiteWithoutCertCondition, Description = siteName,
      IpAddress = ipAddress,
      Port = port
    },
    new IISWebSiteHttps(true)
    {
      Condition = HttpsSiteWithCertCondition, Description = siteName,
      IpAddress = ipAddress,
      Port = port
    }
  }

Создаем БД из msiВ предыдущей версии установщика, создание/обновление БД делало внешнее приложение. Так как Wix# позволяет запускать свои Custom Action, мы решили добавить возможность создания и обновления БД прямо в msi. В формах заносятся первичные данные по БД (провайдер, адрес сервер, название) и в Custom Action передаются эти данные через глобальные переменные:
[CustomAction]
public static ActionResult ExecMigratorRunner(Session session)
{
  var workDir = session.Data(MIGRATOR_FILE_DIR);
  var appCmdFile = $@"{workDir}{session.Data(MIGRATOR_FILE_NAME)}";
  var args = session.Data(MIGRATOR_ARGS);
  return ProccessHelper.RunApplication(appCmdFile, args);
}
Опытный читатель, скорее всего, спросит, почему не использовали коробочные решения Wix#? Например такое:
var project = new Project("MyProduct",
  new Dir(@"%ProgramFiles%\My Company\My Product",
    new File(@"Files\Bin\MyApp.exe")),
  new User("James") { Password = "Password1" },
  new Binary(new Id("script"), "script.sql"),
  new SqlDatabase("MyDatabase0", ".\\SqlExpress", SqlDbOption.CreateOnInstall,
    new SqlScript("script", ExecuteSql.OnInstall),
    new SqlString("alter login Bryce with password = 'Password1'", ExecuteSql.OnInstall)
    )
  );
Все просто. В нашем проекте используется Fluent Migrator. И для разворачивания новой БД нужна только собранная библиотека, которую нужно вызвать через командную строку с параметрами, содержащими информацию по создаваемой БД. А поддержка различных провайдеров СУБД ложится уже на саму библиотеку.Сценарий обновления БД реализуется по всем канонам накатывания миграций.Какой еще функционал мы реализовали?
  • Назначение прав доступа на папки приложения. Через CustomAction, т.к. коробочное решение раздает права в определенные момент установки, и мы не нашли возможности переиспользовать наработки Wix.
  • Добавление пользователей БД и СУБД (через CustomAction, по тем же причинам).
  • Добавление сертификата ssl в локальное хранилище.
  • Привязка вновь добавленных сертификатов ssl к сайтам на IIS (через CustomAction).
  • Принудительный запуск сайтов на IIS (через CustomAction).
  • Обновление старой версии БД (до внедрения Fluent Migrator) путем запуска скрипта t-sql (через CustomAction).
  • Проверка соединения с сервером БД (на форме).
  • Проверка соединения с сервером RabbitMQ (на форме).
  • Проверка сайтов и их адресов на уникальность на текущей IIS (на форме).
  • Проверка необходимых компонентов на текущей машине (на форме).
А как же обновление?Да. Без этого сценария, установщик для нас и заказчиков стал бы бесполезным.Мы рассмотрели различные варианты обновлений доступных через msi и остановились на major upgrade. Нам нет необходимости хранить устаревшие исполняемые файлы и, например, выпускать патчи. Нас устроил вариант с полным удалением старой версии ПО и установкой новой версии.Wix# из коробки позволяет сделать достаточно неплохую схему обновления. Но, в нашем случае, все-таки пришлось добавить несколько своих событий.Во-первых, мы сделали сохранение глобальных переменных в реестр в зашифрованном виде, чтобы была связь с предыдущей установкой. Это дало возможность отображать ранее введенные данные в формах по установленной версии.Далее добавили собственную проверку установленной версии продукта, для совместимости с предыдущими версиями установщика (приложения установленные через InnoSetup не определялись msi как тот же продукт).И защитили БД от удаления в режиме обновления.Какие у нас планы по расширению функционала?
  • Конфигурирование очередей на сервере RabbitMQ.
  • Разворачивание сервиса на IIS, написанного на Python.
  • Реализовать режим Modify средствами msi (возможность изменить введенные ранее в установщике настройки приложения).
  • Переписать UI c WinForms на WPF.
Надеемся, что наш опыт будет полезен и ждем ваши вопросы и комментарии по нашей реализации.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_.net, #_c#, #_razrabotka_pod_windows (Разработка под Windows), #_wix#, #_wixsharp, #_wix, #_msi, #_deployment, #_blog_kompanii_cross_technologies (
Блог компании Cross Technologies
)
, #_.net, #_c#, #_razrabotka_pod_windows (
Разработка под Windows
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 14:26
Часовой пояс: UTC + 5