воскресенье, 16 февраля 2014 г.

Регистрация windows — сервисов

Наверное, многим программистам приходится иметь дело с windows—сервисами. Часто - с теми, которые пишутся ими самими или коллегами. Такие сервисы имеют интересную особенность - разрабатывая их или обращаясь к ним в своём коде часто удобно запускать сервисы как обычные приложения (как правило, консольные, но иногда даже и с неким графическим интерфейсом). О том, как наилучшим образом обеспечить эту возможность и пойдёт речь.

Все сервисы, что мне приходилось видеть в проектах, где я работал, были сделаны в расчёте на то, что их можно запускать "как обычное приложение". Это и правда очень удобно: запускать можно с помощью иконки в привычном месте, а завершать, закрывая консоль или главное окно. Так приятнее, чем отыскивать нужную строчку в Management Console.

Мне приходилось видеть множество способов решения такой задачи: обычно, для запуска сервиса "как приложения", при запуске ему передавался параметр "/Console" и в функции Main проверялся этот параметр. Где-то в exe-проекте сервиса код для запуска-остановки делался public, потом делался отдельный exe-проект, в который добавлялся reference на exe-проект сервиса и вызывался "сервисный" код. Иногда, в Main проверялось значение Environment.UserInteractive.

Все эти способы мне кажутся не подходящими:

  • Запускать приложение, передавая ему параметр ("/Console") неудобно, когда вы нашли приложение в любимом файл-менеджере. Намного менее удобно, чем просто взять и запустить безо всяких параметров.
  • Делать отдельный проект для приложения и вызывать из него код из другого exe-проекта - можно я вообще оставлю это без коментариёв?
  • Environment.UserInteractive является лишь косвенным признаком - свойство может возвращать true и в случае запуска приложения как сервиса.
Для меня удивительно, что я нигде "в живую" не видел, что бы использовали другой подход - если приложение запущено с некоторым специальным параметром, например, "-Service", то считать, что оно запущено как сервис, а если без оного, то как обычное приложение. Это же гораздо удобнее в использовании, чем первых подход!

Видимо, редкость применения этого подхода вызвана тем, что явных простых способов его реализации нет. Выставлением каких-либо понятных свойств или вызовом подходящих методов добиться требуемого нельзя. Значит, We Need To Go Deeper!

Создавая в MSVS проект Windows-сервиса, вы получаете в распоряжение класс инсталлятора (наследника Installer). В этом классе мы и сделаем всё, что нам нужно. А нужно нам изменить значение, отвечающее за путь к исполняемому файлу в параметрах установки. Это значение хранится под ключом "AssemblyPath" в словаре Parameters контекста инсталлятора. То есть, нам достаточно сделать в классе инсталлятора следующее:

protected override void OnBeforeInstall(IDictionary savedState) {
  const string AssemblyPathContextKey = "AssemblyPath";
  var path = Context.Parameters[AssemblyPathContextKey];
  Context.Parameters[AssemblyPathContextKey] = "\"" + path + "\" -Service";
  base.OnBeforeInstall(savedState);
}

Кажется, достаточно просто для того, чтобы везде это использовать. Единственно, что для инсталляции сервиса теперь необходимо воспользоваться стандартными .NET-средствами, а именно InstallUtil.

Так же, при отладке и тестировании сервиса оказалось удобным иметь возможность извне задавать различные параметры сервиса: такие как имя, способ запуска (автоматический/ручной) или зависимости. В коде инсталлятора эти параметры так же доступны в контексте, как и "AssemblyPath" выше. Пример такой настраиваемой инсталляции:

InstallUtil /ServiceName="My Custom Name" /StartType="Manual" /DependedOn="MSSqlServer;FtpSvc" "MyService.exe"

Надо лишь так же обработать эти параметры в OnBeforeInstall (а изменение имени сервиса - и в OnBeforeUninstall).

Вдобавок, не мешает и возможность само-регистрации в сервисе. То есть, для того, что бы зарегистрировать сервис не нужно использовать InstallUtil, а достаточно запустить исполняемый файл с параметром /Install (или /Uninstall для деинсталляции):

MyService /Install /ServiceName="My Custom Name" /StartType="Manual" /DependedOn="MSSqlServer;FtpSvc"

Учитывая всё вышесказанное, функция Main сервиса будет выглядеть примерно так:

private const int SuccessExitCode = 0;
private const int FailedExitCode = 1;

private static int Main(string[] args) {
  try {
    // Проверяем, запущенна ли само-инсталляция:
    if(ServiceInstall.Install<ProjectInstaller>(args)) {
      return SuccessExitCode;
    } else if(CommandLine.Contains(args, CommandLine.Service)) {
      // Стартуем как сервис
      return StartService(args);
    } else {
      // Стартуем консольное приложение
      return StartConsole(args);
    }//if
  } catch(Exception ex) {
    Console.WriteLine(ex);
    throw;
  }//try
}

Класс ServiceInstall, в котором реализованы все "помогаторы" для удобной инсталляции сервиса я опубликовал на гитхабе в проекте MyServiceInstaller. Там же есть инсталлятора сервиса, использующий класс ServiceInstall — в качестве примера того, как просто использовать его в ваших инсталляторах.