Сообщений 8    Оценка 640 [+1/-0]         Оценить  
Система Orphus

Управление системными службами Windows NT

Часть 2

Автор: Александр Федотов
Опубликовано: 21.04.2002
Исправлено: 13.03.2005
Управление службами
Состояния системной службы
Определение текущего состояния службы
Запуск службы
Использование команд управления
Блокировка базы данных служб
Установка и удаление служб
Установка службы
Удаление службы
Перечисление установленных служб
Заключение
Ссылки

Демонстрационные программы
Исходный код программ

Управление службами

В первой части статьи мы рассматривали службы как статичные объекты - записи в конфигурационной базе данных SCM. Настало время "оживить" их: в этой части статьи мы рассмотрим запуск и остановку системных служб, а также другие вопросы, непосредственно связанные с их выполнением.

Состояния системной службы

Как объект исполнения, системная служба может находиться в одном из семи состояний, которые обозначаются константами, перечисленными в таблице 1.

Константа Состояние службы
SERVICE_STOPPED Служба остановлена
SERVICE_RUNNING Служба работает
SERVICE_PAUSED Служба приостановлена
SERVICE_STOP_PENDING Служба находится в процессе остановки
SERVICE_START_PENDING Служба находится в процессе запуска
SERVICE_PAUSE_PENDING Служба находится в процессе приостановки
SERVICE_CONTINUE_PENDING Служба находится в процессе возобновления после приостановки
Таблица 1. Состояния системной службы

Сразу после загрузки компьютера служба находится в состоянии SERVICE_STOPPED. Она остается в этом состоянии до тех пор, пока не будет запущена, либо автоматически в процессе инициализации системы, либо с помощью StartService позднее. Когда поступает команда на запуск службы, она автоматически переводится менеджером системных служб в состояние SERVICE_START_PENDING. Это один из немногих случаев, когда менеджер системных служб изменяет состояние службы; как правило, службы сами должны отслеживать свое состояние и сообщать его SCM. В состоянии SERVICE_START_PENDING служба находится, пока не завершит свою инициализацию и не сообщит о переходе в состояние SERVICE_RUNNING.

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

При поступлении команды остановки служба переходит сначала в состояние SERVICE_STOP_PENDING, говорящее о том, что она находится в процессе завершения работы, а затем в состояние SERVICE_STOPPED. Многие службы, которые завершаются достаточно быстро, не используют промежуточное состояние SERVICE_STOP_PENDING, а сразу рапортуют о своей остановке, сообщая о переходе в состояние SERVICE_STOPPED. Если процесс службы завершается непредвиденно, например, в результате сбоя, SCM автоматически переводит службу в состояние SERVICE_STOPPED.

При поступлении команды приостановки служба сначала переходит в состояние SERVICE_PAUSE_PENDING, а затем, когда приостановка выполнена, в состояние SERVICE_PAUSED. В приостановленном состоянии процесс службы существует, но служба не обрабатывает запросы клиентов. Точное определение этого состояния зависит от конкретной службы. Например, служба Web-сервера в этом состоянии может перестать принимать соединения от клиентов, а может и принимать соединения, но всегда возвращать код ошибки. Как и в случае с остановкой, промежуточное состояние SERVICE_PAUSE_PENDING является необязательным.

Наконец, при поступлении команды возобновления служба возвращается в состояние SERVICE_RUNNING, возможно, побывав в состоянии SERVICE_CONTINUE_PENDING. На рис. 1 показаны состояния службы и переходы между ними.


Рис. 1. Состояния службы

Следует отметить, что не все службы поддерживают все состояния и все команды управления. Команду приостановки и приостановленное состояние поддерживают лишь некоторые из стандартных служб Windows NT. Некоторые службы не поддерживают команду остановки; будучи однажды запущенными, они продолжают работу до выключения компьютера.

Определение текущего состояния службы

Функция QueryServiceStatus

Для определения текущего состояния службы предназначена функция QueryServiceStatus:

BOOL QueryServiceStatus(
    IN SC_HANDLE hService,              // handle службы
    OUT LPSERVICE_STATUS pServiceStatus // состояние службы
    );

Параметрами функции являются: handle службы, который должен быть открыт с правом доступа SERVICE_QUERY_STATUS, и указатель на структуру SERVICE_STATUS, в которую функция при успешном завершении заносит информацию о состоянии службы:

typedef struct _SERVICE_STATUS { 
    DWORD dwServiceType; 
    DWORD dwCurrentState; 
    DWORD dwControlsAccepted; 
    DWORD dwWin32ExitCode; 
    DWORD dwServiceSpecificExitCode; 
    DWORD dwCheckPoint; 
    DWORD dwWaitHint; 
} SERVICE_STATUS, * LPSERVICE_STATUS;

Давайте детально изучим поля этой структуры.

dwServiceType

Это поле содержит тип системной службы. Как мы помним, службы Win32 бывают двух типов: SERVICE_WIN32_OWN_PROCESS и SERVICE_WIN32_SHARE_PROCESS, а также может быть указан флаг SERVICE_INTERACTIVE_PROCESS.

dwCurrentState

Значением этого поля является одна из семи констант состояния службы, такая как SERVICE_STOPPED или SERVICE_RUNNING. Именно это поле описывает текущее состояние службы.

dwControlsAccepted

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

dwWin32ExitCode и dwServiceSpecificExitCode

Поле dwWin32ExitCode используется только при запуске и остановке службы. Если служба не может запуститься или останавливается в результате ошибки, в это поле заносится код ошибки Win32, который поясняет причину произошедшего. Один из кодов ошибок, ERROR_SERVICE_SPECIFIC_ERROR, говорит о том, что произошла ошибка, которая не описывается ни одним из стандартных кодов ошибок, a поле dwServiceSpecificExitCode содержит код ошибки, определенный службой. Интерпретация этого кода ошибки целиком и полностью зависит от конкретной службы.

dwCheckPoint и dwWaitHint

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

Интервал в 60 секунд не всегда является достаточным. Например, служба базы данных, может загружать некоторые служебные таблицы целиком в память при запуске, и этот процесс может занять длительное время. Чтобы сообщить SCM, что служба не зависла, а всего лишь выполняет длительную операцию, служба должна периодически обновлять свое состояние, каждый раз увеличивая значение dwCheckPoint. Кроме того, служба заносит в поле dwWaitHint время в миллисекундах, через которое она предполагает вновь обновить свое состояние. Программы управления службами могут использовать это поле для отображения индикатора прогресса при выполнении длительных операций.

Функция QueryServiceStatusEx

Функция QueryServiceStatusEx, впервые появившаяся в Windows 2000, является расширением функции QueryServiceStatus. Она возвращает всю ту информацию, которую выдает QueryServiceStatus, плюс идентификатор процесса службы, а также дополнительные флаги.

BOOL QueryServiceStatusEx(
    IN SC_HANDLE hService,       // handle службы
    IN SC_STATUS_TYPE InfoLevel, // вид информации
    OUT PBYTE pBuffer,           // приемный буфер
    IN DWORD cbBufSize,          // размер приемного буфера
    OUT PDWORD pcbBytesNeeded    // требуемый размер буфера
    );

Интерфейс этой функции, в отличие от QueryServiceStatus, более гибок и приспособлен к расширению: параметр InfoLevel задает вид информации, который необходимо получить. Хотя в настоящее время определено только одно значение для этого параметра, SC_STATUS_PROCESS_INFO, ничто не мешает добавлению новых видов информации в следующих версиях системы.

Параметр pBuffer указывает на буфер, в который функция помещает информацию о состоянии службы, в настоящее время это должна быть структура SERVICE_STATUS_PROCESS. В параметре cbBufSize следует передать размер структуры, а параметр pcbBytesNeeded должен указывать на переменную, в которую функция заносит необходимый размер буфера. На сегодняшний день от последнего параметра мало пользы, поскольку структура SERVICE_STATUS_PROCESS не содержит данных переменного размера (таких, как строки), но он может пригодиться в дальнейших версиях операционной системы.

Структура SERVICE_STATUS_PROCESS содержит все поля, которые есть в структуре SERVICE_STATUS, и два дополнительных поля:

typedef struct _SERVICE_STATUS_PROCESS {
    DWORD  dwServiceType;
    DWORD  dwCurrentState;
    DWORD  dwControlsAccepted;
    DWORD  dwWin32ExitCode;
    DWORD  dwServiceSpecificExitCode;
    DWORD  dwCheckPoint;
    DWORD  dwWaitHint;
    DWORD  dwProcessId;
    DWORD  dwServiceFlags;
} SERVICE_STATUS_PROCESS, * LPSERVICE_STATUS_PROCESS;

dwProcessId

Это поле содержит идентификатор процесса службы, если, конечно, служба находится в состоянии, отличном от SERVICE_STOPPED. Если служба остановлена, это поле содержит ноль.

dwServiceFlags

Это поле содержит дополнительные флаги службы. В настоящее время определен только один такой флаг, SERVICE_RUNS_IN_SYSTEM_PROCESS, который, если установлен, говорит о том, что служба выполняется в системном процессе. К таким процессам относятся lsass.exe, services.exe и winlogon.exe. Этот флаг устанавливается, только если служба работает. Он не будет установлен в состоянии SERVICE_STOPPED, даже если служба зарегистрирована для исполнения в одном из системных процессов.

Пример: определение текущего состояния службы

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

//-----------------------------------------------------------------------
// DemoServiceStatus
//
//  Отображает текущее состояние службы.
//
//  Параметры:
//      hSCM           - handle SCM
//      pszServiceName - имя службы
//
//  Возвращает:
//      ноль - при успешном завершении, отличное от нуля значение - в
//      противном случае.
//
int DemoServiceStatus(
    IN SC_HANDLE hSCM, 
    IN PCTSTR pszServiceName
    )
{
    _ASSERTE(hSCM != NULL);
    _ASSERTE(pszServiceName != NULL);
    
    SC_HANDLE hService;
    SERVICE_STATUS Status;

    // открываем handle службы с доступом SERVICE_QUERY_STATUS
    hService = OpenService(hSCM, pszServiceName, SERVICE_QUERY_STATUS);
    if (hService == NULL)
        return PrintError(_T("OpenService failed"), GetLastError());

    // получаем состояние службы
    if (!QueryServiceStatus(hService, &Status))
    {
        PrintError(_T("QueryServiceStatus failed"), GetLastError());
        CloseServiceHandle(hService);
        return 1;
    }

    PCTSTR pszServiceType;
    PCTSTR pszInteractive;
    PCTSTR pszStatus;

    switch (Status.dwServiceType & ~SERVICE_INTERACTIVE_PROCESS)
    {
        case SERVICE_KERNEL_DRIVER:
            pszServiceType = _T("Kernel Driver");
            break;
        case SERVICE_FILE_SYSTEM_DRIVER:
            pszServiceType = _T("File System Driver");
            break;
        case SERVICE_WIN32_OWN_PROCESS:
            pszServiceType = _T("Win32 Own Process");
            break;
        case SERVICE_WIN32_SHARE_PROCESS:
            pszServiceType = _T("Win32 Share Process");
            break;
        default:
            pszServiceType = _T("Unknown");
            break;
    }

    if (Status.dwServiceType & SERVICE_INTERACTIVE_PROCESS)
        pszInteractive = _T("(Interactive)");
    else
        pszInteractive = _T("");

    switch (Status.dwCurrentState)
    {
        case SERVICE_STOPPED:
            pszStatus = _T("Stopped");
            break;
        case SERVICE_RUNNING:
            pszStatus = _T("Running");
            break;
        case SERVICE_PAUSED:
            pszStatus = _T("Paused");
            break;
        case SERVICE_STOP_PENDING:
            pszStatus = _T("Stop Pending");
            break;
        case SERVICE_START_PENDING:
            pszStatus = _T("Start Pending");
            break;
        case SERVICE_PAUSE_PENDING:
            pszStatus = _T("Pause Pending");
            break;
        case SERVICE_CONTINUE_PENDING:
            pszStatus = _T("Continue Pending");
            break;
        default:
            pszStatus = _T("Unknown");
            break;
    }

    // выводим состояние службы
    _tprintf(_T("Type:              %s %s\n"), pszServiceType, 
             pszInteractive);
    _tprintf(_T("State:             %s\n"), pszStatus);
    _tprintf(_T("Controls Accepted:\n"));

    if (Status.dwControlsAccepted & SERVICE_ACCEPT_STOP)
        _tprintf(_T("\tStop\n"));
    if (Status.dwControlsAccepted & SERVICE_ACCEPT_PAUSE_CONTINUE)
        _tprintf(_T("\tPause and Continue\n"));
    if (Status.dwControlsAccepted & SERVICE_ACCEPT_SHUTDOWN)
        _tprintf(_T("\tShutdown\n"));
    if (Status.dwControlsAccepted & SERVICE_ACCEPT_PARAMCHANGE)
        _tprintf(_T("\tParam Change\n"));
    if (Status.dwControlsAccepted & SERVICE_ACCEPT_NETBINDCHANGE)
        _tprintf(_T("\tNet Bind Change\n"));
    if (Status.dwControlsAccepted & SERVICE_ACCEPT_HARDWAREPROFILECHANGE)
        _tprintf(_T("\tHardware Profile Change\n"));
    if (Status.dwControlsAccepted & SERVICE_ACCEPT_POWEREVENT)
        _tprintf(_T("\tPower Event\n"));

    _tprintf(_T("Win 32 Exit Code:  %d\n"), Status.dwWin32ExitCode);
    _tprintf(_T("Service Exit Code: %d\n"), 
             Status.dwServiceSpecificExitCode);
    _tprintf(_T("Check Point:       %d\n"), Status.dwCheckPoint);
    _tprintf(_T("Wait Hint:         %d\n"), Status.dwWaitHint);


#if _WIN32_WINNT >= 0x500
    // получаем и отображаем расширенную информацию о состоянии

    SERVICE_STATUS_PROCESS StatusEx;
    DWORD cbNeeded;

    if (!QueryServiceStatusEx(hService, SC_STATUS_PROCESS_INFO,
                              (PBYTE)&StatusEx, sizeof(StatusEx),
                              &cbNeeded))
    {
        PrintError(_T("QueryServiceStatus failed"), GetLastError());
        CloseServiceHandle(hService);
        return 1;
    }

    PCTSTR pszSystemProcess;
    if (StatusEx.dwServiceFlags & SERVICE_RUNS_IN_SYSTEM_PROCESS)
        pszSystemProcess = _T("Yes");
    else
        pszSystemProcess = _T("No");

    _tprintf(_T("Process Id:        %d\n"), StatusEx.dwProcessId);
    _tprintf(_T("System Process:    %s\n"), pszSystemProcess);

#endif

    CloseServiceHandle(hService);
    return 0;
}

Функция DemoServiceStatus, представленная в этом примере, входит в демонстрационную программу svcctrl.exe, которая содержит все примеры для этой части статьи. DemoServiceStatus будет вызвана, если svcctrl.exe запускается с параметром status в командной строке:

svcctrl status <имя службы>

В этом примере я ограничился директивами препроцессора, чтобы отделить код, предназначенный для исполнения в Windows 2000 и более поздних версиях. В реальной программе следовало бы находить адрес функции QueryServiceStatusEx в процессе выполнения с помощью GetProcAddress в зависимости от версии Windows.

Запуск службы

Для запуска службы Win32 API предоставляет функцию StartService:

BOOL StartService(
    IN SC_HANDLE hService, // handle службы
    IN DWORD dwArgc,       // количество аргументов
    IN PCTSTR * pArgv      // массив аргументов
    );

Функция принимает handle службы, который должен быть открыт с правом доступа SERVICE_START, а также массив аргументов, который будет передан службе. Каждая служба начинает свое выполнение с функции под условным названием ServiceMain, прототип которой напоминает прототип функции main:

VOID WINAPI ServiceMain(
    IN DWORD dwArgc,       // количество аргументов
    IN PCTSTR * pArgv      // массив аргументов
    );

Когда вызывается точка входа службы, pArgv[0] содержит имя службы, а остальные параметры соответствуют параметрам, переданным при вызове StartService.

Возможность передачи параметров службе при запуске используется не очень широко. Мне не известно, чтобы какая-либо из служб, поставляемых в составе Windows NT, принимала параметры таким образом. Если передавать параметры не требуется, то dwArgc следует указать как 0, а pArgv - как NULL.

На рис. 2 показано что происходит, когда приложение вызывает функцию StartService и на момент вызова StartService процесс службы еще не существует.


Рис. 2. Процесс запуска службы

  1. Приложение вызывает функцию StartService, которая посылает запрос на запуск службы менеджеру системных служб. SCM определяет, что процесс, соответствующий службе, еще не запущен, поэтому он вызывает функцию CreateProcessAsUser, передавая ей командную строку, указанную в конфигурации службы.
  2. Стартрует процесс службы. Управление передается функции WinMain (или просто main) процесса службы.
  3. Произведя необходимую инициализацию, WinMain вызывает функцию StartServiceCtrlDispatcher, которая реализует цикл приема и обработки команд SCM.
  4. StartServiceCtrlDispatcher устанавливает соединение со SCM и сразу же получает от него первую команду, а именно команду на запуск службы. Кроме того, менеджер системных служб сообщает приложению, запустившему службу, что служба начала выполняться. Именно в этот момент StartService возвращает управление прикладной программе.
  5. Получив команду на запуск службы, StartServiceCtrlDispatcher создает новый поток вызовом CreateThread, в котором вызывает главную функцию службы, обычно имеющую название ServiceMain. Непосредственно перед созданием потока StartServiceCtrlDispatcher вызывает SetServiceStatus чтобы установить текущее состояние службы в SERVICE_START_PENDING, указывая при этом dwCheckPoint как 0 и dwWaitHint как 2000.
  6. Функция службы получает управление и одним из первых действий вызывает функцию RegisterServiceCtrlHandler, чтобы зарегистрировать функцию-обработчик команд для этой службы.
  7. Служба выполняет инициализацию, например, регистрирует фабрики классов для своих COM-объектов или открывает сокеты, после чего вызывает SetServiceStatus, указывая состояние SERVICE_RUNNING. С этого момента служба считается стартовавшей.

Существенное значение с точки зрения программы управления службами имеет то, что функция StartService лишь инициирует запуск службы, она не дожидается, пока служба завершит свою инициализацию. Следует иметь это в виду, если приложение должно сразу после запуска начать каким-либо образом взаимодействовать со службой. Например, если служба является Web-сервером, то попытка установить TCP/IP соединение с ней сразу после вызова StartService скорее всего закончится неудачей, поскольку служба еще не успела создать сокет, принимающий соединения от клиентов.

В таких случаях следует дождаться завершения инициализации службы, вызывая QueryServiceStatus в цикле до тех пор, пока состояние службы не изменится на SERVICE_RUNNING или SERVICE_STOPPED (последнее возможно, если служба по каким-либо причинам не сможет произвести инициализацию и завершится с ошибкой). Приводимый далее пример как раз иллюстрирует этот механизм.

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

Пример: запуск службы

Функция DemoStartService, исходный код которой приведен в листинге 2, реализует команду start в демонстрационном приложении svcctrl.exe. Чтобы увидеть ее в действии, следует запустить программу следующим образом:

svcctrl start <имя службы>

//-----------------------------------------------------------------------
// DemoServiceStart
//
//  Запускает указанную службу.
//
//  Параметры:
//    hSCM           - handle SCM
//    pszServiceName - имя службы
//
//  Возвращает:
//      ноль - при успешном завершении, отличное от нуля значение - в
//      противном случае.
//
int DemoServiceStart(
    IN SC_HANDLE hSCM, 
    IN PCTSTR pszServiceName
    )
{
    _ASSERTE(hSCM != NULL);
    _ASSERTE(pszServiceName != NULL);
    
    SC_HANDLE hService;
    SERVICE_STATUS Status;

    // открываем handle службы с правами SERVICE_START и
    // SERVICE_QUERY_STATUS
    hService = OpenService(hSCM, pszServiceName, 
                           SERVICE_START|SERVICE_QUERY_STATUS);
    if (hService == NULL)
        return PrintError(_T("OpenService failed"), GetLastError());

    // запускаем службу
    if (!StartService(hService, 0, NULL))
    {
        PrintError(_T("StartService failed"), GetLastError());
        CloseServiceHandle(hService);
        return 1;
    }

    // дожидаемся завершения инициализации службы
    for (;;)
    {
        if (!QueryServiceStatus(hService, &Status))
        {
            PrintError(_T("QueryServiceStatus failed"), GetLastError());
            _VERIFY(CloseServiceHandle(hService));
            return 1;
        }

        if (Status.dwCurrentState != SERVICE_START_PENDING)
            break;

        DWORD dwWait = Status.dwWaitHint;
        if (dwWait == 0)
            dwWait = 1000;

        Sleep(dwWait);
    }
    
    CloseServiceHandle(hService);

    if (Status.dwCurrentState != SERVICE_RUNNING)
    {
        PrintError(_T("Unable to start service"), 
                   Status.dwWin32ExitCode);
        return 1;
    }

    return 0;
}

Использование команд управления

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

BOOL ControlService(
    IN SC_HANDLE hService,              // описатель службы
    IN DWORD dwControl,                 // код управления
    OUT LPSERVICE_STATUS pServiceStatus // состояние службы
    );

Первый параметр этой функции идентифицирует службу, которой посылается команда управления, второй - задает код посылаемой команды, а последний параметр указывает на структуру, в которую функция заносит информацию о текущем состоянии службы, избавляя тем самым программиста от лишнего вызова QueryServiceStatus. Ниже перечислены команды управления, которые могут быть посланы с помощью этой функции:

Чтобы посылаемая команда достигла службы, необходимо выполнение двух условий. Во-первых, handle службы должен быть открыт с подходящим правом доступа. Во-вторых, служба должна сообщить SCM, что она поддерживает этот вид команд. Служба сообщает, какие команды она поддерживает, в поле dwControlsAccepted структуры SERVICE_STATUS. В таблице 2 сведены вместе команды управления, соответствующие им флаги, возвращаемые в поле dwControlsAccepted, и необходимые для выполнения команды права доступа.

Команда SERVICE_CONTROL_... Флаг SERVICE_ACCEPT_... Право доступа SERVICE_...
STOP STOP STOP
PAUSE PAUSE_CONTINUE PAUSE_CONTINUE
CONTINUE PAUSE_CONTINUE PAUSE_CONTINUE
INTERROGATE не определено* INTERROGATE
PARAMCHANGE** PARAMCHANGE PAUSE_CONTINUE
NETBINDADD** NETBINDCHANGE PAUSE_CONTINUE
NETBINDREMOVE** NETBINDCHANGE PAUSE_CONTINUE
NETBINDENABLE** NETBINDCHANGE PAUSE_CONTINUE
NETBINDDISABLE** NETBINDCHANGE PAUSE_CONTINUE
SHUTDOWN SHUTDOWN не определено***
HARDWAREPROFILECHANGE** HARDWAREPROFILECHANGE не определено***
POWEREVENT** POWEREVENT не определено***
SESSIONCHANGE**** SESSIONCHANGE не определено***
Пользовательские команды не определено USER_DEFINED_CONTROL
Таблица 2. Соответствие команд управления, флагов поддерживаемых команд и прав доступа
* Команда SERVICE_CONTROL_INTERROGATE обязательно поддерживается всеми службами.
** Команда доступна начиная с Windows 2000.
*** Команда посылается операционной системой, она не может быть послана с помощью ControlService.
**** Команда доступна начиная с Windows XP.

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


Рис. 3. Обработка команд остановки, приостановки и возобновления

  1. Приложение вызывает ControlService и посылает команду управления SCM, который, в свою очередь, пересылает команду соответствующей службе. Функция StartServiceCtrlDispatcher, ответственная за обработку команд управления, принимает команду и вызывает функцию-обработчик команд службы, которую служба зарегистрировала ранее посредством RegisterServiceCtrlHandler.
  2. Функция-обработчик команд, фигурирующая в MSDN Library под названием Handler, информирует службу о поступившей команде, например, устанавливает событие или посылает сообщение в поток службы. Непосредственно перед этим функция может вызвать SetServiceStatus, чтобы перевести службу в промежуточное состояние (например, если получена команда SERVICE_CONTROL_PAUSE, служба переводится в состояние SERVICE_PAUSE_PENDING). Так поступают не все службы. Если обработка команды занимает непродолжительное время, то служба может и не сообщать о промежуточном состоянии.
  3. Обработчик команд возвращает управление, и StartServiceCtrlDispatcher сообщает менеджеру системных служб, что команда принята к исполнению. В результате ControlService возвращает управление прикладной программе.
  4. Тем временем служба выполняет поступившую команду, и, когда выполнение завершено, вызывает SetServiceStatus, чтобы сообщить о своем состоянии.
  5. Для большинства команд обработка заканчивается на предыдущем шаге. Однако если поступила команда остановки, и служба была единственной в процессе, установка состояния службы в SERVICE_STOPPED приводит к тому, что StartServiceCtrlDispatcher возвращает управление функции WinMain, которая обычно тут же завершается, приводя к завершению процесса службы.

Мы рассмотрели наиболее общий вариант обработки команд службой. Некоторые службы могут поступать проще и поместить всю обработку команды в функцию Handler, в которой сразу же сообщать о новом состоянии. Тем не менее, в программе управления службами мы не можем рассчитывать на то, что служба всегда ведет себя таким образом. Мы должны быть готовы к тому, что с момента возврата из ControlService до момента, когда служба примет конечное состояние, может пройти некоторое время.

Если нужно дождаться момента, когда служба закончит обработку команды, придется вызывать в цикле функцию QueryServiceStatus до тех пор, пока служба не примет нужное нам состояние. Поскольку многие команды обрабатываются сходным образом, я решил написать универсальную функцию под названием ControlServiceAndWait (листинг 3), которая посылает команду управления службе и дожидается, пока служба не примет указанное состояние.

//-----------------------------------------------------------------------
// ControlServiceAndWait
//
//  Посылает команду управления службы и ждет, пока служба не примет
//  указанное cостояние.
//
//  Параметры:
//      hService  - handle службы
//      dwControl - код команды управления
//      dwState   - состояние, которого следует ждать
//      dwTimeout - таймаут в миллисекундах
//
//  Возвращает:
//      TRUE - при успешном завершении, FALSE - в случае ошибки.
//
BOOL ControlServiceAndWait(
    IN SC_HANDLE hService,
    IN DWORD dwControl,
    IN DWORD dwState,
    IN DWORD dwTimeout
    )
{
    _ASSERTE(hService != NULL);

    SERVICE_STATUS Status;
    DWORD dwStart;
    DWORD dwCheckPoint = (DWORD)-1;
    DWORD dwWait;

    // посылаем команду управления
    if (!ControlService(hService, dwControl, &Status))
        return FALSE;

    // отмечаем время начала операции
    dwStart = GetTickCount();

    // ожидаем пока служба примет требуемое состояние
    while (Status.dwCurrentState != dwState &&
           Status.dwCurrentState != SERVICE_STOPPED)
    {
        // проверяем, не истек ли таймаут
        if (dwTimeout != INFINITE)
        {
            if (GetTickCount() - dwStart >= dwTimeout)
                return SetLastError(ERROR_TIMEOUT), FALSE;
        }

        // определяем величину задержки до следующей проверки
        // состояния
        if (dwCheckPoint != Status.dwCheckPoint)
        {
            dwCheckPoint = Status.dwCheckPoint;
            dwWait = Status.dwWaitHint;
        }
        else
        {
            dwWait = 1000;
        }

        // пауза
        Sleep(dwWait);

        // получаем состояние службы
        if (!QueryServiceStatus(hService, &Status))
            return FALSE;
    }

    if (Status.dwCurrentState == SERVICE_STOPPED &&
        Status.dwWin32ExitCode != ERROR_SUCCESS)
    {
        SetLastError(Status.dwWin32ExitCode);
        return FALSE;
    }
	
    return TRUE;
}

Функция первым делом вызывает ControlService, чтобы послать команду управления службе. Кроме того, ControlService возвращает текущее состояние службы, так что если служба сразу выполнила команду и перешла в нужное состояние, ControlServiceAndWait сразу же завершается, не делая никаких дополнительных вызовов.

В цикле ожидания проверяется, не истек ли срок, отведенный на выполнение команды, а также определяется, через какой интервал времени следует снова проверить состояние. Для этого используются поля dwCheckPoint и dwWaitHint структуры SERVICE_STATUS. При выполнении длительной операции, служба периодически увеличивает значение dwCheckPoint, занося при этом в поле dwWaitHint количество миллисекунд до следующей контрольной точки. Если служба не использует эти поля, или если служба не успела обновить свое состояние за время ожидания, делается пауза в одну секунду.

Условие выхода из цикла включает в себя не только требуемое состояние, но и состояние SERVICE_STOPPED. Это сделано для того, чтобы учесть вариант остановки службы из-за ошибки. В таком случае функция анализирует поле dwWin32ExitCode, чтобы сообщить код ошибки вызывающей программе.

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

В конфигурации службы нет явной информации о зависимых от нее службах. К счастью Win32 API предоставляет функцию, которая одним вызовом возвращает сразу все зависимые службы. Эта функция называется EnumDependentServices:

BOOL EnumDependentServices(
    SC_HANDLE hService,              // handle службы
    DWORD dwServiceState,            // состояние служб
    LPENUM_SERVICE_STATUS pServices, // приемный буфер
    DWORD cbBufSize,                 // размер приемного буфера
    PDWORD pcbBytesNeeded,           // требуемый размер буфера
    PDWORD pServicesReturned         // количество служб
    );

Параметр dwServiceState указывает интересующее нас состояние служб. Если его указать как SERVICE_ACTIVE, то функция перечислит только те зависимые службы, которые выполняются в данный момент (или, более точно, состояние которых отлично от SERVICE_STOPPED). Другими допустимыми значениями для этого параметра являются SERVICE_INACTIVE и SERVICE_STATE_ALL. В первом случае функция перечислит только остановленные службы, а во втором - все зависимые службы, независимо от их состояния.

При успешном завершении, функция заполняет буфер, указываемый параметром pServices, структурами ENUM_SERVICE_STATUS, занося при этом в переменную, адрес которой задан в качестве pServicesReturned, количество записанных структур. Если размер буфера недостаточен, чтобы принять информацию обо всех службах, то функция завершается с ошибкой ERROR_MORE_DATA, при этом в переменную, указываемую параметром pcbBytesNeeded, заносится требуемый размер буфера.

Структура ENUM_SERVICE_STATUS определена в заголовочном файле winsvc.h следующим образом:

typedef struct _ENUM_SERVICE_STATUS { 
    PTSTR          lpServiceName;     // имя службы
    PTSTR          lpDisplayName;     // название службы
    SERVICE_STATUS ServiceStatus;     // текущее состояния службы
} ENUM_SERVICE_STATUS, * LPENUM_SERVICE_STATUS;

Как видно из определения, структура содержит внутреннее имя службы и ее название, а также текущее состояние службы, которое описывается уже известной нам структурой SERVICE_STATUS.

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

Пример: остановка службы

Теперь мы можем расширить программу svcctrl.exe функциями остановки, приостановки и возобновления. В частности, функция остановки может выглядеть, как показано в листинге 4.

//-----------------------------------------------------------------------
// DemoServiceStop
//
//  Останавливает указанную службу.
//
//  Параметры:
//      hSCM           - handle SCM
//      pszServiceName - имя службы
//
//  Возвращает:
//      ноль - при успешном завершении, отличное от нуля значение - в
//      противном случае.
//
int DemoServiceStop(
    IN SC_HANDLE hSCM, 
    IN PCTSTR pszServiceName
    )
{
    _ASSERTE(hSCM != NULL);
    _ASSERTE(pszServiceName != NULL);

    SC_HANDLE hService;

    // открываем handle службы с правами SERVICE_STOP и 
    // SERVICE_QUERY_STATUS
    hService = OpenService(hSCM, pszServiceName, 
                           SERVICE_STOP|SERVICE_QUERY_STATUS);
    if (hService == NULL)
        return PrintError(_T("OpenService failed"), GetLastError());

    // посылаем службе команду SERVICE_CONTROL_STOP и дожидаемся
    // пока она примет состояние SERVICE_STOPPED
    if (!ControlServiceAndWait(hService, SERVICE_CONTROL_STOP, 
                               SERVICE_STOPPED, 120000))
    {
        PrintError(_T("ControlServiceAndWait failed"), GetLastError());

        CloseServiceHandle(hService);
        return 1;
    }

    CloseServiceHandle(hService);
    return 0;
}

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

Если мы теперь скомбинируем эту функцию и функцию EnumDependentServices, то получим полноценную функцию остановки службы, учитывающую зависимости. Текст этой функции приведен в листинге 5.

//-----------------------------------------------------------------------
// DemoServiceStopWithDependents
//
//  Останавливает указанную службу и все зависимые от нее службы.
//
//  Параметры:
//      hSCM           - handle SCM
//      pszServiceName - имя службы
//
//  Возвращает:
//      ноль - при успешном завершении, отличное от нуля значение - в
//      противном случае.
//
int DemoServiceStopWithDependents(
    IN SC_HANDLE hSCM, 
    IN PCTSTR pszServiceName
    )
{
    _ASSERTE(hSCM != NULL);
    _ASSERTE(pszServiceName != NULL);

    SC_HANDLE hService;
    LPENUM_SERVICE_STATUS pStatus;
    DWORD cbNeeded, i;
    DWORD cServices = 0;

    // открываем handle службы с правом доступа 
    // SERVICE_ENUMERATE_DEPENDENTS
    hService = OpenService(hSCM, pszServiceName, 
                           SERVICE_ENUMERATE_DEPENDENTS);
    if (hService == NULL)
        return PrintError(_T("OpenService failed"), GetLastError());

    // определяем требуемый размер выходного буфера
    if (!EnumDependentServices(hService, SERVICE_ACTIVE, NULL, 0,
                               &cbNeeded, &cServices))
    {
        if (GetLastError() != ERROR_MORE_DATA)
        {
            PrintError(_T("EnumDependentServices failed"),
                       GetLastError());
            CloseServiceHandle(hService);
            return 1;
        }
    }

    if (cbNeeded > 0)
    {
        // выделяем память для выходного буфера
        pStatus = (LPENUM_SERVICE_STATUS)LocalAlloc(LMEM_FIXED, cbNeeded);
        if (pStatus == NULL)
        {
            _tprintf(_T("Not enough memory\n"));
            CloseServiceHandle(hService);
            return 1;
        }

        // получаем список зависимых служб
        if (!EnumDependentServices(hService, SERVICE_ACTIVE, pStatus,
                                   cbNeeded, &cbNeeded, &cServices))
        {
            PrintError(_T("EnumDependentServices failed"), 
                       GetLastError());

            LocalFree((HLOCAL)pStatus);
            CloseServiceHandle(hService);
            return 1;
        }
    }

    // останавливаем все зависимые службы
    for (i = 0; i < cServices; i++)
    {
        _tprintf(_T("Stopping %s...\n"), pStatus[i].lpDisplayName);

        if (DemoServiceStop(hSCM, pStatus[i].lpServiceName))
            break;
    }

    LocalFree((HLOCAL)pStatus);
    CloseServiceHandle(hService);

    if (i < cServices)
        return 1;
    
    // наконец, останавливаем указанную службу
    _tprintf(_T("Stopping %s...\n"), pszServiceName);
    return DemoServiceStop(hSCM, pszServiceName);
}

Эта функция вызывается, если svcctrl.exe запускается с параметром командной строки stop:

svcctrl stop <имя службы>

Демонстрационная программа svcctrl.exe содержит также функции для приостановки и возобновления служб. Эти функции практически ничем не отличаются от функции DemoServiceStop, поэтому мы не будем здесь на них останавливаться.

Блокировка базы данных служб

Когда база данных SCM заблокирована, ни одна служба не может быть запущена - любая попытка вызвать StartService завершится с ошибкой ERROR_SERVICE_DATABASE_LOCKED. Зачем это может понадобиться? Например, ваша программа может изменять конфигурацию нескольких взаимосвязанных служб, и вы не хотите, чтобы какая-либо из служб была запущена в процессе изменения конфигурации. Особенно это актуально, когда запуск службы не находится под вашим контролем; например, если служба является COM-сервером, то она может быть запущена в результате запроса на активацию, поступившего по сети. Другими словами, блокировка базы данных SCM должна использоваться всякий раз, когда существует вероятность запуска службы в процессе изменения конфигурации.

Для блокировки БД SCM служит функция LockServiceDatabase:

SC_LOCK LockServiceDatabase(
    SC_HANDLE hSCM   // handle SCM
    );

Единственным параметром этой функции является handle SCM, который должен быть открыт с правом доступа SC_MANAGER_LOCK. При успешном завершении функция возвращает значение, идентифицирующее блокировку - оно понадобится позже, чтобы снять блокировку с помощью функции UnlockServiceDatabase:

BOOL UnlockServiceDatabase(
    SC_LOCK ScLock   // идентификатор блокировки
    );

В каждый момент времени БД SCM может блокировать только один процесс, поэтому следует блокировать ее на как можно более короткий промежуток времени, чтобы не вызывать конфликтов с другими приложениями. Если процесс, владеющий блокировкой, завершается, не освободив ее, то менеджер системных служб снимает блокировку автоматически.

Чтобы узнать текущее состояние блокировки, можно воспользоваться функцией QueryServiceLockStatus:

BOOL QueryServiceLockStatus(
    SC_HANDLE hSCM,                          // handle SCM
    LPQUERY_SERVICE_LOCK_STATUS pLockStatus, // приемный буфер
    DWORD cbBufSize,                         // размер буфера в байтах
    PDWORD pcbBytesNeeded                    // требуемый размер буфера
    );

Функция заносит информацию о блокировке в буфер, формат которого описывается структурой QUERY_SERVICE_LOCK_STATUS:

typedef struct _QUERY_SERVICE_LOCK_STATUS {
    DWORD  fIsLocked;           // признак наличия блокировки
    PTSTR  lpLockOwner;         // владелец блокировки 
    DWORD  dwLockDuration;      // длительность владения блокировкой
} QUERY_SERVICE_LOCK_STATUS, *LPQUERY_SERVICE_LOCK_STATUS;

Если база данных SCM не заблокирована, то поле fIsLocked будет установлено в FALSE, а содержимое остальных полей не будет иметь значения. В противном случае, в поле lpLockOwner заносится имя пользователя, владеющего блокировкой, а в поле dwLockDuration - время владения блокировкой в секундах. Когда я впервые познакомился с функцией QueryServiceLockStatus, я был несколько удивлен составом возвращаемой ею информации. Я ожидал увидеть в структуре QUERY_SERVICE_LOCK_STATUS идентификатор процесса, владеющего блокировкой, поскольку блокировка связана с захватившим ее процессом, а не пользователем. Eсли какой-либо процесс захватит блокировку и забудет о ней, продолжая работать, единственный способ найти нарушителя - это поочередно завершать все запущенные процессы. Если бы функция возвращала идентификатор процесса-владельца, задача была бы намного проще.

В заключение хочу еще раз отметить, что блокировка базы данных SCM оказывает влияние лишь на способность запуска новых служб с помощью функции StartService, она никак не влияет на остальные функции управления службами.

Установка и удаление служб

До этого момента мы не задавались вопросом, каким образом службы появляются на компьютере пользователя. Большинство служб устанавливается вместе с Windows NT, однако для независимых разработчиков существует возможность установки дополнительных служб. В этой части статьи мы рассмотрим механизмы установки и удаления системных служб в Windows NT.

Установка службы

Для установки служб Win32 API предоставляет функцию CreateService, которая имеет довольно много параметров, но подавляющее большинство из них уже обсуждалось выше:

SC_HANDLE CreateService(
    IN SC_HANDLE hSCM,              // handle SCM
    IN PCTSTR    pServiceName,      // имя службы
    IN PCTSTR    pDisplayName,      // название службы
    IN DWORD     dwDesiredAccess,   // доступ к объекту службы
    IN DWORD     dwServiceType,     // тип службы
    IN DWORD     dwStartType,       // способ запуска службы
    IN DWORD     dwErrorControl,    // реакция на ошибки при загрузке
    IN PCTSTR    pBinaryPathName,   // исполняемый файл
    IN PCTSTR    pLoadOrderGroup,   // группа загрузки
    IN PDWORD    pdwTagId,          // тег (NULL для служб Win32)
    IN PCTSTR    pDependencies,     // зависимости
    IN PCTSTR    pServiceStartName, // учетная запись службы
    IN PCTSTR    pPassword          // пароль учетной записи
    );

При успешном завершении функция возвращает handle службы. В случае ошибки функция возвращает NULL, и код ошибки может быть получен с помощью GetLastError.

Если посмотреть на описание функции ChangeServiceConfig, описанной в первой части статьи, то можно обнаружить, что большая часть параметров этих функций совпадает. Все совпадающие параметры имеют тот же смысл - в функции CreateService они задают начальную конфигурацию устанавливаемой службы. Параметры, которые не пересекаются с определением ChangeServiceConfig, перечислены ниже.

hSCM

Этот параметр - handle SCM, который, напомню, возвращается функцией OpenSCManager. Фактически, этот параметр идентифицирует компьютер, на котором устанавливается служба. Для успешного выполнения CreateService, handle SCM должен быть открыт с правом доступа SC_MANAGER_CREATE_SERVICE, которое имеют только администраторы компьютера.

pServiceName

Задает имя службы. Это единственное место, где можно задать имя службы. После того, как служба установлена, единственный способ изменить ее внутреннее имя - удалить ее и установить заново.

dwDesiredAccess

Этот параметр имеет тот же смысл, что и аналогичный параметр OpenService - он задает набор прав доступа к объекту службы. Поскольку функцию CreateService могут вызывать только администраторы, они могут указать в этом параметре любой набор прав доступа, не опасаясь отказа. Тем не менее, хороший стиль работы с объектами Windows NT состоит в том, чтобы запрашивать только те права доступа, которые действительно необходимы.

Одно из мест, где может вызываться функция CreateService - это программа-инсталлятор. Также очень часто разработчики включают код для установки службы в саму службу. В этом случае, чтобы произвести установку, необходимо запустить исполняемый файл службы с определенным параметром командной строки. Например, ATL Wizard поступает именно так, когда генерирует код для проекта службы.

Тестовая служба voidsvc, которая упоминалась в первой части статьи, не является исключением из этого правила. Если ее запустить с параметром командной строки "/install",

voidsvc /install:<имя службы>

она установит себя в качестве службы. voidsvc позволяет зарегистрировать себя под произвольным именем, поэтому параметр командной строки "/install" имеет дополнительный аргумент, указывающий имя службы. Это нетипичная ситуация: подавляющее большинство служб имеют фиксированное имя.

Обнаружив параметр "/install" в командной строке, voidsvc вызывает функцию Install (см. листинг 6).

//-----------------------------------------------------------------------
// Install
//
//  Устанавливает службу.
//
//  Параметры:
//      pszName - имя службы
//
//  Возвращает:
//      TRUE - при успешном завершении, FALSE - в случае ошибки.
//
BOOL Install(
    IN PCTSTR pszName
    )
{
    SC_HANDLE hSCM = OpenSCManager(NULL, NULL, 
                                   SC_MANAGER_CREATE_SERVICE);
    if (hSCM == NULL)
        return FALSE;

    TCHAR szPathName[MAX_PATH];
    GetModuleFileName(NULL, szPathName + 1, 
                      countof(szPathName) - 3 - lstrlen(pszName));

    szPathName[0] = _T('\"');
    lstrcat(szPathName, _T("\" "));
    lstrcat(szPathName, pszName);

    SC_HANDLE hService = CreateService(hSCM, pszName, 
            pszName, 0, SERVICE_WIN32_OWN_PROCESS, 
            SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, 
            szPathName, NULL, NULL, NULL, NULL, NULL);

    if (hService != NULL)
        CloseServiceHandle(hService);

    CloseServiceHandle(hSCM);

    return (hService != NULL);
}

Обратите внимание на вызов GetModuleFileName, с помощью которого voidsvc определяет свое текущее местоположение, чтобы сформировать командную строку, передаваемую в качестве параметра CreateService. Это стандартный прием, используемый в функциях самоустановки служб.

Удаление службы

Иногда в голову пользователя приходят совершенно сумасшедшие идеи. Например, его может осенить совершенно дикая идея - удалить со своего компьютера созданный вами шедевр. В этом случае, программа удаления должна удалить и службу, если таковая есть в составе продукта. Для удаления службы предназначена функция DeleteService:

BOOL DeleteService(
    SC_HANDLE hService   // handle службы
    );

Единственным параметром этой функции является handle удаляемой службы, который должен быть открыт с правом доступа DELETE.

Важно заметить, что DeleteService лишь помечает службу для удаления. Фактически служба будет удалена, когда будет закрыт последний указывающий на нее handle. Один открытый handle, очевидно, удерживает программа, вызывающая DeleteService. Другой handle удерживает SCM до тех пор, пока служба не будет остановлена. Таким образом, чтобы окончательно удалить службу, необходимо убедиться, что она остановлена. Впрочем, мы уже умеем останавливать службы, так что это не должно быть проблемой.

Листинг 7 содержит исходный код функции Uninstall. Эту функцию программа voidsvcвызывает при запуске с параметром командной строки "/uninstall".

//-----------------------------------------------------------------------
// Uninstall
//
//  Удаляет службу.
//
//  Параметры:
//      pszName - имя службы
//
//  Возвращает:
//      TRUE - при успешном завершении, FALSE - в случае ошибки.
//
BOOL Uninstall(
    IN PCTSTR pszName
    )
{
    SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
    if (hSCM == NULL)
        return FALSE;

    SC_HANDLE hService = OpenService(hSCM, pszName, SERVICE_STOP|DELETE);
    if (hService == NULL)
    {
        CloseServiceHandle(hSCM);
        return FALSE;
    }

    SERVICE_STATUS Status;
    ControlService(hService, SERVICE_CONTROL_STOP, &Status);

    Sleep(200);

    BOOL bOk = DeleteService(hService);
    
    CloseServiceHandle(hService);
    CloseServiceHandle(hSCM);
    
    return bOk;
}

Функция Uninstall немного нарушает правила в целях упрощения. Во-первых, она вызывает ControlService вне зависимости от текущего состояния службы, полагаясь на то, что функция завершится с ошибкой, если служба в данный момент не запущена. По этой причине возвращаемое значение ControlService не анализируется. Во-вторых, она вызывает Sleep(200), вместо того, чтобы честно дождаться завершения службы, считая, что 200 миллисекунд - более чем достаточно для завершения службы, которая ничего не делает. Если хотите делать все по правилам - вы знаете как.

Перечисление установленных служб

Иногда может встретиться необходимость перечислить все службы, установленные в системе. Например, это понадобится, если вы захотите написать свою программу управления службами, такую, как демонстрационная программа svcadmin, прилагающаяся к этой статье. В это случае придется воспользоваться функцией EnumServicesStatus:

BOOL EnumServicesStatus(
    IN SC_HANDLE hSCM,                   // handle SCM
    IN DWORD dwServiceType,              // тип перечисляемых служб
    IN DWORD dwServiceState,             // состояние перечисляемых служб
    OUT LPENUM_SERVICE_STATUS pServices, // приемный буфер
    IN DWORD cbBufSize,                  // размер буфера в байтах
    OUT PDWORD pcbBytesNeeded,           // требуемый размер буфера
    OUT PDWORD pServicesReturned,        // количество служб
    IN OUT PDWORD pResumeHandle          // идентифицирует перечисление
    );

Ниже перечислены параметры этой функции.

hSCM

Идентифицирует SCM, службы которого требуется перечислить. Этот handle должен быть открыт с правом доступа SC_MANAGER_ENUMERATE_SERVICES.

dwServiceType

Задает тип перечисляемых служб. В качестве этого параметра может быть указан флаг SERVICE_DRIVER, либо флаг SERVICE_WIN32, либо оба. В первом случае будут перечислены только службы ядра, во втором - только службы Win32, а в третьем - и те, и другие.

dwServiceState

Этот параметр позволяет отфильровать службы по состоянию. Если в качестве этого параметра указано значение SERVICE_INACTIVE, будут перечислены только службы в состоянии SERVICE_STOPPED, то есть остановленные. Если же этот параметр указать как SERVICE_ACTIVE, то будут перечислены все запущенные службы, состояние которых отлично от SERVICE_STOPPED. Наконец, если значением этого параметра является SERVICE_STATE_ALL, будут перечислены все службы.

pServices

Указывает на буфер, принимающий информацию об установленных службах. Формат буфера описывается структурой ENUM_SERVICE_STATUS, с которой мы уже знакомы.

cbBufSize

Задает размер приемного буфера в байтах.

pcbBytesNeeded

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

pServicesReturned

Указывает на переменную, в которую функция заносит количество служб, информация о которых поместилась в указанный буфер. Этот параметр необходим, так как количество служб нельзя рассчитать, как cbBufSize / sizeof(ENUM_SERVICE_STATUS). Дело в том, что структура ENUM_SERVICE_STATUS содержит указатели на строки переменной длины, которые также помещаются в выходной буфер.

pResumeHandle

Данная функция позволяет считывать информацию об установленных службах по частям. Через этот параметр возвращается информация, с помощью которой можно продолжить чтение списка. Если функция возвращает FALSE и код ошибки, возвращаемый GetLastError, равен ERROR_MORE_DATA, значит, не вся информация о службах поместилась в выходной буфер, и следует произвести повторное чтение.

Есть две стратегии использования этой функции. Первая состоит в том, чтобы заранее определить размер буфера необходимый для вмещения информации о всех службах. Для этого нужно вызвать EnumServicesStatus, передав NULL в параметр pServices и 0 в cbBufSize. В этом случае функция вернет необходимый размер буфера в переменную, адрес которой необходимо указать в параметре pcbBytesNeeded. После этого нужно выделить буфер соответствующего размера и вызвать функцию еще раз.

Вторая стратегия заключается в том, чтобы заранее выделить буфер, гарантированно вмещающий информацию об одной или более службах. Поскольку длина внутреннего имени и названия службы ограничены 256 символами плюс один символ для завершающего нулевого символа, то минимальный размер буфера можно рассчитать как sizeof(ENUM_SERVICE_STATUS) + (256 + 1) * 2 * sizeof(TCHAR). Теперь, если вызвать функцию EnumServicesStatus, она поместит в буфер порцию информации о состоянии служб, а в переменную по адресу pResumeHandle запишет значение, идентифицирующее текущую позицию в перечислении. При первом вызове EnumServicesStatus нужно инициализировать эту переменную нулем. При последующих вызовах функция будет использовать ее значение, чтобы определить, информация о каких службах уже выдана и возвращать очередную порцию данных.

Чтобы окончательно разобраться в этом вопросе, давайте рассмотрим второй подход на примере. В листинге 8 приведен текст программы, которая перечисляет все установленные службы (вы также можете найти его под именем svclist в архиве с исходным кодом; скомпилированная версия программы доступна в архиве с демонстрационными программами).

int _tmain(
    int argc,
    _TCHAR * argv[]
    )
{
    SC_HANDLE hSCM;
    int nRet = 0;

    // соединяемся со SCM на локальной машине
    hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ENUMERATE_SERVICE);
    if (hSCM == NULL)
        return PrintError(_T("OpenSCManager failed"), GetLastError());

    BYTE bBuffer[2048];
    DWORD dwResumeHandle = 0;
    DWORD cbNeeded;
    DWORD cServices;
    BOOL bContinue;
    DWORD dwError;
    LPENUM_SERVICE_STATUS pStatus;

    do
    {
        bContinue = FALSE;
        pStatus = (LPENUM_SERVICE_STATUS)bBuffer;

        // извлекаем очередную порцию
        if (!EnumServicesStatus(hSCM, SERVICE_WIN32, SERVICE_STATE_ALL,
                                pStatus, sizeof(bBuffer), &cbNeeded,
                                &cServices, &dwResumeHandle))
        {
            dwError = GetLastError();

            if (dwError != ERROR_MORE_DATA)
            {
                nRet = PrintError(_T("EnumServicesStatus failed"), 
                                  dwError);
                break;
            }

            bContinue = TRUE;
        }

        // отображаем информацию о службах в этой порции
        for (DWORD i = 0; i < cServices; i++)
        {
            PCTSTR pszStatus;
            switch (pStatus->ServiceStatus.dwCurrentState)
            {
                case SERVICE_STOPPED:
                    pszStatus = _T("Stopped");
                    break;
                case SERVICE_RUNNING:
                    pszStatus = _T("Running");
                    break;
                case SERVICE_PAUSED:
                    pszStatus = _T("Paused");
                    break;
                case SERVICE_STOP_PENDING:
                    pszStatus = _T("Stop Pending");
                    break;
                case SERVICE_START_PENDING:
                    pszStatus = _T("Start Pending");
                    break;
                case SERVICE_PAUSE_PENDING:
                    pszStatus = _T("Pause Pending");
                    break;
                case SERVICE_CONTINUE_PENDING:
                    pszStatus = _T("Continue Pending");
                    break;
                default:
                    pszStatus = _T("Unknown");
                    break;
            }

            if (lstrlen(pStatus->lpDisplayName) > 60)
                pStatus->lpDisplayName[60] = 0;

            _tprintf(_T("%-60s %s\n"), pStatus->lpDisplayName,
                     pszStatus);

            // переходим к следующему элементу
            pStatus++;
        }
    }
    while (bContinue);

    CloseServiceHandle(hSCM);
    return nRet;
}

В Windows NT 4 EnumServicesStatus была единственной функцией, которая позволяла перечислить установленные службы. Начиная с Windows 2000, это можно сделать еще и с помощью функции EnumServicesStatusEx, которая является расширенным вариантом предыдущей функции.

BOOL EnumServicesStatusEx(
    IN SC_HANDLE hSCManager,      // handle SCM
    IN SC_ENUM_TYPE InfoLevel,    // вид информации
    IN DWORD dwServiceType,       // тип перечисляемых служб
    IN DWORD dwServiceState,      // состояние перечисляемых служб
    OUT PBYTE pServices,          // приемный буфер
    IN DWORD cbBufSize,           // размер приемного буфера
    OUT PDWORD pcbBytesNeeded,    // требуемый размер буфера
    OUT PDWORD pServicesReturned, // количество служб
    IN OUT PDWORD pResumeHandle,  // идентифицирует перечисление
    IN PCTSTR pszGroupName        // имя группы загрузки
    );

Эта функция отличается от EnumServicesStatus в двух аспектах. Во-первых, она позволяет перечислить службы, принадлежащие конкретной группе загрузки, для чего служит параметр pszGroupName. Если этот параметр указан как NULL, то, как и раньше, будут перечислены все службы. Если указана пустая строка, то будут перечислены службы, которые не принадлежат никакой из групп загрузки.

Во-вторых, функция позволяет управлять видом возвращаемой информации посредством параметра InfoLevel. Хотя в настоящее время определено только одно значение для этого параметра - SC_ENUM_PROCESS_INFO - его наличие позволит Microsoft избежать добавления новой функции, когда в следующий раз потребуется расширить информацию о службе. Сегодня же, формат выходного буфера описывается структурой ENUM_SERVICE_STATUS_PROCESS:

typedef struct _ENUM_SERVICE_STATUS_PROCESS {
    LPTSTR                  lpServiceName;        // имя службы
    LPTSTR                  lpDisplayName;        // название службы
    SERVICE_STATUS_PROCESS  ServiceStatusProcess; // состояние службы
} ENUM_SERVICE_STATUS_PROCESS, * LPENUM_SERVICE_STATUS_PROCESS;

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

Заключение

Вот и все, о чем я хотел вам рассказать про управление системными службами в Windows NT. Согласитесь, это не было очень сложно.

Если вы оглянетесь назад, то увидите, что мы затронули практически все функции Win32 API, так или иначе связанные с управлением службами. Для каждой функции вы можете найти работающий пример, а для некоторых функций - даже несколько, что позволит вам использовать эту статью в качестве практического руководства. В то же время, мы подробно изучили все конфигурационные параметры служб, а также рассмотрели по шагам процесс запуска и остановки службы. Эти знания помогут вам принять правильные решения при проектировании ваших служб.

Наконец, утилита svcadmin, которая изначально задумывалась как демонстрационное приложение, но быстро переросла в полноценную программу управления службами, просто поможет вам в повседневной работе, открывая доступ к тем параметрам конфигурации, от которых решили оградить нас разработчики MMC snap-in "Службы".

Управление системными службами - важная, но все же часть в общей архитектуре системных служб Windows NT. Если вы серьезно занимаетесь разработкой системных служб, то вам обязательно надо изучить использование журнала событий, счетчиков производительности, и, конечно, систему безопасности Windows NT. Статьи об этом и о многом другом обязательно появятся в нашем журнале. Stay tuned.

Ссылки

  1. Jeffrey Richter, Jason D. Clark, Programming Server-Side Applications for Microsoft Windows 2000. Microsoft Press, 2000. (Имеется русский перевод: Джеффри Рихтер, Джейсон Кларк, Программирование серверных приложений для Windows 2000. Издательства "Русская Редакция", "Питер", 2001.
  2. Kevin Miller, Professional NT Services. Wrox Press, 1998.
  3. David A. Solomon, Mark E. Russinovich, Inside Microsoft Windows 2000, Third Edition. Microsoft Press, 2000. (Имеется русский перевод: Дэвид Соломон, Марк Руссинович, Внутреннее устройство Microsoft Windows 2000. Издательства "Русская Редакция", "Питер", 2001).

Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 8    Оценка 640 [+1/-0]         Оценить