Пишем свой сервис? Легко!

Пишем свой сервис? Легко!

На страницах КГ уже неоднократно поднимался вопрос о службах в ОС т.н. NT-серии (т.е. WinNT, 2000, XP). Но ведь, как известно, многие программы не только используют стандартные сервисы, но и устанавливают свои. Зачем это нужно? Допустим, вам необходимо выполнять какие-либо периодические действия, причем выполняться они должны вне зависимости от того, каким пользователем используется данная машина. Понятно, что простой программкой, висящей в трее, тут не обойтись. В принципе, вход в систему может быть вообще не выполнен (т.е. не использоваться ни одним из юзеров). В наибольшей степени это касается различных сетевых серверов (всем известный Apache именно так и предлагает инсталлировать себя — а это, что называется, "то, что доктор прописал"). Поэтому службы — это то, что "нужно знать и уметь".

Итак, что же такое служба (или сервис, что одно и то же)? Службы бывают двух типов: службы, взаимодействующие с системой через менеджер управления службами (SCM), и драйверы. Службы взаимодействуют с системой по определенному стандартному протоколу. А посему в принципе любая программа или сам пользователь может остановить (приостановить) выполнение службы. Службы могут выполняться при отсутствии в системе зарегистрированных пользователей. Они работают незаметно, в фоновом режиме. Чтобы любой процесс имел доступ к службе, информация о ней хранится в реестре в ключе HKLM\SYSTEM\CurrentControlSet\Services\ServiceName. Там содержится информация о:

типе службы — в приложении может быть одна или несколько служб, службы могут быть вложенными;
типе запуска (автоматический, вручную или же отключена);
пути и имени исполняемого файла;
порядок запуска (т.н. зависимости);
информация о безопасности (имя и пароль).

Для работы со службами используется много функций. Приведу описание некоторых из них.
HANDLE на менеджер управления сервисами может быть получен при помощи следующей функции:
SC_HANDLE OpenSCManager(
LPCTSTR lpMachineName, // имя компьютера(NULL если локальный)
LPCTSTR lpDatabaseName, // имя SCM базы данных(NULL если локальный)
DWORD dwDesiredAccess // тип доступа
);
Закрыть HANDLE можно используя CloseServiceHandle().
Прежде, чем обращаться к какому-либо сервису и передавать ему какие-либо команды, необходимо получить HANDLE системы управления. А после открыть службу:
SC_HANDLE OpenService( SC_HANDLE hSCManager, // HANDLE LPCTSTR lpServiceName, // имя сервиса D
WORD dwDesiredAccess // доступ ); Функция установки службы в систему: SC_HANDLE CreateService( SC_HA
NDLE hSCManager, // HANDLE на менеджер управления службами LPCTSTR lpServiceName, // имя сервиса LPC
TSTR lpDisplayName, // отображаемое имя DWORD dwDesiredAccess, // тип доступа к службе DWORD dwServi
ceType, // тип сервиса DWORD dwStartType, // тип запуска службы DWORD dwErrorControl, // обработка о
шибок LPCTSTR lpBinaryPathName, // имя (путь) исполняемого файла LPCTSTR lpLoadOrderGroup, // имя гр
уппы LPDWORD lpdwTagId, // идентификатор признака LPCTSTR lpDependencies, // зависимости LPCTSTR lpS
erviceStartName, // имя аккаунта LPCTSTR lpPassword // пароль );

Ниже, в практической части, приведу пример ее использования.
Удаление службы проще:
BOOL DeleteService(
SC_HANDLE hService // HANDLE на сервис
);"
Управление службой производится при помощи
"BOOL ControlService(
SC_HANDLE hService, // HANDLE на службу
DWORD dwControl, // управляющий код
LPSERVICE_STATUS lpServiceStatus // Информация о статусе
);
Запуск службы: StartService();
Получение информации о службе: QueryServiceConfig();
Изменения конфигурационной информации службы:ChangeServiceConfig, SetServiceObjectSecurity, LockServiceDatabase, UnlockServiceDatabase, QueryServiceLockStatus;

Варианты применения остальных часто используемых функций будут даны в примере программы управления ниже (если у кого-то хватит сил дочитать до конца:-)). На практике изучение гораздо продуктивнее, чем простое чтение документации.
Что касается самой программы-службы, то она состоит из нескольких частей. Структура ее несколько отличается от "обычной" структуры консольного приложения. Она (структура) состоит из нескольких функций, которые последовательно перечислены ниже.
Итак, функция main(). Как обычно, при старте сначала начинает выполняться код этой функции. После запуска в течение 30 секунд должна быть вызвана функция StartServiceCtrlDispatcher для установления соединения между приложением и SCM.
ServiceMain(). Выполняется при старте службы. Если в приложении организовано несколько сервисов, то их имена передаются в качестве параметра при вызове StartServiceCtrlDispatcher. ServiceMain должна зарегистрировать обработчик запросов для каждой службы. После этого провести инициализацию (выделить память и др.), сообщив системе, что служба находится в состоянии запуска. После инициализации— послать сигнал о том, что служба успешно запущена. После этого можно выполнять полезный код, т.е. то, для чего служба была собственно написана. Если процесс запуска продолжается длительное время, необходимо уведомлять систему с заданной периодичностью о том, что сервис не повис. Если этого не делать, SCM принудительно завершит его работу.

Функция обработки запросов. Она вызывается, когда службе приходит какая-либо команда, после чего сообщает системе о новом своем состоянии.
Система безопасности. Следует помнить, что только процессы, работающие с административными правами, могут устанавливать и деинсталлировать службы. Вообще желательно, чтобы полезные действия при функционировании сервиса выполнялись в отдельном потоке.
Переходя к практической части нашей статьи, хотелось бы начать с описания собственно самой программы— службы. Итак, создаем новое пустое консольное приложение, создаем новый файл *.cpp и помещаем в него следующий текст (я постарался максимально пояснить комментариями выполняемые там действия):
#include <windows.h> #include <windowsx.h> #include <stdio.h> #include <c
onio.h> #include <stdlib.h> //Используемые функции void WINAPI ServiceMain(DWORD argc, LPST
R *siz_arc); void WINAPI ServiceControl(DWORD control); void ReportStatus(DWORD dwCurrentState,DWORD
 dwWin32ExitCode, DWORD dwWaitHint); //Имя сервиса char *Srv_name="My first service"; // И
дентификатор сервиса SERVICE_STATUS_HANDLE Srv_Handle; // Здесь храним статус службы SERVICE_STATUS 
Stat; // Для кода ошибки выделяем переменную DWORD Error_n; // main функция. Выполняется при запуске
 и инициализации void main() { // Таблица точек входа SERVICE_TABLE_ENTRY DispatcherTable[] = { { //
 Имя сервиса Srv_name, // Функция main сервиса (LPSERVICE_MAIN_FUNCTION)ServiceMain }, { NULL, NULL 
} }; printf("Тестовый сервис"); // Пытаемся запустить Dispatcher if(!StartServiceCtrlDispa
tcher(DispatcherTable)) { fprintf(stdout, "StartServiceCtrlDispatcher: Error %ld\n", GetLa
stError()); getch(); return; }; } //Функция, выполняющаяся при инициализации сервиса и его работе vo
id WINAPI ServiceMain(DWORD argc, LPSTR *argv) { Srv_Handle =RegisterServiceCtrlHandler(Srv_name, Se
rviceControl); if(!Srv_Handle) return;//Служба установлена как отдельный процессStat.dwServiceType =
 SERVICE_WIN32_OWN_PROCESS; //Код выхода по ошибке не используетсяStat.dwServiceSpecificExitCode = 0
; //Сообщаем о том, что служба находится в стадии запуска ReportStatus(SERVICE_START_PENDING, NO_ERR
OR, 4000); //Здесь выделяем динамическую память, читаем конфиг //в общем, делаем инициализирующие де
йствия //Устанавливаем состояние службы как рабочее ReportStatus(SERVICE_RUNNING, NOERROR, 0); // Зд
есь следует поместить код, выполняющийся при функционировании сервиса // В данном случае - выдача си
гнала по таймеру while (TRUE) { MessageBeep(1000); Sleep(1000); }; return; } //Функция, ответственна
я за обработку управляющих сигналов void WINAPI ServiceControl(DWORD control) { //Получаем управляющ
ий код и выполняем соответствующие действия switch(control) { //Попытка остановки службы case SERVIC
E_CONTROL_STOP: { // Информируем систему о том, что будет осуществлена попытка остановки //службы St
at.dwCurrentState = SERVICE_STOP_PENDING; ReportStatus(Stat.dwCurrentState, NOERROR, 0); //Здесь нео
бходимо освободить память, завершить дочерние процессы и //выполнить необходимые действия для заверш
ения приложения //Информируем о том, что служба успешно остановлена ReportStatus(SERVICE_STOPPED, NO
ERROR, 0); break; } //При попытке получить информацию о состоянии службы case SERVICE_CONTROL_INTERR
OGATE: { // Возвращаем текущее состояние службы ReportStatus(Stat.dwCurrentState, NOERROR, 0); break
; } //Действие по-умолчанию - возврат текущего состояния default: { ReportStatus(Stat.dwCurrentState
, NOERROR, 0); break; } } } //Функция взаимодействия с системой. Ответственна за посылку кодов систе
ме управления //службами void ReportStatus(DWORD dwCurrentState,DWORD dwWin32ExitCode, DWORD dwWaitH
int) { // Счетчик операций static DWORD dwCheckPoint = 1; //Проверка возможности остановки службы //
Остановка возможна, если служба не находится в процессе запуска if(dwCurrentState == SERVICE_START_P
ENDING) Stat.dwControlsAccepted = 0; else Stat.dwControlsAccepted = SERVICE_ACCEPT_STOP; //Переприсв
аиваем параметры функции Stat.dwWin32ExitCode = dwWin32ExitCode; Stat.dwWaitHint = dwWaitHint; Stat.
dwCurrentState = dwCurrentState; //В случае, если служба находится в неопределенном состоянии, // ин
крементируем счетчик if((dwCurrentState == SERVICE_RUNNING) || (dwCurrentState == SERVICE_STOPPED)) 
Stat.dwCheckPoint = 0; else Stat.dwCheckPoint = dwCheckPoint++; //В конце устанавливаем состояние сл
ужбы SetServiceStatus(Srv_Handle, &Stat); } 

Пожалуй, особо распространяться по поводу приведенной программы не стану. Комментариев будет наверняка более чем достаточно для того, чтобы понять ее работу. Все, что делает наш новоиспеченный сервис, так это выдает звуковой сигнал каждую секунду (фрагмент кода):
while (TRUE)
{
MessageBeep(1000);
Sleep(1000);
}

Особенность работы служб состоит в том, что они как правило не интерактивны. Поэтому приходится все максимально автоматизировать. Теперь, если вы попытаетесь запустить откомпилированную вышеприведенную программу, то ничего не произойдет:-). Вы просто получите надпись, сообщающую, что это "printf("Тестовый сервис");". В принципе, у нас все готово, только надо каким-то образом установить данный сервис в систему. В этом нам поможет следующее приложение, о котором я хочу вам рассказать.
Итак, пишем управляющую программу, при помощи которой мы будем устанавливать, удалять, запускать и останавливать сервис, а также считывать информацию о нем. Открываем новый диалоговый проект, обзываем его MNG и проектируем интерфейс таким вот образом:



Кнопка EXIT переделана (путем переименования:-) кнопки OK). Все остальные, естественно, придется вставлять новые. Наша программа будет по введенному пользователем пути к исполняемому файлу инсталлировать сервис в систему, после чего его можно будет деинсталлировать так же легко, как и установили. Ниже привожу в таблице список элементов интерфейса с соответствующими обработчиками:
Имя ID Имя переменной Ее тип Имя функции
Path IDC_path m_path CString
Brouse IDC_Brouse void CMngDlg::OnBrouse()
Install Service IDC_Install void CMngDlg::OnInstall()
Start Service IDC_Start void CMngDlg::OnStart()
Stop Service IDC_Stop void CMngDlg::OnStop()
Conf IDC_Conf void CMngDlg::OnConf()
Unistall Service IDC_De_inst void CMngDlg::OnDeinst()

Теперь, когда интерфейс создан, начнем писать собственно тело приложения. Нам дополнительно понадобятся следующие переменные класса CMngDlg:
SC_HANDLE Service— будем хранить в ней HANDLE на наш сервис при его открытии;
SERVICE_STATUS Status— понадобится для сохранения статуса (извините за каламбур, масло ведь масленое, не так ли?);
SC_HANDLE Mngr— а тут будет содержаться HANDLE на менеджер управления сервисами.

Дополнительных функций не понадобится. Теперь, если вы попробуете откомпилировать этот "полуфабрикат", то у вас ничего не выйдет:-). Необходимо еще в файле mngDlg.h включить *.h-файл winsvc.h: #include <winsvc.h> . А также определить указатель типа char: char *Srv_name="My ferst service";. Именно это имя будет отображаться в панели управления после инсталляции нашего сервиса в систему. Поместите вышеуказанную строку перед описанием класса CaboutDlg в файле mngDlg.cpp. Все, теперь подготовительная часть закончена, осталось только соответствующим образом изменить тела функций. Привожу их краткое описание. Начнем с void CMngDlg::OnBrouse() (начинать работу с приложением также необходимо с этой кнопки, указав путь к .exe-файлу нашего сервиса). Функция стандартна и пояснения своей работы не требует (если не ошибаюсь, я описывал подобную "конструкцию" уже неоднократно).
void CMngDlg::OnBrouse() { CFileDialog Dfile(TRUE);// Создаем элемент класса CfileDialog Dfile
 int iResult=Dfile.DoModal();//а теперь собственно "ищем файл" m_path = Dfile.GetPathName(
);//Присваиваем переменной m_path путь к найденному файлу UpdateData( FALSE ); //и обновляем данные 
в форме, чтобы пользователь увидел результаты своей "работы":-) } 

Переходим к более интересной части, а именно к установке сервисного приложения в систему:
void CMngDlg::OnInstall() { // Открываем менеджер управления сервисами Mngr = OpenSCManager(NU
LL, NULL, SC_MANAGER_ALL_ACCESS); //Обрабатываем ошибку открытия if(!Mngr) { MessageBox("Ошибка
 открытия менеджера"); return; }; //Если мы до этого не указали путь к исполняемому файлу, сооб
щаем об этом //пользователю if(m_path=="") { MessageBox("Не указан путь к исполняемом
у сервису"); return; } UpdateData(TRUE); // Инсталлируем сервис с именем Srv_name Service = Cre
ateService( Mngr, Srv_name, Srv_name, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_
START, SERVICE_ERROR_NORMAL, m_path, NULL, NULL, "", NULL, NULL); //Закрываем HANDLE на ме
неджер сервисов и выходим из функции CloseServiceHandle(Mngr);} 

Работа функции проста и прямолинейна. В результате при корректно указанном пути к файлу собственно службы мы можем активно пользоваться ею при помощи стандартных системных средств— Панели управления. Итак, открываем Пуск-> Настройка-> Панель управления-> Администрирование-> Службы и ищем нашу службу. В принципе, программа выполнила свою основную задачу:-). Но у нас немножко другая цель. Нам нужно научиться полностью управлять сервисом. А посему продолжу описание остальных обработчиков.

void CMngDlg::OnStart()— функция запуска службы:
void CMngDlg::OnStart() {// Открываем службу управления сервисами Mngr = OpenSCManager(NULL, N
ULL, SC_MANAGER_ALL_ACCESS); //Обрабатываем возможную ошибку открытия if(!Mngr) return; //Обращаемся
 к сервису с именем Srv_name (получаем на него HANDLE) Service = OpenService( Mngr, Srv_name, SERVIC
E_ALL_ACCESS); //Снова обработка ошибки if(!Service) return; // Запускаем сервис StartService(Servic
e, 0, NULL); //Закрываем HANDLE CloseServiceHandle(Mngr); } 

Что еще сказать об этой функции? Добавить к комментарию особо нечего. Идем дальше.
void CMngDlg::OnStop()— нам иногда требуется и останавливать службы:
void CMngDlg::OnStop() {//Вновь открываем менеджер служб Mngr = OpenSCManager(NULL, NULL, SC_M
ANAGER_ALL_ACCESS); if(!Mngr) return; //Открываем нужный нам сервис Service = OpenService( Mngr, Srv
_name, SERVICE_ALL_ACCESS); // И останавливаем его ControlService(Service, SERVICE_CONTROL_STOP, &St
atus); //Закрываем HANDLE за ненадобностью CloseServiceHandle(Mngr); }

Иногда неплохо было бы получить информацию о службе. Для чего используем void CMngDlg::OnConf(), тело которой должно выглядеть следующим образом:
void CMngDlg::OnConf() { //Определяем буферные переменные LPQUERY_SERVICE_CONFIG Buf; DWORD By
tes; char sz_Buf[1024]; //Открываем менеджер сервисов Mngr = OpenSCManager(NULL, NULL, SC_MANAGER_AL
L_ACCESS); //Обрабатываем ошибки if(!Mngr) return; //Открываем нужный нам сервис Service = OpenServi
ce( Mngr, Srv_name, SERVICE_ALL_ACCESS); if(!Service) return; //Выделяем память для буфера Buf = (LP
QUERY_SERVICE_CONFIG)malloc(4096); //Если выделение памяти прошло успешно, получаем конфигурацию слу
жбы и сохраняем //ее в буфере if(Buf != NULL) { QueryServiceConfig(Service, Buf, 4096, &Bytes); // О
тображаем некоторые поля конфигурации wsprintf(sz_Buf, "Путь к исполняемому файлу: %s\n" &
quot;Имя запуска: %s\n" "Отображаемое имя: %s\n", Buf->lpBinaryPathName, Buf->l
pServiceStartName, Buf->lpDisplayName); MessageBox(sz_Buf); // Очищаем память free(Buf); } //Удал
яем HANDLE CloseServiceHandle(Mngr); return; }

После того, как вы наигрались с установкой, запуском и остановкой службы, не мешало бы удалить ее из Windows при помощи void CMngDlg::OnDeinst():
void CMngDlg::OnDeinst() { //Обращаемся к менеджеру Mngr = OpenSCManager(NULL, NULL, SC_MANAGE
R_ALL_ACCESS); if(!Mngr) return; // Заставляем его найти нужный нам сервис Service = OpenService( Mn
gr, Srv_name, SERVICE_ALL_ACCESS); //Обрабатываем ошибки if(!Service) return; // Останавливаем служб
у ControlService(Service, SERVICE_CONTROL_STOP, &Status); // Удаляем службу из системы DeleteService
(Service); //Закрываем HANDLE CloseServiceHandle(Mngr); } 

Ну вот, пожалуй, и все на сегодня. Со службами теперь все умеют работать и могут писать хоть свой Apache, хоть MySQL. Исходя из приведенных примеров, можно создавать службы практически любой сложности:-). А посему желаю удачи вам во всех благих начинаниях:-). Читайте Help aka MSDN!

Спичеков Александр aka MentalzavR,
Zavr6@mail.ru



Компьютерная газета. Статья была опубликована в номере 14 за 2004 год в рубрике soft :: win

©1997-2024 Компьютерная газета