Пара нюансов при создании службы (Windows Service) и не только
Оказывается, в VCL нет встроенных средств для запуска службы. Службу можно установить, а запускаться она будет только после перезагрузки компьютера. Довольно странно, но факт.
Решить эту проблему через WinAPI (если класс нашей службы называется TMyService):
Каким же образом сообщить службе об изменении конфигурации (чтобы она прочитала её заново – из реестра или с диска)? Проще всего – создать глобальное именованное событие (CreateEvent) и через него сигнализировать (SetEvent) из конфигуратора службе (которая будет ждать его, либо периодически проверять, через WaitForSingleObject). Но тут нас поджидает засада: при попытке открыть из конфигуратора событие (OpenEvent), созданное службой, мы получим ошибку запрета доступа (GetLastError = ERROR_ACCESS_DENIED).
Это происходит из-за того, что объект (событие) создан «системой» (службы запускаются от имени «системы»), а открывается администратором (если конфигуратор находится в том же EXE – установить/удалить службу может только администратор) или даже обычным пользователем (если конфигуратор находится в другом EXE-шнике). Тоже самое будет при попытке открыть обычным пользователем объект, созданный администратором.
Что же делать? Сначала я решил пойти по странному пути: создать объект в конфигураторе, а в службе периодически пытаться открыть его, и после успешного открытия начать проверять состояние («система» может открыть объект, созданный администратором или обычным пользователем). Но потом эта идея мне показалась костыльной, и я решил разобраться, как создать объект в службе, не защищённый от доступа админов и обычных юзеров.
Вот так это делается (в службе):
(разумеется, при необходимости создания нескольких событий нет смысла создавать и настраивать несколько дескрипторов и атрибутов безопасности, можно использовать одни и те же; при завершении работы ничего, кроме события, закрывать или освобождать не нужно)
Delphi: службы
КОМПЬЮТЕРНЫЕ КУРСЫ «ПОИСК»
Пример проекта службы
Попробуем создать демонстрационный проект простейшего сервиса для ОС Windows NT. Для этого откроем диалоговое окно New Items и на странице New дважды щелкнем по пиктограмме Service Application.
В результате Delphi создаст шаблон проекта службы с одним сервисом.
Начнем работу с конфигурирования сервиса. Для этого выберем модуль Service1 и внесем ряд изменений в его свойства:
Тип сервиса: | ServiceType:=stWin32; |
---|---|
Интерактивность: | Interactive:=true; |
Имя сервиса: | Name:=DemoService; |
Название: | DisplayName:=Демонстрация сервиса; |
Тип старта: | StartType:=stManual; |
Реакция на ошибки при старте: | ErrorSeverity:=esIgnory; |
Сохраните проект в отдельном каталоге, при этом модуль службы назовите DemoSrv.pas, а весь проект – dmsrv.dpr. В секции частных объявлений модуля DemoSrv.pas опишем две переменные:
В момент старта сервиса получаем контекст дисплея и обнуляем счетчик:
Переходим к описанию события OnExecute() сервиса. О факте работы сервиса информируем пользователя текстовой строкой, в которой выводим текущее значение счетчика. Служба остановится в случае, когда счетчик превысит значение 100 или по команде от внешней управляющей программы. Для этого внутри цикла с помощью ProcessRequests() регулярно производим асинхронный опрос менеджера служб на предмет поступления команд от внешних приложений.
Обращаю внимание, что вывод текстовой строки на экран возможен только в случае, когда сервис работает в интерактивном режиме. Событие остановки используем для освобождения дескриптора контекста устройства:
Регистрация службы средствами приложения
Для регистрации службы в операционной системе владеющее службой приложение должно быть запущено из командной строки с ключом /INSTALL. Например:
Для снятия с регистрации применяют ключ /UNINSTALL. Процесс установки сервиса сопровождается выводом уведомляющего сообщения. Для отказа от показа окна уведомления используйте ключ /SILENT. Откройте консоль управления службами компьютера, найдите в ней наш сервис «Демонстрация сервиса» и запустите его на выполнение…
Исходный код примера здесь. Выполнен на Delphi XE.
Создание служб Windows в Delphi с использованием VCL
Автор: © Александр Алексеев
Вступление
Службы работают без участия человека, поэтому они не могут иметь пользовательского интерфейса, т.к. почти всегда с этим интерфейсом некому работать. Службы начинают свою работу ещё до того, как пользователь вошёл в систему. Более того, на серверных машинах пользователь может вообще не входить в систему за всё время работы.
Как и в случае стандартных приложений Windows, Delphi позволяет легко и быстро написать службу, используя VCL (увы, чего не скажешь об оснастке к MMC).
Установка и удаление службы
Итак, запускаем Delphi, открываем в главное меню пункт File/New/Other, выбираем «Service Application»:
После этого Delphi создаст пустое приложение службы. Заметим, что в новом проекте были автоматически сгенерированы две подпрограммы:
Как уже было сказано, в одном проекте (соответственно, exe-файле) может быть несколько служб. Чтобы добавить ещё одну службу в текущий проект, лезем в File/New/Other и там выбираем «Service» (а не «Service Application», как было ранее). В BDS пункт «Service» в разделе «Delphi Files» появляется только, если текущий проект является проектом службы. В Delphi 7 и ниже пункт «Service» виден всегда, он всегда создаёт пустой модуль с TService.
В это самое окно службы мы можем добавлять невизуальные компоненты, если они нам понадобятся. Визуальные компоненты мы использовать не можем по понятным причинам: в конце концов, мы пишем фоновую службу, которая может работать, даже если пользователь не зашёл в систему, поэтому мы не можем использовать никаких интерактивных средств взаимодействия с пользователем.
Служба сообщит, что она успешно установлена:
Открываем Панель управления/Администрирование/Службы и любуемся на нашу службу: «Service1».
Заметим, что служба установилась в остановленном состоянии с заданными ей свойствами (свойства службы мы рассмотрим ниже).
Для удаления службы запускаем exe-файл с параметром «/uninstall» (дополнительно можно добавить «/silent»), например:
Вообще говоря, служба редко ставится описанным способом, т.е. «руками». Обычно все операции по установке/удалению службы возлагаются на установщик (инсталлятор) приложения. Тем не менее, все службы, написанные на Delphi с использованием VCL, обладают такой вот приятной возможностью самоустановки и самоудаления. Поэтому инсталлятор может просто вызывать exe-файл службы с нужными параметрами (с указанием «/silent»).
Свойства службы
Теперь мы приведём свою службу в божеский вид, для этого посмотрим свойства службы:
Примечание: часть свойств логически относятся к процессу работы службы, поэтому они отнесены к пункту 4. Для новой службы обычно требуется задать Name, DisplayName и StartType.
События службы
Теперь разберёмся с событиями службы.
Вариант первый: стартует служба, вы начинаете что-то делать, время от времени уведомляя систему о своём состоянии, потом служба останавливается.
Вариант второй: стартует служба, создаёт рабочие потоки. По уведомлению от системы потоки останавливаются и служба выключается.
Итак, обычно для реализации минимальной службы требуется написать обработчики OnStart/OnStop и/или OnExecute.
Примечание: В новых версиях Windows изменено поведение критических секций при выходе из программы. Вместо блокировки вы получите данные в несогласованном состоянии (но только для ресурсов, защищённых критической секцией, и только начиная с Windows XP). Подробнее см. вопрос №61596.
Хорошо, предположим, вы сохранили объект потока в переменную и при получении сигнала останова вы вызовете ему Free. Но кто сказал, что объект будет существовать в момент вызова Free? Ведь вы же установили FreeOnTerminate, что означает, что поток может завершиться (и, следовательно, объект будет удалён, а в переменной потока будет указатель на мусор), а после этого может прийти сигнал об остановке. Получим Access Violation в службе?
Хорошо, вы добавили обнуление переменной потока при завершении работы потока, а перед Free вы поставили проверку указателя на nil. Но ведь возможна и ситуация, когда сперва выполнится проверка, затем будет удалён объект потока, а только потом вызван его деструктор после вашей проверки. Конечно, можно сделать синхронизацию потоков, гарантирующую корректную работу. Но не будет ли это изобретение велосипеда вместо использования готовых средств VCL?
Методы службы
Теперь окинем взглядом рабочий инструментарий службы: методы объекта TService.
Работа службы
SCM хранит базу данных служб в ключе реестра
Обращаться напрямую к этому ключу вам не нужно, работа с ним происходит программными средствами (функциями WinAPI или обёртками VCL) и оснасткой «Службы». В этой базе данных перечислены все установленные службы и их свойства. Исключение составляет чтение/запись своей конфигурации службы. Удобно все параметры службы хранить в ключе
Так, дождавшись запуска приложения службы и установления связи с ним, SCM отправит сообщение приложению с указанием запуска запрошенной службы. И если при попытке запуска новой службы SCM обнаружит, что служба размещается в уже запущенном приложении, то он просто отправит приложению сообщение о запуске новой службы и не будет пытаться запустить новый процесс. Если в реестре путь к одному и тому же exe-файлу двух служб указан по-разному, то SCM не сумеет определить, что это один файл и запустит два процесса вместо одного.
OnCreate, OnDestroy и все события установки/удаления службы выполняются в контексте главного потока. События OnExecute, OnStart, OnStop, OnPause, OnContinue и OnShutdown выполняются в контексте потока службы.
Настройки и взаимодействие с пользователем
Одним из часто задаваемых вопросов по службам является примерно такой вопрос: «как я могу из службы сделать X для текущего пользователя?». Здесь «X» может быть, например, установка ловушки (hook), показ окна настроек и т.п.
Но кто сказал, что такой «активный» пользователь только один?
Системные логи (регистрация событий)
В принципе, сказанного должно быть вполне достаточно, чтобы написать службу. Но хотелось бы коснуться ещё одного момента: системный логгинг событий.
Откройте оснастку «Просмотр событий»:
Как видим, каждый подключ в реестре соответствует разделу логов (слева) в «Просмотре событий»:
Бонус-пак: создание своего лога.
Если вы захотите добавить свой пункт (в оснастке «Просмотр событий» он появится слева, вместе с Приложение/Безопасность/Система), то вы добавляете подраздел в ключе EventLog:
Вне зависимости от того, добавляете ли вы свой раздел логов или используете раздел Application, следующим шагом вы создаёте подраздел с именем своей службы (то, что записано в TService.Name), например:
Далее, в свежесозданном ключе добавляете следующие параметры (а вот они уже обязательны):
Для компиляции в один файл, подключаемый к exe-файлу службы, удобно составить примерно такой bat-файл:
.mc-файл имеет примерно такое содержание:
Поэтому, обычно эти строки опускаются. Дальше идут собственно сообщения. Сообщение начинается с MessageId=номер. В этих строках сообщениям присваиваются их номера (номера выбираем мы). Именно по этим номерам мы потом будем добавлять их в лог. Кстати, и этот параметр опционален. Если его не указывать, то первое сообщение получит номер 0, а последующие будут увеличиваться на 1. Дальше могут идти наборы из Severity и Facility, которые по умолчанию имеют вид:
Дальше следует язык сообщения и само сообщение. Точка в начале строки означает конец сообщения. Если языков несколько, они следуют друг за другом. В самих сообщениях можно использовать специальные символы:
То же, что и точка в начале строки, но в отличие от точки, в конце сообщения не будет вставлен перенос строки.
Вставляет точку. Может использоваться для вставки точки в начало строки, без обрыва сообщения.
Подробнее о содержимом mc-файлов можно прочитать в MSDN/Platform SDK в разделе «Message Text Files». Вот ещё пример mc-файла оттуда:
Например, если в файле msg.mc были строки:
Ну и теперь же мы можем разобраться с методом TService.LogMessage:
LogMessage (Message: String; EventType: DWord; Category, ID: Integer)
Например, чтобы добавить в лог сообщение типа «служба не сконфигурирована» (для самого первого примера mc-файла), пишем:
LogMessage (», EVENTLOG_WARNING_TYPE, 0, 3);
А чтобы добавить произвольное сообщение:
LogMessage (‘произвольный текст’, EVENTLOG_INFORMATION_TYPE, 0, 1);
К сожалению, LogMessage имеет ограничения: вы можете использовать лишь 1 параметр.
Для того чтобы использовать несколько параметров, вам придётся написать свой аналог LogMessage, например такой:
Пример вызова такой процедуры:
Кстати говоря, сообщения из подготовленной таблицы строк можно ещё извлечь для собственного использования через функцию FormatMessage.
Дополнительно
К сожалению, при своей самоустановке служба не добавляет комментарий к себе (эта возможность появилась в Windows 2000), а также не регистрирует в реестре источник логов. Поэтому, часто в службу приходится добавлять обработчики событий OnAfterInstall и OnAfterUninstall, например, так:
Во-вторых, можно использовать стандартный Run/Attach to process для подключения отладчика Delphi к уже запущенной службе. К сожалению, это не даст отлаживать код инициализации, т.к. к моменту подключения отладчика служба уже должна быть запущена. Впрочем, обходным путём можно решить проблему, например, так: вставить в начало dpr файла что-то вроде Sleep(5000) и за это время сразу после старта нужно успеть подключить отладчик Delphi.
Кстати, для управления службами в Windows есть консольные утилиты net.exe и sc.exe. Вы можете запустить их из командной строки без параметров для просмотра списка выполняемых действий.
Final words
И пару слов в завершение темы.
По поводу обработки ошибок в службах будет интересно посмотреть «вопрос КС №60359»
Об опасностях использования различных компонент без понимания принципов их работы: «вопрос КС №55674» 🙂
В частности, в статье Пример использования Private Object Security в Delphi можно посмотреть пример готовой службы.
В этой статье не рассматривались вопросы написания приложений для MMC. В Delphi нет поддержки написания таких приложений. Но в Интернете можно найти компоненты, упрощающие их разработку, например: MMC Snapin Framework.
Создание служб Windows в Delphi с использованием VCL
Автор: © Александр Алексеев
Вступление
Службы работают без участия человека, поэтому они не могут иметь пользовательского интерфейса, т.к. почти всегда с этим интерфейсом некому работать. Службы начинают свою работу ещё до того, как пользователь вошёл в систему. Более того, на серверных машинах пользователь может вообще не входить в систему за всё время работы.
Как и в случае стандартных приложений Windows, Delphi позволяет легко и быстро написать службу, используя VCL (увы, чего не скажешь об оснастке к MMC).
Установка и удаление службы
Итак, запускаем Delphi, открываем в главное меню пункт File/New/Other, выбираем «Service Application»:
После этого Delphi создаст пустое приложение службы. Заметим, что в новом проекте были автоматически сгенерированы две подпрограммы:
Как уже было сказано, в одном проекте (соответственно, exe-файле) может быть несколько служб. Чтобы добавить ещё одну службу в текущий проект, лезем в File/New/Other и там выбираем «Service» (а не «Service Application», как было ранее). В BDS пункт «Service» в разделе «Delphi Files» появляется только, если текущий проект является проектом службы. В Delphi 7 и ниже пункт «Service» виден всегда, он всегда создаёт пустой модуль с TService.
В это самое окно службы мы можем добавлять невизуальные компоненты, если они нам понадобятся. Визуальные компоненты мы использовать не можем по понятным причинам: в конце концов, мы пишем фоновую службу, которая может работать, даже если пользователь не зашёл в систему, поэтому мы не можем использовать никаких интерактивных средств взаимодействия с пользователем.
Служба сообщит, что она успешно установлена:
Открываем Панель управления/Администрирование/Службы и любуемся на нашу службу: «Service1».
Заметим, что служба установилась в остановленном состоянии с заданными ей свойствами (свойства службы мы рассмотрим ниже).
Для удаления службы запускаем exe-файл с параметром «/uninstall» (дополнительно можно добавить «/silent»), например:
Вообще говоря, служба редко ставится описанным способом, т.е. «руками». Обычно все операции по установке/удалению службы возлагаются на установщик (инсталлятор) приложения. Тем не менее, все службы, написанные на Delphi с использованием VCL, обладают такой вот приятной возможностью самоустановки и самоудаления. Поэтому инсталлятор может просто вызывать exe-файл службы с нужными параметрами (с указанием «/silent»).
Свойства службы
Теперь мы приведём свою службу в божеский вид, для этого посмотрим свойства службы:
Примечание: часть свойств логически относятся к процессу работы службы, поэтому они отнесены к пункту 4. Для новой службы обычно требуется задать Name, DisplayName и StartType.
События службы
Теперь разберёмся с событиями службы.
Вариант первый: стартует служба, вы начинаете что-то делать, время от времени уведомляя систему о своём состоянии, потом служба останавливается.
Вариант второй: стартует служба, создаёт рабочие потоки. По уведомлению от системы потоки останавливаются и служба выключается.
Итак, обычно для реализации минимальной службы требуется написать обработчики OnStart/OnStop и/или OnExecute.
Примечание: В новых версиях Windows изменено поведение критических секций при выходе из программы. Вместо блокировки вы получите данные в несогласованном состоянии (но только для ресурсов, защищённых критической секцией, и только начиная с Windows XP). Подробнее см. вопрос №61596.
Хорошо, предположим, вы сохранили объект потока в переменную и при получении сигнала останова вы вызовете ему Free. Но кто сказал, что объект будет существовать в момент вызова Free? Ведь вы же установили FreeOnTerminate, что означает, что поток может завершиться (и, следовательно, объект будет удалён, а в переменной потока будет указатель на мусор), а после этого может прийти сигнал об остановке. Получим Access Violation в службе?
Хорошо, вы добавили обнуление переменной потока при завершении работы потока, а перед Free вы поставили проверку указателя на nil. Но ведь возможна и ситуация, когда сперва выполнится проверка, затем будет удалён объект потока, а только потом вызван его деструктор после вашей проверки. Конечно, можно сделать синхронизацию потоков, гарантирующую корректную работу. Но не будет ли это изобретение велосипеда вместо использования готовых средств VCL?
Методы службы
Теперь окинем взглядом рабочий инструментарий службы: методы объекта TService.
Работа службы
SCM хранит базу данных служб в ключе реестра
Обращаться напрямую к этому ключу вам не нужно, работа с ним происходит программными средствами (функциями WinAPI или обёртками VCL) и оснасткой «Службы». В этой базе данных перечислены все установленные службы и их свойства. Исключение составляет чтение/запись своей конфигурации службы. Удобно все параметры службы хранить в ключе
Так, дождавшись запуска приложения службы и установления связи с ним, SCM отправит сообщение приложению с указанием запуска запрошенной службы. И если при попытке запуска новой службы SCM обнаружит, что служба размещается в уже запущенном приложении, то он просто отправит приложению сообщение о запуске новой службы и не будет пытаться запустить новый процесс. Если в реестре путь к одному и тому же exe-файлу двух служб указан по-разному, то SCM не сумеет определить, что это один файл и запустит два процесса вместо одного.
OnCreate, OnDestroy и все события установки/удаления службы выполняются в контексте главного потока. События OnExecute, OnStart, OnStop, OnPause, OnContinue и OnShutdown выполняются в контексте потока службы.
Настройки и взаимодействие с пользователем
Одним из часто задаваемых вопросов по службам является примерно такой вопрос: «как я могу из службы сделать X для текущего пользователя?». Здесь «X» может быть, например, установка ловушки (hook), показ окна настроек и т.п.
Но кто сказал, что такой «активный» пользователь только один?
Системные логи (регистрация событий)
В принципе, сказанного должно быть вполне достаточно, чтобы написать службу. Но хотелось бы коснуться ещё одного момента: системный логгинг событий.
Откройте оснастку «Просмотр событий»:
Как видим, каждый подключ в реестре соответствует разделу логов (слева) в «Просмотре событий»:
Бонус-пак: создание своего лога.
Если вы захотите добавить свой пункт (в оснастке «Просмотр событий» он появится слева, вместе с Приложение/Безопасность/Система), то вы добавляете подраздел в ключе EventLog:
Вне зависимости от того, добавляете ли вы свой раздел логов или используете раздел Application, следующим шагом вы создаёте подраздел с именем своей службы (то, что записано в TService.Name), например:
Далее, в свежесозданном ключе добавляете следующие параметры (а вот они уже обязательны):
Для компиляции в один файл, подключаемый к exe-файлу службы, удобно составить примерно такой bat-файл:
.mc-файл имеет примерно такое содержание:
Поэтому, обычно эти строки опускаются. Дальше идут собственно сообщения. Сообщение начинается с MessageId=номер. В этих строках сообщениям присваиваются их номера (номера выбираем мы). Именно по этим номерам мы потом будем добавлять их в лог. Кстати, и этот параметр опционален. Если его не указывать, то первое сообщение получит номер 0, а последующие будут увеличиваться на 1. Дальше могут идти наборы из Severity и Facility, которые по умолчанию имеют вид:
Дальше следует язык сообщения и само сообщение. Точка в начале строки означает конец сообщения. Если языков несколько, они следуют друг за другом. В самих сообщениях можно использовать специальные символы:
То же, что и точка в начале строки, но в отличие от точки, в конце сообщения не будет вставлен перенос строки.
Вставляет точку. Может использоваться для вставки точки в начало строки, без обрыва сообщения.
Подробнее о содержимом mc-файлов можно прочитать в MSDN/Platform SDK в разделе «Message Text Files». Вот ещё пример mc-файла оттуда:
Например, если в файле msg.mc были строки:
Ну и теперь же мы можем разобраться с методом TService.LogMessage:
LogMessage (Message: String; EventType: DWord; Category, ID: Integer)
Например, чтобы добавить в лог сообщение типа «служба не сконфигурирована» (для самого первого примера mc-файла), пишем:
LogMessage (», EVENTLOG_WARNING_TYPE, 0, 3);
А чтобы добавить произвольное сообщение:
LogMessage (‘произвольный текст’, EVENTLOG_INFORMATION_TYPE, 0, 1);
К сожалению, LogMessage имеет ограничения: вы можете использовать лишь 1 параметр.
Для того чтобы использовать несколько параметров, вам придётся написать свой аналог LogMessage, например такой:
Пример вызова такой процедуры:
Кстати говоря, сообщения из подготовленной таблицы строк можно ещё извлечь для собственного использования через функцию FormatMessage.
Дополнительно
К сожалению, при своей самоустановке служба не добавляет комментарий к себе (эта возможность появилась в Windows 2000), а также не регистрирует в реестре источник логов. Поэтому, часто в службу приходится добавлять обработчики событий OnAfterInstall и OnAfterUninstall, например, так:
Во-вторых, можно использовать стандартный Run/Attach to process для подключения отладчика Delphi к уже запущенной службе. К сожалению, это не даст отлаживать код инициализации, т.к. к моменту подключения отладчика служба уже должна быть запущена. Впрочем, обходным путём можно решить проблему, например, так: вставить в начало dpr файла что-то вроде Sleep(5000) и за это время сразу после старта нужно успеть подключить отладчик Delphi.
Кстати, для управления службами в Windows есть консольные утилиты net.exe и sc.exe. Вы можете запустить их из командной строки без параметров для просмотра списка выполняемых действий.
Final words
И пару слов в завершение темы.
По поводу обработки ошибок в службах будет интересно посмотреть «вопрос КС №60359»
Об опасностях использования различных компонент без понимания принципов их работы: «вопрос КС №55674» 🙂
В частности, в статье Пример использования Private Object Security в Delphi можно посмотреть пример готовой службы.
В этой статье не рассматривались вопросы написания приложений для MMC. В Delphi нет поддержки написания таких приложений. Но в Интернете можно найти компоненты, упрощающие их разработку, например: MMC Snapin Framework.