Сообщений 54 Оценка 535 [+0/-1] Оценить |
Общие особенности служб Подробности программирования Мелочи Код |
Наша служба и опасна и трудна И на первый взгляд как будто не видна Ю. Энтин
В статье описаны некоторые детали, относящиеся к программированию служб Windows NT/2000/XP. Она не претендует на полноту или уникальность. Кое-что не охвачено, многое (хотя и не всё) из охваченного вы сможете найти в MSDN или другой литературе. Если вы написали свою первую службу и хотите двигаться дальше, эта статья вам поможет.
Для понимания написанного ниже вы должны быть знакомы со службами. Глубоких знаний не потребуется, достаточно представлять себе архитектуру службы (с точки зрения программиста) и помнить примерное предназначение нескольких API-функций.
Большая часть содержащихся в статье утверждений описывает реакцию Windows на какие-то действия со стороны службы. Полноценная проверка таких утверждений не представляется возможной. Тем более что некоторые из них не документированы.
Я поступил так:
В этой части статьи разобраны вопросы, имеющие непосредственное отношение к любой службе. Разделение на «непосредственные» и «косвенные» условно и субъективно. Принцип, которого я придерживался, таков: если проблема/возможность свойственна службам из-за особенностей их архитектуры, она описана в этой части. Иначе – в следующей.
Работа с любой программой начинается с установки и заканчивается удалением. Службы – не исключение. Отличие состоит в том, что при установке службу необходимо зарегистрировать. Можно, конечно, возложить эту задачу на инсталлятор, но, по-моему, правильней и проще писать службы, умеющие устанавливаться/удаляться в полуавтоматическом режиме.
ПРИМЕЧАНИЕ На всякий случай: некоторые умные люди, которые знают, как правильно писать инсталляторы, считают, что в этом вопросе я заблуждаюсь. |
Например, так:
int _tmain(int argc, TCHAR* argv[]) { // Если в командной строке что-то есть - // предположительно, запускает пользователь.if (argc == 2) { // lstricmp - сравнение без учёта регистра. if (lstrcmpi(argv[1], TEXT("/install"))==0) { CmdLine::Install(); } elseif (lstrcmpi(argv[1], TEXT("/uninstall"))==0) { CmdLine::Uninstall(); } else { CmdLine::DisplayHelp(); } return 0; } ... |
ПРИМЕЧАНИЕ TEXT() и _tmain – для поддержки Unicode (а можно сказать «для поддержки ANSI»). Подробнее в разделе «Unicode». CmdLine – пространство имён. Я их нежно люблю и часто использую. Вообще-то, то, что в командной строке «что-то есть» ничего не доказывает, см. «Мелочи». |
Функции, выполняющие собственно установку/удаление, выглядят примерно так:
void CmdLine::Install() { открываем SCM (OpenSCManager) создаём службу (CreateService) закрываем всё, что открыли } void CmdLine::Uninstall() { открываем SCM (OpenSCManager) открываем службу (OpenService) удаляем службу (DeleteService) закрываем всё, что открыли } |
На некоторых этапах выполнения служба должна выполнить определённые действия за определённый срок. В MSDN с разной степенью конкретности перечислены пять требований. В книге Джеффри Рихтера и Джейсона Кларка «Программирование серверных приложений в Windows 2000» приведено шестое. Ниже перечислены сами требования и мои комментарии к ним.
Если ваша служба быстро инициализируется и мгновенно обрабатывает сообщения, в результате чего автоматически удовлетворяет всем пунктам, вам повезло. Если нет, можно использовать несколько потоков. Например, так:
DWORD WINAPI SendPending(LPVOID dwState) { sStatus.dwCheckPoint = 0; sStatus.dwCurrentState = (DWORD) dwState; sStatus.dwWaitHint = 2000; for (;;) { if (WaitForSingleObject(eSendPending, 1000)!=WAIT_TIMEOUT) break; sStatus.dwCheckPoint++; SetServiceStatus(ssHandle, &sStatus); } sStatus.dwCheckPoint = 0; sStatus.dwWaitHint = 0; return 0; } |
Уведомления посылаются с помощью функции SetServiceStatus. sStatus – глобальная переменная типа SERVICE_STATUS, описывающая состояние службы, в dwState передаётся состояние, о котором необходимо сообщать, eSendPending – событие, установка которого означает окончание работы этого потока.
Забавно, что при таком подходе для служб, запускаемых вручную, видимый результат не меняется (см. ниже о нарушениях во время инициализации).
Идея в том, что, если обработка сообщения может затянуться, функция Handler[Ex] инициирует её и завершается, не дожидаясь окончания. Если рабочий поток службы в цикле ожидает каких-то событий, обработку может выполнить он, Handler[Ex] должна только проинформировать его о приходе сообщения, если рабочий поток постоянно занят, можно породить ещё один поток. При подобной реализации необходимо учесть, что следующее сообщение может прийти в течение обработки предыдущего, то есть до того, как служба пошлёт уведомление об окончании обработки. С помощью Services этого не сделать, но пользователь может использовать утилиту Net.exe (синтаксис запуска: net команда имя_службы) или написать свою.
Ограничения, накладываемые требованиями (5) и (6) обойти не удаётся. Но, в отличие от (5), в (6) момент посылки уведомления о завершении регулируете вы. Поэтому можно выполнять всю необходимую очистку/сохранение/ещё что-то заранее.
Теперь о «нарушениях в процессе инициализации». Варианты нарушений:
Во всех перечисленных случаях реакция системы будет одинаковой. А именно:
A) Служба запускается автоматически.
B) Служба запускается вручную из Services.
В любом случае служба, в конце концов, запустится.
Эта информация не очень важна (и, кстати, не документирована), так как даже таких нарушений лучше не допускать. Но представлять, что будет, если по каким-то причинам, ваша служба слегка притормозит, полезно.
Этот вопрос возник у меня, когда я писал свою первую службу. Если чётче сформулировать, то звучит он так: который из потоков можно использовать в качестве рабочего? На первый взгляд задействовано три потока: один исполняет main/WinMain, второй – ServiceMain, третий – Handler[Ex] (не совсем так, см. «Мелочи»). Очевидно, что первый и третий потоки не подходят. Про второй поток ничего не известно и, вполне возможно, функция ServiceMain должна возвращать управление. Я поступил просто: создал в ServiceMain дополнительный поток, который выполнял работу. Окончание функции выглядело так:
...
// Создаёт рабочий поток и возвращает управление
Begin();
}
|
Это работает. Никаких дополнительных проблем при таком подходе не обнаружено.
После внимательного прочтения MSDN выяснилось, что вообще-то для работы предназначен поток, выполняющий ServiceMain. Более того, в описании написано: «A ServiceMain function does not return until its services are ready to terminate.» Возвращать управление из ServiceMain сразу рекомендуется только службам, не нуждающимся в потоке для выполнения работы (например, вся работа может заключаться в реакции на сообщения). Я советую следовать рекомендациям Microsoft.
Если ваша служба успешно выполнила свою миссию или, наоборот, окончательно провалилась (неважно, во время выполнения или инициализации), её нужно завершить. Несколько вариантов того, «как делать не надо»:
А теперь о том, как надо. Об окончании работы служба должна сообщить. Как обычно, для сообщения об изменении состояния используется функция SetServiceStatus. В данном случае из всех полей передаваемой в неё структуры SERVICE_STATUS интерес представляют dwCurrentState, dwWin32ExitCode и dwServiceSpecificExitCode. dwCurrentState в любом случае должно быть установлено в SERVICE_STOPPED, значения остальных зависят от ситуации.
Если для завершения службы необходимо выполнить продолжительные действия, в процессе их выполнения имеет смысл посылать уведомления SERVICE_STOP_PENDING. Но это не обязательно.
Ещё один тонкий момент: что будет с вашей службой после вызова SetServiceStatus? Все потоки прекратят исполняться сразу и окончательно, или им дадут «умереть естественной смертью»? Я попытался выяснить это, и получил следующее (это верно для любых вариантов завершения службы, при которых вызывается SetServiceStatus с соответствующими параметрами, кроме случая с SERVICE_CONTROL_SHUTDOWN):
ПРЕДУПРЕЖДЕНИЕ Так как ServiceControlDispatcher возвращает управление в main/WinMain сразу после вызова SetServiceStatus, main/WinMain может вызвать ExitProcess раньше, чем ваш рабочий поток (или потоки, если у вас их несколько) закончат выполнение. В результате, например, могут оказаться невызванными деструкторы стековых объектов. Чтобы избежать этого можно поступить так: - получить описатель рабочего потока (например, с помошью DuplicateHandle) и сохранить его в глобальной переменной - в main/WinMain дождаться завершения рабочего потока Другие (но тоже печальные) возможные последствия преждевременного вызова ExitProcess описаны в MSDN в Q201349: "PRB: ServiceMain thread May Get Terminated in DllMain() when a Service Process Exits Normally". Большое спасибо Dima2 за это замечание. |
В один exe-файл можно поместить несколько служб. Название раздела характеризует моё отношение к таким проектным решениям. Это затрудняет кодирование и отладку, а единственный известный мне выигрыш – экономия ресурсов на компьютере пользователя (если вы пишете несколько зависимых друг от друга служб, наверное, появляются и другие выигрыши; я этим ни разу не занимался). Но, тем не менее, на моей машине в service.exe находятся службы Alerter, AppMgmt, Browser, Dhcp, dmserver, Dnscache, Eventlog, lanmanserver, lanmanworkstation, LmHosts, Messenger, PlugPlay, ProtectedStorage, seclogon, TrkWks, W32Time и Wmi. Вряд ли их писали люди глупее меня.
Интерактивности в службах следует избегать. Службы предназначены для непрерывной работы в отсутствии пользователей, поэтому дожидаться, пока оператор нажмёт «OK», можно очень долго. Но, тем не менее, возможности есть.
ПРЕДУПРЕЖДЕНИЕ Учтите, что все описанные в этом разделе методы (а о существовании других я не слышал) позволяют взаимодействовать только с консольной сессией (службы запускаются в консольной сессии, так как в ней запускается SCM). Поэтому интерактивная (в широком смысле этого слова) служба будет некорректно работать в Windows NT/2000 Terminal Service и, что гораздо более важно, в Windows XP при использовании возможности Fast User Switching. То есть, всё будет корректно, но, возможно, не совсем так, как вы ожидали. Рекомендую почитать про Terminal Service (это круто!) и никогда не использовать интерактивность в службах. |
Самое простое – отобразить сообщение (MessageBox). Это может любая служба, какие бы флаги не стояли. Для этого нужно в функцию MessageBox[Ex] помимо прочих флагов передать MB_SERVICE_NOTIFICATION или MB_DEFAULT_DESKTOP_ONLY. Первый флаг заставит функцию вывести сообщение на экран, даже если пользователь ещё не вошёл в систему. Выглядит забавно. Представьте: на экране приглашение ввести пароль и десяток сообщений, поздравляющих пользователя с 1 апреля. Но для этого придётся написать десять служб, так как процесс не может отображать на экране несколько таких сообщений одновременно, судя по всему, они ставятся в очередь (к MB_DEFAULT_DESKTOP_ONLY это тоже относится). Если установлен второй флаг, сообщение появится только на «нормальном» рабочем столе. Более строго, MB_SERVICE_NOTIFICATION заставляет сообщение появиться на текущем активном desktop-е, а MB_DEFAULT_DESKTOP_ONLY только на «нормальном». Этими флагами можно пользоваться, если определен макрос _WIN32_WINNT и его значение больше или равно 0x0400.
ПРИМЕЧАНИЕ Для реализации этой возможности привлечены неслабые средства. В Spy++ видно, что окна (MessageBox) принадлежат одному из потоков CSRSS.EXE. Это имеет забавный побочный эффект: сообщение может висеть на экране даже после завершения приложения. Соберите и запустите такую программку: . #define _WIN32_WINNT 0x0500 #include <windows.h> int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { MessageBox(NULL, "try to kill me", "undead", MB_SERVICE_NOTIFICATION); return 0; } . А теперь попробуйте убить процесс из Task Manager’а. Если сделать несколько потоков, можно проверить ExitProcess, она в этой ситуации тоже не помогает. |
Если пользовательский интерфейс вашей службы должен быть богаче, существует два выхода. В первом случае должны выполняться следующие условия:
Если всё это так, служба может выводить на экран что угодно. Иначе, служба может попробовать самостоятельно открыть и использовать нужный ей desktop. Подобнее об объектах «desktop» и «window station» смотрите в MSDN.
Ниже рассмотрены вопросы, имеющие косвенное отношение к службам, но прямое к их программированию. То есть, при разработке вашей конкретной службы всё это может вам и не понадобиться, но вообще разбираться в этом надо. Большинство тем имеют отношение не только к службам, но и к обычным приложениям. Рассмотрение не очень подробно, но введение дано (кроме вопроса о системе безопасности).
Есть несколько причин, по которым отлаживать службы сложнее, чем обычные приложения.
Однако чаще всего удаётся написать приложение, полностью воспроизводящее рабочую часть службы, отладить его и поместить в обёртку из некоторого стандартного кода, превращающего его в службу. Если ядро приложения отлажено, и код, реализующий стандартные функции службы, проверен, при стыковке больших проблем быть не должно. Но (как это ни странно) появляются средние и мелкие. Помимо «отладки без отладчика» остаётся следующее.
При отладке кода запуска следует помнить, что ограничение на время (30 секунд) никуда не исчезает, если вы не уложитесь, служба завершится.
Обычно у службы есть какие-то параметры, которые можно настраивать. Иногда нужно иметь возможность определять и корректировать текущее состояние службы.
ПРИМЕЧАНИЕ Изменение состояния и изменение параметров – это не одно и то же. Например, вы написали какую-то сетевую службу. Если есть возможность изменять состояние, администратор может, просмотрев список текущих соединений, какие-то из них разорвать. Изменением параметров этого не достигнуть. |
Остановимся на случае не интерактивной службы, так как в интерактивной службе проблему можно решить так же, как и в обычном приложении.
Для администрирования пишется отдельное приложение (далее – конфигуратор), которое каким-то образом взаимодействует со службой. Могу предложить следующие варианты:
Это очень большая тема, про которую я очень мало знаю. Но она имеет прямое отношение к службам, если вы хотите заниматься их разработкой, вам (и мне, естественно) придется с ней разобраться.
Интерактивность – это нехорошо. Но служба должна каким-то образом сообщать администратору об ошибках и других важных событиях. Службам, генерирующим несколько десятков (или больше) сообщений в день, целесообразно использовать для этого файлы в формате какой-нибудь СУБД. Для остальных лучшее решение – Event Log. Он достаточно гибок, стандартен, администратор может просмотреть его утилитой Event Viewer.
ПРИМЕЧАНИЕ Event Log может использоваться любым приложением. Поскольку статья посвящена службам, я использую слово «служба». Возможности Event Log-а не исчерпываются тем, что описано в этом разделе, но обычно ничего большего и не требуется. Если вам нужно больше, советую обратиться к уже упоминавшейся книге Джеффри Рихтера и Джейсона Кларка «Программирование серверных приложений в Windows 2000» или к MSDN. |
Если вы хотите записать сообщение в Event Log, нужно:
Файл сообщений – это exe- или dll-файл, содержащий ресурс «таблица сообщений». Для получения нужно скомпилировать message compiler’ом (mc.exe) правильно написанный текстовый файл (ниже), получившийся файл ресурсов включить в проект и скомпоновать. Mc.exe не интегрирован в Visual Studio и запускается из командной строки. Ключами можно не пользоваться, достаточно написать «mc filename.mc». На выходе будет filename.h, filename.rc, filenameXXXX.bin (в некоторых случаях несколько штук). filename.rc – тот самый файл ресурсов, он ссылается на filenameXXXX.bin. filename.h содержит идентификаторы сообщений и используется службой.
«Правильно написанный текстовый файл» имеет следующую структуру:
[Заголовок] сообщение_1 ... сообщение_N |
Заголовок необязателен, он может отсутствовать или присутствовать частично. Обычно используется только поле LanguageNames. Синтаксис несложен:
LanguageNames = (обозначение_для_языка_1 = LangID_1 : имя_bin_файла_1) ... LanguageNames = (обозначение_для_языка_n = LangID_n : имя_bin_файла_n) |
«обозначение_для_языка» и «имя_bin_файла» могут быть произвольными, таблица LangID есть в MSDN (смотрите GetLocalInfo()).
Смысл этого поля – перечисление поддерживаемых языков. Если файл сообщений поддерживает несколько языков, в разных локализациях Windows (русской, английской, ...) Event Log будет отображать разные версии сообщений.
Само сообщение выглядит так:
MessageId = 0x1 Severity = Success Facility = Application SymbolicName = MSG_COOL_HACK Language = English Hi! I hack you! You login: %1, you password: %2. . Language = Russian Привет! Я тебя взломал! Твой логин: %1, твой пароль: %2. . |
Поля «MessageId», «Severity», «Facility» и «SymbolicName» составляют заголовок сообщения.
MessageId | Идентификатор сообщения. Не обязателен, в случае отсутствия инкрементируется MessageId предыдущего сообщения (для первого сообщения MessageId – 0). |
---|---|
Severity | Тип сообщения, определены типы «Success», «Informational», «Warning», и «Error», их названия можно переопределить в заголовке, но на название типа и иконку, отображаемые Event Viewer’ом, это не повлияет. Не обязательно, в случае отсутствия наследуется от предыдущего сообщения, по умолчанию – значение «Success». |
Facility | Позволяет задать категорию сообщения. Определены значения «System» и «Application», можно определить (в заголовке файла) ещё около четырёх тысяч. Не обязательно, в случае отсутствия наследуется от предыдущего сообщения, первое сообщение по умолчанию имеет значение «Application». |
SymbolicName | Имя соответствующего сообщению макроса в генерируемом h-файле. |
Тело сообщения начинается после строки «Language = XXXX» и заканчивается строкой, не содержащей ничего, кроме точки и перевода строки. На каждый определённый в заголовке язык должно быть по одному «телу» (если вы не определили ни одного языка, используйте «English»). Вместо «%1» ... «%99» будут вставлены строки, которые приложение передаст при записи сообщения. Учтите, что этот механизм предназначен для передачи имён файлов, IP-адресов, каких-то чисел и т.д. Но не для передачи текста. Можно, конечно, сделать так:
Language = English %1 . |
но, с моей точки зрения, это плохая идея. Дело в том, что в файлах Event Log-а хранится имя источника, номер сообщения, переданные строки и прикреплённые данные, но не сам текст. Поэтому, если записать сообщение, а потом изменить dll или значение параметра EventMessageFile в реестре, текст изменится. Насколько я знаю, это нужно, чтобы, когда пользователь из Китая, у которого всё на китайском, посылает свой файл с логом (лог, описываемый в этом разделе, находится в WinNT\System32\config\AppEvent.Evt) разработчику из Нигерии, тот мог бы, используя свою dll, прочитать те же сообщения на нигерийском.
В ключе реестра HKLM\System\CurrentControlSet\Services\Eventlog\Application\ нужно создать ключ с любым именем (это имя будет именем источника сообщений), в этом ключе создать строковый параметр EventMessageFile и записать в него полный путь к файлу сообщений.
Имя источника сообщений отображается Event Viewer-ом в колонке «Source», оно же используется для получения описателя (handle) при записи в Event Log.
Для начала нужно получить описатель.
ПРИМЕЧАНИЕ На вопрос «описатель чего?» я ответить не могу. Просто описатель.… В MSDN сказано так: «If the function succeeds, the return value is a handle that can be used with the ReportEvent function.» |
Эту операцию выполняет функция RegisterEventSource. Она выглядит так:
HANDLE RegisterEventSource( LPCTSTR lpUNCServerName, LPCTSTR lpSourceName ); |
Параметры:
lpUNCServerName | Имя сервера, на котором находится лог. Для записи в лог текущей машины передавайте NULL. |
---|---|
lpSourceName | Имя зарегистрированного источника сообщений. |
Для закрытия описателя используется функция DeregisterEventSource.
BOOL DeregisterEventSource( HANDLE hEventLog ); |
Запись сообщения производит функция ReportEvent.
BOOL ReportEvent( HANDLE hEventLog, WORD wType, WORD wCategory, DWORD dwEventId, PSID pUserSid, WORD wNumOfStrings, DWORD dwDataSize, LPCTSTR* pStrings, LPVOID pRawData ); |
Параметры:
hEventLog | Описатель, полученный от RegisterEventSource. |
---|---|
wType | Тип сообщения, должен совпадать с типом, записанным в файле сообщений. Варианты: EVENTLOG_SUCCESS, EVENTLOG_INFORMATION_TYPE, EVENTLOG_WARNING_TYPE, EVENTLOG_ERROR_TYPE. |
wCategory | Передавайте 0. |
dwEventId | Идентификатор сообщения. Не равен «MessageId». Берётся из создаваемого mc.exe заголовочного файла. |
pUserSid | Передавайте NULL. |
wNumOfStrings | Количество передаваемых строк. |
dwDataSize | Размер передаваемых данных. |
pStrings | Массив строк. Если строк меньше, чем позиций, в лишних позициях будет «%n», где n – номер позиции. |
pRawData | Данные, прикрепляемые к сообщению. |
Unicode – кодировка, в которой одному символу соответствуют два байта. В результате получается 65536 различных символов. Использовать Unicode в программах для Windows NT/2000/XP полезно и просто. Полезно потому, что, во-первых, это повышает производительность (почему – ниже), во-вторых, позволяет вывести на экран или в файл именно то, что вы хотите, независимо от локализации ОС пользователя и, в-третьих, иногда это гораздо удобнее. А просто потому, что вся необходимая поддержка обеспечена.
Большинство API-функций, принимающих в качестве параметров строки, существуют в двух вариантах – ANSI и Unicode. ANSI-вариант имеет суффикс «A», Unicode-вариант – суффикс «W» (от wide – широкий). В Windows NT/2000/XP ANSI-функции просто преобразуют переданные строки в Unicode и вызывают соответствующую Unicode-функцию. Unicode – «родная» кодировка для этих ОС. Для Win 9x «родная» кодировка – ANSI, в ОС этой группы полностью реализовано всего несколько Unicode-функций, остальные сразу возвращают ошибку. Поэтому программа, использующая Unicode, в Windows NT/2000/XP будет работать быстрее, а в Win 9x не будет работать вообще. Поскольку в Win 9x служба всё равно не сможет работать, это не должно вас волновать.
Если вы не сталкивались с Unicode раньше и не изучали заголовочные файлы с объявлениями API-функций, предыдущий абзац может вас озадачить. Скорее всего, вы неоднократно использовали API-функции, принимающие строки и точно помните, что у них не было никаких суффиксов. А оказывается – есть. Ниже приведёна часть файла winbase.h:
WINADVAPI BOOL WINAPI EncryptFileA( LPCSTR lpFileName ); WINADVAPI BOOL WINAPI EncryptFileW( LPCWSTR lpFileName ); #ifdef UNICODE #define EncryptFile EncryptFileW #else#define EncryptFile EncryptFileA #endif// !UNICODE |
Если макрос UNICODE определён, вы будете использовать EncryptFileW, в противном случае – EncryptFileA. Так можно менять используемую версию API-функций. Осталось научиться регулировать тип передаваемой строки. Это тоже несложно, достаточно пользоваться типом TCHAR при объявлении строковых и символьных переменных и заключать соответствующие константы внутрь макроса TEXT. И TCHAR, и TEXT определены в tchar.h. Кроме них, в этом файле определёны макросы для функций стандартной библиотеки С. Например, макрос _tscanf разворачивается или как wscanf, или как scanf, в зависимости от макроса UNICODE.
При последовательном употреблении TCHAR, TEXT, _tscanf,.. можно простым изменением настроек переключаться между ANSI и Unicode версиями проекта. Вряд ли вы будете часто пользоваться такой возможностью, но то, что она есть – хорошо.
ПРИМЕЧАНИЕ Никто не заставляет вас использовать одну и ту же кодировку везде, достаточно просто быть последовательным. Например, модуль, осуществляющий запись в лог-файл, может явно вызывать ANSI-функции (с суффиксом «A») и передавать им char-строки. С таким модулем можно работать, но нужно помнить, что его функциям не стоит передавать TCHAR-строки. Иначе в ANSI-версии проекта это будет работать, а в Unicode-версии даже не скомпилируется. В основной части службы предпочтительнее использовать Unicode. |
Здесь собраны факты, знать которые полезно, но не необходимо.
В качестве примера я написал небольшую службу, конфигуратор и файл сообщений. Служба почти полностью состоит из стандартной (для меня) обёртки вокруг рабочего потока и может использоваться как заготовка. Краткое описание структуры проекта:
Файл | Описание |
---|---|
Stddef.h | Помимо традиционного включения windows.h, содержит объявления следующих макросов: ServiceName – «внутреннее» имя службы; DisplayName – «отображаемое» имя службы; EventSource – имя источника сообщений; MsgFileName – путь к файлу сообщений из корня службы. |
main.cpp | Содержит функцию main – точку входа приложения. Main проверяет командную строку, и в зависимости от её содержимого выполняет следующие действия: /install – пытается инсталлировать службу; /uninstall – пытается удалить службу; что-то иное – выводит справочное сообщение. Если в командной строке ничего нет, предположительно приложение запущено SCM-ом. В этом случае main вызывает функцию для выполнения некой глобальной инициализации, вызывает StartServiceCtrlDispatcher, после возвращения управления вызывает функцию для выполнения глобальной очистки. |
Cmdline.h, cmdline.cpp | Функции, вызываемые при обработке командной строки. Это установка/удаление службы, вывод справочного сообщения. |
Stdfunc.h, stdfunc.cpp | «Стандартные» функции службы. ServiceMain, ServiceHandler и функции, посылающие SCM сообщения типа «процесс идёт». Наружу выставляются ServiceMain (указатель на неё передаётся в StartServiceCtrlDispatcher) и FatalError, используемая для информирования SCM о внезапном (т.е. не вызванном сообщением SERVICE_CONTROL_SHUTDOWN или SERVICE_CONTROL_STOP) завершении работы службы. |
Report.h, report.cpp | Интерфейс к Event Log-у. |
Parameters.h, parameters.cpp | Читает из реестра параметры службы. |
Work.h, work.cpp | Рабочая часть службы. Содержит функции: GlobalInit – глобальная инициализация; GlobalEnd – глобальная очистка;Init – инициализация конкретной службы;Run – функция, выполняющая основную работу;Stop, Pause, Continue, ParametersChanged – вызываются из ServiceHandler при получении соответствующего сообщения от SCM. |
Чтобы создать свою службу, используя этот шаблон, нужно внести следующие изменения:
Файл | Изменение, комментарии |
---|---|
Stddef.h | Изменить значения макросов ServiceName и т.п. |
Cmdline.cpp | Если при установке/удалении необходимо выполнить какие-то специфические действия (записать что-то в реестр, и т.п.), нужно реализовать эти самые действия. |
Stdfunc.cpp | В этот файл вносятся изменения, если служба должна реагировать на некие сообщения от SCM. В этом случае изменения незначительны: в функции ServiceHandler нужно реализовать соответствующие обработчики, а SetServiceStatus будет вызываться с различными флагами. |
Report.h, report.cpp | Поскольку у разных служб разный набор сообщений, в report.h объявляются разные функции. Однако все функции отправки сообщений очень похожи друг на друга, а функции регистрации/дерегистрации источника сообщений просто совпадают. |
Parameters.h, parameters.cpp | Скорее всего, у служб различные параметры, поэтому будут отличаться и интерфейс и реализация. Но обычно все параметры получаются каким-то однотипным путём (в одном ключике реестра), причём этот путь может совпадать в различных службах. Т.е. функция получения конкретного параметра может состоять из вызова некоторой промежуточной функции, одинаковой в разных службах. |
Work.h, work.cpp | В этом файле реализуется основная логика службы.. |
На основе такой заготовки можно достаточно быстро сделать простенькую, но работающую службочку. Если нужно разобраться с тем, как эти самые службы работают, или протестировать какую-либо функцию службы (так как службы работают под необычными учётными записями и с использованием необычных WindowStation, некоторые функции могут работать необычно), это очень удобно.
В реальных проектах службы обычно выступают ещё и в роли COM-серверов. Такие службы я рекомендую писать с использованием "ATL COM Wizard". Если же ваша служба не является COM-сервером, или вы по каким-то причинам не переносите ATL… Что ж, можете попробовать использовать нечто подобное и в настоящем проекте. Можете даже взять мой код. Но, как говорится, код предоставляется as is, к сожалению, я написал его уже после завершения «настоящего» проекта, поэтому «в бою» пока не проверял. Заметите ошибки или пути улучшения – пишите, буду благодарен.
Сообщений 54 Оценка 535 [+0/-1] Оценить |