ПРОГРАММИРОВАНИЕ    НА    V I S U A L   C + +
РАССЫЛКА САЙТА       
RSDN.RU  

    Выпуск No. 74 от 5 мая 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

Здравствуйте, дорогие подписчики!


 CТАТЬЯ

Написание служб Windows NT/2000

Автор: Сергей Холодилов

Я предполагаю, что вы представляете себе, что такое служба и слышали о том, как их писать.

Об утверждениях и тестировании

Эта статья содержит много разных утверждений. Некоторые из них описывает реакцию Windows на какие-то действия со стороны службы. Полноценная проверка таких утверждений нереальна (для меня), так как, во-первых, Windows большая и исходников у меня нет, во-вторых, надо проверять реакцию всех версий Windows, поддерживающих службы, а это линейка Windows NT, начиная с версии 3.1.

Я поступил так:

  1. В Windows 2000 Server SP1 я проверил всё. В других версиях только кое-что.
  2. Если утверждение есть в MSDN и/или другом источнике, я проверял его один/два/три раза, если всё сходилось, считал его верным. Если не сходилось, переходим к пункту (3).
  3. Если утверждение противоречит тому, что написано в MSDN и/или других источниках, продолжительность тестирования зависела от его важности (с моей точки зрения). В этом случае в статье указаны и мои результаты и информация из MSDN/других источников. Если я считаю утверждение важным, кроме этого указано, какие моменты я мог упустить во время тестирования.
  4. Если утверждение не встретилось мне ни в одном источнике, я поступал аналогично (3).

Поэтому, в том, что касается реакции Windows, написанное ниже только приближение.

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

Работа с любой программой начинается установкой и заканчивается удалением. Службы не исключение. Отличие состоит в том, что при установке службу необходимо зарегистрировать. Можно, конечно, возложить эту задачу на инсталлятор, но, по-моему, правильней и проще писать службы, умеющие устанавливаться/удаляться в полуавтоматическом режиме. Например, так:


...
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;
	}
...

Комментарий к листингу:

  1. TEXT() и _tmain - для поддержки Unicode. Это не обязательно, но так лучше.
  2. CmdLine - пространство имён. Я их нежно люблю и часто использую.
  3. Вообще-то, то, что в командной строке <что-то есть> ничего не доказывает, см.<Подробности>.

Функции, выполняющие собственно установку/удаление выглядят примерно так:


void CmdLine::Install()
{
	открываем SCM (OpenSCManager())
	создаём службу (CreateService())
	закрываем всё, что открыли
}

void CmdLine::Uninstall()
{
	открываем SCM (OpenSCManager())
	открываем службу (OpenService())
	удаляем службу (DeleteService())
	закрываем всё, что открыли
}

Если служба использует Event Log, придётся ещё кое-что добавить, но об этом ниже.

Отсчёт пошёл:

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

  1. Служба должна вызвать StartServiceCtrlDispatcher() не позже, чем через 30 секунд после начала работы, иначе выполнение службы завершится. Практика подтвердила. Кроме того, в раздел Event Log'а System будет добавлена запись об ошибке (источник - <Service Control Manager>). Если служба запускается вручную из программы Services, пользователь получит сообщение (MessageBox) о произошедшем.
  2. Функция ServiceMain() должна вызвать RegisterServiceCtrlHandler(Ex) немедленно. Что будет в противном случае - не указано. Несоблюдение этого правила - один из случаев <нарушений во время инициализации> (термин мой), описанных ниже в этом же разделе.
  3. Функция ServiceMain() должна вызвать SetServiceStatus() первый раз <почти сразу> после RegisterServiceCtrlHandler(Ex), после чего служба должна продолжать вызывать её, пока инициализация не закончится. Неправильное использование SetServiceStatus() - второй случай <нарушений во время инициализации>.
  4. При обработке сообщения служба должна вернуть управление из функции Handler(Ex) в течение 30 секунд, иначе SCM сгенерирует ошибку. Практика подтверждает, запись в Event Log добавляется. Но никаких репрессивных действий по отношению к службе я не дождался.
  5. При получении сообщения SERVICE_CONTROL_SHUTDOWN служба должна закончить работу за время, не превышающее число миллисекунд, указанное в параметре WaitToKillServiceTimeout ключа HKLM\System\CurrentControlSet\Control, иначе будет завершена принудительно. Практика подтвердила.
  6. После завершения работы в качестве службы (то есть после посылки службой уведомления об этом) процессу даётся 20 секунд на очистку/сохранение/ещё что-то, после этого процесс завершается. Я не нашёл упоминаний об этом факте в MSDN, но это так. Подробнее в разделе <Корректное завершение>.

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

  1. Дополнительный поток выполняет необходимую инициализацию, а основной поток вызывает StartServiceCtrlDispatcher().
  2. Я не смог придумать, зачем делать что-либо до вызова RegisterServiceCtrlHandler(Ex), но если надо, можно сделать так же как в (1).
  3. Один из потоков посылает уведомления о продвижении процесса (см. ниже), второй выполняет инициализацию. Забавно, что при таком подходе для служб, запускаемых вручную, видимый результат не меняется (см. ниже о нарушениях во время инициализации).
  4. Идея в том, что, если обработка сообщения может затянуться, функция Handler(Ex) инициирует её и завершается, не дожидаясь окончания. Если рабочий поток службы в цикле ожидает каких-то событий, обработку может выполнить он, Handler(Ex) должна только проинформировать его о приходе сообщения, если рабочий поток постоянно занят, можно породить ещё один поток. При подобной реализации необходимо учесть, что следующее сообщение может прийти в любой момент, в том числе, повторно или в течение обработки предыдущего сообщения. С помощью Services этого не сделать, но пользователь может использовать утилиту Net.exe (синтаксис запуска: net команда имя_службы) или написать свою.

Ограничения (5) и (6) обойти не удаётся. Но, в отличие от (5), в (6) момент посылки уведомления о завершении регулируете вы. Поэтому можно выполнять всю необходимую очистку/сохранение/ещё что-то заранее.

Уведомления посылаются с помощью функции SetServiceStatus(). Функция потока, посылающего уведомления, может быть такой:


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;
}

В параметре передаётся состояние, о котором необходимо сообщать, sStatus содержит параметры службы, eSendPending - событие, установка которого означает окончание работы этого потока.

Теперь о <нарушениях впроцессе инициализации>. Термин придумал я, тестировал тоже я. Ни в MSDN, ни в других источниках я не нашёл упоминаний о чем-либо подобном. Возможно, другие авторы посчитали это неважным. Я тоже так считаю, но, по-моему, программист должен знать, что именно случится, если он поступит <не по правилам>. Варианты нарушений:

  1. Задержка перед вызовом RegisterServiceCtrlHandler(Ex).
  2. Задержка перед первым вызовом SetServiceStatus().
  3. Слишком большие паузы между вызовами SetServiceStatus().
  4. Не меняется поле dwCheckPoint структуры, передаваемой SetServiceStatus().

На всё это система реагирует так:

A) Служба запускается автоматически.

  1. Минуты через две (если за это время <нарушение> не прекратится и служба не начнёт работать нормально) в Event Log'е появится запись <The ... service hung on starting.>
  2. Если хоть одна служба<повисла>, пользователь получит сообщение <At least one service or driver failed during system startup. Use Event Viewer to examine the event log for details.> Такое ощущение, что это сообщение появляется в тот момент, когда запускается первая <зависшая> служба (сам понимаю, что звучит нелогично, но что делать...).

B) Служба запускается вручную из Services.

  1. Минуты три система подождёт.
  2. Появится сообщение об ошибке.
  3. В программе Services в столбце Status служба будет помечена словом<Starting>.

Но в любом случае служба, в конце концов, запустится.

Кто будет работать?

Этот вопрос возник у меня, когда я писал свою первую службу. Если чётче сформулировать, то звучит он так: который из потоков я могу использовать в качестве рабочего? На первый взгляд задействовано три потока: первый исполняет main()/WinMain(), второй ServiceMain(), третий Handler(Ex) (не совсем так, см.<Подробности>). Очевидно, что первый и третий потоки не подходят. Про второй поток ничего не известно (если не читать MSDN) и, вполне возможно, функция ServiceMain() должна как можно быстрее возвращать управление. Я поступил просто: создал в ServiceMain() дополнительный поток, который выполнял работу. Окончание функции выглядело так:


	:
	// Создаёт рабочий поток и возвращает управление
	Begin();
}

Это работает.

Потом я внимательнее почитал MSDN и выяснил, что для работы предназначен поток, выполняющий ServiceMain(). Более того, в описании написано: <A ServiceMain function does not return until its services are ready to terminate.> Возвращать управление из ServiceMain() сразу рекомендуется только службам, не нуждающимся в потоке для выполнения работы (например, RPC-серверам).

Итак, есть два способа (или три, так как рабочий поток может порождать не только ServiceMain(), но и main()/WinMain()), Microsoft рекомендует второй. Возможно, в первом есть скрытые проблемы, но я их не нашёл. На всякий случай, я использую только второй.

Корректное завершение

Если ваша служба успешно выполнила свою миссию или, наоборот, окончательно провалилась (неважно, во время выполнения или инициализации), её нужно завершить. Несколько вариантов <как делать ненадо>:

  1. Завершить все рабочие потоки, поток, выполняющий Handler(Ex) не трогать. В этом случае SCM <ничего не заметит> и служба продолжит выполняться. Это не смертельно, но и не очень хорошо, так как ресурсы-то используются.
  2. Завершить все рабочие потоки, поток, выполняющий Handler(Ex) завершить вызовом ExitThread() при обработке первого следующего сообщения. SCM генерирует ошибку и добавляет запись о ней в Event Log.
  3. Завершить процесс вызовом ExitProcess(). Аналогично (2), даже ошибка такая же. Странно, что код завершения процесса не записывается.

А теперь о том, как надо. Об окончании работы служба должна сообщить. Как обычно, для сообщения об изменении состояния используется функция SetServiceStatus(). Из всех полей передаваемой в неё структуры в данном случае нас интересуют dwCurrentState, dwWin32ExitCode и dwServiceSpecificExitCode. dwCurrentState в любом случае должно быть установлено в SERVICE_STOPPED, остальные два в разных ситуациях по-разному.

  1. Служба завершилась успешно. dwWin32ExitCode = NO_ERROR, в Event Log ничего записано не будет.
  2. Произошла неисправимая ошибка, и это одна из стандартных ошибок Windows. dwWin32ExitCode = ERROR_..., в Event Log будет добавлена запись, описывающая ошибку, численное значение ошибки указано не будет.
  3. Произошла неисправимая ошибка, специфичная для вашей службы. dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR, в dwServiceSpecificExitCode код ошибки. Так как систему кодирования ошибок вы придумали сами, расшифровать значение кода можете тоже только вы. В Event Log будет добавлена запись следующего содержания: <The ... service terminated with service-specific error...> (в местах многоточий - имя службы и код ошибки).

Если для завершения службы необходимо выполнить продолжительные действия, в процессе их выполнения, имет смысл посылать уведомления SERVICE_STOP_PENDING. Но это не обязательно.

Ещё один тонкий момент: что будет с вашей службой после вызова SetServiceStatus()? Все потоки прекратят исполняться сразу и окончательно, или им дадут <умереть естественнойсмертью>? Я попытался выяснить это, и получил следующее (это верно для любых вариантов завершения службы, при которых вызывается SetServiceStatus() с соответствующими параметрами, кроме случая с SERVICE_CONTROL_SHUTDOWN):

  1. Если exe-файл содержит несколько служб и хоть одна из них (не считая завершающейся) запущена, ничего интересного не произойдёт. То есть, ни процесс, ни один из потоков не будут завершены насильно.
  2. После завершения последней службы, функция ServiceControlDispatcher() возвращает управление в main()/WinMain(). Если main()/WinMain() самостоятельно заканчивается в течение 20-ти секунд, то, как у нормального приложения, завершаются все потоки (в том числе и <исключения>, см. ниже).
  3. Через 20 секунд после завершения последней службы принудительно завершатся все потоки, кроме <исключений>. Даже если поток, исполнявший main()/WinMain() будет завершён таким образом, <исключения> будут выполняться. Никаких записей в Event Log добавлено не будет.
  4. <Исключения> - потоки, вызвавшие функцию MessageBox() с флагом MB_SERVICE_NOTIFICATION или MB_DEFAULT_DESKTOP_ONLY (см.раздел <Интерактивность>). Даже если сообщение пока не отображено, поток (если его не<убить>, конечно) будет ждать реакции пользователя. Если 20 секунд уже истекло, сразу после завершения функции MessageBox() поток завершится.

Идеи, по поводу того, что я мог пропустить:

  1. Число <20> может зависеть от версии Windows или каких-нибудь ключей реестра.
  2. Возможно, существуют ещё какие-то<исключения>.

Я считаю этот момент важным и удивлён, что где-нибудь на видном месте в MSDN нет соответствующей статьи.

Интерактивность

Интерактивности в службах следует избегать. Службы предназначены для непрерывной работы в отсутствии пользователей, поэтому дожидаться пока оператор нажмёт <OK> можно очень долго. Но, тем не менее, возможности есть.

Самое простое - отобразить сообщение (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.

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

  1. Служба запущена в контексте безопасности LocalSystem.
  2. Служба должна быть помечена как интерактивная.
  3. Значение параметра NoInteractiveServices ключа HKLM\SYSTEM\CurrentControlSet\ Control\Windows\ должно быть равно 0.

Если всё это так, служба может выводить на экран что угодно. Иначе, служба может попробовать самостоятельно открыть и использовать нужный ей desktop. Подобнее об объектах <desktop> и <window station> смотрите в MSDN.

Безопасность

Это очень большая тема, про которую я очень мало знаю. Но она имеет прямое отношение к службам, если вы хотите заниматься их разработкой, вам (и мне, естественно) придется с ней разобраться.

Отладка

Отладка служб дело не простое. Причин несколько.

  1. Даже если вы будете очень стараться, меньше чем два потока у вас не получится.
  2. Службу запускает SCM.
  3. Если в exe-файле несколько служб (см. раздел <Свальныйгрех>) отладка будет ещё неприятнее.

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

  1. Присоединить отладчик к запущенной службе.
  2. Использовать DebugBreak().
  3. В HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options добавить ключ <имя_исполяемого_файла.exe> (без пути), в нём создать строковый параметр Debugger и записать в него полное имя отладчика. При запуске указанного файла, он будет запущен под отладчиком.
  4. Использовать SoftICE.

При отладке кода запуска, следует помнить, что ограничение на время (30 секунд) никуда не исчезает, если вы не уложитесь, служба завершится.

Event Log

Интерактивность - нехорошо. Но служба должна каким-то образом сообщать администратору об ошибках и других важных событиях. Службам, генерирующим несколько десятков (или больше) сообщений в день целесообразно использовать для этого файлы в формате какой-нибудь СУБД. Для остальных лучшее решение - Event Log. Дело не в том, что записывать в него проще, чем в обычный файл, а в том, что администратор может просмотреть его утилитой Event Viewer.

Ниже приведены упрощенные рекомендации по работе с Event Log'ом. Для практического использования этого достаточно, подробнее смотрите в MSDN.

  1. При установке служба (вообще-то, Event Log может использовать любое приложение) должна зарегистрироваться как источник сообщений. Для этого в ключе HKLM\System\CurrentControlSet\Services\Eventlog\Application\ нужно создать ключ с любым именем, в этом ключе создать строковый параметр EventMessageFile и записать в него полный путь к файлу сообщений (см. ниже).
  2. При запуске служба вызывает функцию RegisterEventSource(), в первый параметр передаёт NULL, во второй имя ключа, добавленного в реестр в (1).
  3. Полученное значение используется при вызове функции ReportEvent() (см. ниже), а в конце работы передаётся функции DeregisterEventSource().
  4. Файл сообщений это exe или dll-файл, содержащий ресурс типа<таблица сообщений>. Для получения нужно скомпилировать message compiler'ом (mc.exe) правильно написанный текстовый файл (ниже), получившийся файл ресурсов включить в проект и скомпоновать. Mc.exe не интегрирован в Visual Studio и запускается из командной строки. Ключами можно не пользоваться, достаточно написать <mcfilename.mc>. На выходе будет filename.h, filename.rc, filenameXXXX.bin (в некоторых случаях несколько штук). filename.rc - тот самый файл ресурсов, он ссылается на filenameXXXX.bin. filename.h содержит идентификаторы сообщений и используется службой.

<Правильно написанный текстовый файл> выглядит так:


[Заголовок]
сообщение_1
:
сообщение_N

Заголовок необязателен, может отсутствовать или присутствовать частично. Обычно используется только одно поле, в котором перечисляются используемые языки.


LanguageNames=(English=
0x0409:MSG0409)LanguageNames=(Russian=0x0419:MSG0419)

<English> и <Russian> - названия, могут быть любыми, <MSG0409> и <MSG0419> - имена выходных bin-файлов, могут быть любыми, 0x0409 и 0x0419 - идентификаторы языков, полная таблица есть в MSDN (смотрите GetLocalInfo()). Если файл сообщений поддерживает несколько языков, в разных версиях Windows (в смысле русской, английской, ...) в Event Log'е будут отображаться разные версии сообщений.

Сообщение выглядит так:


MessageId    = 0x1
Severity     = Error
Facility     = Application
SymbolicName = MSG_NO_COMMAND
Language = English
You must enter a command. Valid commands: %1, %2, %3.
.

Language = Russian
Введите команду. Допустимые команды: %1, %2, %3.
.

<MessageId> - идентификатор сообщения. <Severity>- типсообщения, определенытипы <Success>, <Informational>,<Warning>, и <Error>, их названия можно переопределить в заголовке, но на название типа и иконку, отображаемые Event Viewer'ом, это не повлияет. <Facility> - смысл не ясен, по аналогии с HRESULT, можно предположить, что оно как-то определяет источник сообщения. Определены <System>и <Application>, можно определить ещё около четырёх тысяч. Поля <Severity> и <Facility> не обязательны, при отсутствии они наследуются от предыдущего сообщения, первое сообщение наследует значения <Success> и<Application>. <SymbolicName> - имя соответствующего сообщению макроса в генерируемом h-файле. Эти четыре поля - заголовок сообщения.

Тело сообщения начинается после строки <Language= XXXX>, заканчивается строкой, на которой нет ничего кроме точки и перевода строки. На каждый язык должно быть по одному <телу> (если в заголовке вы не определили ни одного языка, используйте<English>). Вместо <%1> .. <%99> будут вставлены строки, которые служба передаст функции ReportEvent(). Учтите, что этот механизм предназначен для передачи имён файлов, IP-адресов, каких-то чисел и т.д. Но не для передачи текста. Можно, конечно, сделать так:


Language = English
%1
.

но, с моей точки зрения, это плохая идея. Дело в том, что в файлах Event Log'а. хранится имя источника, номер сообщения, переданные строки и прикреплённые данные, но не сам текст. Поэтому, если записать сообщение, а потом изменить dll или значение параметра EventMessageFile в реестре, текст изменится. Насколько я знаю, это нужно для того, что бы, когда пользователь из Китая, у которого всё на китайском, посылает свой Event Log (находится в WinNT\System32\config\ AppEvent.Evt) разработчику из Нигерии, тот мог бы, используя свою dll, прочитать те же сообщения на нигерийском.

Для записи сообщений используется функция ReportEvent()


HANDLE 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 заголовочного файла.
pUserSid Передавайте NULL.
wNumOfStrings Количество передаваемых строк.
dwDataSize Размер передаваемых данных.
pStrings Массив строк. Если строк меньше чем позиций, в лишних позициях будет<%n>, где n - номер позиции.
pRawData Данные, прикрепляемые к сообщению.

Для использования Event Log'а приведённой выше информации вполне достаточно. Но это, конечно, далеко не всё.

Администрирование

Обычно, у службы есть какие-то параметры, которые нужно настраивать. Иногда нужно иметь возможность определять и корректировать текущее состояние службы.

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

Для администрирования пишется отдельное приложение (далее - конфигуратор), которое каким-то образом взаимодействует со службой. Могу предложить следующие варианты:

  1. Параметры хранятся в реестре (обычно в HKLM\System\CurrentControlSet\ Services\имя_службы\Parameters\). Конфигуратор их изменяет, служба, используя функцию RegNotifyChangeKeyValue(), отслеживает эти изменения.
  2. Параметры хранятся где угодно. После их изменения конфигуратор посылает службе сообщение. Для этого нужно открыть службу функцией OpenService() с правом доступа SERVICE_USER_DEFINED_CONTROL и послать сообщение функцией ControlService(). Конечно, в службе необходимо предусмотреть соответствующую обработку.
  3. Используются именованные каналы или любое другое средство коммуникации между процессами. В этом случае помимо передачи параметров можно получать и изменять состояние службы.

Самый простой вариант - служба реагирует на изменение параметров только после перезапуска.

Свальный грех

В один exe-файл можно поместить несколько служб. Название раздела характеризует моё отношение к этому. Это сильно затрудняет кодирование и отладку, а единственный известный мне выигрыш - экономия ресурсов на компьютере пользователя (если вы пишете несколько зависимых друг от друга служб, наверное, появляются и другие выигрыши; я этим ни разу не занимался). Но, тем не менее, на моей машине в service.exe находятся службы Alerter, AppMgmt, Browser, Dhcp, dmserver, Dnscache, Eventlog, lanmanserver, lanmanworkstation, LmHosts, Messenger, PlugPlay, ProtectedStorage, seclogon, TrkWks, W32Time и Wmi. Вряд ли их писали люди глупее меня.

Подробности

Здесь собраны факты, знать которые полезно, но не необходимо.

Служба не обязательно является консольным приложением.

В параметре ImagePath ключа HKLM\System\CurrentControlSet\Services\имя_службы можно задать командную строку (можно даже), но, по-моему, этой возможностью лучше не пользоваться.

Начиная с Win200 в параметре Description ключа HKLM\System\CurrentControlSet\ Services\имя_службы можно задать описание службы. Оно отображается Services в столбце<Description>. Для установки этого параметра можно воспользоваться RegSetValueEx() или ChangeServiceConfig2().

Судя по всему, пока служба не вызовет StartServiceCtrlDispatcher(), SCM не может запустить следующую. Это ещё одна причина не помещать инициализацию в main()/WinMain().

После вызова StartServiceCtrlDispatcher() основной поток приложения не простаивает. Как минимум, он исполняет обработчики сообщений всех служб exe-файла. Поэтому <задействовано> не три потока, а два.

Когда вызывается функция MessageBox() с флагом MB_SERVICE_NOTIFICATION или MD_SERVICE_DEFAULT_DESKTOP_ONLY, в раздел Event Log'а System добавляется запись. Источник - <ApplicationPopup>, внутри - содержимое сообщения. Время создания записи соответствует времени вызова функции MessageBox(), а не времени отображения.

Код

В качестве примера я написал небольшую службу, конфигуратор и файл сообщений. Функциональность службы близка к нулю, зато она умеет почти всё, о чём говорилось выше. Повторность использования стандартных элементов кода в моих службах близка к 100%. И вам советую. В службе активно используются пространства имён и она, с моей точки зрения (на данный момент), написана <правильно> (ну, или почти правильно). Конфигуратор на это не претендует.

serv.zip (служба) - 67 Кб
mymsg.zip (файл сообщений) - 56 Кб
config.zip (конфигуратор) - 33 Кб

Для установки нужно положить serv.exe и mymsg.dll в один каталог и запустить serv.exe с ключом /install. Для успешной установки необходимо иметь права администратора. Работа службы заключается в добавлении строчек в файл servfile.log, находящийся в её корневом каталоге. Каждые n миллисикунд будет добавляться строчка. Число n регулируется конфигуратором, по умолчанию 10000. Для удаления службы нужно запустить serv.exe с ключом /uninstall. Это Unicode-версия службы и пишет в файл она тоже в Unicode. Читать это умеет Word и Notepad.


 ВОПРОС-ОТВЕТ

Как изменить текст в edit box'е?

Полная замена текста

Поскольку edit box является окном, то для изменения текста в нем вполне подойдет функция SetWindowText() (либо SetDlgItemText(), что, в сущности, дела не меняет). В этом случае произойдет полная замена текста в окне.

// Пример полной замены текста.
LPCSTR szText   = "Полная замена текста в окне редактирования.";
HWND   hwndEdit = GetDlgItem(hDlg, IDC_EDIT);
SetWindowText(hwndEdit, szText);

Вставка/замена фрагмента

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


// Пример вставки фрагмента.
// Если имеется выделение, оно будет заменено фрагментом текста, 
// если выделение отсутствует, фрагмент будет добавлен к концу текста.

int selFirst = 0, selLast = 0;
SendDlgItemMessage ( hDlg,   IDC_EDIT1, EM_GETSEL, 
                    (WPARAM) &selFirst, (LPARAM) &selLast );
if(selFirst == selLast)
{
    // Получим длину текста для многострочного окна редактирования
    selFirst = selLast = lstrlen(
              *(char**) SendDlgItemMessage ( 
                            hDlg, IDC_EDIT1, 
                            EM_GETHANDLE, 0, 0 ) );
}
SendDlgItemMessage ( hDlg, IDC_EDIT1, EM_SETSEL, selFirst, selLast );
SendDlgItemMessage ( hDlg, IDC_EDIT1, EM_REPLACESEL, TRUE, (LONG)szReplace );

ПРИМЕЧАНИЕ

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

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

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


// Вставка текста из буфера обмена
SendDlgItemMessage(hDlg, IDC_EDIT1, WM_PASTE, 0, 0);

Удаление фрагмента

Удаление фрагмента текста эквивалентно замене выделенного текста строкой нулевой длины. Следовательно, описанная выше методика позволяет выполнить удаление части (или всего) текста. Опять-таки, для этого необходимо осуществить выделение нужного участка текста и послать сообщение EM_REPLACESEL окну редактирования.

SendMessage(hwndEdit, EM_REPLACESEL, TRUE, (LPARAM)"");

Для той же цели можно воспользоваться посылкой сообщения WM_CLEAR.

SendMessage(hwndEdit, WM_CLEAR, 0, 0);


Это все на сегодня. Пока!

Алекс Jenter   jenter@rsdn.ru
Duisburg, 2001.    Публикуемые в рассылке материалы принадлежат сайту RSDN.

| Предыдущие выпуски     | Статистика рассылки