Публикации Lazarus

Создание Windows сервисов средствами FPC и Lazarus

03.04.2009
Лагунов А.А.

1. Введение

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

В Linux и прочих клонах UNIX они называются демонами, в Windows это службы. Хотя название этого класса программ в windows и Unix-подобных системах различно — принцип работы их одинаков. Программа запускается в фоновом режиме и не требует, чтобы оператор совершал при этом какие-либо действия.

Компилятор FPC имеет в своём составе прекрасные библиотеки для облегчения создания таких программ. IDE Lazarus с помощью своих визуальных средств — большое подспорье в этом интересном деле.

Процесс создания в общем не зависит от операционной системы, полностью проявляя главный девиз Lazarus: «Пишем код один раз — компилируем везде».

2. Общая настройка Lazarus

Сначала подготовим Lazarus к работе — этот этап общий для любой операционной системы, где он запускается.

Для этого необходимо установить в Lazarus дополнительные компоненты для создания сервисов. То есть необходимо открыть пакет lazdaemon.lpk и установить его. Он находится в каталоге стандартных компонентов — $lazarus/components/daemon ($lazarus — это та папка, куда установлен IDE lazarus). После успешной компиляции и перезапуска IDE в меню создания нового проекта (меню «Проект/Создать новый проект») будет доступен новый тип проекта (Daemon (service) Application:

А в меню «Файл/Создать...» будет доступно создание новых объектов (Daemon Application, Daemon Module, Daemon Mapper):

Пункт меню «Файл/Создать...» необходим в том случае, если планируется в одном исполняемом модуле разместить несколько служб.

3. Создание сервиса

3.1. Что происходит при создании проекта?

При создании нового проекта будет открыто автоматически 2 невизуальных формы - класс TDaemon и TDaemonMapper.

TDaemon — непосредственно экземпляр сервиса. Именно этот объект реализует сам сервис (его мы видим в Windows в «Управление компьютерами/Сервисы»).

TDaemonMapper — это компонент, отвечающий за управление работой сервисов. Особенно актуален он в случае включения в один исполняемый модуль нескольких сервисов.

3.2. Описание свойств компоненты TDaemonMapper

Рассмотрим свойства и события, доступные у TDaemonMapper.

Уникальным свойством компоненты TDaemonMapper является DaemonDefs.

property DaemonDefs : TDaemonDefs;
Именно оно определяет, какие сервисы содержит исполняемый модуль, производит регистрацию сервисов в системе. Это свойство — коллекция элементов. Ниже, в п.3.3 подробно рассматривается элемент, который может содержаться в данной коллекции.

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

property OnCreate : TNotifyEvent;
Событие, которое возникает в момент запуска исполняемого модуля программы и создания экземпляра объекта TDaemonMapper. Полезно,например, когда необходимо при запуске считать значения некоторых переменных из конфигурационного файла, подготовить какие либо структуры данных и т.п.
property OnDestroy : TNotifyEvent;
Событие, которое возникает в момент завершения работы исполняемого модуля программы и уничтожения экземпляра объекта TDaemonMapper. Используется для «уборки» после работы программы: освобождение ресурсов, закрытие файлов и т.п.
property OnRun : TNotifyEvent;
Событие возникает при запуске на выполнение исполняемого файла с нестандартными параметрами запуска или вообще без параметров. Стандартными считаются 3 параметра командной строки: -i, -r, -u (подробнее параметры расписаны в Приложение А. Стандартные параметры командной строки)
property OnInstall : TNotifyEvent;
Событие возникает в момент регистрации сервисов в системе — запуск программы с ключом -i.
property OnUnInstall : TNotifyEvent;
Событие возникает в момент удаления регистрационной информации о сервисе из системы — запуск программы с ключом -u.

3.3. Более подробно класс TDaemonDef

Экземпляры данного класса хранятся в свойстве-коллекции DaemonDefs класса TDaemonMapper.

property DaemonClassName : String;
Свойство содержит имя типа регистрируемого сервиса — например TDaemon1. На данном этапе Lazarus не позволяет визуально выбрать значение — пока можно только вписать вручную. Но, я надеюсь, в ближайшее время будет реализован редактор.

property Name : String;
Наименование сервиса
property DisplayName : String;
Наименование сервиса — отображается в окне ОС просмотра списка зарегистрированных сервисов.
property RunArguments : String;
Список дополнительных аргументов командной строки, которые будут передаваться указанному сервису в момент старта сервиса.
property Options : TDaemonOptions;
Параметры работы сервиса — может ли сервис быть остановлен или приостановлен вручную, может ли он взаимодействовать с рабочим столом оператора (см. Приложение В. Параметры сервиса — TDaemonOption)
property Enabled : Boolean;
Признак доступности сервиса. Если это свойство включено, то в момент запуска исполняемого файла с ключом регистрации сервисов (-i), сервис будет зарегистрирован. В противном случае — сервис не регистрируется. Аналогичное поведение и при деинсталяции сервисаов. Этот параметр удобно менять в момент создания экземпляра TDaemonMapper из его соответствующего события OnCreate.
property WinBindings : TwinBindings;
Сложное свойство, отвечает за параметры регистрации сервиса в Windows системах:
  • property Dependencies : TDependencies;
    Список служб, от которых зависит данная служба.
  • property GroupName : String;
    Наименование группы, к которой относится служба.
  • property Password : String;
    Пароль учётной записи, от имени которой производится запуск данной службы (пустой при запуске от System).
  • property UserName : String;
    Наименование учётной записи, от имени которой производится запуск данной службы.
  • property StartType : TStartType;
    Тип запуска службы — автоматический или ручной. Допустимые значения приведены ниже (см. Приложение Б. Типы запуска служб).
  • property WaitHint : Integer;
    Временной интервал, который необходим службе для выполнения действий. Например, время необходимое для старта службы. По истечении этого времени будет сгенерированно сообщение о том, что служба не отвечает на внешние запросы. Если в качестве значения указать 0, то ожидание не прерывается.
  • property ServiceType : TServiceType;
    Определяется тип службы — либо просто программа, либо драйвер устройства или файловой системы. В данной статье рассматриваются только службы с типом stWin32.
property LogStatusReport : Boolean;
Писать в файл протокола факт изменения состояния сервиса (запуск / приостановка / остановка)
property OnCreateInstance : TNotifyEvent;
Это событие возникает в момент создания самого экземпляра сервиса.

3.4. Описание компоненты TDaemon

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

Ниже приведен перечень свойств этого компонента:

property Status;
Свойство содержит текущее состояние сервиса.
property OnStart : TDaemonOKEvent;
Событие возникает в момент начала выполнения сервиса.
property OnStop : TDaemonOKEvent;
Событие возникает в момент остановки сервиса.
property OnPause : TDaemonOKEvent;
Событие возникает в момент приостановки работы сервиса.
property OnContinue : TDaemonOKEvent;
Событие возникает в момент возобновления работы сервиса.
property OnShutDown : TDaemonEvent;
Событие возникает в момент выгрузки сервиса.
property OnExecute : TDaemonEvent;
Событие возникает в момент начала выполнения сервиса.
property BeforeInstall : TDaemonEvent;
property AfterInstall : TDaemonEvent;
property BeforeUnInstall : TDaemonEvent;
property AfterUnInstall : TDaemonEvent;
События, возникают до и после момента установки и деинсталяции сервиса соответственно.
property OnControlCode : TcustomControlCodeEvent;
Это событие возникает для обработки не стандартного сообщения от системы (не имеющего типового обработчика).

3.5. Пример кода

Для примера рассмотрим создание сервиса, который будет определять наличие файла в указанной папке по специальной маске, и если такой файл или файлы будут найдены — сохранит информацию об этом в базу данных FireBird.

Внешний вид окна разработки сервиса приведён на рисунке ниже:

Разместим в окне сервиса компоненты доступа к базе данных (UIBDataBase1, UIBQuery1, UIBTransaction1). Для периодического сканирования каталога, в котором могут находится и появляться в течение работы сервиса файлы, будем использовать таймер (Timer1).

Создадим обработчики на события сервиса:

  • OnStart — Старт сервиса. В момент старта сервиса укажем для объекта подключения к базе данных (UIBDataBase1) имя пользователя, пароль и строку подключения. Устанавливаем соединение с базой данных. Затем для таймера (Timer1) установим временной интервал сканирования каталога. Последней операцией — включим таймер в работу.
  • OnStop — Остановка сервиса. Выключаем таймер. Отсоединяемся от базы данных.
  • OnPause — Оператор запрашивает с помощью инструментов операционной системы приостановку работы сервиса. Мы это реализуем просто выключением таймера.
  • OnContinue — Работа сервиса продолжается. Таймер можно включить.

Теперь приступим к реализации основного рабочего кода сервиса — сканирование каталога и сохранение результата сканирования в базе данных. Запуск процесса сканирования будет происходит по сигналу таймера — поэтому пишем его обработчик. Полный текст всего примера есть в прикреплённом архиве — а тут приведу только часть кода:

procedure TTestDaemon.Timer1Timer(Sender: TObject);
begin
  try
    DoScanFolder;
    if UIBTransaction1.InTransaction then
    //Если файлы были обнаружены - то транзакция будет запущена -
    //комитем данные в базу
    UIBTransaction1.Commit;
  except
    on E:Exception do begin
      //Если файлы были обнаружены - то транзакция будет запущена -
      //откатим всё
      if UIBTransaction1.InTransaction then
      UIBTransaction1.RollBack;
      //Чтобы наш сервис продолжал работать -
      //сообщение об ошибке запишем в протокол
      //А саму ошибку - подавим
      Application.Logger.Log(E.Message);
    end;
  end;
end;

procedure TTestDaemon.DoScanFolder;
var
  R:TSearchRec;
  Code:integer;
begin
  Code:=FindFirst(FScanFolder+FScanMask, faAnyFile, R);
  try
    while Code=0 do begin
      if (R.Attr and faDirectory) = 0 then begin
        DoSaveFileNameToBD(R.Name);
        DeleteFile(FScanFolder+R.Name);
      end;
      FindNext(R);
    end;
  finally
    FindClose(R);
  end;
end;

procedure TTestDaemon.DoSaveFileNameToBD(AFileName: string);
begin
  if not UIBTransaction1.InTransaction then
  UIBTransaction1.StartTransaction;
  UIBQuery1.Params.ByNameAsString['sys_log_name']:=AFileName;
  UIBQuery1.ExecSQL;
end;

Таким образом, функционал всего сервиса обеспечивается 3-мя методами.

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

Если же при выполнении кода была ошибка — то она будет подавлена. Работа сервиса прервана не будет, но факт ошибки будет записан в файле протокола.

Метод DoScanFolder выполнит сканирование указанного каталога. Если при этом будут обнаружены файлы удовлетворяющие условию, их названия будут записаны в БД программы с помощью процедуры DoSaveFileNameToBD, а затем, после успешной записи — удалены.

Метод DoSaveFileNameToBD просто проверяет была ли начата транзакция в данной итерации, если нет — то открывает её и записывает данные в базу.

3.6. Замечания из личного опыта

  • Использование таймера таит странную на первый взгляд ошибку. Если просто пытаться использовать таймер — то ничего не выйдет. Это обусловлено реализацией кода компонента таймера в Lazarus. Код таймера находится в библиотеках, которые относятся к визуальным виджетам — просто в разных операционных системах механизм таймеров реализован по разному. Из-за этого без подключения модуля Interfaces тут не обойтись. Первый раз я долго голову ломал — что не так?
  • Объект Application служб реализует систему протоколирования работы. Эта удобная вещь таит не очень явную трудность для случая, если в одном исполняемом файле находится несколько сервисов. В случае когда Application.Logger пишет протокол в системный протокол — то всё будет нормально. Но если запись протокола идёт в файл (а именно так по умолчанию всё работает) — то возникнет ошибка при попытке запуска второго и последующих сервисов. Всё дело в том, что Application.Logger открывает файл протокола на запись в монопольном доступе и имя, если не задано явно, будет равно имени исполняемого модуля с расширением .log. В этом случае я просто в момент создания экземпляра TDaemonMapper определяю какой именно сервис запускается и Application.Logger.FileName присваиваю необходимое имя файла протокола для этого сервиса. Получается, что на каждый сервис у меня ведётся отдельный протокол.

4. Специфика для Windows

Рассмотрим создание сервиса с помощью FPC и Lazarus для использования его в OS Windows. Само создание сервисов не отличается от описанного выше.

Специфика начинается в момент установки. Фактически установку созданного сервиса можно проводить из любого каталога, но если сервис будет запускаться от специальной учётной записи, необходимо отследить возможность этой учётной записи работать с папкой, где расположены файлы сервиса (исполняемые и вспомогательные). Также для этой учётной записи должна быть доступна на чтение и удаление папка, в которой сервис будет производить поиск файлов по маске.

Весь процесс установки сервиса сводится к запуску исполняемого файла с ключом командной строки -i. После выполнения этой команды сервис зарегистрирует себя в реестре Windows. Можно открыть окно управления компьютером и запустить сервис.

Для деинсталляции сервиса необходимо просто запустить сервис с ключом -u.

По умолчанию протоколирование ведётся в системный каталог — %SYSTEM_ROOT%system32<Имя программы>.log.

5. Заключение

Данная статья написана с использованием Lazarus версии 0.9.27 (18965) и FPC версии 2.3.1. Но эти же примеры должны заработать и на предыдущих версиях (не очень старых). Лично для меня эта статья была инициирована необходимостью написания сервиса рассылки СМС сообщений. Так что всё проверено опытным путём.

6. Приложение

6.1. Приложение А. Стандартные параметры командной строки

Параметр Длинная форма параметра Описание
-r --run Запуск сервисов на выполнение
-i --install Регистрация сервисов в системе
-u --uninstall Удаление регистрационной информации о сервисе

6.2. Приложение Б. Типы запуска служб

Тип запуска Описание
stBoot Используется только для драйверов устройств. Для обычных служб эквивалентно stAuto.
stSystem Используется только для драйверов устройств. Для обычных служб эквивалентно stAuto.
stAuto Служба будет запущена в момент старта операционной системы.
stManual Запуск данной службы производится вручную либо оператором, либо по требованию другой службы.
stDisabled Запуск данной службы запрещён.

6.3. Приложение В. Параметры сервиса — TDaemonOption

Параметр Описание
doAllowStop Разрешена ручная остановка сервиса
doAllowPause Разрешено приостанавливать сервис
doInteractive Сервису разрешено взаимодействовать с рабочим столом оператора
Актуальные версии
FPC3.2.2release
Lazarus3.2release
MSE5.10.0release
fpGUI1.4.1release
links