По рабочей необходимости приходится иногда писать системные сервисы для Microsoft Windows.
На Хабре уже есть статья Создание своего Windows Service , но по моему мнению — статья не более чем краткий обзор, который можно найти в MSDN. В ней не рассмотрены, например, возможные варианты поведения сервиса в случае ошибки, или запись в журналы сообщений.
Постараюсь, используя опыт написания такого рода приложений, изложить максимально возможный объем информации.
Рассматривать варианты, когда сервис функционирует в качестве драйвера устройства (и, соответственно, располагается в файле с расширением .sys) не будем, это слишком специализировано.
Обычно сервис — это просто приложение, запускаемое SCM (Service Control Manager) и контролируемое им же. Для программиста это означает, что для запуска нужно предусмотреть несколько дополнительный действий.
Кроме этого, правильный сервис не должен напрямую взаимодействовать с пользователем. Что это означает на практике? Запуская любое приложение — мы видим или консольное окно, или GUI этого приложения. Системным сервисам, работающим под управлением операционных систем Windows 2000/Windows XP/Windows 2003 Server это тоже разрешено, такой сервис называется — интерактивным. Но, при этом, Начиная с Windows Vista — наложен запрет на интерактивные сервисы. Получается, что, разрабатывая сервис правильнее отказаться от интерактивности.
При этом в самом MSDN предлагаются следующие варианты взаимодействия с пользователем:
1) Отображение диалога функцией WTSSendMessage()
поведение очень гибкое, можно вывести диалог и ожидать реакции пользователя, можно просто проинформировать пользователя и продолжить работу приложения и т.д., но сервису требуется или работать с терминальными сессиями, или полагаться на то, что пользователь, активный в текущей терминальной сессии обладает нужными знаниями и правами, что бы отреагировать на всплывающее окно (многих несведующих в IT людей подобное поведение пугает, это тоже стоит учитывать при проектировании)
2) Создание отдельного приложения в текущей сессии пользователя используя CreateProcessAsUser()
наиболее правильный вариант, хотя бы потому, что пользователю дается простой инструмент взаимодействия с сервисом, но при этом нужно понимать, что взаимодействие должно быть организовано или через сокеты (наиболее распространенный вариант), или посредством IPC, СОМ и прочего, что несколько усложняет общий код, но упрощает сам сервис.
Так же можно не создавать отдельного исполняемого файла, а использовать тот же файл сервиса, запускаемый с определенным ключем, что несколько упростит распространение и обновление программы.
3) Вызов MessageBox() с параметром MB_SERVICE_NOTIFICATION
самый плохой вариант, сервис (или вызвавшая нить) «зависнет» на время отклика пользователя.
При установке сервиса в систему или удаление следует так же учитывать, что права на взаимодействие с SCM имеет только Администратор, или равный ему по правам пользователь.
Кроме этого, рабочей папкой сервиса является System32 Вашей системы, а прав на запись в папку, в которую установлен сервис может и не быть, поэтому для ведения журнала следует воспользоваться Eventlog самой системы (про это — ниже).
Прежде чем приводить код отмечу, что разрабатывая сервисы для NT систем, лично я предпочитаю писать полные имена функций не доверяя разворачиванию их макросами, поэтому вместо OpenSCManager я обычно пишу OpenSCManagerW.
И так, обо всем по порядку.
Точкой входа в сервис является или консольная функция main(), или WinMain(). Так как идиологически серсис — это не консольный проект — я пользуюсь второй функцией.
В самой функции следует сделать парсер командной строки и предусмотреть обработку команд install, uninstall, run и stop, сделать это можно кому как нравится. Запуск исполняемого файла без параметров будем считать запуском сервиса через SCM. Это наиболее правильное поведение. Если кто-то попробует запустить файл без ключей, сервис просто не запуститься, а обрабатывая нужные команды можно легко управлять поведением Вашего сервиса не запоминая консольный команд и параметров.
Дополнительно понадобится пара функций для проверки запущен ли сервис и установлен ли он в системе:
тут и далее g_str_srv_name — строка std::wstring содержащая имя сервиса.
Давайте разберем, что же тут сделано:
1) get_path()/get_name() — это функции получения имени папки где расположен исполняемый файл сервиса и имени этого исполняемого файла. Самописные, поэтому реализация — произвольная.
2) значение флага SERVICE_WIN32_OWN_PROCESS подходит для подавляющего большинства сервисов. Исключение составляют не рассматриваемые драйвера устройств и файловых систем. Второй возможный, в рамках этой статьи флаг SERVICE_WIN32_SHARE_PROCESS — означает, что процесс сервиса имеет общую с другими сервисами область памяти, это требуется, если в одном исполняемом файле у Вас находится несколько сервисов. Общая область памяти позволит съэкономить оперативную память, и упростить взаимодействие сервисов, но значительно усложнит отладку, поэтому советую использовать эту возможность только четко понимая зачем она Вам нужна и что иначе — ну ни как.
3) SERVICE_AUTO_START — сервис запустится автоматически при старте системы. Обратите внимание, что момент запуска сервиса в таком случае — это инициализация системных служб, т.е. до входа пользователя в систему. Этот момент стоит учитывать если сервис работате на сервере. Другие возможные варианты в рамках стстьи, SERVICE_DEMAND_START — запуск «вручную» и SERVICE_DISABLED — не запускать вообще.
4) Структура SERVICE_FAILURE_ACTIONSW описывает поведение SCM в случае экстренного завершения сервиса.
* dwResetPeriod — период обнуления данных о поведении сервиса, задается в секундах. Значение INFINITE говорит о том, что данные не будут затираться.
* cActions — количество элементов в массиве описывающем реакции SCM на экстренное поведение сервиса, обычно это число равно 3 (трем). Соответственно пунктам «Первый сбой», «Второй сбой» и «Последующие сбои»:
Соответственно заполнение массива l_srv_action[] полями { SC_ACTION_RESTART, 500 } означает, что сервис будет автоматически перезапускаться через каждые 500 миллисекунд после аварийного завершения.
Вся информация, заполняемая в этих полях будет сохранена в ветке реестра
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\MyService
Кроме этого следует добавить обработку сообщений сервиса, для этого добавляем специальную ветку в реестр HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\EventLog. В этой ветке необходимо указать файл в ресурсах которого MMC сможет найти описание для логируемых ошибок. В принципе, можно добавить эти ресурсы и в исполняемый файл, но проще создать отдельную динамическую библиотеку (в моем случае messages.dll) Как создать эту библиотеку — напишу ниже.
Ну вот собственно и все, с установкой.
Удаление, запуск и остановку сервиса можно подсмотреть в приведенной в заголовке статье, ни чего особенного в коде этих процедур нет.
В итоге с установкой, удалением и прочими командами мы вроде разобрались, значит можно продолжать дальше.
С точкой входа в сервис хорошо рассказано в предыдущей статье, добавлю лишь несколько уточнений:
Вызвать StartServiceCtrlDispatcherW() Ваш сервис должен не позднее чем через 30 секунд после входа в WinMain(), иначе SCM решит, что запускаемый процесс «завис» и выгрузит его.
Далее, наиболее правильным поведением сервиса будет сперва объявление статуса как «запускаюсь», а потом смена его на «запущен», впрочем, ни что не мешает установить статус сервиса как «запущен» сразу.
Для этого в ServiceMain нужно выполнить следующие действия:
Тут сервис сообщает SCM, что ему требуется 5 секунд на инициализацию, это означает, что до того, как истечет эти 5 секунд необходимо сообщить SCM о завершении инициализации. Иначе сервис будет выгружен как «зависший».
В приведенном выше коде предполагается, что при инициализации класса сервиса поля структуры были заполнены следующим образом:
При этом значение поля dwServiceType должно соответствовать типу сервиса, который мы указали при установке, иначе SCM расценит это как неправильное поведение и не даст сервису запуститься.
Скрытие сообщений об ошибках и перехват исключений позволят самостоятельно обработать ошибки в коде программы, а так же, порой позволят избежать подвисания сервиса. Дополнительно это избавит пользователя от необходимости отправлять отчеты об ошибках разработчикам из Microsoft.
Естественноым будет вставка в тело основного цикла сервиса Sleep(10) для того, что бы дать возможность нормально функционировать другим программам (описание этого выходит за рамки этой статьи).
Обработчик сообщений у меня выглядит так:
Собственно осталось то, что освещено в Интернете меньше всего, как правильно добавить сообщение из сервиса в журнал системы. О том, как зарегистрировать библиотеку в реестре уже написано выше, теперь о том, как эту библиотеку создать.
1) создаем файл с расширением .mc примерно следующего содержания:
Зарезервировано четыре уровня важности сообщений и они не должны меняться, а вот индекс объекта (FacilityNames) — произвольный. Завершение описания — точка в новой строке, это не стоит забывать.
MessageID должен быть уникален для каждого описываемого типа сообщений (например 1, 2, 3 и т.д.)
Этот файл необходимо «скомпилировать» в ресурсный, для этого в поставке MSVS имеется mc.exe, создающий из .mc файлов .h и .rc
Заголовочный файл, сгенерированный утилитой необходимо подключить к проекту, а ресурсный собрать в библиотеку (или подключить к исполняемому файлу).
Интересной особенностью является то, что для просмотра сообщений в корректном формате требуется перезапуск службы Eventlog. В противном случае Вы увидите что-то такое:
Приятной возможностью является то, что журнал сообщений сервис может вести и свой, а не складывать в общий для всех приложений. Для этого, при регистрации библиотеки описания сообщений необходимо указать не ветку HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog\Application, а HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog\MyService.
Но тут есть занятный нюанс.
После создания ветки реестра в MMC появится соответствующая папка в «Событиях», но в некоторых случаях, сообщения из этой ветки не обрабатываются корректно даже после перезапуска Eventlog. В таких, «тяжелых случаях», требуется перезапуск компьютера, что может быть проблематично для сервера. Но, к счастью, встречается это крайне редко.
Ну собственно и все. Получилось много текста, много кода, но это и там, по моему мнению минимум, который необходимо знать. Остальное легко почерпнуть из Интернета и MSDN.
На Хабре уже есть статья Создание своего Windows Service , но по моему мнению — статья не более чем краткий обзор, который можно найти в MSDN. В ней не рассмотрены, например, возможные варианты поведения сервиса в случае ошибки, или запись в журналы сообщений.
Постараюсь, используя опыт написания такого рода приложений, изложить максимально возможный объем информации.
Рассматривать варианты, когда сервис функционирует в качестве драйвера устройства (и, соответственно, располагается в файле с расширением .sys) не будем, это слишком специализировано.
Обычно сервис — это просто приложение, запускаемое SCM (Service Control Manager) и контролируемое им же. Для программиста это означает, что для запуска нужно предусмотреть несколько дополнительный действий.
Кроме этого, правильный сервис не должен напрямую взаимодействовать с пользователем. Что это означает на практике? Запуская любое приложение — мы видим или консольное окно, или GUI этого приложения. Системным сервисам, работающим под управлением операционных систем Windows 2000/Windows XP/Windows 2003 Server это тоже разрешено, такой сервис называется — интерактивным. Но, при этом, Начиная с Windows Vista — наложен запрет на интерактивные сервисы. Получается, что, разрабатывая сервис правильнее отказаться от интерактивности.
При этом в самом MSDN предлагаются следующие варианты взаимодействия с пользователем:
1) Отображение диалога функцией WTSSendMessage()
поведение очень гибкое, можно вывести диалог и ожидать реакции пользователя, можно просто проинформировать пользователя и продолжить работу приложения и т.д., но сервису требуется или работать с терминальными сессиями, или полагаться на то, что пользователь, активный в текущей терминальной сессии обладает нужными знаниями и правами, что бы отреагировать на всплывающее окно (многих несведующих в IT людей подобное поведение пугает, это тоже стоит учитывать при проектировании)
2) Создание отдельного приложения в текущей сессии пользователя используя CreateProcessAsUser()
наиболее правильный вариант, хотя бы потому, что пользователю дается простой инструмент взаимодействия с сервисом, но при этом нужно понимать, что взаимодействие должно быть организовано или через сокеты (наиболее распространенный вариант), или посредством IPC, СОМ и прочего, что несколько усложняет общий код, но упрощает сам сервис.
Так же можно не создавать отдельного исполняемого файла, а использовать тот же файл сервиса, запускаемый с определенным ключем, что несколько упростит распространение и обновление программы.
3) Вызов MessageBox() с параметром MB_SERVICE_NOTIFICATION
самый плохой вариант, сервис (или вызвавшая нить) «зависнет» на время отклика пользователя.
При установке сервиса в систему или удаление следует так же учитывать, что права на взаимодействие с SCM имеет только Администратор, или равный ему по правам пользователь.
Кроме этого, рабочей папкой сервиса является System32 Вашей системы, а прав на запись в папку, в которую установлен сервис может и не быть, поэтому для ведения журнала следует воспользоваться Eventlog самой системы (про это — ниже).
Прежде чем приводить код отмечу, что разрабатывая сервисы для NT систем, лично я предпочитаю писать полные имена функций не доверяя разворачиванию их макросами, поэтому вместо OpenSCManager я обычно пишу OpenSCManagerW.
И так, обо всем по порядку.
Точкой входа в сервис является или консольная функция main(), или WinMain(). Так как идиологически серсис — это не консольный проект — я пользуюсь второй функцией.
В самой функции следует сделать парсер командной строки и предусмотреть обработку команд install, uninstall, run и stop, сделать это можно кому как нравится. Запуск исполняемого файла без параметров будем считать запуском сервиса через SCM. Это наиболее правильное поведение. Если кто-то попробует запустить файл без ключей, сервис просто не запуститься, а обрабатывая нужные команды можно легко управлять поведением Вашего сервиса не запоминая консольный команд и параметров.
Дополнительно понадобится пара функций для проверки запущен ли сервис и установлен ли он в системе:
Copy Source | Copy HTML<br/>/* проверка установлен ли сервис<br/> */<br/>bool is_install()<br/>{<br/> bool Result = false;<br/> SC_HANDLE l_srv_manager = NULL;<br/> SC_HANDLE l_srv_process = NULL;<br/> <br/> l_srv_manager = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);<br/> if (l_srv_manager)<br/> {<br/> l_srv_process = OpenServiceW(<br/> l_srv_manager,<br/> g_str_srv_name.c_str(),<br/> SERVICE_ALL_ACCESS<br/> );<br/> if (l_srv_process)<br/> {<br/> Result = true;<br/> CloseServiceHandle(l_srv_process);<br/> }<br/> CloseServiceHandle(l_srv_manager);<br/> }<br/> else<br/> /* не удалось подключиться к службе управления сервисами<br/> */<br/> MessageBoxW(<br/> 0,<br/> L"Cannot connect to Service Manager\ntry run with Administrator right !",<br/> L"ERROR",<br/> MB_ICONERROR<br/> );<br/> return Result;<br/>}<br/>//------------------------------------------------------------------------------ <br/>
Copy Source | Copy HTML<br/>/* проверка запущен ли сервис<br/> */<br/>bool is_run()<br/>{<br/> if (!is_install())<br/> return false;<br/> <br/> bool Result = false;<br/> SC_HANDLE l_srv_manager = NULL;<br/> SC_HANDLE l_srv_process = NULL;<br/> <br/> l_srv_manager = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);<br/> if (l_srv_manager)<br/> {<br/> l_srv_process = OpenServiceW(<br/> l_srv_manager,<br/> g_str_srv_name.c_str(),<br/> SERVICE_ALL_ACCESS<br/> );<br/> if (l_srv_process)<br/> {<br/> SERVICE_STATUS_PROCESS l_srv_status;<br/> DWORD l_dw_temp;<br/> if (QueryServiceStatusEx(<br/> l_srv_process,<br/> SC_STATUS_PROCESS_INFO,<br/> reinterpret_cast<LPBYTE> (&l_srv_status),<br/> sizeof(SERVICE_STATUS_PROCESS),<br/> &l_dw_temp<br/> ) == TRUE<br/> )<br/> {<br/> if (l_srv_status.dwCurrentState == SERVICE_RUNNING)<br/> Result = true;<br/> }<br/> CloseServiceHandle(l_srv_process);<br/> }<br/> CloseServiceHandle(l_srv_manager);<br/> }<br/> else<br/> /* не удалось подключиться к службе управления сервисами<br/> */<br/> MessageBoxW(<br/> 0,<br/> L"Cannot connect to Service Manager\ntry run with Administrator right !",<br/> L"ERROR",<br/> MB_ICONERROR<br/> );<br/> return Result;<br/>}<br/>//------------------------------------------------------------------------------ <br/>
тут и далее g_str_srv_name — строка std::wstring содержащая имя сервиса.
Copy Source | Copy HTML<br/>/* установка сервиса<br/> */<br/>int srv_install()<br/>{<br/> if (is_install())<br/> return srv_start();<br/> <br/> int Result = -1;<br/> SC_HANDLE l_srv_manager = NULL;<br/> SC_HANDLE l_srv_process = NULL;<br/> <br/> l_srv_manager = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);<br/> if (l_srv_manager)<br/> {<br/> std::wstring l_wstr = get_path();<br/> <br/> l_srv_process = CreateServiceW(<br/> l_srv_manager,<br/> g_str_srv_name.c_str(),<br/> g_str_srv_name.c_str(),<br/> SERVICE_ALL_ACCESS,<br/> SERVICE_WIN32_OWN_PROCESS,<br/> SERVICE_AUTO_START,<br/> SERVICE_ERROR_NORMAL,<br/> (l_wstr + get_name()).c_str(),<br/> NULL,<br/> NULL,<br/> NULL,<br/> NULL,<br/> NULL<br/> );<br/> if (l_srv_process)<br/> {<br/> registry_editor_t l_reg_edit;<br/> <br/> Result = 0;<br/> <br/> HKEY l_kservice = NULL;<br/> <br/> SERVICE_DESCRIPTIONW l_srv_descr;<br/> SERVICE_FAILURE_ACTIONSW l_srv_action_f;<br/> SC_ACTION l_srv_action[] =<br/> {<br/> { SC_ACTION_RESTART, 500 },<br/> { SC_ACTION_RESTART, 500 },<br/> { SC_ACTION_RESTART, 500 }<br/> };<br/> <br/> l_srv_descr.lpDescription = const_cast<wchar_t*> (g_str_srv_descr.c_str());<br/> l_srv_action_f.dwResetPeriod = 120;<br/> l_srv_action_f.lpRebootMsg = NULL;<br/> l_srv_action_f.lpCommand = NULL;<br/> l_srv_action_f.cActions = 3;<br/> l_srv_action_f.lpsaActions = l_srv_action;<br/> <br/> ChangeServiceConfig2W(<br/> l_srv_process,<br/> SERVICE_CONFIG_DESCRIPTION,<br/> &l_srv_descr<br/> );<br/> ChangeServiceConfig2W(<br/> l_srv_process,<br/> SERVICE_CONFIG_FAILURE_ACTIONS,<br/> &l_srv_action_f<br/> );<br/> <br/> // HKEY_LOCAL_MACHINE\Software\MyService<br/> l_kservice = l_reg_edit.create(<br/> registry_editor_t::root::local_mashine,<br/> L"Software\\MyService"<br/> );<br/> if (l_kservice)<br/> {<br/> l_reg_edit.write(l_kservice, L"Path", l_wstr);<br/> l_reg_edit.close(l_kservice);<br/> }<br/> <br/> // HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog\*<br/> DWORD l_support = EVENTLOG_ERROR_TYPE |<br/> EVENTLOG_WARNING_TYPE |<br/> EVENTLOG_INFORMATION_TYPE;<br/> HKEY l_kevent_log = l_reg_edit.create(<br/> registry_editor_t::root::local_mashine,<br/> (L"SYSTEM\\CurrentControlSet\\Services\\EventLog\\Application\\MySvrMessages").c_str()<br/> );<br/> if (l_kevent_log)<br/> {<br/> l_reg_edit.write(<br/> l_kevent_log,<br/> L"EventMessageFile",<br/> l_wstr + L"messages.dll"<br/> );<br/> l_reg_edit.write(<br/> l_kevent_log,<br/> L"TypesSupported",<br/> l_support<br/> );<br/> l_reg_edit.write(<br/> l_kevent_log,<br/> L"CategoryMessageFile",<br/> l_wstr + L"messages.dll"<br/> );<br/> l_support = 3;<br/> l_reg_edit.write(<br/> l_kevent_log,<br/> L"CategoryCount",<br/> l_support<br/> );<br/> }<br/> <br/> // start service<br/> StartServiceW(l_srv_process, 0, NULL);<br/> <br/> CloseServiceHandle(l_srv_process);<br/> }<br/> else<br/> {<br/> /* не удалось установить сервис в систему<br/> */<br/> std::wstring l_wstr = L"Installation service failed\n";<br/> l_wstr += get_error();<br/> MessageBoxW( 0, l_wstr.c_str(), L"ERROR", MB_ICONERROR);<br/> }<br/> <br/> CloseServiceHandle(l_srv_manager);<br/> }<br/> else<br/> /* не удалось подключиться к службе управления сервисами<br/> */<br/> MessageBoxW(<br/> 0,<br/> L"Cannot connect to Service Manager\ntry run with Administrator right !",<br/> L"ERROR",<br/> MB_ICONERROR<br/> );<br/> return Result;<br/>}<br/>//------------------------------------------------------------------------------ <br/>
Давайте разберем, что же тут сделано:
1) get_path()/get_name() — это функции получения имени папки где расположен исполняемый файл сервиса и имени этого исполняемого файла. Самописные, поэтому реализация — произвольная.
2) значение флага SERVICE_WIN32_OWN_PROCESS подходит для подавляющего большинства сервисов. Исключение составляют не рассматриваемые драйвера устройств и файловых систем. Второй возможный, в рамках этой статьи флаг SERVICE_WIN32_SHARE_PROCESS — означает, что процесс сервиса имеет общую с другими сервисами область памяти, это требуется, если в одном исполняемом файле у Вас находится несколько сервисов. Общая область памяти позволит съэкономить оперативную память, и упростить взаимодействие сервисов, но значительно усложнит отладку, поэтому советую использовать эту возможность только четко понимая зачем она Вам нужна и что иначе — ну ни как.
3) SERVICE_AUTO_START — сервис запустится автоматически при старте системы. Обратите внимание, что момент запуска сервиса в таком случае — это инициализация системных служб, т.е. до входа пользователя в систему. Этот момент стоит учитывать если сервис работате на сервере. Другие возможные варианты в рамках стстьи, SERVICE_DEMAND_START — запуск «вручную» и SERVICE_DISABLED — не запускать вообще.
4) Структура SERVICE_FAILURE_ACTIONSW описывает поведение SCM в случае экстренного завершения сервиса.
* dwResetPeriod — период обнуления данных о поведении сервиса, задается в секундах. Значение INFINITE говорит о том, что данные не будут затираться.
* cActions — количество элементов в массиве описывающем реакции SCM на экстренное поведение сервиса, обычно это число равно 3 (трем). Соответственно пунктам «Первый сбой», «Второй сбой» и «Последующие сбои»:
Соответственно заполнение массива l_srv_action[] полями { SC_ACTION_RESTART, 500 } означает, что сервис будет автоматически перезапускаться через каждые 500 миллисекунд после аварийного завершения.
Вся информация, заполняемая в этих полях будет сохранена в ветке реестра
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\MyService
Кроме этого следует добавить обработку сообщений сервиса, для этого добавляем специальную ветку в реестр HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\EventLog. В этой ветке необходимо указать файл в ресурсах которого MMC сможет найти описание для логируемых ошибок. В принципе, можно добавить эти ресурсы и в исполняемый файл, но проще создать отдельную динамическую библиотеку (в моем случае messages.dll) Как создать эту библиотеку — напишу ниже.
Ну вот собственно и все, с установкой.
Удаление, запуск и остановку сервиса можно подсмотреть в приведенной в заголовке статье, ни чего особенного в коде этих процедур нет.
В итоге с установкой, удалением и прочими командами мы вроде разобрались, значит можно продолжать дальше.
С точкой входа в сервис хорошо рассказано в предыдущей статье, добавлю лишь несколько уточнений:
Вызвать StartServiceCtrlDispatcherW() Ваш сервис должен не позднее чем через 30 секунд после входа в WinMain(), иначе SCM решит, что запускаемый процесс «завис» и выгрузит его.
Далее, наиболее правильным поведением сервиса будет сперва объявление статуса как «запускаюсь», а потом смена его на «запущен», впрочем, ни что не мешает установить статус сервиса как «запущен» сразу.
Для этого в ServiceMain нужно выполнить следующие действия:
Copy Source | Copy HTML<br/>/* основная функция сервиса<br/> */<br/>void service_t::main()<br/>{<br/> m_handle = RegisterServiceCtrlHandlerExW(<br/> g_str_srv_name.c_str(),<br/> &service_handler,<br/> NULL<br/> );<br/> if (!m_handle)<br/> {<br/> system_logger_t::instance()->trace_info(<br/> L"register service handler failed"<br/> );<br/> _set_stop();<br/> return;<br/> }<br/> //<br/> m_status.dwCurrentState = SERVICE_START_PENDING;<br/> m_status.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;<br/> m_status.dwServiceSpecificExitCode = ERROR_NOT_READY;<br/> m_status.dwWaitHint = 5000;<br/> if (SetServiceStatus(m_handle, &m_status) == FALSE)<br/> {<br/> std::wstring l_error = get_error();<br/> system_logger_t::instance()->trace_info(l_error);<br/> _set_stop();<br/> return;<br/> }<br/> //<br/> SetUnhandledExceptionFilter(exception_filter);<br/> SetErrorMode(SEM_FAILCRITICALERRORS);<br/> // init<br/> // ----<br/> m_status.dwControlsAccepted = SERVICE_ACCEPT_STOP |<br/> SERVICE_ACCEPT_SESSIONCHANGE |<br/> SERVICE_ACCEPT_SHUTDOWN;<br/> m_status.dwWin32ExitCode = NO_ERROR;<br/> m_status.dwCurrentState = SERVICE_RUNNING;<br/> m_status.dwWaitHint = 0;<br/> if (SetServiceStatus(m_handle, &m_status) == FALSE)<br/> {<br/> std::wstring l_error = get_error();<br/> trace_msg(l_error);<br/> system_logger_t::instance()->trace_info(l_error);<br/> return;<br/> }<br/> m_run = true;<br/> system_logger_t::instance()->trace_info(<br/> L"service start success"<br/> );<br/> while ( true )<br/> {<br/> if (!m_run)<br/> {<br/> break;<br/> }<br/> else<br/> Sleep(10);<br/> }<br/> _set_stop();<br/>}<br/>//------------------------------------------------------------------------------<br/>/*<br/> */<br/>void service_t::_set_stop()<br/>{<br/> m_status.dwCurrentState = SERVICE_STOPPED;<br/> SetServiceStatus(m_handle, &m_status);<br/> system_logger_t::instance()->trace_info(<br/> L"MyService stop"<br/> );<br/>}<br/>//------------------------------------------------------------------------------ <br/>
Тут сервис сообщает SCM, что ему требуется 5 секунд на инициализацию, это означает, что до того, как истечет эти 5 секунд необходимо сообщить SCM о завершении инициализации. Иначе сервис будет выгружен как «зависший».
В приведенном выше коде предполагается, что при инициализации класса сервиса поля структуры были заполнены следующим образом:
memset(&m_status, 0, sizeof(SERVICE_STATUS));
m_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
При этом значение поля dwServiceType должно соответствовать типу сервиса, который мы указали при установке, иначе SCM расценит это как неправильное поведение и не даст сервису запуститься.
Скрытие сообщений об ошибках и перехват исключений позволят самостоятельно обработать ошибки в коде программы, а так же, порой позволят избежать подвисания сервиса. Дополнительно это избавит пользователя от необходимости отправлять отчеты об ошибках разработчикам из Microsoft.
Естественноым будет вставка в тело основного цикла сервиса Sleep(10) для того, что бы дать возможность нормально функционировать другим программам (описание этого выходит за рамки этой статьи).
Обработчик сообщений у меня выглядит так:
Copy Source | Copy HTML<br/>/* обработчик событий системы<br/> */<br/>DWORD service_t::handler(DWORD dwControl,<br/> DWORD dwEnterType,<br/> LPVOID lpEventData,<br/> LPVOID lpContext)<br/>{<br/> switch (dwControl)<br/> {<br/> case SERVICE_CONTROL_STOP:<br/> case SERVICE_CONTROL_SHUTDOWN:<br/> system_logger_t::instance()->trace_info(<br/> L"service try to stop"<br/> );<br/> m_status.dwCurrentState = SERVICE_STOP_PENDING;<br/> m_status.dwWaitHint = 10000;<br/> SetServiceStatus(m_handle, &m_status);<br/> m_run = false;<br/> break;<br/> case SERVICE_CONTROL_SESSIONCHANGE:<br/> break;<br/> default:<br/> SetServiceStatus(m_handle, &m_status);<br/> }<br/> return NO_ERROR;<br/>}<br/>//------------------------------------------------------------------------------ <br/>
Собственно осталось то, что освещено в Интернете меньше всего, как правильно добавить сообщение из сервиса в журнал системы. О том, как зарегистрировать библиотеку в реестре уже написано выше, теперь о том, как эту библиотеку создать.
1) создаем файл с расширением .mc примерно следующего содержания:
Copy Source | Copy HTML<br/>MessageIdTypedef = DWORD<br/>SeverityNames =<br/> (<br/> Success = 0x0 : STATUS_SEVERITY_SUCCESS<br/> Informational = 0x1 : STATUS_SEVERITY_INFORMATIONAL<br/> Warning = 0x2 : STATUS_SEVERITY_WARNING<br/> Error = 0x3 : STATUS_SEVERITY_ERROR<br/> )<br/> <br/>FacilityNames =<br/> (<br/> System = 0x0 : FACILITY_SYSTEM<br/> Runtime = 0x2 : FACILITY_RUNTIME<br/> Io = 0x3 : FACILITY_IO_ERROR_CODE<br/> )<br/> <br/>LanguageNames =<br/> (<br/> English = 0x409 : MSG00409<br/> )<br/> <br/> ;// messages definition<br/>MessageId = 0x1<br/>Severity = Success<br/>Facility = System<br/>SymbolicName = SRV_MSG_SYSTEM_SUCCESS<br/>Language = English<br/>Operation %1 success<br/>. <br/>
Зарезервировано четыре уровня важности сообщений и они не должны меняться, а вот индекс объекта (FacilityNames) — произвольный. Завершение описания — точка в новой строке, это не стоит забывать.
MessageID должен быть уникален для каждого описываемого типа сообщений (например 1, 2, 3 и т.д.)
Этот файл необходимо «скомпилировать» в ресурсный, для этого в поставке MSVS имеется mc.exe, создающий из .mc файлов .h и .rc
Заголовочный файл, сгенерированный утилитой необходимо подключить к проекту, а ресурсный собрать в библиотеку (или подключить к исполняемому файлу).
rc -r messages.rc
link -dll -noentry -out:messages.dll messages.res
Интересной особенностью является то, что для просмотра сообщений в корректном формате требуется перезапуск службы Eventlog. В противном случае Вы увидите что-то такое:
Не найдено описание для события с кодом ( 1 ) в источнике ( MySrvMessages ). Возможно, на локальном компьютере нет нужных данных в реестре или файлов DLL сообщений для отображения сообщений удаленного компьютера. Попробуйте использовать ключ /AUXSOURCE= для получения этого описания, - дополнительные сведения об этом содержатся в справке. В записи события содержится следующая информация: service starting.
Приятной возможностью является то, что журнал сообщений сервис может вести и свой, а не складывать в общий для всех приложений. Для этого, при регистрации библиотеки описания сообщений необходимо указать не ветку HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog\Application, а HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog\MyService.
Но тут есть занятный нюанс.
После создания ветки реестра в MMC появится соответствующая папка в «Событиях», но в некоторых случаях, сообщения из этой ветки не обрабатываются корректно даже после перезапуска Eventlog. В таких, «тяжелых случаях», требуется перезапуск компьютера, что может быть проблематично для сервера. Но, к счастью, встречается это крайне редко.
Ну собственно и все. Получилось много текста, много кода, но это и там, по моему мнению минимум, который необходимо знать. Остальное легко почерпнуть из Интернета и MSDN.