|
Пара нюансов при создании службы (Windows Service) и не только
Пишу службу (Windows Service) на Delphi через VCL. В том же EXE-шнике делаю конфигуратор с оконным интерфейсом.
Получается так:- При запуске с опцией /install служба устанавливается (встроенная фича TServiceApplication).
- При запуске с опцией /uninstall служба удаляется (встроенная фича TServiceApplication).
- При запуске с опцией /config запускается конфигуратор.
- Запуск службы происходит по специальной схеме (через тот же EXE-шник). Это внутренняя кухня модуля [Vcl.]SvcMgr.
Если вырезать интимные подробности, DPR-файл выглядит примерно так:
| Delphi | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| program ServiceTest;
uses
System.SysUtils,
Vcl.Forms,
Vcl.SvcMgr,
MyService in 'MyService.pas',
ConfigForm in 'ConfigForm.pas';
{$R *.RES}
begin
if FindCmdLineSwitch('CONFIG') then
begin
// Запуск конфигуратора
Vcl.Forms.Application.Initialize;
Vcl.Forms.Application.MainFormOnTaskbar := True;
Vcl.Forms.Application.CreateForm(TFormConfig, FormConfig);
Vcl.Forms.Application.Run;
end
else
begin
// Запуск сервиса или установщика сервиса
if not Vcl.SvcMgr.Application.DelayInitialize or Vcl.SvcMgr.Application.Installing then
Vcl.SvcMgr.Application.Initialize;
Vcl.SvcMgr.Application.CreateForm(TMyService, MyService);
Vcl.SvcMgr.Application.Run;
end;
end. |
|
Проблема №1
Оказывается, в VCL нет встроенных средств для запуска службы. Службу можно установить, а запускаться она будет только после перезагрузки компьютера. Довольно странно, но факт.
Решить эту проблему через WinAPI (если класс нашей службы называется TMyService):
| Delphi | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // Запуск (Start = True) или остановка (Start = False) службы (метод наследника TService).
procedure TMyService.StartStopService(Start: Boolean);
var
SvcMgr, Svc: SC_HANDLE;
P: PChar;
SS: TServiceStatus;
begin
// Открываем диспетчер управления службами (SCM)
SvcMgr := OpenSCManager(nil, nil, STANDARD_RIGHTS_REQUIRED);
if SvcMgr <> 0 then
begin
// Открываем службу с именем Name (это свойство объекта класса-наследника TService)
Svc := OpenService(SvcMgr, PChar(Name), SERVICE_ALL_ACCESS);
if Svc <> 0 then
begin
// Запуск или остановка?
if Start then
begin
P := nil;
StartService(Svc, 0, P); // запускаем службу
end
else
begin
FillChar(SS, SizeOf(SS), 0);
ControlService(Svc, SERVICE_CONTROL_STOP, SS); // останавливаем службу
end;
CloseServiceHandle(Svc);
end;
CloseServiceHandle(SvcMgr);
end;
end; |
|
Если нам нужно запускать службу сразу после её установки, делаем так:
| Delphi | 1
2
3
4
| procedure TMyService.ServiceAfterInstall(Sender: TService);
begin
StartStopService(True);
end; |
|
Представьте себе, остановки при удалении (деинсталляции) службы тоже не происходит, поэтому делаем ещё и так:
| Delphi | 1
2
3
4
| procedure TMyService.ServiceBeforeUninstall(Sender: TService);
begin
StartStopService(False);
end; |
|
Проблема №2
Каким же образом сообщить службе об изменении конфигурации (чтобы она прочитала её заново – из реестра или с диска)? Проще всего – создать глобальное именованное событие (CreateEvent) и через него сигнализировать (SetEvent) из конфигуратора службе (которая будет ждать его, либо периодически проверять, через WaitForSingleObject). Но тут нас поджидает засада: при попытке открыть из конфигуратора событие (OpenEvent), созданное службой, мы получим ошибку запрета доступа (GetLastError = ERROR_ACCESS_DENIED).
Это происходит из-за того, что объект (событие) создан "системой" (службы запускаются от имени "системы"), а открывается администратором (если конфигуратор находится в том же EXE – установить/удалить службу может только администратор) или даже обычным пользователем (если конфигуратор находится в другом EXE-шнике). Тоже самое будет при попытке открыть обычным пользователем объект, созданный администратором.
Что же делать? Сначала я решил пойти по странному пути: создать объект в конфигураторе, а в службе периодически пытаться открыть его, и после успешного открытия начать проверять состояние ("система" может открыть объект, созданный администратором или обычным пользователем). Но потом эта идея мне показалась костыльной, и я решил разобраться, как создать объект в службе, не защищённый от доступа админов и обычных юзеров.
Вот так это делается (в службе):
| Delphi | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Создать событие с именем Name в глобальном пространстве имён, доступный любым процессам.
function CreateUnprotectedEvent(const Name: String): THandle;
var
SA: TSecurityAttributes;
SD: TSecurityDescriptor;
begin
Result := 0;
// Создаем дескриптор безопасности
if InitializeSecurityDescriptor(@SD, SECURITY_DESCRIPTOR_REVISION) and
// DACL не установлен, объект не защищён
SetSecurityDescriptorDacl(@SD, True, nil, False) then
begin
// Настраиваем атрибуты безопасности, передавая указатель на дескриптор безопасности
SA.nLength := SizeOf(SA);
SA.lpSecurityDescriptor := @SD;
SA.bInheritHandle := False;
// Создаём событие
Result := CreateEvent(@SA, True, False, Pointer('Global\' + Name));
end;
end; |
|
(разумеется, при необходимости создания нескольких событий нет смысла создавать и настраивать несколько дескрипторов и атрибутов безопасности, можно использовать одни и те же; при завершении работы ничего, кроме события, закрывать или освобождать не нужно)
Как вы понимаете, это всё можно делать не только при создании служб и не только для создания событий. Если вам не нравится взаимодействие через события, и вы хотите иметь возможность передавать какие-то данные, можете создать, к примеру, разделяемую память. В общем, простор для творчества есть
|