Информационные технологииStfw.Ru 🔍

Программирование, установка и конфигурирование сервисов Windows NT

Часто у программистов и системных администраторов появляются вопросы на тему служб. Для того, чтобы не искать ответы на разных форумах, появился этот документ. Давайте рассмотрим эти типичные вопросы подробнее. HandleX
🕛 27.10.2006, 11:13
Чем отличаются службы от обычных приложений? Что такое Service Control Manager?

Исполняемый файл службы использует во время работы специальный протокол общения с особой подсистемой Windows NT - Service Control Manager (в дальнейшем SCM). API для «общения» с SCM экспортирует стандартная библиотека Windows advapi32.dll. Подсистема служб загружается в самом начале старта OS из исполняемого файла services.exe, одноимённый процесс вы можете наблюдать в списке активных процессов во всё время работы компьютера. Библиотека advapi32.dll общается с services.exe посредством RPC (Remote Procedure Call), поэтому всё, что вы можете делать со службами на локальной машине, вы можете делать и на удалённой машине по сети, что очень удобно.

SCM достаточно старая подсистема (она появилась ещё в NT3), и интерфейс взаимодействия с ней не менялся до сих пор, исключая мелкие косметические примочки вроде описаний служб, он кажется немножко сыроватым и запутанным. Однако работает надёжно.

Ещё одно отличие службы от «обычной программы» в том, что служба, будучи запущенной, работает и после выхода пользователя из системы - да-да, даже тогда, когда вы видите приглашение «Для входа в систему нажмите Ctrl+Alt+Delete». Поэтому в службы чаще всего оформляют такие программы, которые должны предоставлять некие услуги даже тогда, когда нет вошедших в систему пользователей. Из встроенных служб NT это, к примеру, Spooler (служба печати), Alerter (служба оповещения, часто используемая системными администраторами), Server (позволяет открывать файлы, named pipes и проч. на вашем компьютере другими машинами в сети) и т.п.

Кстати, SCM запускает также и драйверы, поэтому захват власти над ним является лакомой добычей хакеров и вирусов, поскольку именно оттуда они могут запустить драйвер ядра. А драйвер ядра может взять полный контроль над системой.
Cool! А как мне написать драйвер ядра?

Написание драйверов ядра занятие не для слабонервных. Чем-то похоже на сборку кораблика в бутылке с узким горлышком... в тёмной комнате :) И не всякий компилятор сможет это сделать. К примеру, Delphi, даже при создании простейшего исполняемого файла .exe, который запускается и тут же выходит, вставляет в него функции из стандартной дельфийской run-time library «System», в которой есть вызовы Windows user-mode API, и запуск такого «драйвера» вызовет ошибку. На момент написания этой статьи драйверы могут компилировать M$ VC, gcc+MinGW. Итак, для написания драйвера необходимо наличие «правильного» компилятора, DDK и MSDN от M$, в которой эта тема достаточно подробно разъясняется на нескольких сотнях страниц, плюс изрядный запас здорового оптимизма и терпения. Я видел людей, которые начинали изучать программирование в Windows именно с написания драйверов под неё! Вот это молодцы, уважаю! Это не мышью компоненты по форме раскладывать :)
Что будет, если «грохнуть» процесс Services.exe?

Это жизненно важный процесс, его принудительное завершение (отладочные привилегии + TerminateProcess) вызывает разное поведение OS в зависимости от её версии. К примеру, под Win2K и выше появится сообщение о том, что система будет перезагружена в течении 1 минуты. А под NT4 завершение services.exe вызывает труднопонимаемые подвисания пользовательских процессов, сам services.exe остаётся в списке активных процессов (хотя фунция TerminateProcess отрабатывает без ошибки), что говорит о глюках в ядре, а при попытке перезагрузки системы, она перезагружается минут эдак 15... В любом случае работа системы будет парализована до перезагрузки. Скажите, зачем вам это надо? :)
Насколько трудно переделать обычную программу в сервис - вероятно, нужно поддерживать некий специфический интерфейс?

Да, менять прийдётся немало. Поскольку вся работа службы происходит через вызовы SCM API, нужно использовать его, и по возможности, только его. У обычных программ имеется пользовательский интерфейс - оконный или консольный... Если сконфигурировать службу как Interactive Service (есть такой флаг), то можно показать пользовательский интерфейс прямо из самой службы, поскольку при запуске процесса службы он будет подключен к оконной станции вошедшего в систему пользователя... Однако это «плохой тон», поскольку при таком подходе взаимодействие пользователя со службой будет невозможно с другого компьютера. Для сложного взаимодействия (к примеру, для изменения параметров службы «на лету»), лучше вынести пользовательский интерфейс в отдельную программу и использовать какой-нибудь механизм IPC (InterProcess Communications) - будь то Named Pipes, Mailslots, Sockets и т.п., но очень желательно уместиться в рамки тех скудных возможностей, что предоставляет SCM - Start\Pause\Continue\Stop, а также User-Defined коды управления (о них ниже).
Краткий обзор некоторых важных функций API для работы со службами

OpenSCManager() Если вы хотите устанавливать, удалять или контролировать службы в системе (локальной или удалённой), то всё начинается с этой функции, которая возвращает вам дескриптор на SCМ. Самые важные параметры - имя хоста (позволяет подключаться к удалённым серверам) и уровень доступа. Если вы не уверены, что у вас есть административные привилегии, то не следует заказывать доступ SC_MANAGER_ALL_ACCESS, поскольку такой доступ вам система не даст, для экспериментов с правами есть чудесный флаг MAXIMUM_ALLOWED.

CreateService() Позволяет установить новую службу в систему. Вы получите дескриптор на созданную службу, что позволит её запустить (функция StartService) или ещё поконфигурировать (функция ChangeServiceConfig), вообще большинство функций для работы со службами требуют открытый дескриптор на неё. Важные параметры практически все, поскольку они определяют:
hSCManager указывает на SCM системы, в которую будет установлена новая служба;
lpServiceName определяет имя службы;
lpDisplayName определяет имя, которое будет выводится в оснастке управления службы, и применяемое во встроенной команде «net start». Можете иначе называть это как User-Friendly Name;
dwDesiredAccess определяет уровень доступа, который будет иметь полученный после установки службы дескриптор на неё;
dwServiceType оперделяет тип службы. Указывает, что за службу вы ставите - драйвер ядра или файловой системы, просто службу, службу, разделяющую процесс с другой службой, интерактивна ли она.
dwStartType определяет тип запуска службы: SERVICE_BOOT_START, SERVICE_SYSTEM_START для драйверов ядра, SERVICE_AUTO_START - автоматический старт user-mode службы после загрузки OS, SERVICE_DEMAND_START для ручного запуска, и SERVICE_DISABLED для отключенных служб;
dwErrorControl определяет поведение OS при появлении ошибок запуска службы, вплоть до отката базы данных служб в состояние последнего благополучного запуска системы... Для user-mode служб используется флаг SERVICE_ERROR_NORMAL или SERVICE_ERROR_IGNORE;
lpBinaryPathName определяет полный путь к файлу образа службы (*.exe);
lpLoadOrderGroup - если служба отнесена к определённой группе, то пока не будут запущены все службы в группе, другие службы, зависящие от этой группы, стартовать в это время не будут... Понятно обяснил? :-)
lpdwTagId определяет порядок запуска служб в группе, имеет смысл только для драйверов ядра;
lpDependencies определяет список групп или служб, от которых зависит устанавливаемая служба. Иными словами, пока эти службы не будут запущены, устанавливаемую службу невозможно будет запустить;
lpServiceStartName определяет пользователя, от имени которого будет служба запущена. Если указать NULL, то служба будет запускаться под аккаунтом LocalSystem;
Пароль для аккаунта. Для LocalSystem, LocalService и NetworkService должен быть NULL.

OpenService() позволяет получить дескриптор для уже установленной службы. Три параметра - дескриптор на SCM, имя службы, желаемый уровень доступа. С этим всё понятно, но давайте разберём то, каким может быть этот загадочный уровень доступа к службам. Итак, это может быть сумма (логическая операция OR) вот таких констант:
SERVICE_CHANGE_CONFIG позволяет менять параметры установленной службы с помощью ChangeServiceConfig();
SERVICE_ENUMERATE_DEPENDENTS позволяет узнать зависимости данной службы от других служб;
SERVICE_INTERROGATE позволяет немедленно узнать состояние службы с помощью функции ControlService();
SERVICE_PAUSE_CONTINUE позволяет загнать службу в паузу и возвратить её из этого состояния функцией ControlService();
SERVICE_QUERY_CONFIG позволяет посмотреть параметры службы с помощью функции QueryServiceConfig();
SERVICE_START позволяет запустить службу с использованием функции StartService();
SERVICE_STOP позволяет остановить службу с помощи функции ControlService();
SERVICE_USER_DEFINED_CONTROL позволяет отправить службе т.н. User Defined Service Control Code. Может быть полезной штукой для управления своими службами. Отправление такого кода производится при помощи функции ControlService();
SERVICE_ALL_ACCESS - сумма всего вышеперичисленного. Если у вас маловато прав, то система откажет вам в доступе. Поэтому уровень доступа следует запрашивать только тот, который нужен, множество неприятностей в плане отказа работы некоторых программ из-под обычного пользователя часто случались только из-за лени программиста подумать и выполнить операцию OR над несколькими константами. Ещё можно использовать флаг MAXIMUM_ALLOWED, система отдаст вам дескриптор с теми правами, которые вам доступны... или не отдаст вообще.

StartService() позволяет запускать только что установленную службу, или службу, у которой тип запуска SERVICE_DEMAND_START, по открытому дескриптору, при этом передаёт ей указанное количество строковых параметров (если нужно). Здесь всё просто.

QueryServiceConfig() заполняет структуру QUERY_SERVICE_CONFIG параметрами интересующей вас службы. Практически все поля этой структуры идентичны параметрам, указываемым при вызове CreateService().

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

EnumServicesStatus() позволяет перечислить службы в системе, узнать их имена и текущее состояние. Полезна для всяких утилит, показывающих список служб пользователю. Рассмотрим параметры этой функции:
hSCManager - дескриптор на SCM;
dwServiceType позволяет указать, что за службы мы хотим перечислить - драйверы ядра или user-mode, можно и те, и другие (операция OR);
dwServiceState указывает, что нам нужны активные (запущенные) или неактивные (остановленные) службы, можно и те, и другие (операция OR);
lpServices - указатель на буфер памяти (массив), который будет заполнен информацией о запрошенных службах... Массив структур типа ENUM_SERVICE_STATUS, в каждой структуре указатель на ServiceName, DisplayName, ServiceStatus;
cbBufSize - размер в байтах того прокрустова ложа, что вы выделили под массив :)
pcbBytesNeeded - указатель на переменную, в которую будет возвращено реальное кол-во байт, необходимое для перечисления всех служб;
lpServicesReturned - по этому указателю функция возвратит кол-во служб, которое она вам выдала;
lpResumeHandle позволит вам перечислить все службы, не выделяя большое кол-во памяти. В MSDN делают так: выделяют 128 байт памяти (шутка). При первом вызове устанавливают значение переменной по указателю lpResumeHandle в 0. Крутят цикл и перечисляют все службы, доколе она возвращает ошибку вида ERROR_MORE_DATA. Можно сделать всё за две итерации, я обычно так и делаю. Выставляю lpServices = NULL, cbBufSize = 0. Вызываю функцию и получаю нужное кол-во байт в cbBytesNeeded... Ну а дальше дело техники - выделяю память и вызываю функцию повторно с «правильными» параметрами. Всё, нет никакого цикла.

ControlService() позволяет посылать коды управления запущенной службе. Параметры:
hService ;- ранее открытый дескриптор на службу;
dwControl определяет код управления, который будет послан службе. Это может быть SERVICE_CONTROL_STOP, SERVICE_CONTROL_PAUSE, SERVICE_CONTROL_CONTINUE (понятно из названий), а также SERVICE_CONTROL_INTERROGATE (позволяет узнать о состоянии службы в данный момент) и, так называемый USER DEFINED CONTROL CODE, т.е. любое число от 128 до 255 включительно. Служба, получив эти управляющие коды, должна вести себя в соответствии со здравым смыслом программиста :)
pServiceStatus - указатель на структуру SERVICE_STATUS, в которую отобразится состояние службы после полученного ею кода управления.

DeleteService() помечает службу на удаление. Будет удалена из базы SCM после того, как будут закрыты все дескрипторы на неё. Иными словами, служба может быть запущенной и работать, но после этой функции она уже не жилец :)

CloseServiceHandle() освобождает открытый дескриптор на объект службы или на SCM. Желательно не забывать вызывать эту функцию, когда дескриптор становится не нужным.

В Win2K и выше появилась функция ChangeServiceConfig2(), позволяющая установить параметр ServiceDescription. Это такой дополнительный строковый параметр, который выводится в оснастке управления службами, как разъяснение пользователю о том, зачем эта служба нужна и чем она занимается. Также, с помощью этой функции, можно выставить параметры FailureActions, которые указывают системе, что ей делать, если служба сбойнула (неожиданно завершился её процесс без оповещения SCM о завершении работы) - к примеру, система может попробовать перезапустить службу, перезагрузиться, и даже выполнить внешнюю команду, например, отправить SMS администратору по e-mail :)

Итак, мы рассмотрели функции, нужные для работы со службами и их конфигурирования. Напомню, что SCM запускает службы, исходя из того, какие параметры имеются в базе данных служб. Эту базу данных можно найти в реестре по адресу HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services. Там каждый раздел - имя установленной службы. Некоторые могут спросить: «А не проще ли добавлять службы через реестр?» Отвечу вам: нет, не проще. Во-первых, вам прийдётся перезагружать машину после такой «установки» службы, поскольку база грузится в SCM единожды при старте системы. А во-вторых, M$ не гарантирует, что ключи реестра в следующих версиях Windows будут обозначать то же самое, что они обозначают сейчас. Более того, никто не гарантирует, что в следующих версиях эта база вообще будет находиться в реестре. У вирмейкеров и троянщиков особое мнение на этот счёт - анализ подозрительной программы, в которой импортируется вызов CreateService() только усилит подозрения, а так там будет «безобидная» работа с реестром... Но мы знаем, что у этих «товарищей» вообще на многие вещи особое мнение ;-)

Однако вернёмся к user-mode службам, а именно к той части API, которую они используют для работы с SCM. Сперва службу нужно написать... Там всё просто, в MSDN есть примеры :) Поскольку службу запускает SCM, он некоторое время ждёт, когда служба вызовет StartServiceCtrlDispatcher(), параметром этой функции является массив из записей вида «Имя службы - адрес входа в процедуру MainServiceProc()», из чего делаем вывод, что в одном исполняемом файле службы может содержаться несколько служб с разными именами. Возврат из StartServiceCtrlDispatcher() происходит только после остановки всех прописанных в этом вызове служб. Если StartServiceCtrlDispatcher() возвращает false, значит что-то не так, и нам поможет GetLastError() :)

В конце концов, если всё в порядке, SCM запускает новый поток и передаёт управление на ServiceMain(). В этой функции программист должен первым делом зарегистрировать свою функцию Handler() через функцию API RegisterServiceCtrlHandler(), она возвратит дескриптор, используя который, служба будет «отчитываться» SCM о своём состоянии и о своих ошибках, а в Handler() SCM будет бросать сообщения о том, что нужно делать службе - перейти в паузу, выйти из паузы, остановиться и прочее - т.е. те команды, которые указывают при вызове ControlService() «снаружи».

Необходимо сказать о многопоточности. Handler() и ServiceMain() исполняются в отдельных потоках, это нужно учитывать. Но это и кое-что облегчает, часто программисты, для перевода службы в паузу, просто исполняют SuspendThread() для потока ServiceMain(), а с помощью ResumeThread() восстанавливают работу службы. «Монстроидальные» службы, от которых многое зависит, могут по-разному имплементировать своё состояние паузы. К примеру, служба lanmanserver (да-да, та самая, которая «Supports file, print, and named-pipe sharing over the network»), в паузе перестаёт открывать новые файлы, а в остальном продолжает работать как обычно.

Как уже говорилось, служба должна отчитываться SCM о своём состоянии, для этого используется функция API SetServiceStatus(), где в списке параметров есть указатель на структуру SERVICE_STATUS. В этой структуре служба указывает, какие команды она может воспринимать, в каком она состоянии в данный момент находится, если была ошибка, то заполнит поле для кода ошибки. В частности, службу можно запрограммировать так, что остановить её после запуска, или загнать в паузу, будет невозможно, эдакий «неубиваемый» сервис... Единственное, его можно грохнуть через TerminateProcess(), но это отдельная история...

И вот, скелет службы создан, и руки чешутся нажать на кнопку «Run» любимой IDE... Однако спешка нужна... сами знаете где... Теперь службу нужно «прописать» в систему, т.е. или в самой программе службы встроить реакцию на, скажем, параметр командной строки типа -install, либо написать программу-установщик, которая зарегистрирует службу в системе. Делается это, как вы помните, при помощи вызова функций OpenSCManager() и CreateService(), параметры которых мы достаточно подробно рассмотрели.

Ну и наконец, после того, как служба инсталлирована в систему, можно попробовать её запустить... Делается это встроенным в OS «запускателем» командной строки «net start ИмяСлужбы» или через оснастку управления службами.

Отладка служб тоже занятие не для слабонервных :) Однако, есть подсказки от M$ о том, как это делается. Суть метода такова: если служба прописана в систему, но запускается не с помощью SCM, а другой программой (к примеру, отладчиком), то OS терпеливо выжидает (порядка 10 секунд), пока вы судорожно в оснастке управления служб запускаете свою многострадальную... Время ожидания, кстати, часто зависит от расположения планет :) Если же у вас так не получается (у меня кое-когда не получалось), то вы можете сделать отладочную версию службы, которая запустившись, сразу уходит в паузу. Вы подключаетесь к процессу службы отладчиком, и делаете ей Continue через оснастку. Я надеюсь, что к этому времени вы расставите точки останова где следует :) Если вы отлаживаете Handler(), и вывалились в точке останова в отладчик, не следует долго держать Handler() в остановленном состоянии, поскольку SCM может «обидеться» и запишет в логи системы сообщение о том, что «служба не ответила за отведённое ей время», и дальнейшее поведение SCM по отношению к вашей службе будет достаточно непредсказуемым... В тяжёлых, критических по времени случаях, лучше не использовать отладчик, а писать нужную информацию (флаги, переменные) в текстовые логи для дальнейшего анализа.

Бывают сложные ситуации... Есть профессиональные отладчики уровня ядра, к примеру Compuware SoftICE и M$ kernel debugger, которые полностью останавливают систему в точках останова, и получасовое медитирование на переменными (да-да, современные отладчики прекрасно работают с символами и позволяют отлаживать не хуже, чем в том же VC (отладка в исходниках, просмотр локальных переменных, добавление watch-ей и проч.) поможет понять проблему и не вызовет побочных эффектов со стороны SCM. Надеюсь, фортуна вам улыбнётся.
Насколько я понимаю, устанавливать и запускать службы может только администратор?

При вызове функции OpenSCManager() одним из параметров является уровень доступа к службе. По умолчанию, обычный пользователь не может установить службу. А вот дальше доступ, т.е. сможет ли пользователь запустить, остановить или даже удалить службу, задаётся после создания службы при помощи функции SetServiceObjectSecurity(), иныим словами группа администраторов для каждой службы может сконфигурировать любой Список Контроля Доступа (DACL) для любых пользователей и групп в системе. По умолчанию, после установки службы в систему, Windows определяет для неё такой DACL:
SYSTEM: QueryServiceConfig, QueryServiceStatus, EnumerateDependentServices, StartService, StopService, PauseContinueService, InterrogateService, UserDefinedControl, Read_DACL. Интересно, правда? Процесс под LocalSystem не может удалять и конфигурировать службы :)
Администраторы: могут всё;
Прошедшие проверку: QueryServiceConfig, QueryServiceStatus, EnumerateDependentServices, InterrogateService, UserDefinedControl, Read_DACL. Объясняю по русски: обычный пользователь может перечислить службы (зависимые), прочитать ACL для неё, посмотреть её конфигурацию и в каком она состоянии, послать UserDefinedControl code. Но запустить или остановить её он не может, как и удалить;
Опытные пользователи: то же самое, что и «Прошедшие проверку», но могут запустить и остановить службу, загнать её в паузу.

По просьбам некоторых товарищей, приведу пример небольшой программы, которая выводит в консоль DACL для любой службы.
program ShowServiceDacl;
{$APPTYPE CONSOLE}
uses SysUtils, Windows, WinSVC;

Var scHndl, sHndl: THandle;
 Function toOEM(aStr: String): String; Begin Result := aStr; UniqueString(Result); ANSItoOEM(PChar(Result), PChar(Result)); End;
 Function GetServiceNameByDisplayName(aDispName: String): String; Var bufSize: DWORD; Begin Result := ''; bufSize := 0; GetServiceKeyName(scHndl, PChar(ParamStr(1)), Nil, bufSize); If GetLastError <> ERROR_INSUFFICIENT_BUFFER Then RaiseLastWin32Error; SetLength(Result, bufSize Div 2); If Not GetServiceKeyName(scHndl, PChar(ParamStr(1)), PChar(Result), bufSize) Then RaiseLastWin32Error; End;

//Эти объявления взяты из winnt.h, в станд. библиотеках Delphi этого нет :(
Const ACCESS_ALLOWED_ACE_TYPE = 0; ACCESS_DENIED_ACE_TYPE = 1;
Type _ACE_HEADER = Packed Record AceType, AceFlags: BYTE; AceSize: WORD; End; ACE_HEADER = _ACE_HEADER; PACE_HEADER = ^ACE_HEADER;
 _ACCESS_ALLOWED_ACE = Record Header: ACE_HEADER; Mask: ACCESS_MASK; SidStart: DWORD; End; PACCESS_ALLOWED_ACE = ^_ACCESS_ALLOWED_ACE;
 _SID = Record Revision: BYTE; SubAuthorityCount: BYTE; IdentifierAuthority: SID_IDENTIFIER_AUTHORITY; SubAuthority: DWORD; End; TSID = _SID; PSID = ^TSID;
 Function SidToStr(aSID: PSID): String; Var AuthID: PSidIdentifierAuthority; SubAuthCount: PUCHAR; SubAuth: PDWORD; J: Integer; begin Win32Check(IsValidSID(aSID));
 Result := 'S-' + IntToStr(aSID.Revision) + '-';
 AuthID := GetSidIdentifierAuthority(aSID); Result := Result + IntToStr(AuthID.Value[High(AuthID.Value)]) + '-'; For J := High(AuthID.Value) - 1 DownTo Low(AuthID.Value) Do If AuthID.Value[J] <> 0 Then Result := Result + IntToStr(AuthID.Value[J]) + '-';
 SubAuthCount := GetSIDSubAuthorityCount(aSID); For J := 0 To SubAuthCount^ - 1 Do Begin SubAuth := GetSIDSubAuthority(ASID, J); Result := Result + IntToStr(SubAuth^) + '-'; End;
 J := Length(Result); If J <> 0 Then SetLength(Result, J - 1); End;
 Function GetAccountBySID(ServerName: String; aSID: PSID): String; Var nSize, dSize, pUse: DWORD; aName, dName: PChar; Begin If Not IsValidSID(aSID) Then Begin Result := 'Erroneus SID!'; Exit; End; nSize := 0; dSize := 0; If Not LookupAccountSid(PChar(ServerName), aSID, Nil, nSize, Nil, dSize, pUse) Then If GetLastError <> ERROR_INSUFFICIENT_BUFFER Then If GetLastError <> ERROR_NONE_MAPPED Then RaiseLastWin32Error Else Begin Result := SIDToStr(aSID); Exit; End; aName := StrAlloc(nSize); dName := StrAlloc(dSize); Try Win32Check(LookupAccountSid( PChar(ServerName), aSID, aName, nSize, dName, dSize, pUSE)); Result := aName + ' (' + dName + ')'; Finally StrDispose(aName); StrDispose(dName); End; End;

Var TempStr: String; SD: Pointer; aDACL: PACL; anACE: PACCESS_ALLOWED_ACE; bytesNeeded, tempMask: DWORD; DummyFlag1, DummyFlag2: BOOL; J, I: Integer;

Const AccessMaskStringMap: Array[0..31] Of String = ( 'Query Service Config', 'Change Service Config', 'Query Service Status', 'Enumerate Dependent Services', 'Start Service', 'Stop Service', 'Pause\Continue Service', 'Interrogate Service', 'User Defined Service Control', '', '', '', '', '', '', '', 'Delete Service', 'Read DACL', 'Write DACL', 'Write Owner', '', '', '', '', 'Access System ACL', '', '', '', '', '', '', '' );

begin Try If ParamCount < 1 Then Raise Exception.Create('Service DisplayName must be first input parameter!');
 scHndl := OpenSCManager(Nil, Nil, SC_MANAGER_CONNECT); Try TempStr := GetServiceNameByDisplayName(ParamStr(1));
 sHndl := OpenService(scHndl, PChar(TempStr), READ_CONTROL); If sHndl = 0 Then RaiseLastWin32Error; Try GetMem(SD, 1024); If Not QueryServiceObjectSecurity( sHndl, DACL_SECURITY_INFORMATION, SD, 1024, bytesNeeded) Then RaiseLastWin32Error;
 //На этом этапе имеем Security Descriptor (в SD). Начинаем его "ковырять" //Получаем указатель на DACL If Not GetSecurityDescriptorDACL(SD, DummyFlag1, aDACL, DummyFlag2) Then RaiseLastWin32Error;
 WriteLn('DACL information for Service "', ParamStr(1), '"'); WriteLn('ACE''s count: ', aDACL.AceCount);
 If aDACL.AceCount = 0 Then Begin WriteLn('Nothing to show!'); Exit; End;
 WriteLn('Start dumping...'); WriteLn;
 //Вытаскиваем ACE's... For J := 0 To aDACL.AceCount - 1 Do Begin If Not GetACE(aDACL^, J, Pointer(anACE)) Then RaiseLastWin32Error;
 Case anACE.Header.AceType Of ACCESS_ALLOWED_ACE_TYPE: TempStr := 'Allowed'; ACCESS_DENIED_ACE_TYPE: TempStr := 'Denied'; else TempStr := 'Unknown'; End;
 //Выводим пользователя в этом ACE... Writeln( 'Access ', TempStr, ' Ace (at index ', J, ') for user "', toOEM(GetAccountBySID('', @anACE.SidStart)), '"' );
 WriteLn('Access mask:'); //Выводим AccessRights для ACE: For I := 0 To 31 Do Begin //Проверяем каждый бит в 32-битной маске доступа... TempMask := 1 shl I; If (anACE.Mask And TempMask) = TempMask Then WriteLn(' ', AccessMaskStringMap[I]); //Выводим описание бита в маске End; WriteLn; End;
 Writeln('Information for ', aDACL.AceCount, ' ACE''s printed successfully!'); Writeln(toOEM('Благодарности слать на alex_wh@mail.ru')); Write(toOEM('Можно пивом! ;-) Нажмите Enter для выхода...')); ReadLN; Finally CloseServiceHandle(sHndl); End; Finally CloseServiceHandle(scHndl); End; Except On E: Exception Do If Not (E is EAbort) Then Begin WriteLn('Exception Class ' + E.ClassName + ' occured.'); WriteLn('Error message: "' + toOEM(E.Message) + '"'); WriteLn('Program terminated.'); End; End;
end.

Я понимаю, что в низкоуровневое Security API достаточно тяжело залезать начинающемо программисту, но Security требует жертв... а также хороший повод всё-таки залезть туда :) После изучения этой программы может возникнуть другой вопрос: как же менять DACL для службы? Практически также, как и читать. Конструируете Security Descriptor, в котором делаете DACL, какой вам нужен, и вызываете SetServiceObjectSecurity() с этим Security Descriptor'ом. Будьте осторожны - неверно выставленные права могут лишить вас доступа к службе, прийдётся удалять её через реестр с последующей перезагрузкой системы.

Если же надо просто подправить AccessMask для существующего пользователя в DACL, можно поступить проще: вытаскиваем SD для службы при помощи QueryServiceObjectSecurity(). Находим DACL. Находим нужный ACE. Правим маску. Вызываем SetServiceObjectSecurity()... Вуаля!

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

После изучения примера у вас могут заскрести кошки: «Всё как-то сложно... Неудобно... :-( Прога, которая у меня и так работает, в трее сидит и никому не мешает, мне только надо, чтобы она не выгружалась при смене пользователя... Может есть другой вариант?» Да, вариант есть, написали его уважаемые программеры от M$ для того, чтобы... лентяи продолжали лентяйничать :-) Программа называется srvany.exe + маленький установщик служб. Суть идеи такая: устанавливаем в систему интерактивную службу srvany.exe. Идёте в реестр, находите ключ реестра для этой вновь созданной службы. Там добавляете подключ «Parameters», в котором указываете, что за программу (вашу) должна запустить эта служба + дополнительные параметры командой строки для вашей программы (если надо). Запускаете службу srvany, которая стартует вашу программу. Возможные подводные камни:
При остановке службы программа будет «убита» через TerminateProcess(). А службы останавливаются при остановке системы. Т.е. чаще всего ваша программа будет «завершаться» именно таким образом. Для некоторых программ это неприемлемо.
При завершении сеанса (когда пользователь выходит из системы), окно вашей программы получит WM_QUERYENDSESSION. Некоторые программы в ужасе завершаются от этого сообщения... :)
Если ваша программа должна сидеть значком в трее, и тип запуска службы srvany вы установили как автоматический, то ваша программа будет запущена ещё до входа пользователя в систему. Соответственно, Explorer в режиме рабочего стола ещё не запущен, и значок, который программа будет пытаться зарегистрировать в трей, после входа пользователя в систему в трее так и не появится.
То же самое с треем и при смене пользователя - значок, зарегистрированный в сеансе предыдущего пользователя, будет «забыт» системой.

Я это всё к тому, что скорее всего некоторые изменения в программе для запуска под srvany делать всё равно прийдётся (реагировать на смены пользователей и т.п.). Так что может оказаться, что проще «до основанья, а затем»... всё-таки переписать программу в службу :)

Успехов на ниве программирования служб, ваш HandleX.
program SimpleServ;
{$APPTYPE CONSOLE}
uses Windows, WinSVC; //Не используем SysUtils - размер *.exe будет ~25 Kb

Const ServicesCount = 1; ServiceName = 'SimpleServ'; DisplayName = 'Simple Service'; ServiceType = SERVICE_WIN32_OWN_PROCESS; ServiceDescription = 'SimpleServ by HandleX... This sample program will help ' + 'you to understand Windows NT Services. ' + 'Mail to alex_wh@mail.ru.';

Var ServThrHndl: THandle = 0; StopEvent: THandle = 0; aServHndl: DWord = 0; aServStatus: SERVICE_STATUS;
 Function IntToStr(Value: Integer): String; //Included we don't use SysUtils Var aSign: Bool; Begin Result := '';
 aSign := Value >= 0; If Not aSign Then Value := -Value;
 Repeat Result := Char(Value - (Value Div 10) * 10 + Byte('0')) + Result; Value := Value Div 10; Until Value = 0;
 If Not aSign Then Result := '-' + Result; End;
 // Helper function for windows error strings //Included because we don't use SysUtils function SysErrorMessage(ErrorCode: Integer): string; var Len: Integer; Buffer: array[0..255] of Char; begin Len := FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM or FORMAT_MESSAGE_ARGUMENT_ARRAY, nil, ErrorCode, 0, Buffer, SizeOf(Buffer), nil); while (Len > 0) and (Buffer[Len - 1] in [#0..#32]) do Dec(Len); SetString(Result, Buffer, Len); UniqueString(Result); ANSItoOEM(PChar(Result), PChar(Result)); If Result <> '' Then Result := IntToStr(ErrorCode) + ' ' + Result; end;
 Procedure ShowInfo; Begin WriteLn; WriteLn(' -=* SIMPLE TRAINING SERVICE BY HandleX *=-'); End;
 Procedure ProcessStartupParams; //Реакция на install, uninstall
 //Устанавливает "описание" для службы, под Win2k и выше Function SetServiceDescription(aSHndl: THandle; aDesc: String): Bool; Const SERVICE_CONFIG_DESCRIPTION: DWord = 1; Var DynChangeServiceConfig2: Function( hService: SC_HANDLE; // handle to service dwInfoLevel: DWORD; // information level lpInfo: Pointer): Bool; StdCall; // new data aLibHndl: THandle; TempP: PChar; Begin aLibHndl := GetModuleHandle(advapi32); Result := aLibHndl <> 0; If Not Result Then Exit; DynChangeServiceConfig2 := GetProcAddress(aLibHndl, 'ChangeServiceConfig2A'); Result := @DynChangeServiceConfig2 <> Nil; If Not Result Then Exit; TempP := PChar(aDesc); //ChangeServiceConfig2 хочет указатель на указатель строки Result := DynChangeServiceConfig2(aSHndl, SERVICE_CONFIG_DESCRIPTION, @TempP); End;
 Type TToDo = (tdError, tdInstall, tdUninstall); TToDo_s = Set of TToDo; Const ParamStrings: Array[tdInstall..tdUninstall] of String = ('install', 'uninstall');
 Function MapParam(aParam: String): TToDo; //Узнаёт из параметра о вашем желании ;-) Var J: TToDo; TempStr: String; Begin Result := tdError; TempStr := aParam; If TempStr[1] In ['/', '-'] Then TempStr := Copy(TempStr, 2, Length(TempStr) - 1); UniqueString(TempStr); CharLower(PChar(TempStr)); For J := Low(ParamStrings) to High(ParamStrings) Do If ParamStrings[J] = TempStr Then Begin Result := J; Exit; End; End;
 Var J: Integer; scHndl, sHndl: THandle; aStatus: TServiceStatus; toDo: TTodo_s; Begin toDo := []; For J := 1 to ParamCount Do Begin Include(ToDo, MapParam(ParamStr(J))); If tdError in toDo Then Begin ExitCode := ERROR_INVALID_PARAMETER; WriteLn('Unknown parameter - ' + ParamStr(J) + '. RTFM, please...'); Exit; End; End;
 If [tdInstall, tdUninstall] <= toDo Then Begin ExitCode := ERROR_INVALID_PARAMETER; WriteLn('Error: you can not install and uninstall service simultaniosly. Check params.'); Exit; End;
 If tdInstall in toDo Then //Устанавливаем сервис Begin Write('Connecting Service Control Manager...'); scHndl := OpenSCManager(Nil, Nil, SC_MANAGER_CREATE_SERVICE); If scHndl = 0 Then Begin ExitCode := GetLastError; WriteLn('Failed!'); WriteLn('Error: ', SysErrorMessage(ExitCode)); Exit; End; Try WriteLn('Ok'); Write('Creating service database record...'); sHndl := CreateService( SCHndl, ServiceName, DisplayName, SERVICE_QUERY_CONFIG Or SERVICE_CHANGE_CONFIG, ServiceType, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, PChar(ParamStr(0)), Nil, Nil, Nil, Nil, Nil);
 If sHndl = 0 Then Begin ExitCode := GetLastError; WriteLn('Failed!'); WriteLn('Error: ', SysErrorMessage(ExitCode)); Exit; End; Try WriteLn('Ok');
 If ServiceDescription <> '' Then Begin Write('Setting service description...'); If Not SetServiceDescription(sHndl, ServiceDescription) Then Begin WriteLn('Failed!'); WriteLn('Warning: ', SysErrorMessage(GetLastError)); WriteLn('Warning: SetServiceDesc() failed, but service is installed!'); End Else WriteLn('Ok'); End; Finally CloseServiceHandle(sHndl); End; Finally CloseServiceHandle(SCHndl); End; WriteLn('Service "', DisplayName, '" install success.'); End;
 If tdUninstall in toDo Then //Удаляем сервис... Begin Write('Connecting Service Control Manager...'); scHndl := OpenSCManager(Nil, Nil, GENERIC_EXECUTE); If scHndl = 0 Then Begin ExitCode := GetLastError; WriteLn('Failed!'); WriteLn('Error: ', SysErrorMessage(ExitCode)); Exit; End; Try WriteLn('Ok');
 Write('Opening and Quering Service...'); sHndl := OpenService(SCHndl, ServiceName, STANDARD_RIGHTS_REQUIRED Or SERVICE_QUERY_STATUS Or SERVICE_STOP); If sHndl = 0 Then Begin ExitCode := GetLastError; WriteLn('Failed!'); WriteLn('Error: ', SysErrorMessage(ExitCode)); Exit; End; Try If Not QueryServiceStatus(sHndl, aStatus) Then Begin ExitCode := GetLastError; WriteLn('Failed!'); WriteLn('Error: ', SysErrorMessage(ExitCode)); Exit; End; WriteLn('Ok');
 If aStatus.dwCurrentState <> SERVICE_STOPPED Then Begin Write('Service is running, wait until stopped...'); If Not ControlService(sHndl, SERVICE_CONTROL_STOP, aStatus) Then Begin ExitCode := GetLastError; WriteLn('Failed!'); WriteLn('Error: ', SysErrorMessage(ExitCode)); Exit; End; While aStatus.dwCurrentState <> SERVICE_STOPPED Do Begin Sleep(250); Write('.'); If Not QueryServiceStatus(sHndl, aStatus) Then Begin ExitCode := GetLastError; WriteLn('Failed!'); WriteLn('Error: ', SysErrorMessage(ExitCode)); Exit; End; End; WriteLn('Stopped'); End;
 Write('Deleting Service...'); If Not DeleteService(sHndl) Then Begin ExitCode := GetLastError; WriteLn('Failed!'); WriteLn('Error: ', SysErrorMessage(ExitCode)); Exit; End; WriteLn('Ok'); Finally CloseServiceHandle(sHndl); End; Finally CloseServiceHandle(SCHndl); End; WriteLn('Service uninstall success.'); End;
 End;
 Function SetState(aState: DWORD): DWORD; Begin aServStatus.dwCurrentState := aState; If aServHndl <> 0 Then SetServiceStatus(aServHndl, aServStatus); Result := aServStatus.dwCurrentState; End;
 Procedure ServiceHandler(fdwControl: DWORD); StdCall; Begin Case fdwControl Of //Requests the service to stop. SERVICE_CONTROL_STOP: Begin SetState(SERVICE_STOP_PENDING); SetEvent(StopEvent); //Если сервис был в паузе, то рабочий поток надо возобновить ResumeThread(ServThrHndl); End; //Requests the service to pause. SERVICE_CONTROL_PAUSE: Begin SetState(SERVICE_PAUSE_PENDING); SuspendThread(ServThrHndl); //Останавливаем рабочий поток сервиса SetState(SERVICE_PAUSED); End; //Requests the paused service to resume. SERVICE_CONTROL_CONTINUE: Begin SetState(SERVICE_CONTINUE_PENDING); ResumeThread(ServThrHndl); //Восстанавливаем рабочий поток сервиса SetState(SERVICE_RUNNING); End; //Requests the service to update immediately its current status //information to the service control manager. SERVICE_CONTROL_INTERROGATE: Begin //Говорим SCM о том, в каком состоянии находится наша служба SetState(aServStatus.dwCurrentState); End; //Протяжно пищим в спикер - кто-то послал USER DEFINED CONTROL CODE. //В реальных применениях может использоваться для подачи различных сигналов //самописным службам. Удобны тем, что передаются даже удалённым хостам //при помощи ControlService() и соотв. дескриптора удалённого SCM. 128..255: Begin SuspendThread(ServThrHndl); Windows.Beep(1000, 500); //Возвращать результаты можно вызовом SetServiceStatus(). aServStatus.dwWin32ExitCode := ERROR_SUCCESS; SetState(aServStatus.dwCurrentState); ResumeThread(ServThrHndl); End; End; End;
 Procedure MainServiceProc( // Каждая служба может иметь параметры запуска... dwArgc: DWORD; // Кол-во параметров - у нас не используется lpszArgv: Pointer); StdCall // Массив параметров - у нас не используется Begin aServHndl := RegisterServiceCtrlHandler(ServiceName, @ServiceHandler); If aServHndl = 0 Then Begin ExitCode := GetLastError; Exit; //Какая-то ошибка, выходим, SCM будет ругаться, но сообщить мы ему ничего не можем... End; ZeroMemory(@aServStatus, SizeOf(aServStatus)); aServStatus.dwServiceType := ServiceType; aServStatus.dwControlsAccepted := SERVICE_ACCEPT_STOP Or SERVICE_ACCEPT_PAUSE_CONTINUE; //Здесь может быть подсказка для небыстрых служб о том, как долго она реагирует на команды //aServStatus.dwWaitHint := 500;
 //Извещаем SCM, что начинается старт службы... SetState(SERVICE_START_PENDING);
 //Пошла процедура инициализации...
 //Получаем реальный дескриптор потока службы... If Not DuplicateHandle( GetCurrentProcess, GetCurrentThread, GetCurrentProcess, @ServThrHndl, 0, FALSE, DUPLICATE_SAME_ACCESS) Then Begin aServStatus.dwWin32ExitCode := GetLastError; SetState(SERVICE_STOPPED); Exit; End;
 //Создаём unnamed event для остановки службы по сигналу из Handler... StopEvent := CreateEvent(Nil, True, False, Nil); If StopEvent = 0 Then //Ошибка создания Event'а - выходим... Begin aServStatus.dwWin32ExitCode := GetLastError; SetState(SERVICE_STOPPED); Exit; End;
 SetState(SERVICE_RUNNING); //Инит прошёл, работаем!
 //Крутим цикл, бипаем по таймауту, если срабатывает event - выходим... //В реальной службе именно здесь может быть конкретная полезная работа While WaitForSingleObject(StopEvent, 500) = WAIT_TIMEOUT Do Windows.Beep(10000, 10); 
 //Выполняем остановку сервиса, вычищаемся... CloseHandle(ServThrHndl); ServThrHndl := 0; CloseHandle(StopEvent); StopEvent := 0;
 SetState(SERVICE_STOPPED); //Извещаем, SCM, что работа службы остановлена... End; //Поток ЭТОЙ службы завершил свою работу.

Var ServTableEntryArray: Array[0..ServicesCount] Of TServiceTableEntryA;

begin
 //Старт программы...
 If ParamCount > 0 Then //От нас что-то хотят... Begin ShowInfo; ProcessStartupParams; //Выясняем что и выходим... Exit; End;
 //Готовимся к вызову StartServiceCtrlDispatcher ZeroMemory(@ServTableEntryArray, SizeOf(ServTableEntryArray)); ServTableEntryArray[0].lpServiceName := ServiceName; ServTableEntryArray[0].lpServiceProc := @MainServiceProc;
 If Not StartServiceCtrlDispatcher(ServTableEntryArray[0]) Then Begin ExitCode := GetLastError; ShowInfo; //Какая-то ошибка, выводим в консоль сообщение и выходим... WriteLn('Error: ', SysErrorMessage(ExitCode)); WriteLn('This program is Windows NT Service, so it CAN NOT be run from command prompt.'); WriteLn('You can install it with "/install" parameter.'); WriteLn('If this service is already installed, you can run it with "net start" command.'); End; //Процесс службы завершает работу, всем до свидания... //Если вы разместили в своём *.exe несколько служб, то здесь //вы окажетесь только после остановки ВСЕХ служб процесса.
end.

Windows   Теги:

Читать IT-новости в Telegram
Информационные технологии
Мы в соцсетях ✉