Программирование для Windows NT© Александр Фролов, Григорий ФроловТом 27, часть 2, М.: Диалог-МИФИ, 1996, 272 стр. |
Помимо обычных процессов, в операционной системе Microsoft Windows NT создаются так называемые сервисные процессы или сервисы (services). Эти процессы могут стартовать автоматически при загрузке операционной системы, по запросу приложений или других сервисов, а также в ручном режиме.
Функции, выполняемые сервисами, могут быть самыми разнообразными: от обслуживания аппаратуры и программных интерфейсов до серверов приложений, таких, например, как серверы баз данных или серверы World Wide Web (WWW).
Информация о всех серверах, установленных в системе, хранится в регистрационной базе данных. Ниже мы привели путь к этой информации:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Servicies
Для просмотра и редактирования регистрационной базы данных вы можете воспользоваться приложением regedt32.exe, которое находится в каталоге winnt\system32. Однако в программном интерфейсе WIN32 имеется набор функций, специально предназначенных для работы с записями регистрационной базы данных, имеющих отношение к сервисным процессам.
Чтобы просмотреть список установленных сервисов, вы можете запустить приложение Services из папки Control Panel. В диалоговой панели Services (рис. 5.1) отображается список установленных сервисов, их текущее состояние и режим запуска (ручной или автоматический).
Рис. 5.1. Диалоговая панель приложения Services, предназначенная для управления сервисными процессами
Если сделать двойной щелчок левой клавишей мыши по названию сервиса, на экране появится диалоговая панель, с помощью которой можно настроить параметры сервиса (рис. 5.2).
Рис. 5.2. Диалоговая панель, предназначенная для настройки параметров сервиса
С помощью группы переключателей Startup Type вы можете выбрать один из трех способов запуска сервиса.
Если включен переключатель Automatic, сервис будет запускаться автоматически при загрузке операционной системы. Этот режим удобен для тех сервисов, которые нужны постоянно, например для сервера базы данных. При этом сервер базы данных будет запускаться автоматически без участия оператора, что очень удобно.
При включении переключателя Manual сервис будет запускаться в ручном режиме. Пользователь может запустить сервис, нажав кнопку Start в диалоговой панели Services, показанной на рис. 5.1. Другое приложение или другой сервис также может запустить этот сервис при помощи специальной функции программного интерфейса WIN32. Эту функцию мы рассмотрим позже.
И, наконец, если включить переключатель Disabled, работа сервиса будет заблокирована.
Сервис может работать с привилегиями выбранных пользователей или с привилегиями системы LocalSystem. Для выбора имени пользователя в ы должны включить переключатель This Account и выбрать из списка, расположенного справа от этого переключателя, имя пользователя. Дополнительно в полях Password и Confirm Password необходимо ввести пароль пользователя.
Если включить переключатель System Account, сервис будет работать с привилегиями системы. Если сервис будет взаимодействовать с программным интерфейсом рабочего стола Desktop, следует включить переключатель Allow Service to Interact with Desktop.
Сервисы могут быть двух типов: стандартные сервисы и сервисы, соответствующие протоколам драйверов устройств Microsoft Windows NT. Последние описаны в документации DDK и не рассмотрены в нашей книге. Нажав в диалоговой панели Services, показанной на рис. 5.1, кнопку HW Profiles, вы можете выбрать один из установленных файлов конфигурации аппаратуры, которая обслуживается данным сервисом (рис. 5.3), разрешить или запретить использование выбранного файла конфигурации.
Рис. 5.3. Выбор файла конфигурации аппаратуры
При запуске операционной системы Microsoft Windows NT автоматически стартует специальный процесс, который называется процессом управления сервисами (Service Control Manager). В программном интерфейсе WIN32 имеются функции, с помощью которых приложения и сервисы могут управлять работой сервисов, обращаясь к процессу управления сервисами. Некоторые из этих функций будут рассмотрены в нашей книге.
Для того чтобы создать загрузочный модуль сервиса, вы должны подготовить исходные тексты обычного консольного приложения, имеющего функцию main (не WinMain, а именно main).
В простейшем случае функция main вызывает функцию StartServiceCtrlDispatcher, что необходимо для подключения главной задачи сервисного процесса к процессу управления сервисами. Ниже мы привели пример функции main сервисного процесса:
#define MYServiceName "Sample of simple service" void main(int agrc, char *argv[]) { SERVICE_TABLE_ENTRY DispatcherTable[] = { { MYServiceName, (LPSERVICE_MAIN_FUNCTION)ServiceMain }, { NULL, NULL } }; if(!StartServiceCtrlDispatcher(DispatcherTable)) { fprintf(stdout,"StartServiceCtrlDispatcher: Error %ld\n", GetLastError()); getch(); return; } }
Функции StartServiceCtrlDispatcher передается указатель на массив структур типа SERVICE_TABLE_ENTRY. В этом массиве описываются точки входа всех сервисов, определенных в данном файле. Таким образом, в одном файле можно определить сразу несколько сервисов. Последняя строка таблицы всегда должна содержать значения NULL - это признак конца таблицы.
Тип SERVICE_TABLE_ENTRY и соответствующий указатель определены следующим образом:
typedef struct _SERVICE_TABLE_ENTRY { LPTSTR lpServiceName; LPSERVICE_MAIN_FUNCTION lpServiceProc; } SERVICE_TABLE_ENTRY, *LPSERVICE_TABLE_ENTRY;
В поле lpServiceName записывается указатель на текстовую строку имени сервиса, а в поле lpServiceProc - указатель на точку входа сервиса.
Заметим, что функция main должна вызвать функцию StartServiceCtrlDispatcher достаточно быстро - не позднее чем через 30 секунд после запуска.
Получив управление, функция StartServiceCtrlDispatcher не возвращает его до тех пор, пока все сервисы, запущенные в рамках данного процесса, не завершат свою работу.
При успешном завершении функция StartServiceCtrlDispatcher возвращает значение TRUE. Если же произойдет ошибка, возвращается значение FALSE. Код ошибки можно определить при помощи функции GetLastError.
Точка входа сервиса - это функция, адрес которой записывается в поле lpServiceProc массива структур SERVICE_TABLE_ENTRY. Имя функции может быть любым, а прототип должен быть таким, как показанный ниже:
void WINAPI ServiceMain(DWORD dwArgc, LPSTR *lpszArgv);
Точка входа сервиса вызывается при запуске сервиса функцией StartService (эту функцию мы рассмотрим позже). Через параметр dwArgc передается счетчик аргументов, а через параметр lpszArgv - указатель на массив строк параметров. В качестве первого параметра всегда передается имя сервиса. Остальные параметры можно задать при запуске сервиса функцией StartService.
Функция точки входа сервиса должна зарегистрировать функцию обработки команд и выполнить инициализацию сервиса.
Первая задача решается с помощью функции RegisterServiceCtrlHandler, прототип которой приведен ниже:
SERVICE_STATUS_HANDLE RegisterServiceCtrlHandler( LPCTSTR lpszServiceName, // имя сервиса LPHANDLER_FUNCTION lpHandlerProc); // адрес функции // обработки команд
Через первый параметр этой функции необходимо передать адрес текстовой строки имени сервиса, а через второй - адрес функции обработки команд (функция обработки команд будет рассмотрена ниже).
Вот пример использования функции RegisterServiceCtrlHandler:
SERVICE_STATUS_HANDLE ssHandle; ssHandle = RegisterServiceCtrlHandler(MYServiceName, ServiceControl);
Функция RegisterServiceCtrlHandler в случае успешного завершения возвращает идентификатор состояния сервиса. При ошибке возвращается нулевое значение.
Заметим, что регистрация функции обработки команд должна быть выполнена немедленно в самом начале работы функции точки входа сервиса.
Теперь перейдем к решению второй задачи - инициализации сервиса.
В процессе инициализации функция точки входа сервиса выполняет действия, которые зависят от назначения сервиса. Необходимо, однако, помнить, что без принятия дополнительных мер инициализация должна выполняться не дольше одной секунды.
А что делать, если инициализация сервиса представляет собой достаточно длительный процесс?
В этом случае перед началом инициализации функция точки входа сервиса должна сообщить процессу управления сервисами, что данный сервис находится в состоянии ожидания запуска. Это можно сделать с помощью функции SetServiceStatus, которая будет описана позже. Перед началом инициализации вы должны сообщить процессу управления сервисами, что сервис находится в состоянии SERVICE_START_PENDING.
После завершения инициализации функция точки входа сервиса должна указать процессу управления сервисами, что процесс запущен и находится в состоянии SERVICE_RUNNING.
Как следует из названия, функция обработки команд, зарегистрированная функцией RegisterServiceCtrlHandler, обрабатывает команды, передаваемые сервису операционной системой, другими сервисами или приложениями. Эта функция может иметь любое имя и выглядит следующим образом:
void WINAPI ServiceControl(DWORD dwControlCode) { switch(dwControlCode) { case SERVICE_CONTROL_STOP: { ss.dwCurrentState = SERVICE_STOP_PENDING; ReportStatus(ss.dwCurrentState, NOERROR, 0); // Выполняем остановку сервиса, вызывая функцию, // которая выполняет все необходимые для этого действия // ServiceStop(); ReportStatus(SERVICE_STOPPED, NOERROR, 0); break; } case SERVICE_CONTROL_INTERROGATE: { ReportStatus(ss.dwCurrentState, NOERROR, 0); break; } default: { ReportStatus(ss.dwCurrentState, NOERROR, 0); break; } } }
В приведенном выше фрагменте кода для сообщения процессу управления сервисами текущего состояния сервиса мы вызываем созданную нами функцию ReportStatus. Эта функция будет описана в следующем разделе.
Через единственный параметр функция обработки команд получает код команды, который может принимать одно из перечисленных ниже значений.
Значение |
Описание |
SERVICE_CONTROL_STOP |
Остановка сервиса |
SERVICE_CONTROL_PAUSE |
Временная остановка сервиса |
SERVICE_CONTROL_CONTINUE |
Продолжение работы сервиса после временной остановки |
SERVICE_CONTROL_INTERROGATE |
Когда поступает эта команда, сервис должен немедленно сообщить процессу управления сервисами свое состояние |
SERVICE_CONTROL_SHUTDOWN |
Сервис должен прекратить работу в течении 20 секунд, так как завершается работа операционной системы |
Как мы уже говорили, сервис может сообщить процессу управления сервисами свое состояние, для чего он должен вызвать функцию SetServiceStatus. Прототип этой функции мы привели ниже:
BOOL SetServiceStatus( SERVICE_STATUS_HANDLE sshServiceStatus, // идентификатор // состояния сервиса LPSERVICE_STATUS lpssServiceStatus); // адрес структуры, // содержащей состояние сервиса
Через параметр sshServiceStatus функции SetServiceStatus вы должны передать идентификатор состояния сервиса, полученный от функции RegisterServiceCtrlHandler.
В параметре lpssServiceStatus вы должны передать адрес предварительно заполненной структуры типа SERVICE_STATUS:
typedef struct _SERVICE_STATUS { DWORD dwServiceType; // тип сервиса DWORD dwCurrentState; // текущее состояние сервиса DWORD dwControlsAccepted; // обрабатываемые команды DWORD dwWin32ExitCode; // код ошибки при запуске // и остановке сервиса DWORD dwServiceSpecificExitCode; // специфический код ошибки DWORD dwCheckPoint; // контрольная точка при // выполнении длительных операций DWORD dwWaitHint; // время ожидания } SERVICE_STATUS, *LPSERVICE_STATUS;
В поле dwServiceType необходимо записать один из перечисленных ниже флагов, определяющих тип сервиса:
Флаг |
Описание |
SERVICE_WIN32_OWN_PROCESS |
Сервис работает как отдельный процесс |
SERVICE_WIN32_SHARE_PROCESS |
Сервис работает вместе с другими сервисами в рамках одного и того же процесса |
SERVICE_KERNEL_DRIVER |
Сервис представляет собой драйвер операционной системы Microsoft Windows NT |
SERVICE_FILE_SYSTEM_DRIVER |
Сервис является драйвером файловой системы |
SERVICE_INTERACTIVE_PROCESS |
Сервисный процесс может взаимодействовать с программным интерфейсом рабочего стола Desktop |
В поле dwCurrentState вы должны записать текущее состояние сервиса. Здесь можно использовать одну из перечисленных ниже констант:
Константа |
Состояние сервиса |
SERVICE_STOPPED |
Сервис остановлен |
SERVICE_START_PENDING |
Сервис находится в состоянии запуска, но еще не работает |
SERVICE_STOP_PENDING |
Сервис находится в состоянии остановки, но еще не остановился |
SERVICE_RUNNING |
Сервис работает |
SERVICE_CONTINUE_PENDING |
Сервис начинает запускаться после временной остановки, но еще не работает |
SERVICE_PAUSE_PENDING |
Сервис начинает переход в состояние временной остановки, но еще не остановился |
SERVICE_PAUSED |
Сервис находится в состоянии верменной остановки |
Задавая различные значения в поле dwControlsAccepted, вы можете указать, какие команды обрабатывает сервис. Ниже приведен список возможных значений:
Значение |
Команды,, которые может воспринимать сервис |
SERVICE_ACCEPT_STOP |
Команда остановки сервиса SERVICE_CONTROL_STOP |
SERVICE_ACCEPT_PAUSE_CONTINUE |
Команды временной остановки SERVICE_CONTROL_PAUSE и продолжения работы после временной остановки SERVICE_CONTROL_CONTINUE |
SERVICE_ACCEPT_SHUTDOWN |
Команда остановки при завершении работы операционной системы SERVICE_CONTROL_SHUTDOWN |
Значение в поле dwWin32ExitCode определяет код ошибки WIN32, который используется для сообщения о возникновении ошибочной ситуации при запуске и остановки сервиса. Если в этом поле указать значение ERROR_SERVICE_SPECIFIC_ERROR, то будет использован специфический для данного сервиса код ошибки, указанной в поле dwServiceSpecificExitCode структуры SERVICE_STATUS. Если ошибки нет, в поле dwWin32ExitCode необходимо записать значение NO_ERROR.
Поле dwServiceSpecificExitCode используется в том случае, когда в поле dwWin32ExitCode указано значение ERROR_SERVICE_SPECIFIC_ERROR.
Теперь о поле dwCheckPoint.
Это поле должно содержать значение, которое должно периодически увеличиваться при выполнении длительных операций запуска, остановки или продолжения работы после временной остановки. Если выполняются другие операции, в это поле необходимо записать нулевой значение.
Содержимое поля dwWaitHint определяет ожидаемое время выполнения (в миллисекундах) длительной операции запуска, остановки или продолжения работы после временной остановки. Если за указанное время не изменится содержимое полей dwCheckPoint или dwCurrentState, процесс управления сервисами будет считать, что произошла ошибка.
В наших примерах для сообщения текущего состояния сервиса процессу управления сервисами мы используем функцию ReportStatus, исходный текст которой приведен ниже:
void ReportStatus(DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwWaitHint) { static DWORD dwCheckPoint = 1; if(dwCurrentState == SERVICE_START_PENDING) ss.dwControlsAccepted = 0; else ss.dwControlsAccepted = SERVICE_ACCEPT_STOP; ss.dwCurrentState = dwCurrentState; ss.dwWin32ExitCode = dwWin32ExitCode; ss.dwWaitHint = dwWaitHint; if((dwCurrentState == SERVICE_RUNNING) || (dwCurrentState == SERVICE_STOPPED)) ss.dwCheckPoint = 0; else ss.dwCheckPoint = dwCheckPoint++; SetServiceStatus(ssHandle, &ss); }
При заполнении структуры SERVICE_STATUS эта функция проверяет содержимое поля dwCurrentState. Если сервис находится в состоянии ожидания запуска, в поле допустимых команд dwControlsAccepted записывается нулевое значение. В противном случае функция записывает туда значение SERVICE_ACCEPT_STOP, в результате чего сервису может быть передана команда остановки. Далее функция заполняет поля dwCurrentState, dwWin32ExitCode и dwWaitHint значениями, полученными через параметры.
В том случае, когда сервис выполняет команды запуска или остановки, функция увеличивает значение счетчика шагов длительных операций dwCheckPoint. Текущее значение счетчика хранится в статической переменной dwCheckPoint, определенной в нашей функции.
После подготовки структуры SERVICE_STATUS ее адрес передается функции установки состояния сервиса SetServiceStatus.
Для определения текущего состояния сервиса вы можете использовать функцию QueryServiceStatus, прототип которой приведен ниже:
BOOL QueryServiceStatus( SC_HANDLE schService, // идентификатор сервиса LPSERVICE_STATUS lpssServiceStatus); // адрес структуры // SERVICE_STATUS
Идентификатор сервиса вы можете получить от функций OpenService или CreateService, которые будут описаны ниже.
Вы можете создать приложение или сервис, управляющее сервисами. В этом разделе мы рассмотрим основные функции программного интерфейса WIN32, предназначенные для управления сервисами. Более подробную информацию вы найдете в документации SDK.
Идентификатор системы управления сервисами нужен для выполнения различных операций над сервисами, таких например, как установка сервиса. Вы можете получить этот идентификатор с помощью функции OpenSCManager:
SC_HANDLE OpenSCManager( LPCTSTR lpszMachineName, // адрес имени рабочей станции LPCTSTR lpszDatabaseName, // адрес имени базы данных DWORD fdwDesiredAccess); // нужный тип доступа
Задавая имя рабочей станции через параметр lpszMachineName, вы можете получить идентификатор системы управления сервисами на любом компьютере сети. Для локального компьютера необходимо указать значение NULL.
Для наших примеров параметр lpszDatabaseName, определяющий имя базы данных системы управления сервисами, нужно указать как NULL. При этом по умолчанию будет использована база данных активных сервисов ServicesActive.
Через параметр fdwDesiredAccess нужно задать требуемый тип доступа. Здесь можно использовать следующие константы:
Константа |
Разрешенный тип доступа |
SC_MANAGER_ALL_ACCESS |
Полный доступ |
SC_MANAGER_CONNECT |
Подключение к системе управления сервисами |
SC_MANAGER_CREATE_SERVICE |
Создание новых сервисов и добавление их к регистрационной базе данных |
SC_MANAGER_ENUMERATE_SERVICE |
Просмотр списка всех установленных сервисов при попщи функции EnumServicesStatus (в нашей книге эта функция и следующие две функции не описаны) |
SC_MANAGER_LOCK |
Блокирование базыв данных функцией LockServiceDatabase |
SC_MANAGER_QUERY_LOCK_STATUS |
Определение состояние блокировки базы данных функцией QueryServiceLockStatus |
Ниже мы привели пример вызова функции OpenSCManager:
schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
После использования вы должны закрыть идентификатор, полученный от функции OpenSCManager. Для этого необходимо вызвать функцию CloseServiceHandle:
CloseServiceHandle(schSCManager);
Для установки сервиса в систему вы должны использовать функцию CreateService, которая вносит все необходимые дополнения в регистрационную базу данных.
Прототип функции CreateService мы привели ниже:
SC_HANDLE CreateService( SC_HANDLE hSCManager, // идентификатор базы данных системы // управления сервисами LPCTSTR lpServiceName, // имя сервиса, которое будет использовано // для запуска LPCTSTR lpDisplayName, // имя сервиса для отображения DWORD dwDesiredAccess, // тип доступа к сервису DWORD dwServiceType, // тип сервиса DWORD dwStartType, // способ запуска сервиса DWORD dwErrorControl, // действия при ошибках в момент запуска LPCTSTR lpBinaryPathName, // путь к загрузочному файлу сервиса LPCTSTR lpLoadOrderGroup, // имя группы порядка загрузки LPDWORD lpdwTagId, // адрес переменной для сохранения // идентификатора тега LPCTSTR lpDependencies, // адрес массива имен взаимосвязей LPCTSTR lpServiceStartName, // адрес имени пользователя, права // которого будут применены для работы сервиса LPCTSTR lpPassword ); // адрес пароля пользователя
Через параметр hSCManager вы должны передать функции CreateService идентификатор базы данных системы управления сервисами, полученный от функции OpenSCManager, описанной выше.
Через параметры lpServiceName и lpDisplayName задаются, соответственно, имя сервиса, которое будет использовано для запуска и имя сервиса для отображения в списке установленных сервисов.
С помощью параметра dwDesiredAccess вы должны указать тип доступа, разрешенный при обращении к данному сервису. Здесь вы можете указать следующие значения:
Значение |
Разрешенный тип доступа |
SERVICE_ALL_ACCESS |
Полный доступ |
SERVICE_CHANGE_CONFIG |
Изменение конфигурации сервиса функцией ChangeServiceConfig |
SERVICE_ENUMERATE_DEPENDENTS |
Просмотр сервиса в списке сервисов, созданных на базе данного сервиса функцией EnumDependentServices |
SERVICE_INTERROGATE |
Выдача сервису команды немедленного определения текущего состояния сервиса при помощи функции ControlService (эта функция будет описана ниже) |
SERVICE_PAUSE_CONTINUE |
Временная остановка сервиса или продолжение работы после временной остановки |
SERVICE_QUERY_CONFIG |
Определение текущей конфигурации функцией QueryServiceConfig |
SERVICE_QUERY_STATUS |
Определение текущего состояния сервиса функцией QueryServiceStatus |
SERVICE_START |
Запуск сервиса функцией StartService |
SERVICE_STOP |
Остановка сервиса выдачей соответствующей команды функцией ControlService |
SERVICE_USER_DEFINE_CONTROL |
Возможность передачи сервису команды, определенной пользователем, с помощью функции ControlService |
Через параметр dwServiceType необходимо передать тип сервиса. Здесь вы можете указывать те же самые флаги, что и в поле dwServiceType структуры SERVICE_STATUS, описанной выше:
Флаг |
Описание |
SERVICE_WIN32_OWN_PROCESS |
Сервис работает как отдельный процесс |
SERVICE_WIN32_SHARE_PROCESS |
Сервис работает вместе с другими сервисами в рамках одного и того же процесса |
SERVICE_KERNEL_DRIVER |
Сервис представляет собой драйвер операционной системы Microsoft Windows NT |
SERVICE_FILE_SYSTEM_DRIVER |
Сервис является драйвером файловой системы |
SERVICE_INTERACTIVE_PROCESS |
Сервисный процесс может взаимодействовать с программным интерфейсом рабочего стола Desktop |
В параметре dwStartType указывается один из следующих способов запуска сервиса:
Константа |
Способ запуска |
SERVICE_BOOT_START |
Используется только для сервисов типа SERVICE_KERNEL_DRIVER или SERVICE_FILE_SYSTEM_DRIVER (драйверы). Указывает, что драйвер должен загружаться при загрузке операционной системы |
SERVICE_SYSTEM_START XE |
Аналогично предыдущему, но драйвер запускается при помощи функции IoInitSystem, не описанной в нашей книге |
SERVICE_AUTO_START |
Драйвер или обычный сервис, который запускается при загрузке операционной системы |
SERVICE_DEMAND_START |
Драйвер или обычный сервис, который запускается функцией StartService |
SERVICE_DISABLED |
Отключение возможности запуска драйвера или обычного сервиса |
Параметр dwErrorControl задает действия, выполняемые при обнаружении ошибки в момент загрузки сервиса. Здесь можно указывать одно из следующих значений:
Значение |
Реакция на ошибку |
SERVICE_ERROR_IGNORE |
Протоколирование ошибки в системном журнале и продолжение процедуры запуска сервиса |
SERVICE_ERROR_NORMAL |
Протоколирование ошибки в системном журнале без продолжения процедуры запуска сервиса |
SERVICE_ERROR_SEVERE |
Протоколирование ошибки в системном журнале. Если это возможно, используется конфигурация, с которой сервис успешно был запущен в прошлый раз. В противном случае система перезапускается с использованием работоспособной конфигурации |
SERVICE_ERROR_CRITICAL |
Криичная ошибка. Сообщение при возможности записывается в системный журнал. Операция запуска отменяется, система перезапускается с с использованием работоспособной конфигурации |
В параметре lpBinaryPathName вы должны указать полный путь к загрузочному файлу сервиса.
Через параметр lpLoadOrderGroup передается указатель на имя группы порядка загрузки сервиса. Сделав сервис членом одной из групп порядка загрузки, вы можете определить последовательность загрузки вашего сервиса относительно других сервисов. Список групп порядка загрузки находится в регистрационной базе данных:
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\ServiceGroupOrder
Если относительный порядок загрузки не имеет значения, укажите для параметра lpLoadOrderGroup значение NULL.
Параметр lpdwTagId используется только в том случае, если значение параметра lpLoadOrderGroup не равно NULL.
Параметр lpDependencies должен содержать указатель на массив строк имен сервисов или групп порядка загрузки, которые должны быть запущены перед запуском данного сервиса. Последняя строка такого массива должна быть закрыта двумя двоичными нулями. Если зависимостей от других сервисов нет, для параметра lpDependencies можно указать значение NULL.
Последние два параметра функции lpServiceStartName и lpPassword указывают, соответственно, имя и пароль пользователя, с правами которого данный сервис будет работать в системе (имя указывается в форме “ИмяДомена\имяПользователя”). Если параметр lpServiceStartName указан как NULL, сервис подключится к системе как пользователь LocalSystem. При этом параметр lpPassword должен быть указан как NULL.
Ниже мы привели фрагмент исходного текста приложения, в котором выполняется установка сервиса из каталога c:\ntbk2\src\service\small\debug:
schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); schService = CreateService( schSCManager, MYServiceName, MYServiceName, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, "c:\\ntbk2\\src\\service\\small\\debug\\small.exe", NULL, NULL, "", NULL, NULL); CloseServiceHandle(schSCManager);
Для выполнения операций с сервисом вы должны получить его идентификатор. Это нетрудно сделать с помощью функции OpenService, прототип которой мы привели ниже:
SC_HANDLE OpenService( SC_HANDLE schSCManager, // идентификатор базы данных системы // управления сервисами LPCTSTR lpszServiceName, // имя сервиса DWORD fdwDesiredAccess); // тип доступа к сервису
Через параметр schSCManager вы должны передать функции OpenService идентификатор базы данных системы управления сервисами, полученный от функции OpenSCManager.
Параметр lpszServiceName определяет имя сервиса, а параметр fdwDesiredAccess - желаемый тип доступа к сервису.
Приложение или сервис может выдать команду сервису, вызвав функцию ControlService:
BOOL ControlService( SC_HANDLE hService, // идентификатор сервиса DWORD dwControl, // код команды LPSERVICE_STATUS lpServiceStatus); // адрес структуры состояния // сервиса SERVICE_STATUS
В качестве кода команды вы можете указать один из следующих стандартных кодов:
Код |
Команда |
SERVICE_CONTROL_STOP |
Остановка сервиса |
SERVICE_CONTROL_PAUSE |
Временная остановка сервиса |
SERVICE_CONTROL_CONTINUE |
Продолжение работы сервиса после временной установки |
SERVICE_CONTROL_INTERROGATE |
Запрос текущего состояния сервиса |
Дополнительно вы можете указывать коды команд, определенные вами. Они должны находиться в интервале значений от 128 до 255.
Для удаления сервиса из системы используется функция DeleteService. В качетсве единственного параметра этой функции необходимо передать идентификатор сервиса, полученный от функции OpenService.
Ниже мы привели фрагмент приложения, удаляющий сервис с именем MYServiceName из системы:
schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); schService = OpenService( schSCManager, MYServiceName, SERVICE_ALL_ACCESS); ControlService(schService, SERVICE_CONTROL_STOP, &ss); DeleteService(schService); CloseServiceHandle(schSCManager);
Заметим, что перед удалением мы останавливаем сервис.
Для запуска сервиса вы должны использовать функцию StartService:
BOOL StartService( SC_HANDLE schService, // идентификатор сервиса DWORD dwNumServiceArgs, // количество аргументов LPCTSTR *lpszServiceArgs); // адрес массива аргументов
Через параметр schService вы должны передать функции StartService идентификатор сервиса, полученный от функции OpenService.
Параметры dwNumServiceArgs и lpszServiceArgs определяют, соответственно, количество аргументов и адрес массива аргументов, которые получит функция точки входа сервиса. Эти параметры могут использоваться в процессе инициализации.
Ниже мы привели фрагмент исходного текста приложения, выполняющий запуск сервиса:
schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); schService = OpenService( schSCManager, MYServiceName, SERVICE_ALL_ACCESS); StartService(schService, 0, NULL); CloseServiceHandle(schSCManager);
Остановка сервиса выполняется посылкой сервису команды SERVICE_CONTROL_STOP, для чего эта комнда передается функции ControlService XE "ControlService" :
ControlService(schService, SERVICE_CONTROL_STOP, &ss);
Приложение или сервис может определить конфигурацию заданного сервиса, вызвав для этого функцию QueryServiceConfig XE "QueryServiceConfig" :
BOOL QueryServiceConfig( SC_HANDLE schService, // идентификатор сервиса LPQUERY_SERVICE_CONFIG lpqscServConfig, // адрес структуры // QUERY_SERVICE_CONFIG, в которую будет // записана конфигурация сервиса DWORD cbBufSize, // размер буфера для записи конфигурации LPDWORD lpcbBytesNeeded); // адрес переменной, в котоую будет // записан размер буфера, необходимый для // сохранения всей информации о конфигурации
Формат структуры QUERY_SERVICE_CONFIG приведен ниже:
typedef struct _QUERY_SERVICE_CONFIG { DWORD dwServiceType; DWORD dwStartType; DWORD dwErrorControl; LPTSTR lpBinaryPathName; LPTSTR lpLoadOrderGroup; DWORD dwTagId; LPTSTR lpDependencies; LPTSTR lpServiceStartName; LPTSTR lpDisplayName; } QUERY_SERVICE_CONFIG, LPQUERY_SERVICE_CONFIG;
Содержимое полей этой структуры соответствует содержимому параметров функции CreateService, описанной ранее.
Ниже расположен фрагмент кода, в котором определяется текущая конфигурация сервиса:
schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); schService = OpenService( schSCManager, MYServiceName, SERVICE_ALL_ACCESS); lpBuf = (LPQUERY_SERVICE_CONFIG)malloc(4096); if(lpBuf != NULL) { QueryServiceConfig(schService, lpBuf, 4096, &dwBytesNeeded); . . . free(lpBuf); }
В этом разделе мы приведем исходные тексты простейшего сервиса Simple и приложения SRVCTRL, с помощью которого можно установить данный сервис, запустить, остановить или удалить его, а также определить текущую конфигурацию.
Главное меню приложения SRVCTRL и временное меню Service показано на рис. 5.4.
Рис. 5.4. Главное меню приложения SRVCTRL
С помощью строки Install вы можете установить сервис. Не забудьте перед этим записать загрузочный файл сервиса в каталог c:\ntbk2\src\service\small\debug, так как приложение SRVCTRL может установить сервис только из этого каталога.
После установки имя сервиса появится в списке сервисов, который можно просмотреть при помощи приложения Services из папки Control Panel (рис. 5.5).
Рис. 5.5. В списке сервисов появился новый сервис Sample of simple service
Если теперь из меню Service нашего приложения выбрать строку Get configuration, на экране появится диалоговая панель, в которой будут отображены некоторые поля структуры конфигрурации сервиса (рис. 5.6).
Рис. 5.6. Просмотр конфигурации сервиса
Исходный текст сервиса представлен в листинге 5.1. Так как ранее мы уже подробно описывали структуру этого сервиса, то мы оставим вам этот листинг и листинг приложения SRVCTRL на самостоятельное изучение.
Листинг 5.1. Файл service/small/small.c
// ================================================== // Сервис "Sample of simple service" // Шаблон простейшего сервиса Windows NT // // (С) Фролов А.В., 1996 // Email: frolov@glas.apc.org // ================================================== #define STRICT #include <windows.h> #include <windowsx.h> #include <stdio.h> #include <conio.h> #include <stdlib.h> #include "small.h" // ----------------------------------------------------- // Глобальные переменные // ----------------------------------------------------- // Код ошибки DWORD dwErrCode; // Текущее состояние сервиса SERVICE_STATUS ss; // Идентификатор сервиса SERVICE_STATUS_HANDLE ssHandle; // ----------------------------------------------------- // Функция main // Точка входа процесса // ----------------------------------------------------- void main(int agrc, char *argv[]) { // Таблица точек входа SERVICE_TABLE_ENTRY DispatcherTable[] = { { // Имя сервиса MYServiceName, // Функция main сервиса (LPSERVICE_MAIN_FUNCTION)ServiceMain }, { NULL, NULL } }; printf("Sample of simple service\n" "(C) A. Frolov, 1996, Email: frolov@glas.apc.org\n"); // Запуск диспетчера if(!StartServiceCtrlDispatcher(DispatcherTable)) { fprintf(stdout, "StartServiceCtrlDispatcher: Error %ld\n", GetLastError()); getch(); return; } } // ----------------------------------------------------- // Функция ServiceMain // Точка входа сервиса // ----------------------------------------------------- void WINAPI ServiceMain(DWORD argc, LPSTR *argv) { // Регистрируем управляющую функцию сервиса ssHandle = RegisterServiceCtrlHandler(MYServiceName, ServiceControl); if(!ssHandle) return; // Устанавливаем состояние сервиса // Сервис работает как отдельный процесс ss.dwServiceType = SERVICE_WIN32_OWN_PROCESS; // Код ошибки при инициализации и завершения сервиса // не используется ss.dwServiceSpecificExitCode = 0; // Начинаем запуск сервиса. // Прежде всего устанавливаем состояние ожидания // запуска сервиса ReportStatus(SERVICE_START_PENDING, NO_ERROR, 4000); // Вызываем функцию, которая выполняет все // необходимые инициализирующие действия // ServiceStart(argc, argv); // После завершения инициализации устанавливаем // состояние работающего сервиса ReportStatus(SERVICE_RUNNING, NOERROR, 0); return; } // ----------------------------------------------------- // Функция ServiceControl // Точка входа функции обработки команд // ----------------------------------------------------- void WINAPI ServiceControl(DWORD dwControlCode) { // Анализируем код команды и выполняем эту команду switch(dwControlCode) { // Команда остановки сервиса case SERVICE_CONTROL_STOP: { // Устанавливаем состояние ожидания остановки ss.dwCurrentState = SERVICE_STOP_PENDING; ReportStatus(ss.dwCurrentState, NOERROR, 0); // Выполняем остановку сервиса, вызывая функцию, // которая выполняет все необходимые для этого действия // ServiceStop(); // Отмечаем состояние как остановленный сервис ReportStatus(SERVICE_STOPPED, NOERROR, 0); break; } // Определение текущего состояния сервиса case SERVICE_CONTROL_INTERROGATE: { // Возвращаем текущее состояние сервиса ReportStatus(ss.dwCurrentState, NOERROR, 0); break; } // В ответ на другие команды просто возвращаем // текущее состояние сервиса default: { ReportStatus(ss.dwCurrentState, NOERROR, 0); break; } } } // ----------------------------------------------------- // Функция ReportStatus // Посылка состояния сервиса системе управления сервисами // ----------------------------------------------------- void ReportStatus(DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwWaitHint) { // Счетчик шагов длительных операций static DWORD dwCheckPoint = 1; // Если сервис не находится в процессе запуска, // его можно остановить if(dwCurrentState == SERVICE_START_PENDING) ss.dwControlsAccepted = 0; else ss.dwControlsAccepted = SERVICE_ACCEPT_STOP; // Сохраняем состояние, переданное через // параметры функции ss.dwCurrentState = dwCurrentState; ss.dwWin32ExitCode = dwWin32ExitCode; ss.dwWaitHint = dwWaitHint; // Если сервис не работает и не остановлен, // увеличиваем значение счетчика шагов // длительных операций if((dwCurrentState == SERVICE_RUNNING) || (dwCurrentState == SERVICE_STOPPED)) ss.dwCheckPoint = 0; else ss.dwCheckPoint = dwCheckPoint++; // Вызываем функцию установки состояния SetServiceStatus(ssHandle, &ss); }
В файле small.h (листинг 5.2) определено имя сервиса MYServiceName и прототипы функций.
Листинг 5.2. Файл service/small/small.h
#define MYServiceName "Sample of simple service" void WINAPI ServiceMain(DWORD dwArgc, LPSTR *lpszArv); void WINAPI ServiceControl(DWORD dwControlCode); void ReportStatus(DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwWaitHint);
Главный файл исходных текстов приложения SRVCTRL, предназначенного для управления сервисом Sample of simple service приведен в листинге 5.3.
Листинг 5.3. Файл service/srvctrl.c
// ================================================== // Приложение SRVCTRL // Работа с сервисом "Sample of simple service" // // (С) Фролов А.В., 1996 // Email: frolov@glas.apc.org // ================================================== #define STRICT #include <windows.h> #include <windowsx.h> #include "resource.h" #include "afxres.h" #include "srvctrl.h" HINSTANCE hInst; char szAppName[] = "ServiceCtlApp"; char szAppTitle[] = "Simple Service Control"; // Состояние сервиса SERVICE_STATUS ss; // ----------------------------------------------------- // Функция WinMain // ----------------------------------------------------- int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASSEX wc; HWND hWnd; MSG msg; // Сохраняем идентификатор приложения hInst = hInstance; // Преверяем, не было ли это приложение запущено ранее hWnd = FindWindow(szAppName, NULL); if(hWnd) { // Если было, выдвигаем окно приложения на // передний план if(IsIconic(hWnd)) ShowWindow(hWnd, SW_RESTORE); SetForegroundWindow(hWnd); return FALSE; } // Регистрируем класс окна memset(&wc, 0, sizeof(wc)); wc.cbSize = sizeof(WNDCLASSEX); wc.hIconSm = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICONSM), IMAGE_ICON, 16, 16, 0); wc.style = 0; wc.lpfnWndProc = (WNDPROC)WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst; wc.hIcon = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICON), IMAGE_ICON, 32, 32, 0); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); wc.lpszMenuName = MAKEINTRESOURCE(IDR_APPMENU); wc.lpszClassName = szAppName; if(!RegisterClassEx(&wc)) if(!RegisterClass((LPWNDCLASS)&wc.style)) return FALSE; // Создаем главное окно приложения hWnd = CreateWindow(szAppName, szAppTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInst, NULL); if(!hWnd) return(FALSE); // Отображаем окно и запускаем цикл // обработки сообщений ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } // ----------------------------------------------------- // Функция WndProc // ----------------------------------------------------- LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { HANDLE_MSG(hWnd, WM_COMMAND, WndProc_OnCommand); HANDLE_MSG(hWnd, WM_DESTROY, WndProc_OnDestroy); default: return(DefWindowProc(hWnd, msg, wParam, lParam)); } } // ----------------------------------------------------- // Функция WndProc_OnDestroy // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnDestroy(HWND hWnd) { PostQuitMessage(0); return 0L; } // ----------------------------------------------------- // Функция WndProc_OnCommand // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify) { // Идентификатор сервиса SC_HANDLE schService; // Идентификатор системы управления сервисами SC_HANDLE schSCManager; LPQUERY_SERVICE_CONFIG lpBuf; DWORD dwBytesNeeded; char szBuf[1024]; switch (id) { case ID_FILE_EXIT: { // Завершаем работу приложения PostQuitMessage(0); return 0L; break; } // Установка сервиса в систему case ID_SERVICE_INSTALL: { // Открываем систему управления сервисами schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); if(!schSCManager) break; // Создаем сервис с именем MYServiceName schService = CreateService( schSCManager, MYServiceName, MYServiceName, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, "c:\\ntbk2\\src\\service\\small\\debug\\small.exe", NULL, NULL, "", NULL, NULL); // Закрываем идентификатор системы управления // сервисами CloseServiceHandle(schSCManager); break; } // Удаление сервиса из системы case ID_SERVICE_REMOVE: { // Открываем систему управления сервисами schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); if(!schSCManager) break; // Открываем сервис с именем MYServiceName schService = OpenService( schSCManager, MYServiceName, SERVICE_ALL_ACCESS); if(!schService) break; // Останавливаем сервис ControlService(schService, SERVICE_CONTROL_STOP, &ss); // Вызываем функцию удаления сервиса из системы DeleteService(schService); // Закрываем идентификатор системы управления // сервисами CloseServiceHandle(schSCManager); break; } case ID_SERVICE_START: { // Открываем систему управления сервисами schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); if(!schSCManager) break; // Открываем сервис с именем MYServiceName schService = OpenService( schSCManager, MYServiceName, SERVICE_ALL_ACCESS); if(!schService) break; // Запускаем сервис StartService(schService, 0, NULL); // Закрываем идентификатор системы управления // сервисами CloseServiceHandle(schSCManager); break; } case ID_SERVICE_STOP: { // Открываем систему управления сервисами schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); if(!schSCManager) break; // Открываем сервис с именем MYServiceName schService = OpenService( schSCManager, MYServiceName, SERVICE_ALL_ACCESS); // Останавливаем сервис ControlService(schService, SERVICE_CONTROL_STOP, &ss); // Закрываем идентификатор системы управления // сервисами CloseServiceHandle(schSCManager); break; } case ID_SERVICE_GETCONFIGURATION: { // Открываем систему управления сервисами schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); if(!schSCManager) break; // Открываем сервис с именем MYServiceName schService = OpenService( schSCManager, MYServiceName, SERVICE_ALL_ACCESS); if(!schService) break; // Получаем буфер для сохранения конфигурации lpBuf = (LPQUERY_SERVICE_CONFIG)malloc(4096); if(lpBuf != NULL) { // Сохраняем конфигурацию в буфере QueryServiceConfig(schService, lpBuf, 4096, &dwBytesNeeded); // Отображаем некоторые поля конфигурации wsprintf(szBuf, "Binary path: %s\n" "Start Name: %s\n" "Display Name: %s\n", lpBuf->lpBinaryPathName, lpBuf->lpServiceStartName, lpBuf->lpDisplayName); MessageBox(hWnd, szBuf, szAppTitle, MB_OK | MB_ICONINFORMATION); // Освобождаем буфер free(lpBuf); } // Закрываем идентификатор системы управления // сервисами CloseServiceHandle(schSCManager); break; } case ID_HELP_ABOUT: { MessageBox(hWnd, "Simple Service Control\n" "(C) Alexandr Frolov, 1996\n" "Email: frolov@glas.apc.org", szAppTitle, MB_OK | MB_ICONINFORMATION); return 0L; break; } default: break; } return FORWARD_WM_COMMAND(hWnd, id, hwndCtl, codeNotify, DefWindowProc); }
В файле srvctrl.h (листинг 5.4) определено имя сервиса и прототипы функций.
Листинг 5.4. Файл service/srvctrl.h
// Имя сервиса #define MYServiceName "Sample of simple service" // ----------------------------------------------------- // Описание функций // ----------------------------------------------------- LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); void WndProc_OnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify); void WndProc_OnDestroy(HWND hWnd); LRESULT WINAPI DlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam); BOOL DlgProc_OnInitDialog(HWND hdlg, HWND hwndFocus, LPARAM lParam); void DlgProc_OnCommand(HWND hdlg, int id, HWND hwndCtl, UINT codeNotify);
Файл resource.h (листинг 5.5) содержит описания констант, которые используются в файле определения ресурсов приложения.
Листинг 5.5. Файл service/resource.h
//{{NO_DEPENDENCIES}} // Microsoft Developer Studio generated include file. // Used by srvctrl.rc // #define IDR_MENU1 101 #define IDR_APPMENU 101 #define IDI_APPICON 102 #define IDI_APPICONSM 103 #define ID_FILE_EXIT 40001 #define ID_HELP_ABOUT 40002 #define ID_SERVICE_INSTALL 40010 #define ID_SERVICE_REMOVE 40011 #define ID_SERVICE_START 40012 #define ID_SERVICE_STOP 40013 #define ID_SERVICE_GETCONFIGURATION 40014 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 105 #define _APS_NEXT_COMMAND_VALUE 40015 #define _APS_NEXT_CONTROL_VALUE 1006 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
Файл определения ресурсов приложения приведен в листинге 5.6.
Листинг 5.6. Файл service/srvctrl.rc
//Microsoft Developer Studio generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ////////////////////////////////////////////////////////////// // Russian resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_RUS) #ifdef _WIN32 LANGUAGE LANG_RUSSIAN, SUBLANG_DEFAULT #pragma code_page(1251) #endif //_WIN32 ////////////////////////////////////////////////////////////// // // Menu // IDR_APPMENU MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "E&xit", ID_FILE_EXIT END POPUP "&Service" BEGIN MENUITEM "&Install", ID_SERVICE_INSTALL MENUITEM "&Remove", ID_SERVICE_REMOVE MENUITEM SEPARATOR MENUITEM "&Start", ID_SERVICE_START MENUITEM "Sto&p", ID_SERVICE_STOP MENUITEM SEPARATOR MENUITEM "&Get configuration", ID_SERVICE_GETCONFIGURATION END POPUP "&Help" BEGIN MENUITEM "&About...", ID_HELP_ABOUT END END #ifdef APSTUDIO_INVOKED ////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE DISCARDABLE BEGIN "resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE DISCARDABLE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure // application icon // remains consistent on all systems. IDI_APPICON ICON DISCARDABLE "srvctrl.ico" IDI_APPICONSM ICON DISCARDABLE "srvctrsm.ico" #endif // Russian resources ////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED