Создание сервисов для Windows NT

Приложение, управляющее сервисом Код, описанный в этой статье, работает только в Windows NT / 2000 / XP, поскольку Windows 98 не поддерживает работу с сервисами.

Создание сервиса

Как правило сервис представляет собой консольное приложение, поэтому программа должна содержать функцию main().

Сначала объявим глобальные переменные:

DWORD dwErrCode;                      //код ошибки
SERVICE_STATUS ss; //текущее сосотояние сервиса
SERVICE_STATUS_HANDLE ssHandle; //дескриптор статуса сервиса

LPTSTR SomeServiceName = "SomeService"; //имя сервиса

Функция main()

void main(int argc, char* argv[]) {
//таблица точек входа
SERVICE_TABLE_ENTRY DispatcherTable[] =
{ { SomeServiceName, //имя сервиса
(LPSERVICE_MAIN_FUNCTION)ServiceMain }, //главная функция сервиса
{ NULL, NULL }
};

Здесь мы создаем таблицу точек входа сервиса.

Структура типа SERVICE_TABLE_ENTRY позволяет задать функцию ServiceMain() для сервиса, носящего указанное в структуре имя.

   //запуск диспетчера
if(!StartServiceCtrlDispatcher(DispatcherTable)) {
printf("StartServiceCtrlDispatcher: Error %ld\n",
GetLastError());
getch();
return;
}
}

Функция StartServiceCtrlDispatcher() связывает главный поток сервиса с менеджером сервисов (service control manager, SCM).

Когда SCM запускает сервис, он ожидает выполнения сервисом функции StartServiceCtrlDispatcher(). Главный поток сервиса должен вызвать эту функцию как можно быстрее. Если функция выполняется успешно, она связывает вызывающий ее поток с SCM и не завершается, пока не будет остановлен сервис. SCM использует это соединение, чтобы посылать сервису управляющие запросы.

Если сервис запускается как отдельный процесс (как, например, в нашем случае), он должен вызвать функцию StartServiceCtrlDispatcher() немедленно. Поэтому всю инициализацию следует делать позже, в функции ServiceMain(). Если в рамках одного процесса запускается несколько сервисов, главный поток должен вызвать функцию StartServiceCtrlDispatcher() не позднее, чем через 30 секунд.

Функция ServiceMain()

Функция ServiceMain() определяется процессом в качестве точки входа для данного сервиса. Эта функция может носить любое имя, заданное приложением.

void WINAPI ServiceMain(DWORD argc, LPSTR* argv) {
//регистрация управляющей функции сервиса
ssHandle = RegisterServiceCtrlHandler(SomeServiceName, ServiceControl);
if(!ssHandle) {
printf("Error registering ServiceControl\n");
getch();
return;
}

Аргументы функции ServiceMain() (аналогичны аргументам функции main()): dwArgc - количество аргументов, lpszArgv - указатель на массив, который содержит указатели на строковые аргументы функции.

Функция RegisterServiceCtrlHandler() регистрирует функцию, которая будет обрабатывать управляющие запросы к сервису (такие, например, как остановка сервиса). В случае успешного завершения функция возвращает дескриптор статуса сервиса (service status handle). При неудачном завершении функция возвращает нулевое значение.

   //заполняем структуру, определяющую состояние сервиса:
//сервис выполняется как отдельный процесс
ss.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
//устанавливаем состояние ожидания запуска сервиса
SetSomeServiceStatus(SERVICE_START_PENDING, NO_ERROR, 4000);

//инициализация для SomeService
InitSomeServiceData(argc, argv);

//устанавливаем сосотояние работающего сервиса
SetSomeServiceStatus(SERVICE_RUNNING, NO_ERROR, 0);

//основной код программы
//...

return;
}

Поле dwServiceType структуры ss устанавливаем в SERVICE_WIN32_OWN_PROCESS. Это означает, что сервис будет выполняться как отдельный процесс, т.е. будет иметь собственное адресное пространство.

Затем устанавливаем состояние ожидания запуска сервиса c помощью созданной нами вспомогательной функции SetSomeServiceStatus(). Эта функция изменяет содержимое структуры ss, которое использует SCM для получения информации о сервисе.

Далее можно выполнить всю необходимую инициализацию для сервиса (определяемая пользователем функция InitSomeServiceData()).

Когда инициализация выполнена, вызываем функцию SetSomeServiceStatus() с параметром SERVICE_RUNNING, чтобы установить состояние работающего сервиса.

После изменения состояния сервиса выполняется основной код программы.

Функция ServiceControl()

Эта функция является управляющей функцией сервиса и может носить любое имя, определенное приложением.

void WINAPI ServiceControl(DWORD dwControlCode) {
//анализируем код команды и выполняем ее
switch(dwControlCode) {
//команда остановки сервиса
case SERVICE_CONTROL_STOP: {
//устанавливаем состояние ожидания остановки
SetSomeServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);

//остановка сервиса
StopSomeService();

//устанавливаем сосотояние остановки сервиса
SetSomeServiceStatus(SERVICE_STOPPED, NO_ERROR, 0);
break;
}

С помощью функции SetSomeServiceStatus() мы устанавливаем состояние ожидания остановки сервиса, после чего можно вызвать определяемую пользователем функцию StopSomeService(), которая выполнит все необходимые действия перед остановкой сервиса. Содержимое этой функции зависит от конкретного сервиса.

Далее с помощью функции SetSomeServiceStatus() сообщаем SCM, что сервис остановлен.

      //определение текущего состояния сервиса
case SERVICE_CONTROL_INTERROGATE: {
SetSomeServiceStatus(ss.dwCurrentState, NO_ERROR, 0);
break;
}
default: {
SetSomeServiceStatus(ss.dwCurrentState, NO_ERROR, 0);
break;
}
}
}

Когда сервис получает команду SERVICE_CONTROL_INTERROGATE, это означает, что он должен немедленно обновить информацию о статусе сервиса, используемую SCM.

{mospagebreak} 

Функция SetSomeServiceStatus()

Эта функция изменяет содержимое структуры ss, которое использует SCM для получения информации о статусе сервиса.

void SetSomeServiceStatus(DWORD dwCurrentState,
DWORD dwWin32ExitCode,
DWORD dwWaitHint)
{
//счетчик шагов длительных операций
static DWORD dwCheckPoint = 1;

//если сервис не находится в процессе запуска, его можно остановить
if(dwCurrentState == SERVICE_START_PENDING)
ss.dwControlsAccepted = 0;
else
ss.dwControlsAccepted = SERVICE_ACCEPT_STOP;


ss.dwCurrentState = dwCurrentState;
ss.dwWin32ExitCode = dwWin32ExitCode;
//время ожидания запуска сервиса
ss.dwWaitHint = dwWaitHint;

//если сервис не работает и не остановлен, увеличиваем значение счетчика
//шагов длительных операций
if(dwCurrentState == SERVICE_RUNNING || dwCurrentState == SERVICE_STOPPED)
ss.dwCheckPoint = 0;
else
ss.dwCheckPoint = dwCheckPoint++;


//обновить информацию о сервисе
SetServiceStatus(ssHandle, &ss);
}

Значение поля dwCheckPoint следует периодически изменять во время длительного запуска или остановки. Например, если при запуске сервиса инициализация выполняется в несколько этапов, то нужно увеличивать значение dwCheckPoint после каждого этапа. При этом программа, которая запросила услуги сервиса, может узнать, какой объем работы уже выполнен сервисом. Поле dwCheckPoint должно быть равно нулю, когда сервис не находится в состоянии запуска, остановки или выполнения длительной операции.

Поле dwControlsAccepted определяет управляющий код, который может принимать и обрабатывать данный сервис. По умолчанию все сервисы могут принимать команду SERVICE_CONTROL_INTERROGATE. Если это поле имеет значение SERVICE_ACCEPT_STOP, то сервис может быть остановлен с помощью команды SERVICE_CONTROL_STOP.

Функции InitSomeServiceData() и StopSomeService()

BOOL InitSomeServiceData() {
//...
return TRUE;
}


BOOL StopSomeService() {
//...
return TRUE;
}

С помощью этих функций проводится инициализация, необходимая для данного сервиса, а также действия, которые необходимо выполнить перед остановкой сервиса. Содержимое этих функций зависит от конкретного сервиса.

Приложение, управляющее сервисом

Приложение SomeSrvcApp может инсталлировать сервис в систему, запускать и останавливать его, получать информацию о сервисе и удалять сервис из системы.

Здесь не будут описываться функции WinMain() и InitApp(), поскольку они не обладают никакими особенностями, заслуживающими внимания. Функция WinMain() создает главное окно с шестью кнопками: "Install Service", "Start Service", "Get Service Configuration", "Stop Service", "Remove Service" и "Exit".

Функция WndProc()

В функции WndProc() используются макросы для обработки сообщений WM_COMMAND и WM_DESTROY:

LRESULT WINAPI
WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg) {
HANDLE_MSG(hWnd, WM_COMMAND, WndProcOnCommand);
HANDLE_MSG(hWnd, WM_DESTROY, WndProcOnDestroy);
default:
return DefWindowProc(hWnd, msg, wParam, lParam);
}
}

Функция WndProcOnCommand()

Функция WndProcOnCommand() вызывается функцией WndProc(), если окно получает сообщение WM_COMMAND. Функция WndProcOnCommand() содержит код, выполняющийся при нажатии на кнопки главного окна приложения.

Приведем полностью код этой функции:

void
WndProcOnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify) {
SC_HANDLE hService; //дескриптор сервиса
SC_HANDLE hSCManager; //дескриптор Service Control Manager
LPQUERY_SERVICE_CONFIG lpBufConfig;
DWORD dwBytesNeeded;
char szBufConfig[4096];

switch(id) {
//установка сервиса в систему
case IDB_INSTALL: {
//открываем SCM
hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
if(!hSCManager) {
MessageBox(NULL, "Can't open Service Control Manager", "Error",
MB_OK | MB_ICONERROR);
break;
}
//создаем сервис SomeService
hService = CreateService(hSCManager, SomeServiceName,
SomeServiceName, SERVICE_ALL_ACCESS,
SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START,
SERVICE_ERROR_NORMAL,
"c:\\Services\\SomeSrvc.exe",
NULL, NULL, NULL, NULL, NULL);
if(!hService) {
MessageBox(NULL, "Can't create service", "Error",
MB_OK | MB_ICONERROR);
CloseServiceHandle(hSCManager);
break;
}
//закрываем дескриптор сервиса
CloseServiceHandle(hService);
//закрываем дескриптор SCM
CloseServiceHandle(hSCManager);
break;
}

Здесь мы отрываем базу данных Service Control Manager (SCM) с помощью функции OpenSCManager() с уровнем доступа SC_MANAGER_CREATE_SERVICE, который позволяет создавать сервисы.

Для создания сервиса используется функция CreateService(), которая возвращает дескриптор созданного сервиса или NULL в случае неудачного выполнения. Одним из параметров функции является путь к сервису, который должен соответствовать расположению сервиса в Вашем компьютере. Проследите, чтобы путь был указан правильно, иначе создать сервис не получится.

В качестве типа сервиса мы указали SERVICE_WIN32_OWN_PROCESS. Это означает, что сервис будет выполняться как отдельный процесс. При успешном завершении функция CreateService() добавляет сервис в базу данных SCM. Вы можете в этом убедиться, открыв системную панель управления сервисами (Панель управления -> Администрирование -> Службы).

      //запуск сервиса
case IDB_START: {
hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if(!hSCManager) {
MessageBox(NULL, "Can't open Service Control Manager",
"Error", MB_OK | MB_ICONERROR);
break;
}
hService = OpenService(hSCManager, SomeServiceName,
SERVICE_START);
if(!hService) {
GetSomeSvcError();
break;
}
if(!StartService(hService, 0, NULL))
MessageBox(NULL, "Can't start service", "Error",
MB_OK | MB_ICONERROR);
CloseServiceHandle(hService);
CloseServiceHandle(hSCManager);
break;
}
{mospagebreak} 

Для открытия сервиса мы используем функцию OpenService(). Третьим параметром этой функции является тип доступа к сервису. Указывая тип доступа SERVICE_START, мы получаем возможность запускать сервис.

Запуск сервиса осуществляется с помощью функции StartService(), которая возвращает нулевое значение, если запустить сервис не удалось.

      //получить информацию о сервисе
case IDB_CONFIG: {
hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if(!hSCManager) {
MessageBox(NULL, "Can't open Service Control Manager",
"Error", MB_OK | MB_ICONERROR);
break;
}
hService = OpenService(hSCManager, SomeServiceName,
SERVICE_QUERY_CONFIG);
if(!hService) {
GetSomeSvcError();
break;
}
lpBufConfig = (LPQUERY_SERVICE_CONFIG)malloc(4096);
if(lpBufConfig != NULL) {
//сохраняем конфигурацию в буфере
QueryServiceConfig(hService, lpBufConfig, 4096,
&dwBytesNeeded);
//отображаем поля конфигурации
wsprintf(szBufConfig,
"Binary path: %s\nStart Name: %s\nDisplay Name: %s",
lpBufConfig->lpBinaryPathName,
lpBufConfig->lpServiceStartName,
lpBufConfig->lpDisplayName);
MessageBox(NULL, szBufConfig, szWindowTitle,
MB_OK | MB_ICONINFORMATION);
free(lpBufConfig);
}
else {
MessageBox(NULL, "Can't create buffer.\nNot enough memory.",
"Error", MB_OK | MB_ICONERROR);
}
CloseServiceHandle(hService);
CloseServiceHandle(hSCManager);
break;
}

На этот раз, открывая сервис, мы указываем тип доступа SERVICE_QUERY_CONFIG, чтобы получить информацию о конфигурации сервиса. Эту информацию мы помещаем в структуру типа QUERY_SERVICE_CONFIG (через указатель на эту структуру lpBufConfig, используя динамически выделяемую память) с помощью функции QueryServiceConfig(). Функция QueryServiceConfig() помещает в структуру типа QUERY_SERVICE_CONFIG информацию о сервисе, которая находится в реестре. Эта информация была помещена в реестр функцией CreateService().

Далее мы помещаем содержимое некоторых полей структуры в буфер szBufConfig и выводим его содержимое с помощью функции MessageBox().

      //остановка сервиса
case IDB_STOP: {
hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if(!hSCManager) {
MessageBox(NULL, "Can't open Service Control Manager",
"Error", MB_OK | MB_ICONERROR);
break;
}
hService = OpenService(hSCManager, SomeServiceName,
SERVICE_STOP);
if(!hService) {
GetSomeSvcError();
break;
}
//останавливаем сервис
if(!ControlService(hService, SERVICE_CONTROL_STOP, &ss))
MessageBox(NULL, "Can't stop service", "Error",
MB_OK | MB_ICONERROR);
CloseServiceHandle(hService);
CloseServiceHandle(hSCManager);
break;
}

Здесь мы открываем сервис, используя уровень доступа SERVICE_STOP.

Для остановки сервиса используется функция ControlService(). Эта функция посылает сервису управляющий код, который она получает в качестве второго параметра. В нашем случае управляющий код равен константе SERVICE_CONTROL_STOP. Третьим параметром функции является адрес структуры типа SERVICE_STATUS, в которой содержится информация о текущем статусе сервиса. Содержимое этой структуры использует SCM. Если сервис остановить не удалось функция ControlService() возвращает нулевое значение.

      //удаление сервиса из системы
case IDB_REMOVE: {
hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if(!hSCManager) {
MessageBox(NULL, "Can't open Service Control Manager",
"Error", MB_OK | MB_ICONERROR);
break;
}
hService = OpenService(hSCManager, SomeServiceName,
SERVICE_STOP | DELETE);
if(!hService) {
GetSomeSvcError();
break;
}
if(ss.dwCurrentState != SERVICE_STOPPED)
ControlService(hService, SERVICE_CONTROL_STOP, &ss);
//удаляем сервис из системы
DeleteService(hService);
CloseServiceHandle(hService);
CloseServiceHandle(hSCManager);
break;
}

В этом случае, открывая сервис, мы используем комбинацию флагов SERVICE_STOP и DELETE, поскольку если сервис в данный момент работает, то прежде чем его удалять из системы, его необходимо остановить. Для удаления сервиса из системы используется функция DeleteService().

      //завершаем работу приложения
case IDB_EXIT: {
PostQuitMessage(0);
return;
}
default: break;
}
return FORWARD_WM_COMMAND(hWnd, id, hwndCtl, codeNotify, DefWindowProc);
}

Функции WndProcOnDestroy() и GetSomeSvcError()

Приведем код функции WndProcOnDestroy() и функции GetSomeSvcError(), которая используется для вывода сообщений об ошибках:

void
WndProcOnDestroy(HWND hWnd) {
PostQuitMessage(0);
}


void GetSomeSvcError() {
DWORD dwError = GetLastError();
char errMess[100];
switch(dwError) {
case ERROR_ACCESS_DENIED: {
sprintf(errMess, "The specified service control manager"\
"database handle does not have access to"\
"the service.");
break;
}
case ERROR_INVALID_HANDLE: {
sprintf(errMess, "The specified handle is invalid.");
break;
}
case ERROR_INVALID_NAME: {
sprintf(errMess, "The specified service name is invalid.");
break;
}
case ERROR_SERVICE_DOES_NOT_EXIST: {
sprintf(errMess, "The specified service does not exist.");
break;
}
default: {
sprintf(errMess, "Can't open service.\nError number not found.");
}
}
MessageBox(NULL, errMess, "Error", MB_OK | MB_ICONERROR);
return;
}

В функции GetSomeSvcError() анализируется код ошибки, полученный с помощью функции Win API GetLastError(). Полный список кодов ошибок и их значений можно посмотреть в файле WinError.h или в документации по Win API.