ВСЕВОЛОД СТАХОВ
Программирование сервисов в Windows 2000
Недавно я занимался разработкой утилитки, позволяющей запускать с сервера по сети некоторые программы на определённое время. При этом обмен происходил через UDP-сокеты. Для написания клиентской части необходимы были требования, чтобы она работала под управлением Windows 2000, и обычный непривилегированный пользователь не мог выключить или включить её.
Для этой цели идеально подходят сервисы – служебные программы, выполняющиеся в фоновом режиме и без возможности завершения процесса через диспетчер задач. Для управления сервисами используется административный компонент Windows NT – services.msc (находится в папке «Администрирование» панели управления). Как сервисы организовано множество системных служб Windows, таких как сетевые клиенты и серверы, утилиты управления оборудованием (пресловутый Plug and Play), а также драйверы ядра. Из компонента управления сервисами можно изменять режим работы сервисов. Сервис характеризуется состоянием: запущен, остановлен, приостановлен. Для управления сервисами используются соответствующие кнопки: запустить, остановить, приостановить (доступно лишь для некоторых сервисов), перезапустить. Можно сделать, чтобы некоторые сервисы запускались при запуске компьютера. Для этого существует три режима запуска сервиса: автоматический (auto) – сервис запускается при входе в систему вручную, (manual) – сервис запускается по требованию пользователя или системы, отключено (disabled) – сервис вообще не будет запускаться.
Для изменения режима запуска откройте диалог свойств сервиса и на вкладке «Общие» выберите нужный режим. На данной вкладке также показано имя и описание сервиса (их можно изменить), а также путь к исполняемому файлу сервиса. Кроме того, можно изменять и другие параметры сервиса. Для этого смотрите встроенную справку Windows. При любых манипуляциях с сервисами учтите, что изменение базы данных сервисов доступно только администраторам. Кроме этого, возможна модификация сервисов на членах NT домена через MMC (Microsoft Management Console – mmc.exe) пользователем, имеющим права администратора домена.
Создание сервиса в ОС Windows – задача нетривиальная. Например, создание демона в ОС UNIX намного проще, хотя, я думаю, система сервисов лучше продумана и более централизована. Для создания нового сервиса, прежде всего, его нужно зарегистрировать в базе данных сервисов. Для этого открывается база данных сервисов функцией OpenSCManager для записи-создания, далее добавляется сам сервис функцией CreateService. После этого система может запустить приложение, зарегистрировавшее себя сервисом, но запуск идёт несколько необычным образом: запускается не WinMain, а ServiceMain, которая определяет обработчики событий сервиса (таких как запуск, пауза, остановка) функцией RegisterServiceCtrlHandler, устанавливает текущее состояние сервиса SetServiceStatus и выполняет функцию StartService для каждого зарегистрированного сервиса (их может быть несколько). Немного сложновато, не так ли? Но реально создать сервис ещё сложнее, так как данные функции принимают очень много параметров, например функция CreateProcess принимает аж 13 (!) параметров. Но я всё же попытаюсь вкратце рассказать о каждой функции и приведу конкретный пример сервиса. Итак, начнём с функции открытия базы данных сервисов:
SC_HANDLE OpenSCManager (const char *MachineName, const char* DatabaseName, DWORD Desired Access);
Функция возвращает дескриптор базы данных сервисов с именем DatabaseName, если данный параметр равен NULL, то используется база данных по умолчанию (для этой же цели можно использовать константу SERVICES_ACTIVE_DATABASE) на компьютере MachineName или на локальном компьютере, если данный параметр NULL. Параметр DesiredAccess определяет режим доступа к файлу. Обычно используются константы GENERIC_READ, для чтения GENERIC_WRITE для создания новых сервисов или изменения параметров старых и GENERIC_EXECUTE разрешение на выполнение сервисов. При ошибке возвращается NULL, иначе – дескриптор базы данных.
Далее происходит регистрация сервиса функцией CreateService, которая возвращает дескриптор сервиса для использования в данном процессе:
SC_HANDLE CreateService(
SC_HANDLE hSCManager,
LPCTSTR lpServiceName,
LPCTSTR lpDisplayName,
DWORD dwDesiredAccess,
DWORD dwServiceType,
DWORD dwStartType,
DWORD dwErrorControl,
LPCTSTR lpBinaryPathName,
LPCTSTR lpLoadOrderGroup,
LPDWORD lpdwTagId,
LPCTSTR lpDependencies,
LPCTSTR lpServiceStartName,
LPCTSTR lpPassword
);
Итак, поподробнее о параметрах, так как я не думаю, что их названия говорят сами за себя.
- SC_HANDLE hSCManager – дескриптор базы данных сервисов, полученный функцией OpenSCManager.
- LPCTSTR lpServiceName – строка (до 256 символов), реальное имя сервиса в базе данных.
- LPCTSTR lpDisplayName – данный параметр – имя сервиса, которое показывается в инструменте управления сервисами.
- DWORD dwDesiredAccess – флаг доступа к сервису. Может принимать любые значения доступа. Обычно используются константы GENERIC_READ, GENERIC_WRITE, GENERIC_EXECUTE, а также значение SERVICE_ALL_ACCESS для предоставления полного доступа к сервису.
- DWORD dwServiceType – флаг типа сервиса, то есть то, как он будет выполняться системой. Возможные значения:
- SERVICE_WIN32_OWN_PROCESS – сервис существует в виде отдельного процесса;
- SERVICE_WIN32_SHARE_PROCESS – сервис разделяет процесс под названием services с другими сервисами;
- SERVICE_WIN32_KERNEL_DRIVER – сервис является драйвером ядра (то есть сам является частью ядра);
- SERVICE_WIN32_FILE_SYSTEM_DRIVER – сервис представляет собой драйвер файловой системы. Практически все сервисы, не являющиеся системными, создаются как собственные процессы SERVICE_WIN32_OWN_PROCESS. В этом случае, если необходимо, чтобы сервис взаимодействовал с рабочим столом, можно указать через битовое «или» флаг SERVICE_INTERACTIVE_PROCESS.
- DWORD dwStartType – флаг способа запуска сервиса, может принимать следующие значения:
- SERVICE_BOOT_START – служба запускается загрузчиком, NT то есть при загрузке ядра (допустимо только при создании драйвера ядра);
- SERVICE_SYSTEM_START – сервис запускается после загрузки ядра при инициализации драйверов (это также допустимо только для драйверов);
- SERVICE_AUTO_START – служба запускается при входе в систему;
- SERVICE_DEMAND_START – сервис запускается другим приложением (в том числе инструментом управления службами);
- SERVICE_DISABLED – сервис не может быть запущен. Для стандартных служб обычно применяются флаги SERVICE_AUTO_START и SERVICE_DEMAND_START для, соответственно, автоматической или ручной загрузки.
- DWORD dwErrorControl – флаг поведения при ошибке. Для большинства сервисов (кроме критичных и системных драйверов) разумно писать SERVICE_ERROR_IGNORE для игнорирования ошибки или SERVICE_ERROR_NORMAL для вывода сообщения о невозможности запуска (также добавляется запись в системный журнал). Для драйверов при невозможности их загрузки обычно происходит откат системы и (или) перезагрузка.
- LPCTSTR lpBinaryPathName – полный путь к исполняемому файлу сервиса. Учтите, что если путь содержит пробелы или является очень длинным, то писать его нужно в кавычках внутри строки, то есть «d:my foldermy_super service.exe».
- LPCTSTR lpLoadOrderGroup – имя группы сервиса, обычное значение – NULL, для указания, что сервис не принадлежит никакой группе.
- LPDWORD lpdwTagId – этот параметр содержит тег группы, которые применяются драйверами для идентификации внутри группы, обычное значение – NULL.
- LPCTSTR lpDependencies – массив строк-зависимостей сервиса. Если службы, от которых зависит данный сервис не запустились, то не произойдёт и запуска данного сервиса. Данный параметр помогает выстроить иерархическую структуру служб.
- LPCTSTR lpServiceStartName – имя пользователя для запуска сервиса. Имя вводится в формате Имя_доменаимя_пользователя (или .имя_пользователя для локального домена). Если данный параметр NULL, то используется системный профиль LocalSystem, что подходит для большинства сервисов.
- LPCTSTR lpPassword – параметр, определяющий пароль для выбранного профиля. Если параметр равен NULL, то считается, что пароль – пустая строка. Для профиля локальной системы LocalSystem пароль всегда должен быть NULL.
Итак, если вы дочитали, до этого момента, то всё остальное должно показаться вам детскими играми.
Кстати, сообщу информацию, которая многим покажется полезной: после регистрации сервиса его параметры записываются в системный реестр Windows в улей HKEY_LOCAL_MACHINESystemCurrentControlSetServices имя_сервиса. Для людей, любящих копаться в настройках ОС сообщаю: большинство системных драйверов являются сервисами и их настройки можно поменять в реестре. Например, очень интересным является ключ Tcpip – можно менять очень много «скрытых» настроек сети (включая настройки сетевых карт!). Для поклонников безопасной сети есть ещё один ключик: Lanmanserver – позволяет прикрыть некоторые дыры безопасности, например NetBios NULL session. Но вернёмся к реальности – к созданию сервисов. Чтобы еще больше отпугнуть читателя, приведу пример создания функции CreateService:
CreateService (hsSManager,lpServiceName, lpDisplayName, dwDesiredAccess, dwServiceType, dwStartControl,
lpBinaryPathName, lpLoadOrderGroup, lpdwTagId, lpDependencies, lpServiceStartName, lpPassword);
После создания сервиса считайте, что ваше дело завершено – смело пишите CloseServiceHandle (SC_HANDLE sh) и выходите из программы. Тут я сделаю небольшое лирическое отступление. Как известно, в ОС Windows после загрузки программы в память вызывается специализированная функция main для консольных приложений и WinMain для приложений GUI Windows.
Программа, работающая как служба, ведёт себя несколько по-другому: вызов сервиса происходит функцией StartService(SC_HANDLE sh, DWORD param_count, char **parameters), после чего он загружается в память (если ещё не был загружен) и вызывается функция ServiceMain(DWORD param_count, char **parameters), которая также ведёт себя очень хитро, но об этом чуть позднее.
Если для регистрации и для самого сервиса используется одна и та же программа, то обычно считывают аргументы командной строки. При этом используется следующая схема:
#include <windows.h>
int main(int argc, char **argv){
SC_HANDLE sh;
SC_HANDLE svdb; // Дескриптор базы сервисов
SERVICE_TABLE_ENTRY DispatchTable[] =
{
{ "MyService", MyServiceStart },
{ NULL, NULL }
};
if(argc < 2){ // Запуск без аргументов
// Запуск сервиса
if (!StartServiceCtrlDispatcher( DispatchTable))
{
write_to_log("Can`t execute service");
}
}
if(argc == 2){ // Передан один параметр
if(strcmp(argv[1], “-i”))
install_service(); // Установка сервиса если аргумент -i
if(strcmp(argv[1], “-u”))
uninstall_service(); // Удаление сервиса если аргумент -u
}
else{
write_to_log(“Bad usage”);
return -1;
}
return 0;
}
В принципе, всё не так уж и сложно, хотя я кое-чего не рассказал. Во-первых, функция OpenService служит для загрузки сервиса в память. Функция принимает три параметра: дескриптор базы сервисов, имя сервиса и тип доступа – тут всё понятно. Для удаления сервиса используется функция DeleteService(SC_HANDLE sh), которая возвращает ноль в случае ошибки. При использовании данной функции учтите, что при открытии сервиса функцией OpenService, вы должны указать в правах доступа единственный флаг DELETE для удаления его из базы. Для запуска самого сервиса используется функция StartServiceCtrlDispatcher(LPSERVICE_TABLE_ENTRY lpServiceStartTable). Функция принимает единственный аргумент – массив строк, содержащий две строки – имя сервиса и имя функции-обработчика, заканчивается список двумя пустыми строками. После этого начинается обработка функции сервиса ServiceMain.
Итак, начало самого интересного – написание функции ServiceMain. Данная функция служит для запуска, инициализации и изменения статуса сервиса. Поведение данной функции строго регламентировано: во-первых, она должна заполнить поля структуры SERVICE_STATUS значениями параметров данного сервиса; во-вторых – зарегистрировать обработчик событий сервиса функцией RegisterServiceCtrlHandler; и, в-третьих – выполнить действия по инициализации сервиса и установить состояние сервиса функцией SetServiceStatus. Итак, обо всём по порядку. Думаю, отдельного описания заслуживает структура SERVICE_STATUS:
struct SERVICE_STATUS {
DWORD dwServiceType; – это поле означает то же, что и в функции CreateService, т.е. тип приложения сервиса (отдельный, драйвер ядра,
драйвер ФС)
DWORD dwCurrentState; – а вот это специфическое поле – содержит текущее состояние сервиса, именно его должна устанавливать ServiceMain.
Допустимые значения:
SERVICE_STOPPED – сервис остановлен;
SERVICE_START_PENDING – сервис запускается;
SERVICE_STOP_PENDING – сервис останавливается;
SERVICE_RUNNING – сервис уже запущен;
SERVICE_CONTINUE_PENDING – сервис продолжает работу;
SERVICE_PAUSE_PENDING – сервис переходит в режим паузы;
SERVICE_PAUSED – сервис находится в режиме паузы.
DWORD dwControlsAccepted; – битовая маска, содержащая допустимые состояния сервиса (через побитное «или»).
Допустимые константы:
SERVICE_ACCEPT_STOP – сервис может быть остановлен;
SERVICE_ACCEPT_PAUSE_CONTINUE – сервис может быть поставлен и снят с паузы;
SERVICE_ACCEPT_SHUTDOWN – сервис будет оповещён при выходе из системы.
DWORD dwWin32ExitCode; – этот параметр сообщает системе значение, которое возвращает сервис при ошибке, подробнее ошибка определяется
следующим параметром.
DWORD dwServiceSpecificExitCode; – конкретная ошибка, произошедшая в сервисе
DWORD dwCheckPoint; – а это положение прогресс-бара при запуске-остановке сервиса, используется для визуализации процесса запуска
сервиса.
DWORD dwWaitHint; – время в миллисекундах, которое ждёт вызывающая программа до изменения либо текущего статуса, либо dwCheckPoint.
Если этого не случилось, то считается, что запуск сервиса был неудачен. Если данное значение ноль, то убиения не происходит.
};
Итак, обычно вначале устанавливается значение статуса SERVICE_START_PENDING, затем устанавливается обработчик сообщений, инициализация сервиса и только после этого статус сервиса устанавливается в SERVICE_RUNNING. При данных действиях не забудьте корректно установить dwWaitHint, иначе система сочтёт, что ваш сервис не успел запуститься, и замочит его без сожаления. Для регистрации обработчика событий используется функция SERVICE_STATUS_HANDLE RegisterServiceCtrlHandler(LPCTSTR service_name, LPHANDLER_FUNCTION handler_function) – первый параметр – имя сервиса, второй – указатель на функцию-обработчик, данная функция возвращает дескриптор состояния сервиса для функции SetServiceStatus или NULL в случае ошибки. Для установки текущего состояния сервиса используется функция SetServiceStatus (SERVICE_STATUS_HANDLE ssh, LPSERVICE_STATUS status) – первый параметр – дескриптор состояния, а второй – указатель на структуру статуса. После этого приведу простенький пример главной функции:
void MyServiceStart (DWORD argc, LPTSTR *argv)
{
DWORD status;
DWORD specificError;
SERVICE_STATUS MyServiceStatus; //Эта та самая структура статуса
// А вот мы её заполняем
MyServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
MyServiceStatus.dwCurrentState = SERVICE_START_PENDING;
MyServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
MyServiceStatus.dwWin32ExitCode = 0;
MyServiceStatus.dwServiceSpecificExitCode = 0;
MyServiceStatus.dwCheckPoint = 0;
MyServiceStatus.dwWaitHint = 5000;
//Регистрируем обработчик событий
MyServiceStatusHandle = RegisterServiceCtrlHandler("MyService", MyServiceCtrlHandler);
if (MyServiceStatusHandle == (SERVICE_STATUS_HANDLE)0)
{
write_to_log("Can not register handler function");
return;
}
//А теперь выполняем некоторые инициализационные действия.
status = MyServiceInitialization(argc,argv, &specificError);
// Проверяем, как прошла инициализация
if (status != NO_ERROR)
{
//Ошибка - останавливаем сервис и выходим
MyServiceStatus.dwCurrentState = SERVICE_STOPPED;
MyServiceStatus.dwCheckPoint = 0;
MyServiceStatus.dwWaitHint = 5000;
MyServiceStatus.dwWin32ExitCode = status;
MyServiceStatus.dwServiceSpecificExitCode = specificError;
SetServiceStatus (MyServiceStatusHandle, &MyServiceStatus);
return;
}
// Всё прошло успешно :) идём дальше.
MyServiceStatus.dwCurrentState = SERVICE_RUNNING;
MyServiceStatus.dwCheckPoint = 0;
MyServiceStatus.dwWaitHint = 0;
if (!SetServiceStatus (MyServiceStatusHandle, &MyServiceStatus))
{
status = GetLastError();
write_to_log("Can not set service status 8-(");
}
//Сервис начал работать.
write_to_log("Yeah, this works!",0);
return;
}
Прекрасная функция, не так ли? Ну и для последнего штриха расскажу ещё про обработчик сообщений. Если Вы когда-нибудь писали GUI программу, то, наверное, знаете, что обработчики событий – это длинная функция, которая получает номер события и выполняет в соответствии с этим некие действия. Так, в сервисах наблюдается нечто подобное. Формат функции должен быть такой:
void WINAPI Handler(DWORD fdwControl);
параметр fdwControl содержит текущее сообщение от системы.
Возможные значения этого параметра:
- SERVICE_CONTROL_STOP – cервис должен остановиться.
- StartServiceCtrlDispatcher (LPSERVICE_TABLE_ENTRY lpServiceStartTable) SERVICE_CONTROL_PAUSE – cервис должен перейти в режим паузы.
- SERVICE_CONTROL_CONTINUE – cервис должен выйти из режима паузы.
- SERVICE_CONTROL_INTERROGATE – cервис должен сообщить о себе информацию.
- SERVICE_CONTROL_SHUTDOWN – система собирается выключаться.
Ну вот, в принципе, и всё. Для дальнейшей информации Вы можете обратиться к MSDN. Но для большинства сервисов этой информации оказывается достаточно. Итак, дерзайте!