SFL – Service Framework Library

Каркас для написания служб Windows

Автор: Igor Vartanov
The RSDN Group

Источник: RSDN Magazine #6-2004
Опубликовано: 14.03.2005
Версия текста: 1.0
Нечто вроде вступления…
Термины
Краткое описание
Ограничения текущей версии
Введение
Минимальное приложение-служба в исполнении SFL
MinSvc – минимальное сервис-приложение в стиле Windows NT
…и его анализ
Сервис в стиле Windows 2000
MidSvc – приложение-сервис в стиле Windows 2000
Иерархия классов SFL
Классы приложения
Классы сервиса
Класс переходника
Характерные точки входа каркаса
Старт приложения
Старт сервиса
Остановка сервиса
Остановка приложения
Обработка состояний ожидания
О дополнительных возможностях
Имена сервисов
Несколько в одном
Немного о клонах
Варианты конструирования экземпляров сервисов
Несколько слов об инсталляции сервисов
Благодарности
Warranties and Liabilities


I deserve nothing more then I get.
‘Cos nothing I have is truly mine. 
Dido. «Life For Rent»

Исходный текст библиотеки – SflBase.h
Демонстрационный пример – MinSvc
Демонстрационный пример – MidSvc

Нечто вроде вступления…

В переписке с одним из членов RSDN Team я как-то неосторожно заявил, что не пишу сервисы направо и налево, подразумевая, что пишу я их очень редко. Да, я ошибался. Случилось так, что я был вынужден за достаточно короткий срок написать несколько сервисов – сначала один, и затем, спустя совсем небольшое время, еще парочку. Приступив к написанию второго, я вдруг почувствовал острое ощущение бессмысленности траты времени на тупое копирование типового кода. А впереди ведь ожидал еще и третий проект… Поэтому работа над вторым сервисом была отложена в сторону (по принципу «лучше день потерять, зато потом за пять минут долететь»), и был написан код, впоследствии легший в основу SFL.

Термины

В этом документе термины «сервис» и «служба» означают одно и то же и применяются равноправно, если явно не указано обратное. То же самое относится к терминам «каркас» и «библиотека» – в обоих случаях имеется в виду SFL.

Краткое описание

SFL v1.0 позволяет создавать приложения-сервисы Windows, пригодные для использования на Win32-платформах: Windows NT/2000/XP/2003. Создаваемый код проверялся на совместимость с компиляторами Visual C++ 6.0, 7.1 и 8.0 beta. Библиотека предоставляет объектную модель приложения-контейнера сервисов, позволяя создавать как own process, так и shared process приложения. Количество сервисов, помещаемых в одно приложение, ничем не ограничено. Предполагается, что сервисы будут выполнены в виде С++-классов, наследуемых от базовых шаблонных классов библиотеки. Библиотека позволяет создавать сервисы как в стиле Windows NT, так и в стиле Windows 2000.

Ограничения текущей версии

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

Кроме того, в коде текущей версии используется технология переходников – исполняемого кода, располагаемого в секции данных. Это естественным образом ограничивает применение библиотеки – код будет выполняться лишь на iX86-архитектурах, поскольку только для нее и реализованы переходники.

Возможно, когда-нибудь я доберусь и до несколько другого железа. Но пока есть то, что есть.

Введение

Почему каркас? Во-первых, потому что сервисы Windows работают по достаточно жесткой схеме, которую следует неукоснительно соблюдать, если вы не хотите, чтобы ваша служба вела себя непредсказуемо. Во-вторых, поскольку существует «во-первых», постольку постоянное переписывание обязательного шаблонного кода доставляет весьма сомнительное удовольствие и является источником вероятных досадных ошибок, связанных с генерацией кода методом Copy-and-Paste. В-третьих, это просто удобно – иметь готовую объектно-ориентированную схему, избавляющую от вопросов типа «как сделать ServiceMain членом класса». Вы просто наследуете ваши классы сервисов от готового базового класса, добавляете горстку макросов… Очень хочется написать «и на этом все трудности заканчиваются», но, увы – легко догадаться, что не все в этой жизни просто. Но и не все так сложно, в чем мы имеем возможность немедленно удостовериться.

Минимальное приложение-служба в исполнении SFL

Классики рекомендуют «есть слона по кусочкам». Начнем с простейшей службы, которая не делает никакой полезной работы. Пока для нас не это будет главным – нам необходимо освоить методику создания сервисов средствами SFL. И первым кусочком слона (которого мы, уверяю вас, съедим) будет служба, которая умеет запускаться (Start) и завершаться (Stop), как всякая уважающая себя служба. Более того, по нашей прихоти мы обяжем ее уметь приостанавливаться (Pause) и возобновлять работу после приостановки (Continue).

ПРЕДУПРЕЖДЕНИЕ

Сразу хочу сказать – ликбеза по сервисам не будет. Вы должны хотя бы в общих чертах иметь представление о жизненном цикле сервиса, его типах и связанных с этим темах. Очень желательно знакомство с API, используемым для написания сервисов.

MinSvc – минимальное сервис-приложение в стиле Windows NT

Итак, пишем минимальный сервис средствами SFL. Для создания такого приложения необходимо написать класс приложения, унаследованный от шаблонного класса CServiceAppT<>, класс сервиса, унаследованный от шаблонного класса CServiceBaseT<> и карту сервисов, связывающую их в единое приложение.

        #include
        "stdafx.h"
        #include
        "SflBase.h"
        class CMyApp: public CServiceAppT<CMyApp>
{
};

class CMyService: public CServiceBaseT<CMyService,
 SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE >
{
  CMyService(LPCTSTR pszName): CServiceBaseClass(pszName)
  {
  }
#if(_MSC_VER < 1300)  
  friend class CServiceBaseT<CMyService, 
SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE >;
#endif
  SFL_DECLARE_SERVICECLASS_FRIENDS()

  SFL_BEGIN_CONTROL_MAP(CMyService)
    SFL_HANDLE_CONTROL_STOP()
    SFL_HANDLE_CONTROL_PAUSE()
    SFL_HANDLE_CONTROL_CONTINUE()
  SFL_END_CONTROL_MAP()

  DWORD OnStop(DWORD& /*dwWin32Err*/, DWORD& /*dwSpecificErr*/, 
    BOOL& bHandled)
  {
    bHandled = TRUE;
    return SERVICE_STOPPED;
  }
  DWORD OnPause(DWORD& /*dwWin32Err*/, DWORD& /*dwSpecificErr*/,
    BOOL& bHandled)
  {
    bHandled = TRUE;
    return SERVICE_PAUSED;
  }
  DWORD OnContinue(DWORD& /*dwWin32Err*/, DWORD& /*dwSpecificErr*/,
    BOOL& bHandled)
  {
    bHandled = TRUE;
    return SERVICE_RUNNING;
  }
};

SFL_BEGIN_SERVICE_MAP(CMyApp)
  SFL_SERVICE_ENTRY_(CMyService, TEXT("MyService"))
SFL_END_SERVICE_MAP()

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

…и его анализ

Заголовочные файлы

Рассмотрим характерные фрагменты этого кода. Секция включения заголовочных файлов содержит две инструкции #include, а именно – включение стандартного заголовочного файла stdafx.h, через который подключаются предкомпилируемые заголовки, и собственно заголовочный файл библиотеки SFL:

          #include
          "stdafx.h"
          #include
          "SflBase.h"
        
ПРЕДУПРЕЖДЕНИЕ

Следует помнить следующее – SFL создает приложение-сервис как консольное приложение, и именно этот тип приложения необходимо выбирать при создании проекта Visual Studio. Однако по умолчанию проект консольного приложения Visual Studio .NET подключает через stdafx.h заголовочные файлы STL и tchar.h, а Visual Studio 6 вообще не включает ни одного файла заголовков. Чтобы приложение успешно компилировалось, необходимо самостоятельно добавить в него включение заголовочного файла windows.h.

Класс приложения

Далее следует определение класса приложения CMyApp:

class CMyApp: public CServiceAppT<CMyApp>
{
};

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

Класс сервиса

Перейдем к рассмотрению класса сервиса.

          class CMyService: public CServiceBaseT<CMyService, 
  SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE >
{

Из определения класса CMyService видно, что он унаследован от шаблонного базового класса сервиса, специфицированного набором флагов, определяющих поведение нашего класса в отношении того, на какие управляющие воздействия SCM он способен реагировать. Опять-таки, ничего нового – как мы и намеревались, наш сервис будет реагировать на команды Stop, Pause и Continue.

  CMyService(LPCTSTR pszName): CServiceBaseClass(pszName)
  {
  }
#if(_MSC_VER < 1300)  
  friend class CServiceBaseT<CMyService, SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE >;
#endif
  SFL_DECLARE_SERVICECLASS_FRIENDS()

Класс сервиса имеет единственный конструктор, принимающий указатель на строку, заканчивающуюся нулем. Эта строка представляет собой имя, с которым наш сервис будет зарегистрирован в базе SCM.

ПРИМЕЧАНИЕ

Может возникнуть некоторое недоумение при виде вызова странного конструктора некоего класса CServiceBaseClass. Не надо волноваться по этому поводу – CServiceBaseClass есть всего лишь переименование базового класса, предоставляемое библиотекой для краткости и удобства. Правда, Visual C++ 6 издает по этому поводу warning C4097, но на это не стоит обращать слишком много внимания, тем более что VC++ 7.1 воспринимает этот код совершенно спокойно.

Но, как видим, у VC++ 6 есть другая проблема – он неправильно воспринимает переименование в объявлении друзей класса, поэтому для него эту конструкцию приходится прописывать полностью.

Назначение макроса SFL_DECLARE_SERVICECLASS_FRIENDS() станет понятно после того, как мы внимательно посмотрим на конструктор нашего класса – он объявлен как private. Чтобы библиотека имела возможность конструировать объекты, необходимо объявить соответствующих друзей для этого класса.

Вы можете объявить конструктор как public и забыть о макросе SFL_DECLARE_SERVICECLASS_FRIENDS().

Карта обработчиков управляющих кодов

Карта обработчиков управляющих кодов выглядит достаточно привычно для тех, кто имеет опыт общения с библиотеками MFC или ATL.

SFL_BEGIN_CONTROL_MAP(CMyService)
  SFL_HANDLE_CONTROL_STOP()
  SFL_HANDLE_CONTROL_PAUSE()
  SFL_HANDLE_CONTROL_CONTINUE()
SFL_END_CONTROL_MAP()

Каждый макрос SFL_HANDLE_CONTROL_xxx() связывает определенный управляющий код с его обработчиком. Для примера рассмотрим первый макрос – SFL_HANDLE_CONTROL_STOP(). Он разворачивается препроцессором в другой макрос в соответствии с определением:

#define SFL_HANDLE_CONTROL_STOP()        SFL_HANDLE_CONTROL( SERVICE_CONTROL_STOP, OnStop )

Обработчики

Теперь становится видно, что коду SERVICE_CONTROL_STOP ставится в соответствие метод OnStop() нашего класса сервиса.

  DWORD OnStop(DWORD& /*dwWin32Err*/, DWORD& /*dwSpecificErr*/, BOOL& bHandled )
  {
    bHandled = TRUE;
    return SERVICE_STOPPED;
  }

Познакомимся поближе с кодом метода-обработчика. Из прототипа метода видно, что в него передаются три ссылки – на dwWin32Err, dwSpecificErr и bHandled. Значения этих переменных обнулены перед вызовом обработчика. Последняя переменная, будучи установлена в TRUE, является признаком того, что управляющий код обработан. В этом случае значение, которое возвращается из метода, будет установлено как текущее состояние сервиса (SERVICE_STATUS::dwCurrentState).

ПРЕДУПРЕЖДЕНИЕ

Если bHandled не установить в TRUE, статус сервиса не будет изменен (SetServiceStatus() не будет вызвана), что по истечении таймаута будет воспринято SCM как ошибка.

Переменные dwWin32Err (ссылка на поле SERVICE_STATUS::dwWin32ExitCode структуры статуса) и dwSpecificErr (ссылка на поле SERVICE_STATUS::dwServiceSpecificExitCode) служат для сообщения системе о том, что при выполнении сервиса произошла ошибка. При этом возврат ненулевого значения dwWin32Err означает, что ошибка произошла в одной из функций Win32 API, и код возврата представляет собой код ошибки Windows. Ненулевой код dwSpecificErr означает, что ошибка произошла в логике самого обработчика (при этом значение dwWin32Err будет проигнорировано). Код возврата в этом случае определяется самим приложением.

ПРИМЕЧАНИЕ

Информация об ошибках проверяется системой лишь при выполнении остановки сервиса. Во всех остальных случаях она игнорируется.

Остальные обработчики принципиально ничем не отличаются от рассмотренного уже нами OnStop().

Карта сервисов приложения

Настало время познакомиться с картой сервисов приложения.

SFL_BEGIN_SERVICE_MAP(CMyApp)
  SFL_SERVICE_ENTRY_(CMyService, TEXT("MyService"))
SFL_END_SERVICE_MAP()

Как уже говорилось выше, карта сервисов связывает воедино класс приложения и класс сервиса. Макрос SFL_BEGIN_SERVICE_MAP() содержит объявление функции _tmain(), вызов функции SflGetServiceApp(), в которой конструируется статическая переменная класса приложения CMyApp, и определение массива, каждый из членов которого (за исключением последнего NULL) инициализируется указателем на конструируемый объект класса сервиса (о вариантах конструирования будет рассказано подробнее в разделе «О дополнительных возможностях»).

Макрос SFL_SERVICE_ENTRY_() определяет, объект какого именно класса сервиса будет конструироваться, и с каким именем этот сервис будет регистрироваться в базе SCM. Поскольку архитектура Windows позволяет, чтобы одно приложение-сервис содержало несколько сервисов, то карта сервисов может содержать один или больше макросов семейства SFL_SERVICE_ENTRY (об остальных вариантах этого макроса будет рассказано в разделе «О дополнительных возможностях»).

Макрос SFL_END_SERVICE_MAP() закрывает список инициализаторов массива указателей на объекты классов сервиса членом, равным NULL, и последовательно производит вызов методов PreMain(), Main(), и PostMain() класса приложения. О назначении вызовов PreMain() и PostMain() будет сказано в разделе «Характерные точки входа каркаса», вызов же Main() в итоге приводит к вызову StartServiceCtrlDispatcher() и, следовательно, старту всех сервисов, зарегистрированных приложением.

Сервис в стиле Windows 2000

Скорее всего, вам уже известно, что отличие сервисов Windows NT и Windows 2000 лежит в прототипе функции диспетчера-обработчика управляющих кодов сервиса – в MSDN она фигурирует как HandlerEx(). Ее отличие от старой версии состоит в том, что SCM способен передать через нее в обработчик дополнительные сведения – так называемый контекст и информацию об обрабатываемом событии. Контекст – это определенное пользователем некое значение, задаваемое при регистрации диспетчера-обработчика. Обычно (особенно, если сервис написан на C) туда кладут указатель на некую структуру, содержащую всю информацию, необходимую для правильного функционирования сервиса. Поскольку наши сервисы пишутся на C++, то класс сервиса уже сам по себе является такой информационной структурой (даже если мы пишем сервис в стиле Windows NT), и информация о контексте в сервисе в стиле Windows 2000 становится избыточной. Но получить расширенную информацию о событиях в системе (о изменениях в сетевых настройках, в составе аппаратуры и ее состояниях) иным способом невозможно. Поэтому, если ваш сервис предназначен для работы с аппаратными и сетевыми событиями, то выбора у вас нет.

MidSvc – приложение-сервис в стиле Windows 2000

В качестве конечной цели поставим слежение из нашего сервиса за состоянием привода CD-ROM и запись лога связанных с ним событий в некий текстовый файл. Полный код проекта можно посмотреть в архиве, прилагающемся к данной статье. Но на ключевые фрагменты кода мы посмотрим поближе.

Класс службы

Класс, как и в первом случае, содержит привычные конструктор и деструктор, объявление друзей класса, карту обработчиков управляющих кодов. Но нам будут интересны новые, ранее не встречавшиеся, элементы.

          #pragma once

class CDeviceService: public CServiceBaseT< CDeviceService, 
                                  SERVICE_ACCEPT_STOP |
                                  SERVICE_ACCEPT_HARDWAREPROFILECHANGE >
{
  CDeviceService(LPCTSTR pszName);
#if(_MSC_VER < 1300)
  friend class CServiceBaseT< CDeviceService, SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_HARDWAREPROFILECHANGE >;
#endif
  SFL_DECLARE_SERVICECLASS_FRIENDS()
  
  SFL_BEGIN_CONTROL_MAP_EX(CDeviceService)
    SFL_HANDLE_CONTROL_STOP()
    SFL_HANDLE_CONTROL_EX( SERVICE_CONTROL_DEVICEEVENT, OnDeviceChange )
  SFL_END_CONTROL_MAP_EX()

  SFL_DECLARE_VOID_SERVICE_CONTEXT()
  SFL_DECLARE_REGISTER_HANDLER_EX(CDeviceService)
  DWORD OnStop(DWORD& dwWin32Err, DWORD& dwSpecificErr, BOOL& bHandled);
  BOOL InitInstance(DWORD dwArgc, LPTSTR* lpszArgv, DWORD& dwSpecificErr);
  DWORD OnDeviceChange(DWORD& dwState, 
                       DWORD& dwWin32Err, 
                       DWORD& dwSpecificErr, 
                       BOOL&  bHandled, 
                       DWORD  dwEventType, 
                       LPVOID lpEventData, 
                       LPVOID lpContext);
  void LogEvent(DWORD dwEvent, LPVOID lpParam);

private:
  HDEVNOTIFY m_hDevNotify;
  LPTSTR     m_logfile;
};

Во-первых, появился флаг ACCEPT, характерный только для сервисов Windows 2000, –SERVICE_ACCEPT_HARDWAREPROFILECHANGE. Его наличие означает, что сервис необходимо оповещать об аппаратных событиях системы. Поскольку подобные события способен принимать сервис, имеющий расширенную версию обработчика управляющих кодов, то и карта обработчиков использует расширенный вариант - SFL_BEGIN_CONTROL_MAP_EX. Для регистрации в системе подобного обработчика требуется также наличие вызова функции регистрации обработчика, тоже имеющей расширенный формат (о технических подробностях можно справиться в MSDN в статье на тему RegisterServiceCtrlHandlerEx), поэтому в классе присутствует объявление такой функции регистрации SFL_DECLARE_REGISTER_HANDLER_EX. С ним же связано и наличие объявления «пустого» контекста сервиса SFL_DECLARE_VOID_SERVICE_CONTEXT: как мы уже ранее говорили, сам класс сервиса способен выполнять функции такого контекста, поэтому формально регистрируемый указатель на контекст может быть просто NULL, что и скрывается за данным макросом.

ПРИМЕЧАНИЕ

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

Ну и последнее – это несколько необычно выглядящий прототип обработчика OnDeviceChange, определяющего реакцию на SERVICE_CONTROL_DEVICEEVENT – на вид он отличается от того же OnStop наличием дополнительных параметров. При внимательном рассмотрении в них без труда можно узнать три последних параметра HandlerEx. Ничего удивительного – это они и есть.

Инициализация сервиса

В классе присутствует метод InitInstance. По его названию нетрудно догадаться, что в нем выполняются действия, необходимые для инициализации текущего экземпляра сервиса.

BOOL CDeviceService::InitInstance(DWORD   dwArgc, 
                                  LPTSTR* lpszArgv, 
                                  DWORD&  dwSpecificErr)
{
  TCHAR cdDrive[] = _T("\\\\.\\A:\\");
  DWORD drives = GetLogicalDrives();

  for( int i = 0; i < 27; i++ )
  {
    if(i == 26)
      return FALSE; // no cd-romif((drives % 2) && (DRIVE_CDROM == GetDriveType(cdDrive + 4)))
    {
      cdDrive[6] = _T('\0');
      break;
    }
    cdDrive[4]++;
    drives >>= 1;
  }

  DEV_BROADCAST_HANDLE nf = {0};
  nf.dbch_size = sizeof(nf);
  nf.dbch_devicetype = DBT_DEVTYP_HANDLE;
  nf.dbch_handle     = CreateFile(cdDrive, GENERIC_READ, FILE_SHARE_READ,
                                 NULL, OPEN_EXISTING, 0, NULL);

    if(INVALID_HANDLE_VALUE == nf.dbch_handle)
    return FALSE;

    m_hDevNotify = RegisterDeviceNotification( m_status.GetHandle(),
                                               (LPVOID)&nf,
                                               DEVICE_NOTIFY_SERVICE_HANDLE);
    if(!m_hDevNotify)
    {
        dwSpecificErr = GetLastError();
        CErrCodeMsg err(dwSpecificErr);
        MessageBox(NULL, err.GetString(), TEXT("DeviceService"),
                    MB_OK | MB_SERVICE_NOTIFICATION);
        return FALSE;
    }

    LogEvent(0, NULL);
    return TRUE;
}

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

ПРЕДУПРЕЖДЕНИЕ

Необходимо заметить, что неудача инициализации сервиса заставит код библиотеки перевести состояние сервиса в SERVICE_STOPPED, что вызовет генерацию системой сообщения об ошибке – неудачном старте сервиса.

Обработка нотификаций

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

DWORD CDeviceService::OnDeviceChange(DWORD& dwState, 
                                     DWORD& dwWin32Err, 
                                     DWORD& dwSpecificErr, 
                                     BOOL&  bHandled, 
                                     DWORD  dwEventType, 
                                     LPVOID lpEventData, 
                                     LPVOID lpContext)
{
    LogEvent(dwEventType, lpEventData);
    bHandled = TRUE;
    return NO_ERROR;
}

Наш сервис обязан выставить bHandled в TRUE, иначе библиотека вернет системе ERROR_CALL_NOT_IMPLEMENTED в качестве результата выполнения обработки. Со всеми вытекающими последствиями…

Замечание о значениях кодов возврата из обработчиков

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

Иерархия классов SFL

После рассмотрения конкретных примеров самое время перейти к принципам работы каркаса.

Архитектура SFL-приложения проста: существует единственный объект класса приложения, который содержит указатели на объекты классов сервисов. Каждый из объектов классов сервисов, в свою очередь, содержит два объекта-переходника – для ServiceMain() и Handler().

Классы приложения

Иерархия классов приложения проста до безобразия – в SFL имеется шаблонный класс CServiceAppT<>, от которого нужно унаследовать класс приложения. В задачу класса входит:

Для построения таблицы сервисов в метод CServiceAppT<>::Main() передается массив указателей классов сервисов, из которых и извлекается вся необходимая информация для указанной таблицы.

Классы сервиса

Иерархия классов сервиса несколько сложнее – в ней присутствует в качестве корневого класс CServiceRoot, от которого унаследован шаблонный класс CServiceBaseT<>. Все классы сервисов должны быть унаследованы от именно от класса CServiceBaseT<>.

Класс CServiceRoot выполняет все типичные действия, свойственные сервису – хранит и предоставляет информацию о статусе сервиса, его имени и зарегистрированных для сервиса кодах управления (наборе флагов SERVICE_ACCEPT_xxx). Он выполняет все действия по изменению статуса сервиса, а также выполняет т.н. “check point” для длительных операций, связанных с изменением статуса.

Переходники (о них сказано ниже) для ServiceMain и Handler(Ex) находятся также в классе CServiceRoot.

Базовый шаблонный класс CServiceBaseT<> вводит в действие механику шаблонов и занимается деталями работы по инициализации сервиса и подключению переходников, посредством которых выполняется трансформация статического вызова системы в вызов метода класса сервиса. Именно здесь находится фактически используемая реализация ServiceMain() и один из возможных вариантов RegisterHandler() (об альтернативном варианте будет упомянуто ниже).

Класс сервиса, будучи унаследованным от базового, предоставляет реализацию метода Handler() (либо HandlerEx(), если вы реализуете Windows2000-службу).

ПРИМЕЧАНИЕ

Легко видеть, что метод хэндлера попадает в класс в результате того, что в нем объявляется карта кодов управления – собственно, вся карта и является реализацией этого метода.

Класс переходника

Идея переходника далеко не нова, проста, эффективна… и не очень-то распространена, что странно. Ну да ладно, SFL использует ее, и этим все сказано.

Класс переходника CFnThunk() унаследован от структуры FNTHUNKSTRUCT. Переходник преобразует вызов stdcall-функции в вызов соответствующего статического метода класса, который в свою очередь производит вызов нужного метода экземпляра класса.

Аналогичный класс переходника для stdcall-функции содержится в ATL, но мне не хотелось устанавливать зависимость от какой бы то ни было библиотеки, пусть даже и поставляемой в составе среды разработки. Поэтому была сделана совершенно самостоятельная реализация.

Характерные точки входа каркаса

Для более полного понимания проследим процедуру запуска/останова нашего сервиса от запуска приложения до момента перехода сервиса в статус SERVICE_RUNNING и обратно. Рассмотрим, как и на каком этапе ваш код может внедриться в иерархию, предоставляемую каркасом.

Старт приложения

Как было сказано ранее, приложение SFL представляет собой консольный Win32-процесс. Следовательно, приложение должно стартовать с функции main(). Она (а точнее _tmain()) упрятана в карту сервисов приложения.

Экземпляр класса приложения создается статически, еще до вызова _tmain(). Сразу после старта _tmain() создается экземпляр класса сервиса.

Не выполняйте никаких действий по инициализации сервиса в конструкторе его класса – у вас будет возможность сделать все необходимое позже, в InitInstance(). Более того, вся проделанная в конструкторе работа может пропасть зря – если приложение не дойдет до запуска сервисов.

После этого происходит вызов метод PreMain() класса приложения.

ПРИМЕЧАНИЕ

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

PreMain( )

В метод PreMain() передаются параметры argc и argv, следовательно, здесь вы можете выполнить анализ аргументов командной строки и выполнить необходимые действия, связанные с ними. Грубо говоря, вы можете принять решение, как запускать приложение (известно, что многие утилиты умеют работать как сервис и как обычное приложение) и запускать ли вообще. Если вы решили запустить его как сервис, PreMain() обязан вернуть TRUE.

InitApp( )

Далее, если все же принято решение о запуске, происходит вызов метода Main() базового класса, в котором строится таблица сервисов и вызывается метод Run() базового класса (если вы не перекрыли его собственной реализацией), внутри которого немедленно происходит вызов метода InitApp().

ПРИМЕЧАНИЕ

Все, что было сказано о PreMain(), в равной степени относится и к InitApp() – если вы не поставляете собственной реализации, будет вызван «пустой» метод базового класса.

Если вы считаете, что инициализация приложения прошла успешно, для продолжения работы вы должны вернуть TRUE. Только в этом случае будет выполнен вызов системной функции StartServiceCtrlDispatcher(), в результате чего стартовавший процесс приложения приобретает новый статус с точки зрения системы – он становится процессом службы.

Старт сервиса

После упомянутого вызова диспетчера системы SCM производит старт сервисов из таблицы, переданной ему. Для этого он вызывает ServiceMain() каждого сервиса из этой таблицы. В результате этого управление будет передано соответствующему коду переходника, и в итоге будет вызван метод ServiceMain() базового класса сервиса. Внутри него происходит вызов метода RegisterHandle(), регистрирующего обработчик кодов управления, после чего будет вызван метод InitInstance() класса сервиса.

InitInstance( )

По названию легко понять, что данный метод производит инициализацию экземпляра класса. В качестве аргументов методу передаются параметры Argc и Argv, переданные системой, и ссылка на dwSpecific, в которую можно вернуть специфический код ошибки, если инициализация закончится неудачей. Если же все пройдет успешно, для продолжения работы нужно вернуть TRUE, после чего статус сервиса перейдет в SEVICE_RUNNING. Сервис стартовал!

ПРИМЕЧАНИЕ

Если вы не считаете нужным самостоятельно реализовывать InitInstance(), каркас вызовет «пустой» метод базового класса сервиса.

Остановка сервиса

Сразу разочарую – каркас не предусматривает вызов ExitInstance(), как можно было бы предположить на первый взгляд. Все действия по деинициализации сервиса необходимо выполнять в обработчике SERVICE_CONTROL_STOP и связанных с ним возможных “check point”-процедурах. Собственно, для этого они предназначены.

Остановка приложения

После остановки всех сервисов в приложении произойдет возврат из функции StartServiceCtrlDispatcher(),после чего сразу будет вызван метод ExitApp().

ExitApp( )

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

PostMain( )

После выхода из метода будет произведен выход из Run() и Main(), после чего произойдет вызов PostMain(). Этот метод тоже не возвращает ничего и также служит для возможной очистки того, что было «взято» в PreMain() (разумеется, если было «взято»).

PostMain() – последняя точка входа, которая доступна из каркаса перед остановкой приложения.

ПРЕДУПРЕЖДЕНИЕ

И последнее, что нужно помнить об остановке приложения – все перечисленные «остановочные» точки входа будут вызваны, если система действительно останавливает ваше приложение.

Обработка состояний ожидания

В случае длительных процедур перехода сервиса из одного состояния в другое вы обязаны известить об этом SCM переводом статуса сервиса в одно из состояний SERVICE_xxx_PENDING. При этом, чтобы SCM имел представление о «жизнеспособности» вашего сервиса, вы должны периодически выполнять процедуру подтверждения – т.н. check point. Каркас поддерживает эту функциональность, предоставляя метод CheckPoint(). Первый его параметр, dwWaitHint, представляет собой ориентировочный интервал времени, который должен пройти до следующей точки отметки-подтверждения, второй – dwCheckPoint – является индикатором: покуда он изменяется, сервис считается «живым». Вы можете самостоятельно от вызова к вызову изменять значение dwCheckPoint, а можете предоставить делать это каркасу: если метод вызван без второго параметра (или в нем передано значение -1, что то же самое), то метод будет выполнять инкремент dwCheckPoint автоматически. Типичный фрагмент кода, использующий метод CheckPoint():

        // определяем, с каким интервалом и сколько чекпоинтов будет отмечено
DWORD dwHint = 1000, dwMax = 20, dwRes, dwThreadId;
// стартуем длительную операцию
HANDLE hThread = ::CreateThread( NULL, 0, ThreadProc, pParams, 0, &dwThreadId );
// инициализируем счетчик чекпоинтов
CheckPoint( dwHint, 1 );
// ждем завершения операцииwhile( dwMax )
{
  dwMax--;
  dwRes = ::WaitForSingleObject( hThread, dwHint );
  if( WAIT_TIMER != dwRes )
    break;
  // автоматически продвигаем счетчик чекпоинтов
  CheckPoint( dwHint );
}
::CloseHandle( hThread );
// установка ‘non-pending’-статуса обнуляет счетчик чекпоинтов
SetServiceStatus( (WAIT_OBJECT_0 == dwRes) ? SERVICE_RUNNING : SERVICE_STOPPED );

О дополнительных возможностях

Имена сервисов

Имя сервиса может быть задано двумя способами – либо указанием идентификатора строки, находящейся в ресурсах приложения, либо явным заданием строки в коде приложения. В первом случае для объявления сервиса в таблице сервисов приложения используется макрос SFL_SERVICE_ENTRY(TService,resID). Во втором случае используется SFL_SERVICE_ENTRY_(TService,name).

ПРЕДУПРЕЖДЕНИЕ

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

Несколько в одном

Да, SFL позволяет вам без особых усилий разместить несколько сервисов в одном приложении. Для этого в карте сервисов необходимо добавить столько макросов SFL_SERVICE_ENTRYxxx, сколько экземпляров сервисов вы собираетесь зарегистрировать в системе. Причем, ваши сервисы могут быть как совершенно различными (использующими разные классы), так и абсолютно одинаковыми (использующими один и тот же класс, т.н. «клоны») – и все это в произвольной комбинации.

Немного о клонах

Необходимо уточнить некоторые технические подробности. Если вы используете сервисы-клоны и задаете имена через идентификатор ресурса, то никаких проблем не предвидится. В случае, когда вы используете клоны вместе с явным заданием строки, первый из клонов может быть (хотя это не обязательно) объявлен макросом SFL_SERVICE_ENTRY_(), однако все остальные клоны должны быть объявлены макросом SFL_SERVICE_ENTRY_CLONE(TService, cloneID, name). Думаю, нет необходимости разъяснять, почему у каждого клона должен быть задан уникальный cloneID и имя сервиса.

В любом случае, при использовании клонов необходимо перед включением заголовочного файла sflbase.h определить символ SFL_USE_CLONES.

ПРИМЕЧАНИЕ

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

Варианты конструирования экземпляров сервисов

«Какие еще варианты?» спросите вы. Есть варианты… Без дополнительных директив компиляции SFL использует по умолчанию статическое конструирование экземпляров сервисов. Подобный подход позволяет устранить утечки памяти – в этом случае код каркаса не содержит ни одного оператора new. И в большинстве случаев это срабатывает…

Вы уже догадались, что в бочке меда присутствует ложка дегтя. Но присутствует она лишь в узкоспецифичном случае – вы используете сервисы-клоны, для их именования используете явное задание строки в коде и при этом собираете проект шестой студией (VS 6.x). В подобной комбинации VC98-компилятор падает при компиляции.

В этом случае вам необходимо использовать вариант динамического конструирования – тем самым оператором new, присутствия которого мы старались избежать. Для этого перед включением заголовочного файла sflbase.h вам необходимо определить символ SFL_USE_DYNAMIC_CONSTRUCTION. В результате будут использованы альтернативные варианты макросов, участвующих в конструировании экземпляров сервисов и их уничтожении.

Во всех остальных случаях (VC7.x, VC8.x) статическое конструирование не вызывает никаких проблем.

Несколько слов об инсталляции сервисов

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

Более того, если вы используете стандартный (типизированный) набор настроек, вам не составит труда написать собственный класс-инсталлятор.

Однако для тестирования ваших сервисов вполне подойдет утилита SC.EXE (она входит в ресурс-кит любой Windows и DDK). Так для инсталляции MinSvc необходимо в командной строке выполнить следующую команду:

sc.exe create MinSvc binPath= <full_path_to_MinSvc.exe>
ПРЕДУПРЕЖДЕНИЕ

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

Аналогично, для удаления сервиса строка будет выглядеть следующим образом:

sc.exe delete MinSvc

Надеюсь, вы самостоятельно сможете познакомиться со всеми возможностями этой утилиты и оценить ее по достоинству.

Благодарности

Автор выражает благодарность Алексу Федотову и Виталию Брусенцеву за ценные советы и рецензирование кода SFL.

Warranties and Liabilities

Никаких, как вы наверняка уже догадались…


Эта статья опубликована в журнале RSDN Magazine #6-2004. Информацию о журнале можно найти здесь