Автор: Джеффри Рихтер


Управление сервисами NT с помощью SCP-приложений


      От переводчика
      Вступление
      Разработка SCP-приложения
      Добавление сервиса в базу данных SCM
      Удаление сервиса из базы данных SCM
      Управление сервисом
      Изменение настроек сервиса
      Блокировка базы данных SCM
      Различные функции SCP
      Сервис "MSJ Time"
      Клиент сервиса "MSJ Time"

Скачать service2.exe (9Кб)

От переводчика

      Данная статья впервые была опубликована в Microsoft Systems Journal в 1998 году и является логическим продолжением предыдущей статьи Джеффри Рихтера "Разработка сервисов NT". Мне остается только пожелать Вам приятного чтения.

Вступление

      Каждый день я нахожу все новые применения сервисам Windows NT. В моей статье, опубликованной в октябре 1997 г. "Разработка сервисов Windows NT" я определил три основных компонента архитектуры сервисов Windows NT: "Контролёр сервисов" (Service Control Manager - SCM), непосредственно сам сервис и программа управления сервисом (Service Control Program - SCP-приложение). В данной статье мы с Вами обсудим SCP-приложения. При написании сервиса, очень важно понимать чем являются SCP-приложения и для чего они нужны, поскольку они являются программами, которые управляют сервисами.

Разработка SCP-приложения

      SCP-приложение - это Win32 программа, которая взаимодействует с SCM, запущенным либо на локальном, либо на удаленном компьютере. Многие считают, что предназначение SCP-приложений заключается в запуске (start), остановке (stop) сервиса, перевода его в режим паузы (pause) и, затем, повторного запуска (continue). Фактически возможностей у SCP-приложения намного больше. SCP-приложение может взаимодействовать с базой данных SCM для обеспечения установки, удаления и перечисления сервисов. Также, с помощью SCP-приложения можно изменять настройки сервиса. В этой статье я покажу различные пути взаимодействия SCP-приложения с SCM. SCM также ответственен за запуск и остановку драйверов оборудования. Многие из функций, объясняемых в этой статье, подходят как для сервисов, так и для драйверов оборудования. Но я, все же, сконцентрируюсь на сервисах, избегая темы драйверов.

      Первым шагом обеспечения взаимодействия с SCM, является вызов функции OpenSCManager:

SC_HANDLE OpenSCManager( LPCTSTR lpMachineName, LPCTSTR lpDatabaseName, DWORD dwDesiredAccess );

      Эта функция устанавливает канал связи с SCM на компьютере, указанном в параметре lpMachineName. Для открытия SCM на локальной машине, передайте в этом параметре NULL. Параметр lpDatabaseName, определяет какую базу данных необходимо открыть: всегда передавайте в этом параметре SERVICES_ACTIVE_DATABASE или NULL. Последний параметр - dwDesiredAccess - определяет что именно Вы собираетесь делать с базой данных. В таблице ниже показаны типы доступа, которые Вы можете использовать.

Флаг доступа к объекту Значение флага Описание
SC_MANAGER_ALL_ACCESS STANDARD_RIGHTS_REQUIRED|
SC_MANAGER_CONNECT|
SC_MANAGER_CREATE_SERVICE|
SC_MANAGER_ENUMERATE_SERVICE|
SC_MANAGER_LOCK|
SC_MANAGER_QUERY_LOCK_STATUS|
SC_MANAGER_MODIFY_BOOT_CONFIG
Является совокупностью всех нижеперечисленных флагов. Кроме этого включает флаг STANDARD_RIGHTS_REQUIRED(0x000F0000L).
SC_MANAGER_CONNECT 0x0001 Позволяет подключаться к SCM.
SC_MANAGER_CREATE_SERVICE 0x0002 Позволяет создать сервис функцией CreateService и добавить его в базу данных SCM.
SC_MANAGER_ENUMERATE_SERVICE 0x0004 Позволяет перечислять сервисы, находящиеся в базе данных SCM, используя функцию EnumServiceStatus.
SC_MANAGER_LOCK 0x0008 Позволяет блокировать базу данных SCM, путем вызова функции LockServiceDatabase.
SC_MANAGER_QUERY_LOCK_STATUS 0x0010 Позволяет запросить статус блокировки базы данных SCM, путем вызова функции QueryServiceLockStatus
SC_MANAGER_MODIFY_BOOT_CONFIG 0x0020

      Кроме этого, параметр dwDesiredAccess может содержать все или один из следующих флагов общего типа доступа (Прим. пер.):

Флаг общего доступа Доступ к SCM
GENERIC_READ Является совокупностью следующих флагов доступа: STANDARD_RIGHTS_READ, SC_MANAGER_ENUMERATE_SERVICE и SC_MANAGER_QUERY_LOCK_STATUS.
GENERIC_WRITE Является совокупностью следующих флагов доступа: STANDARD_RIGHTS_WRITE и SC_MANAGER_CREATE_SERVICE.
GENERIC_EXECUTE Является совокупностью следующих флагов доступа: STANDARD_RIGHTS_EXECUTE, SC_MANAGER_CONNECT и SC_MANAGER_LOCK.

      Функция OpenSCManager возвращает идентификатор SC_HANDLE, который Вы будете передавать в другие функции для работы с базой данных SCM. По завершении работы с базой данных SCM, Вы должны закрыть идентификатор, передав его в функцию CloseServiceHandle:

BOOL CloseServiceHandle( SC_HANDLE hSCManager );

Добавление сервиса в базу данных SCM

      Главная причина, из-за которой у Вас все же появится надобность в открытии базы данных SCM - добавление в нее сервиса NT. Для добавления сервиса, необходимо вызвать функцию OpenSCManager, указав флаг доступа SC_MANAGER_CREATE_SERVICE и, затем, вызвать функцию 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);

      Как видите, функция требует целых 13-ти параметров. Параметр hSCManager - идентификатор, полученный вызовом OpenSCManager. Следующие два параметра - lpServiceName и lpDisplayName - определяют имя сервиса. У сервисов имеется внутреннее имя, предназначенное для программистов, и внешнее (отображаемое) имя, предназначенное для пользователей. Внутреннее имя сервиса, определенное в параметре lpServiceName, используется SCM для сохранения информации о сервисе в реестре. Например, сервис Directory Replicator имеет внутреннее имя Replicator, и информация о нем может быть найдена в реестре в ключе HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Replicator.

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

Флаг доступа к объекту Значение флага Описание
SERVICE_ALL_ACCESS STANDARD_RIGHTS_REQUIRED|
SERVICE_QUERY_CONFIG|
SERVICE_CHANGE_CONFIG|
SERVICE_QUERY_STATUS|
SERVICE_ENUMERATE_DEPENDENTS|
SERVICE_START|
SERVICE_STOP|
SERVICE_PAUSE_CONTINUE|
SERVICE_INTERROGATE|
SERVICE_USER_DEFINED_CONTROL
Помимо перечисленных в этой таблице ниже, включает флаг STANDARD_RIGHTS_REQUIRED(0x000F0000L).
SERVICE_QUERY_CONFIG 0x0001 Позволяет запросить конфигурацию сервиса вызовом QueryServiceConfig.
SERVICE_CHANGE_CONFIG 0x0002 Позволяет конфигурировать сервис вызовом ChangeServiceConfig.
SERVICE_QUERY_STATUS 0x0004 Позволяет запросить текущий статус сервиса у SCM вызовом QueryServiceStatus.
SERVICE_ENUMERATE_DEPENDENTS 0x0008 Позволяет перечислять подчиненные сервисы вызовом EnumDependentServices.
SERVICE_START 0x0010 Позволяет вызывать ControlService для запуска сервиса.
SERVICE_STOP 0x0020 Позволяет вызывать ControlService для остановки сервиса.
SERVICE_PAUSE_CONTINUE 0x0040 Позволяет вызывать ControlService для перевода сервиса в режим паузы и последующего запуска.
SERVICE_INTERROGATE 0x0080 Позволяет вызывать ControlService для запроса текущего статуса сервиса.
SERVICE_USER_DEFINED_CONTROL 0x0100 Позволяет вызывать ControlService для указания сервису собственного управляющего кода.

      Кроме этого, параметр dwDesiredAccess может содержать все или один из следующих флагов общего доступа (Прим. пер.):

Флаг общего доступа Доступ к сервису
GENERIC_READ Является совокупностью следующих флагов доступа: STANDARD_RIGHTS_READ, SERVICE_QUERY_CONFIG, SERVICE_QUERY_STATUS, SERVICE_INTERROGATE и SERVICE_ENUMERATE_DEPENDENTS.
GENERIC_WRITE Является совокупностью следующих флагов доступа: STANDARD_RIGHTS_WRITE и SERVICE_CHANGE_CONFIG.
GENERIC_EXECUTE Является совокупностью следующих флагов доступа: STANDARD_RIGHTS_EXECUTE, SERVICE_START, SERVICE_STOP, SERVICE_PAUSE_CONTINUE и SERVICE_USER_DEFINED_CONTROL.

      Константа STANDARD_RIGHTS_REQUIRED (0x000F0000L) разрешает следующие типы доступа к объекту (Прим.пер.):

Флаги стандартного доступа Значение флага Доступ к сервису
ACCESS_SYSTEM_SECURITY 0x01000000L Позволяет вызывать QueryServiceObjectSecurity или SetServiceObjectSecurity для получения доступа к SACL.
DELETE 0x00010000L Позволяет удалять сервис вызовом DeleteService.
READ_CONTROL 0x00020000L Позволяет получить дескриптор безопасности сервиса вызовом QueryServiceObjectSecurity.
WRITE_DAC 0x00040000L Позволяет вызов SetServiceObjectSecurity для модификации члена Dacl дескриптора безпасности сервиса.
WRITE_OWNER 0x00080000L Позволяет вызов SetServiceObjectSecurity для модификации членов Owner и Group дескриптора безпасности сервиса.

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

Тип сервиса Значение типа Описание
SERVICE_WIN32_OWN_PROCESS 0x00000010 Показывает, что сервис запускается как самостоятельный процесс (в собственном адресном пространстве).
SERVICE_WIN32_SHARE_PROCESS 0x00000020 Показывает, что сервис "делит" адресное пространство с другими сервисами.
SERVICE_KERNEL_DRIVER 0x00000001 Определяет сервис драйвера устройства.
SERVICE_FILE_SYSTEM_DRIVER 0x00000002 Определяет сервис драйвера файловой системы.
SERVICE_INTERACTIVE_PROCESS 0x00000100 Позволяет сервису взаимодействовать с рабочим столом. Данная опция доступна в случае, если в параметре lpServiceStartName функции указана учетная запись системы.
SERVICE_ADAPTER 0x00000004 Описание в Platform SDK не найдено, но определен в WINNT.H (прим.пер.).
SERVICE_RECOGNIZER_DRIVER 0x00000008 Описание в Platform SDK не найдено, но определен в WINNT.H (прим.пер.).
SERVICE_DRIVER SERVICE_KERNEL_DRIVER|
SERVICE_FILE_SYSTEM_DRIVER|
SERVICE_RECOGNIZER_DRIVER
Комбинация указанных флагов (прим.пер.).
SERVICE_WIN32 SERVICE_WIN32_OWN_PROCESS|
SERVICE_WIN32_SHARE_PROCESS
Комбинация указанных флагов (прим.пер.).
SERVICE_TYPE_ALL SERVICE_WIN32|
SERVICE_ADAPTER|
SERVICE_DRIVER|
SERVICE_INTERACTIVE_PROCESS
Комбинация указанных флагов (прим.пер.).

      Параметр dwStartType функции CreateService указывает системе когда сервис должен быть запущен. Если Вы желаете, чтобы система стартовала сервис сразу после создания, укажите в этом параметре флаг SERVICE_AUTO_START. Для того, чтобы сервис запускался пользователем "вручную" или чтобы он был запущен, когда быдет запущен подчиненный сервис, укажите флаг SERVICE_DEMAND_START. И, если Вы желаете, чтобы сервис не был запущен вовсе, укажите флаг SERVICE_DISABLED. Эти и дополнительные флаги перечислены в таблице ниже (прим.пер.).

Тип запуска сервиса Значение флага Описание
SERVICE_BOOT_START 0x00000000 Определяет драйвер устройства, запускаемый загрузчиком системы. Применим только для сервисов драйверов устройств.
SERVICE_SYSTEM_START 0x00000001 Определяет драйвер устройства, запускаемый функцией IoInitSystem. Применим только для сервисов драйверов устройств.
SERVICE_AUTO_START 0x00000002 Определяет сервис, запускаемый SCM автоматически во время загрузки системы.
SERVICE_DEMAND_START 0x00000003 Определяет сервис, запускаемый SCM при вызове процессом функции StartService.
SERVICE_DISABLED 0x00000004 Определяет сервис, который более не должен быть запущен.

      Сервис является значительной частью системы и, следовательно, операционная система должна знать что ей делать, если запуск сервиса не удался. Для этого предназначен следующий параметр функции CreateService - dwErrorControl. Передача в этот параметр флагов SERVICE_ERROR_IGNORE и SERVICE_ERROR_NORMAL, предписывает операционной системе прописать код ошибки в системном журнале событий и продолжить загрузку системы. Разница между этими двумя флагами состоит в том, что флаг SERVICE_ERROR_NORMAL заставляет систему показать пользователю диалоговое окно с описанием ошибки. Для сервисов, запускаемых по требованию, всегда указывайте SERVICE_ERROR_IGNORE. Эти и другие коды ошибок сервиса перечислены в таблице ниже (прим.пер.).

Флаг ошибки Значение Описание
SERVICE_ERROR_IGNORE 0x00000000 Код ошибки прописывается в журнале событий, но загрузка системы продолжается.
SERVICE_ERROR_NORMAL 0x00000001 Код ошибки прописывается в журнале событий и пользователю показывается диалоговое окно ошибки, но загрузка системы продолжается.
SERVICE_ERROR_SEVERE 0x00000002 Код ошибки прописывается в журнале событий. Если при загрузке выбрана опция "last-known-good configuration", загрузка системы продолжается, иначе система будет перезапущена с опцией "last-known-good configuration".
SERVICE_ERROR_CRITICAL 0x00000003 Код ошибки прописывается в журнале событий, если есть такая возможность. Если при загрузке выбрана опция "last-known-good configuration", загрузка системы прерывается, иначе система будет перезапущена с опцией "last-known-good configuration".

      Флаги SERVICE_ERROR_SEVERE и SERVICE_ERROR_CRITICAL предписывают системе прекратить загрузку системы, если запуск сервиса не удался. Если запуск сервиса закончился неудачей и получен один из этих флагов ошибки, система записывает код ошибки в системный журнал событий и автоматически перезагружает систему, используя последнюю правильную конфигурацию (last-known-good configuration). Если после перезагрузки, сервис не запустился и получен флаг SERVICE_ERROR_SEVERE, загрузка системы продолжится. Если же, после перезагрузки, сервис не запустился и получен флаг SERVICE_ERROR_CRITICAL, загрузка системы прервется и будет произведена перезагрузка системы с использованием последней правильной конфигурации (last-known-good configuration).

      Следующий параметр функции CreateService - lpBinaryPathName - определяет полный путь к exe-файлу сервиса. Сервисы в основном устанавливают в системную папку (%SystemRoot%\system32), но Вы вольны поместить exe-файл Вашего сервиса куда угодно.

      Теперь давайте рассмотрим зависимости сервисов. Если говорить коротко, сервис является частью операционной системы и некоторые сервисы будут работать неправильно (если вообще будут работать прим.пер.), если какие-либо компоненты операционной системы, от которых зависит работоспособность сервиса, не были предварительно загружены и запущены на выполнение. При старте операционной системы, она придерживается определенного алгоритма, определяющего порядок запуска сервисов. Microsoft определила специальные группы, в которые собраны сервисы, как это показано в таблице ниже. Этот список можно также найти в ключе реестра HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ServiceGroupOrder.

Название группы Часть системы
System Bus Extender PCMCIA support
SCSI miniport SCSI device driver
Port None
Primary disk Floppy drives and hard disks
SCSI class SCSI drives
SCSI CDROM class CD-ROM drives
Filter CD Audio
boot file system Fast FAT drive access
Base System beep
Pointer Port Mouse support
Keyboard Port Keyboard (i8042prt) support
Pointer Class More mouse support
Keyboard Class More keyboard support
Video Init Video support
Video Video chip support
Video Save More video support
file system CD-ROM and NTFS file system support
Event log Event log support
Streams Drivers None
PNP_TDI NetBT and TCP/IP support
NDIS Netword support
NDISWAN None
TDI AFD Networking Support & DHCP
NetBIOSGroup NetBIOS support
SpoolerGroup Spool support
NetDDEGroup Network DDE support
Parallel arbitrator Parallel port support
extended base Modem, serial, and parallel support
RemoteValidation Net logon support
PCI Configuration PCI configuration support

      Во время загрузки, система проходит по данному списку, загружая драйверы оборудования и сервисы, входящие в каждую из указанных групп. То есть, система загружает драйверы устройств и сервисы, входящие в группу System Bus Extender до того, как загрузить драйверы и сервисы группы SCSI miniport.

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

      При добавлении в базу SCM драйвера устройства (в противопоставление сервисам), Вы можете добиться даже большей детализации порядка загрузки, если Ваш драйвер будет запускаться по указанному в параметре lpdwTagId функции CreateService ID драйвера. Сервисы не могут воспользоваться этой возможностью, поэтому, при добавлении в базу SCM сервиса, передавайте в этом параметре NULL.

      Дополнительно, при указании SCM, что Ваш сервис является составной частью конкретной группы, Вы можете также указать SCM какие сервисы (или группы) должны быть загружены до запуска Вашего сервиса. Например, сервису Computer Browser необходимо, чтобы предварительно были загружены сервисы Workstation и Server, а сервису Clipbook Server для нормальной работы необходим сервис NetDDE.

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

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

      Для создания сервиса, который зависит от сервиса Workstation (например, сервис Alerter имеет такую зависимость), Вы должны инициализировать параметр lpDependencies так, как показано ниже, до передачи его функции CreateService.

// Буфер содержит в конце два нулевых символа.
LPCTSTR lpDependencies = __TEXT("LanmanWorkstation\0");
CreateService(…, lpDependencies, …);

      Для создания сервиса, зависящего от сервисов Workstation и NetBios (как, например, сервис Messenger), передайте в параметр lpDependencies следующее:

// Строки в буфере разделены нулевыми символами
// и на конце также указан дополнительный нулевой символ.
LPCTSTR lpDependencies = __TEXT("LanmanWorkstation\0NetBios\0");
CreateService(…, lpDependencies, …);

      Кроме зависимости от отдельных сервисов, Вы вольны указать зависимость Вашего сервиса от целой группы. Зависимость от группы означает, что Ваш сервис будет запущен системой только, если, по крайней мере, один из сервисов, входящих в группу будет работать после попытки системы запустить все драйверы и сервисы группы. Для указания группы в параметре lpDependencies, необходимо указать специальный символ SC_GROUP_IDENTIFIER (определен в файле WINSVC.H), непосредственно после которого указывается название группы:

#define SC_GROUP_IDENTIFIERW    L'+'
#define SC_GROUP_IDENTIFIERA    '+'

#ifdef UNICODE
#define SC_GROUP_IDENTIFIER     SC_GROUP_IDENTIFIERW
#else
#define SC_GROUP_IDENTIFIER     SC_GROUP_IDENTIFIERA
#endif

      Следовательно, для создания сервиса, зависящего от сервиса Workstation и группы TDI, в параметре lpDependencies Вы должны указать следующее:

// Буфер содержит две зависимости:
// от сервиса Workstation и от группы TDI
// (на зависимость от группы указывает префикс '+'
// перед названием группы).
LPCTSTR lpDependencies = __TEXT("LanmanWorkstation\0+TDI\0");
CreateService(…, lpDependencies, …);

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

      Последние два параметра функции CreateService - lpServiceStartName и lpPassword - позволяют назначить имя пользователя и пароль, от имени которого сервис будет запускаться системой. Если сервис будет запускаться в контексте системной учетной записи - System Account - передайте в эти параметры NULL. Замечу, если исполняемый файл сервиса содержит несколько сервисов, он может быть запущен только от имени системы (т.е. Вы дожны использовать System Account для запуска сервиса). Если есть необходимость запускать сервис от имени конкретного пользователя, передайте в в параметр lpServiceStartName строку имени пользователя в виде "DomainName\UserName" и не забудьте передать пароль пользователя в параметр lpPassword.

      Если создание сервиса прошло успешно и информация о сервисе была добавлена в базу данных SCM, то функция CreateService возвратит идентификатор (хэндл) сервиса. Этот идентификатор потребуется в дальнейшем при использовании других API-функций для управления сервисом. Не забудьте передать этот идентификатор функции CloseServiceHandle по завершении работы сервиса. Если вызов функции CreateService завершился неудачей, она вернет NULL и последующий вызов GetLastError укажет на причину ошибки. В таблице ниже перечислены наиболее часто встречающиеся при вызове CreateService ошибки:

Флаг ошибки Код ошибки Описание
ERROR_ACCESS_DENIED 5 Идентификатор (хэндл) базы данных SCM не имеет прав на создание сервисов SC_MANAGER_CREATE_SERVICE.
ERROR_CIRCULAR_DEPENDENCY 1059 Обнаружены циклические зависимости сервисов.
ERROR_DUP_NAME 52 В базе данных SCM уже имеется сервис с таким названием или с таким же отображаемым именем.
ERROR_INVALID_HANDLE 6 Неверный идентификатор базы данных SCM.
ERROR_INVALID_NAME 123 Неверное имя сервиса.
ERROR_INVALID_PARAMETER 87 Неверный параметр.
ERROR_INVALID_SERVICE_ACCOUNT 1057 Имя пользователя, указанное в параметре lpServiceStartName отсутствует.
ERROR_SERVICE_EXISTS 1073 Указанный сервис уже имеется в базе данных SCM.

      Я всегда пишу свои сервисы таким образом, чтобы они могли сами себя устанавливать на компьютере пользователя. Для этого я, в функции WinMain, вызываю функцию наподобие ServiceInstall (смотри код ниже), если "-install" передается в виде аргумента командной строки. Файл MSJTimeSrv.c, код которого я приведу позднее, демонстрирует эту технику.

Процедура ServiceInstall
void ServiceInstall(LPCTSTR pszInternalName, LPCTSTR pszDisplayName, 
	DWORD dwServiceType, DWORD dwStartType, DWORD dwErrorControl) 
{
    // Открываем базу данных SCM для добавления сервиса
    SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
 
    // Получаем полный путь к исполняемому файлу сервиса
    char szModulePathname[_MAX_PATH];
    GetModuleFileName(NULL, szModulePathname, sizeof(szModulePathname));
 
    // Добавляем сервис в базу данных SCM
    SC_HANDLE hService = CreateService(
       hSCM, pszInternalName, pszDisplayName, 0, dwServiceType,
       dwStartType, dwErrorControl, szModulePathname,
       NULL, NULL, NULL, NULL, NULL);
 
    // Закрываем созданный сервис и базу данных SCM
    CloseServiceHandle(hService);
    CloseServiceHandle(hSCM);
 }

Удаление сервиса из базы данных SCM

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

SC_HANDLE OpenService( SC_HANDLE hSCManager,
                       LPCTSTR pszInternalName, 
                       DWORD dwDesiredAccess );

      Передайте этой функции идентификатор, полученный при вызове OpenSCManager, внутреннее имя сервиса (соответствует значению, которое Вы передавали в параметре lpServiceName функции CreateService), а в качестве флага dwDesiredAccess укажите DELETE. Функция вернет Вам идентификатор указанного сервиса. И, уже имея этот идентификатор, Вы можете удалить сервис вызовом DeleteService,

BOOL DeleteService( SC_HANDLE hService );

передав ей в качестве единственного параметра, идентификатор сервиса. Эта функция на самом деле не удаляет сервис из базы данных SCM, а только помечает его для удаления. SCM удалит сервис только после того, как сервис будет остановлен и все идентификаторы, ссылающиеся на него будут закрыты. Лично я пишу сервисы таким образом, чтобы они могли удалять себя сами из базы данных SCM. Если в качестве аргумента командной строки передано "-remove", я вызываю процедуру наподобие ServiceRemove (смотри код ниже):

Процедура ServiceRemove
void ServiceRemove(LPCTSTR pszInternalName) {
	// Открываем базу данных SCM
    SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
 
    // Открываем сервис с доступом на удаление
    SC_HANDLE hService = OpenService(hSCM, pszInternalName, DELETE);
 
    // Помечаем сервис на удаление
    // NOTE: Сервис не будет удален пока все идентификаторы, ссылающиеся на
	// него не будут закрыты, а сам сервис не будет остановлен
    DeleteService(hService);
 
    // Закрываем идентификаторы сервиса и базы данных SCM
    CloseServiceHandle(hService);
    CloseServiceHandle(hSCM);
}

Управление сервисом

      Как было сказано ранее, многие сервисы поставляются с программами, позволяющими запускать, останавливать, ставить на паузу, - иными словами, управлять сервисом. Разработка такого рода программы (SCP) - дело несложное. Она сначала должна открыть SCM на указанном компьютере, вызовом OpenSCManager, используя при этом доступ SC_MANAGER_CONNECT. После этого Вы вызываете OpenService для открытия сервиса, работой которого собираетесь управлять. При этом используете необходимое Вам сочетание флагов доступа: SERVICE_START, SERVICE_ STOP, SERVICE_PAUSE_CONTINUE, SERVICE_USER_DEFINED_CONTROL, SERVICE_INTERROGATE (остальные возможные значения Вы найдете в Platform SDK. прим.пер.).

      После этого вызывайте функцию управления сервисом. Для запуска сервиса:

BOOL StartService( SC_HANDLE hService, 
                   DWORD dwNumServiceArgs,
                   LPCTSTR *lpServiceArgVectors );

      Где параметр hService - идентификатор открытого сервиса, а параметры dwNumServiceArgs и lpServiceArgVectors - набор аргументов, передаваемых сервису. Многие сервисы не нуждаются в параметрах, поэтому для них передают соответственно 0 и NULL. Имейте в виду, что запуск сервиса может повлечь запуск некоторых других сервисов или групп сервисов, с которыми он связан зависимостями. В таблице ниже приведены ошибки, которые могут возникнуть при вызове StartService.

Флаг ошибки Код ошибки Описание
ERROR_ACCESS_DENIED 5 Указанный идентификатор (хэндл) не был открыт с доступом SERVICE_START.
ERROR_INVALID_HANDLE 6 Неверный идентификатор.
ERROR_PATH_NOT_FOUND 3 Исполняемый файл сервиса не найден.
ERROR_SERVICE_ALREADY_RUNNING 1056 Один экземпляр сервиса уже запущен.
ERROR_SERVICE_DATABASE_LOCKED 1055 База данных SCM заблокирована.
ERROR_SERVICE_DEPENDENCY_DELETED 1075 У сервиса имеется зависимость от помеченного на удаление или отсутствующего сервиса.
ERROR_SERVICE_DEPENDENCY_FAIL 1068 У сервиса имеется зависимость от сервиса, запуск которого завершился неудачей.
ERROR_SERVICE_DISABLED 1058 Сервис был отключен.
ERROR_SERVICE_LOGON_FAILED 1069 Сервис не может быть допущен в систему. Эта ошибка возникает, если сервис был запущен от имени пользователя, у которого не права "Log on as a service"
ERROR_SERVICE_MARKED_FOR_DELETE 1072 Сервис был помечен для удаления.
ERROR_SERVICE_NO_THREAD 1054 Ошибка создания потока для указанного сервиса.
ERROR_SERVICE_REQUEST_TIMEOUT 1053 Процесс сервиса был запущен, но он не вызвал StartServiceCtrlDispatcher или поток, вызвавший StartServiceCtrlDispatcher, был заблокирован функцией Handler.

      После запуска сервиса, Вы можете вызывать ControlService, для дальнейшего управления сервисом:

BOOL ControlService( SC_HANDLE hService,
                     DWORD dwControl,
                     LPSERVICE_STATUS lpServiceStatus );

      Где, опять же, параметр hService - идентификатор открытого сервиса. Параметр dwControl - является кодом уведомления, предписывающим, чего именно Вы хотите от сервиса - одно из значений: SERVICE_CONTROL_STOP, SERVICE_CONTROL_PAUSE, SERVICE_CONTROL_CONTINUE, SERVICE_CONTROL_INTERROGATE (остальные возможные значения Вы найдете в Platform SDK. прим. пер.). В дополнение к перечисленным, Вы можете указать собственный код уведомления в диапазоне от 128 до 255 включительно. Замечу, что вызов функции ControlService потерпит неудачу, если в dwControl передать значение SERVICE_CONTROL_SHUTDOWN; только сама операционная система вправе послать такое уведомление Handler-процедуре сервиса. Последний параметр - lpServiceStatus - является указателем на структуру SERVICE_STATUS. Функция заполняет члены структуры значениями последнего статуса сервиса. После отработки ControlService, Вы можете проверить значения членов структуры, для получения отчета о работе сервиса. В таблице ниже перечисленны ошибки, возможные при вызове ControlService.

Флаг ошибки Код ошибки Описание
ERROR_ACCESS_DENIED 5 Указанный идентификатор (хэндл) не был открыт с нужным уровнем доступа.
ERROR_DEPENDENT_SERVICES_RUNNING 1051 Сервис не может быть остановлен, т.к. имеются работающие сервисы, зависящие от него.
ERROR_INVALID_HANDLE 6 Указанный идентификатор не был получен вызовом CreateService или OpenService, либо является недопустимым.
ERROR_INVALID_PARAMETER 87 Код уведомления неопределен.
ERROR_INVALID_SERVICE_CONTROL 1052 Код уведомления недопустим, либо неприемлем для сервиса.
ERROR_SERVICE_CANNOT_ACCEPT_CTRL 1061 Код уведомления не может быть послан сервису, т.к. статус сервиса SERVICE_STOPPED, SERVICE_START_PENDING или SERVICE_STOP_PENDING.
ERROR_SERVICE_NOT_ACTIVE 1062 Сервис не запущен.
ERROR_SERVICE_REQUEST_TIMEOUT 1053 Процесс сервиса был запущен, но он не вызвал StartServiceCtrlDispatcher или поток, вызвавший StartServiceCtrlDispatcher, был заблокирован функцией Handler.
ERROR_SHUTDOWN_IN_PROGRESS 1115 В данный момент идет процесс завершения работы операционной системы.

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

Показать код

Процедура StopService
void StopService(LPCTSTR pszInternalName) {
    // Открываем SCM и нужный сервис.
    SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
    SC_HANDLE hService = OpenService(hSCM, pszInternalName, SERVICE_STOP);
 
    // Приказываем сервису остановиться.
    SERVICE_STATUS ss;
    ControlService(hService, SERVICE_CONTROL_STOP, &ss);
 
    // Ждем остановки сервиса в течение 15 секунд.
    WaitForServiceToReachState(hService, SERVICE_STOP, &ss, 15000);
 
    // Закрываем сервис и SCM.
    CloseServiceHandle(hService);
    CloseServiceHandle(hSCM);
 }

Функция WaitForServiceToReachState BOOL WaitForServiceToReachState(SC_HANDLE hService, DWORD dwDesiredState, SERVICE_STATUS* pss, DWORD dwMilliseconds) { DWORD dwLastState, dwLastCheckPoint; BOOL fFirstTime = TRUE; // Для начала не сравниваем статус и контрольную точку BOOL fServiceOk = TRUE; DWORD dwTimeout = GetTickCount() + dwMilliseconds; // Цикл, пока сервис не примет нужное состояние, // либо пока не возникнет ошибка, либо пока не истечет таймаут while (TRUE) { // Получаем текущее состояние сервиса fServiceOk = ::QueryServiceStatus(hService, pss); // Если не можем послать запрос сервису - выходим if (!fServiceOk) break; // Если сервис принял нужное состояние - выходим if (pss->dwCurrentState == dwDesiredState) break; // Если истек таймаут - выходим if ((dwMilliseconds != INFINITE) && (dwTimeout > GetTickCount())) { SetLastError(ERROR_TIMEOUT); break; } // Если это первый проход - сохраняем состояние сервиса if (fFirstTime) { dwLastState = pss->dwCurrentState; dwLastCheckPoint = pss->dwCheckPoint; fFirstTime = FALSE; } else { // Если это не первый проход или состояние сервиса изменилось - сохраняем состояние if (dwLastState != pss->dwCurrentState) { dwLastState = pss->dwCurrentState; dwLastCheckPoint = pss->dwCheckPoint; } else { // Состояние сервиса не изменилось - проверяем увеличена ли контрольная точка if (pss->dwCheckPoint > dwLastCheckPoint) { // Если увеличена - сохраняем checkpoint dwLastCheckPoint = pss->dwCheckPoint; } else { // Если не увеличена - сервис вышел из строя, выходим. fServiceOk = FALSE; break; } } } // Ничего не делаем, просто ждем указанный период времени Sleep(pss->dwWaitHint); } // Note: Последний SERVICE_STATUS возвращается вызывающей процедуре, // чтобы она могла проверить статус сервиса и код ошибки. return(fServiceOk); }

      Желательно, чтобы Ваша SCP отслеживала состояние сервиса. Например, пользователь SCP-апплета нажимает кнопку "Stop". Апплет уведомляет SCM о том, что выбранный сервис должен быть остановлен. SCM, в свою очередь, посылает уведомление сервису, который должен отрапортовать выставлением флага SERVICE_STOP_PENDING в dwCurrentState. Однако, сервис при этом еще не закончил работу. Поэтому апплет не должен обновлять информацию о статусе сервиса. Операционная система не поддерживает никакого механизма, позволяющего отслеживать изменение состояния сервиса. Поэтому SCP должна периодически опрашивать сервис на предмет изменения статуса. Функция WaitForServiceToReachState как раз демонстрирует технику опроса сервиса.

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

      В дополнение, SCP должна проверять контрольную точку, возвращаемую сервисом, на предмет ее приращения. Если сервис вернул предыдущее или меньшее значение контрольной точки, SCP должна решить, что работа сервиса прервана из-за ошибки. Это еще одна неудобная сторона сервиса, нуждающаяся в улучшении.

      Вы наверняка заметите, что функция WaitForServiceToReachState вызывает QueryServiceStatus:

BOOL QueryServiceStatus( SC_HANDLE hService, LPSERVICE_STATUS lpServiceStatus );

      Вызов этой функции аналогичен вызову ControlService с передачей флага SERVICE_CONTROL_INTERROGATE. Однако, в отличие от ControlService, которая спрашивает у сервиса каков его статус, QueryServiceStatus спрашивает у SCM каков статус сервиса. Это означает, что QueryServiceStatus всегда возвращает Вам самую верную информацию.

      Проблема заключается в том, что вызов ControlService проваливается, если указанный сервис в момент вызова не работает. Допустим, Вы хотите остановить сервис и дождаться момента его остановки. В этом случае, Вы можете вызвать ControlService с установкой флага SERVICE_CONTROL_STOP, а затем периодически вызывать ControlService передавая флаг SERVICE_CONTROL_INTERROGATE. В случае, если сервис уже остановлен в момент первого вызова ControlService, Handler-функция сервиса не сможет ответить на уведомление SERVICE_CONTROL_INTERROGATE и, в результате, работа функции ControlService провалится.

      Функция же QueryServiceStatus, запрашивает SCM о статусе сервиса. Если сервис остановлен, то SCM передает в член dwCurrentState структуры SERVICE_STATUS значение SERVICE_STOPPED. Вы можете даже вызывать QueryServiceStatus во время работы сервиса, поскольку SCM кэширует информацию о статусе сервиса каждый раз, когда сервис обращается к функции SetServiceStatus.

      Второе преимущество QueryServiceStatus состоит в том, что она возвращает управление сразу после вызова, т.к. не отправляет никаких уведомлений сервису. Если Handler-функция сервиса занята обработкой уведомления, она просто не сможет быстро ответить на запрос, что, в свою очередь, повлечет "зависание" SCP. Однако, есть и другая сторона медали. Из-за кэширования данных о статусе сервиса, SCM не всегда может правильно отрапортовать о текущем статусе сервиса. Теперь, когда Вы познакомились с обеими техниками, позволяющими отслеживать состояние сервиса, выбирайте сами что Вам в вашей ситуации подходит больше.

Изменение настроек сервиса

      Функция CreateService позволяет добавить запись о новом сервисе в юазу данных SCM. Хотя и редко, но возможно у Вас появится необходимость изменить информацию о сервисе, находящуюся в этой базе данных. Для примера, учетная запись пользователя, ассоциированная с записью в базу SCM нуждается в замене пароля или в смене режима запуска сервиса с ручного на автоматический. На этот случай в Win32 имеется две API-функции, позволяющие произвести перенастройку сервиса. Первая из них - QueryServiceConfig - позволит Вам извлечь информацию о сервисе из базы данных SCM:

BOOL QueryServiceConfig( SC_HANDLE hService, LPQUERY_SERVICE_CONFIG lpServiceConfig,
			 DWORD cbBufSize, LPDWORD pcbBytesNeeded );

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

typedef struct _QUERY_SERVICE_CONFIG {
    DWORD   dwServiceType;
    DWORD   dwStartType;
    DWORD   dwErrorControl;
    LPTSTR  lpBinaryPathName;
    LPTSTR  lpLoadOrderGroup;
    DWORD   dwTagId;
    LPTSTR  lpDependencies;
    LPTSTR  lpServiceStartName;
    LPTSTR  lpDisplayName;
 } QUERY_SERVICE_CONFIG, *LPQUERY_SERVICE_CONFIG; 

      Третий параметр функции QueryServiceConfig - cbBufSize - указывает на размер буфера, а в четвертый - pcbBytesNeeded - функция передает значение размера буфера, который ей нужен. Буфер, передаваемый функции QueryServiceConfig должен быть больше размера структуры QUERY_SERVICE_CONFIG, т.к. функция копирует всю текстовую информацию о сервисе непосредственно сразу за членами структуры, имеющими фиксированный размер. Члены структуры типа LPTSTR указывают на адреса памяти внутри этого буфера.

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

DWORD cbBytesNeeded;
LPQUERY_SERVICE_CONFIG pqsc;
	
// выясняю нужный размер буфера
QueryServiceConfig(hService, NULL, 0, &cbBytesNeeded);

// резервирую необходимый блок памяти под буфер
pqsc = malloc(cbBytesNeeded);
	
// повторно вызываю функцию для извлечения данных
QueryServiceConfig(hService, pqsc, cbBytesNeeded, &cbBytesNeeded);
 
// Обращаюсь к членам внутри pqsc
•
•
•
free(pqsc);

      Поскольку Вы имеете информацию о текущей конфигурации сервиса, то можете ее изменить вызовом ChangeServiceConfig:

BOOL ChangeServiceConfig( SC_HANDLE hService, DWORD dwServiceType, DWORD dwStartType, 
			 DWORD dwErrorControl, LPCTSTR lpBinaryPathName,
			 LPCTSTR lpLoadOrderGroup, LPDWORD lpdwTagId, 
			 LPCTSTR lpDependencies, LPCTSTR lpServiceStartName, 
			 LPCTSTR lpPassword, LPCTSTR lpDisplayName );

      Как Вы можете заметить, параметры этой функции идентичны параметрам, передаваемым в CreateService. Разница состоит в том, что Вы не можете изменить внутреннее имя сервиса и что отображаемое имя lpDisplayName сервиса передается в последнем параметре.

      Если сервис работает, изменения настроек не вступят в силу до момента его остановки. Исключением является смена отображаемого имени сервиса lpDisplayName - это изменение вступает в силу немедленно.

Блокировка базы данных SCM

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

SC_LOCK LockServiceDatabase( SC_HANDLE hSCManager );

      передав ей идентификатор SCM, полученный вызовом OpenSCManager с использованием флага доступа SC_MANAGER_LOCK. Функция возвращает 32-битное значение, идентифицирующее блокировку. Сохраните этот идентификатор, поскольку он Вам понадобится в вызове функции UnlockServiceDatabase, когда Вы захотите разблокировать базу SCM:

BOOL UnlockServiceDatabase( SC_LOCK ScLock );

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

      Запомните, при закрытии идентификатора SCM база данных не разблокируется. Например:

SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_LOCK);
// Блокируем базу данных SCM
SC_LOCK scLock = LockServiceDatabase(hSCM);
// закрываем идентификатор SCM
CloseServiceHandle(hSCM);
// NOTE: База данных все еще заблокирована
•
•
•
UnlockServiceDatabase(scLock);
// Теперь база данных SCM разблокирована

      Еще имеется функция QueryServiceLockStatus для просмотра статуса блокировки базы данных SCM:

BOOL QueryServiceLockStatus( SC_HANDLE hSCManager,
                             LPQUERY_SERVICE_LOCK_STATUS 
                             lpLockStatus,
                             DWORD cbBufSize, 
                             LPDWORD pcbBytesNeeded );

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

typedef struct _QUERY_SERVICE_LOCK_STATUS {
     DWORD   fIsLocked;
     LPTSTR  lpLockOwner;
     DWORD   dwLockDuration;
 } QUERY_SERVICE_LOCK_STATUS, *LPQUERY_SERVICE_LOCK_STATUS;

      Таким же образом, как при вызове QueryServiceConfig, буфер, который Вы передаете в QueryServiceLockStatus должен быть больше размера структуры. И снова повторю, это нужно потому, что структура содержит строковое значение lpLockOwner, которое располагается сразу за членом структуры, имеющим фиксированную длину.

Различные функции SCP

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

BOOL GetServiceDisplayName( SC_HANDLE hSCManager, LPCTSTR lpServiceName, 
			LPTSTR lpDisplayName, LPDWORD lpcchBuffer );

      И обратная ей функция:

BOOL GetServiceKeyName( SC_HANDLE hSCManager, LPCTSTR lpDisplayName, 
			LPTSTR lpServiceName, LPDWORD lpcchBuffer);

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

      Вторая - функция, позволяющая перечислить все сервисы (и их статусы), содержащиеся в базе данных SCM:

BOOL EnumServicesStatus( SC_HANDLE hSCManager, DWORD dwServiceType, 
			DWORD dwServiceState, LPENUM_SERVICE_STATUS lpServices,
			DWORD cbBufSize, LPDWORD pcbBytesNeeded, 
			LPDWORD lpServicesReturned, LPDWORD lpResumeHandle );

      SCP-апплет сам использует эту функцию для заполнения списка установленных сервисов. Первый параметр функции - hSCManager - идентификатор SCM, чьи сервисы Вы желаете перечислить. Второй параметр - dwServiceType - показывает какого типа сервисы Вы желаете перечислить: сервисы или драйверы устройств. Для перечисления сервисов передайте SERVICE_WIN32. Третий параметр - dwServiceState - позволит еще больше детализировать запрос. Можете передать SERVICE_ACTIVE (0x00000001) для перечисления работающих сервисов, SERVICE_INACTIVE (0x00000002) для перечисления остановленных сервисов или SERVICE_STATE_ALL (SERVICE_ACTIVE | SERVICE_INACTIVE) для перечисления и тех и других.

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

typedef struct _ENUM_SERVICE_STATUS {
    LPTSTR lpServiceName;
    LPTSTR lpDisplayName;
	SERVICE_STATUS ServiceStatus; 
} ENUM_SERVICE_STATUS, *LPENUM_SERVICE_STATUS;

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

DWORD cbBytesNeeded, dwServicesReturned, 
dwResumeHandle = 0;
LPQUERY_SERVICE_CONFIG pqsc;

EnumServicesStatus(hSCManager, SERVICE_WIN32, SERVICE_STATE_ALL, 
			NULL, 0, &cbBytesNeeded, 
			&dwServicesReturned, &dwResumeHandle);
 
pess = malloc(cbBytesNeeded);
EnumServicesStatus(hSCManager, SERVICE_WIN32, SERVICE_STATE_ALL, 
			pess, cbBytesNeeded, &cbBytesNeeded, 
			&dwServicesReturned, &dwResumeHandle);
 
for (DWORD dw = 0; dw < dwServicesReturned; dw++) {
	// Обращаемся к члену структуры pess, например
    printf("%s\n", pess[dw].lpDisplayName);
}
free(pess);

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

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

BOOL EnumDependentServices( SC_HANDLE hService, DWORD dwServiceState, 
			LPENUM_SERVICE_STATUS lpServices, DWORD cbBufSize,
			LPDWORD pcbBytesNeeded, LPDWORD lpServicesReturned );

      Поскольку эта функция очень похожа на функцию EnumServicesStatus, все ее параметры не нуждаются в дополнительном объяснении. SCM-апплет вызывает эту функцию, когда имеется попытка остановки сервиса, имеющего зависимости от других сервисов системы. Например, если я делаю попытку остановить сервис Workstation, то получаю диалоговое окно (рис.1). Функция EnumDependentServices используется для заполнения списка зависимостей.

depend.jpg
рис.1 Диалоговое окно зависимостей сервиса

      И, наконец, я перехожу к двум оставшимся функциям управления сервисами - QueryServiceObjectSecurity и SetServiceObjectSecurity:

BOOL QueryServiceObjectSecurity( SC_HANDLE hService, 
			SECURITY_INFORMATION dwSecurityInformation,
			PSECURITY_DESCRIPTOR lpSecurityDescriptor,
			DWORD cbBufSize, LPDWORD pcbBytesNeeded );
BOOL SetServiceObjectSecurity( SC_HANDLE hService,
			SECURITY_INFORMATION dwSecurityInformation,
			PSECURITY_DESCRIPTOR lpSecurityDescriptor );

      Эти две функции позволяют запросить и изменить дескриптор безопасности, ассоциированный с сервисом. Когда Вы вызываете CreateService, сервису назначается дескриптор безопасности процесса, создавшего его. Функция SetServiceObjectSecurity позволяет изменить это назначение.

Сервис "MSJ Time"

      Сервис MSJTimeSrv, код которого показан ниже, включает все компоненты, необходимые для построения сервиса Windows NT. Этот сервис очень простой и служит для возвращения серверных даты и времени, при подключении к нему клиента. Код сервиса подразумевает, что Вы знакомы с такими понятиями, как "каналы" (pipes) и "порты завершения ввода/вывода" (I/O completion ports). Если Вам нужна подробная информация по этим понятиям, обращайтесь к документации Platform SDK.

Показать код

Код MSJTimeSrv.c
/*****************************************************************************
Module :     MSJTimeSrv.c
Notices:     Written 1997 by Jeffrey Richter
Description: Minimal Service Template
*****************************************************************************/

#define STRICT
#define UNICODE
#include <Windows.h>

//////////////////////////////////////////////////////////////////////////////

#define dimof(A)  (sizeof(A) / sizeof(A[0]))

//////////////////////////////////////////////////////////////////////////////

WCHAR g_szAppName[] = L"MSJ Time Service";

//////////////////////////////////////////////////////////////////////////////

HANDLE g_hIOCP = NULL;

// The completion port wakes for 1 of 2 reasons:
enum COMPKEY { 
   CK_SERVICECONTROL,   // A service control code
   CK_PIPE              // A client connects to our pipe
};

//////////////////////////////////////////////////////////////////////////////

void WINAPI TimeServiceHandler(DWORD fdwControl) {
   // The Handler thread is very simple and executes very quickly because
   // it just passes the control code off to the ServiceMain thread.
   PostQueuedCompletionStatus(g_hIOCP, fdwControl, CK_SERVICECONTROL, NULL);
}

//////////////////////////////////////////////////////////////////////////////

#define SERVICE_CONTROL_RUN            0x00000000

DWORD dwSrvCtrlToPend[256] = {   // 255 is max user-defined code
   /* 0: SERVICE_CONTROL_RUN         */ SERVICE_START_PENDING, 
   /* 1: SERVICE_CONTROL_STOP        */ SERVICE_STOP_PENDING,
   /* 2: SERVICE_CONTROL_PAUSE       */ SERVICE_PAUSE_PENDING,
   /* 3: SERVICE_CONTROL_CONTINUE    */ SERVICE_CONTINUE_PENDING,
   /* 4: SERVICE_CONTROL_INTERROGATE */ 0, 
   /* 5: SERVICE_CONTROL_SHUTDOWN    */ SERVICE_STOP_PENDING,
   /* 6 - 255: User-defined codes    */ 0
};

DWORD dwSrvPendToState[] = { 
   /* 0: Undefined                */ 0,
   /* 1: SERVICE_STOPPED          */ 0,
   /* 2: SERVICE_START_PENDING    */ SERVICE_RUNNING,
   /* 3: SERVICE_STOP_PENDING     */ SERVICE_STOPPED, 
   /* 4: SERVICE_RUNNING          */ 0,
   /* 5: SERVICE_CONTINUE_PENDING */ SERVICE_RUNNING,
   /* 6: SERVICE_PAUSE_PENDING    */ SERVICE_PAUSED,
   /* 7: SERVICE_PAUSED           */ 0
};

//////////////////////////////////////////////////////////////////////////////

void WINAPI TimeServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) {
   DWORD dwCompKey  = CK_SERVICECONTROL;
   DWORD fdwControl = SERVICE_CONTROL_RUN;
   DWORD dwBytesTransferred;
   SYSTEMTIME st;
   HANDLE hpipe;
   OVERLAPPED o, *po;
   SERVICE_STATUS ss;
   SERVICE_STATUS_HANDLE hSS;

   // Create the completion port and save its handle in a global
   // variable so that the Handler function can access it.
   g_hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, CK_PIPE, 0);

   // Give SCM the address of this service's Handler
   // NOTE: hSS does not have to be closed.
   hSS = RegisterServiceCtrlHandler(g_szAppName, TimeServiceHandler);

   // Do what the service should do.
   // Initialize the members that never change
   ss.dwServiceType = SERVICE_WIN32_OWN_PROCESS; 
   ss.dwControlsAccepted = SERVICE_ACCEPT_STOP | 
      SERVICE_ACCEPT_PAUSE_CONTINUE | SERVICE_ACCEPT_SHUTDOWN;
   
   do {
      switch (dwCompKey) {
      case CK_SERVICECONTROL:
         // We got a new control code
         ss.dwWin32ExitCode = NO_ERROR; 
         ss.dwServiceSpecificExitCode = 0; 
         ss.dwCheckPoint = 0; 
         ss.dwWaitHint = 0;

         if (fdwControl == SERVICE_CONTROL_INTERROGATE) {
            SetServiceStatus(hSS, &ss);
            break;
         }

         // Determine which PENDING state to return
         if (dwSrvCtrlToPend[fdwControl] != 0) {
            ss.dwCurrentState = dwSrvCtrlToPend[fdwControl]; 
            ss.dwCheckPoint = 0;
            ss.dwWaitHint = 500;   // half a second
            SetServiceStatus(hSS, &ss);
         }

         switch (fdwControl) {
            case SERVICE_CONTROL_RUN:
            case SERVICE_CONTROL_CONTINUE:
               // While running, create a pipe that clients can connect to.
               hpipe = CreateNamedPipe(L"\\\\.\\pipe\\MSJTime", 
                  PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED,
                  PIPE_TYPE_BYTE, 1, sizeof(st), sizeof(st), 1000, NULL);

               // Associate the pipe with the completion port
               CreateIoCompletionPort(hpipe, g_hIOCP, CK_PIPE, 0);	

               // Pend an asynchronous connect against the pipe
               ZeroMemory(&o, sizeof(o));
               ConnectNamedPipe(hpipe, &o);
               break;

            case SERVICE_CONTROL_PAUSE:
            case SERVICE_CONTROL_STOP:
            case SERVICE_CONTROL_SHUTDOWN:
               // When not running, close the pipe so clients can't connect
               CloseHandle(hpipe);
               break;

            case 128:   // User-defined control (demonstration purposes)
               MessageBox(NULL, L"Got control code 128", g_szAppName, MB_OK);
               break;
         }

         // Determine which complete state to return
         if (dwSrvPendToState[ss.dwCurrentState] != 0) {
            ss.dwCurrentState = dwSrvPendToState[ss.dwCurrentState]; 
            ss.dwCheckPoint = ss.dwWaitHint = 0;
            SetServiceStatus(hSS, &ss);
         }
         break;

      case CK_PIPE:
         // We got a client request: Send our current time to the client
         GetSystemTime(&st);
         WriteFile(hpipe, &st, sizeof(st), &dwBytesTransferred, NULL);
         FlushFileBuffers(hpipe);
         DisconnectNamedPipe(hpipe);

         // Allow another client to connect 
         ZeroMemory(&o, sizeof(o));
         ConnectNamedPipe(hpipe, &o);
      }

      if (ss.dwCurrentState != SERVICE_STOPPED) {
         // Sleep until a control code comes in or a client connects
         GetQueuedCompletionStatus(g_hIOCP, &dwBytesTransferred, 
            &dwCompKey, &po, INFINITE);
         fdwControl = dwBytesTransferred;
      }
   } while (ss.dwCurrentState != SERVICE_STOPPED);

   // Cleanup and stop this service
   CloseHandle(g_hIOCP);   
}

//////////////////////////////////////////////////////////////////////////////

void InstallService() {
   TCHAR szModulePathname[_MAX_PATH];
   SC_HANDLE hService;

   // Open the SCM on this machine.
   SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);

   // Get our full pathname
   GetModuleFileName(NULL, szModulePathname, dimof(szModulePathname));

   // Add this service to the SCM's database.
   hService = CreateService(hSCM, g_szAppName, g_szAppName, 0,
      SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, 
      szModulePathname, NULL, NULL, NULL, NULL, NULL);

   // Close the service and the SCM
   CloseServiceHandle(hService);
   CloseServiceHandle(hSCM);
}

//////////////////////////////////////////////////////////////////////////////

void RemoveService() {
   // Open the SCM on this machine.
   SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);

   // Open this service for DELETE access
   SC_HANDLE hService = OpenService(hSCM, g_szAppName, DELETE);

   // Remove this service from the SCM's database.
   DeleteService(hService);

   // Close the service and the SCM
   CloseServiceHandle(hService);
   CloseServiceHandle(hSCM);
}

//////////////////////////////////////////////////////////////////////////////

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstExePrev, 
   LPSTR pszCmdLine, int nCmdShow) {

   int nArgc = __argc;
#ifdef UNICODE
   LPCTSTR *ppArgv = (LPCTSTR*) CommandLineToArgvW(GetCommandLine(), &nArgc);
#else
   LPCTSTR *ppArgv = (LPCTSTR*) __argv;
#endif

   BOOL fStartService = (nArgc < 2), fDebug = FALSE;
   int i;

   for (i = 1; i < nArgc; i++) {
      if ((ppArgv[i][0] == __TEXT('-')) || (ppArgv[i][0] == __TEXT('/'))) {
         // Command line switch
         if (lstrcmpi(&ppArgv[i][1], __TEXT("install")) == 0) 
            InstallService();

         if (lstrcmpi(&ppArgv[i][1], __TEXT("remove"))  == 0)
            RemoveService();

         if (lstrcmpi(&ppArgv[i][1], __TEXT("debug"))   == 0)
            fDebug = TRUE;
      }
   }

#ifdef UNICODE
   HeapFree(GetProcessHeap(), 0, (PVOID) ppArgv);
#endif

   if (fDebug) {
      // Running as EXE not as service, just run the service for debugging
      TimeServiceMain(0, NULL);
   }

   if (fStartService) {
      SERVICE_TABLE_ENTRY ServiceTable[] = {
         { g_szAppName, TimeServiceMain },
         { NULL, NULL }   // End of list
      };
      StartServiceCtrlDispatcher(ServiceTable);
   }

   return(0);
}

//////////////////////////////// End Of File /////////////////////////////////

      Если Вы обратите внимание на функцию WinMain, то увидите, что сервис имеет возможность устанавливать и удалять сам себя из базы данных SCM, в зависимости от того "-install" или "-remove" переданы в нее в качестве аргумента командной строки. При установке сервиса запустите его из командной строки с параметром "-install". Когда надобность в сервисе отпадет, запустите его с параметром "-remove".

      Самая важная вещь в функции WinMain - это то, что массив структур SERVICE_TABLE_ENTRY имеет два члена. Один для сервиса и второй, со значениями NULL для обозначения конца массива. Адрес этой структуры затем передается в функцию StartServiceCtrlDispatcher, которая создает поток для сервиса. Созданный поток начинает свою работу в функции TimeServiceMain. Имейте в виду, что StartServiceCtrlDispatcher не возвратит управление функции WinMain до тех пор, пока не вернет управление функция TimeServiceMain и ее поток не закончит работу.

      Функция TimeServiceMain содержит код обработки клиентских запросов. Она начинает свою работу с создания порта завершения ввода/вывода. Поток сервиса находится в цикле в ожидании запроса клиента для входа в порт завершения. Возможны два типа запросов: соединение клиента и запрос даты и времени. Однако, поток сервиса может проснуться и по причине поступления уведомления об изменении статуса, например, паузы или остановки.

      Как только порт завершения создан, происходит вызов RegisterServiceCtrlHandler для сообщения SCM адреса Handler-функции сервиса - TimeServiceHandler, которая принимает все уведомления, посылаемые сервису. Помните, что поток, первоначально вызванный функцией StartServiceCtrlDispatcher - это тот же самый поток, который выполняет код функции TimeServiceHandler.

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

      Внутри функции TimeServiceMain начинается цикл do/while. Внутри этого цикла я проверяю переменную dwCompKey, для проверки того, что должен сервис делать в дальнейшем. Пока эта переменная имеет значение CK_SERVICECONTROL, первой задачей сервиса является создание именованного канала, который клиентское приложение сможет использовать для получения ответов от сервиса. Этот канал затем ассоциируется с портом завершения с флагом завершения CK_PIPE, и выполняется асинхронный вызов ConnectNamedPipe. Теперь сервис рапортует SCM о том, что он запущен и работает, путем заполнения структуры SERVICE_STATUS и передачи ее адреса функции SetServiceStatus.

      Теперь сервис вызывает GetQueuedCompletionStatus, которая заставляет его поток "спать", пока не появится событие в порте завершения. Если поступает уведомление (по причине вызова PostQueuedCompletionStatus из TimeServiceHandler), поток сервиса "просыпается", рапортует SCM о том, что задача в процессе выполнения (если таковая имеется), обрабатывает код уведомления (если имеется) и, затем, снова рапортует SCM о том, что выполнение задачи завершено.

      Если поток сервиса "просыпается" по причине того, что GetQueuedCompletionStatus возвратила флаг завершения CK_PIPE, значит клиент присоединился к каналу. В этом случае сервис берет системное время компьютера и вызывает функцию WriteFile для пересылки значения даты клиенту. Затем сервис отсоединяет клиента и выполняет повторный асинхронный вызов ConnectNamedPipe для того, чтобы другой клиент мог соединиться с каналом.

      При "пробуждении" потока сервиса в результате получения кода уведомления SERVICE_CONTROL_STOP или SERVICE_CONTROL_SHUTDOWN, он закрывает канал и поток сервиса завершает работу. Это заставляет закрыться порт завершения, после чего функция TimeServiceMain возвращает управление, "убивая" поток сервиса. В этот момент функция StartServiceCtrlDispatcher возвращает управление функции WinMain, которая, в свою очередь, также завершается, "убивая" процесс.

Клиент сервиса "MSJ Time"

      Для тестирования работы сервиса, Вы также должны запустить клиентское приложение, код которого приведен ниже.

Показать код

Код MSJTimeClient.c
/*****************************************************************************
Module :     MSJTimeClient.c
Notices:     Written 1997 by Jeffrey Richter
Description: Client to request machine system time
*****************************************************************************/

#define STRICT
#include <Windows.h>
#include <WindowsX.h>
#include "Resource.h"

//////////////////////////////////////////////////////////////////////////////

#define dimof(A)  (sizeof(A) / sizeof(A[0]))

// The normal HANDLE_MSG macro in WINDOWSX.H does not work properly for
// dialog boxes because DlgProc's return a BOOL instead of an LRESULT (like
// WndProcs). This chHANDLE_DLGMSG macro corrects the problem:
#define chHANDLE_DLGMSG(hwnd, message, fn)                           \
   case (message): return (SetDlgMsgResult(hwnd, uMsg,               \
      HANDLE_##message((hwnd), (wParam), (lParam), (fn))))

//////////////////////////////////////////////////////////////////////////////

BOOL MSJTimeClient_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) {

   // Assume that the server is on the same machine as the client
   SetDlgItemText(hwnd, IDC_SERVER, __TEXT("."));
   return(TRUE);
}

//////////////////////////////////////////////////////////////////////////////

void MSJTimeClient_OnCommand(HWND hwnd, int id, HWND hwndCtl, 
   UINT codeNotify) {

   SYSTEMTIME st;
   TCHAR sz[500];
   DWORD cbRead = 0;
   HANDLE hpipe;
   BOOL fOk;

   switch (id) {
   case IDCANCEL:
      EndDialog(hwnd, id); 
      break;

   case IDOK:
      // Construct the pathname of the pipe
      sz[0] = sz[1] = __TEXT('\\');
      GetDlgItemText(hwnd, IDC_SERVER, &sz[2], dimof(sz) - 2);
      lstrcat(sz, __TEXT("\\pipe\\MSJTime"));

      // Attempt to connect to the pipe
      fOk = WaitNamedPipe(sz, NMPWAIT_USE_DEFAULT_WAIT);
      if (fOk) {
         // Get a handle to use to talk to the pipe
         hpipe = CreateFile(sz, GENERIC_READ, 0, NULL, 
            OPEN_EXISTING, 0, NULL);
         fOk = (hpipe != INVALID_HANDLE_VALUE);
      }

      if (fOk) {
         // Valid handle, read time from pipe
         ReadFile(hpipe, &st, sizeof(st), &cbRead, NULL);
         CloseHandle(hpipe);

         // Convert UTC time to client machine's local time and display it
         SystemTimeToTzSpecificLocalTime(NULL, &st, &st);

         GetDateFormat(LOCALE_USER_DEFAULT, DATE_LONGDATE, &st, 
            NULL, sz, dimof(sz));
         SetDlgItemText(hwnd, IDC_DATE, sz);

         GetTimeFormat(LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE, &st, 
            NULL, sz, dimof(sz));
         SetDlgItemText(hwnd, IDC_TIME, sz);

      } else {
         LPCTSTR pszError = (GetLastError() == ERROR_FILE_NOT_FOUND) 
            ? __TEXT("Service not found") : __TEXT("Service busy");

         SetDlgItemText(hwnd, IDC_DATE, pszError);
         SetDlgItemText(hwnd, IDC_TIME, pszError);
      }
      break;
   }
}

//////////////////////////////////////////////////////////////////////////////

BOOL WINAPI MSJTimeClient_DlgProc(HWND hwnd, UINT uMsg, 
   WPARAM wParam, LPARAM lParam) {

   switch (uMsg) {
      chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, MSJTimeClient_OnInitDialog);
      chHANDLE_DLGMSG(hwnd, WM_COMMAND, MSJTimeClient_OnCommand);
   }
   return(FALSE);
}

//////////////////////////////////////////////////////////////////////////////

int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstExePrev, 
   LPSTR pszCmdLine, int nCmdShow) {

   return(DialogBox(hinstExe, MAKEINTRESOURCE(IDD_MSJTIMECLIENT), NULL, 
      MSJTimeClient_DlgProc));
}

//////////////////////////////// End Of File /////////////////////////////////

      При запуске клиентского приложения, Вы увидите диалоговое окно (рис.2)

MSJTimeClient1.JPG
рис.2 Диалоговое окно клиентского приложения

      Чтобы увидеть как взаимодействуют клиент с сервером, Вы должны вписать имя сервера в окно для ввода текста. Если Вы запускаете сервис и клиентское приложение на одном компьютере, вместо имени сервера впишите точку, как показано на рис.3.

MSJTimeClient2.JPG
рис.3 Диалоговое окно клиентского приложения

      При нажатии кнопки "Request Server's Time", клиентское приложение вызывает функцию WaitNamedPipe, которая соединяет клиента с сервером - это заставляет сервис "проснуться" для обработки клиентского запроса. Если же сервер не запущен, то вызов функции WaitNamedPipe завершится неудачей и вместо значений даты и времени отобразится текст "Service not found". Не забудьте запустить MSJ Time сервис, используя SCM-апплет.

      Если сервис запущен и работает, функция WaitNamedPipe возвращает TRUE и, после этого, клиент вызывает функцию CreateFile, чтобы получить данные из канала. Затем клиент выполняет синхронный вызов функции ReadFile, который позволяет ему ждать окончания передачи данных по каналу. Получив данные из канала, клиент закрывает идентификатор канала, конвертирует полученное от сервиса значение даты и показывает его в диалоговом окне (рис.3).

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

Перевод: Роман Панышев (irrona)


© 2005 ironahot@idknet.com - при использовании статей просьба делать ссылку на автора