Программирование на Visual С++ - No.74 (Написание служб | Как изменить текст в editbox'e)
Информационный Канал Subscribe.Ru |
|
РАССЫЛКА САЙТА
RSDN.RU |
Здравствуйте, дорогие подписчики!
CТАТЬЯ Написание служб Windows NT/2000 Автор: Сергей ХолодиловЯ предполагаю, что вы представляете себе, что такое служба и слышали о том, как их писать. Об утверждениях и тестированииЭта статья содержит много разных утверждений. Некоторые из них описывает реакцию Windows на какие-то действия со стороны службы. Полноценная проверка таких утверждений нереальна (для меня), так как, во-первых, Windows большая и исходников у меня нет, во-вторых, надо проверять реакцию всех версий Windows, поддерживающих службы, а это линейка Windows NT, начиная с версии 3.1. Я поступил так:
Поэтому, в том, что касается реакции 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; } ... Комментарий к листингу:
Функции, выполняющие собственно установку/удаление выглядят примерно так: void CmdLine::Install() { открываем SCM (OpenSCManager()) создаём службу (CreateService()) закрываем всё, что открыли } void CmdLine::Uninstall() { открываем SCM (OpenSCManager()) открываем службу (OpenService()) удаляем службу (DeleteService()) закрываем всё, что открыли } Если служба использует Event Log, придётся ещё кое-что добавить, но об этом ниже. Отсчёт пошёл:В MSDN много написано о том, что при запуске и обработке сообщений служба должна действовать очень оперативно. В некоторых местах указано максимальное число секунд между событиями (вызовами соответствующих функций), иногда написано <немедленно> или <как можнобыстрее>. Конкретно, есть шесть моментов, когда службе советуют поторопиться:
Понятно, что эти требования ограничивают программиста. Некоторые из ограничений можно обойти, используя несколько потоков. Например, так:
Ограничения (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, ни в других источниках я не нашёл упоминаний о чем-либо подобном. Возможно, другие авторы посчитали это неважным. Я тоже так считаю, но, по-моему, программист должен знать, что именно случится, если он поступит <не по правилам>. Варианты нарушений:
На всё это система реагирует так: A) Служба запускается автоматически.
B) Служба запускается вручную из Services.
Но в любом случае служба, в конце концов, запустится. Кто будет работать?Этот вопрос возник у меня, когда я писал свою первую службу. Если чётче сформулировать, то звучит он так: который из потоков я могу использовать в качестве рабочего? На первый взгляд задействовано три потока: первый исполняет 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 рекомендует второй. Возможно, в первом есть скрытые проблемы, но я их не нашёл. На всякий случай, я использую только второй. Корректное завершениеЕсли ваша служба успешно выполнила свою миссию или, наоборот, окончательно провалилась (неважно, во время выполнения или инициализации), её нужно завершить. Несколько вариантов <как делать ненадо>:
А теперь о том, как надо. Об окончании работы служба должна сообщить. Как обычно, для сообщения об изменении состояния используется функция SetServiceStatus(). Из всех полей передаваемой в неё структуры в данном случае нас интересуют dwCurrentState, dwWin32ExitCode и dwServiceSpecificExitCode. dwCurrentState в любом случае должно быть установлено в SERVICE_STOPPED, остальные два в разных ситуациях по-разному.
Если для завершения службы необходимо выполнить продолжительные действия, в процессе их выполнения, имет смысл посылать уведомления SERVICE_STOP_PENDING. Но это не обязательно. Ещё один тонкий момент: что будет с вашей службой после вызова SetServiceStatus()? Все потоки прекратят исполняться сразу и окончательно, или им дадут <умереть естественнойсмертью>? Я попытался выяснить это, и получил следующее (это верно для любых вариантов завершения службы, при которых вызывается SetServiceStatus() с соответствующими параметрами, кроме случая с SERVICE_CONTROL_SHUTDOWN):
Идеи, по поводу того, что я мог пропустить:
Я считаю этот момент важным и удивлён, что где-нибудь на видном месте в 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. Если пользовательский интерфейс вашей службы должен быть богаче, существует два выхода. В первом случае должны выполняться следующие условия:
Если всё это так, служба может выводить на экран что угодно. Иначе, служба может попробовать самостоятельно открыть и использовать нужный ей desktop. Подобнее об объектах <desktop> и <window station> смотрите в MSDN. БезопасностьЭто очень большая тема, про которую я очень мало знаю. Но она имеет прямое отношение к службам, если вы хотите заниматься их разработкой, вам (и мне, естественно) придется с ней разобраться. ОтладкаОтладка служб дело не простое. Причин несколько.
Но чаще всего удаётся написать обычное приложение, полностью воспроизводящее рабочую часть службы, отладить его и поместить в обёртку из некоторого стандартного кода, превращающего приложение в службу. Это основной ход. Если ядро приложения отлажено, код, реализующий стандартные функции службы, проверен, при стыковке больших проблем быть не должно. Но (как это не странно) появляются средние и мелкие. Помимо <отладки без отладчика> остаётся следующее.
При отладке кода запуска, следует помнить, что ограничение на время (30 секунд) никуда не исчезает, если вы не уложитесь, служба завершится. Event LogИнтерактивность - нехорошо. Но служба должна каким-то образом сообщать администратору об ошибках и других важных событиях. Службам, генерирующим несколько десятков (или больше) сообщений в день целесообразно использовать для этого файлы в формате какой-нибудь СУБД. Для остальных лучшее решение - Event Log. Дело не в том, что записывать в него проще, чем в обычный файл, а в том, что администратор может просмотреть его утилитой Event Viewer. Ниже приведены упрощенные рекомендации по работе с Event Log'ом. Для практического использования этого достаточно, подробнее смотрите в MSDN.
<Правильно написанный текстовый файл> выглядит так: [Заголовок] сообщение_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 );
Для использования Event Log'а приведённой выше информации вполне достаточно. Но это, конечно, далеко не всё. АдминистрированиеОбычно, у службы есть какие-то параметры, которые нужно настраивать. Иногда нужно иметь возможность определять и корректировать текущее состояние службы. Изменение состояния и изменение параметров вещи разные. Например, вы написали какую-то сетевую службу. Если есть возможность изменять состояние, администратор может, просмотрев список текущих соединений, какие-то из них разорвать. Изменением параметров этого не достигнуть. Для администрирования пишется отдельное приложение (далее - конфигуратор), которое каким-то образом взаимодействует со службой. Могу предложить следующие варианты:
Самый простой вариант - служба реагирует на изменение параметров только после перезапуска. Свальный грехВ один 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 Кб Для установки нужно положить 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 );
Кроме того, вставку фрагмента можно осуществить посредством операций с буфером обмена (посылкой окну сообщения 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 |
http://subscribe.ru/
E-mail: ask@subscribe.ru |
Отписаться
Убрать рекламу |
В избранное | ||