Как стать автором
Обновить

Реализация сервисов в MSWin

Время на прочтение 14 мин
Количество просмотров 1.1K
По рабочей необходимости приходится иногда писать системные сервисы для 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. Это наиболее правильное поведение. Если кто-то попробует запустить файл без ключей, сервис просто не запуститься, а обрабатывая нужные команды можно легко управлять поведением Вашего сервиса не запоминая консольный команд и параметров.
Дополнительно понадобится пара функций для проверки запущен ли сервис и установлен ли он в системе:

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.
Теги:
Хабы:
+16
Комментарии 4
Комментарии Комментарии 4

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн