Наверное, многим программистам приходится иметь дело с windows—сервисами. Часто - с теми, которые пишутся ими самими или коллегами. Такие сервисы имеют интересную особенность - разрабатывая их или обращаясь к ним в своём коде часто удобно запускать сервисы как обычные приложения (как правило, консольные, но иногда даже и с неким графическим интерфейсом). О том, как наилучшим образом обеспечить эту возможность и пойдёт речь.
Все сервисы, что мне приходилось видеть в проектах, где я работал, были сделаны в расчёте на то, что их можно запускать "как обычное приложение". Это и правда очень удобно: запускать можно с помощью иконки в привычном месте, а завершать, закрывая консоль или главное окно. Так приятнее, чем отыскивать нужную строчку в Management Console.
Мне приходилось видеть множество способов решения такой задачи: обычно, для запуска сервиса "как приложения", при запуске ему передавался параметр "/Console" и в функции Main проверялся этот параметр. Где-то в exe-проекте сервиса код для запуска-остановки делался public, потом делался отдельный exe-проект, в который добавлялся reference на exe-проект сервиса и вызывался "сервисный" код. Иногда, в Main проверялось значение Environment.UserInteractive.
Все эти способы мне кажутся не подходящими:
- Запускать приложение, передавая ему параметр ("/Console") неудобно, когда вы нашли приложение в любимом файл-менеджере. Намного менее удобно, чем просто взять и запустить безо всяких параметров.
- Делать отдельный проект для приложения и вызывать из него код из другого exe-проекта - можно я вообще оставлю это без коментариёв?
- Environment.UserInteractive является лишь косвенным признаком - свойство может возвращать true и в случае запуска приложения как сервиса.
Видимо, редкость применения этого подхода вызвана тем, что явных простых способов его реализации нет. Выставлением каких-либо понятных свойств или вызовом подходящих методов добиться требуемого нельзя. Значит, We Need To Go Deeper!
Создавая в MSVS проект Windows-сервиса, вы получаете в распоряжение класс инсталлятора (наследника Installer). В этом классе мы и сделаем всё, что нам нужно. А нужно нам изменить значение, отвечающее за путь к исполняемому файлу в параметрах установки. Это значение хранится под ключом "AssemblyPath" в словаре Parameters контекста инсталлятора. То есть, нам достаточно сделать в классе инсталлятора следующее:
const string AssemblyPathContextKey = "AssemblyPath";
var path = Context.Parameters[AssemblyPathContextKey];
Context.Parameters[AssemblyPathContextKey] = "\"" + path + "\" -Service";
base.OnBeforeInstall(savedState);
}
Кажется, достаточно просто для того, чтобы везде это использовать. Единственно, что для инсталляции сервиса теперь необходимо воспользоваться стандартными .NET-средствами, а именно InstallUtil.
Так же, при отладке и тестировании сервиса оказалось удобным иметь возможность извне задавать различные параметры сервиса: такие как имя, способ запуска (автоматический/ручной) или зависимости. В коде инсталлятора эти параметры так же доступны в контексте, как и "AssemblyPath" выше. Пример такой настраиваемой инсталляции:
Надо лишь так же обработать эти параметры в OnBeforeInstall (а изменение имени сервиса - и в OnBeforeUninstall).
Вдобавок, не мешает и возможность само-регистрации в сервисе. То есть, для того, что бы зарегистрировать сервис не нужно использовать InstallUtil, а достаточно запустить исполняемый файл с параметром /Install (или /Uninstall для деинсталляции):
Учитывая всё вышесказанное, функция Main сервиса будет выглядеть примерно так:
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 — в качестве примера того, как просто использовать его в ваших инсталляторах.
Комментариев нет:
Отправить комментарий