Сергей Холодилов
Наша служба и опасна и трудна
И на первый взгляд как будто не видна
Ю. Энтин
В статье описаны некоторые детали, относящиеся к программированию служб Windows NT/2000/XP. Она не претендует на полноту или уникальность. Кое-что не охвачено, многое (хотя и не всё) из охваченного вы сможете найти в MSDN или другой литературе. Если вы написали свою первую службу и хотите двигаться дальше, эта статья вам поможет.
Для понимания написанного ниже вы должны быть знакомы со службами. Глубоких знаний не потребуется, достаточно представлять себе архитектуру службы (с точки зрения программиста) и помнить примерное предназначение нескольких API-функций.
Большая часть содержащихся в статье утверждений описывает реакцию Windows на какие-то действия со стороны службы. Полноценная проверка таких утверждений не представляется возможной. Тем более что некоторые из них не документированы.
Я поступил так:
В Windows 2000 Server SP1 я постарался проверить всё. В других версиях только кое-что. Возможно, некоторые полученные факты я истолковал неверно. Но пока что ошибок я не нашёл.
Если утверждение есть в MSDN и/или другом источнике, я проверял его два-три раза, если всё сходилось, считал его верным.
Если утверждение противоречит тому, что написано в MSDN и/или других источниках, продолжительность тестирования зависела от его важности (с моей точки зрения). В этом случае в статье указаны и мои результаты, и информация из MSDN или других источников. Если я считаю утверждение важным, кроме этого указано, какие моменты могли быть упущены во время тестирования. Эта версия статьи не содержит важных спорных утверждений.
Если утверждение не встретилось мне ни в одном источнике, я поступал аналогично предыдущему пункту.
Общие особенности служб
В этой части статьи разобраны вопросы, имеющие непосредственное отношение к любой службе. Разделение на «непосредственные» и «косвенные» условно и субъективно. Принцип, которого я придерживался, таков: если проблема/возможность свойственна службам из-за особенностей их архитектуры, она описана в этой части. Иначе – в следующей.
Установка/удаление
Работа с любой программой начинается с установки и заканчивается удалением. Службы – не исключение. Отличие состоит в том, что при установке службу необходимо зарегистрировать. Можно, конечно, возложить эту задачу на инсталлятор, но, по-моему, правильней и проще писать службы, умеющие устанавливаться/удаляться в полуавтоматическом режиме.
ПРИМЕЧАНИЕ На всякий случай: некоторые умные люди, которые знают, как правильно писать инсталляторы, считают, что в этом вопросе я заблуждаюсь. |
Например, так:
int _tmain(int argc, TCHAR* argv[]) { // Если в командной строке что-то есть - // предположительно, запускает пользователь. if (argc == 2) { // lstricmp - сравнение без учёта регистра. if (lstrcmpi(argv[1], TEXT("/install"))==0) { CmdLine::Install(); } else if (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» приведено шестое. Ниже перечислены сами требования и мои комментарии к ним.
Служба должна вызвать StartServiceCtrlDispatcher не позже, чем через 30 секунд после начала работы, иначе выполнение службы завершится. Практика подтвердила. Кроме того, в раздел Event Log’а System будет добавлена запись об ошибке (источник – «Service Control Manager»). Если служба запускается вручную из программы Services, пользователь получит сообщение (MessageBox).
Функция ServiceMain должна вызвать RegisterServiceCtrlHandler[Ex] немедленно. Что будет в противном случае – не указано. Несоблюдение этого правила – один из случаев «нарушений во время инициализации» (термин мой), описанных ниже в этом же разделе.
Функция ServiceMain должна вызвать SetServiceStatus первый раз «почти сразу» после RegisterServiceCtrlHandler[Ex], после чего служба должна продолжать вызывать её до тех пор, пока инициализация не закончится. Неправильное использование SetServiceStatus – второй случай «нарушений во время инициализации».
При обработке сообщения служба должна вернуть управление из функции Handler[Ex] в течение 30 секунд, иначе SCM сгенерирует ошибку. Практика подтверждает, запись в Event Log добавляется. Но никаких репрессивных действий по отношению к службе я не дождался.
При получении сообщения SERVICE_CONTROL_SHUTDOWN служба должна закончить работу за время, не превышающее число миллисекунд, указанное в параметре WaitToKillServiceTimeout ключа HKLMSystemCurrentControlSetControl, иначе будет завершена принудительно. Практика подтвердила.
После завершения работы в качестве службы (то есть после посылки службой уведомления об этом) процессу даётся 20 секунд на очистку/сохранение/ещё что-то, после этого процесс завершается. Подробнее в разделе «Корректное завершение».
Если ваша служба быстро инициализируется и мгновенно обрабатывает сообщения, в результате чего автоматически удовлетворяет всем пунктам, вам повезло. Если нет, можно использовать несколько потоков. Например, так:
Дополнительный поток выполняет необходимую инициализацию, а основной поток вызывает StartServiceCtrlDispatcher.
Я не смог придумать, зачем делать что-либо до вызова RegisterServiceCtrlHandler[Ex], но если надо, можно сделать так же, как в (1).
Один из потоков посылает уведомления о продвижении процесса, второй выполняет инициализацию. Функция первого потока может быть такой:
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) момент посылки уведомления о завершении регулируете вы. Поэтому можно выполнять всю необходимую очистку/сохранение/ещё что-то заранее.
Теперь о «нарушениях в процессе инициализации». Варианты нарушений:
Задержка перед вызовом RegisterServiceCtrlHandler[Ex].
Задержка перед первым вызовом SetServiceStatus.
Слишком большие паузы между вызовами SetServiceStatus.
Не меняется поле dwCheckPoint структуры, передаваемой SetServiceStatus.
Во всех перечисленных случаях реакция системы будет одинаковой. А именно:
A) Служба запускается автоматически.
Минуты через две (если за это время «нарушение» не прекратится и служба не начнёт работать нормально) в Event Log-е появится запись «The ... service hung on starting.»
Если хоть одна служба «повисла», пользователь получит сообщение «At least one service or driver failed during system startup. Use Event Viewer to examine the event log for details.» Такое ощущение, что это сообщение появляется в тот момент, когда запускается первая «зависшая» служба (сам понимаю, что звучит нелогично, но что делать...).
B) Служба запускается вручную из Services.
Минуты три система подождёт.
Появится сообщение об ошибке.
В программе Services в столбце Status служба будет помечена словом «Starting».
В любом случае служба, в конце концов, запустится.
Эта информация не очень важна (и, кстати, не документирована), так как даже таких нарушений лучше не допускать. Но представлять, что будет, если по каким-то причинам, ваша служба слегка притормозит, полезно.
Кто будет работать?
Этот вопрос возник у меня, когда я писал свою первую службу. Если чётче сформулировать, то звучит он так: который из потоков можно использовать в качестве рабочего? На первый взгляд задействовано три потока: один исполняет 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.
Корректное завершение
Если ваша служба успешно выполнила свою миссию или, наоборот, окончательно провалилась (неважно, во время выполнения или инициализации), её нужно завершить. Несколько вариантов того, «как делать не надо»:
Завершить все рабочие потоки, поток, выполняющий Handler[Ex] не трогать. В этом случае SCM «ничего не заметит» и служба продолжит выполняться. Это не смертельно, но и не очень хорошо, так как ресурсы-то используются.
Завершить все рабочие потоки, поток, выполняющий Handler[Ex] завершить вызовом ExitThread при обработке первого следующего сообщения. SCM генерирует ошибку и добавляет запись о ней в Event Log.
Завершить процесс вызовом ExitProcess. Результат аналогичен предыдущему, даже ошибка такая же. Странно, что код завершения процесса не сохраняется.
А теперь о том, как надо. Об окончании работы служба должна сообщить. Как обычно, для сообщения об изменении состояния используется функция SetServiceStatus. В данном случае из всех полей передаваемой в неё структуры SERVICE_STATUS интерес представляют dwCurrentState, dwWin32ExitCode и dwServiceSpecificExitCode. dwCurrentState в любом случае должно быть установлено в SERVICE_STOPPED, значения остальных зависят от ситуации.
Служба завершилась успешно. dwWin32ExitCode = NO_ERROR, в Event Log ничего записано не будет.
Произошла неисправимая ошибка, и это одна из стандартных ошибок Windows. dwWin32ExitCode = ERROR_..., в Event Log будет добавлена запись, описывающая ошибку, числовое значение ошибки указано не будет.
Произошла неисправимая ошибка, специфичная для вашей службы. dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR, в dwServiceSpecificExitCode код ошибки. Так как систему кодирования ошибок вы придумали сами, расшифровать значение кода можете тоже только вы. В Event Log будет добавлена запись следующего содержания: «The ... service terminated with service-specific error ...» (в местах многоточий – имя службы и код ошибки).
Если для завершения службы необходимо выполнить продолжительные действия, в процессе их выполнения имеет смысл посылать уведомления SERVICE_STOP_PENDING. Но это не обязательно.
Ещё один тонкий момент: что будет с вашей службой после вызова SetServiceStatus? Все потоки прекратят исполняться сразу и окончательно, или им дадут «умереть естественной смертью»? Я попытался выяснить это, и получил следующее (это верно для любых вариантов завершения службы, при которых вызывается SetServiceStatus с соответствующими параметрами, кроме случая с SERVICE_CONTROL_SHUTDOWN):
Если exe-файл содержит несколько служб и хоть одна из них (не считая завершающейся) запущена, ничего интересного не произойдёт. То есть, ни процесс, ни один из потоков не будут завершены насильно.
После завершения последней службы функция ServiceControlDispatcher возвращает управление в main/WinMain. Если main/WinMain самостоятельно заканчивается в течение 20-ти секунд, то, как и у нормального приложения, выполняется функция ExitProcess() и завершаются все потоки.
ПРЕДУПРЕЖДЕНИЕ Так как 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 за это замечание. |
Через 20 секунд после завершения последней службы процесс уничтожается.
Свальный грех
В один 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, она в этой ситуации тоже не помогает. |
Если пользовательский интерфейс вашей службы должен быть богаче, существует два выхода. В первом случае должны выполняться следующие условия:
Служба запущена в контексте безопасности LocalSystem.
Служба должна быть помечена как интерактивная.
Значение параметра NoInteractiveServices ключа HKLMSYSTEMCurrentControlSet ControlWindows должно быть равно 0.
Если всё это так, служба может выводить на экран что угодно. Иначе, служба может попробовать самостоятельно открыть и использовать нужный ей desktop. Подобнее об объектах «desktop» и «window station» смотрите в MSDN.
Подробности программирования
Ниже рассмотрены вопросы, имеющие косвенное отношение к службам, но прямое к их программированию. То есть, при разработке вашей конкретной службы всё это может вам и не понадобиться, но вообще разбираться в этом надо. Большинство тем имеют отношение не только к службам, но и к обычным приложениям. Рассмотрение не очень подробно, но введение дано (кроме вопроса о системе безопасности).
Отладка
Есть несколько причин, по которым отлаживать службы сложнее, чем обычные приложения.
В службе будет как минимум два потока.
Службу запускает SCM.
Если в exe-файле несколько служб, отладка будет ещё неприятнее.
Однако чаще всего удаётся написать приложение, полностью воспроизводящее рабочую часть службы, отладить его и поместить в обёртку из некоторого стандартного кода, превращающего его в службу. Если ядро приложения отлажено, и код, реализующий стандартные функции службы, проверен, при стыковке больших проблем быть не должно. Но (как это ни странно) появляются средние и мелкие. Помимо «отладки без отладчика» остаётся следующее.
Присоединить отладчик к запущенной службе.
Использовать DebugBreak.
В HKLMSOFTWAREMicrosoftWindows NTCurrentVersionImage File Execution Options добавить ключ «имя_исполяемого_файла.exe» (без пути), в нём создать строковый параметр Debugger и записать в него полное имя отладчика. При запуске указанного файла, он будет запущен под отладчиком.
Использовать SoftIce.
При отладке кода запуска следует помнить, что ограничение на время (30 секунд) никуда не исчезает, если вы не уложитесь, служба завершится.
Администрирование
Обычно у службы есть какие-то параметры, которые можно настраивать. Иногда нужно иметь возможность определять и корректировать текущее состояние службы.
ПРИМЕЧАНИЕ Изменение состояния и изменение параметров – это не одно и то же. Например, вы написали какую-то сетевую службу. Если есть возможность изменять состояние, администратор может, просмотрев список текущих соединений, какие-то из них разорвать. Изменением параметров этого не достигнуть. |
Остановимся на случае не интерактивной службы, так как в интерактивной службе проблему можно решить так же, как и в обычном приложении.
Для администрирования пишется отдельное приложение (далее – конфигуратор), которое каким-то образом взаимодействует со службой. Могу предложить следующие варианты:
Параметры хранятся в реестре (обычно в HKLMSystemCurrentControlSetServicesимя_службыParameters). Конфигуратор их изменяет, служба, используя функцию RegNotifyChangeKeyValue, отслеживает эти изменения.
Параметры хранятся где угодно. После их изменения конфигуратор посылает службе сообщение. Для этого нужно открыть службу функцией OpenService с правом доступа SERVICE_USER_DEFINED_CONTROL и послать сообщение функцией ControlService. Конечно, в службе необходимо предусмотреть соответствующую обработку.
Используются именованные каналы, сокеты, DCOM или любое другое средство коммуникации между процессами. В этом случае помимо передачи параметров можно получать и изменять состояние службы.
Самый простой вариант – служба реагирует на изменение параметров только после перезапуска.
Безопасность
Это очень большая тема, про которую я очень мало знаю. Но она имеет прямое отношение к службам, если вы хотите заниматься их разработкой, вам (и мне, естественно) придется с ней разобраться.
Event Log
Интерактивность – это нехорошо. Но служба должна каким-то образом сообщать администратору об ошибках и других важных событиях. Службам, генерирующим несколько десятков (или больше) сообщений в день, целесообразно использовать для этого файлы в формате какой-нибудь СУБД. Для остальных лучшее решение – 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 в реестре, текст изменится. Насколько я знаю, это нужно, чтобы, когда пользователь из Китая, у которого всё на китайском, посылает свой файл с логом (лог, описываемый в этом разделе, находится в WinNTSystem32configAppEvent.Evt) разработчику из Нигерии, тот мог бы, используя свою dll, прочитать те же сообщения на нигерийском.
Регистрация источника сообщений
В ключе реестра HKLMSystemCurrentControlSetServicesEventlogApplication нужно создать ключ с любым именем (это имя будет именем источника сообщений), в этом ключе создать строковый параметр 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
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. |
Мелочи
Здесь собраны факты, знать которые полезно, но не необходимо.
Служба не обязательно является консольным приложением.
В параметре ImagePath ключа HKLMSystemCurrentControlSetServicesимя_службы можно задать командную строку (можно даже «/uninstall»), но, по-моему, этой возможностью лучше не пользоваться.
Начиная с Windows 2000 в параметре Description ключа HKLMSystemCurrentControl SetServicesимя_службы можно задать описание службы. Оно отображается Services в столбце «Description». Для установки этого параметра можно воспользоваться RegSetValueEx или ChangeServiceConfig2. Предпочтительнее пользоваться ChangeServiceConfig2, но проще RegSetValueEx…
Судя по всему, пока служба не вызовет StartServiceCtrlDispatcher, SCM не может запустить следующую. Это ещё одна причина не помещать инициализацию в main/WinMain.
После вызова StartServiceCtrlDispatcher основной поток приложения не простаивает. Как минимум, он исполняет обработчики сообщений всех служб exe-файла. Поэтому «задействовано» не три потока, а два.
Когда функция MessageBox вызывается с флагом MB_SERVICE_NOTIFICATION или MD_SERVICE_DEFAULT_DESKTOP_ONLY, в раздел Event Log’а System добавляется запись. Источник – «Application Popup», внутри – содержимое сообщения. Время создания записи соответствует времени вызова функции MessageBox, а не времени отображения сообщения.
Сообщения могут приходить абсолютно бессистемно. То есть, например, несмотря на то, что ваша служба не стоит на паузе, пользователь может (утилитой net.exe или какой-нибудь своей) отправить ей сообщение SERVICE_CONTROL_CONTINUE. Если в результате ваша служба упадёт, он будет очень рад, но уважения к вам у него не прибавится.
Функция CreateProcessW имеет одну особенность – её второй параметр имеет тип LPWSTR, а не LPCWSTR, причём, если этот параметр будет указателем на константу, произойдет исключение. Несмотря на это, в функцию CreateProcessA можно спокойно передавать указатель на константу, так как при преобразовании из ANSI в Unicode она выделяет буфер и передаёт CreateProcessW указатель на него.
Код
В качестве примера я написал небольшую службу, конфигуратор и файл сообщений. Служба почти полностью состоит из стандартной (для меня) обёртки вокруг рабочего потока и может использоваться как заготовка. Краткое описание структуры проекта:
Файл | Описание |
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, к сожалению, я написал его уже после завершения «настоящего» проекта, поэтому «в бою» пока не проверял. Заметите ошибки или пути улучшения – пишите, буду благодарен.